@frebreco/canvas 0.2.0 → 0.3.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frebreco/canvas",
3
- "version": "0.2.0",
3
+ "version": "0.3.0-next.1",
4
4
  "description": "A multiplayer canvas for coding agents that run on your machine.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,7 @@
12
12
  * new frame's rect and patches for the frames that make room: frames later in
13
13
  * the row shift right, rows further down shift down. Nothing outside the
14
14
  * anchor's cluster moves. Anything placed away from other frames stays free.
15
+ * A frame leaving (moved or removed) closes its gap the same way, reversed.
15
16
  */
16
17
 
17
18
  export interface Rect {
@@ -178,10 +179,50 @@ export function placeNear(
178
179
  );
179
180
  }
180
181
 
181
- /** A frame leaves its row: the frames after it close the gap. */
182
+ /**
183
+ * A frame leaves its place: the frames after it in its row close the gap, or,
184
+ * when it was alone in its row, the rows below move up into it.
185
+ */
182
186
  export function lift(rects: ReadonlyArray<Rect>, id: string): Patch[] {
183
- const { row, frame } = locate(rects, id);
184
- return row.filter((f) => f.x > frame.x).map((f) => ({ id: f.id, x: f.x - frame.w - GAP }));
187
+ const { cluster: c, row, frame } = locate(rects, id);
188
+ if (row.length > 1)
189
+ return row.filter((f) => f.x > frame.x).map((f) => ({ id: f.id, x: f.x - frame.w - GAP }));
190
+ const below = c.rows.slice(c.rows.indexOf(row) + 1).flat();
191
+ if (below.length === 0) return [];
192
+ const dy = Math.min(...below.map((f) => f.y)) - frame.y;
193
+ return below.map((f) => ({ id: f.id, y: f.y - dy }));
194
+ }
195
+
196
+ /**
197
+ * Whether placing next to `target` goes in between two frames — two of a
198
+ * row, or two rows — and if so the gap's centre line (`w` or `h` is 0), for a
199
+ * drop's preview. `movingId`, the frame being placed, is no neighbour.
200
+ */
201
+ export function insertion(
202
+ rects: ReadonlyArray<Rect>,
203
+ target: Target,
204
+ movingId?: string,
205
+ ): Box | null {
206
+ const rest = rects.filter((f) => f.id !== movingId);
207
+ const { cluster: c, row, frame: anchor } = locate(rest, target.anchor);
208
+ switch (target.side) {
209
+ case "right":
210
+ case "left": {
211
+ const i = row.indexOf(anchor);
212
+ const [l, r] = target.side === "right" ? [anchor, row[i + 1]] : [row[i - 1], anchor];
213
+ if (!l || !r) return null;
214
+ const y = Math.min(l.y, r.y);
215
+ return { x: (l.x + l.w + r.x) / 2, y, w: 0, h: Math.max(l.y + l.h, r.y + r.h) - y };
216
+ }
217
+ case "below":
218
+ case "above": {
219
+ const i = c.rows.indexOf(row);
220
+ const [upper, lower] = target.side === "below" ? [row, c.rows[i + 1]] : [c.rows[i - 1], row];
221
+ if (!upper || !lower) return null;
222
+ const y = (rowBottom(upper) + Math.min(...lower.map((f) => f.y))) / 2;
223
+ return { x: c.bounds.x, y, w: c.bounds.w, h: 0 };
224
+ }
225
+ }
185
226
  }
186
227
 
187
228
  /** Where a frame being dragged would snap to: next to the nearest frame within reach. */