@tldraw/editor 3.14.0-canary.6f68b0394e8a → 3.14.0-canary.6fbbca54ff57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist-cjs/index.d.ts +82 -13
  2. package/dist-cjs/index.js +4 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/editor/Editor.js +35 -21
  5. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  6. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +2 -2
  7. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  8. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +0 -10
  9. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  10. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  11. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +6 -2
  12. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  13. package/dist-cjs/lib/utils/reparenting.js +232 -0
  14. package/dist-cjs/lib/utils/reparenting.js.map +7 -0
  15. package/dist-cjs/version.js +3 -3
  16. package/dist-cjs/version.js.map +1 -1
  17. package/dist-esm/index.d.mts +82 -13
  18. package/dist-esm/index.mjs +4 -1
  19. package/dist-esm/index.mjs.map +2 -2
  20. package/dist-esm/lib/editor/Editor.mjs +35 -21
  21. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  22. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -2
  23. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  24. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +0 -10
  25. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  26. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +6 -2
  27. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  28. package/dist-esm/lib/utils/reparenting.mjs +216 -0
  29. package/dist-esm/lib/utils/reparenting.mjs.map +7 -0
  30. package/dist-esm/version.mjs +3 -3
  31. package/dist-esm/version.mjs.map +1 -1
  32. package/package.json +7 -7
  33. package/src/index.ts +5 -0
  34. package/src/lib/editor/Editor.ts +45 -29
  35. package/src/lib/editor/managers/TextManager/TextManager.ts +3 -2
  36. package/src/lib/editor/shapes/ShapeUtil.ts +47 -15
  37. package/src/lib/editor/types/emit-types.ts +4 -0
  38. package/src/lib/primitives/geometry/Geometry2d.ts +7 -2
  39. package/src/lib/utils/reparenting.ts +383 -0
  40. package/src/version.ts +3 -3
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/utils/reparenting.ts"],
4
+ "sourcesContent": ["import { EMPTY_ARRAY } from '@tldraw/state'\nimport { TLGroupShape, TLParentId, TLShape, TLShapeId } from '@tldraw/tlschema'\nimport { IndexKey, compact, getIndexAbove, getIndexBetween } from '@tldraw/utils'\nimport { Editor } from '../editor/Editor'\nimport { Vec } from '../primitives/Vec'\nimport { Geometry2d } from '../primitives/geometry/Geometry2d'\nimport { Group2d } from '../primitives/geometry/Group2d'\nimport {\n\tintersectPolygonPolygon,\n\tpolygonIntersectsPolyline,\n\tpolygonsIntersect,\n} from '../primitives/intersect'\nimport { pointInPolygon } from '../primitives/utils'\n\n/**\n * Reparents shapes that are no longer contained within their parent shapes.\n * todo: rename me to something more descriptive, like `reparentOccludedShapes` or `reparentAutoDroppedShapes`\n *\n * @param editor - The editor instance.\n * @param shapeIds - The IDs of the shapes to reparent.\n * @param opts - Optional options, including a callback to filter out certain parents, such as when removing a frame.\n *\n * @public\n */\nexport function kickoutOccludedShapes(\n\teditor: Editor,\n\tshapeIds: TLShapeId[],\n\topts?: { filter?(parent: TLShape): boolean }\n) {\n\tconst parentsToCheck = new Set<TLShape>()\n\n\tfor (const id of shapeIds) {\n\t\tconst shape = editor.getShape(id)\n\n\t\tif (!shape) continue\n\t\tparentsToCheck.add(shape)\n\n\t\tconst parent = editor.getShape(shape.parentId)\n\t\tif (!parent) continue\n\t\tparentsToCheck.add(parent)\n\t}\n\n\t// Check all of the parents and gather up parents who have lost children\n\tconst parentsToLostChildren = new Map<TLShape, TLShapeId[]>()\n\n\tfor (const parent of parentsToCheck) {\n\t\tconst childIds = editor.getSortedChildIdsForParent(parent)\n\t\tif (opts?.filter && !opts.filter(parent)) {\n\t\t\t// If the shape is filtered out, we kick out all of its children\n\t\t\tparentsToLostChildren.set(parent, childIds)\n\t\t} else {\n\t\t\tconst overlappingChildren = getOverlappingShapes(editor, parent.id, childIds)\n\t\t\tif (overlappingChildren.length < childIds.length) {\n\t\t\t\tparentsToLostChildren.set(\n\t\t\t\t\tparent,\n\t\t\t\t\tchildIds.filter((id) => !overlappingChildren.includes(id))\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Get all of the shapes on the current page, sorted by their index\n\tconst sortedShapeIds = editor.getCurrentPageShapesSorted().map((s) => s.id)\n\n\tconst parentsToNewChildren: Record<\n\t\tTLParentId,\n\t\t{ parentId: TLParentId; shapeIds: TLShapeId[]; index?: IndexKey }\n\t> = {}\n\n\tfor (const [prevParent, lostChildrenIds] of parentsToLostChildren) {\n\t\tconst lostChildren = compact(lostChildrenIds.map((id) => editor.getShape(id)))\n\n\t\t// Don't fall \"up\" into frames in front of the shape\n\t\t// if (pageShapes.indexOf(shape) < frameSortPosition) continue shapeCheck\n\n\t\t// Otherwise, we have no next dropping shape under the cursor, so go find\n\t\t// all the frames on the page where the moving shapes will fall into\n\t\tconst { reparenting, remainingShapesToReparent } = getDroppedShapesToNewParents(\n\t\t\teditor,\n\t\t\tlostChildren,\n\t\t\t(shape, maybeNewParent) => {\n\t\t\t\t// If we're filtering out a potential parent, don't reparent shapes to the filtered out shape\n\t\t\t\tif (opts?.filter && !opts.filter(maybeNewParent)) return false\n\t\t\t\treturn (\n\t\t\t\t\tmaybeNewParent.id !== prevParent.id &&\n\t\t\t\t\tsortedShapeIds.indexOf(maybeNewParent.id) < sortedShapeIds.indexOf(shape.id)\n\t\t\t\t)\n\t\t\t}\n\t\t)\n\n\t\treparenting.forEach((childrenToReparent, newParentId) => {\n\t\t\tif (childrenToReparent.length === 0) return\n\t\t\tif (!parentsToNewChildren[newParentId]) {\n\t\t\t\tparentsToNewChildren[newParentId] = {\n\t\t\t\t\tparentId: newParentId,\n\t\t\t\t\tshapeIds: [],\n\t\t\t\t}\n\t\t\t}\n\t\t\tparentsToNewChildren[newParentId].shapeIds.push(...childrenToReparent.map((s) => s.id))\n\t\t})\n\n\t\t// Reparent the rest to the page (or containing group)\n\t\tif (remainingShapesToReparent.size > 0) {\n\t\t\t// The remaining shapes are going to be reparented to the old parent's containing group, if there was one, or else to the page\n\t\t\tconst newParentId =\n\t\t\t\teditor.findShapeAncestor(prevParent, (s) => editor.isShapeOfType<TLGroupShape>(s, 'group'))\n\t\t\t\t\t?.id ?? editor.getCurrentPageId()\n\n\t\t\tremainingShapesToReparent.forEach((shape) => {\n\t\t\t\tif (!parentsToNewChildren[newParentId]) {\n\t\t\t\t\tlet insertIndexKey: IndexKey | undefined\n\n\t\t\t\t\tconst oldParentSiblingIds = editor.getSortedChildIdsForParent(newParentId)\n\t\t\t\t\tconst oldParentIndex = oldParentSiblingIds.indexOf(prevParent.id)\n\t\t\t\t\tif (oldParentIndex > -1) {\n\t\t\t\t\t\t// If the old parent is a direct child of the new parent, then we'll add them above the old parent but below the next sibling.\n\t\t\t\t\t\tconst siblingsIndexAbove = oldParentSiblingIds[oldParentIndex + 1]\n\t\t\t\t\t\tconst indexKeyAbove = siblingsIndexAbove\n\t\t\t\t\t\t\t? editor.getShape(siblingsIndexAbove)!.index\n\t\t\t\t\t\t\t: getIndexAbove(prevParent.index)\n\t\t\t\t\t\tinsertIndexKey = getIndexBetween(prevParent.index, indexKeyAbove)\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// If the old parent is not a direct child of the new parent, then we'll add them to the \"top\" of the new parent's children.\n\t\t\t\t\t\t// This is done automatically if we leave the index undefined, so let's do that.\n\t\t\t\t\t}\n\n\t\t\t\t\tparentsToNewChildren[newParentId] = {\n\t\t\t\t\t\tparentId: newParentId,\n\t\t\t\t\t\tshapeIds: [],\n\t\t\t\t\t\tindex: insertIndexKey,\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tparentsToNewChildren[newParentId].shapeIds.push(shape.id)\n\t\t\t})\n\t\t}\n\t}\n\n\teditor.run(() => {\n\t\tObject.values(parentsToNewChildren).forEach(({ parentId, shapeIds, index }) => {\n\t\t\tif (shapeIds.length === 0) return\n\t\t\t// Before we reparent, sort the new shape ids by their place in the original absolute order on the page\n\t\t\tshapeIds.sort((a, b) => (sortedShapeIds.indexOf(a) < sortedShapeIds.indexOf(b) ? -1 : 1))\n\t\t\teditor.reparentShapes(shapeIds, parentId, index)\n\t\t})\n\t})\n}\n\n/**\n * Get the shapes that overlap with a given shape.\n *\n * @param editor - The editor instance.\n * @param shape - The shapes or shape IDs to check against.\n * @param otherShapes - The shapes or shape IDs to check for overlap.\n * @returns An array of shapes or shape IDs that overlap with the given shape.\n */\nfunction getOverlappingShapes<T extends TLShape[] | TLShapeId[]>(\n\teditor: Editor,\n\tshape: T[number],\n\totherShapes: T\n) {\n\tif (otherShapes.length === 0) {\n\t\treturn EMPTY_ARRAY\n\t}\n\n\tconst parentPageBounds = editor.getShapePageBounds(shape)\n\tif (!parentPageBounds) return EMPTY_ARRAY\n\n\tconst parentGeometry = editor.getShapeGeometry(shape)\n\tconst parentPageTransform = editor.getShapePageTransform(shape)\n\tconst parentPageCorners = parentPageTransform.applyToPoints(parentGeometry.vertices)\n\n\tconst parentPageMaskVertices = editor.getShapeMask(shape)\n\tconst parentPagePolygon = parentPageMaskVertices\n\t\t? intersectPolygonPolygon(parentPageMaskVertices, parentPageCorners)\n\t\t: parentPageCorners\n\n\tif (!parentPagePolygon) return EMPTY_ARRAY\n\n\treturn otherShapes.filter((childId) => {\n\t\tconst shapePageBounds = editor.getShapePageBounds(childId)\n\t\tif (!shapePageBounds || !parentPageBounds.includes(shapePageBounds)) return false\n\n\t\tconst parentPolygonInShapeShape = editor\n\t\t\t.getShapePageTransform(childId)\n\t\t\t.clone()\n\t\t\t.invert()\n\t\t\t.applyToPoints(parentPagePolygon)\n\n\t\tconst geometry = editor.getShapeGeometry(childId)\n\n\t\treturn doesGeometryOverlapPolygon(geometry, parentPolygonInShapeShape)\n\t})\n}\n\n/**\n * @public\n */\nexport function doesGeometryOverlapPolygon(\n\tgeometry: Geometry2d,\n\tparentCornersInShapeSpace: Vec[]\n): boolean {\n\t// If the child is a group, check if any of its children overlap the box\n\tif (geometry instanceof Group2d) {\n\t\treturn geometry.children.some((childGeometry) =>\n\t\t\tdoesGeometryOverlapPolygon(childGeometry, parentCornersInShapeSpace)\n\t\t)\n\t}\n\n\t// Otherwise, check if the geometry overlaps the box\n\tconst { vertices, center, isFilled, isEmptyLabel, isClosed } = geometry\n\n\t// We'll do things in order of cheapest to most expensive checks\n\n\t// Skip empty labels\n\tif (isEmptyLabel) return false\n\n\t// If any of the shape's vertices are inside the occluder, it's inside\n\tif (vertices.some((v) => pointInPolygon(v, parentCornersInShapeSpace))) {\n\t\treturn true\n\t}\n\n\t// If the shape is filled and closed and its center is inside the parent, it's inside\n\tif (isClosed) {\n\t\tif (isFilled) {\n\t\t\t// If closed and filled, check if the center is inside the parent\n\t\t\tif (pointInPolygon(center, parentCornersInShapeSpace)) {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\t// ..then, slightly more expensive check, see the shape covers the entire parent but not its center\n\t\t\tif (parentCornersInShapeSpace.every((v) => pointInPolygon(v, vertices))) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\t// If any the shape's vertices intersect the edge of the occluder, it's inside.\n\t\t// for example when a rotated rectangle is moved over the corner of a parent rectangle\n\t\t// If the child shape is closed, intersect as a polygon\n\t\tif (polygonsIntersect(parentCornersInShapeSpace, vertices)) {\n\t\t\treturn true\n\t\t}\n\t} else {\n\t\t// if the child shape is not closed, intersect as a polyline\n\t\tif (polygonIntersectsPolyline(parentCornersInShapeSpace, vertices)) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// If none of the above checks passed, the shape is outside the parent\n\treturn false\n}\n\n/**\n * Get the shapes that will be reparented to new parents when the shapes are dropped.\n *\n * @param editor - The editor instance.\n * @param shapes - The shapes to check.\n * @param cb - A callback to filter out certain shapes.\n * @returns An object with the shapes that will be reparented to new parents and the shapes that will be reparented to the page or their ancestral group.\n *\n * @public\n */\nexport function getDroppedShapesToNewParents(\n\teditor: Editor,\n\tshapes: Set<TLShape> | TLShape[],\n\tcb?: (shape: TLShape, parent: TLShape) => boolean\n) {\n\tconst shapesToActuallyCheck = new Set<TLShape>(shapes)\n\tconst movingGroups = new Set<TLGroupShape>()\n\n\tfor (const shape of shapes) {\n\t\tconst parent = editor.getShapeParent(shape)\n\t\tif (parent && editor.isShapeOfType<TLGroupShape>(parent, 'group')) {\n\t\t\tif (!movingGroups.has(parent)) {\n\t\t\t\tmovingGroups.add(parent)\n\t\t\t}\n\t\t}\n\t}\n\n\t// If all of a group's children are moving, then move the group instead\n\tfor (const movingGroup of movingGroups) {\n\t\tconst children = compact(\n\t\t\teditor.getSortedChildIdsForParent(movingGroup).map((id) => editor.getShape(id))\n\t\t)\n\t\tfor (const child of children) {\n\t\t\tshapesToActuallyCheck.delete(child)\n\t\t}\n\t\tshapesToActuallyCheck.add(movingGroup)\n\t}\n\n\t// this could be cached and passed in\n\tconst shapeGroupIds = new Map<TLShapeId, TLShapeId | undefined>()\n\n\tconst reparenting = new Map<TLShapeId, TLShape[]>()\n\n\tconst remainingShapesToReparent = new Set(shapesToActuallyCheck)\n\n\tconst potentialParentShapes = editor\n\t\t.getCurrentPageShapesSorted()\n\t\t// filter out any shapes that aren't frames or that are included among the provided shapes\n\t\t.filter(\n\t\t\t(s) =>\n\t\t\t\teditor.getShapeUtil(s).canReceiveNewChildrenOfType?.(s, s.type) &&\n\t\t\t\t!remainingShapesToReparent.has(s)\n\t\t)\n\n\tparentCheck: for (let i = potentialParentShapes.length - 1; i >= 0; i--) {\n\t\tconst parentShape = potentialParentShapes[i]\n\t\tconst parentShapeContainingGroupId = editor.findShapeAncestor(parentShape, (s) =>\n\t\t\teditor.isShapeOfType<TLGroupShape>(s, 'group')\n\t\t)?.id\n\n\t\tconst parentGeometry = editor.getShapeGeometry(parentShape)\n\t\tconst parentPageTransform = editor.getShapePageTransform(parentShape)\n\t\tconst parentPageMaskVertices = editor.getShapeMask(parentShape)\n\t\tconst parentPageCorners = parentPageTransform.applyToPoints(parentGeometry.vertices)\n\t\tconst parentPagePolygon = parentPageMaskVertices\n\t\t\t? intersectPolygonPolygon(parentPageMaskVertices, parentPageCorners)\n\t\t\t: parentPageCorners\n\n\t\tif (!parentPagePolygon) continue parentCheck\n\n\t\tconst childrenToReparent = []\n\n\t\t// For each of the dropping shapes...\n\t\tshapeCheck: for (const shape of remainingShapesToReparent) {\n\t\t\t// Don't reparent a frame to itself\n\t\t\tif (parentShape.id === shape.id) continue shapeCheck\n\n\t\t\t// Use the callback to filter out certain shapes\n\t\t\tif (cb && !cb(shape, parentShape)) continue shapeCheck\n\n\t\t\tif (!shapeGroupIds.has(shape.id)) {\n\t\t\t\tshapeGroupIds.set(\n\t\t\t\t\tshape.id,\n\t\t\t\t\teditor.findShapeAncestor(shape, (s) => editor.isShapeOfType<TLGroupShape>(s, 'group'))?.id\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tconst shapeGroupId = shapeGroupIds.get(shape.id)\n\n\t\t\t// Are the shape and the parent part of different groups?\n\t\t\tif (shapeGroupId !== parentShapeContainingGroupId) continue shapeCheck\n\n\t\t\t// Is the shape is actually the ancestor of the parent?\n\t\t\tif (editor.findShapeAncestor(parentShape, (s) => shape.id === s.id)) continue shapeCheck\n\n\t\t\t// Convert the parent polygon to the shape's space\n\t\t\tconst parentPolygonInShapeSpace = editor\n\t\t\t\t.getShapePageTransform(shape)\n\t\t\t\t.clone()\n\t\t\t\t.invert()\n\t\t\t\t.applyToPoints(parentPagePolygon)\n\n\t\t\t// If the shape overlaps the parent polygon, reparent it to that parent\n\t\t\tif (doesGeometryOverlapPolygon(editor.getShapeGeometry(shape), parentPolygonInShapeSpace)) {\n\t\t\t\t// Use the util to check if the shape can be reparented to the parent\n\t\t\t\tif (\n\t\t\t\t\t!editor.getShapeUtil(parentShape).canReceiveNewChildrenOfType?.(parentShape, shape.type)\n\t\t\t\t)\n\t\t\t\t\tcontinue shapeCheck\n\n\t\t\t\tif (shape.parentId !== parentShape.id) {\n\t\t\t\t\tchildrenToReparent.push(shape)\n\t\t\t\t}\n\t\t\t\tremainingShapesToReparent.delete(shape)\n\t\t\t\tcontinue shapeCheck\n\t\t\t}\n\t\t}\n\n\t\tif (childrenToReparent.length) {\n\t\t\treparenting.set(parentShape.id, childrenToReparent)\n\t\t}\n\t}\n\n\treturn {\n\t\t// these are the shapes that will be reparented to new parents\n\t\treparenting,\n\t\t// these are the shapes that will be reparented to the page or their ancestral group\n\t\tremainingShapesToReparent,\n\t}\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["shapeIds"]
7
+ }
@@ -1,8 +1,8 @@
1
- const version = "3.14.0-canary.6f68b0394e8a";
1
+ const version = "3.14.0-canary.6fbbca54ff57";
2
2
  const publishDates = {
3
3
  major: "2024-09-13T14:36:29.063Z",
4
- minor: "2025-06-23T08:33:08.428Z",
5
- patch: "2025-06-23T08:33:08.428Z"
4
+ minor: "2025-06-24T16:09:26.541Z",
5
+ patch: "2025-06-24T16:09:26.541Z"
6
6
  };
