Spaces:
Running
Running
| 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; | |
| } | |