Explicit shape type checks (#1594)

This PR adds shape type checks that use the shape util, e.g.
`this.editor.isShapeOfType(shape, FrameShapeUtil)`. In part this is
designed to help us track down where dependencies exist between the
editor and our default shapes.

### Change Type

- [x] `internal` — Any other changes that don't affect the published
package
This commit is contained in:
Steve Ruiz 2023-06-15 16:09:41 +01:00 committed by GitHub
parent 0bbdcdd91b
commit 21377c0f22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 118 additions and 87 deletions

View file

@ -1185,18 +1185,12 @@ export const INDENT = " ";
// @public
export function isAnimated(buffer: ArrayBuffer): boolean;
// @public (undocumented)
export function isGeoShape(shape: TLShape): shape is TLGeoShape;
// @public
export function isGIF(buffer: ArrayBuffer): boolean;
// @public (undocumented)
export const isImage: (ext: string) => boolean;
// @public (undocumented)
export function isNoteShape(shape: TLShape): shape is TLNoteShape;
// @public
export function isSerializable(value: any): boolean;

View file

@ -248,8 +248,6 @@ export {
getSvgAsImage,
getSvgAsString,
getTextBoundingBox,
isGeoShape,
isNoteShape,
type TLCopyType,
type TLExportType,
} from './lib/utils/export'

View file

@ -2,6 +2,8 @@ import { RotateCorner, toDomPrecision } from '@tldraw/primitives'
import classNames from 'classnames'
import { useRef } from 'react'
import { track } from 'signia-react'
import { EmbedShapeUtil } from '../editor/shapes/embed/EmbedShapeUtil'
import { TextShapeUtil } from '../editor/shapes/text/TextShapeUtil'
import { getCursor } from '../hooks/useCursor'
import { useEditor } from '../hooks/useEditor'
import { useSelectionEvents } from '../hooks/useSelectionEvents'
@ -32,7 +34,7 @@ export const SelectionFg = track(function SelectionFg() {
let bounds = editor.selectionBounds
const shapes = editor.selectedShapes
const onlyShape = shapes.length === 1 ? shapes[0] : null
const onlyShape = editor.onlySelectedShape
const isLockedShape = onlyShape && editor.isShapeOrAncestorLocked(onlyShape)
// if all shapes have an expandBy for the selection outline, we can expand by the l
@ -92,12 +94,15 @@ export const SelectionFg = track(function SelectionFg() {
(showSelectionBounds &&
editor.isIn('select.resizing') &&
onlyShape &&
shapes[0].type === 'text')
editor.isShapeOfType(onlyShape, TextShapeUtil))
if (IS_FIREFOX && shouldDisplayBox) {
if (editor.onlySelectedShape?.type === 'embed') {
shouldDisplayBox = false
}
if (
onlyShape &&
editor.isShapeOfType(onlyShape, EmbedShapeUtil) &&
shouldDisplayBox &&
IS_FIREFOX
) {
shouldDisplayBox = false
}
const showCropHandles =
@ -180,7 +185,8 @@ export const SelectionFg = track(function SelectionFg() {
const showTextResizeHandles =
shouldDisplayControls &&
isCoarsePointer &&
onlyShape?.type === 'text' &&
onlyShape &&
editor.isShapeOfType(onlyShape, TextShapeUtil) &&
textHandleHeight * zoom >= 4
return (

View file

@ -728,7 +728,9 @@ export class Editor extends EventEmitter<TLEventMap> {
return undefined
}
const frameAncestors = this.getAncestorsById(shape.id).filter((s) => s.type === 'frame')
const frameAncestors = this.getAncestorsById(shape.id).filter((shape) =>
this.isShapeOfType(shape, FrameShapeUtil)
)
if (frameAncestors.length === 0) return undefined
@ -1087,7 +1089,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @internal
*/
private _extractSharedProps(shape: TLShape, sharedProps: TLNullableShapeProps) {
if (shape.type === 'group') {
if (this.isShapeOfType(shape, GroupShapeUtil)) {
// For groups, ignore the props of the group shape and instead include
// the props of the group's children. These are the shapes that would have
// their props changed if the user called `setProp` on the current selection.
@ -1210,7 +1212,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// For groups, ignore the opacity of the group shape and instead include
// the opacity of the group's children. These are the shapes that would have
// their opacity changed if the user called `setOpacity` on the current selection.
if (shape.type === 'group') {
if (this.isShapeOfType(shape, GroupShapeUtil)) {
for (const childId of this.getSortedChildIds(shape.id)) {
addShape(childId)
}
@ -1274,7 +1276,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/** @internal */
@computed
private get _arrowBindingsIndex() {
return arrowBindingsIndex(this.store)
return arrowBindingsIndex(this)
}
/**
@ -1541,9 +1543,8 @@ export class Editor extends EventEmitter<TLEventMap> {
const nextFocusLayerId =
filtered.length === 0
? next?.focusLayerId
: this.findCommonAncestor(
compact(filtered.map((id) => this.getShapeById(id))),
(shape) => shape.type === 'group'
: this.findCommonAncestor(compact(filtered.map((id) => this.getShapeById(id))), (shape) =>
this.isShapeOfType(shape, GroupShapeUtil)
)
if (filtered.length !== next.selectedIds.length || nextFocusLayerId != next.focusLayerId) {
@ -3003,7 +3004,9 @@ export class Editor extends EventEmitter<TLEventMap> {
if (focusedShape) {
// If we have a focused layer, look for an ancestor of the focused shape that is a group
const match = this.findAncestor(focusedShape, (s) => s.type === 'group')
const match = this.findAncestor(focusedShape, (shape) =>
this.isShapeOfType(shape, GroupShapeUtil)
)
// If we have an ancestor that can become a focused layer, set it as the focused layer
this.setFocusLayer(match?.id ?? null)
this.select(focusedShape.id)
@ -3237,7 +3240,7 @@ export class Editor extends EventEmitter<TLEventMap> {
let node = shape as TLShape | undefined
while (node) {
if (
node.type === 'group' &&
this.isShapeOfType(node, GroupShapeUtil) &&
this.focusLayerId !== node.id &&
!this.hasAncestor(this.focusLayerShape, node.id) &&
(filter?.(node) ?? true)
@ -4590,15 +4593,16 @@ export class Editor extends EventEmitter<TLEventMap> {
for (const shape of this.selectedShapes) {
if (lowestDepth === 0) break
const isFrame = this.isShapeOfType(shape, FrameShapeUtil)
const ancestors = this.getAncestors(shape)
if (shape.type === 'frame') ancestors.push(shape)
if (isFrame) ancestors.push(shape)
const depth = shape.type === 'frame' ? ancestors.length + 1 : ancestors.length
const depth = isFrame ? ancestors.length + 1 : ancestors.length
if (depth < lowestDepth) {
lowestDepth = depth
lowestAncestors = ancestors
pasteParentId = shape.type === 'frame' ? shape.id : shape.parentId
pasteParentId = isFrame ? shape.id : shape.parentId
} else if (depth === lowestDepth) {
if (lowestAncestors.length !== ancestors.length) {
throw Error(`Ancestors: ${lowestAncestors.length} !== ${ancestors.length}`)
@ -4836,7 +4840,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (rootShapes.length === 1) {
const onlyRoot = rootShapes[0] as TLFrameShape
// If the old bounds are in the viewport...
if (onlyRoot.type === 'frame') {
if (this.isShapeOfType(onlyRoot, FrameShapeUtil)) {
while (
this.getShapesAtPoint(point).some(
(shape) =>
@ -6015,7 +6019,9 @@ export class Editor extends EventEmitter<TLEventMap> {
if (!bbox) return
const singleFrameShapeId =
ids.length === 1 && this.getShapeById(ids[0])?.type === 'frame' ? ids[0] : null
ids.length === 1 && this.isShapeOfType(this.getShapeById(ids[0])!, FrameShapeUtil)
? ids[0]
: null
if (!singleFrameShapeId) {
// Expand by an extra 32 pixels
bbox.expandBy(padding)
@ -6620,7 +6626,7 @@ export class Editor extends EventEmitter<TLEventMap> {
shapes = compact(
shapes
.map((shape) => {
if (shape.type === 'group') {
if (this.isShapeOfType(shape, GroupShapeUtil)) {
return this.getSortedChildIds(shape.id).map((id) => this.getShapeById(id))
}

View file

@ -1,6 +1,7 @@
import { TLArrowShape, TLShapeId } from '@tldraw/tlschema'
import { TestEditor } from '../../test/TestEditor'
import { TL } from '../../test/jsx'
import { GeoShapeUtil } from '../shapes/geo/GeoShapeUtil'
let editor: TestEditor
@ -184,7 +185,7 @@ describe('arrowBindingsIndex', () => {
editor.duplicateShapes()
const [box1Clone, box2Clone] = editor.selectedShapes
.filter((s) => s.type === 'geo')
.filter((shape) => editor.isShapeOfType(shape, GeoShapeUtil))
.sort((a, b) => a.x - b.x)
expect(editor.getArrowsBoundTo(box2Clone.id)).toHaveLength(3)

View file

@ -1,16 +1,15 @@
import { TLArrowShape, TLShape, TLShapeId, TLStore } from '@tldraw/tlschema'
import { TLArrowShape, TLShape, TLShapeId } from '@tldraw/tlschema'
import { Computed, RESET_VALUE, computed, isUninitialized } from 'signia'
import { Editor } from '../Editor'
import { ArrowShapeUtil } from '../shapes/arrow/ArrowShapeUtil'
export type TLArrowBindingsIndex = Record<
TLShapeId,
undefined | { arrowId: TLShapeId; handleId: 'start' | 'end' }[]
>
function isArrowType(shape: any): shape is TLArrowShape {
return shape.type === 'arrow'
}
export const arrowBindingsIndex = (store: TLStore): Computed<TLArrowBindingsIndex> => {
export const arrowBindingsIndex = (editor: Editor): Computed<TLArrowBindingsIndex> => {
const { store } = editor
const shapeHistory = store.query.filterHistory('shape')
const arrowQuery = store.query.records('shape', () => ({ type: { eq: 'arrow' as const } }))
function fromScratch() {
@ -35,6 +34,7 @@ export const arrowBindingsIndex = (store: TLStore): Computed<TLArrowBindingsInde
return bindings2Arrows
}
return computed<TLArrowBindingsIndex>('arrowBindingsIndex', (_lastValue, lastComputedEpoch) => {
if (isUninitialized(_lastValue)) {
return fromScratch()
@ -83,7 +83,7 @@ export const arrowBindingsIndex = (store: TLStore): Computed<TLArrowBindingsInde
for (const changes of diff) {
for (const newShape of Object.values(changes.added)) {
if (isArrowType(newShape)) {
if (editor.isShapeOfType(newShape, ArrowShapeUtil)) {
const { start, end } = newShape.props
if (start.type === 'binding') {
addBinding(start.boundShapeId, newShape.id, 'start')
@ -95,7 +95,11 @@ export const arrowBindingsIndex = (store: TLStore): Computed<TLArrowBindingsInde
}
for (const [prev, next] of Object.values(changes.updated) as [TLShape, TLShape][]) {
if (!isArrowType(prev) || !isArrowType(next)) continue
if (
!editor.isShapeOfType(prev, ArrowShapeUtil) ||
!editor.isShapeOfType(next, ArrowShapeUtil)
)
continue
for (const handle of ['start', 'end'] as const) {
const prevTerminal = prev.props[handle]
@ -120,7 +124,7 @@ export const arrowBindingsIndex = (store: TLStore): Computed<TLArrowBindingsInde
}
for (const prev of Object.values(changes.removed)) {
if (isArrowType(prev)) {
if (editor.isShapeOfType(prev, ArrowShapeUtil)) {
const { start, end } = prev.props
if (start.type === 'binding') {
removingBinding(start.boundShapeId, prev.id, 'start')

View file

@ -11,10 +11,8 @@ import {
import { last, structuredClone } from '@tldraw/utils'
import { DRAG_DISTANCE } from '../../../../constants'
import { uniqueId } from '../../../../utils/data'
import { DrawShapeUtil } from '../../../shapes/draw/DrawShapeUtil'
import { TLEventHandlers, TLPointerEventInfo } from '../../../types/event-types'
import { HighlightShapeUtil } from '../../../shapes/highlight/HighlightShapeUtil'
import { StateNode } from '../../../tools/StateNode'
type DrawableShape = TLDrawShape | TLHighlightShape
@ -28,11 +26,6 @@ export class Drawing extends StateNode {
shapeType: DrawableShape['type'] = this.parent.id === 'highlight' ? 'highlight' : 'draw'
util =
this.shapeType === 'highlight'
? this.editor.getShapeUtil(HighlightShapeUtil)
: this.editor.getShapeUtil(DrawShapeUtil)
isPen = false
segmentMode = 'free' as 'free' | 'straight' | 'starting_straight' | 'starting_free'

View file

@ -84,7 +84,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
if (editingId && hoveredId !== editingId) {
const editingShape = this.editor.getShapeById(editingId)
if (editingShape && editingShape.type === 'embed') {
if (editingShape && this.editor.isShapeOfType(editingShape, EmbedShapeUtil)) {
return true
}
}

View file

@ -4,6 +4,7 @@ import { last } from '@tldraw/utils'
import { SVGContainer } from '../../../components/SVGContainer'
import { defaultEmptyAs } from '../../../utils/string'
import { BaseBoxShapeUtil } from '../BaseBoxShapeUtil'
import { GroupShapeUtil } from '../group/GroupShapeUtil'
import { TLOnResizeEndHandler } from '../ShapeUtil'
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
import { TLExportColors } from '../shared/TLExportColors'
@ -171,15 +172,15 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
}
onDragShapesOut = (_shape: TLFrameShape, shapes: TLShape[]): void => {
const parentId = this.editor.getShapeById(_shape.parentId)
const isInGroup = parentId?.type === 'group'
const parent = this.editor.getShapeById(_shape.parentId)
const isInGroup = parent && this.editor.isShapeOfType(parent, GroupShapeUtil)
// If frame is in a group, keep the shape
// moved out in that group
if (isInGroup) {
this.editor.reparentShapesById(
shapes.map((shape) => shape.id),
parentId.id
parent.id
)
} else {
this.editor.reparentShapesById(

View file

@ -1,5 +1,6 @@
import { StateNode } from '../../../tools/StateNode'
import { TLEventHandlers } from '../../../types/event-types'
import { GeoShapeUtil } from '../GeoShapeUtil'
export class Idle extends StateNode {
static override id = 'idle'
@ -15,7 +16,7 @@ export class Idle extends StateNode {
onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
if (info.key === 'Enter') {
const shape = this.editor.onlySelectedShape
if (shape && shape.type === 'geo') {
if (shape && this.editor.isShapeOfType(shape, GeoShapeUtil)) {
// todo: ensure that this only works with the most recently created shape, not just any geo shape that happens to be selected at the time
this.editor.mark('editing shape')
this.editor.setEditingId(shape.id)

View file

@ -55,7 +55,11 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
const isHintingOtherGroup =
hintingIds.length > 0 &&
hintingIds.some((id) => id !== shape.id && this.editor.getShapeById(id)?.type === 'group')
hintingIds.some(
(id) =>
id !== shape.id &&
this.editor.isShapeOfType(this.editor.getShapeById(id)!, GroupShapeUtil)
)
if (
// always show the outline while we're erasing the group

View file

@ -1,5 +1,7 @@
import { StateNode } from '../../../tools/StateNode'
import { TLEventHandlers } from '../../../types/event-types'
import { GeoShapeUtil } from '../../geo/GeoShapeUtil'
import { TextShapeUtil } from '../TextShapeUtil'
export class Idle extends StateNode {
static override id = 'idle'
@ -17,7 +19,7 @@ export class Idle extends StateNode {
(parent) => !selectedIds.includes(parent.id)
)
if (hoveringShape.id !== focusLayerId) {
if (hoveringShape.type === 'text') {
if (this.editor.isShapeOfType(hoveringShape, TextShapeUtil)) {
this.editor.setHoveredId(hoveringShape.id)
}
}
@ -39,7 +41,7 @@ export class Idle extends StateNode {
const { hoveredId } = this.editor
if (hoveredId) {
const shape = this.editor.getShapeById(hoveredId)!
if (shape.type === 'text') {
if (this.editor.isShapeOfType(shape, TextShapeUtil)) {
requestAnimationFrame(() => {
this.editor.setSelectedIds([shape.id])
this.editor.setEditingId(shape.id)
@ -63,7 +65,7 @@ export class Idle extends StateNode {
onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
if (info.key === 'Enter') {
const shape = this.editor.selectedShapes[0]
if (shape && shape.type === 'geo') {
if (shape && this.editor.isShapeOfType(shape, GeoShapeUtil)) {
this.editor.setSelectedTool('select')
this.editor.setEditingId(shape.id)
this.editor.root.current.value!.transition('editing_shape', {

View file

@ -1,6 +1,8 @@
import { pointInPolygon } from '@tldraw/primitives'
import { TLScribble, TLShapeId } from '@tldraw/tlschema'
import { ScribbleManager } from '../../../managers/ScribbleManager'
import { FrameShapeUtil } from '../../../shapes/frame/FrameShapeUtil'
import { GroupShapeUtil } from '../../../shapes/group/GroupShapeUtil'
import { TLEventHandlers, TLPointerEventInfo } from '../../../types/event-types'
import { StateNode } from '../../StateNode'
@ -22,7 +24,8 @@ export class Erasing extends StateNode {
.filter(
(shape) =>
this.editor.isShapeOrAncestorLocked(shape) ||
((shape.type === 'group' || shape.type === 'frame') &&
((this.editor.isShapeOfType(shape, GroupShapeUtil) ||
this.editor.isShapeOfType(shape, FrameShapeUtil)) &&
this.editor.isPointInShape(originPagePoint, shape))
)
.map((shape) => shape.id)
@ -95,7 +98,7 @@ export class Erasing extends StateNode {
const erasing = new Set<TLShapeId>(erasingIdsSet)
for (const shape of shapesArray) {
if (shape.type === 'group') continue
if (this.editor.isShapeOfType(shape, GroupShapeUtil)) continue
// Avoid testing masked shapes, unless the pointer is inside the mask
const pageMask = this.editor.getPageMaskById(shape.id)

View file

@ -1,4 +1,6 @@
import { TLShapeId } from '@tldraw/tlschema'
import { FrameShapeUtil } from '../../../shapes/frame/FrameShapeUtil'
import { GroupShapeUtil } from '../../../shapes/group/GroupShapeUtil'
import { TLEventHandlers } from '../../../types/event-types'
import { StateNode } from '../../StateNode'
@ -15,12 +17,12 @@ export class Pointing extends StateNode {
for (const shape of [...this.editor.sortedShapesArray].reverse()) {
if (this.editor.isPointInShape(inputs.currentPagePoint, shape)) {
// Skip groups
if (shape.type === 'group') continue
if (this.editor.isShapeOfType(shape, GroupShapeUtil)) continue
const hitShape = this.editor.getOutermostSelectableShape(shape)
// If we've hit a frame after hitting any other shape, stop here
if (hitShape.type === 'frame' && erasing.size > initialSize) break
if (this.editor.isShapeOfType(hitShape, FrameShapeUtil) && erasing.size > initialSize) break
erasing.add(hitShape.id)
}

View file

@ -7,6 +7,8 @@ import {
VecLike,
} from '@tldraw/primitives'
import { TLPageId, TLShape, TLShapeId } from '@tldraw/tlschema'
import { FrameShapeUtil } from '../../../shapes/frame/FrameShapeUtil'
import { GroupShapeUtil } from '../../../shapes/group/GroupShapeUtil'
import { ShapeUtil } from '../../../shapes/ShapeUtil'
import {
TLCancelEvent,
@ -39,7 +41,11 @@ export class Brushing extends StateNode {
this.excludedShapeIds = new Set(
this.editor.shapesArray
.filter((shape) => shape.type === 'group' || this.editor.isShapeOrAncestorLocked(shape))
.filter(
(shape) =>
this.editor.isShapeOfType(shape, GroupShapeUtil) ||
this.editor.isShapeOrAncestorLocked(shape)
)
.map((shape) => shape.id)
)
@ -130,7 +136,7 @@ export class Brushing extends StateNode {
// Should we even test for a single segment intersections? Only if
// we're not holding the ctrl key for alternate selection mode
// (only wraps count!), or if the shape is a frame.
if (ctrlKey || shape.type === 'frame') {
if (ctrlKey || this.editor.isShapeOfType(shape, FrameShapeUtil)) {
continue testAllShapes
}

View file

@ -1,6 +1,7 @@
import { Vec2d } from '@tldraw/primitives'
import { TLGeoShape, TLShape, TLTextShape, createShapeId } from '@tldraw/tlschema'
import { debugFlags } from '../../../../utils/debug-flags'
import { GroupShapeUtil } from '../../../shapes/group/GroupShapeUtil'
import {
TLClickEventInfo,
TLEventHandlers,
@ -318,7 +319,7 @@ export class Idle extends StateNode {
case 'Enter': {
const { selectedShapes } = this.editor
if (selectedShapes.every((shape) => shape.type === 'group')) {
if (selectedShapes.every((shape) => this.editor.isShapeOfType(shape, GroupShapeUtil))) {
this.editor.setSelectedIds(
selectedShapes.flatMap((shape) => this.editor.getSortedChildIds(shape.id))
)

View file

@ -1,4 +1,5 @@
import { TLShape } from '@tldraw/tlschema'
import { GroupShapeUtil } from '../../../shapes/group/GroupShapeUtil'
import { TLEventHandlers, TLPointerEventInfo } from '../../../types/event-types'
import { StateNode } from '../../StateNode'
@ -35,7 +36,7 @@ export class PointingShape extends StateNode {
const parent = this.editor.getParentShape(info.shape)
if (parent && parent.type === 'group') {
if (parent && this.editor.isShapeOfType(parent, GroupShapeUtil)) {
this.editor.cancelDoubleClick()
}

View file

@ -10,6 +10,7 @@ import {
VecLike,
} from '@tldraw/primitives'
import { TLShape, TLShapeId, TLShapePartial } from '@tldraw/tlschema'
import { FrameShapeUtil } from '../../../shapes/frame/FrameShapeUtil'
import {
TLEnterEventHandler,
TLEventHandlers,
@ -369,12 +370,12 @@ export class Resizing extends StateNode {
const shape = this.editor.getShapeById(id)
if (shape) {
shapeSnapshots.set(shape.id, this._createShapeSnapshot(shape))
if (shape.type === 'frame' && selectedIds.length === 1) return
if (this.editor.isShapeOfType(shape, FrameShapeUtil) && selectedIds.length === 1) return
this.editor.visitDescendants(shape.id, (descendantId) => {
const descendent = this.editor.getShapeById(descendantId)
if (descendent) {
shapeSnapshots.set(descendent.id, this._createShapeSnapshot(descendent))
if (descendent.type === 'frame') {
if (this.editor.isShapeOfType(descendent, FrameShapeUtil)) {
return false
}
}

View file

@ -2,6 +2,8 @@ import { intersectLineSegmentPolyline, pointInPolygon } from '@tldraw/primitives
import { TLScribble, TLShape, TLShapeId } from '@tldraw/tlschema'
import { ScribbleManager } from '../../../managers/ScribbleManager'
import { ShapeUtil } from '../../../shapes/ShapeUtil'
import { FrameShapeUtil } from '../../../shapes/frame/FrameShapeUtil'
import { GroupShapeUtil } from '../../../shapes/group/GroupShapeUtil'
import { TLEventHandlers } from '../../../types/event-types'
import { StateNode } from '../../StateNode'
@ -104,9 +106,9 @@ export class ScribbleBrushing extends StateNode {
util = this.editor.getShapeUtil(shape)
if (
shape.type === 'group' ||
this.editor.isShapeOfType(shape, GroupShapeUtil) ||
this.newlySelectedIds.has(shape.id) ||
(shape.type === 'frame' &&
(this.editor.isShapeOfType(shape, FrameShapeUtil) &&
util.hitTestPoint(shape, this.editor.getPointInShapeSpace(shape, originPagePoint))) ||
this.editor.isShapeOrAncestorLocked(shape)
) {

View file

@ -1,4 +1,5 @@
import { TLArrowShape, TLShapePartial, createShapeId } from '@tldraw/tlschema'
import { ArrowShapeUtil } from '../editor/shapes/arrow/ArrowShapeUtil'
import { TestEditor } from './TestEditor'
let editor: TestEditor
@ -186,7 +187,11 @@ describe('When duplicating shapes that include arrows', () => {
.selectAll()
.deleteShapes()
.createShapes(shapes)
.select(...editor.shapesArray.filter((s) => s.type === 'arrow').map((s) => s.id))
.select(
...editor.shapesArray
.filter((s) => editor.isShapeOfType(s, ArrowShapeUtil))
.map((s) => s.id)
)
const boundsBefore = editor.selectionBounds!
editor.duplicateShapes()

View file

@ -6,6 +6,7 @@ import { TestEditor } from '../TestEditor'
import { defaultShapes } from '../../config/defaultShapes'
import { defineShape } from '../../config/defineShape'
import { ArrowShapeUtil } from '../../editor/shapes/arrow/ArrowShapeUtil'
import { getSnapLines } from '../testutils/getSnapLines'
type __TopLeftSnapOnlyShape = any
@ -1948,7 +1949,9 @@ describe('translating a shape with a bound shape', () => {
props: { start: { type: 'binding' }, end: { type: 'binding' } },
})
const newArrow = editor.shapesArray.find((s) => s.type === 'arrow' && s.id !== arrow1)
const newArrow = editor.shapesArray.find(
(s) => editor.isShapeOfType(s, ArrowShapeUtil) && s.id !== arrow1
)
expect(newArrow).toMatchObject({
props: { start: { type: 'binding' }, end: { type: 'point' } },
})

View file

@ -1,4 +1,3 @@
import { TLGeoShape, TLNoteShape, TLShape } from '@tldraw/tlschema'
import { debugFlags } from './debug-flags'
import { getBrowserCanvasMaxSize } from './getBrowserCanvasMaxSize'
import { setPhysChunk } from './png'
@ -170,13 +169,3 @@ export function getTextBoundingBox(text: SVGTextElement) {
return bbox
}
/** @public */
export function isGeoShape(shape: TLShape): shape is TLGeoShape {
return shape.type === 'geo'
}
/** @public */
export function isNoteShape(shape: TLShape): shape is TLNoteShape {
return shape.type === 'note'
}

View file

@ -5,6 +5,7 @@ import {
DEFAULT_BOOKMARK_WIDTH,
Editor,
EmbedShapeUtil,
GroupShapeUtil,
TLEmbedShape,
TLShapeId,
TLShapePartial,
@ -388,7 +389,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
readonlyOk: false,
onSelect(source) {
trackEvent('group-shapes', { source })
if (editor.selectedShapes.length === 1 && editor.selectedShapes[0].type === 'group') {
const { onlySelectedShape } = editor
if (onlySelectedShape && editor.isShapeOfType(onlySelectedShape, GroupShapeUtil)) {
editor.mark('ungroup')
editor.ungroupShapes(editor.selectedIds)
} else {

View file

@ -1,4 +1,10 @@
import { useEditor } from '@tldraw/editor'
import {
ArrowShapeUtil,
DrawShapeUtil,
GroupShapeUtil,
LineShapeUtil,
useEditor,
} from '@tldraw/editor'
import { useValue } from 'signia-react'
export function useOnlyFlippableShape() {
@ -11,10 +17,10 @@ export function useOnlyFlippableShape() {
selectedShapes.length === 1 &&
selectedShapes.every(
(shape) =>
shape.type === 'group' ||
shape.type === 'arrow' ||
shape.type === 'line' ||
shape.type === 'draw'
editor.isShapeOfType(shape, GroupShapeUtil) ||
editor.isShapeOfType(shape, ArrowShapeUtil) ||
editor.isShapeOfType(shape, LineShapeUtil) ||
editor.isShapeOfType(shape, DrawShapeUtil)
)
)
},