Files
TREK/shared/src/book/book-ops-groups.ts
T
Maurice 2f4644d434 feat(studio): groups, a layers panel, marquee, nudge, multi-resize, align and paste style
Elements can be grouped on a page and picked, moved, resized, aligned and
restacked as one. A layers panel lists the page front to back with lock,
visibility, rename and drag to restack. The sheet gains marquee selection,
arrow-key nudge, one box that resizes a multi-selection with Shift holding
the proportions, and the design-tool shortcuts. The inspector lines a
selection up and spaces it out, and copies and pastes a style.

The assistant gets the same through the group, ungroup, name and hide
operations, and describe_book_page lists the groups. The format gains the
optional group, name and hidden fields and a spread's group names, so
BOOK_SCHEMA_REV goes to 2.
2026-10-23 19:31:14 +02:00

69 lines
2.4 KiB
TypeScript

import type { BOOK_GROUP_OP_SCHEMAS } from './book-group-ops.schema';
import {
groupElements,
groupMembers,
renameElement,
renameGroup,
setElementsHidden,
ungroupElements,
} from './book-groups';
import { failOp, pageElement, unlockedElement } from './book-ops-core';
import type { BookSpread } from './book.schema';
import type { z } from 'zod';
/**
* What `group`, `ungroup`, `name` and `hide` do to a page, for `applyBookOps`.
*
* Grouping moves the members next to each other in the paint order, so a
* locked element refuses it like any other change. Ungrouping, naming and
* hiding move nothing on the page and take locked elements too, as the
* Studio's layers panel does: a locked background can still be hidden to get
* at what is behind it.
*/
type GroupOp = z.infer<(typeof BOOK_GROUP_OP_SCHEMAS)[number]>;
interface GroupOpEnv {
newId: (prefix: string) => string;
opIndex: number;
}
const BOOK_GROUP_OP_NAMES: ReadonlySet<string> = new Set(['group', 'ungroup', 'name', 'hide']);
export function isGroupOp(op: { op: string }): op is GroupOp {
return BOOK_GROUP_OP_NAMES.has(op.op);
}
function existingGroup(spread: BookSpread, groupId: string, opIndex: number): void {
if (!groupMembers(spread, groupId).length) {
throw failOp('group_not_found', `no group ${groupId} on this page`, opIndex);
}
}
export function applyGroupOp(spread: BookSpread, op: GroupOp, env: GroupOpEnv): BookSpread {
switch (op.op) {
case 'group': {
const ids = [...new Set(op.elementIds)];
for (const id of ids) unlockedElement(pageElement(spread, id, env.opIndex), env.opIndex);
if (ids.length < 2) throw failOp('not_enough_elements', 'a group needs two different elements', env.opIndex);
return groupElements(spread, ids, env.newId('g'), op.name);
}
case 'ungroup':
existingGroup(spread, op.groupId, env.opIndex);
return ungroupElements(spread, op.groupId);
case 'name':
if (op.groupId !== undefined) {
existingGroup(spread, op.groupId, env.opIndex);
return renameGroup(spread, op.groupId, op.name);
}
pageElement(spread, op.elementId ?? '', env.opIndex);
return renameElement(spread, op.elementId ?? '', op.name);
case 'hide': {
const ids = [...new Set(op.elementIds)];
for (const id of ids) pageElement(spread, id, env.opIndex);
return setElementsHidden(spread, ids, op.hidden);
}
}
}