export function applySnapToGuides(dragged, items, options = {}) { const threshold = options.threshold ?? 10; let next = { ...dragged }; const snapGuides = []; const candidates = items.filter((item) => item.id !== dragged.id); const best = { x: null, y: null }; const verticalPreference = new Set(options.preferVertical || []); const horizontalPreference = new Set(options.preferHorizontal || []); for (const item of candidates) { const guides = [ { type: "top", value: item.y, delta: item.y - next.y }, { type: "centerY", value: item.y + item.height / 2, delta: item.y + item.height / 2 - (next.y + next.height / 2) }, { type: "bottom", value: item.y + item.height, delta: item.y + item.height - (next.y + next.height) }, { type: "left", value: item.x, delta: item.x - next.x }, { type: "centerX", value: item.x + item.width / 2, delta: item.x + item.width / 2 - (next.x + next.width / 2) }, { type: "right", value: item.x + item.width, delta: item.x + item.width - (next.x + next.width) }, ]; for (const guide of guides) { if (Math.abs(guide.delta) > threshold) continue; if (guide.type === "top" || guide.type === "centerY" || guide.type === "bottom") { if (isBetterGuide(guide, best.y, verticalPreference)) { best.y = { ...guide, targetId: item.id }; } } else { if (isBetterGuide(guide, best.x, horizontalPreference)) { best.x = { ...guide, targetId: item.id }; } } } } if (best.x) { next = { ...next, x: next.x + best.x.delta }; snapGuides.push({ type: best.x.type, value: best.x.value, targetId: best.x.targetId }); } if (best.y) { next = { ...next, y: next.y + best.y.delta }; snapGuides.push({ type: best.y.type, value: best.y.value, targetId: best.y.targetId }); } return { ...next, snapGuides }; } function isBetterGuide(candidate, current, preference) { if (!current) return true; const candidateDistance = Math.abs(candidate.delta); const currentDistance = Math.abs(current.delta); if (candidateDistance < currentDistance) return true; if (candidateDistance > currentDistance) return false; return preference.has(candidate.type) && !preference.has(current.type); } export function distributeEvenly(items, selectedIds) { if (!Array.isArray(selectedIds) || selectedIds.length < 3) return items; const selected = items .filter((item) => selectedIds.includes(item.id)) .sort((a, b) => a.x - b.x); if (selected.length < 3) return items; const first = selected[0]; const last = selected[selected.length - 1]; const step = (last.x - first.x) / (selected.length - 1); const nextById = new Map(selected.map((item, index) => [item.id, { ...item, x: Math.round((first.x + step * index) * 100) / 100, }])); return items.map((item) => nextById.get(item.id) || item); } export function exportWorkbenchLayout(city, items, stage) { const books = {}; items.forEach((item) => { books[item.id] = { title: item.title, category: item.category, x: round(item.x), y: round(item.y), width: round(item.width), height: round(item.height), rx: roundRatio(item.x, stage.width), ry: roundRatio(item.y, stage.height), }; }); return { city, exportedAt: new Date().toISOString(), stage: { width: Math.round(stage.width), height: Math.round(stage.height), }, books, }; } function round(value) { return Math.round(value * 100) / 100; } function roundRatio(value, size) { if (!size) return 0; return Math.round((value / size) * 10000) / 10000; }