7
7
  export {
8
8
  publishDates,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/version.ts"],
4
- "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '3.14.0-canary.6f68b0394e8a'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-06-23T08:33:08.428Z',\n\tpatch: '2025-06-23T08:33:08.428Z',\n}\n"],
4
+ "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '3.14.0-canary.6fbbca54ff57'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-06-24T16:09:26.541Z',\n\tpatch: '2025-06-24T16:09:26.541Z',\n}\n"],
5
5
  "mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "A tiny little drawing app (editor).",
4
- "version": "3.14.0-canary.6f68b0394e8a",
4
+ "version": "3.14.0-canary.6fbbca54ff57",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -48,12 +48,12 @@
48
48
  "@tiptap/core": "^2.9.1",
49
49
  "@tiptap/pm": "^2.9.1",
50
50
  "@tiptap/react": "^2.9.1",
51
- "@tldraw/state": "3.14.0-canary.6f68b0394e8a",
52
- "@tldraw/state-react": "3.14.0-canary.6f68b0394e8a",
53
- "@tldraw/store": "3.14.0-canary.6f68b0394e8a",
54
- "@tldraw/tlschema": "3.14.0-canary.6f68b0394e8a",
55
- "@tldraw/utils": "3.14.0-canary.6f68b0394e8a",
56
- "@tldraw/validate": "3.14.0-canary.6f68b0394e8a",
51
+ "@tldraw/state": "3.14.0-canary.6fbbca54ff57",
52
+ "@tldraw/state-react": "3.14.0-canary.6fbbca54ff57",
53
+ "@tldraw/store": "3.14.0-canary.6fbbca54ff57",
54
+ "@tldraw/tlschema": "3.14.0-canary.6fbbca54ff57",
55
+ "@tldraw/utils": "3.14.0-canary.6fbbca54ff57",
56
+ "@tldraw/validate": "3.14.0-canary.6fbbca54ff57",
57
57
  "@types/core-js": "^2.5.8",
