chalk-talk 0.12.0 → 0.13.0

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.
@@ -1,4 +1,4 @@
1
- // GENERATED by tools/assets.mjs from the drawing engine v0.252.0. Do not edit.
1
+ // GENERATED by tools/assets.mjs from the drawing engine v0.253.0. Do not edit.
2
2
  var WORDS = ["and", "or", "not"];
3
3
 
4
4
  var PROP_NAMES = [
@@ -31,7 +31,24 @@ var PROP_NAMES = [
31
31
  "bulb",
32
32
  "gear",
33
33
  "database",
34
- "robot"
34
+ "robot",
35
+ // Onda B (spec 2026-11-01, chalk-talk): logistics, cloud, school
36
+ "ship",
37
+ "container",
38
+ "warehouse",
39
+ "box",
40
+ "pallet",
41
+ "shop",
42
+ "shelf",
43
+ "field",
44
+ "cup",
45
+ "function",
46
+ "api-gateway",
47
+ "cache",
48
+ "cluster",
49
+ "board",
50
+ "book",
51
+ "pencil"
35
52
  ];
36
53
  var PROP_ALIASES = {
37
54
  pessoa: "person",
@@ -69,7 +86,9 @@ var C = {
69
86
  bus: "#E9C46A",
70
87
  green: "#7DBE8A",
71
88
  dark: "#4A4A4A",
72
- metal: "#C9D3DD"
89
+ metal: "#C9D3DD",
90
+ board: "#2F4F3F",
91
+ chalk: "#F2EFE6"
73
92
  };
74
93
  var rect = (x, y, w, h, fill, main) => ({
75
94
  kind: "polygon",
@@ -275,6 +294,115 @@ var PROPS = {
275
294
  line([0.28, -0.55], [0.42, -0.35]),
276
295
  rect(-0.2, -0.2, 0.12, 0.2, C.device),
277
296
  rect(0.08, -0.2, 0.12, 0.2, C.device)
297
+ ],
298
+ // Onda B (spec 2026-11-01, chalk-talk): logistics
299
+ ship: [
300
+ { kind: "polygon", points: [[-0.48, -0.3], [0.48, -0.3], [0.36, -0.04], [-0.4, -0.04]], fill: C.car, main: true },
301
+ rect(-0.42, -0.62, 0.14, 0.32, C.wall),
302
+ rect(-0.24, -0.5, 0.2, 0.2, C.roof),
303
+ rect(-0.02, -0.5, 0.2, 0.2, C.bus),
304
+ rect(0.2, -0.5, 0.2, 0.2, C.green),
305
+ { kind: "curve", points: [[-0.5, 0], [-0.3, -0.04], [-0.1, 0], [0.1, -0.04], [0.3, 0], [0.5, -0.04]] }
306
+ ],
307
+ container: [
308
+ rect(-0.46, -0.52, 0.92, 0.52, C.roof, true),
309
+ ...[-0.3, -0.15, 0, 0.15, 0.3].map((x) => line([x, -0.48], [x, -0.04]))
310
+ ],
311
+ warehouse: [
312
+ { kind: "polygon", points: [[-0.46, 0], [0.46, 0], [0.46, -0.55], [0, -0.82], [-0.46, -0.55]], fill: C.wall, main: true },
313
+ rect(-0.2, -0.38, 0.4, 0.38, C.metal),
314
+ line([-0.2, -0.26], [0.2, -0.26]),
315
+ line([-0.2, -0.13], [0.2, -0.13])
316
+ ],
317
+ box: [
318
+ rect(-0.36, -0.5, 0.52, 0.5, C.wood, true),
319
+ { kind: "polygon", points: [[-0.36, -0.5], [0.16, -0.5], [0.36, -0.64], [-0.16, -0.64]], fill: C.wood },
320
+ { kind: "polygon", points: [[0.16, 0], [0.36, -0.14], [0.36, -0.64], [0.16, -0.5]], fill: C.wood },
321
+ line([-0.1, -0.5], [0.1, -0.64])
322
+ ],
323
+ pallet: [
324
+ rect(-0.46, -0.12, 0.92, 0.06, C.wood),
325
+ ...[-0.42, -0.05, 0.32].map((x) => rect(x, -0.06, 0.1, 0.06, C.wood)),
326
+ rect(-0.42, -0.5, 0.4, 0.38, C.wood, true),
327
+ rect(0.02, -0.5, 0.4, 0.38, C.wood),
328
+ rect(-0.2, -0.82, 0.4, 0.32, C.wood)
329
+ ],
330
+ shop: [
331
+ rect(-0.42, -0.6, 0.84, 0.6, C.wall, true),
332
+ { kind: "polygon", points: [[-0.48, -0.6], [0.48, -0.6], [0.42, -0.78], [-0.42, -0.78]], fill: C.roof },
333
+ rect(-0.34, -0.48, 0.34, 0.26, C.screen),
334
+ rect(0.08, -0.42, 0.24, 0.42, C.wood)
335
+ ],
336
+ shelf: [
337
+ rect(-0.4, -0.92, 0.8, 0.92, C.paper, true),
338
+ line([-0.4, -0.62], [0.4, -0.62]),
339
+ line([-0.4, -0.32], [0.4, -0.32]),
340
+ rect(-0.32, -0.8, 0.16, 0.18, C.roof),
341
+ rect(-0.1, -0.78, 0.14, 0.16, C.bus),
342
+ rect(0.1, -0.5, 0.22, 0.18, C.green),
343
+ rect(-0.3, -0.2, 0.24, 0.2, C.wood)
344
+ ],
345
+ field: [
346
+ line([-0.48, 0], [0.48, 0]),
347
+ ...[-0.3, 0, 0.3].flatMap((x) => [
348
+ { kind: "curve", points: [[x, 0], [x - 0.02, -0.25], [x, -0.5]] },
349
+ { kind: "circle", c: [x - 0.08, -0.32], r: 0.07, fill: C.green, main: true },
350
+ { kind: "circle", c: [x + 0.08, -0.42], r: 0.07, fill: C.green, main: true },
351
+ { kind: "circle", c: [x, -0.52], r: 0.06, fill: C.roof }
352
+ ])
353
+ ],
354
+ cup: [
355
+ { kind: "polygon", points: [[-0.26, -0.52], [0.26, -0.52], [0.2, -0.1], [-0.2, -0.1]], fill: C.paper, main: true },
356
+ { kind: "curve", points: [[0.24, -0.44], [0.4, -0.38], [0.22, -0.2]] },
357
+ { kind: "polygon", points: [[-0.46, -0.1], [0.46, -0.1], [0.36, 0], [-0.36, 0]], fill: C.paper },
358
+ { kind: "curve", points: [[-0.08, -0.62], [-0.02, -0.72], [-0.08, -0.84]] },
359
+ { kind: "curve", points: [[0.08, -0.62], [0.14, -0.72], [0.08, -0.84]] }
360
+ ],
361
+ // cloud
362
+ function: [
363
+ rect(-0.38, -0.72, 0.76, 0.62, C.screen, true),
364
+ line([-0.14, -0.62], [0, -0.62], [0.14, -0.2]),
365
+ line([0.02, -0.42], [-0.14, -0.2])
366
+ ],
367
+ "api-gateway": [
368
+ rect(-0.12, -0.8, 0.24, 0.8, C.metal, true),
369
+ line([-0.48, -0.55], [-0.16, -0.55]),
370
+ line([-0.24, -0.62], [-0.16, -0.55], [-0.24, -0.48]),
371
+ line([0.16, -0.3], [0.48, -0.3]),
372
+ line([0.4, -0.37], [0.48, -0.3], [0.4, -0.23])
373
+ ],
374
+ cache: [
375
+ rect(-0.34, -0.66, 0.68, 0.66, C.metal, true),
376
+ { kind: "polygon", points: [[0.04, -0.58], [-0.12, -0.3], [0, -0.3], [-0.06, -0.08], [0.14, -0.38], [0.02, -0.38]], fill: C.gold }
377
+ ],
378
+ // a cluster: three machines inside a rounded outline (a generic drawing, not any product's logo)
379
+ cluster: [
380
+ { kind: "curve", points: [[-0.4, -0.8], [0, -0.86], [0.4, -0.8], [0.48, -0.45], [0.4, -0.1], [0, -0.04], [-0.4, -0.1], [-0.48, -0.45], [-0.4, -0.8]] },
381
+ rect(-0.3, -0.7, 0.24, 0.2, C.screen, true),
382
+ rect(0.06, -0.7, 0.24, 0.2, C.screen, true),
383
+ rect(-0.12, -0.38, 0.24, 0.2, C.screen, true)
384
+ ],
385
+ // school: the chalk on the board is light, so it is drawn as thin light strips
386
+ board: [
387
+ rect(-0.48, -0.9, 0.96, 0.6, C.board, true),
388
+ rect(-0.34, -0.76, 0.42, 0.03, C.chalk),
389
+ rect(-0.34, -0.62, 0.58, 0.03, C.chalk),
390
+ rect(-0.34, -0.48, 0.3, 0.03, C.chalk),
391
+ line([-0.3, -0.3], [-0.36, 0]),
392
+ line([0.3, -0.3], [0.36, 0])
393
+ ],
394
+ book: [
395
+ { kind: "polygon", points: [[0, -0.62], [-0.46, -0.7], [-0.46, -0.1], [0, -0.04]], fill: C.paper, main: true },
396
+ { kind: "polygon", points: [[0, -0.62], [0.46, -0.7], [0.46, -0.1], [0, -0.04]], fill: C.paper, main: true },
397
+ line([-0.36, -0.52], [-0.1, -0.48]),
398
+ line([-0.36, -0.38], [-0.1, -0.34]),
399
+ line([0.1, -0.48], [0.36, -0.52]),
400
+ line([0.1, -0.34], [0.36, -0.38])
401
+ ],
402
+ pencil: [
403
+ { kind: "polygon", points: [[-0.38, -0.12], [0.22, -0.72], [0.34, -0.6], [-0.26, 0]], fill: C.bus, main: true },
404
+ { kind: "polygon", points: [[0.22, -0.72], [0.34, -0.6], [0.44, -0.86]], fill: C.wood },
405
+ { kind: "polygon", points: [[-0.38, -0.12], [-0.26, 0], [-0.46, -0.02]], fill: C.roof }
278
406
  ]
279
407
  };
280
408
  var MOODS = ["happy", "sad", "surprised", "worried", "idea"];
@@ -288,6 +416,8 @@ var hasFace = (name) => {
288
416
  return !!key && key in HEADS;
289
417
  };
290
418
 
419
+ var MAX_DEFS = 8;
420
+ var MAX_DEF_SHAPES = 24;
291
421
  function defaultValues(scene) {
292
422
  const values = {};
293
423
  for (const [k, p] of Object.entries(scene.params ?? {})) values[k] = p.value;
@@ -322,10 +452,11 @@ var STEP_TYPES = [
322
452
  "brace",
323
453
  "note",
324
454
  "table",
325
- "bars"
455
+ "bars",
456
+ "chain"
326
457
  ];
327
458
  var PLACEABLE = ["text", "prop", "card", "counter", "circle", "ellipse"];
328
- var ARRANGEMENTS = ["row", "column", "grid"];
459
+ var ARRANGEMENTS = ["row", "column", "grid", "chain"];
329
460
  var MAX_REPEAT = 50;
330
461
  function object(value2, name) {
331
462
  if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) throw new Error(`${name}: expected an object.`);
@@ -476,8 +607,9 @@ function validateStep(step2, name, ids, inside, inRow = false) {
476
607
  optionalText(step2.ask, `${name}.ask`);
477
608
  break;
478
609
  case "prop":
479
- if (!isPropName(step2.name)) {
480
- throw new Error(`${name}.name: unknown prop "${String(step2.name)}"; use one of ${PROP_NAMES.join(", ")}.`);
610
+ if (!isPropName(step2.name) && !ids.figures.has(String(step2.name))) {
611
+ const own = ids.figures.size ? `, or one the recipe defines (${[...ids.figures].join(", ")})` : "";
612
+ throw new Error(`${name}.name: unknown prop "${String(step2.name)}"; use one of ${PROP_NAMES.join(", ")}${own}.`);
481
613
  }
482
614
  pointOrPlaced(step2, "at", name, placed);
483
615
  if (step2.size !== void 0) {
@@ -550,6 +682,27 @@ function validateStep(step2, name, ids, inside, inRow = false) {
550
682
  validateStep(inner, `${name}.steps[${j}]`, ids, step2.type);
551
683
  });
552
684
  break;
685
+ case "chain":
686
+ if (arranged) throw new Error(`${name}: a chain cannot go inside a ${inside}.`);
687
+ pointOrPlaced(step2, "at", name, false);
688
+ if (step2.gap !== void 0 && step2.gap !== "auto") finite(step2.gap, `${name}.gap`, 0, 400);
689
+ choice(step2.direction, ["right", "down"], `${name}.direction`);
690
+ if (step2.wrap !== void 0 && typeof step2.wrap !== "boolean") throw new Error(`${name}.wrap: expected true or false.`);
691
+ optionalText(step2.forward, `${name}.forward`);
692
+ optionalText(step2.back, `${name}.back`);
693
+ if (step2.size !== void 0) {
694
+ numeric(step2.size, `${name}.size`);
695
+ if (typeof step2.size === "number") finite(step2.size, `${name}.size`, 10, 1e3);
696
+ }
697
+ if (!Array.isArray(step2.steps) || step2.steps.length < 2 || step2.steps.length > 12) {
698
+ throw new Error(`${name}.steps: a chain joins 2 to 12 pieces.`);
699
+ }
700
+ step2.steps.forEach((inner, j) => {
701
+ object(inner, `${name}.steps[${j}]`);
702
+ if (!PLACEABLE.includes(inner.type)) throw new Error(`${name}.steps[${j}]: a chain holds only ${PLACEABLE.join(", ")}.`);
703
+ validateStep(inner, `${name}.steps[${j}]`, ids, "chain");
704
+ });
705
+ break;
553
706
  case "axis":
554
707
  case "timeline":
555
708
  point(step2.from, `${name}.from`);
@@ -636,6 +789,38 @@ function validateStep(step2, name, ids, inside, inRow = false) {
636
789
  }
637
790
  if (typeof step2.id === "string") (inside === "repeat" ? ids.repeated : ids.exact).add(step2.id);
638
791
  }
792
+ function validateFigure(key, def) {
793
+ const name = `defs.${key}`;
794
+ if (!/^[a-z][a-z0-9-]{1,30}$/.test(key)) throw new Error(`${name}: a figure's name is 2 to 31 lowercase letters, digits or -, starting with a letter.`);
795
+ if (propKey(key) || Object.hasOwn(PROP_ALIASES, key) || STEP_TYPES.includes(key) || ["define", "title", "param", "switch", "var"].includes(key)) {
796
+ throw new Error(`${name}: "${key}" is already a prop or a step; choose another name.`);
797
+ }
798
+ object(def, name);
799
+ if (!Array.isArray(def.shapes) || def.shapes.length === 0 || def.shapes.length > MAX_DEF_SHAPES) {
800
+ throw new Error(`${name}.shapes: expected 1 to ${MAX_DEF_SHAPES} shapes.`);
801
+ }
802
+ const grid = (v, at) => {
803
+ if (typeof v !== "number" || !Number.isInteger(v) || v < 0 || v > 100) throw new Error(`${at}: expected a whole number from 0 to 100 (the figure's grid, y down; it stands at 50,100).`);
804
+ };
805
+ def.shapes.forEach((shape, j) => {
806
+ const at = `${name}.shapes[${j}]`;
807
+ object(shape, at);
808
+ if (shape.fill !== void 0 && (typeof shape.fill !== "string" || !/^(#[0-9a-fA-F]{3,8}|[a-z]{3,20})$/.test(shape.fill))) throw new Error(`${at}.fill: a colour, as hex or a plain name.`);
809
+ if (shape.kind === "circle") {
810
+ if (!Array.isArray(shape.c) || shape.c.length !== 2) throw new Error(`${at}.c: expected [x, y].`);
811
+ shape.c.forEach((v, k) => grid(v, `${at}.c[${k}]`));
812
+ grid(shape.r, `${at}.r`);
813
+ return;
814
+ }
815
+ if (!["polygon", "polyline", "curve"].includes(shape.kind)) throw new Error(`${at}.kind: choose one of polygon, polyline, curve, circle.`);
816
+ const least = shape.kind === "polygon" ? 3 : 2;
817
+ if (!Array.isArray(shape.points) || shape.points.length < least || shape.points.length > 40) throw new Error(`${at}.points: a ${shape.kind} takes ${least} to 40 points.`);
818
+ shape.points.forEach((p, k) => {
819
+ if (!Array.isArray(p) || p.length !== 2) throw new Error(`${at}.points[${k}]: expected [x, y].`);
820
+ p.forEach((v, n) => grid(v, `${at}.points[${k}][${n}]`));
821
+ });
822
+ });
823
+ }
639
824
  function validateScene(data) {
640
825
  object(data, "The recipe");
641
826
  const s = data;
@@ -701,7 +886,17 @@ function validateScene(data) {
701
886
  if (!Array.isArray(s.steps) || s.steps.length === 0 || s.steps.length > 250) {
702
887
  throw new Error("A recipe needs 1 to 250 steps.");
703
888
  }
704
- const ids = { exact: /* @__PURE__ */ new Set(), repeated: /* @__PURE__ */ new Set(), count: 0 };
889
+ const figures = /* @__PURE__ */ new Set();
890
+ if (s.defs !== void 0) {
891
+ object(s.defs, "defs");
892
+ const entries = Object.entries(s.defs);
893
+ if (entries.length > MAX_DEFS) throw new Error(`defs: at most ${MAX_DEFS} figures.`);
894
+ for (const [key, def] of entries) {
895
+ validateFigure(key, def);
896
+ figures.add(key);
897
+ }
898
+ }
899
+ const ids = { exact: /* @__PURE__ */ new Set(), repeated: /* @__PURE__ */ new Set(), count: 0, figures };
705
900
  s.steps.forEach((step2, i) => validateStep(step2, `Step ${i + 1}`, ids, null));
706
901
  return {
707
902
  ...s,
@@ -818,12 +1013,13 @@ var STEP_WORDS = /* @__PURE__ */ new Set([
818
1013
  "note",
819
1014
  "table",
820
1015
  "bars",
821
- "json"
1016
+ "json",
1017
+ "chain"
822
1018
  ]);
823
1019
  var SIDES = { "right-of": "right", "left-of": "left", above: "above", below: "below" };
824
1020
  var STYLE = ["color", "fill", "roughness", "bowing", "fillStyle", "opacity"];
825
1021
  var SPACED = /* @__PURE__ */ new Set(["gap", "size", "r", "rx", "ry", "w", "h", "columns", "ms", "thickness"]);
826
- var FLAGS = /* @__PURE__ */ new Set(["dashed", "flip", "noheader"]);
1022
+ var FLAGS = /* @__PURE__ */ new Set(["dashed", "flip", "noheader", "wrap"]);
827
1023
  function attrs(tokens, step2, no) {
828
1024
  const style = {};
829
1025
  for (let i = 0; i < tokens.length; i++) {
@@ -833,6 +1029,7 @@ function attrs(tokens, step2, no) {
833
1029
  if (FLAGS.has(t)) {
834
1030
  if (t === "dashed") style.dashed = true;
835
1031
  else if (t === "flip") step2.flip = true;
1032
+ else if (t === "wrap") step2.wrap = true;
836
1033
  else step2.header = false;
837
1034
  continue;
838
1035
  }
@@ -845,7 +1042,7 @@ function attrs(tokens, step2, no) {
845
1042
  else if (key === "nudge") step2.nudge = point2(raw, no).map(Number);
846
1043
  else if (key === "ticks") step2.ticks = raw.includes(",") ? parts(raw).map(Number) : Number(raw);
847
1044
  else if (key === "labels") step2.labels = parts(raw).map(unquote);
848
- else if (key === "id" || key === "anchor" || key === "font" || key === "mood" || key === "along" || key === "side" || key === "kind" || key === "var" || key === "align" || key === "label" || key === "format" || key === "fn") step2[key] = unquote(raw);
1045
+ else if (key === "id" || key === "anchor" || key === "font" || key === "mood" || key === "along" || key === "side" || key === "kind" || key === "var" || key === "align" || key === "label" || key === "format" || key === "fn" || key === "forward" || key === "back" || key === "direction") step2[key] = unquote(raw);
849
1046
  else step2[key] = value(raw, no);
850
1047
  }
851
1048
  if (Object.keys(style).length) step2.style = style;
@@ -974,7 +1171,8 @@ function step(line2, props) {
974
1171
  }
975
1172
  case "row":
976
1173
  case "column":
977
- case "grid": {
1174
+ case "grid":
1175
+ case "chain": {
978
1176
  const s = { type: w };
979
1177
  const more = placement(rest, s, "at", no);
980
1178
  attrs(more, s, no);
@@ -1063,6 +1261,50 @@ function step(line2, props) {
1063
1261
  }
1064
1262
  throw new ChalkTextError(no, `\xAB${w}\xBB is not a step, a prop or an id followed by -> / says.`);
1065
1263
  }
1264
+ function links(line2) {
1265
+ const { no, tokens } = line2;
1266
+ const out = [];
1267
+ let from = tokens[0];
1268
+ let i = 1;
1269
+ while (tokens[i] === "->") {
1270
+ const to = tokens[i + 1];
1271
+ if (!to || isQuoted(to)) throw new ChalkTextError(no, 'write a -> b "label" -> c "label".');
1272
+ const s = { type: "link", from, to };
1273
+ i += 2;
1274
+ if (tokens[i] && isQuoted(tokens[i])) s.label = unquote(tokens[i++]);
1275
+ out.push(s);
1276
+ from = to;
1277
+ }
1278
+ const more = tokens.slice(i);
1279
+ return out.map((s) => attrs([...more], s, no));
1280
+ }
1281
+ function figureShape(line2) {
1282
+ const { no, tokens } = line2;
1283
+ const [kind, ...rest] = tokens;
1284
+ const grid = (t) => {
1285
+ const m = t?.match(/^(\d{1,3}),(\d{1,3})$/);
1286
+ if (!m || Number(m[1]) > 100 || Number(m[2]) > 100) throw new ChalkTextError(no, `on a figure's grid a point is two whole numbers from 0 to 100, like 24,48 (got ${t ?? "nothing"}).`);
1287
+ return [Number(m[1]), Number(m[2])];
1288
+ };
1289
+ const shape = { kind };
1290
+ let more = rest;
1291
+ if (kind === "circle") {
1292
+ if (more[0] === "at") more = more.slice(1);
1293
+ shape.c = grid(more[0]);
1294
+ more = more.slice(1);
1295
+ } else if (kind === "polygon" || kind === "polyline" || kind === "curve") {
1296
+ const pts = more.filter((t) => t.includes(",") && !t.includes("="));
1297
+ shape.points = pts.map(grid);
1298
+ more = more.slice(pts.length);
1299
+ } else throw new ChalkTextError(no, `a figure is made of polygon, polyline, curve and circle lines, not \xAB${kind}\xBB.`);
1300
+ for (const t of more) {
1301
+ const [k, v] = t.split("=");
1302
+ if (k === "fill" && v) shape.fill = unquote(v);
1303
+ else if (k === "r" && v && /^\d{1,3}$/.test(v)) shape.r = Number(v);
1304
+ else throw new ChalkTextError(no, `did not understand \xAB${t}\xBB in a figure (only fill=\u2026 and, on a circle, r=\u2026).`);
1305
+ }
1306
+ return shape;
1307
+ }
1066
1308
  function parseChalk(text2, props) {
1067
1309
  const names = new Set(props);
1068
1310
  const scene = { id: "lesson", title: "", width: 900, height: 560 };
@@ -1136,6 +1378,13 @@ function parseChalk(text2, props) {
1136
1378
  scene.learning = { ...scene.learning, [part]: unquote(t ?? "") };
1137
1379
  continue;
1138
1380
  }
1381
+ case "define": {
1382
+ const name = rest[0];
1383
+ if (!name) throw new ChalkTextError(no, "write define <name> and its shapes under it.");
1384
+ scene.defs = { ...scene.defs, [name]: { shapes: line2.children.map(figureShape) } };
1385
+ names.add(name);
1386
+ continue;
1387
+ }
1139
1388
  case "quiz": {
1140
1389
  scene.quiz = {
1141
1390
  question: unquote(rest[0] ?? ""),
@@ -1150,10 +1399,11 @@ function parseChalk(text2, props) {
1150
1399
  continue;
1151
1400
  }
1152
1401
  }
1153
- const s = step(line2, names);
1402
+ const made = rest[0] === "->" && rest.indexOf("->", 1) > 0 ? links(line2) : [step(line2, names)];
1403
+ const [s] = made;
1154
1404
  if (chapter !== void 0) [s.chapter, chapter] = [chapter, void 0];
1155
1405
  if (say !== void 0) [s.say, say] = [say, void 0];
1156
- steps.push(s);
1406
+ steps.push(...made);
1157
1407
  }
1158
1408
  scene.steps = steps;
1159
1409
  return scene;
@@ -1174,8 +1424,8 @@ function printAttrs(s, skip) {
1174
1424
  else return null;
1175
1425
  }
1176
1426
  } else if (k === "nudge") out.push(`nudge=${v.join(",")}`);
1177
- else if (k === "flip") {
1178
- if (v === true) out.push("flip");
1427
+ else if (k === "flip" || k === "wrap") {
1428
+ if (v === true) out.push(k);
1179
1429
  } else if (typeof v === "number" || typeof v === "string") out.push(`${k}=${typeof v === "number" ? v : num(v)}`);
1180
1430
  else return null;
1181
1431
  }
@@ -1231,7 +1481,8 @@ function printStep(s, props, indent) {
1231
1481
  }
1232
1482
  case "row":
1233
1483
  case "column":
1234
- case "grid": {
1484
+ case "grid":
1485
+ case "chain": {
1235
1486
  const head = line2(`${s.type} ${place(s, "at")}`, ["steps", ...placedSkip]);
1236
1487
  if (head[0].trimStart().startsWith("json")) return head;
1237
1488
  return [...head, ...s.steps.flatMap((c) => printStep(c, props, `${indent} `))];
@@ -1247,6 +1498,7 @@ function printStep(s, props, indent) {
1247
1498
  }
1248
1499
  function printChalk(scene, props) {
1249
1500
  const names = new Set(props);
1501
+ for (const name of Object.keys(scene.defs ?? {})) names.add(name);
1250
1502
  const out = [];
1251
1503
  if (scene.id !== void 0) out.push(`id ${scene.id}`);
1252
1504
  if (scene.title !== void 0) out.push(`title ${scene.title}`);
@@ -1258,6 +1510,13 @@ function printChalk(scene, props) {
1258
1510
  for (const [k, sw] of Object.entries(scene.switches ?? {})) out.push(`switch ${k} ${q(sw.label)} ${sw.on ? "on" : "off"}`);
1259
1511
  for (const [k, v] of Object.entries(scene.vars ?? {})) out.push(`var ${k} = ${num(v)}`);
1260
1512
  for (const [k, v] of Object.entries(scene.learning ?? {})) out.push(`learning ${k} ${q(v)}`);
1513
+ for (const [name, def] of Object.entries(scene.defs ?? {})) {
1514
+ out.push("", `define ${name}`);
1515
+ for (const sh of def.shapes) {
1516
+ const fill = sh.fill !== void 0 ? ` fill=${sh.fill}` : "";
1517
+ out.push(sh.kind === "circle" ? ` circle at ${sh.c.join(",")} r=${sh.r}${fill}` : ` ${sh.kind} ${sh.points.map((p) => p.join(",")).join(" ")}${fill}`);
1518
+ }
1519
+ }
1261
1520
  for (const s of scene.steps ?? []) {
1262
1521
  if (s.chapter !== void 0 || s.say !== void 0) out.push("");
1263
1522
  if (s.chapter !== void 0) out.push(`# ${s.chapter}`);