rspace-online/modules/rdesign/sla-bridge.ts

278 lines
7.2 KiB
TypeScript

/**
* SLA Bridge — bidirectional conversion between Automerge CRDT state and Scribus bridge commands.
*
* designDocToScribusCommands(doc) → array of bridge commands (for replaying state into Scribus)
* scribusStateToDesignDocPatch(state, doc) → diff patches (for updating CRDT from Scribus changes)
*/
import type { DesignDoc, DesignFrame, DesignPage } from './schemas';
// ── Types from the bridge ──
interface BridgeCommand {
action: string;
args: Record<string, any>;
}
interface ScribusFrameState {
name: string;
type: string; // TextFrame, ImageFrame, Rectangle, Ellipse, etc.
x: number;
y: number;
width: number;
height: number;
text?: string;
fontSize?: number;
fontName?: string;
}
interface ScribusPageState {
number: number;
width: number;
height: number;
}
interface ScribusState {
pages: ScribusPageState[];
frames: ScribusFrameState[];
}
// ── CRDT → Scribus commands ──
/**
* Convert a full DesignDoc into an ordered list of bridge commands
* to recreate the design in Scribus from scratch.
*/
export function designDocToScribusCommands(doc: DesignDoc): BridgeCommand[] {
const commands: BridgeCommand[] = [];
const pages = Object.values(doc.document.pages).sort((a, b) => a.number - b.number);
// Create document with first page dimensions (or A4 default)
const firstPage = pages[0];
commands.push({
action: 'new_document',
args: {
width: firstPage?.width || 210,
height: firstPage?.height || 297,
margins: firstPage?.margins || 10,
pages: pages.length || 1,
},
});
// Add frames sorted by page, then by creation time
const frames = Object.values(doc.document.frames).sort((a, b) => {
if (a.page !== b.page) return a.page - b.page;
return (a.createdAt || 0) - (b.createdAt || 0);
});
for (const frame of frames) {
switch (frame.type) {
case 'text':
commands.push({
action: 'add_text_frame',
args: {
x: frame.x,
y: frame.y,
width: frame.width,
height: frame.height,
text: frame.text || '',
fontSize: frame.fontSize || 12,
fontName: frame.fontName || 'Liberation Sans',
name: frame.id,
},
});
break;
case 'image':
commands.push({
action: 'add_image_frame',
args: {
x: frame.x,
y: frame.y,
width: frame.width,
height: frame.height,
imagePath: frame.imagePath || '',
name: frame.id,
},
});
break;
case 'rect':
case 'ellipse':
commands.push({
action: 'add_shape',
args: {
shapeType: frame.type === 'ellipse' ? 'ellipse' : 'rect',
x: frame.x,
y: frame.y,
width: frame.width,
height: frame.height,
fill: frame.fill,
name: frame.id,
},
});
break;
}
}
return commands;
}
/**
* Convert a subset of changed commands (for incremental updates).
* Only generates commands for frames that differ from existing state.
*/
export function designDocToIncrementalCommands(
doc: DesignDoc,
changedFrameIds: string[],
): BridgeCommand[] {
const commands: BridgeCommand[] = [];
for (const frameId of changedFrameIds) {
const frame = doc.document.frames[frameId];
if (!frame) {
// Frame was deleted
commands.push({ action: 'delete_frame', args: { name: frameId } });
continue;
}
// For simplicity, delete and recreate the frame
commands.push({ action: 'delete_frame', args: { name: frameId } });
switch (frame.type) {
case 'text':
commands.push({
action: 'add_text_frame',
args: {
x: frame.x, y: frame.y, width: frame.width, height: frame.height,
text: frame.text || '', fontSize: frame.fontSize || 12,
fontName: frame.fontName || 'Liberation Sans', name: frame.id,
},
});
break;
case 'image':
commands.push({
action: 'add_image_frame',
args: {
x: frame.x, y: frame.y, width: frame.width, height: frame.height,
imagePath: frame.imagePath || '', name: frame.id,
},
});
break;
case 'rect':
case 'ellipse':
commands.push({
action: 'add_shape',
args: {
shapeType: frame.type === 'ellipse' ? 'ellipse' : 'rect',
x: frame.x, y: frame.y, width: frame.width, height: frame.height,
fill: frame.fill, name: frame.id,
},
});
break;
}
}
return commands;
}
// ── Scribus → CRDT patches ──
/** Map Scribus object type strings to our frame types. */
function mapScribusType(scribusType: string): DesignFrame['type'] {
switch (scribusType) {
case 'TextFrame': return 'text';
case 'ImageFrame': return 'image';
case 'Rectangle': return 'rect';
case 'Ellipse': return 'ellipse';
default: return 'rect'; // fallback
}
}
/**
* Diff Scribus state against CRDT doc and produce patches.
* Returns arrays of frames to add, update, and delete in the CRDT.
*/
export function scribusStateToDesignDocPatch(
scribusState: ScribusState,
existingDoc: DesignDoc,
): {
pagesToUpdate: DesignPage[];
framesToAdd: DesignFrame[];
framesToUpdate: Array<{ id: string; updates: Partial<DesignFrame> }>;
framesToDelete: string[];
} {
const result = {
pagesToUpdate: [] as DesignPage[],
framesToAdd: [] as DesignFrame[],
framesToUpdate: [] as Array<{ id: string; updates: Partial<DesignFrame> }>,
framesToDelete: [] as string[],
};
// Pages
for (const sp of scribusState.pages) {
const pageId = `page_${sp.number}`;
result.pagesToUpdate.push({
id: pageId,
number: sp.number,
width: sp.width,
height: sp.height,
margins: 10, // Scribus doesn't report margins in getAllObjects; keep default
});
}
// Frames — build a set of Scribus frame names
const scribusFrameMap = new Map<string, ScribusFrameState>();
for (const sf of scribusState.frames) {
scribusFrameMap.set(sf.name, sf);
}
const existingFrameIds = new Set(Object.keys(existingDoc.document.frames));
// Check for new or updated frames from Scribus
for (const [name, sf] of scribusFrameMap) {
const existing = existingDoc.document.frames[name];
const frameType = mapScribusType(sf.type);
if (!existing) {
// New frame in Scribus not in CRDT
result.framesToAdd.push({
id: name,
type: frameType,
page: 1, // Scribus getAllObjects doesn't report per-frame page easily
x: sf.x,
y: sf.y,
width: sf.width,
height: sf.height,
text: sf.text,
fontSize: sf.fontSize,
fontName: sf.fontName,
});
} else {
// Check for position/size/text changes
const updates: Partial<DesignFrame> = {};
const tolerance = 0.5; // mm tolerance for position changes
if (Math.abs(existing.x - sf.x) > tolerance) updates.x = sf.x;
if (Math.abs(existing.y - sf.y) > tolerance) updates.y = sf.y;
if (Math.abs(existing.width - sf.width) > tolerance) updates.width = sf.width;
if (Math.abs(existing.height - sf.height) > tolerance) updates.height = sf.height;
if (sf.text !== undefined && sf.text !== existing.text) updates.text = sf.text;
if (sf.fontSize !== undefined && sf.fontSize !== existing.fontSize) updates.fontSize = sf.fontSize;
if (Object.keys(updates).length > 0) {
result.framesToUpdate.push({ id: name, updates });
}
}
}
// Check for frames deleted in Scribus
for (const frameId of existingFrameIds) {
if (!scribusFrameMap.has(frameId)) {
result.framesToDelete.push(frameId);
}
}
return result;
}