58
58
  "@use-gesture/react": "^10.3.1",
59
59
  "classnames": "^2.5.1",
package/src/index.ts CHANGED
@@ -182,6 +182,10 @@ export { BaseBoxShapeUtil, type TLBaseBoxShape } from './lib/editor/shapes/BaseB
182
182
  export {
183
183
  ShapeUtil,
184
184
  type TLCropInfo,
185
+ type TLDragShapesInInfo,
186
+ type TLDragShapesOutInfo,
187
+ type TLDragShapesOverInfo,
188
+ type TLDropShapesOverInfo,
185
189
  type TLGeometryOpts,
186
190
  type TLHandleDragInfo,
187
191
  type TLResizeInfo,
@@ -446,6 +450,7 @@ export { hardResetEditor } from './lib/utils/hardResetEditor'
446
450
  export { isAccelKey } from './lib/utils/keyboard'
447
451
  export { normalizeWheel } from './lib/utils/normalizeWheel'
448
452
  export { refreshPage } from './lib/utils/refreshPage'
453
+ export { getDroppedShapesToNewParents, kickoutOccludedShapes } from './lib/utils/reparenting'
449
454
  export {
450
455
  getFontsFromRichText,
451
456
  type RichTextFontVisitor,
@@ -5528,7 +5528,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5528
5528
  if (!id) return undefined
5529
5529
  const freshShape = this.getShape(id)
5530
5530
  if (freshShape === undefined || !isShapeId(freshShape.parentId)) return undefined
5531
- return this.store.get(freshShape.parentId)
5531
+ return this.getShape(freshShape.parentId)
5532
5532
  }
5533
5533
 
5534
5534
  /**
@@ -5711,6 +5711,10 @@ export class Editor extends EventEmitter<TLEventMap> {
5711
5711
  const newPoint = invertedParentTransform.applyToPoint(pagePoint)
5712
5712
  const newRotation = pageTransform.rotation() - parentPageRotation
5713
5713
 
5714
+ if (shape.id === parentId) {
5715
+ throw Error('Attempted to reparent a shape to itself!')
5716
+ }
5717
+
5714
5718
  changes.push({
5715
5719
  id: shape.id,
5716
5720
  type: shape.type,
@@ -5814,6 +5818,11 @@ export class Editor extends EventEmitter<TLEventMap> {
5814
5818
  return shapeIds
5815
5819
  }
5816
5820
 
5821
+ /** @deprecated Use {@link Editor.getDraggingOverShape} instead */
5822
+ getDroppingOverShape(point: Vec, droppingShapes: TLShape[]): TLShape | undefined {
5823
+ return this.getDraggingOverShape(point, droppingShapes)
5824
+ }
5825
+
5817
5826
  /**
5818
5827
  * Get the shape that some shapes should be dropped on at a given point.
5819
5828
  *
@@ -5824,35 +5833,33 @@ export class Editor extends EventEmitter<TLEventMap> {
5824
5833
  *
5825
5834
  * @public
5826
5835
  */
5827
- getDroppingOverShape(point: VecLike, droppingShapes: TLShape[] = []) {
5828
- // starting from the top...
5829
- const currentPageShapesSorted = this.getCurrentPageShapesSorted()
5830
- for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
5831
- const shape = currentPageShapesSorted[i]
5832
-
5833
- if (
5834
- // ignore hidden shapes
5835
- this.isShapeHidden(shape) ||
5836
- // don't allow dropping on selected shapes
5837
- this.getSelectedShapeIds().includes(shape.id) ||
5838
- // only allow shapes that can receive children
5839
- !this.getShapeUtil(shape).canDropShapes(shape, droppingShapes) ||
5840
- // don't allow dropping a shape on itself or one of it's children
5841
- droppingShapes.find((s) => s.id === shape.id || this.hasAncestor(shape, s.id))
5842
- ) {
5843
- continue
5844
- }
5836
+ getDraggingOverShape(point: Vec, droppingShapes: TLShape[]): TLShape | undefined {
5837
+ // get fresh moving shapes
5838
+ const draggingShapes = compact(droppingShapes.map((s) => this.getShape(s))).filter(
5839
+ (s) => !s.isLocked && !this.isShapeHidden(s)
5840
+ )
5845
5841
 
5846
- // Only allow dropping into the masked page bounds of the shape, e.g. when a frame is
5847
- // partially clipped by its own parent frame
5848
- const maskedPageBounds = this.getShapeMaskedPageBounds(shape.id)
5842
+ const maybeDraggingOverShapes = this.getShapesAtPoint(point, {
5843
+ hitInside: true,
5844
+ margin: 0,
5845
+ }).filter(
5846
+ (s) =>
5847
+ !droppingShapes.includes(s) &&
5848
+ !s.isLocked &&
5849
+ !this.isShapeHidden(s) &&
5850
+ !draggingShapes.includes(s)
5851
+ )
5849
5852
 
5853
+ for (const maybeDraggingOverShape of maybeDraggingOverShapes) {
5854
+ const shapeUtil = this.getShapeUtil(maybeDraggingOverShape)
5855
+ // Any shape that can handle any dragging interactions is a valid target
5850
5856
  if (
5851
- maskedPageBounds &&
5852
- maskedPageBounds.containsPoint(point) &&
5853
- this.getShapeGeometry(shape).hitTestPoint(this.getPointInShapeSpace(shape, point), 0, true)
5857
+ shapeUtil.onDragShapesOver ||
5858
+ shapeUtil.onDragShapesIn ||
5859
+ shapeUtil.onDragShapesOut ||
5860
+ shapeUtil.onDropShapesOver
5854
5861
  ) {
5855
- return shape
5862
+ return maybeDraggingOverShape
5856
5863
  }
5857
5864
  }
5858
5865
  }
@@ -7806,9 +7813,10 @@ export class Editor extends EventEmitter<TLEventMap> {
7806
7813
 
7807
7814
  for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7808
7815
  const parent = currentPageShapesSorted[i]
7816
+ const util = this.getShapeUtil(parent)
7809
7817
  if (
7818
+ util.canReceiveNewChildrenOfType(parent, partial.type) &&
7810
7819
  !this.isShapeHidden(parent) &&
7811
- this.getShapeUtil(parent).canReceiveNewChildrenOfType(parent, partial.type) &&
7812
7820
  this.isPointInShape(
7813
7821
  parent,
7814
7822
  // If no parent is provided, then we can treat the
@@ -7937,6 +7945,8 @@ export class Editor extends EventEmitter<TLEventMap> {
7937
7945
  }
7938
7946
  })
7939
7947
 
7948
+ this.emit('created-shapes', shapeRecordsToCreate)
7949
+ this.emit('edit')
7940
7950
  this.store.put(shapeRecordsToCreate)
7941
7951
  })
7942
7952
 
@@ -8331,6 +8341,8 @@ export class Editor extends EventEmitter<TLEventMap> {
8331
8341
  updates.push(updated)
8332
8342
  }
8333
8343
 
8344
+ this.emit('edited-shapes', updates)
8345
+ this.emit('edit')
8334
8346
  this.store.put(updates)
8335
8347
  })
