canvas-website/src/utils/shapeCollisionUtils.ts

189 lines
5.5 KiB
TypeScript

import { Editor, TLShape, Box, TLShapeId } from "@tldraw/tldraw"
/**
* Check if two boxes overlap
*/
function boxesOverlap(
box1: { x: number; y: number; w: number; h: number },
box2: { x: number; y: number; w: number; h: number },
padding: number = 10
): boolean {
return !(
box1.x + box1.w + padding < box2.x - padding ||
box1.x - padding > box2.x + box2.w + padding ||
box1.y + box1.h + padding < box2.y - padding ||
box1.y - padding > box2.y + box2.h + padding
)
}
/**
* Get the bounding box of a shape
*/
function getShapeBounds(editor: Editor, shape: TLShape | string): Box | null {
const shapeId = typeof shape === 'string' ? (shape as TLShapeId) : shape.id
const bounds = editor.getShapePageBounds(shapeId)
return bounds ?? null
}
/**
* Check if a shape overlaps with any other custom shapes and move it aside if needed
*/
export function resolveOverlaps(editor: Editor, shapeId: string): void {
const allShapes = editor.getCurrentPageShapes()
const customShapeTypes = [
'ObsNote', 'ObsidianBrowser', 'HolonBrowser', 'VideoChat',
'Transcription', 'Holon', 'FathomMeetingsBrowser', 'Prompt',
'Embed', 'Slide', 'Markdown', 'MycrozineTemplate', 'ChatBox'
]
const shape = editor.getShape(shapeId as TLShapeId)
if (!shape || !customShapeTypes.includes(shape.type as string)) return
const shapeBounds = getShapeBounds(editor, shape)
if (!shapeBounds) return
const shapeBox = {
x: shape.x,
y: shape.y,
w: shapeBounds.w,
h: shapeBounds.h
}
// Check all other custom shapes for overlaps
const otherShapes = allShapes.filter(
s => s.id !== shapeId && customShapeTypes.includes(s.type)
)
for (const otherShape of otherShapes) {
const otherBounds = getShapeBounds(editor, otherShape)
if (!otherBounds) continue
const otherBox = {
x: otherShape.x,
y: otherShape.y,
w: otherBounds.w,
h: otherBounds.h
}
if (boxesOverlap(shapeBox, otherBox, 20)) {
// Simple solution: move the shape to the right of the overlapping shape
const newX = otherBox.x + otherBox.w + 20
const newY = shapeBox.y // Keep same Y position
editor.updateShape({
id: shapeId as TLShapeId,
type: shape.type,
x: newX,
y: newY,
})
// Recursively check if the new position also overlaps (shouldn't happen often)
const newBounds = getShapeBounds(editor, shapeId)
if (newBounds) {
const newShapeBox = {
x: newX,
y: newY,
w: newBounds.w,
h: newBounds.h
}
// If still overlapping, try moving down instead
if (boxesOverlap(newShapeBox, otherBox, 20)) {
const newY2 = otherBox.y + otherBox.h + 20
editor.updateShape({
id: shapeId as TLShapeId,
type: shape.type,
x: shapeBox.x, // Keep original X
y: newY2,
})
}
}
// Only resolve one overlap at a time to avoid infinite loops
break
}
}
}
/**
* Find a non-overlapping position for a new shape using spiral search
*/
export function findNonOverlappingPosition(
editor: Editor,
baseX: number,
baseY: number,
width: number,
height: number,
excludeShapeIds: string[] = []
): { x: number; y: number } {
const allShapes = editor.getCurrentPageShapes()
const customShapeTypes = [
'ObsNote', 'ObsidianBrowser', 'HolonBrowser', 'VideoChat',
'Transcription', 'Holon', 'FathomMeetingsBrowser', 'Prompt',
'Embed', 'Slide', 'Markdown', 'MycrozineTemplate', 'ChatBox'
]
const existingShapes = allShapes.filter(
s => !excludeShapeIds.includes(s.id) && customShapeTypes.includes(s.type)
)
const padding = 20
const stepSize = Math.max(width, height) + padding
// Helper function to check if a position overlaps with any existing shape
const positionOverlaps = (x: number, y: number): boolean => {
const testBox = { x, y, w: width, h: height }
for (const existingShape of existingShapes) {
const shapeBounds = getShapeBounds(editor, existingShape)
if (shapeBounds) {
const existingBox = {
x: existingShape.x,
y: existingShape.y,
w: shapeBounds.w,
h: shapeBounds.h
}
if (boxesOverlap(testBox, existingBox, padding)) {
return true
}
}
}
return false
}
// First, check the base position
if (!positionOverlaps(baseX, baseY)) {
return { x: baseX, y: baseY }
}
// Spiral search pattern: check positions in expanding circles
// Try positions: right, down, left, up, then expand radius
const directions = [
{ dx: stepSize, dy: 0 }, // Right
{ dx: 0, dy: stepSize }, // Down
{ dx: -stepSize, dy: 0 }, // Left
{ dx: 0, dy: -stepSize }, // Up
{ dx: stepSize, dy: stepSize }, // Down-right
{ dx: -stepSize, dy: stepSize }, // Down-left
{ dx: -stepSize, dy: -stepSize }, // Up-left
{ dx: stepSize, dy: -stepSize }, // Up-right
]
// Try positions at increasing distances
for (let radius = 1; radius <= 10; radius++) {
for (const dir of directions) {
const testX = baseX + dir.dx * radius
const testY = baseY + dir.dy * radius
if (!positionOverlaps(testX, testY)) {
return { x: testX, y: testY }
}
}
}
// If all positions overlap (unlikely), return a position far to the right
return { x: baseX + stepSize * 10, y: baseY }
}