oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1// Layout conversion between a slide's two modes (spec §2.3) —
2// core/src/layout.rs, table for table. A flow slide places blocks by
3// region; a canvas slide by an absolute box and z. Switching carries the
4// blocks across, lossily in both directions.
5import * as Y from 'yjs'
6import { getString, getObject, getArray, orderedMaps, renumber } from './ydoc.mjs'
7
8export const REGIONS = ['heading', 'body', 'aside', 'media']
9
10/** Rounds a box coordinate to the four decimals §2.3 specifies (Rust's round(): half away from zero). */
11export function round4 (f) {
12 const scaled = f * 10000
13 return (scaled < 0 ? -Math.round(-scaled) : Math.round(scaled)) / 10000
14}
15
16export function boxJSON (x, y, w, h) {
17 return { x: round4(x), y: round4(y), w: round4(w), h: round4(h) }
18}
19
20/** The region a block lands in when nobody said. */
21export function defaultRegion (kind) {
22 if (kind === 'heading') return 'heading'
23 if (kind === 'image') return 'media'
24 return 'body'
25}
26
27/** The frame fraction each region occupies: [x, y, w, h]. body narrows beside an aside. */
28export function regionRect (region, hasAside) {
29 switch (region) {
30 case 'heading': return [0.06, 0.08, 0.88, 0.18]
31 case 'aside': return [0.54, 0.30, 0.40, 0.62]
32 case 'media': return [0.50, 0.08, 0.44, 0.84]
33 default: return hasAside ? [0.06, 0.30, 0.44, 0.62] : [0.06, 0.30, 0.88, 0.62]
34 }
35}
36
37function regionOf (block) {
38 const r = getString(block, 'region')
39 if (r !== null && REGIONS.includes(r)) return r
40 return defaultRegion(getString(block, 'type') ?? '')
41}
42
43/** The slide's block array, created if a peer wrote a slide without one. */
44export function blocksOf (slide) {
45 let a = getArray(slide, 'blocks')
46 if (a === null) {
47 a = new Y.Array()
48 slide.set('blocks', a)
49 }
50 return a
51}
52
53/**
54 * Flow to canvas: every block gets the box its region gave it, blocks
55 * sharing a region split its height equally in reading order, z is the
56 * reading-order index, and region goes.
57 */
58export function toCanvas (slide) {
59 const maps = orderedMaps(blocksOf(slide))
60 const regions = maps.map(([, m]) => regionOf(m))
61 const hasAside = regions.includes('aside')
62 const counts = new Map()
63 for (const r of regions) counts.set(r, (counts.get(r) ?? 0) + 1)
64 const seen = new Map()
65 maps.forEach(([, m], at) => {
66 const region = regions[at]
67 const n = counts.get(region)
68 const k = seen.get(region) ?? 0
69 const [x, y, w, h] = regionRect(region, hasAside)
70 const slot = h / n
71 seen.set(region, k + 1)
72 m.set('box', boxJSON(x, y + slot * k, w, slot))
73 m.set('z', at)
74 m.delete('region')
75 })
76}
77
78/**
79 * Canvas to flow: boxes go, regions come from position (a heading in
80 * the top quarter is heading, an image is media, the rest body), and
81 * the reading order becomes top to bottom, left to right.
82 *
83 * The reorder is written as order on each block map (ydoc.mjs, "block
84 * order") and never by rebuilding the array. Rebuilding it — clone,
85 * clear, insert the copies — is what this did once, and a peer typing
86 * into a block at that moment lost the sentence: their edit addressed
87 * the deleted original and the merge kept the copy (codex 4).
88 */
89export function toFlow (slide) {
90 const blocks = blocksOf(slide)
91 const maps = orderedMaps(blocks)
92 const order = []
93 maps.forEach(([, m], pos) => {
94 const kind = getString(m, 'type') ?? ''
95 const bx = getObject(m, 'box')
96 const coord = (k, dflt) => (bx !== null && typeof bx[k] === 'number' ? bx[k] : dflt)
97 const x = coord('x', 0.06)
98 const y = coord('y', 0.30)
99 let region = 'body'
100 if (kind === 'heading' && y < 0.25) region = 'heading'
101 else if (kind === 'image') region = 'media'
102 m.set('region', region)
103 m.delete('box')
104 m.delete('z')
105 order.push([y, x, pos])
106 })
107 order.sort((a, b) => (a[0] - b[0]) || (a[1] - b[1]))
108 if (order.every(([, , pos], i) => i === pos)) return
109 renumber(order.map(([, , pos]) => maps[pos][1]))
110}
111
112