8336
8348
  }
@@ -8380,6 +8392,8 @@ export class Editor extends EventEmitter<TLEventMap> {
8380
8392
  })
8381
8393
  }
8382
8394
 
8395
+ this.emit('deleted-shapes', [...allShapeIdsToDelete])
8396
+ this.emit('edit')
8383
8397
  return this.run(() => this.store.remove([...allShapeIdsToDelete]))
8384
8398
  }
8385
8399
 
@@ -9506,6 +9520,8 @@ export class Editor extends EventEmitter<TLEventMap> {
9506
9520
  previousPagePoint,
9507
9521
  currentScreenPoint,
9508
9522
  currentPagePoint,
9523
+ originScreenPoint,
9524
+ originPagePoint,
9509
9525
  } = this.inputs
9510
9526
 
9511
9527
  const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
@@ -9534,8 +9550,8 @@ export class Editor extends EventEmitter<TLEventMap> {
9534
9550
  // Reset velocity on pointer down, or when a pinch starts or ends
9535
9551
  if (info.name === 'pointer_down' || this.inputs.isPinching) {
9536
9552
  pointerVelocity.set(0, 0)
9537
- this.inputs.originScreenPoint.setTo(currentScreenPoint)
9538
- this.inputs.originPagePoint.setTo(currentPagePoint)
9553
+ originScreenPoint.setTo(currentScreenPoint)
9554
+ originPagePoint.setTo(currentPagePoint)
9539
9555
  }
9540
9556
 
9541
9557
  // todo: We only have to do this if there are multiple users in the document
@@ -26,6 +26,7 @@ export interface TLMeasureTextOpts {
26
26
  fontWeight: string
27
27
  fontFamily: string
28
28
  fontSize: number
29
+ /** This must be a number, e.g. 1.35, not a pixel value. */
29
30
  lineHeight: number
30
31
  /**
31
32
  * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
@@ -124,7 +125,7 @@ export class TextManager {
124
125
  elm.style.setProperty('font-style', opts.fontStyle)
125
126
  elm.style.setProperty('font-weight', opts.fontWeight)
126
127
  elm.style.setProperty('font-size', opts.fontSize + 'px')
127
- elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
128
+ elm.style.setProperty('line-height', opts.lineHeight.toString())
128
129
  elm.style.setProperty('padding', opts.padding)
129
130
 
130
131
  if (opts.maxWidth) {
@@ -288,7 +289,7 @@ export class TextManager {
288
289
  elm.style.setProperty('font-style', opts.fontStyle)
289
290
  elm.style.setProperty('font-weight', opts.fontWeight)
290
291
  elm.style.setProperty('font-size', opts.fontSize + 'px')
291
- elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
292
+ elm.style.setProperty('line-height', opts.lineHeight.toString())
292
293
 
293
294
  const elementWidth = Math.ceil(opts.width - opts.padding * 2)
294
295
  elm.style.setProperty('width', `${elementWidth}px`)
@@ -4,12 +4,15 @@ import { LegacyMigrations, MigrationSequence } from '@tldraw/store'
4
4
  import {
5
5
  RecordProps,
6
6
  TLHandle,
7
+ TLParentId,
7
8
  TLPropsMigrations,
8
9
  TLShape,
9
10
  TLShapeCrop,
11
+ TLShapeId,
10
12
  TLShapePartial,
11
13
  TLUnknownShape,
12
14
  } from '@tldraw/tlschema'
15
+ import { IndexKey } from '@tldraw/utils'
13
16
  import { ReactElement } from 'react'
14
17
  import { Box, SelectionHandle } from '../../primitives/Box'
15
18
  import { Vec } from '../../primitives/Vec'
@@ -387,17 +390,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
387
390
  return false
388
391
  }
389
392
 
390
- /**
391
- * Get whether the shape can receive children of a given type.
392
- *
393
- * @param shape - The shape type.
394
- * @param shapes - The shapes that are being dropped.
395
- * @public
396
- */
397
- canDropShapes(_shape: Shape, _shapes: TLShape[]) {
398
- return false
399
- }
400
-
401
393
  /**
402
394
  * Get the shape as an SVG object.
403
395
  *
@@ -517,7 +509,16 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
517
509
  ): Omit<TLShapePartial<Shape>, 'id' | 'type'> | undefined | void
518
510
 
519
511
  /**
520
- * A callback called when some other shapes are dragged over this one.
512
+ * A callback called when some other shapes are dragged into this one. This fires when the shapes are dragged over the shape for the first time.
513
+ *
514
+ * @param shape - The shape.
515
+ * @param shapes - The shapes that are being dragged in.
516
+ * @public
517
+ */
518
+ onDragShapesIn?(shape: Shape, shapes: TLShape[], info: TLDragShapesInInfo): void
519
+
520
+ /**
521
+ * A callback called when some other shapes are dragged over this one. This fires when the shapes are dragged over the shape for the first time (after the onDragShapesIn callback), and again on every update while the shapes are being dragged.
521
522
  *
522
523
  * @example
523
524
  *
@@ -531,7 +532,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
531
532
  * @param shapes - The shapes that are being dragged over this one.
532
533
  * @public
533
534
  */
534
- onDragShapesOver?(shape: Shape, shapes: TLShape[]): void
535
+ onDragShapesOver?(shape: Shape, shapes: TLShape[], info: TLDragShapesOverInfo): void
535
536
 
536
537
  /**
537
538
  * A callback called when some other shapes are dragged out of this one.
@@ -540,7 +541,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
540
541
  * @param shapes - The shapes that are being dragged out.
541
542
  * @public
542
543
  */
543
- onDragShapesOut?(shape: Shape, shapes: TLShape[]): void
544
+ onDragShapesOut?(shape: Shape, shapes: TLShape[], info: TLDragShapesOutInfo): void
544
545
 
545
546
  /**
546
547
  * A callback called when some other shapes are dropped over this one.
@@ -549,7 +550,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
549
550
  * @param shapes - The shapes that are being dropped over this one.
550
551
  * @public
551
552
  */
552
- onDropShapesOver?(shape: Shape, shapes: TLShape[]): void
553
+ onDropShapesOver?(shape: Shape, shapes: TLShape[], info: TLDropShapesOverInfo): void
553
554
 
554
555
  /**
555
556
  * A callback called when a shape starts being resized.
@@ -745,6 +746,37 @@ export interface TLCropInfo<T extends TLShape> {
745
746
  crop: TLShapeCrop
746
747
  uncroppedSize: { w: number; h: number }
747
748
  initialShape: T
749
+ aspectRatioLocked?: boolean
750
+ }
751
+
752
+ /** @public */
753
+ export interface TLDragShapesInInfo {
754
+ initialDraggingOverShapeId: TLShapeId | null
755
+ prevDraggingOverShapeId: TLShapeId | null
756
+ initialParentIds: Map<TLShapeId, TLParentId>
757
+ initialIndices: Map<TLShapeId, IndexKey>
758
+ }
759
+
760
+ /** @public */
761
+ export interface TLDragShapesOverInfo {
762
+ initialDraggingOverShapeId: TLShapeId | null
763
+ initialParentIds: Map<TLShapeId, TLParentId>
764
+ initialIndices: Map<TLShapeId, IndexKey>
765
+ }
766
+
767
+ /** @public */
768
+ export interface TLDragShapesOutInfo {
769
+ nextDraggingOverShapeId: TLShapeId | null
770
+ initialDraggingOverShapeId: TLShapeId | null
771
+ initialParentIds: Map<TLShapeId, TLParentId>
772
+ initialIndices: Map<TLShapeId, IndexKey>
773
+ }
774
+
775
+ /** @public */
776
+ export interface TLDropShapesOverInfo {
777
+ initialDraggingOverShapeId: TLShapeId | null
778
+ initialParentIds: Map<TLShapeId, TLParentId>
779
+ initialIndices: Map<TLShapeId, IndexKey>
748
780
  }
749
781
 
750
782
  /**
@@ -18,6 +18,10 @@ export interface TLEventMap {
18
18
  frame: [number]
19
19
  'select-all-text': [{ shapeId: TLShapeId }]
20
20
  'place-caret': [{ shapeId: TLShapeId; point: { x: number; y: number } }]
21
+ 'created-shapes': [TLRecord[]]
22
+ 'edited-shapes': [TLRecord[]]
23
+ 'deleted-shapes': [TLShapeId[]]
24
+ edit: []
21
25
  }
22
26
 
23
27
  /** @public */
@@ -44,6 +44,7 @@ export const Geometry2dFilters: {
44
44
  /** @public */
45
45
  export interface TransformedGeometry2dOptions {
46
46
  isLabel?: boolean
47
+ isEmptyLabel?: boolean
47
48
  isInternal?: boolean
48
49
  debugColor?: string
49
50
  ignore?: boolean
@@ -57,20 +58,24 @@ export interface Geometry2dOptions extends TransformedGeometry2dOptions {
57
58
 
58
59
  /** @public */
59
60
  export abstract class Geometry2d {
61
+ // todo: consider making accessors for these too, so that they can be overridden in subclasses by geometries with more complex logic
60
62
  isFilled = false
61
63
  isClosed = true
62
64
  isLabel = false
65
+ isEmptyLabel = false
63
66
  isInternal = false
64
67
  debugColor?: string
65
68
  ignore?: boolean
66
69
 
67
70
  constructor(opts: Geometry2dOptions) {
71
+ const { isLabel = false, isEmptyLabel = false, isInternal = false } = opts
68
72
  this.isFilled = opts.isFilled
69
73
  this.isClosed = opts.isClosed
70
- this.isLabel = opts.isLabel ?? false
71
- this.isInternal = opts.isInternal ?? false
72
74
  this.debugColor = opts.debugColor
73
75
  this.ignore = opts.ignore
76
+ this.isLabel = isLabel
77
+ this.isEmptyLabel = isEmptyLabel
78
+ this.isInternal = isInternal
74
79
  }
75
80
 
76
81
  isExcludedByFilter(filters?: Geometry2dFilters) {