UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

217 lines (216 loc) • 9.28 kB
import { EMPTY_ARRAY } from "@tldraw/state"; import { compact, getIndexAbove, getIndexBetween } from "@tldraw/utils"; import { Group2d } from "../primitives/geometry/Group2d.mjs"; import { intersectPolygonPolygon, polygonIntersectsPolyline, polygonsIntersect } from "../primitives/intersect.mjs"; import { pointInPolygon } from "../primitives/utils.mjs"; function kickoutOccludedShapes(editor, shapeIds, opts) { const parentsToCheck = /* @__PURE__ */ new Set(); for (const id of shapeIds) { const shape = editor.getShape(id); if (!shape) continue; parentsToCheck.add(shape); const parent = editor.getShape(shape.parentId); if (!parent) continue; parentsToCheck.add(parent); } const parentsToLostChildren = /* @__PURE__ */ new Map(); for (const parent of parentsToCheck) { const childIds = editor.getSortedChildIdsForParent(parent); if (opts?.filter && !opts.filter(parent)) { parentsToLostChildren.set(parent, childIds); } else { const overlappingChildren = getOverlappingShapes(editor, parent.id, childIds); if (overlappingChildren.length < childIds.length) { parentsToLostChildren.set( parent, childIds.filter((id) => !overlappingChildren.includes(id)) ); } } } const sortedShapeIds = editor.getCurrentPageShapesSorted().map((s) => s.id); const parentsToNewChildren = {}; for (const [prevParent, lostChildrenIds] of parentsToLostChildren) { const lostChildren = compact(lostChildrenIds.map((id) => editor.getShape(id))); const { reparenting, remainingShapesToReparent } = getDroppedShapesToNewParents( editor, lostChildren, (shape, maybeNewParent) => { if (opts?.filter && !opts.filter(maybeNewParent)) return false; return maybeNewParent.id !== prevParent.id && sortedShapeIds.indexOf(maybeNewParent.id) < sortedShapeIds.indexOf(shape.id); } ); reparenting.forEach((childrenToReparent, newParentId) => { if (childrenToReparent.length === 0) return; if (!parentsToNewChildren[newParentId]) { parentsToNewChildren[newParentId] = { parentId: newParentId, shapeIds: [] }; } parentsToNewChildren[newParentId].shapeIds.push(...childrenToReparent.map((s) => s.id)); }); if (remainingShapesToReparent.size > 0) { const newParentId = editor.findShapeAncestor(prevParent, (s) => editor.isShapeOfType(s, "group"))?.id ?? editor.getCurrentPageId(); remainingShapesToReparent.forEach((shape) => { if (!parentsToNewChildren[newParentId]) { let insertIndexKey; const oldParentSiblingIds = editor.getSortedChildIdsForParent(newParentId); const oldParentIndex = oldParentSiblingIds.indexOf(prevParent.id); if (oldParentIndex > -1) { const siblingsIndexAbove = oldParentSiblingIds[oldParentIndex + 1]; const indexKeyAbove = siblingsIndexAbove ? editor.getShape(siblingsIndexAbove).index : getIndexAbove(prevParent.index); insertIndexKey = getIndexBetween(prevParent.index, indexKeyAbove); } else { } parentsToNewChildren[newParentId] = { parentId: newParentId, shapeIds: [], index: insertIndexKey }; } parentsToNewChildren[newParentId].shapeIds.push(shape.id); }); } } editor.run(() => { Object.values(parentsToNewChildren).forEach(({ parentId, shapeIds: shapeIds2, index }) => { if (shapeIds2.length === 0) return; shapeIds2.sort((a, b) => sortedShapeIds.indexOf(a) < sortedShapeIds.indexOf(b) ? -1 : 1); editor.reparentShapes(shapeIds2, parentId, index); }); }); } function getOverlappingShapes(editor, shape, otherShapes) { if (otherShapes.length === 0) { return EMPTY_ARRAY; } const parentPageBounds = editor.getShapePageBounds(shape); if (!parentPageBounds) return EMPTY_ARRAY; const parentGeometry = editor.getShapeGeometry(shape); const parentPageTransform = editor.getShapePageTransform(shape); const parentPageCorners = parentPageTransform.applyToPoints(parentGeometry.vertices); const parentPageMaskVertices = editor.getShapeMask(shape); const parentPagePolygon = parentPageMaskVertices ? intersectPolygonPolygon(parentPageMaskVertices, parentPageCorners) : parentPageCorners; if (!parentPagePolygon) return EMPTY_ARRAY; return otherShapes.filter((childId) => { const shapePageBounds = editor.getShapePageBounds(childId); if (!shapePageBounds || !parentPageBounds.includes(shapePageBounds)) return false; const parentPolygonInShapeShape = editor.getShapePageTransform(childId).clone().invert().applyToPoints(parentPagePolygon); const geometry = editor.getShapeGeometry(childId); return doesGeometryOverlapPolygon(geometry, parentPolygonInShapeShape); }); } function doesGeometryOverlapPolygon(geometry, parentCornersInShapeSpace) { if (geometry instanceof Group2d) { return geometry.children.some( (childGeometry) => doesGeometryOverlapPolygon(childGeometry, parentCornersInShapeSpace) ); } const { vertices, center, isFilled, isEmptyLabel, isClosed } = geometry; if (isEmptyLabel) return false; if (vertices.some((v) => pointInPolygon(v, parentCornersInShapeSpace))) { return true; } if (isClosed) { if (isFilled) { if (pointInPolygon(center, parentCornersInShapeSpace)) { return true; } if (parentCornersInShapeSpace.every((v) => pointInPolygon(v, vertices))) { return true; } } if (polygonsIntersect(parentCornersInShapeSpace, vertices)) { return true; } } else { if (polygonIntersectsPolyline(parentCornersInShapeSpace, vertices)) { return true; } } return false; } function getDroppedShapesToNewParents(editor, shapes, cb) { const shapesToActuallyCheck = new Set(shapes); const movingGroups = /* @__PURE__ */ new Set(); for (const shape of shapes) { const parent = editor.getShapeParent(shape); if (parent && editor.isShapeOfType(parent, "group")) { if (!movingGroups.has(parent)) { movingGroups.add(parent); } } } for (const movingGroup of movingGroups) { const children = compact( editor.getSortedChildIdsForParent(movingGroup).map((id) => editor.getShape(id)) ); for (const child of children) { shapesToActuallyCheck.delete(child); } shapesToActuallyCheck.add(movingGroup); } const shapeGroupIds = /* @__PURE__ */ new Map(); const reparenting = /* @__PURE__ */ new Map(); const remainingShapesToReparent = new Set(shapesToActuallyCheck); const potentialParentShapes = editor.getCurrentPageShapesSorted().filter( (s) => editor.getShapeUtil(s).canReceiveNewChildrenOfType?.(s, s.type) && !remainingShapesToReparent.has(s) ); parentCheck: for (let i = potentialParentShapes.length - 1; i >= 0; i--) { const parentShape = potentialParentShapes[i]; const parentShapeContainingGroupId = editor.findShapeAncestor( parentShape, (s) => editor.isShapeOfType(s, "group") )?.id; const parentGeometry = editor.getShapeGeometry(parentShape); const parentPageTransform = editor.getShapePageTransform(parentShape); const parentPageMaskVertices = editor.getShapeMask(parentShape); const parentPageCorners = parentPageTransform.applyToPoints(parentGeometry.vertices); const parentPagePolygon = parentPageMaskVertices ? intersectPolygonPolygon(parentPageMaskVertices, parentPageCorners) : parentPageCorners; if (!parentPagePolygon) continue parentCheck; const childrenToReparent = []; shapeCheck: for (const shape of remainingShapesToReparent) { if (parentShape.id === shape.id) continue shapeCheck; if (cb && !cb(shape, parentShape)) continue shapeCheck; if (!shapeGroupIds.has(shape.id)) { shapeGroupIds.set( shape.id, editor.findShapeAncestor(shape, (s) => editor.isShapeOfType(s, "group"))?.id ); } const shapeGroupId = shapeGroupIds.get(shape.id); if (shapeGroupId !== parentShapeContainingGroupId) continue shapeCheck; if (editor.findShapeAncestor(parentShape, (s) => shape.id === s.id)) continue shapeCheck; const parentPolygonInShapeSpace = editor.getShapePageTransform(shape).clone().invert().applyToPoints(parentPagePolygon); if (doesGeometryOverlapPolygon(editor.getShapeGeometry(shape), parentPolygonInShapeSpace)) { if (!editor.getShapeUtil(parentShape).canReceiveNewChildrenOfType?.(parentShape, shape.type)) continue shapeCheck; if (shape.parentId !== parentShape.id) { childrenToReparent.push(shape); } remainingShapesToReparent.delete(shape); continue shapeCheck; } } if (childrenToReparent.length) { reparenting.set(parentShape.id, childrenToReparent); } } return { // these are the shapes that will be reparented to new parents reparenting, // these are the shapes that will be reparented to the page or their ancestral group remainingShapesToReparent }; } export { doesGeometryOverlapPolygon, getDroppedShapesToNewParents, kickoutOccludedShapes }; //# sourceMappingURL=reparenting.mjs.map