@tldraw/editor
Version:
tldraw infinite canvas SDK (editor).
217 lines (216 loc) • 9.28 kB
JavaScript
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