[feature] check-box
geo shape (#1330)
This PR adds a `check-box` geo shape. ![Kapture 2023-05-08 at 15 31 49](https://user-images.githubusercontent.com/23072548/236853749-99ba786f-73a4-4b65-86ca-f2cdac61a903.gif) It also improves some logic around the `onClick` shape util handler and some surprisingly related fixes to point hit testing. ### Test Plan 1. Create a geo shape 2. Set it as a checkbox style 3. *easter egg* double click while holding alt to toggle between check-box and rectangle - [x] Unit Tests ### Release Note - Adds checkbox geo shape.
This commit is contained in:
parent
a8910e5491
commit
bb96852b9d
32 changed files with 429 additions and 152 deletions
5
assets/icons/icon/geo-check-box.svg
Normal file
5
assets/icons/icon/geo-check-box.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 3H25C26.1046 3 27 3.89543 27 5V25C27 26.1046 26.1046 27 25 27H5C3.89543 27 3 26.1046 3 25V5C3 3.89543 3.89543 3 5 3Z" stroke="black" stroke-width="2"/>
|
||||
<path d="M8 15L13 22" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M22 8L13 22" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 425 B |
|
@ -142,7 +142,8 @@
|
|||
"geo-style.star": "Star",
|
||||
"geo-style.trapezoid": "Trapezoid",
|
||||
"geo-style.triangle": "Triangle",
|
||||
"geo-style.x-box": "X Box",
|
||||
"geo-style.x-box": "X box",
|
||||
"geo-style.check-box": "Check box",
|
||||
"arrowheadStart-style.none": "None",
|
||||
"arrowheadStart-style.arrow": "Arrow",
|
||||
"arrowheadStart-style.bar": "Bar",
|
||||
|
@ -185,6 +186,7 @@
|
|||
"tool.trapezoid": "Trapezoid",
|
||||
"tool.triangle": "Triangle",
|
||||
"tool.x-box": "X box",
|
||||
"tool.check-box": "Check box",
|
||||
"tool.asset": "Asset",
|
||||
"tool.frame": "Frame",
|
||||
"tool.note": "Note",
|
||||
|
|
|
@ -87,6 +87,7 @@ export function getBundlerAssetUrls(opts?: AssetUrlOptions): {
|
|||
readonly 'geo-arrow-left': string;
|
||||
readonly 'geo-arrow-right': string;
|
||||
readonly 'geo-arrow-up': string;
|
||||
readonly 'geo-check-box': string;
|
||||
readonly 'geo-diamond': string;
|
||||
readonly 'geo-ellipse': string;
|
||||
readonly 'geo-hexagon': string;
|
||||
|
|
|
@ -97,6 +97,7 @@ import iconsGeoArrowDown from '../icons/icon/geo-arrow-down.svg'
|
|||
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 iconsGeoDiamond from '../icons/icon/geo-diamond.svg'
|
||||
import iconsGeoEllipse from '../icons/icon/geo-ellipse.svg'
|
||||
import iconsGeoHexagon from '../icons/icon/geo-hexagon.svg'
|
||||
|
@ -299,6 +300,7 @@ export function getBundlerAssetUrls(opts?: AssetUrlOptions) {
|
|||
'geo-arrow-left': formatAssetUrl(iconsGeoArrowLeft, opts),
|
||||
'geo-arrow-right': formatAssetUrl(iconsGeoArrowRight, opts),
|
||||
'geo-arrow-up': formatAssetUrl(iconsGeoArrowUp, opts),
|
||||
'geo-check-box': formatAssetUrl(iconsGeoCheckBox, opts),
|
||||
'geo-diamond': formatAssetUrl(iconsGeoDiamond, opts),
|
||||
'geo-ellipse': formatAssetUrl(iconsGeoEllipse, opts),
|
||||
'geo-hexagon': formatAssetUrl(iconsGeoHexagon, opts),
|
||||
|
|
|
@ -275,6 +275,10 @@ export function getBundlerAssetUrls(opts?: AssetUrlOptions) {
|
|||
new URL('../icons/icon/geo-arrow-up.svg', import.meta.url).href,
|
||||
opts
|
||||
),
|
||||
'geo-check-box': formatAssetUrl(
|
||||
new URL('../icons/icon/geo-check-box.svg', import.meta.url).href,
|
||||
opts
|
||||
),
|
||||
'geo-diamond': formatAssetUrl(
|
||||
new URL('../icons/icon/geo-diamond.svg', import.meta.url).href,
|
||||
opts
|
||||
|
|
|
@ -2049,12 +2049,14 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
// (undocumented)
|
||||
hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean;
|
||||
// (undocumented)
|
||||
hitTestPoint(shape: TLGeoShape, point: VecLike): boolean;
|
||||
// (undocumented)
|
||||
indicator(shape: TLGeoShape): JSX.Element;
|
||||
// (undocumented)
|
||||
onBeforeCreate: (shape: TLGeoShape) => {
|
||||
props: {
|
||||
growY: number;
|
||||
geo: "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "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" | "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";
|
||||
|
@ -2082,7 +2084,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
onBeforeUpdate: (prev: TLGeoShape, next: TLGeoShape) => {
|
||||
props: {
|
||||
growY: number;
|
||||
geo: "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "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" | "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";
|
||||
|
@ -2107,6 +2109,34 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
typeName: "shape";
|
||||
} | undefined;
|
||||
// (undocumented)
|
||||
onDoubleClick: (shape: TLGeoShape) => {
|
||||
props: {
|
||||
geo: "check-box";
|
||||
};
|
||||
type: "geo";
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
index: string;
|
||||
parentId: TLParentId;
|
||||
isLocked: boolean;
|
||||
id: ID<TLGeoShape>;
|
||||
typeName: "shape";
|
||||
} | {
|
||||
props: {
|
||||
geo: "rectangle";
|
||||
};
|
||||
type: "geo";
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
index: string;
|
||||
parentId: TLParentId;
|
||||
isLocked: boolean;
|
||||
id: ID<TLGeoShape>;
|
||||
typeName: "shape";
|
||||
} | undefined;
|
||||
// (undocumented)
|
||||
onEditEnd: OnEditEndHandler<TLGeoShape>;
|
||||
// (undocumented)
|
||||
onResize: OnResizeHandler<TLGeoShape>;
|
||||
|
|
|
@ -2277,12 +2277,13 @@ export class App extends EventEmitter {
|
|||
*/
|
||||
getShapesAtPoint(point: VecLike): TLShape[] {
|
||||
return this.shapesArray.filter((shape) => {
|
||||
// Check the page mask too
|
||||
const pageMask = this._pageMaskCache.get(shape.id)
|
||||
if (pageMask) {
|
||||
const hasHit = pointInPolygon(point, pageMask)
|
||||
if (!hasHit) return false
|
||||
return pointInPolygon(point, pageMask)
|
||||
}
|
||||
|
||||
// Otherwise, use the shape's own hit test method
|
||||
return this.getShapeUtil(shape).hitTestPoint(shape, this.getPointInShapeSpace(shape, point))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ describe('arrowBindingsIndex', () => {
|
|||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
fill: 'solid',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -38,6 +39,7 @@ describe('arrowBindingsIndex', () => {
|
|||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
fill: 'solid',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
linesIntersect,
|
||||
longAngleDist,
|
||||
Matrix2d,
|
||||
pointInPolygon,
|
||||
shortAngleDist,
|
||||
toDomPrecision,
|
||||
Vec2d,
|
||||
|
@ -256,77 +257,101 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
const pageTransform = this.app.getPageTransformById(next.id)!
|
||||
const pointInPageSpace = Matrix2d.applyToPoint(pageTransform, handle)
|
||||
|
||||
const target = this.app.inputs.ctrlKey
|
||||
? undefined
|
||||
: last(
|
||||
this.app.getShapesAtPoint(pointInPageSpace).filter((hitShape) => {
|
||||
if (hitShape.id === shape.id) return
|
||||
const util = this.app.getShapeUtil(hitShape)
|
||||
|
||||
return (
|
||||
util.canBind(next) &&
|
||||
util.hitTestPoint(
|
||||
hitShape,
|
||||
this.app.getPointInShapeSpace(hitShape, pointInPageSpace)
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
if (target) {
|
||||
const targetBounds = this.app.getBounds(target)
|
||||
const pointInTargetSpace = this.app.getPointInShapeSpace(target, pointInPageSpace)
|
||||
|
||||
const prevHandle = next.props[handle.id]
|
||||
|
||||
const startBindingId =
|
||||
shape.props.start.type === 'binding' && shape.props.start.boundShapeId
|
||||
const endBindingId = shape.props.end.type === 'binding' && shape.props.end.boundShapeId
|
||||
|
||||
let precise =
|
||||
// If externally precise, then always precise
|
||||
isPrecise ||
|
||||
// If the other handle is bound to the same shape, then precise
|
||||
((startBindingId || endBindingId) && startBindingId === endBindingId) ||
|
||||
// If the other shape is not closed, then precise
|
||||
!this.app.getShapeUtil(target).isClosed(next)
|
||||
|
||||
if (
|
||||
// If we're switching to a new bound shape, then precise only if moving slowly
|
||||
prevHandle.type === 'point' ||
|
||||
(prevHandle.type === 'binding' && target.id !== prevHandle.boundShapeId)
|
||||
) {
|
||||
precise = this.app.inputs.pointerVelocity.len() < 0.5
|
||||
}
|
||||
|
||||
if (precise) {
|
||||
// Funky math but we want the snap distance to be 4 at the minimum and either
|
||||
// 16 or 15% of the smaller dimension of the target shape, whichever is smaller
|
||||
precise =
|
||||
Vec2d.Dist(pointInTargetSpace, targetBounds.center) >
|
||||
Math.max(4, Math.min(Math.min(targetBounds.width, targetBounds.height) * 0.15, 16)) /
|
||||
this.app.zoomLevel
|
||||
}
|
||||
|
||||
next.props[handle.id] = {
|
||||
type: 'binding',
|
||||
boundShapeId: target.id,
|
||||
normalizedAnchor: precise
|
||||
? {
|
||||
x: (pointInTargetSpace.x - targetBounds.minX) / targetBounds.width,
|
||||
y: (pointInTargetSpace.y - targetBounds.minY) / targetBounds.height,
|
||||
}
|
||||
: { x: 0.5, y: 0.5 },
|
||||
isExact: this.app.inputs.altKey,
|
||||
}
|
||||
} else {
|
||||
if (this.app.inputs.ctrlKey) {
|
||||
next.props[handle.id] = {
|
||||
type: 'point',
|
||||
x: handle.x,
|
||||
y: handle.y,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const target = last(
|
||||
this.app.sortedShapesArray.filter((hitShape) => {
|
||||
if (hitShape.id === shape.id) {
|
||||
// We're testing against the arrow
|
||||
return
|
||||
}
|
||||
|
||||
const util = this.app.getShapeUtil(hitShape)
|
||||
if (!util.canBind(hitShape)) {
|
||||
// The shape can't be bound to
|
||||
return
|
||||
}
|
||||
|
||||
// Check the page mask
|
||||
const pageMask = this.app.getPageMaskById(hitShape.id)
|
||||
if (pageMask) {
|
||||
if (!pointInPolygon(pointInPageSpace, pageMask)) return
|
||||
}
|
||||
|
||||
const pointInTargetSpace = this.app.getPointInShapeSpace(hitShape, pointInPageSpace)
|
||||
|
||||
if (util.isClosed(hitShape)) {
|
||||
// Test the polygon
|
||||
return pointInPolygon(pointInTargetSpace, util.outline(hitShape))
|
||||
}
|
||||
|
||||
// Test the point using the shape's idea of what a hit is
|
||||
return util.hitTestPoint(hitShape, pointInTargetSpace)
|
||||
})
|
||||
)
|
||||
|
||||
if (target) {
|
||||
const targetBounds = this.app.getBounds(target)
|
||||
const pointInTargetSpace = this.app.getPointInShapeSpace(target, pointInPageSpace)
|
||||
|
||||
const prevHandle = next.props[handle.id]
|
||||
|
||||
const startBindingId =
|
||||
shape.props.start.type === 'binding' && shape.props.start.boundShapeId
|
||||
const endBindingId = shape.props.end.type === 'binding' && shape.props.end.boundShapeId
|
||||
|
||||
let precise =
|
||||
// If externally precise, then always precise
|
||||
isPrecise ||
|
||||
// If the other handle is bound to the same shape, then precise
|
||||
((startBindingId || endBindingId) && startBindingId === endBindingId) ||
|
||||
// If the other shape is not closed, then precise
|
||||
!this.app.getShapeUtil(target).isClosed(next)
|
||||
|
||||
if (
|
||||
// If we're switching to a new bound shape, then precise only if moving slowly
|
||||
prevHandle.type === 'point' ||
|
||||
(prevHandle.type === 'binding' && target.id !== prevHandle.boundShapeId)
|
||||
) {
|
||||
precise = this.app.inputs.pointerVelocity.len() < 0.5
|
||||
}
|
||||
|
||||
if (precise) {
|
||||
// Funky math but we want the snap distance to be 4 at the minimum and either
|
||||
// 16 or 15% of the smaller dimension of the target shape, whichever is smaller
|
||||
precise =
|
||||
Vec2d.Dist(pointInTargetSpace, targetBounds.center) >
|
||||
Math.max(
|
||||
4,
|
||||
Math.min(Math.min(targetBounds.width, targetBounds.height) * 0.15, 16)
|
||||
) /
|
||||
this.app.zoomLevel
|
||||
}
|
||||
|
||||
next.props[handle.id] = {
|
||||
type: 'binding',
|
||||
boundShapeId: target.id,
|
||||
normalizedAnchor: precise
|
||||
? {
|
||||
x: (pointInTargetSpace.x - targetBounds.minX) / targetBounds.width,
|
||||
y: (pointInTargetSpace.y - targetBounds.minY) / targetBounds.height,
|
||||
}
|
||||
: { x: 0.5, y: 0.5 },
|
||||
isExact: this.app.inputs.altKey,
|
||||
}
|
||||
} else {
|
||||
next.props[handle.id] = {
|
||||
type: 'point',
|
||||
x: handle.x,
|
||||
y: handle.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -504,12 +529,14 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
|
||||
hitTestPoint(shape: TLArrowShape, point: VecLike): boolean {
|
||||
const outline = this.outline(shape)
|
||||
const zoomLevel = this.app.zoomLevel
|
||||
const offsetDist = this.app.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
|
||||
for (let i = 0; i < outline.length - 1; i++) {
|
||||
const C = outline[i]
|
||||
const D = outline[i + 1]
|
||||
|
||||
if (Vec2d.DistanceToLineSegment(C, D, point) < 4) return true
|
||||
if (Vec2d.DistanceToLineSegment(C, D, point) < offsetDist) return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
|
|
@ -7,11 +7,18 @@ import {
|
|||
linesIntersect,
|
||||
PI,
|
||||
PI2,
|
||||
pointInPolygon,
|
||||
TAU,
|
||||
Vec2d,
|
||||
VecLike,
|
||||
} from '@tldraw/primitives'
|
||||
import { geoShapeMigrations, geoShapeTypeValidator, TLDashType, TLGeoShape } from '@tldraw/tlschema'
|
||||
import {
|
||||
geoShapeMigrations,
|
||||
geoShapeTypeValidator,
|
||||
TLDashType,
|
||||
TLGeoShape,
|
||||
TLGeoShapeProps,
|
||||
} from '@tldraw/tlschema'
|
||||
import { SVGContainer } from '../../../components/SVGContainer'
|
||||
import { defineShape } from '../../../config/TLShapeDefinition'
|
||||
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
||||
|
@ -67,21 +74,51 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean {
|
||||
const outline = this.outline(shape)
|
||||
|
||||
// Check the outline
|
||||
for (let i = 0; i < outline.length; i++) {
|
||||
const C = outline[i]
|
||||
const D = outline[(i + 1) % outline.length]
|
||||
if (linesIntersect(A, B, C, D)) return true
|
||||
}
|
||||
|
||||
if (shape.props.geo === 'x-box') {
|
||||
const { w, h } = shape.props
|
||||
if (linesIntersect(A, B, new Vec2d(0, 0), new Vec2d(w, h))) return true
|
||||
if (linesIntersect(A, B, new Vec2d(0, h), new Vec2d(w, 0))) return true
|
||||
// Also check lines, if any
|
||||
const lines = getLines(shape.props, 0)
|
||||
if (lines !== undefined) {
|
||||
for (const [C, D] of lines) {
|
||||
if (linesIntersect(A, B, C, D)) return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
hitTestPoint(shape: TLGeoShape, point: VecLike): boolean {
|
||||
const outline = this.outline(shape)
|
||||
|
||||
if (shape.props.fill === 'none') {
|
||||
const zoomLevel = this.app.zoomLevel
|
||||
const offsetDist = this.app.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
// Check the outline
|
||||
for (let i = 0; i < outline.length; i++) {
|
||||
const C = outline[i]
|
||||
const D = outline[(i + 1) % outline.length]
|
||||
if (Vec2d.DistanceToLineSegment(C, D, point) < offsetDist) return true
|
||||
}
|
||||
|
||||
// Also check lines, if any
|
||||
const lines = getLines(shape.props, 1)
|
||||
if (lines !== undefined) {
|
||||
for (const [C, D] of lines) {
|
||||
if (Vec2d.DistanceToLineSegment(C, D, point) < offsetDist) return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return pointInPolygon(point, outline)
|
||||
}
|
||||
|
||||
getBounds(shape: TLGeoShape) {
|
||||
return new Box2d(0, 0, shape.props.w, shape.props.h + shape.props.growY)
|
||||
}
|
||||
|
@ -272,6 +309,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
new Vec2d(ox, h - oy),
|
||||
]
|
||||
}
|
||||
case 'check-box':
|
||||
case 'x-box':
|
||||
case 'rectangle': {
|
||||
return [new Vec2d(0, 0), new Vec2d(w, 0), new Vec2d(w, h), new Vec2d(0, h)]
|
||||
|
@ -361,8 +399,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
}
|
||||
default: {
|
||||
const outline = this.outline(shape)
|
||||
const lines =
|
||||
shape.props.geo === 'x-box' ? getXBoxLines(w, h, strokeWidth, props.dash) : undefined
|
||||
const lines = getLines(shape.props, strokeWidth)
|
||||
|
||||
if (dash === 'solid' || (dash === 'draw' && forceSolid)) {
|
||||
return (
|
||||
|
@ -452,9 +489,9 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
path = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
|
||||
}
|
||||
|
||||
if (shape.props.geo === 'x-box') {
|
||||
const lines = getXBoxLines(w, h, strokeWidth, props.dash)
|
||||
const lines = getLines(shape.props, strokeWidth)
|
||||
|
||||
if (lines) {
|
||||
for (const [A, B] of lines) {
|
||||
path += `M${A.x},${A.y}L${B.x},${B.y}`
|
||||
}
|
||||
|
@ -555,10 +592,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
}
|
||||
default: {
|
||||
const outline = this.outline(shape)
|
||||
const lines =
|
||||
shape.props.geo === 'x-box'
|
||||
? getXBoxLines(shape.props.w, shape.props.h, strokeWidth, props.dash)
|
||||
: undefined
|
||||
const lines = getLines(shape.props, strokeWidth)
|
||||
|
||||
switch (props.dash) {
|
||||
case 'draw':
|
||||
|
@ -857,6 +891,33 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClick = (shape: TLGeoShape) => {
|
||||
// Little easter egg: double-clicking a rectangle / checkbox while
|
||||
// holding alt will toggle between check-box and rectangle
|
||||
if (this.app.inputs.altKey) {
|
||||
switch (shape.props.geo) {
|
||||
case 'rectangle': {
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
geo: 'check-box' as const,
|
||||
},
|
||||
}
|
||||
}
|
||||
case 'check-box': {
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
geo: 'rectangle' as const,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function getLabelSize(app: App, shape: TLGeoShape) {
|
||||
|
@ -907,6 +968,20 @@ function getLabelSize(app: App, shape: TLGeoShape) {
|
|||
}
|
||||
}
|
||||
|
||||
function getLines(props: TLGeoShapeProps, sw: number) {
|
||||
switch (props.geo) {
|
||||
case 'x-box': {
|
||||
return getXBoxLines(props.w, props.h, sw, props.dash)
|
||||
}
|
||||
case 'check-box': {
|
||||
return getCheckBoxLines(props.w, props.h)
|
||||
}
|
||||
default: {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getXBoxLines(w: number, h: number, sw: number, dash: TLDashType) {
|
||||
const inset = dash === 'draw' ? 0.62 : 0
|
||||
|
||||
|
@ -925,6 +1000,16 @@ function getXBoxLines(w: number, h: number, sw: number, dash: TLDashType) {
|
|||
]
|
||||
}
|
||||
|
||||
function getCheckBoxLines(w: number, h: number) {
|
||||
const size = Math.min(w, h) * 0.82
|
||||
const ox = (w - size) / 2
|
||||
const oy = (h - size) / 2
|
||||
return [
|
||||
[new Vec2d(ox + size * 0.25, oy + size * 0.52), new Vec2d(ox + size * 0.45, oy + size * 0.82)],
|
||||
[new Vec2d(ox + size * 0.45, oy + size * 0.82), new Vec2d(ox + size * 0.82, oy + size * 0.22)],
|
||||
]
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const TLGeoShapeDef = defineShape<TLGeoShape, TLGeoUtil>({
|
||||
type: 'geo',
|
||||
|
|
|
@ -173,7 +173,9 @@ export class TLLineUtil extends TLShapeUtil<TLLineShape> {
|
|||
}
|
||||
|
||||
hitTestPoint(shape: TLLineShape, point: Vec2d): boolean {
|
||||
return pointNearToPolyline(point, this.outline(shape))
|
||||
const zoomLevel = this.app.zoomLevel
|
||||
const offsetDist = this.app.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
return pointNearToPolyline(point, this.outline(shape), offsetDist)
|
||||
}
|
||||
|
||||
hitTestLineSegment(shape: TLLineShape, A: VecLike, B: VecLike): boolean {
|
||||
|
|
|
@ -165,22 +165,23 @@ export class Idle extends StateNode {
|
|||
const change = util.onDoubleClick?.(shape)
|
||||
if (change) {
|
||||
this.app.updateShapes([change])
|
||||
return
|
||||
} else if (util.canCrop(shape)) {
|
||||
// crop on double click
|
||||
this.app.mark('select and crop')
|
||||
this.app.select(info.shape?.id)
|
||||
this.parent.transition('crop', info)
|
||||
return
|
||||
}
|
||||
}
|
||||
// If the shape can edit, then begin editing
|
||||
if (util.canEdit(shape)) {
|
||||
this.startEditingShape(shape, info)
|
||||
} else {
|
||||
// If the shape can edit, then begin editing
|
||||
if (util.canEdit(shape)) {
|
||||
this.startEditingShape(shape, info)
|
||||
} else {
|
||||
// If the shape's double click handler has not created a change,
|
||||
// and if the shape cannot edit, then create a text shape and
|
||||
// begin editing the text shape
|
||||
this.createTextShapeAtPoint(info)
|
||||
}
|
||||
// If the shape's double click handler has not created a change,
|
||||
// and if the shape cannot edit, then create a text shape and
|
||||
// begin editing the text shape
|
||||
this.createTextShapeAtPoint(info)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
|
@ -11,19 +11,12 @@ export class PointingShape extends StateNode {
|
|||
didSelectOnEnter = false
|
||||
|
||||
onEnter = (info: TLPointerEventInfo & { target: 'shape' }) => {
|
||||
const { shape } = info
|
||||
|
||||
if (shape) {
|
||||
// If a shape has a click handler, then don't do anything here;
|
||||
// we're run its click handler on pointer up.
|
||||
const util = this.app.getShapeUtil(shape)
|
||||
if (util.onClick !== undefined) return
|
||||
}
|
||||
|
||||
this.eventTargetShape = info.shape
|
||||
this.selectingShape = this.app.getOutermostSelectableShape(info.shape)
|
||||
|
||||
if (this.selectingShape.id === this.app.focusLayerId) {
|
||||
const util = this.app.getShapeUtil(info.shape)
|
||||
|
||||
if (util.onClick || this.selectingShape.id === this.app.focusLayerId) {
|
||||
this.didSelectOnEnter = false
|
||||
return
|
||||
}
|
||||
|
@ -67,8 +60,9 @@ export class PointingShape extends StateNode {
|
|||
const change = util.onClick?.(shape)
|
||||
if (change) {
|
||||
this.app.updateShapes([change])
|
||||
this.parent.transition('idle', info)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -253,6 +253,7 @@ export const STYLES: TLStyleCollections = {
|
|||
{ id: 'arrow-up', type: 'geo', icon: 'geo-arrow-up' },
|
||||
{ id: 'arrow-down', type: 'geo', icon: 'geo-arrow-down' },
|
||||
{ id: 'x-box', type: 'geo', icon: 'geo-x-box' },
|
||||
{ id: 'check-box', type: 'geo', icon: 'geo-check-box' },
|
||||
],
|
||||
arrowheadStart: [
|
||||
{ id: 'none', type: 'arrowheadStart', icon: 'arrowhead-none' },
|
||||
|
|
|
@ -149,24 +149,25 @@ describe('arrows', () => {
|
|||
let arrow: TLShape
|
||||
beforeEach(() => {
|
||||
// draw a first box
|
||||
app.setSelectedTool('geo')
|
||||
app.pointerDown(200, 200)
|
||||
app.pointerMove(300, 300)
|
||||
app.pointerUp(300, 300)
|
||||
app
|
||||
.setSelectedTool('geo')
|
||||
.pointerDown(200, 200)
|
||||
.pointerMove(300, 300)
|
||||
.pointerUp(300, 300)
|
||||
.setProp('fill', 'solid')
|
||||
firstBox = app.onlySelectedShape!
|
||||
|
||||
// draw a second box
|
||||
app.setSelectedTool('geo')
|
||||
app.pointerDown(400, 400)
|
||||
app.pointerMove(500, 500)
|
||||
app.pointerUp(500, 500)
|
||||
app
|
||||
.setSelectedTool('geo')
|
||||
.pointerDown(400, 400)
|
||||
.pointerMove(500, 500)
|
||||
.pointerUp(500, 500)
|
||||
.setProp('fill', 'solid')
|
||||
secondBox = app.onlySelectedShape!
|
||||
|
||||
// draw an arrow from the first box to the second box
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(250, 250)
|
||||
app.pointerMove(450, 450)
|
||||
app.pointerUp(450, 450)
|
||||
app.setSelectedTool('arrow').pointerDown(250, 250).pointerMove(450, 450).pointerUp(450, 450)
|
||||
arrow = app.onlySelectedShape!
|
||||
})
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ beforeEach(() => {
|
|||
app.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
|
||||
})
|
||||
|
||||
it.only('updates the culling viewport', () => {
|
||||
it('updates the culling viewport', () => {
|
||||
app.updateCullingBounds = jest.fn(app.updateCullingBounds)
|
||||
app.pan(-201, -201)
|
||||
jest.advanceTimersByTime(500)
|
||||
|
@ -74,7 +74,7 @@ it.only('updates the culling viewport', () => {
|
|||
expect(app.getPageBoundsById(ids.A)).toMatchObject({ x: 100, y: 100, w: 100, h: 100 })
|
||||
})
|
||||
|
||||
it.only('lists shapes in viewport', () => {
|
||||
it('lists shapes in viewport', () => {
|
||||
expect(
|
||||
app.renderingShapes.map(({ id, isCulled, isInViewport }) => [id, isCulled, isInViewport])
|
||||
).toStrictEqual([
|
||||
|
@ -122,25 +122,24 @@ it.only('lists shapes in viewport', () => {
|
|||
})
|
||||
|
||||
it('lists shapes in viewport sorted by id', () => {
|
||||
// Expect the results to have the correct index
|
||||
expect(app.renderingShapes.map(({ index }) => index)).toStrictEqual([0, 1, 2])
|
||||
|
||||
// Expect the results to be sorted correctly by id
|
||||
expect(app.renderingShapes.map(({ id, index }) => [id, index])).toStrictEqual([
|
||||
[ids.A, 0],
|
||||
[ids.B, 1],
|
||||
[ids.C, 2],
|
||||
// A is at the back, then C, then its child ellipse1
|
||||
[ids.D, 3],
|
||||
// A is at the back, then B, and then B's children
|
||||
])
|
||||
|
||||
// Send C to the back
|
||||
// Send B to the back
|
||||
app.reorderShapes('toBack', [ids.B])
|
||||
|
||||
// The items should still be sorted by id
|
||||
expect(app.renderingShapes.map(({ id, index }) => [id, index])).toStrictEqual([
|
||||
[ids.A, 2],
|
||||
[ids.A, 3],
|
||||
[ids.B, 0],
|
||||
[ids.C, 1],
|
||||
// C is now at the back, then its child, and finally A is now in the front
|
||||
[ids.D, 2],
|
||||
// B is now at the back, then its children, and finally A is now in the front
|
||||
])
|
||||
})
|
||||
|
|
|
@ -14,7 +14,7 @@ beforeEach(() => {
|
|||
app.createShapes(createDefaultShapes())
|
||||
})
|
||||
|
||||
it.only('Sets selected shapes', () => {
|
||||
it('Sets selected shapes', () => {
|
||||
expect(app.selectedIds).toMatchObject([])
|
||||
app.setSelectedIds([ids.box1, ids.box2])
|
||||
expect(app.selectedIds).toMatchObject([ids.box1, ids.box2])
|
||||
|
|
|
@ -15,7 +15,7 @@ beforeEach(() => {
|
|||
})
|
||||
|
||||
describe('When resizing', () => {
|
||||
it.only('sets the viewport bounds with App.resize', () => {
|
||||
it('sets the viewport bounds with App.resize', () => {
|
||||
app.setScreenBounds({ x: 100, y: 200, w: 700, h: 600 })
|
||||
expect(app.viewportScreenBounds).toMatchObject({
|
||||
x: 0,
|
||||
|
@ -25,7 +25,7 @@ describe('When resizing', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it.only('updates the viewport as an ephemeral change', () => {
|
||||
it('updates the viewport as an ephemeral change', () => {
|
||||
app.setScreenBounds({ x: 100, y: 200, w: 700, h: 600 })
|
||||
|
||||
app.undo() // this should have no effect
|
||||
|
|
|
@ -169,7 +169,7 @@ describe('When interacting with a shape...', () => {
|
|||
expect(fnEnd).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('Fires click events', () => {
|
||||
it('Uses the shape utils onClick handler', () => {
|
||||
const util = app.getShapeUtilByDef(TLFrameShapeDef)
|
||||
|
||||
const fnClick = jest.fn()
|
||||
|
@ -178,7 +178,29 @@ describe('When interacting with a shape...', () => {
|
|||
app.pointerDown(50, 50, ids.frame1)
|
||||
app.pointerUp(50, 50, ids.frame1)
|
||||
|
||||
// If a shape has an onClick handler, it will not be selected by default
|
||||
// If a shape has an onClick handler, and if the handler returns nothing,
|
||||
// then normal selection rules should apply.
|
||||
expect(app.selectedIds.length).toBe(1)
|
||||
})
|
||||
|
||||
it('Uses the shape utils onClick handler', () => {
|
||||
const util = app.getShapeUtilByDef(TLFrameShapeDef)
|
||||
|
||||
const fnClick = jest.fn((shape: any) => {
|
||||
return {
|
||||
...shape,
|
||||
x: 100,
|
||||
y: 100,
|
||||
}
|
||||
})
|
||||
|
||||
util.onClick = fnClick
|
||||
|
||||
app.pointerDown(50, 50, ids.frame1)
|
||||
app.pointerUp(50, 50, ids.frame1)
|
||||
|
||||
// If a shape has an onClick handler, and it returns something, then
|
||||
// it should not be selected.
|
||||
expect(app.selectedIds.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -23,18 +23,27 @@ beforeEach(() => {
|
|||
type: 'geo',
|
||||
x: 0,
|
||||
y: 0,
|
||||
props: {
|
||||
fill: 'none',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ids.box2,
|
||||
type: 'geo',
|
||||
x: 75, // overlapping box1
|
||||
y: 75,
|
||||
props: {
|
||||
fill: 'solid',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ids.box3,
|
||||
type: 'geo',
|
||||
x: 300,
|
||||
y: 300,
|
||||
props: {
|
||||
fill: 'solid',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ids.frame1,
|
||||
|
@ -52,6 +61,9 @@ beforeEach(() => {
|
|||
parentId: ids.frame1,
|
||||
x: 50,
|
||||
y: 50, // clipped by frame
|
||||
props: {
|
||||
fill: 'solid',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ids.draw1,
|
||||
|
@ -89,7 +101,7 @@ describe('When clicking', () => {
|
|||
|
||||
const shapesBeforeCount = app.shapesArray.length
|
||||
|
||||
app.pointerDown(25, 25) // in box1
|
||||
app.pointerDown(0, 0) // near enough to box1
|
||||
|
||||
// Enters the pointing state
|
||||
app.expectPathToBe('root.eraser.pointing')
|
||||
|
@ -129,7 +141,7 @@ describe('When clicking', () => {
|
|||
|
||||
const shapesBeforeCount = app.shapesArray.length
|
||||
|
||||
app.pointerDown(80, 80) // in box1 AND box2
|
||||
app.pointerDown(99, 99) // neat to box1 AND in box2
|
||||
|
||||
expect(new Set(app.erasingIds)).toEqual(new Set([ids.box1, ids.box2]))
|
||||
expect(app.erasingIdsSet).toEqual(new Set([ids.box1, ids.box2]))
|
||||
|
@ -240,7 +252,7 @@ describe('When clicking', () => {
|
|||
|
||||
const shapesBeforeCount = app.shapesArray.length
|
||||
|
||||
app.pointerDown(25, 25) // in box1
|
||||
app.pointerDown(0, 0) // in box1
|
||||
app.expectPathToBe('root.eraser.pointing')
|
||||
|
||||
expect(app.erasingIds).toEqual([ids.box1])
|
||||
|
@ -267,7 +279,7 @@ describe('When clicking', () => {
|
|||
|
||||
const shapesBeforeCount = app.shapesArray.length
|
||||
|
||||
app.pointerDown(25, 25) // in box1
|
||||
app.pointerDown(0, 0) // near to box1
|
||||
app.expectPathToBe('root.eraser.pointing')
|
||||
|
||||
expect(app.erasingIds).toEqual([ids.box1])
|
||||
|
@ -380,6 +392,7 @@ describe('When clicking and dragging', () => {
|
|||
it('Only erases masked shapes when pointer is inside the mask', () => {
|
||||
app.setSelectedTool('eraser')
|
||||
app.pointerDown(425, 0) // Above the masked part of box3
|
||||
expect(app.erasingIds).toEqual([])
|
||||
app.pointerMove(425, 500) // Through the masked part of box3
|
||||
jest.advanceTimersByTime(16)
|
||||
expect(app.scribble).not.toBe(null)
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`When resizing a shape with children Resizes a rotated draw shape: draw shape after rotating 1`] = `
|
||||
Object {
|
||||
"id": "shape:lineA",
|
||||
"index": "a3",
|
||||
"isLocked": false,
|
||||
"parentId": "shape:boxA",
|
||||
"props": Object {
|
||||
"color": "black",
|
||||
"dash": "draw",
|
||||
"fill": "none",
|
||||
"isClosed": false,
|
||||
"isComplete": false,
|
||||
"isPen": false,
|
||||
"opacity": "1",
|
||||
"segments": Array [
|
||||
Object {
|
||||
"points": Array [
|
||||
Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0.5,
|
||||
},
|
||||
Object {
|
||||
"x": 110.00000000000001,
|
||||
"y": 110.00000000000001,
|
||||
"z": 0.5,
|
||||
},
|
||||
],
|
||||
"type": "free",
|
||||
},
|
||||
],
|
||||
"size": "m",
|
||||
},
|
||||
"rotation": 3.141592653589793,
|
||||
"type": "draw",
|
||||
"typeName": "shape",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
}
|
||||
`;
|
|
@ -112,7 +112,9 @@ describe('creating frames', () => {
|
|||
})
|
||||
|
||||
it('can snap', () => {
|
||||
app.createShapes([{ type: 'geo', id: ids.boxA, x: 0, y: 0, props: { w: 50, h: 50 } }])
|
||||
app.createShapes([
|
||||
{ type: 'geo', id: ids.boxA, x: 0, y: 0, props: { w: 50, h: 50, fill: 'solid' } },
|
||||
])
|
||||
|
||||
app.setSelectedTool('frame')
|
||||
app.pointerDown(100, 100).pointerMove(49, 149)
|
||||
|
@ -242,7 +244,9 @@ describe('frame shapes', () => {
|
|||
|
||||
const frameId = app.onlySelectedShape!.id
|
||||
|
||||
app.createShapes([{ type: 'geo', id: ids.boxA, x: 250, y: 250, props: { w: 50, h: 50 } }])
|
||||
app.createShapes([
|
||||
{ type: 'geo', id: ids.boxA, x: 250, y: 250, props: { w: 50, h: 50, fill: 'solid' } },
|
||||
])
|
||||
|
||||
expect(app.onlySelectedShape!.parentId).toBe(app.currentPageId)
|
||||
|
||||
|
@ -276,7 +280,9 @@ describe('frame shapes', () => {
|
|||
const frameId = app.onlySelectedShape!.id
|
||||
|
||||
// Create a new shape off of the frame
|
||||
app.createShapes([{ type: 'geo', id: ids.boxA, x: 250, y: 250, props: { w: 50, h: 50 } }])
|
||||
app.createShapes([
|
||||
{ type: 'geo', id: ids.boxA, x: 250, y: 250, props: { w: 50, h: 50, fill: 'solid' } },
|
||||
])
|
||||
|
||||
// It should be a child of the page
|
||||
expect(app.onlySelectedShape!.parentId).toBe(app.currentPageId)
|
||||
|
@ -333,7 +339,9 @@ describe('frame shapes', () => {
|
|||
app.setSelectedTool('frame')
|
||||
app.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
|
||||
|
||||
app.createShapes([{ type: 'geo', id: ids.boxA, x: 250, y: 250, props: { w: 50, h: 50 } }])
|
||||
app.createShapes([
|
||||
{ type: 'geo', id: ids.boxA, x: 250, y: 250, props: { w: 50, h: 50, fill: 'solid' } },
|
||||
])
|
||||
|
||||
app.setSelectedTool('select')
|
||||
app.select(ids.boxA)
|
||||
|
@ -480,7 +488,7 @@ describe('frame shapes', () => {
|
|||
const frameId = app.onlySelectedShape!.id
|
||||
|
||||
app.setSelectedTool('geo')
|
||||
app.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175)
|
||||
app.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175).setProp('fill', 'solid')
|
||||
const boxId = app.onlySelectedShape!.id
|
||||
|
||||
app.setSelectedTool('arrow')
|
||||
|
@ -587,7 +595,7 @@ describe('frame shapes', () => {
|
|||
|
||||
// make a shape inside the frame that extends out of the frame
|
||||
app.setSelectedTool('geo')
|
||||
app.pointerDown(150, 150).pointerMove(400, 400).pointerUp(400, 400)
|
||||
app.pointerDown(150, 150).pointerMove(400, 400).pointerUp(400, 400).setProp('fill', 'solid')
|
||||
const innerBoxId = app.onlySelectedShape!.id
|
||||
|
||||
// Make an arrow that binds to the inner box's bottom right corner
|
||||
|
@ -636,15 +644,15 @@ test('arrows bound to a shape within a group within a frame are reparented if th
|
|||
const frameId = app.onlySelectedShape!.id
|
||||
|
||||
app.setSelectedTool('geo')
|
||||
app.pointerDown(110, 110).pointerMove(120, 120).pointerUp(120, 120)
|
||||
app.pointerDown(110, 110).pointerMove(120, 120).pointerUp(120, 120).setProp('fill', 'solid')
|
||||
const boxAId = app.onlySelectedShape!.id
|
||||
|
||||
app.setSelectedTool('geo')
|
||||
app.pointerDown(180, 110).pointerMove(190, 120).pointerUp(190, 120)
|
||||
app.pointerDown(180, 110).pointerMove(190, 120).pointerUp(190, 120).setProp('fill', 'solid')
|
||||
const boxBId = app.onlySelectedShape!.id
|
||||
|
||||
app.setSelectedTool('geo')
|
||||
app.pointerDown(160, 160).pointerMove(170, 170).pointerUp(170, 170)
|
||||
app.pointerDown(160, 160).pointerMove(170, 170).pointerUp(170, 170).setProp('fill', 'solid')
|
||||
const boxCId = app.onlySelectedShape!.id
|
||||
|
||||
app.setSelectedTool('select')
|
||||
|
|
|
@ -44,6 +44,7 @@ const box = (id: TLShapeId, x: number, y: number, w = 10, h = 10): TLShapePartia
|
|||
props: {
|
||||
w,
|
||||
h,
|
||||
fill: 'solid',
|
||||
},
|
||||
})
|
||||
const arrow = (id: TLShapeId, start: VecLike, end: VecLike): TLShapePartial => ({
|
||||
|
|
|
@ -3828,7 +3828,7 @@ it('uses the cross cursor when create resizing', () => {
|
|||
})
|
||||
|
||||
describe('Resizing text from the right edge', () => {
|
||||
it.only('Resizes text from the right edge', () => {
|
||||
it('Resizes text from the right edge', () => {
|
||||
const id = app.createShapeId()
|
||||
app.createShapes([{ id, type: 'text', props: { text: 'H' } }])
|
||||
app.updateShapes([{ id, type: 'text', props: { text: 'Hello World' } }]) // auto size
|
||||
|
|
|
@ -344,7 +344,7 @@ export const geoShapeMigrations: Migrations;
|
|||
export const geoShapeTypeValidator: T.Validator<TLGeoShape>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const geoValidator: T.Validator<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
||||
export const geoValidator: T.Validator<"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">;
|
||||
|
||||
// @public (undocumented)
|
||||
export const groupShapeMigrations: Migrations;
|
||||
|
@ -490,7 +490,7 @@ export const TL_FILL_TYPES: Set<"none" | "pattern" | "semi" | "solid">;
|
|||
export const TL_FONT_TYPES: Set<"draw" | "mono" | "sans" | "serif">;
|
||||
|
||||
// @public (undocumented)
|
||||
export const TL_GEO_TYPES: Set<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
||||
export const TL_GEO_TYPES: Set<"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">;
|
||||
|
||||
// @public (undocumented)
|
||||
export const TL_HANDLE_TYPES: Set<"create" | "vertex" | "virtual">;
|
||||
|
|
|
@ -653,6 +653,18 @@ describe('Adding instance_presence to the schema', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Adding check-box to geo shape', () => {
|
||||
const { up, down } = geoShapeMigrations.migrators[4]
|
||||
|
||||
test('up works as expected', () => {
|
||||
expect(up({ props: { geo: 'rectangle' } })).toEqual({ props: { geo: 'rectangle' } })
|
||||
})
|
||||
test('down works as expected', () => {
|
||||
expect(down({ props: { geo: 'rectangle' } })).toEqual({ props: { geo: 'rectangle' } })
|
||||
expect(down({ props: { geo: 'check-box' } })).toEqual({ props: { geo: 'rectangle' } })
|
||||
})
|
||||
})
|
||||
|
||||
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
||||
|
||||
for (const migrator of allMigrators) {
|
||||
|
|
|
@ -73,13 +73,14 @@ const Versions = {
|
|||
AddUrlProp: 1,
|
||||
AddLabelColor: 2,
|
||||
RemoveJustify: 3,
|
||||
AddCheckBox: 4,
|
||||
} as const
|
||||
|
||||
/** @public */
|
||||
export const geoShapeMigrations = defineMigrations({
|
||||
// STEP 2: Update the current version to point to your latest version
|
||||
firstVersion: Versions.Initial,
|
||||
currentVersion: Versions.RemoveJustify,
|
||||
currentVersion: Versions.AddCheckBox,
|
||||
migrators: {
|
||||
// STEP 3: Add an up+down migration for the new version here
|
||||
[Versions.AddUrlProp]: {
|
||||
|
@ -128,5 +129,19 @@ export const geoShapeMigrations = defineMigrations({
|
|||
return { ...shape }
|
||||
},
|
||||
},
|
||||
[Versions.AddCheckBox]: {
|
||||
up: (shape) => {
|
||||
return { ...shape }
|
||||
},
|
||||
down: (shape) => {
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...shape.props,
|
||||
geo: shape.props.geo === 'check-box' ? 'rectangle' : shape.props.geo,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -154,6 +154,7 @@ export const TL_GEO_TYPES = new Set([
|
|||
'arrow-up',
|
||||
'arrow-down',
|
||||
'x-box',
|
||||
'check-box',
|
||||
] as const)
|
||||
|
||||
/** @public */
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -147,6 +147,7 @@ export type TLTranslationKey =
|
|||
| 'geo-style.trapezoid'
|
||||
| 'geo-style.triangle'
|
||||
| 'geo-style.x-box'
|
||||
| 'geo-style.check-box'
|
||||
| 'arrowheadStart-style.none'
|
||||
| 'arrowheadStart-style.arrow'
|
||||
| 'arrowheadStart-style.bar'
|
||||
|
@ -189,6 +190,7 @@ export type TLTranslationKey =
|
|||
| 'tool.trapezoid'
|
||||
| 'tool.triangle'
|
||||
| 'tool.x-box'
|
||||
| 'tool.check-box'
|
||||
| 'tool.asset'
|
||||
| 'tool.frame'
|
||||
| 'tool.note'
|
||||
|
|
|
@ -146,7 +146,8 @@ export const DEFAULT_TRANSLATION = {
|
|||
'geo-style.star': 'Star',
|
||||
'geo-style.trapezoid': 'Trapezoid',
|
||||
'geo-style.triangle': 'Triangle',
|
||||
'geo-style.x-box': 'X Box',
|
||||
'geo-style.x-box': 'X box',
|
||||
'geo-style.check-box': 'Check box',
|
||||
'arrowheadStart-style.none': 'None',
|
||||
'arrowheadStart-style.arrow': 'Arrow',
|
||||
'arrowheadStart-style.bar': 'Bar',
|
||||
|
@ -189,6 +190,7 @@ export const DEFAULT_TRANSLATION = {
|
|||
'tool.trapezoid': 'Trapezoid',
|
||||
'tool.triangle': 'Triangle',
|
||||
'tool.x-box': 'X box',
|
||||
'tool.check-box': 'Check box',
|
||||
'tool.asset': 'Asset',
|
||||
'tool.frame': 'Frame',
|
||||
'tool.note': 'Note',
|
||||
|
|
|
@ -77,6 +77,7 @@ export type TLUiIconType =
|
|||
| 'geo-arrow-left'
|
||||
| 'geo-arrow-right'
|
||||
| 'geo-arrow-up'
|
||||
| 'geo-check-box'
|
||||
| 'geo-diamond'
|
||||
| 'geo-ellipse'
|
||||
| 'geo-hexagon'
|
||||
|
@ -235,6 +236,7 @@ export const TLUiIconTypes = [
|
|||
'geo-arrow-left',
|
||||
'geo-arrow-right',
|
||||
'geo-arrow-up',
|
||||
'geo-check-box',
|
||||
'geo-diamond',
|
||||
'geo-ellipse',
|
||||
'geo-hexagon',
|
||||
|
|
Loading…
Reference in a new issue