@neta-art/cohub-cli 9.1.2 → 10.0.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,5 +1,6 @@
1
1
  import { $ as groupD8, A as bgr2rgb, B as Bounds, C as Sprite, D as TexturePool, E as RenderGroup, G as TextureStyle, H as Texture, I as BigPool, J as deprecation, K as definedProps, L as GlobalResourceRegistry, M as multiplyHexColors, N as getGlobalBounds, O as InstructionSet, P as FilterEffect, Q as Rectangle, R as warn, S as DOMAdapter, T as Container, U as TextureMatrix, V as updateQuadBounds, W as TextureSource, X as v8_3_4, Y as v8_0_0, Z as uid$1, _ as Cache, a as Geometry, at as extensions, b as ImageSource, c as Filter, d as RendererType, et as Matrix, f as BindGroup, g as getTestContext, h as GlProgram, i as canUseNewCanvasBlendModes, it as ExtensionType, j as multiplyColors, k as getLocalBounds, l as State, m as GpuProgram, n as CanvasPool, nt as eventemitter3_default, o as Buffer$1, p as UniformGroup, q as nextPow2, r as canvasUtils, rt as ObservablePoint, s as BufferUsage, t as getPo2TextureFromSource, tt as Point, u as Shader, v as Ticker, w as ViewContainer, x as CanvasSource, z as Color } from "./board-kernel/getPo2TextureFromSource.js";
2
- import { Script, createContext } from "node:vm";
2
+ import { Worker } from "node:worker_threads";
3
+ import { fileURLToPath } from "node:url";
3
4
  //#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/util.js
4
5
  function getEnumValues(entries) {
5
6
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
@@ -44,6 +45,24 @@ function floatSafeRemainder(val, step) {
44
45
  if (Math.abs(ratio - roundedRatio) < tolerance) return 0;
45
46
  return ratio - roundedRatio;
46
47
  }
48
+ const EVALUATING = /* @__PURE__*/ Symbol("evaluating");
49
+ function defineLazy(object, key, getter) {
50
+ let value = void 0;
51
+ Object.defineProperty(object, key, {
52
+ get() {
53
+ if (value === EVALUATING) return;
54
+ if (value === void 0) {
55
+ value = EVALUATING;
56
+ value = getter();
57
+ }
58
+ return value;
59
+ },
60
+ set(v) {
61
+ Object.defineProperty(object, key, { value: v });
62
+ },
63
+ configurable: true
64
+ });
65
+ }
47
66
  function assignProp(target, prop, value) {
48
67
  Object.defineProperty(target, prop, {
49
68
  value,
@@ -1017,6 +1036,7 @@ const anyString = /^[\s\S]{0,}$/;
1017
1036
  const integer = /^-?\d+$/;
1018
1037
  const number$2 = /^-?\d+(?:\.\d+)?$/;
1019
1038
  const boolean$1 = /^(?:true|false)$/i;
1039
+ const _null$2 = /^null$/i;
1020
1040
  const lowercase = /^[^A-Z]*$/;
1021
1041
  const uppercase = /^[^a-z]*$/;
1022
1042
  //#endregion
@@ -1838,6 +1858,22 @@ const $ZodBoolean = /*@__PURE__*/ $constructor("$ZodBoolean", (inst, def) => {
1838
1858
  return payload;
1839
1859
  };
1840
1860
  });
1861
+ const $ZodNull = /*@__PURE__*/ $constructor("$ZodNull", (inst, def) => {
1862
+ $ZodType.init(inst, def);
1863
+ inst._zod.pattern = _null$2;
1864
+ inst._zod.values = /* @__PURE__ */ new Set([null]);
1865
+ inst._zod.parse = (payload, _ctx) => {
1866
+ const input = payload.value;
1867
+ if (input === null) return payload;
1868
+ payload.issues.push({
1869
+ expected: "null",
1870
+ code: "invalid_type",
1871
+ input,
1872
+ inst
1873
+ });
1874
+ return payload;
1875
+ };
1876
+ });
1841
1877
  const $ZodUnknown = /*@__PURE__*/ $constructor("$ZodUnknown", (inst, def) => {
1842
1878
  $ZodType.init(inst, def);
1843
1879
  inst._zod.parse = (payload) => payload;
@@ -2764,6 +2800,21 @@ function handleReadonlyResult(payload) {
2764
2800
  if (!payload.memo) payload.value = Object.freeze(payload.value);
2765
2801
  return payload;
2766
2802
  }
2803
+ const $ZodLazy = /*@__PURE__*/ $constructor("$ZodLazy", (inst, def) => {
2804
+ $ZodType.init(inst, def);
2805
+ defineLazy(inst._zod, "innerType", () => {
2806
+ const d = def;
2807
+ if (!d._cachedInner) d._cachedInner = def.getter();
2808
+ return d._cachedInner;
2809
+ });
2810
+ defineLazyInternal(inst, "pattern", (zod) => zod.innerType?._zod?.pattern);
2811
+ defineLazyInternal(inst, "propValues", (zod) => zod.innerType?._zod?.propValues);
2812
+ defineLazyInternal(inst, "optin", (zod) => zod.innerType?._zod?.optin ?? void 0);
2813
+ defineLazyInternal(inst, "optout", (zod) => zod.innerType?._zod?.optout ?? void 0);
2814
+ inst._zod.parse = (payload, ctx) => {
2815
+ return inst._zod.innerType._zod.run(payload, ctx);
2816
+ };
2817
+ });
2767
2818
  const $ZodCustom = /*@__PURE__*/ $constructor("$ZodCustom", (inst, def) => {
2768
2819
  $ZodCheck.init(inst, def);
2769
2820
  $ZodType.init(inst, def);
@@ -3505,6 +3556,13 @@ function _boolean(Class, params) {
3505
3556
  });
3506
3557
  }
3507
3558
  // @__NO_SIDE_EFFECTS__
3559
+ function _null$1(Class, params) {
3560
+ return new Class({
3561
+ type: "null",
3562
+ ...normalizeParams(params)
3563
+ });
3564
+ }
3565
+ // @__NO_SIDE_EFFECTS__
3508
3566
  function _unknown(Class) {
3509
3567
  return new Class({ type: "unknown" });
3510
3568
  }
@@ -4286,6 +4344,13 @@ const numberProcessor = (schema, ctx, _json, params) => {
4286
4344
  const booleanProcessor = (_schema, _ctx, json, _params) => {
4287
4345
  json.type = "boolean";
4288
4346
  };
4347
+ const nullProcessor = (_schema, ctx, json, _params) => {
4348
+ if (ctx.target === "openapi-3.0") {
4349
+ json.type = "string";
4350
+ json.nullable = true;
4351
+ json.enum = [null];
4352
+ } else json.type = "null";
4353
+ };
4289
4354
  const neverProcessor = (_schema, _ctx, json, _params) => {
4290
4355
  json.not = {};
4291
4356
  };
@@ -4599,6 +4664,12 @@ const optionalProcessor = (schema, ctx, _json, params) => {
4599
4664
  const seen = ctx.seen.get(schema);
4600
4665
  seen.ref = def.innerType;
4601
4666
  };
4667
+ const lazyProcessor = (schema, ctx, _json, params) => {
4668
+ const innerType = schema._zod.innerType;
4669
+ processSchema(innerType, ctx, params);
4670
+ const seen = ctx.seen.get(schema);
4671
+ seen.ref = innerType;
4672
+ };
4602
4673
  //#endregion
4603
4674
  //#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/classic/errors.js
4604
4675
  const _installedErrorProtos = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);
@@ -5167,6 +5238,14 @@ const ZodBoolean = /*@__PURE__*/ $constructor("ZodBoolean", (inst, def) => {
5167
5238
  function boolean(params) {
5168
5239
  return /* @__PURE__ */ _boolean(ZodBoolean, params);
5169
5240
  }
5241
+ const ZodNull = /*@__PURE__*/ $constructor("ZodNull", (inst, def) => {
5242
+ $ZodNull.init(inst, def);
5243
+ ZodType.init(inst, def);
5244
+ inst._zod.processJSONSchema = (ctx, json, params) => nullProcessor(inst, ctx, json, params);
5245
+ });
5246
+ function _null(params) {
5247
+ return /* @__PURE__ */ _null$1(ZodNull, params);
5248
+ }
5170
5249
  const ZodUnknown = /*@__PURE__*/ $constructor("ZodUnknown", (inst, def) => {
5171
5250
  $ZodUnknown.init(inst, def);
5172
5251
  ZodType.init(inst, def);
@@ -5534,6 +5613,18 @@ function readonly(innerType) {
5534
5613
  innerType
5535
5614
  });
5536
5615
  }
5616
+ const ZodLazy = /*@__PURE__*/ $constructor("ZodLazy", (inst, def) => {
5617
+ $ZodLazy.init(inst, def);
5618
+ ZodType.init(inst, def);
5619
+ inst._zod.processJSONSchema = (ctx, json, params) => lazyProcessor(inst, ctx, json, params);
5620
+ inst.unwrap = () => inst._zod.def.getter();
5621
+ });
5622
+ function lazy(getter) {
5623
+ return new ZodLazy({
5624
+ type: "lazy",
5625
+ getter
5626
+ });
5627
+ }
5537
5628
  const ZodCustom = /*@__PURE__*/ $constructor("ZodCustom", (inst, def) => {
5538
5629
  $ZodCustom.init(inst, def);
5539
5630
  ZodType.init(inst, def);
@@ -5545,23 +5636,123 @@ function refine(fn, _params = {}) {
5545
5636
  function superRefine(fn, params) {
5546
5637
  return /* @__PURE__ */ _superRefine(fn, params);
5547
5638
  }
5639
+ function json(params) {
5640
+ const jsonSchema = lazy(() => {
5641
+ return union([
5642
+ string(params),
5643
+ number$1(),
5644
+ boolean(),
5645
+ _null(),
5646
+ array(jsonSchema),
5647
+ record(string(), jsonSchema)
5648
+ ]);
5649
+ });
5650
+ return jsonSchema;
5651
+ }
5548
5652
  const BOARD_ARROW_STROKE_SIZE = 2.5;
5549
- const BOARD_FONT_STACK = "\"Geist\", system-ui, -apple-system, \"Noto Sans CJK SC\", \"Noto Sans SC\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif";
5550
- const BOARD_MONO_FONT_STACK = "\"Geist Mono\", \"Fira Code\", ui-monospace, \"Noto Sans Mono CJK SC\", monospace";
5551
- const BOARD_CAMERA_TARGET = "camera";
5552
- const BoardIdSchema = string().min(1).max(160).regex(/^[^\s/]+$/, "ids must not contain whitespace or '/'").refine((value) => value !== BOARD_CAMERA_TARGET, "\"camera\" is reserved");
5553
- const finite = number$1().finite();
5554
- const nonNegative = finite.nonnegative();
5555
- const ratio = finite.min(0).max(1);
5556
- const BoardVec2Schema = object({
5557
- x: finite,
5558
- y: finite
5653
+ const BOARD_FONT_STACK = "\"Geist\", system-ui, -apple-system, \"Noto Sans CJK SC\", \"Noto Sans SC\", \"PingFang SC\", \"Microsoft YaHei\", \"Noto Sans Math\", \"Noto Sans Symbols 2\", \"DejaVu Sans\", \"FreeSans\", \"FreeSerif\", sans-serif";
5654
+ const BOARD_MONO_FONT_STACK = "\"Geist Mono\", \"Fira Code\", ui-monospace, \"Noto Sans Mono CJK SC\", \"Noto Sans Math\", \"DejaVu Sans\", \"FreeSans\", \"FreeSerif\", monospace";
5655
+ //#endregion
5656
+ //#region ../protocol/src/board-url.ts
5657
+ const BOARD_REMOTE_URL_MAX_LENGTH$1 = 4096;
5658
+ function parseIpv4$1(host) {
5659
+ const parts = host.split(".").map(Number);
5660
+ return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) ? parts : null;
5661
+ }
5662
+ function isBlockedIpv4$1(host) {
5663
+ const parts = parseIpv4$1(host);
5664
+ if (!parts) return false;
5665
+ const [first, second, third] = parts;
5666
+ if (first === 0 || first === 10 || first === 127) return true;
5667
+ if (first === 100 && second >= 64 && second <= 127) return true;
5668
+ if (first === 169 && second === 254) return true;
5669
+ if (first === 172 && second >= 16 && second <= 31) return true;
5670
+ if (first === 192 && second === 168) return true;
5671
+ if (first === 192 && second === 0 && (third === 0 || third === 2)) return true;
5672
+ if (first === 192 && second === 88 && third === 99) return true;
5673
+ if (first === 198 && (second === 18 || second === 19)) return true;
5674
+ if (first === 198 && second === 51 && third === 100) return true;
5675
+ if (first === 203 && second === 0 && third === 113) return true;
5676
+ return first >= 224;
5677
+ }
5678
+ function expandIpv6$1(host) {
5679
+ const [head, tail, extra] = host.toLowerCase().split("::");
5680
+ if (extra !== void 0) return null;
5681
+ const headParts = head ? head.split(":").filter(Boolean) : [];
5682
+ const tailParts = tail ? tail.split(":").filter(Boolean) : [];
5683
+ const missing = 8 - headParts.length - tailParts.length;
5684
+ if (missing < 0 || tail === void 0 && missing !== 0) return null;
5685
+ const parts = [
5686
+ ...headParts,
5687
+ ...Array.from({ length: missing }, () => "0"),
5688
+ ...tailParts
5689
+ ];
5690
+ if (parts.length !== 8 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) return null;
5691
+ return parts.map((part) => part.padStart(4, "0"));
5692
+ }
5693
+ function isBlockedIpv6$1(host) {
5694
+ const parts = expandIpv6$1(host);
5695
+ if (!parts) return true;
5696
+ if (parts.every((part) => part === "0000")) return true;
5697
+ if (parts.slice(0, 7).every((part) => part === "0000") && parts[7] === "0001") return true;
5698
+ if (parts.slice(0, 5).every((part) => part === "0000") && parts[5] === "ffff") {
5699
+ const high = Number.parseInt(parts[6] ?? "0", 16);
5700
+ const low = Number.parseInt(parts[7] ?? "0", 16);
5701
+ return isBlockedIpv4$1(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
5702
+ }
5703
+ if (parts.slice(0, 6).every((part) => part === "0000")) return true;
5704
+ const first = Number.parseInt(parts[0] ?? "0", 16);
5705
+ if ((first & 65024) === 64512) return true;
5706
+ if ((first & 65472) === 65152 || (first & 65472) === 65216) return true;
5707
+ if ((first & 65280) === 65280) return true;
5708
+ return parts[0] === "2001" && parts[1] === "0db8";
5709
+ }
5710
+ function isPublicBoardRemoteAddress$1(value) {
5711
+ const address = value.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
5712
+ if (parseIpv4$1(address)) return !isBlockedIpv4$1(address);
5713
+ return address.includes(":") && !isBlockedIpv6$1(address);
5714
+ }
5715
+ function isBlockedHost$1(hostname) {
5716
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
5717
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
5718
+ if (parseIpv4$1(host) || host.includes(":")) return !isPublicBoardRemoteAddress$1(host);
5719
+ return false;
5720
+ }
5721
+ /**
5722
+ * Normalize a browser-loadable public HTTP(S) URL. This blocks explicit local
5723
+ * addresses; any future server-side fetcher must additionally validate DNS
5724
+ * resolution to defend against rebinding.
5725
+ */
5726
+ function normalizeBoardRemoteUrl$1(value) {
5727
+ if (typeof value !== "string") return void 0;
5728
+ const input = value.trim();
5729
+ if (!input || input.length > 4096) return void 0;
5730
+ try {
5731
+ const url = new URL(input);
5732
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
5733
+ if (url.username || url.password || isBlockedHost$1(url.hostname)) return;
5734
+ const normalized = url.toString();
5735
+ return normalized.length <= 4096 ? normalized : void 0;
5736
+ } catch {
5737
+ return;
5738
+ }
5739
+ }
5740
+ string().max(BOARD_REMOTE_URL_MAX_LENGTH$1).refine((value) => normalizeBoardRemoteUrl$1(value) !== void 0, { message: "URL must be a public HTTP(S) URL without credentials" });
5741
+ const BOARD_CAMERA_TARGET$1 = "camera";
5742
+ const BoardKeySchema$1 = string().min(1).max(160).regex(/^[^\s/]+$/, "ids must not contain whitespace or '/'");
5743
+ const BoardIdSchema$1 = BoardKeySchema$1.refine((value) => value !== BOARD_CAMERA_TARGET$1, "\"camera\" is reserved");
5744
+ const finite$1 = number$1().finite();
5745
+ const nonNegative$1 = finite$1.nonnegative();
5746
+ const ratio$1 = finite$1.min(0).max(1);
5747
+ const BoardVec2Schema$1 = object({
5748
+ x: finite$1,
5749
+ y: finite$1
5559
5750
  }).strict();
5560
- const BoardSizeSchema = object({
5561
- width: finite.positive(),
5562
- height: finite.positive()
5751
+ const BoardSizeSchema$1 = object({
5752
+ width: finite$1.positive(),
5753
+ height: finite$1.positive()
5563
5754
  }).strict();
5564
- const BOARD_COLOR_TOKENS = [
5755
+ const BOARD_COLOR_TOKENS$1 = [
5565
5756
  "brand",
5566
5757
  "neutral",
5567
5758
  "black",
@@ -5572,66 +5763,85 @@ const BOARD_COLOR_TOKENS = [
5572
5763
  "violet",
5573
5764
  "rose"
5574
5765
  ];
5575
- const CSS_NAMED_COLORS = new Set("transparent aliceblue antiquewhite aqua aquamarine azure beige bisque blanchedalmond blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray grey greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen steelblue tan teal thistle tomato turquoise violet wheat whitesmoke yellow yellowgreen".split(" "));
5576
- function isBoardColorString(value) {
5766
+ const CSS_NAMED_COLORS$1 = new Set("transparent aliceblue antiquewhite aqua aquamarine azure beige bisque blanchedalmond blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray grey greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen steelblue tan teal thistle tomato turquoise violet wheat whitesmoke yellow yellowgreen".split(" "));
5767
+ function isBoardColorString$1(value) {
5577
5768
  const color = value.trim().toLowerCase();
5578
- return BOARD_COLOR_TOKENS.includes(color) || CSS_NAMED_COLORS.has(color) || /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(color) || /^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\([^()]*\)$/.test(color);
5769
+ return BOARD_COLOR_TOKENS$1.includes(color) || CSS_NAMED_COLORS$1.has(color) || /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(color) || /^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\([^()]*\)$/.test(color);
5579
5770
  }
5580
- const colorString = string().max(120).refine(isBoardColorString, "expected a palette token or a CSS color");
5581
- const BoardColorSchema = union([colorString, object({
5582
- light: colorString,
5583
- dark: colorString
5771
+ const colorString$1 = string().max(120).refine(isBoardColorString$1, "expected a palette token or a CSS color");
5772
+ const BoardColorSchema$1 = union([colorString$1, object({
5773
+ light: colorString$1,
5774
+ dark: colorString$1
5584
5775
  }).strict()]);
5585
- const BoardStyleSchema = object({
5586
- fill: BoardColorSchema.optional(),
5587
- fillOpacity: ratio.optional(),
5588
- stroke: BoardColorSchema.optional(),
5589
- strokeWidth: finite.min(0).max(64).optional(),
5776
+ const BoardStyleSchema$1 = object({
5777
+ fill: BoardColorSchema$1.optional(),
5778
+ fillOpacity: ratio$1.optional(),
5779
+ stroke: BoardColorSchema$1.optional(),
5780
+ strokeWidth: finite$1.min(0).max(64).optional(),
5590
5781
  dash: _enum([
5591
5782
  "solid",
5592
5783
  "dashed",
5593
5784
  "dotted"
5594
5785
  ]).optional(),
5595
- radius: nonNegative.optional(),
5596
- trim: ratio.optional()
5786
+ radius: nonNegative$1.optional(),
5787
+ trim: ratio$1.optional()
5597
5788
  }).strict();
5598
- function isSafeBoardSourcePath(value) {
5599
- return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
5789
+ function isSafeBoardSourcePath$1(value) {
5790
+ return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !/^[a-z][a-z\d+.-]*:/i.test(value.trimStart()) && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
5600
5791
  }
5601
- const srcSchema = string().refine(isSafeBoardSourcePath, "src must be a relative Space file path");
5602
- const itemEnvelope = {
5603
- parent: BoardIdSchema.optional(),
5604
- z: finite.optional(),
5605
- position: BoardVec2Schema.default({
5792
+ const srcSchema$1 = string().refine(isSafeBoardSourcePath$1, "src must be a relative Space file path");
5793
+ function parseBoardMediaSource$1(src) {
5794
+ if (isSafeBoardSourcePath$1(src)) return {
5795
+ kind: "file",
5796
+ value: src
5797
+ };
5798
+ return normalizeBoardRemoteUrl$1(src) ? {
5799
+ kind: "url",
5800
+ value: src
5801
+ } : null;
5802
+ }
5803
+ const BoardMediaSourceSchema$1 = string().max(4096).refine((src) => parseBoardMediaSource$1(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a stable public HTTP(S) URL; relative Space file paths remain supported.");
5804
+ const BoardScriptSchema$1 = object({
5805
+ src: srcSchema$1.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module"),
5806
+ params: record(string(), json()).default({}),
5807
+ targets: record(string().min(1).max(80), BoardIdSchema$1).default({}),
5808
+ seed: string().min(1).max(160).optional(),
5809
+ enabled: boolean().default(true)
5810
+ }).strict();
5811
+ const itemEnvelope$1 = {
5812
+ script: BoardScriptSchema$1.optional(),
5813
+ parent: BoardIdSchema$1.optional(),
5814
+ z: finite$1.optional(),
5815
+ position: BoardVec2Schema$1.default({
5606
5816
  x: 0,
5607
5817
  y: 0
5608
5818
  }),
5609
- rotation: finite.default(0),
5610
- scale: union([finite, BoardVec2Schema]).default(1),
5611
- origin: BoardVec2Schema.default({
5819
+ rotation: finite$1.default(0),
5820
+ scale: union([finite$1, BoardVec2Schema$1]).default(1),
5821
+ origin: BoardVec2Schema$1.default({
5612
5822
  x: .5,
5613
5823
  y: .5
5614
5824
  }),
5615
- opacity: ratio.default(1),
5616
- style: BoardStyleSchema.default({}),
5825
+ opacity: ratio$1.default(1),
5826
+ style: BoardStyleSchema$1.default({}),
5617
5827
  locked: boolean().optional(),
5618
5828
  metadata: record(string(), unknown()).optional()
5619
5829
  };
5620
- const boxed = (width, height) => ({
5621
- ...itemEnvelope,
5622
- size: BoardSizeSchema.default({
5830
+ const boxed$1 = (width, height) => ({
5831
+ ...itemEnvelope$1,
5832
+ size: BoardSizeSchema$1.default({
5623
5833
  width,
5624
5834
  height
5625
5835
  })
5626
5836
  });
5627
- const unsized = { ...itemEnvelope };
5628
- const fontSize = finite.min(2).max(512);
5629
- const BOARD_TEXT_ALIGNS = [
5837
+ const unsized$1 = { ...itemEnvelope$1 };
5838
+ const fontSize$1 = finite$1.min(2).max(512);
5839
+ const BOARD_TEXT_ALIGNS$1 = [
5630
5840
  "left",
5631
5841
  "center",
5632
5842
  "right"
5633
5843
  ];
5634
- const BOARD_SHAPE_GEOMETRIES = [
5844
+ const BOARD_SHAPE_GEOMETRIES$1 = [
5635
5845
  "rectangle",
5636
5846
  "rounded",
5637
5847
  "ellipse",
@@ -5639,55 +5849,56 @@ const BOARD_SHAPE_GEOMETRIES = [
5639
5849
  "triangle",
5640
5850
  "path"
5641
5851
  ];
5642
- const BoardFrameItemSchema = object({
5643
- ...boxed(480, 320),
5852
+ const BoardFrameItemSchema$1 = object({
5853
+ ...boxed$1(480, 320),
5644
5854
  type: literal("frame"),
5645
5855
  props: object({
5646
5856
  label: string().max(280).default(""),
5857
+ decoration: _enum(["frame", "none"]).optional(),
5647
5858
  clip: boolean().default(true)
5648
5859
  }).strict().prefault({})
5649
5860
  }).strict();
5650
- const BoardTextItemSchema = object({
5651
- ...unsized,
5861
+ const BoardTextItemSchema$1 = object({
5862
+ ...unsized$1,
5652
5863
  type: literal("text"),
5653
5864
  props: object({
5654
5865
  text: string().default(""),
5655
- fontSize: fontSize.default(24),
5656
- fontWeight: finite.min(100).max(900).default(500),
5866
+ fontSize: fontSize$1.default(24),
5867
+ fontWeight: finite$1.min(100).max(900).default(500),
5657
5868
  font: string().min(1).max(120).default("sans"),
5658
- align: _enum(BOARD_TEXT_ALIGNS).default("left"),
5659
- lineHeight: finite.min(.5).max(4).default(4 / 3),
5660
- width: finite.positive().optional(),
5661
- reveal: ratio.default(1),
5869
+ align: _enum(BOARD_TEXT_ALIGNS$1).default("left"),
5870
+ lineHeight: finite$1.min(.5).max(4).default(4 / 3),
5871
+ width: finite$1.positive().optional(),
5872
+ reveal: ratio$1.default(1),
5662
5873
  caret: boolean().default(false)
5663
5874
  }).strict().prefault({})
5664
5875
  }).strict();
5665
- const BoardShapeItemSchema = object({
5666
- ...boxed(240, 160),
5876
+ const BoardShapeItemSchema$1 = object({
5877
+ ...boxed$1(240, 160),
5667
5878
  type: literal("shape"),
5668
5879
  props: object({
5669
- geometry: _enum(BOARD_SHAPE_GEOMETRIES).default("rectangle"),
5880
+ geometry: _enum(BOARD_SHAPE_GEOMETRIES$1).default("rectangle"),
5670
5881
  path: string().min(1).max(2e5).optional(),
5671
5882
  text: string().default(""),
5672
- fontSize: fontSize.default(20),
5673
- align: _enum(BOARD_TEXT_ALIGNS).default("center")
5883
+ fontSize: fontSize$1.default(20),
5884
+ align: _enum(BOARD_TEXT_ALIGNS$1).default("center")
5674
5885
  }).strict().refine((props) => props.geometry !== "path" || Boolean(props.path), {
5675
5886
  message: "path geometry requires props.path",
5676
5887
  path: ["path"]
5677
5888
  }).prefault({})
5678
5889
  }).strict();
5679
- const BoardDrawPointSchema = object({
5680
- x: finite,
5681
- y: finite,
5682
- p: ratio.default(.5)
5890
+ const BoardDrawPointSchema$1 = object({
5891
+ x: finite$1,
5892
+ y: finite$1,
5893
+ p: ratio$1.default(.5)
5683
5894
  }).strict();
5684
- const BoardDrawItemSchema = object({
5685
- ...unsized,
5895
+ const BoardDrawItemSchema$1 = object({
5896
+ ...unsized$1,
5686
5897
  type: literal("draw"),
5687
- props: object({ points: array(BoardDrawPointSchema).min(1).max(1e5) }).strict()
5898
+ props: object({ points: array(BoardDrawPointSchema$1).min(1).max(1e5) }).strict()
5688
5899
  }).strict();
5689
- const BoardArrowEndSchema = union([BoardVec2Schema, object({
5690
- item: BoardIdSchema,
5900
+ const BoardArrowEndSchema$1 = union([BoardVec2Schema$1, object({
5901
+ item: BoardIdSchema$1,
5691
5902
  anchor: union([
5692
5903
  literal("auto"),
5693
5904
  object({
@@ -5697,110 +5908,110 @@ const BoardArrowEndSchema = union([BoardVec2Schema, object({
5697
5908
  "bottom",
5698
5909
  "left"
5699
5910
  ]),
5700
- offset: ratio.default(.5)
5911
+ offset: ratio$1.default(.5)
5701
5912
  }).strict(),
5702
5913
  object({
5703
- x: ratio,
5704
- y: ratio
5914
+ x: ratio$1,
5915
+ y: ratio$1
5705
5916
  }).strict()
5706
5917
  ]).default("auto"),
5707
5918
  port: string().min(1).max(120).optional()
5708
5919
  }).strict()]);
5709
- const BOARD_ARROW_ROUTES = [
5920
+ const BOARD_ARROW_ROUTES$1 = [
5710
5921
  "straight",
5711
5922
  "curve",
5712
5923
  "orthogonal"
5713
5924
  ];
5714
- const BoardArrowItemSchema = object({
5715
- ...unsized,
5925
+ const BoardArrowItemSchema$1 = object({
5926
+ ...unsized$1,
5716
5927
  type: literal("arrow"),
5717
5928
  props: object({
5718
- start: BoardArrowEndSchema,
5719
- end: BoardArrowEndSchema,
5720
- route: _enum(BOARD_ARROW_ROUTES).default("straight"),
5721
- bend: finite.min(-.85).max(.85).default(0),
5722
- waypoints: array(BoardVec2Schema).max(64).default([]),
5929
+ start: BoardArrowEndSchema$1,
5930
+ end: BoardArrowEndSchema$1,
5931
+ route: _enum(BOARD_ARROW_ROUTES$1).default("straight"),
5932
+ bend: finite$1.min(-.85).max(.85).default(0),
5933
+ waypoints: array(BoardVec2Schema$1).max(64).default([]),
5723
5934
  arrowStart: boolean().default(false),
5724
5935
  arrowEnd: boolean().default(true),
5725
5936
  label: string().max(280).default(""),
5726
- fontSize: fontSize.default(14),
5937
+ fontSize: fontSize$1.default(14),
5727
5938
  relation: string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional()
5728
5939
  }).strict()
5729
5940
  }).strict();
5730
- const fileProps = {
5731
- src: srcSchema,
5941
+ const fileProps$1 = {
5942
+ src: BoardMediaSourceSchema$1,
5732
5943
  snapshot: object({
5733
5944
  title: string().optional(),
5734
5945
  mimeType: string().optional(),
5735
- size: nonNegative.optional(),
5736
- mtimeMs: nonNegative.optional(),
5737
- naturalWidth: finite.positive().optional(),
5738
- naturalHeight: finite.positive().optional(),
5739
- durationMs: nonNegative.optional(),
5946
+ size: nonNegative$1.optional(),
5947
+ mtimeMs: nonNegative$1.optional(),
5948
+ naturalWidth: finite$1.positive().optional(),
5949
+ naturalHeight: finite$1.positive().optional(),
5950
+ durationMs: nonNegative$1.optional(),
5740
5951
  excerpt: string().optional(),
5741
5952
  coverPath: string().optional(),
5742
5953
  coverUrl: string().optional()
5743
5954
  }).strict().optional()
5744
5955
  };
5745
- const timedFileProps = {
5746
- ...fileProps,
5747
- time: nonNegative.optional()
5956
+ const timedFileProps$1 = {
5957
+ ...fileProps$1,
5958
+ time: nonNegative$1.optional()
5748
5959
  };
5749
- const BoardCropSchema = object({
5750
- x: ratio,
5751
- y: ratio,
5752
- w: ratio,
5753
- h: ratio
5960
+ const BoardCropSchema$1 = object({
5961
+ x: ratio$1,
5962
+ y: ratio$1,
5963
+ w: ratio$1,
5964
+ h: ratio$1
5754
5965
  }).strict();
5755
- const BoardImageItemSchema = object({
5756
- ...boxed(640, 360),
5966
+ const BoardImageItemSchema$1 = object({
5967
+ ...boxed$1(640, 360),
5757
5968
  type: literal("image"),
5758
5969
  props: object({
5759
- ...fileProps,
5760
- crop: BoardCropSchema.optional()
5970
+ ...fileProps$1,
5971
+ crop: BoardCropSchema$1.optional()
5761
5972
  }).strict()
5762
5973
  }).strict();
5763
- const BoardVideoItemSchema = object({
5764
- ...boxed(640, 360),
5974
+ const BoardVideoItemSchema$1 = object({
5975
+ ...boxed$1(640, 360),
5765
5976
  type: literal("video"),
5766
- props: object(timedFileProps).strict()
5977
+ props: object(timedFileProps$1).strict()
5767
5978
  }).strict();
5768
- const BoardAudioItemSchema = object({
5769
- ...boxed(480, 96),
5979
+ const BoardAudioItemSchema$1 = object({
5980
+ ...boxed$1(480, 96),
5770
5981
  type: literal("audio"),
5771
- props: object(timedFileProps).strict()
5982
+ props: object(timedFileProps$1).strict()
5772
5983
  }).strict();
5773
- const BoardFileItemSchema = object({
5774
- ...boxed(360, 220),
5984
+ const BoardFileItemSchema$1 = object({
5985
+ ...boxed$1(360, 220),
5775
5986
  type: literal("file"),
5776
- props: object(fileProps).strict()
5987
+ props: object(fileProps$1).strict()
5777
5988
  }).strict();
5778
- const httpsUrl = string().max(4096).url();
5779
- const artifactBase = {
5989
+ const httpsUrl$1 = string().max(4096).url();
5990
+ const artifactBase$1 = {
5780
5991
  id: string().min(1).max(240),
5781
5992
  title: string().max(240).optional(),
5782
- url: httpsUrl,
5993
+ url: httpsUrl$1,
5783
5994
  mimeType: string().max(160).optional()
5784
5995
  };
5785
- const BoardTaskArtifactSchema = discriminatedUnion("type", [
5996
+ const BoardTaskArtifactSchema$1 = discriminatedUnion("type", [
5786
5997
  object({
5787
- ...artifactBase,
5998
+ ...artifactBase$1,
5788
5999
  type: literal("image"),
5789
- naturalWidth: finite.positive().optional(),
5790
- naturalHeight: finite.positive().optional()
6000
+ naturalWidth: finite$1.positive().optional(),
6001
+ naturalHeight: finite$1.positive().optional()
5791
6002
  }).strict(),
5792
6003
  object({
5793
- ...artifactBase,
6004
+ ...artifactBase$1,
5794
6005
  type: literal("video"),
5795
- previewUrl: httpsUrl.optional(),
6006
+ previewUrl: httpsUrl$1.optional(),
5796
6007
  durationMs: number$1().int().positive().optional(),
5797
- naturalWidth: finite.positive().optional(),
5798
- naturalHeight: finite.positive().optional()
6008
+ naturalWidth: finite$1.positive().optional(),
6009
+ naturalHeight: finite$1.positive().optional()
5799
6010
  }).strict(),
5800
6011
  object({
5801
- ...artifactBase,
6012
+ ...artifactBase$1,
5802
6013
  type: literal("audio"),
5803
- previewUrl: httpsUrl.optional(),
6014
+ previewUrl: httpsUrl$1.optional(),
5804
6015
  durationMs: number$1().int().positive().optional()
5805
6016
  }).strict(),
5806
6017
  object({
@@ -5810,7 +6021,7 @@ const BoardTaskArtifactSchema = discriminatedUnion("type", [
5810
6021
  textExcerpt: string().min(1).max(480)
5811
6022
  }).strict()
5812
6023
  ]);
5813
- const BoardTaskSnapshotSchema = object({
6024
+ const BoardTaskSnapshotSchema$1 = object({
5814
6025
  taskType: string().min(1).max(120),
5815
6026
  status: _enum([
5816
6027
  "pending",
@@ -5822,81 +6033,78 @@ const BoardTaskSnapshotSchema = object({
5822
6033
  model: string().max(160).optional(),
5823
6034
  promptExcerpt: string().max(480).optional(),
5824
6035
  artifactCount: number$1().int().nonnegative(),
5825
- artifacts: array(BoardTaskArtifactSchema).max(6).default([]),
6036
+ artifacts: array(BoardTaskArtifactSchema$1).max(6).default([]),
5826
6037
  updatedAt: string().optional()
5827
6038
  }).strict();
5828
- const BoardTaskItemSchema = object({
5829
- ...boxed(420, 240),
6039
+ const BoardTaskItemSchema$1 = object({
6040
+ ...boxed$1(420, 240),
5830
6041
  type: literal("task"),
5831
6042
  props: object({
5832
6043
  taskRunId: string().min(1).max(160),
5833
- snapshot: BoardTaskSnapshotSchema
6044
+ snapshot: BoardTaskSnapshotSchema$1
5834
6045
  }).strict()
5835
6046
  }).strict();
5836
- const BOARD_EFFECT_KINDS = [
6047
+ const BOARD_EFFECT_KINDS$1 = [
5837
6048
  "particles",
5838
6049
  "trail",
5839
6050
  "impact",
5840
6051
  "flash",
5841
6052
  "glow"
5842
6053
  ];
5843
- const BoardEffectItemSchema = object({
5844
- ...boxed(240, 240),
6054
+ const BoardEffectItemSchema$1 = object({
6055
+ ...boxed$1(240, 240),
5845
6056
  type: literal("effect"),
5846
6057
  props: object({
5847
- kind: _enum(BOARD_EFFECT_KINDS),
5848
- rate: nonNegative.max(2e4).default(40),
5849
- life: finite.positive().max(6e4).default(1200),
5850
- particleSize: finite.positive().max(512).default(6),
5851
- speed: nonNegative.max(1e4).default(120),
5852
- direction: finite.default(-90),
5853
- spread: finite.min(0).max(360).default(360),
5854
- gravity: finite.default(0),
5855
- intensity: nonNegative.max(10).default(1),
5856
- follow: BoardIdSchema.optional(),
6058
+ kind: _enum(BOARD_EFFECT_KINDS$1),
6059
+ rate: nonNegative$1.max(2e4).default(40),
6060
+ life: finite$1.positive().max(6e4).default(1200),
6061
+ particleSize: finite$1.positive().max(512).default(6),
6062
+ speed: nonNegative$1.max(1e4).default(120),
6063
+ direction: finite$1.default(-90),
6064
+ spread: finite$1.min(0).max(360).default(360),
6065
+ gravity: finite$1.default(0),
6066
+ intensity: nonNegative$1.max(10).default(1),
6067
+ follow: BoardIdSchema$1.optional(),
5857
6068
  seed: string().min(1).max(160).optional()
5858
6069
  }).strict()
5859
6070
  }).strict();
5860
- const BoardSketchItemSchema = object({
5861
- ...boxed(480, 320),
5862
- type: literal("sketch"),
5863
- props: object({
5864
- src: srcSchema.refine((path) => /\.m?js$/.test(path), "sketch src must be a .js or .mjs module"),
5865
- params: record(string(), unknown()).default({}),
5866
- seed: string().min(1).max(160).optional()
5867
- }).strict()
6071
+ const BoardCustomItemSchema$1 = object({
6072
+ ...boxed$1(480, 320),
6073
+ type: literal("custom"),
6074
+ script: BoardScriptSchema$1,
6075
+ props: object({}).strict().default({})
5868
6076
  }).strict();
5869
- const BOARD_EXTENSION_TYPE_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z0-9-]+)+$/;
5870
- const BoardExtensionItemSchema = object({
5871
- ...itemEnvelope,
5872
- size: BoardSizeSchema.default({
6077
+ const BOARD_EXTENSION_TYPE_PATTERN$1 = /^[a-z][a-z0-9]*(?:\.[a-z0-9-]+)+$/;
6078
+ const BoardExtensionItemSchema$1 = object({
6079
+ ...itemEnvelope$1,
6080
+ size: BoardSizeSchema$1.default({
5873
6081
  width: 240,
5874
6082
  height: 160
5875
6083
  }),
5876
- type: string().regex(BOARD_EXTENSION_TYPE_PATTERN),
6084
+ type: string().regex(BOARD_EXTENSION_TYPE_PATTERN$1),
5877
6085
  props: record(string(), unknown()).default({})
5878
6086
  }).strict();
5879
- const BOARD_ITEM_SCHEMAS = {
5880
- frame: BoardFrameItemSchema,
5881
- text: BoardTextItemSchema,
5882
- shape: BoardShapeItemSchema,
5883
- draw: BoardDrawItemSchema,
5884
- arrow: BoardArrowItemSchema,
5885
- image: BoardImageItemSchema,
5886
- video: BoardVideoItemSchema,
5887
- audio: BoardAudioItemSchema,
5888
- file: BoardFileItemSchema,
5889
- task: BoardTaskItemSchema,
5890
- effect: BoardEffectItemSchema,
5891
- sketch: BoardSketchItemSchema
5892
- };
5893
- const BOARD_ITEM_TYPES = Object.keys(BOARD_ITEM_SCHEMAS);
6087
+ const BOARD_ITEM_SCHEMAS$1 = {
6088
+ frame: BoardFrameItemSchema$1,
6089
+ text: BoardTextItemSchema$1,
6090
+ shape: BoardShapeItemSchema$1,
6091
+ draw: BoardDrawItemSchema$1,
6092
+ arrow: BoardArrowItemSchema$1,
6093
+ image: BoardImageItemSchema$1,
6094
+ video: BoardVideoItemSchema$1,
6095
+ audio: BoardAudioItemSchema$1,
6096
+ file: BoardFileItemSchema$1,
6097
+ task: BoardTaskItemSchema$1,
6098
+ effect: BoardEffectItemSchema$1,
6099
+ custom: BoardCustomItemSchema$1
6100
+ };
6101
+ const BOARD_ITEM_TYPES$1 = Object.keys(BOARD_ITEM_SCHEMAS$1);
5894
6102
  function isBuiltinItemType(type) {
5895
- return Object.hasOwn(BOARD_ITEM_SCHEMAS, type);
6103
+ return Object.hasOwn(BOARD_ITEM_SCHEMAS$1, type);
5896
6104
  }
5897
6105
  function boardItemSchema(type) {
5898
- if (isBuiltinItemType(type)) return BOARD_ITEM_SCHEMAS[type];
5899
- return BOARD_EXTENSION_TYPE_PATTERN.test(type) ? BoardExtensionItemSchema : null;
6106
+ if (isBuiltinItemType(type)) return BOARD_ITEM_SCHEMAS$1[type];
6107
+ return BOARD_EXTENSION_TYPE_PATTERN$1.test(type) ? BoardExtensionItemSchema$1 : null;
5900
6108
  }
5901
6109
  function issuesToDiagnostics(error, path) {
5902
6110
  return error.issues.map((issue) => {
@@ -5908,7 +6116,7 @@ function issuesToDiagnostics(error, path) {
5908
6116
  };
5909
6117
  });
5910
6118
  }
5911
- function parseBoardItem(value, path = "item") {
6119
+ function parseBoardItem$1(value, path = "item") {
5912
6120
  const type = value && typeof value === "object" && !Array.isArray(value) ? value.type : void 0;
5913
6121
  if (typeof type !== "string") return {
5914
6122
  ok: false,
@@ -5926,7 +6134,7 @@ function parseBoardItem(value, path = "item") {
5926
6134
  severity: "error",
5927
6135
  code: "INVALID_ITEM",
5928
6136
  path: `${path}.type`,
5929
- message: `unknown type ${type}; expected ${BOARD_ITEM_TYPES.join(", ")} or a namespaced extension type`
6137
+ message: `unknown type ${type}; expected ${BOARD_ITEM_TYPES$1.join(", ")} or a namespaced extension type`
5930
6138
  }]
5931
6139
  };
5932
6140
  const parsed = schema.safeParse(value);
@@ -5938,7 +6146,7 @@ function parseBoardItem(value, path = "item") {
5938
6146
  diagnostics: issuesToDiagnostics(parsed.error, path)
5939
6147
  };
5940
6148
  }
5941
- const CSS_EASE_KEYWORDS = /* @__PURE__ */ new Set([
6149
+ const CSS_EASE_KEYWORDS$1 = /* @__PURE__ */ new Set([
5942
6150
  "linear",
5943
6151
  "ease",
5944
6152
  "ease-in",
@@ -5947,9 +6155,9 @@ const CSS_EASE_KEYWORDS = /* @__PURE__ */ new Set([
5947
6155
  "step-start",
5948
6156
  "step-end"
5949
6157
  ]);
5950
- function isBoardEase(value) {
6158
+ function isBoardEase$1(value) {
5951
6159
  const ease = value.trim();
5952
- if (CSS_EASE_KEYWORDS.has(ease)) return true;
6160
+ if (CSS_EASE_KEYWORDS$1.has(ease)) return true;
5953
6161
  const bezier = /^cubic-bezier\(\s*([^,()]+),\s*([^,()]+),\s*([^,()]+),\s*([^,()]+)\)$/.exec(ease);
5954
6162
  if (bezier) {
5955
6163
  const [x1, , x2] = bezier.slice(1).map(Number);
@@ -5957,16 +6165,16 @@ function isBoardEase(value) {
5957
6165
  }
5958
6166
  return /^steps\(\s*[1-9]\d*\s*(?:,\s*(?:start|end|jump-start|jump-end|jump-none|jump-both)\s*)?\)$/.test(ease);
5959
6167
  }
5960
- const BoardEaseSchema = string().max(120).refine(isBoardEase, "expected a CSS easing: linear, ease-in-out, cubic-bezier(…) or steps(…)");
5961
- const BoardKeyframeSchema = object({
5962
- at: nonNegative,
6168
+ const BoardEaseSchema$1 = string().max(120).refine(isBoardEase$1, "expected a CSS easing: linear, ease-in-out, cubic-bezier(…) or steps(…)");
6169
+ const BoardKeyframeSchema$1 = object({
6170
+ at: nonNegative$1,
5963
6171
  value: unknown(),
5964
- ease: BoardEaseSchema.optional()
6172
+ ease: BoardEaseSchema$1.optional()
5965
6173
  }).strict();
5966
- const BoardTrackSchema = object({
6174
+ const BoardTrackSchema$1 = object({
5967
6175
  target: string().min(1).max(160),
5968
6176
  property: string().min(1).max(200),
5969
- keyframes: array(BoardKeyframeSchema).min(1).max(1e5),
6177
+ keyframes: array(BoardKeyframeSchema$1).min(1).max(1e5),
5970
6178
  composite: _enum(["replace", "add"]).default("replace"),
5971
6179
  interpolation: _enum([
5972
6180
  "auto",
@@ -5985,65 +6193,65 @@ const BoardTrackSchema = object({
5985
6193
  ]
5986
6194
  });
5987
6195
  });
5988
- const BoardMarkerSchema = object({
5989
- at: nonNegative,
6196
+ const BoardMarkerSchema$1 = object({
6197
+ at: nonNegative$1,
5990
6198
  label: string().max(280).optional(),
5991
6199
  pause: boolean().optional()
5992
6200
  }).strict();
5993
- const BoardAnimationHeaderSchema = object({
6201
+ const BoardAnimationHeaderSchema$1 = object({
5994
6202
  name: string().max(280).optional(),
5995
- duration: finite.positive().max(864e5),
6203
+ duration: finite$1.positive().max(864e5),
5996
6204
  play: _enum([
5997
6205
  "manual",
5998
6206
  "auto",
5999
6207
  "always"
6000
6208
  ]).default("manual"),
6001
- delay: nonNegative.default(0),
6209
+ delay: nonNegative$1.default(0),
6002
6210
  loop: boolean().default(false),
6003
6211
  end: _enum(["hold", "reset"]).default("hold"),
6004
- markers: array(BoardMarkerSchema).max(1e4).default([])
6212
+ markers: array(BoardMarkerSchema$1).max(1e4).default([])
6005
6213
  }).strict();
6006
- BoardAnimationHeaderSchema.extend({ tracks: record(BoardIdSchema, BoardTrackSchema).default({}) }).strict();
6007
- const BOARD_ENTER_PRESETS = [
6214
+ BoardAnimationHeaderSchema$1.extend({ tracks: record(BoardKeySchema$1, BoardTrackSchema$1).default({}) }).strict();
6215
+ const BOARD_ENTER_PRESETS$1 = [
6008
6216
  "fade-in",
6009
6217
  "rise",
6010
6218
  "drop",
6011
6219
  "pop",
6012
6220
  "deal"
6013
6221
  ];
6014
- const BoardBackgroundSchema = object({
6222
+ const BoardBackgroundSchema$1 = object({
6015
6223
  kind: _enum([
6016
6224
  "solid",
6017
6225
  "dots",
6018
6226
  "grid",
6019
6227
  "image"
6020
6228
  ]).default("dots"),
6021
- color: BoardColorSchema.optional(),
6229
+ color: BoardColorSchema$1.optional(),
6022
6230
  imageUrl: string().url().max(4096).optional(),
6023
6231
  fit: _enum([
6024
6232
  "cover",
6025
6233
  "contain",
6026
6234
  "repeat"
6027
6235
  ]).optional(),
6028
- opacity: ratio.optional()
6236
+ opacity: ratio$1.optional()
6029
6237
  }).strict();
6030
- const BoardSettingsSchema = object({
6238
+ const BoardSettingsSchema$1 = object({
6031
6239
  title: string().min(1).max(255).optional(),
6032
- background: BoardBackgroundSchema.default({ kind: "dots" }),
6240
+ background: BoardBackgroundSchema$1.default({ kind: "dots" }),
6033
6241
  grid: object({
6034
6242
  visible: boolean().default(false),
6035
- size: finite.min(4).max(512).default(24)
6243
+ size: finite$1.min(4).max(512).default(24)
6036
6244
  }).strict().optional(),
6037
6245
  enter: object({
6038
- preset: _enum(BOARD_ENTER_PRESETS),
6039
- duration: finite.positive().max(1e4).optional()
6246
+ preset: _enum(BOARD_ENTER_PRESETS$1),
6247
+ duration: finite$1.positive().max(1e4).optional()
6040
6248
  }).strict().optional()
6041
6249
  }).strict();
6042
- const patchObject = record(string(), unknown());
6043
- const BoardPatchSchema = object({
6044
- board: patchObject.nullable().optional(),
6045
- items: record(BoardIdSchema, patchObject.nullable()).optional(),
6046
- animations: record(BoardIdSchema, patchObject.and(object({ tracks: record(BoardIdSchema, patchObject.nullable()).optional() })).nullable()).optional()
6250
+ const patchObject$1 = record(string(), unknown());
6251
+ const BoardPatchSchema$1 = object({
6252
+ board: patchObject$1.nullable().optional(),
6253
+ items: record(BoardIdSchema$1, patchObject$1.nullable()).optional(),
6254
+ animations: record(BoardIdSchema$1, patchObject$1.and(object({ tracks: record(BoardKeySchema$1, patchObject$1.nullable()).optional() })).nullable()).optional()
6047
6255
  }).strict();
6048
6256
  //#endregion
6049
6257
  //#region ../protocol/src/board-layout.ts
@@ -6592,7 +6800,8 @@ const NOT_ANIMATABLE = /* @__PURE__ */ new Set([
6592
6800
  "parent",
6593
6801
  "z",
6594
6802
  "locked",
6595
- "metadata"
6803
+ "metadata",
6804
+ "script"
6596
6805
  ]);
6597
6806
  const CAMERA_PROPERTIES = {
6598
6807
  focus: {
@@ -6611,7 +6820,7 @@ const CAMERA_PROPERTIES = {
6611
6820
  },
6612
6821
  center: {
6613
6822
  kind: "vec2",
6614
- schema: BoardVec2Schema
6823
+ schema: BoardVec2Schema$1
6615
6824
  },
6616
6825
  shake: {
6617
6826
  kind: "number",
@@ -6625,7 +6834,7 @@ const ANIMATION_PROPERTIES = { time: {
6625
6834
  function kindOf(schema, path) {
6626
6835
  const leaf = path[path.length - 1];
6627
6836
  if (path.length === 1 && leaf === "scale") return "scale";
6628
- if (schema === BoardColorSchema || unwrap(schema) === BoardColorSchema) return "color";
6837
+ if (schema === BoardColorSchema$1 || unwrap(schema) === BoardColorSchema$1) return "color";
6629
6838
  if (def(unwrap(schema)).type === "number") return "number";
6630
6839
  const shape = unwrapObjectShape(schema);
6631
6840
  if (shape && Object.keys(shape).length === 2 && "x" in shape && "y" in shape) return "vec2";
@@ -6701,7 +6910,7 @@ function trackReferences(track) {
6701
6910
  if (track.target === "camera" && track.property === "focus") {
6702
6911
  for (const keyframe of track.keyframes) if (typeof keyframe.value === "string") refs.add(keyframe.value);
6703
6912
  }
6704
- refs.delete(BOARD_CAMERA_TARGET);
6913
+ refs.delete(BOARD_CAMERA_TARGET$1);
6705
6914
  return [...refs];
6706
6915
  }
6707
6916
  function pruneBoardTrack(track, deleted) {
@@ -6728,7 +6937,7 @@ function stripId(value) {
6728
6937
  return rest;
6729
6938
  }
6730
6939
  function applyBoardPatch(state, input, options = {}) {
6731
- const parsedPatch = BoardPatchSchema.safeParse(input);
6940
+ const parsedPatch = BoardPatchSchema$1.safeParse(input);
6732
6941
  if (!parsedPatch.success) return {
6733
6942
  ok: false,
6734
6943
  diagnostics: parsedPatch.error.issues.map((issue) => error(issue.path.map(String).join("."), "INVALID_PATCH", issue.message))
@@ -6753,7 +6962,7 @@ function applyBoardPatch(state, input, options = {}) {
6753
6962
  };
6754
6963
  if (patch.board !== void 0) {
6755
6964
  const merged = patch.board === null ? {} : mergePatch(state.settings(), patch.board);
6756
- const parsed = BoardSettingsSchema.safeParse(merged);
6965
+ const parsed = BoardSettingsSchema$1.safeParse(merged);
6757
6966
  if (parsed.success) nextSettings = parsed.data;
6758
6967
  else for (const issue of parsed.error.issues) diagnostics.push(error(["board", ...issue.path.map(String)].join("."), "INVALID_BOARD", issue.message));
6759
6968
  }
@@ -6773,7 +6982,7 @@ function applyBoardPatch(state, input, options = {}) {
6773
6982
  candidate.z = current?.z ?? top + 1;
6774
6983
  if (!current) zByParent.set(key, top + 1);
6775
6984
  }
6776
- const parsed = parseBoardItem(candidate, `items.${id}`);
6985
+ const parsed = parseBoardItem$1(candidate, `items.${id}`);
6777
6986
  if (!parsed.ok) {
6778
6987
  diagnostics.push(...parsed.diagnostics);
6779
6988
  continue;
@@ -6799,7 +7008,7 @@ function applyBoardPatch(state, input, options = {}) {
6799
7008
  ...headerPatch,
6800
7009
  ...headerPatch.duration === void 0 ? { duration: lastKeyframeAt([...Object.values(tracks ?? {}), ...state.trackIds(id).map((trackId) => state.track(id, trackId))]) } : {}
6801
7010
  };
6802
- const parsed = BoardAnimationHeaderSchema.safeParse(header);
7011
+ const parsed = BoardAnimationHeaderSchema$1.safeParse(header);
6803
7012
  if (!parsed.success) {
6804
7013
  for (const issue of parsed.error.issues) diagnostics.push(error([
6805
7014
  "animations",
@@ -6816,7 +7025,7 @@ function applyBoardPatch(state, input, options = {}) {
6816
7025
  if (existing) setTrack(id, trackId, null);
6817
7026
  continue;
6818
7027
  }
6819
- const parsed = BoardTrackSchema.safeParse(existing ? mergePatch(existing, trackPatch) : trackPatch);
7028
+ const parsed = BoardTrackSchema$1.safeParse(existing ? mergePatch(existing, trackPatch) : trackPatch);
6820
7029
  if (!parsed.success) {
6821
7030
  for (const issue of parsed.error.issues) diagnostics.push(error([
6822
7031
  "animations",
@@ -6981,7 +7190,7 @@ function applyBoardPatch(state, input, options = {}) {
6981
7190
  function validateTrack(path, animationId, value, item, animation, track, state) {
6982
7191
  const diagnostics = [];
6983
7192
  let targetType;
6984
- if (value.target === "camera") targetType = BOARD_CAMERA_TARGET;
7193
+ if (value.target === "camera") targetType = BOARD_CAMERA_TARGET$1;
6985
7194
  else if (item(value.target)) targetType = item(value.target).type;
6986
7195
  else if (animation(value.target)) {
6987
7196
  targetType = "animation";
@@ -7105,7 +7314,7 @@ function parseBoardDocument(input) {
7105
7314
  diagnostics: [error("", "INVALID_DOCUMENT", "expected an object")]
7106
7315
  };
7107
7316
  const result = applyBoardPatchToDocument({
7108
- board: BoardSettingsSchema.parse({}),
7317
+ board: BoardSettingsSchema$1.parse({}),
7109
7318
  items: {},
7110
7319
  animations: {}
7111
7320
  }, {
@@ -7119,40 +7328,436 @@ function parseBoardDocument(input) {
7119
7328
  } : result;
7120
7329
  }
7121
7330
  //#endregion
7122
- //#region ../sdk/src/board/model/animation.ts
7123
- const linear = (x) => x;
7124
- function cubicBezier(x1, y1, x2, y2) {
7125
- const cx = 3 * x1;
7126
- const bx = 3 * (x2 - x1) - cx;
7127
- const ax = 1 - cx - bx;
7128
- const cy = 3 * y1;
7129
- const by = 3 * (y2 - y1) - cy;
7130
- const ay = 1 - cy - by;
7131
- const sampleX = (t) => ((ax * t + bx) * t + cx) * t;
7132
- const sampleY = (t) => ((ay * t + by) * t + cy) * t;
7133
- const slopeX = (t) => (3 * ax * t + 2 * bx) * t + cx;
7134
- return (x) => {
7135
- if (x <= 0) return 0;
7136
- if (x >= 1) return 1;
7137
- let t = x;
7138
- for (let index = 0; index < 8; index += 1) {
7139
- const error = sampleX(t) - x;
7140
- if (Math.abs(error) < 1e-6) return sampleY(t);
7141
- const slope = slopeX(t);
7142
- if (Math.abs(slope) < 1e-6) break;
7143
- t -= error / slope;
7144
- }
7145
- let low = 0;
7146
- let high = 1;
7147
- t = x;
7148
- for (let index = 0; index < 32; index += 1) {
7149
- const value = sampleX(t);
7150
- if (Math.abs(value - x) < 1e-6) break;
7151
- if (value < x) low = t;
7152
- else high = t;
7153
- t = (low + high) / 2;
7154
- }
7155
- return sampleY(t);
7331
+ //#region ../sdk/src/board/model/script-index.ts
7332
+ const scriptsByItems = /* @__PURE__ */ new WeakMap();
7333
+ function boardScriptItems(document) {
7334
+ let scripts = scriptsByItems.get(document.items);
7335
+ if (!scripts) {
7336
+ scripts = Object.entries(document.items).filter(([, item]) => item.script?.enabled);
7337
+ scriptsByItems.set(document.items, scripts);
7338
+ }
7339
+ return scripts;
7340
+ }
7341
+ function hasBoardScripts(document) {
7342
+ return boardScriptItems(document).length > 0;
7343
+ }
7344
+ //#endregion
7345
+ //#region ../sdk/src/board/model/geometry.ts
7346
+ function worldPoint(x, y) {
7347
+ return {
7348
+ x,
7349
+ y
7350
+ };
7351
+ }
7352
+ const MIN_BOARD_ZOOM = .05;
7353
+ const DEFAULT_BOARD_VIEWPORT = {
7354
+ x: 0,
7355
+ y: 0,
7356
+ zoom: 1
7357
+ };
7358
+ function clampZoom(zoom) {
7359
+ const value = Number.isFinite(zoom) ? zoom : DEFAULT_BOARD_VIEWPORT.zoom;
7360
+ return Math.min(8, Math.max(MIN_BOARD_ZOOM, value));
7361
+ }
7362
+ function normalizeViewport(viewport, fallback = DEFAULT_BOARD_VIEWPORT) {
7363
+ const fallbackX = Number.isFinite(fallback.x) ? fallback.x : DEFAULT_BOARD_VIEWPORT.x;
7364
+ const fallbackY = Number.isFinite(fallback.y) ? fallback.y : DEFAULT_BOARD_VIEWPORT.y;
7365
+ const fallbackZoom = Number.isFinite(fallback.zoom) ? clampZoom(fallback.zoom) : DEFAULT_BOARD_VIEWPORT.zoom;
7366
+ return {
7367
+ x: Number.isFinite(viewport.x) ? viewport.x : fallbackX,
7368
+ y: Number.isFinite(viewport.y) ? viewport.y : fallbackY,
7369
+ zoom: Number.isFinite(viewport.zoom) ? clampZoom(viewport.zoom) : fallbackZoom
7370
+ };
7371
+ }
7372
+ function degToRad(degrees) {
7373
+ return degrees * Math.PI / 180;
7374
+ }
7375
+ function frameRect(frame) {
7376
+ return {
7377
+ x: frame.x,
7378
+ y: frame.y,
7379
+ width: frame.width,
7380
+ height: frame.height
7381
+ };
7382
+ }
7383
+ function rectCenter(rect) {
7384
+ return worldPoint(rect.x + rect.width / 2, rect.y + rect.height / 2);
7385
+ }
7386
+ function rectsIntersect(a, b) {
7387
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
7388
+ }
7389
+ function unionRects(rects) {
7390
+ if (rects.length === 0) return null;
7391
+ let minX = Number.POSITIVE_INFINITY;
7392
+ let minY = Number.POSITIVE_INFINITY;
7393
+ let maxX = Number.NEGATIVE_INFINITY;
7394
+ let maxY = Number.NEGATIVE_INFINITY;
7395
+ for (const rect of rects) {
7396
+ minX = Math.min(minX, rect.x);
7397
+ minY = Math.min(minY, rect.y);
7398
+ maxX = Math.max(maxX, rect.x + rect.width);
7399
+ maxY = Math.max(maxY, rect.y + rect.height);
7400
+ }
7401
+ return {
7402
+ x: minX,
7403
+ y: minY,
7404
+ width: maxX - minX,
7405
+ height: maxY - minY
7406
+ };
7407
+ }
7408
+ function itemBounds(frame) {
7409
+ const rotation = frame.rotation || 0;
7410
+ if (rotation === 0) return frameRect(frame);
7411
+ const center = rectCenter(frameRect(frame));
7412
+ const halfW = frame.width / 2;
7413
+ const halfH = frame.height / 2;
7414
+ const rad = degToRad(rotation);
7415
+ const cos = Math.abs(Math.cos(rad));
7416
+ const sin = Math.abs(Math.sin(rad));
7417
+ const boundW = halfW * cos + halfH * sin;
7418
+ const boundH = halfW * sin + halfH * cos;
7419
+ return {
7420
+ x: center.x - boundW,
7421
+ y: center.y - boundH,
7422
+ width: boundW * 2,
7423
+ height: boundH * 2
7424
+ };
7425
+ }
7426
+ function cameraForState(state, surface) {
7427
+ const zoom = clampZoom(state.zoom);
7428
+ return normalizeViewport({
7429
+ x: surface.width / 2 - state.centerX * zoom,
7430
+ y: surface.height / 2 - state.centerY * zoom,
7431
+ zoom
7432
+ });
7433
+ }
7434
+ function cameraForRect(content, surface, options = {}) {
7435
+ const padding = Math.max(0, Number.isFinite(options.padding) ? options.padding : 32);
7436
+ const availableW = Math.max(1, surface.width - padding * 2);
7437
+ const availableH = Math.max(1, surface.height - padding * 2);
7438
+ const fitZoom = options.fit === "cover" ? Math.max(availableW / content.width, availableH / content.height) : Math.min(availableW / content.width, availableH / content.height);
7439
+ const minZoom = clampZoom(options.minZoom ?? .05);
7440
+ const maxZoom = clampZoom(options.maxZoom ?? 8);
7441
+ const zoom = Math.min(Math.max(fitZoom, Math.min(minZoom, maxZoom)), Math.max(minZoom, maxZoom));
7442
+ const center = rectCenter(content);
7443
+ return cameraForState({
7444
+ centerX: center.x,
7445
+ centerY: center.y,
7446
+ zoom
7447
+ }, surface);
7448
+ }
7449
+ function rectForCameraFocus(focus, getFrame) {
7450
+ if (typeof focus !== "string") return focus;
7451
+ const frame = getFrame(focus);
7452
+ return frame ? itemBounds(frame) : null;
7453
+ }
7454
+ function cameraForFocus(focus, getFrame, surface, options = {}) {
7455
+ const rect = rectForCameraFocus(focus, getFrame);
7456
+ return rect ? cameraForRect(rect, surface, options) : null;
7457
+ }
7458
+ //#endregion
7459
+ //#region ../sdk/src/board/model/script-scene.ts
7460
+ function parseBoardItem(value) {
7461
+ const parsed = parseBoardItem$1(value);
7462
+ if (!parsed.ok) throw new Error(parsed.diagnostics[0]?.message ?? "Invalid script item.");
7463
+ return parsed.item;
7464
+ }
7465
+ const DISPLAY_FIELDS = /* @__PURE__ */ new Set([
7466
+ "position",
7467
+ "size",
7468
+ "rotation",
7469
+ "scale",
7470
+ "opacity",
7471
+ "style",
7472
+ "props"
7473
+ ]);
7474
+ function boardScriptScope(document, id) {
7475
+ const scope = /* @__PURE__ */ new Set([id, ...Object.values(document.items[id]?.script?.targets ?? {}).filter((target) => Boolean(document.items[target]))]);
7476
+ if (document.items[id]?.type !== "frame") return scope;
7477
+ for (const candidate of Object.keys(document.items)) {
7478
+ let parent = document.items[candidate]?.parent;
7479
+ for (let depth = 0; parent && depth < 64; depth++) {
7480
+ if (parent === id) {
7481
+ scope.add(candidate);
7482
+ break;
7483
+ }
7484
+ parent = document.items[parent]?.parent;
7485
+ }
7486
+ }
7487
+ return scope;
7488
+ }
7489
+ function validateBoardScriptOutput(output, scope) {
7490
+ if (!output || typeof output !== "object" || !output.patches || !output.items || !output.hits || !Array.isArray(output.listens)) throw new Error("Invalid script output.");
7491
+ if (JSON.stringify(output).length > 1e6) throw new Error("Script output exceeds the 1 MB limit.");
7492
+ if (output.space !== void 0 && output.space !== "world" && output.space !== "screen") throw new Error("Invalid script scene space.");
7493
+ if (output.parameters) {
7494
+ if (Object.keys(output.parameters).length > 64) throw new Error("A script may declare at most 64 parameters.");
7495
+ for (const spec of Object.values(output.parameters)) {
7496
+ if (!spec || ![
7497
+ "number",
7498
+ "boolean",
7499
+ "string",
7500
+ "color"
7501
+ ].includes(spec.type)) throw new Error("Invalid script parameter type.");
7502
+ if (spec.label !== void 0 && typeof spec.label !== "string") throw new Error("Invalid script parameter label.");
7503
+ if ([
7504
+ spec.min,
7505
+ spec.max,
7506
+ spec.step
7507
+ ].some((value) => value !== void 0 && !Number.isFinite(value))) throw new Error("Invalid script parameter range.");
7508
+ }
7509
+ }
7510
+ if (Object.keys(output.items).length > 1e3 || Object.keys(output.hits).length > 1e3) throw new Error("A script may create at most 1000 temporary items or hit regions.");
7511
+ for (const [id, patch] of Object.entries(output.patches)) {
7512
+ if (!scope.has(id)) throw new Error(`Item is outside the script scope: ${id}`);
7513
+ if (!patch || typeof patch !== "object" || Object.keys(patch).some((key) => !DISPLAY_FIELDS.has(key))) throw new Error("Scripts may only present display properties.");
7514
+ }
7515
+ for (const hit of Object.values(output.hits)) {
7516
+ if (hit.cursor && ![
7517
+ "default",
7518
+ "pointer",
7519
+ "grab",
7520
+ "grabbing",
7521
+ "crosshair",
7522
+ "text",
7523
+ "move",
7524
+ "ew-resize",
7525
+ "ns-resize",
7526
+ "not-allowed"
7527
+ ].includes(hit.cursor)) throw new Error("Invalid script cursor.");
7528
+ if (![
7529
+ hit.x,
7530
+ hit.y,
7531
+ hit.width,
7532
+ hit.height
7533
+ ].every(Number.isFinite) || hit.width < 0 || hit.height < 0) throw new Error("Invalid script hit region.");
7534
+ }
7535
+ }
7536
+ function scriptTemporaryPrefix(owner) {
7537
+ let hash = 2166136261;
7538
+ for (const char of owner) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619) >>> 0;
7539
+ return `~script-${hash.toString(36)}-`;
7540
+ }
7541
+ const preparedOutputs = /* @__PURE__ */ new WeakMap();
7542
+ const preparedScopes = /* @__PURE__ */ new WeakMap();
7543
+ function prepareBoardScriptOutput(document, owner, output) {
7544
+ const previous = preparedOutputs.get(output);
7545
+ if (previous?.document === document && previous.owner === owner) return previous;
7546
+ let scopes = preparedScopes.get(document);
7547
+ if (!scopes) {
7548
+ scopes = /* @__PURE__ */ new Map();
7549
+ preparedScopes.set(document, scopes);
7550
+ }
7551
+ let scope = scopes.get(owner);
7552
+ if (!scope) {
7553
+ scope = boardScriptScope(document, owner);
7554
+ scopes.set(owner, scope);
7555
+ }
7556
+ validateBoardScriptOutput(output, scope);
7557
+ const presented = /* @__PURE__ */ new Map();
7558
+ for (const [id, patch] of Object.entries(output.patches)) {
7559
+ const base = document.items[id];
7560
+ if (base) presented.set(id, {
7561
+ base,
7562
+ item: parseBoardItem(mergePatch(base, patch))
7563
+ });
7564
+ }
7565
+ const prefix = scriptTemporaryPrefix(owner);
7566
+ const rootId = `${prefix}root`;
7567
+ if (document.items[rootId]) throw new Error("Temporary item ID conflicts with the document.");
7568
+ const items = /* @__PURE__ */ new Map();
7569
+ const parsed = new Map(Object.entries(output.items).map(([id, value]) => [id, parseBoardItem(value)]));
7570
+ for (const [local, value] of parsed) {
7571
+ if (!/^[a-zA-Z0-9_-]{1,80}$/.test(local) || local === "root") throw new Error("Temporary IDs must be 1–80 letters, digits, underscores or hyphens; root is reserved.");
7572
+ if (value.script) throw new Error("Temporary items cannot start scripts.");
7573
+ if (value.parent && !(value.parent in output.items)) throw new Error("Temporary parents must belong to the same instance.");
7574
+ let cursor = value.parent;
7575
+ const visited = /* @__PURE__ */ new Set([local]);
7576
+ while (cursor) {
7577
+ if (visited.has(cursor)) throw new Error("Temporary parents must not form a cycle.");
7578
+ visited.add(cursor);
7579
+ const parentItem = parsed.get(cursor);
7580
+ if (!parentItem) throw new Error("Temporary parent is missing.");
7581
+ if (parentItem.type !== "frame") throw new Error("Only frames can contain temporary items.");
7582
+ cursor = parentItem.parent;
7583
+ }
7584
+ const id = `${prefix}${local}`;
7585
+ if (document.items[id]) throw new Error("Temporary item ID conflicts with the document.");
7586
+ const parent = value.parent ? `${prefix}${value.parent}` : rootId;
7587
+ if (value.type === "arrow") {
7588
+ const remap = (endpoint) => {
7589
+ if (!("item" in endpoint)) return endpoint;
7590
+ if (!(endpoint.item in output.items) && !scope.has(endpoint.item)) throw new Error("Arrow target is outside the script scope.");
7591
+ return endpoint.item in output.items ? {
7592
+ ...endpoint,
7593
+ item: `${prefix}${endpoint.item}`
7594
+ } : endpoint;
7595
+ };
7596
+ items.set(id, {
7597
+ ...value,
7598
+ parent,
7599
+ props: {
7600
+ ...value.props,
7601
+ start: remap(value.props.start),
7602
+ end: remap(value.props.end)
7603
+ }
7604
+ });
7605
+ } else items.set(id, {
7606
+ ...value,
7607
+ parent
7608
+ });
7609
+ }
7610
+ const prepared = {
7611
+ document,
7612
+ owner,
7613
+ rootId,
7614
+ items,
7615
+ presented
7616
+ };
7617
+ preparedOutputs.set(output, prepared);
7618
+ return prepared;
7619
+ }
7620
+ function composeBoardScriptItems(document, scene, outputs, incoming = /* @__PURE__ */ new Map(), screen, onError) {
7621
+ const result = new Map(incoming);
7622
+ const screenAnchors = /* @__PURE__ */ new Map();
7623
+ if (outputs.size === 0) return result;
7624
+ for (const owner of Object.keys(document.items)) {
7625
+ const output = outputs.get(owner);
7626
+ if (!output) continue;
7627
+ try {
7628
+ const changes = /* @__PURE__ */ new Map();
7629
+ const prepared = prepareBoardScriptOutput(document, owner, output);
7630
+ for (const [id, patch] of Object.entries(output.patches)) {
7631
+ const base = result.get(id) ?? document.items[id];
7632
+ if (!base) continue;
7633
+ let cached = prepared.presented.get(id);
7634
+ if (cached?.base !== base) {
7635
+ cached = {
7636
+ base,
7637
+ item: parseBoardItem(mergePatch(base, patch))
7638
+ };
7639
+ prepared.presented.set(id, cached);
7640
+ }
7641
+ changes.set(id, cached.item);
7642
+ }
7643
+ if (!prepared.items.size || output.space === "screen" && !screen) {
7644
+ for (const [id, item] of changes) result.set(id, item);
7645
+ continue;
7646
+ }
7647
+ const rootId = prepared.rootId;
7648
+ const item = changes.get(owner) ?? result.get(owner) ?? document.items[owner];
7649
+ if (!item) continue;
7650
+ const box = scene.layout.box(owner);
7651
+ const overlay = output.space === "screen" && screen ? {
7652
+ parent: void 0,
7653
+ position: {
7654
+ x: 0,
7655
+ y: 0
7656
+ },
7657
+ scale: 1 / screen.zoom,
7658
+ rotation: 0,
7659
+ origin: {
7660
+ x: 0,
7661
+ y: 0
7662
+ }
7663
+ } : {};
7664
+ changes.set(rootId, parseBoardItem({
7665
+ type: "frame",
7666
+ parent: item.parent,
7667
+ position: item.position,
7668
+ rotation: item.rotation,
7669
+ scale: item.scale,
7670
+ origin: item.origin,
7671
+ opacity: item.opacity,
7672
+ size: {
7673
+ width: Math.max(1, box.width),
7674
+ height: Math.max(1, box.height)
7675
+ },
7676
+ z: (item.z ?? 0) + .001,
7677
+ props: {
7678
+ decoration: "none",
7679
+ clip: false
7680
+ },
7681
+ ...overlay
7682
+ }));
7683
+ for (const [id, value] of prepared.items) changes.set(id, value);
7684
+ for (const [id, item] of changes) result.set(id, item);
7685
+ if (output.space === "screen") screenAnchors.set(owner, rootId);
7686
+ } catch (error) {
7687
+ if (!onError) throw error;
7688
+ onError(owner, error);
7689
+ }
7690
+ }
7691
+ if (screenAnchors.size) {
7692
+ const layout = createBoardLayout((id) => result.get(id) ?? document.items[id]);
7693
+ for (const [owner, rootId] of screenAnchors) {
7694
+ const root = result.get(rootId);
7695
+ if (root) result.set(rootId, {
7696
+ ...root,
7697
+ position: applyMatrix$1(layout.matrix(owner), {
7698
+ x: 0,
7699
+ y: 0
7700
+ })
7701
+ });
7702
+ }
7703
+ }
7704
+ return result;
7705
+ }
7706
+ //#endregion
7707
+ //#region ../sdk/src/board/model/script-example.ts
7708
+ const BOARD_SCRIPT_DRAWING_EXAMPLE = `export const animated = true;
7709
+
7710
+ export const parameters = {
7711
+ amplitude: { type: "number", label: "Amplitude", default: 40, min: 0, max: 120 }
7712
+ };
7713
+
7714
+ export function draw(ctx, { t, width, height, params }) {
7715
+ ctx.strokeStyle = "#e67845";
7716
+ ctx.lineWidth = 3;
7717
+ ctx.beginPath();
7718
+ for (let x = 0; x <= width; x += 2) {
7719
+ const y = height / 2 + Math.sin(x / 40 - t / 800) * params.amplitude;
7720
+ if (x === 0) ctx.moveTo(x, y);
7721
+ else ctx.lineTo(x, y);
7722
+ }
7723
+ ctx.stroke();
7724
+ }
7725
+ `;
7726
+ //#endregion
7727
+ //#region ../sdk/src/board/model/animation.ts
7728
+ const linear = (x) => x;
7729
+ function cubicBezier(x1, y1, x2, y2) {
7730
+ const cx = 3 * x1;
7731
+ const bx = 3 * (x2 - x1) - cx;
7732
+ const ax = 1 - cx - bx;
7733
+ const cy = 3 * y1;
7734
+ const by = 3 * (y2 - y1) - cy;
7735
+ const ay = 1 - cy - by;
7736
+ const sampleX = (t) => ((ax * t + bx) * t + cx) * t;
7737
+ const sampleY = (t) => ((ay * t + by) * t + cy) * t;
7738
+ const slopeX = (t) => (3 * ax * t + 2 * bx) * t + cx;
7739
+ return (x) => {
7740
+ if (x <= 0) return 0;
7741
+ if (x >= 1) return 1;
7742
+ let t = x;
7743
+ for (let index = 0; index < 8; index += 1) {
7744
+ const error = sampleX(t) - x;
7745
+ if (Math.abs(error) < 1e-6) return sampleY(t);
7746
+ const slope = slopeX(t);
7747
+ if (Math.abs(slope) < 1e-6) break;
7748
+ t -= error / slope;
7749
+ }
7750
+ let low = 0;
7751
+ let high = 1;
7752
+ t = x;
7753
+ for (let index = 0; index < 32; index += 1) {
7754
+ const value = sampleX(t);
7755
+ if (Math.abs(value - x) < 1e-6) break;
7756
+ if (value < x) low = t;
7757
+ else high = t;
7758
+ t = (low + high) / 2;
7759
+ }
7760
+ return sampleY(t);
7156
7761
  };
7157
7762
  }
7158
7763
  function steps(count, position) {
@@ -7254,7 +7859,7 @@ function compileAnimation(animationId, animation, document) {
7254
7859
  const compiled = [];
7255
7860
  const children = [];
7256
7861
  for (const [id, track] of Object.entries(tracks).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
7257
- const targetType = track.target === "camera" ? BOARD_CAMERA_TARGET : document.animations[track.target] ? "animation" : document.items[track.target]?.type;
7862
+ const targetType = track.target === "camera" ? BOARD_CAMERA_TARGET$1 : document.animations[track.target] ? "animation" : document.items[track.target]?.type;
7258
7863
  if (!targetType) continue;
7259
7864
  const entry = compileTrack(animationId, id, track, targetType);
7260
7865
  if (!entry) continue;
@@ -7302,11 +7907,21 @@ function hex(color) {
7302
7907
  return `#${Math.max(0, Math.min(16777215, Math.round(color))).toString(16).padStart(6, "0")}`;
7303
7908
  }
7304
7909
  function mixColor(a, b, t, resolve) {
7305
- const from = resolve(a);
7306
- const to = resolve(b);
7307
- if (from === null || to === null) return t < 1 ? a : b;
7308
- const channel = (shift) => Math.round(lerp(from >> shift & 255, to >> shift & 255, t));
7309
- return hex(channel(16) << 16 | channel(8) << 8 | channel(0));
7910
+ const left = resolve(a);
7911
+ const right = resolve(b);
7912
+ if (left === null || right === null) return t < 1 ? a : b;
7913
+ const from = typeof left === "number" ? {
7914
+ color: left,
7915
+ alpha: 1
7916
+ } : left;
7917
+ const to = typeof right === "number" ? {
7918
+ color: right,
7919
+ alpha: 1
7920
+ } : right;
7921
+ const alpha = lerp(from.alpha, to.alpha, t);
7922
+ const channel = (shift) => alpha > 0 ? Math.round(lerp((from.color >> shift & 255) * from.alpha, (to.color >> shift & 255) * to.alpha, t) / alpha) : 0;
7923
+ const rgb = hex(channel(16) << 16 | channel(8) << 8 | channel(0));
7924
+ return alpha >= 1 ? rgb : `${rgb}${Math.round(Math.max(0, alpha) * 255).toString(16).padStart(2, "0")}`;
7310
7925
  }
7311
7926
  function parseHexColor(color) {
7312
7927
  if (typeof color !== "string") return null;
@@ -7315,6 +7930,23 @@ function parseHexColor(color) {
7315
7930
  const digits = match[1].length === 3 ? [...match[1]].map((digit) => digit + digit).join("") : match[1];
7316
7931
  return Number.parseInt(digits, 16);
7317
7932
  }
7933
+ function parseAnimationColor(color) {
7934
+ if (typeof color === "string") {
7935
+ if (color.trim().toLowerCase() === "transparent") return {
7936
+ color: 0,
7937
+ alpha: 0
7938
+ };
7939
+ const match = /^#([0-9a-f]{4}|[0-9a-f]{8})$/i.exec(color.trim());
7940
+ if (match?.[1]) {
7941
+ const digits = match[1].length === 4 ? [...match[1]].map((digit) => digit + digit).join("") : match[1];
7942
+ return {
7943
+ color: Number.parseInt(digits.slice(0, 6), 16),
7944
+ alpha: Number.parseInt(digits.slice(6), 16) / 255
7945
+ };
7946
+ }
7947
+ }
7948
+ return parseHexColor(color);
7949
+ }
7318
7950
  function blend(track, index, time, resolveColor) {
7319
7951
  const { times, values } = track;
7320
7952
  const from = values[index];
@@ -7348,7 +7980,7 @@ function blend(track, index, time, resolveColor) {
7348
7980
  default: return progress >= 1 ? to : from;
7349
7981
  }
7350
7982
  }
7351
- function sampleBoardTrack(track, time, resolveColor = parseHexColor) {
7983
+ function sampleBoardTrack(track, time, resolveColor = parseAnimationColor) {
7352
7984
  const index = keyframeIndex(track.times, time);
7353
7985
  if (index < 0) return track.values[0];
7354
7986
  return blend(track, index, time, resolveColor);
@@ -7421,7 +8053,7 @@ function outranks(track, time, other, otherTime) {
7421
8053
  return track.id > other.id;
7422
8054
  }
7423
8055
  function evaluateBoardAnimations(compiled, times, options = {}) {
7424
- const resolveColor = options.resolveColor ?? parseHexColor;
8056
+ const resolveColor = options.resolveColor ?? parseAnimationColor;
7425
8057
  const local = new Map(times instanceof Map ? times : Object.entries(times));
7426
8058
  for (const id of compiled.order) {
7427
8059
  const animation = compiled.animations.get(id);
@@ -7462,7 +8094,7 @@ function evaluateBoardAnimations(compiled, times, options = {}) {
7462
8094
  const camera = {};
7463
8095
  const ordered = [...groups.values()].sort((a, b) => a.path.length - b.path.length);
7464
8096
  for (const group of ordered) {
7465
- const isCamera = group.target === BOARD_CAMERA_TARGET;
8097
+ const isCamera = group.target === BOARD_CAMERA_TARGET$1;
7466
8098
  const base = isCamera ? void 0 : items.get(group.target) ?? compiled.document.items[group.target];
7467
8099
  if (!isCamera && !base) continue;
7468
8100
  let value = isCamera ? camera[group.path[0]] : readPath(base, group.path);
@@ -7519,65 +8151,384 @@ function boardDocumentAt(document, animationId, time, options = {}) {
7519
8151
  };
7520
8152
  }
7521
8153
  //#endregion
7522
- //#region ../sdk/src/board/model/geometry.ts
7523
- function worldPoint(x, y) {
7524
- return {
7525
- x,
7526
- y
8154
+ //#region ../sdk/src/board/model/scene.ts
8155
+ function orderBoardItems(items) {
8156
+ const children = /* @__PURE__ */ new Map();
8157
+ for (const [id, item] of Object.entries(items)) {
8158
+ const parent = item.parent && items[item.parent] && item.parent !== id ? item.parent : "";
8159
+ const list = children.get(parent) ?? [];
8160
+ list.push(id);
8161
+ children.set(parent, list);
8162
+ }
8163
+ const compare = (a, b) => (items[a]?.z ?? 0) - (items[b]?.z ?? 0) || (a < b ? -1 : a > b ? 1 : 0);
8164
+ for (const list of children.values()) list.sort(compare);
8165
+ const order = [];
8166
+ const seen = /* @__PURE__ */ new Set();
8167
+ const visit = (id) => {
8168
+ if (seen.has(id)) return;
8169
+ seen.add(id);
8170
+ order.push(id);
8171
+ for (const child of children.get(id) ?? []) visit(child);
7527
8172
  };
8173
+ for (const id of children.get("") ?? []) visit(id);
8174
+ for (const id of Object.keys(items)) visit(id);
8175
+ return order;
7528
8176
  }
7529
- function degToRad(degrees) {
7530
- return degrees * Math.PI / 180;
7531
- }
7532
- function frameRect(frame) {
7533
- return {
7534
- x: frame.x,
7535
- y: frame.y,
7536
- width: frame.width,
7537
- height: frame.height
8177
+ function buildBoardScene(document, previous) {
8178
+ const items = document.items;
8179
+ const children = /* @__PURE__ */ new Map();
8180
+ const binders = /* @__PURE__ */ new Map();
8181
+ for (const [id, item] of Object.entries(items)) {
8182
+ const parent = item.parent && items[item.parent] ? item.parent : "";
8183
+ const list = children.get(parent) ?? [];
8184
+ list.push(id);
8185
+ children.set(parent, list);
8186
+ for (const bound of arrowBindings(item)) binders.set(bound, [...binders.get(bound) ?? [], id]);
8187
+ }
8188
+ const layout = createBoardLayout((id) => items[id], (id) => [...children.get(id) ?? [], ...binders.get(id) ?? []]);
8189
+ const stable = /* @__PURE__ */ new Map();
8190
+ const isStable = (id, depth = 0) => {
8191
+ const known = stable.get(id);
8192
+ if (known !== void 0) return known;
8193
+ const item = items[id];
8194
+ const before = previous?.get(id);
8195
+ let result = Boolean(item && before && stripSceneSource(before) === item) && depth < 64;
8196
+ if (result && item?.parent && items[item.parent]) result = isStable(item.parent, depth + 1);
8197
+ if (result && item) {
8198
+ for (const bound of arrowBindings(item)) if (items[bound] && !isStable(bound, depth + 1)) result = false;
8199
+ }
8200
+ stable.set(id, result);
8201
+ return result;
7538
8202
  };
7539
- }
7540
- function rectCenter(rect) {
7541
- return worldPoint(rect.x + rect.width / 2, rect.y + rect.height / 2);
7542
- }
7543
- function rectsIntersect(a, b) {
7544
- return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
7545
- }
7546
- function unionRects(rects) {
7547
- if (rects.length === 0) return null;
7548
- let minX = Number.POSITIVE_INFINITY;
7549
- let minY = Number.POSITIVE_INFINITY;
7550
- let maxX = Number.NEGATIVE_INFINITY;
7551
- let maxY = Number.NEGATIVE_INFINITY;
7552
- for (const rect of rects) {
7553
- minX = Math.min(minX, rect.x);
7554
- minY = Math.min(minY, rect.y);
7555
- maxX = Math.max(maxX, rect.x + rect.width);
7556
- maxY = Math.max(maxY, rect.y + rect.height);
8203
+ const ordered = orderBoardItems(items);
8204
+ const sceneItems = [];
8205
+ const byId = /* @__PURE__ */ new Map();
8206
+ const indexById = /* @__PURE__ */ new Map();
8207
+ for (const id of ordered) {
8208
+ const item = items[id];
8209
+ const scene = (isStable(id) ? previous?.get(id) : void 0) ?? toSceneItem(id, item, layout);
8210
+ indexById.set(id, sceneItems.length);
8211
+ sceneItems.push(scene);
8212
+ byId.set(id, scene);
7557
8213
  }
7558
8214
  return {
7559
- x: minX,
7560
- y: minY,
7561
- width: maxX - minX,
7562
- height: maxY - minY
7563
- };
7564
- }
7565
- function itemBounds(frame) {
7566
- const rotation = frame.rotation || 0;
7567
- if (rotation === 0) return frameRect(frame);
7568
- const center = rectCenter(frameRect(frame));
7569
- const halfW = frame.width / 2;
7570
- const halfH = frame.height / 2;
7571
- const rad = degToRad(rotation);
7572
- const cos = Math.abs(Math.cos(rad));
7573
- const sin = Math.abs(Math.sin(rad));
7574
- const boundW = halfW * cos + halfH * sin;
7575
- const boundH = halfW * sin + halfH * cos;
8215
+ items: sceneItems,
8216
+ get: (id) => byId.get(id),
8217
+ indexOf: (id) => indexById.get(id) ?? -1,
8218
+ layout,
8219
+ children: (id) => children.get(id ?? "") ?? [],
8220
+ descendants: (id) => {
8221
+ const result = [];
8222
+ const seen = /* @__PURE__ */ new Set([id]);
8223
+ const stack = [...children.get(id) ?? []];
8224
+ while (stack.length) {
8225
+ const next = stack.pop();
8226
+ if (seen.has(next)) continue;
8227
+ seen.add(next);
8228
+ result.push(next);
8229
+ stack.push(...children.get(next) ?? []);
8230
+ }
8231
+ return result;
8232
+ },
8233
+ binders: (id) => binders.get(id) ?? [],
8234
+ opacity: (id) => cumulativeOpacity((key) => items[key], id)
8235
+ };
8236
+ }
8237
+ function cumulativeOpacity(lookup, id) {
8238
+ let value = 1;
8239
+ let cursor = id;
8240
+ for (let depth = 0; cursor && depth < 64; depth += 1) {
8241
+ const item = lookup(cursor);
8242
+ if (!item) break;
8243
+ value *= item.opacity;
8244
+ cursor = item.parent;
8245
+ }
8246
+ return value;
8247
+ }
8248
+ const sources = /* @__PURE__ */ new WeakMap();
8249
+ function stripSceneSource(scene) {
8250
+ return sources.get(scene);
8251
+ }
8252
+ function toSceneItem(id, item, layout) {
8253
+ const scene = {
8254
+ ...item,
8255
+ id,
8256
+ frame: layout.frame(id)
8257
+ };
8258
+ sources.set(scene, item);
8259
+ return scene;
8260
+ }
8261
+ //#endregion
8262
+ //#region ../sdk/src/board/model/sampling.ts
8263
+ function createBoardAnimationSampler(document, animation, options = {}) {
8264
+ const selected = animation ? document.animations[animation] : void 0;
8265
+ if (animation && !selected) throw new Error(`Animation ${animation} does not exist.`);
8266
+ const compiled = compileBoardAnimations(document);
8267
+ const cache = /* @__PURE__ */ new Map();
8268
+ const geometry = /* @__PURE__ */ new Map();
8269
+ function sample(time) {
8270
+ const times = /* @__PURE__ */ new Map();
8271
+ for (const [id, entry] of compiled.animations) if (entry.header.play === "always") times.set(id, boardAnimationTime(entry.header, time) ?? 0);
8272
+ if (animation && selected) times.set(animation, Math.min(time, selected.duration));
8273
+ const evaluation = evaluateBoardAnimations(compiled, times, options);
8274
+ return {
8275
+ document: applyBoardEvaluation(document, evaluation),
8276
+ evaluation
8277
+ };
8278
+ }
8279
+ function stateAt(time) {
8280
+ if (!Number.isFinite(time) || time < 0) throw new Error("Animation time must be finite and non-negative.");
8281
+ let value = cache.get(time);
8282
+ if (!value) {
8283
+ value = sample(time);
8284
+ const oldest = cache.keys().next().value;
8285
+ if (cache.size >= 64 && oldest !== void 0) cache.delete(oldest);
8286
+ cache.set(time, value);
8287
+ }
8288
+ return value;
8289
+ }
8290
+ let current;
7576
8291
  return {
7577
- x: center.x - boundW,
7578
- y: center.y - boundH,
7579
- width: boundW * 2,
7580
- height: boundH * 2
8292
+ frameAt(id, time) {
8293
+ const key = `${time}:${id}`;
8294
+ if (geometry.has(key)) return geometry.get(key);
8295
+ const sampled = stateAt(time).document;
8296
+ const items = {};
8297
+ const pending = [id];
8298
+ while (pending.length) {
8299
+ const next = pending.pop();
8300
+ if (!next || items[next]) continue;
8301
+ const item = sampled.items[next];
8302
+ if (!item) continue;
8303
+ items[next] = item;
8304
+ if (item.parent) pending.push(item.parent);
8305
+ pending.push(...arrowBindings(item));
8306
+ }
8307
+ const frame = buildBoardScene({
8308
+ ...sampled,
8309
+ items
8310
+ }).get(id)?.frame;
8311
+ if (geometry.size >= 2048) geometry.clear();
8312
+ geometry.set(key, frame);
8313
+ return frame;
8314
+ },
8315
+ at(time) {
8316
+ if (current?.time === time) return current.value;
8317
+ const sampled = stateAt(time);
8318
+ const value = {
8319
+ ...sampled,
8320
+ scene: buildBoardScene(sampled.document)
8321
+ };
8322
+ current = {
8323
+ time,
8324
+ value
8325
+ };
8326
+ return value;
8327
+ }
8328
+ };
8329
+ }
8330
+ function resolveBoardAnimationCamera(override, bounds, surface, time, base = {
8331
+ x: 0,
8332
+ y: 0,
8333
+ zoom: 1
8334
+ }) {
8335
+ let camera = override.focus === void 0 ? base : cameraForFocus(override.focus, bounds, surface, { padding: 48 }) ?? base;
8336
+ if (override.zoom !== void 0) camera = {
8337
+ ...camera,
8338
+ zoom: override.zoom
8339
+ };
8340
+ if (override.center) camera = cameraForState({
8341
+ centerX: override.center.x,
8342
+ centerY: override.center.y,
8343
+ zoom: camera.zoom
8344
+ }, surface);
8345
+ if (override.shake) camera = {
8346
+ ...camera,
8347
+ x: camera.x + Math.sin(time / 1e3 * 91.3) * override.shake,
8348
+ y: camera.y + Math.cos(time / 1e3 * 77.9) * override.shake
8349
+ };
8350
+ return camera;
8351
+ }
8352
+ //#endregion
8353
+ //#region ../sdk/src/board/model/authoring.ts
8354
+ function validateChapterComposition(document, chapters) {
8355
+ const uses = /* @__PURE__ */ new Map();
8356
+ for (const [index, chapter] of chapters.entries()) for (const track of Object.values(document.animations[chapter.animation]?.tracks ?? {})) {
8357
+ let properties = uses.get(track.target);
8358
+ if (!properties) {
8359
+ properties = /* @__PURE__ */ new Map();
8360
+ uses.set(track.target, properties);
8361
+ }
8362
+ for (const [property, usage] of properties) if ((property === track.property || property.startsWith(`${track.property}.`) || track.property.startsWith(`${property}.`)) && (usage.additive || track.composite === "add") && [...usage.chapters].some((other) => other !== index)) throw new Error(`Chapters share additive tracks on ${track.target}.${track.property}; use distinct items or combine the tracks before sequencing.`);
8363
+ const usage = properties.get(track.property) ?? {
8364
+ chapters: /* @__PURE__ */ new Set(),
8365
+ additive: false
8366
+ };
8367
+ usage.chapters.add(index);
8368
+ usage.additive ||= track.composite === "add";
8369
+ properties.set(track.property, usage);
8370
+ }
8371
+ }
8372
+ function boardSequence(document, chapters) {
8373
+ if (!chapters.length) throw new Error("A sequence needs at least one chapter.");
8374
+ validateChapterComposition(document, chapters);
8375
+ const tracks = {};
8376
+ const markers = [];
8377
+ let cursor = 0;
8378
+ const seen = /* @__PURE__ */ new Set();
8379
+ const seenFrames = /* @__PURE__ */ new Set();
8380
+ for (const [index, chapter] of chapters.entries()) {
8381
+ const animation = document.animations[chapter.animation];
8382
+ if (!animation) throw new Error(`Animation ${chapter.animation} does not exist.`);
8383
+ if (animation.play !== "manual") throw new Error(`Chapter ${chapter.animation} must use manual playback.`);
8384
+ if (!Number.isFinite(animation.duration) || animation.duration <= 0) throw new Error("Chapter duration must be positive and finite.");
8385
+ if (seen.has(chapter.animation)) throw new Error(`Animation ${chapter.animation} appears more than once; use separate animation IDs.`);
8386
+ seen.add(chapter.animation);
8387
+ if (chapter.frame && document.items[chapter.frame]?.type !== "frame") throw new Error(`Chapter ${chapter.animation} needs a valid frame.`);
8388
+ if (chapter.frame && seenFrames.has(chapter.frame)) throw new Error("Each chapter needs a separate frame, or omit frame visibility control.");
8389
+ if (chapter.frame) seenFrames.add(chapter.frame);
8390
+ const gap = chapter.gap ?? 0;
8391
+ if (!Number.isFinite(gap) || gap < 0) throw new Error("Chapter gap must be finite and non-negative.");
8392
+ cursor += gap;
8393
+ const start = cursor;
8394
+ const end = start + animation.duration;
8395
+ const rankWidth = String(Object.keys(animation.tracks).length).length;
8396
+ const ranks = new Map(Object.keys(animation.tracks).sort().map((id, rank) => [id, String(rank).padStart(rankWidth, "0")]));
8397
+ const chapterIndex = String(index).padStart(String(chapters.length).length, "0");
8398
+ for (const [id, track] of Object.entries(animation.tracks)) {
8399
+ if (document.animations[track.target]) throw new Error(`Chapter ${chapter.animation} contains nested clocks; compose its leaf animations instead.`);
8400
+ if (chapter.frame && track.target === "camera" && track.property === "focus") continue;
8401
+ tracks[`chapter-${chapterIndex}-${ranks.get(id)}`] = {
8402
+ ...track,
8403
+ keyframes: track.keyframes.map((keyframe) => ({
8404
+ ...keyframe,
8405
+ at: start + keyframe.at
8406
+ }))
8407
+ };
8408
+ }
8409
+ const boundary = end + (chapter.pause && index < chapters.length - 1 ? 1 : 0);
8410
+ if (chapter.frame) {
8411
+ tracks[`visibility-${index}`] = {
8412
+ target: chapter.frame,
8413
+ property: "opacity",
8414
+ composite: "add",
8415
+ interpolation: "step",
8416
+ keyframes: [
8417
+ ...start > 0 ? [{
8418
+ at: 0,
8419
+ value: 0
8420
+ }] : [],
8421
+ {
8422
+ at: start,
8423
+ value: 1
8424
+ },
8425
+ ...index < chapters.length - 1 ? [{
8426
+ at: boundary + (chapters[index + 1]?.gap ?? 0),
8427
+ value: 0
8428
+ }] : []
8429
+ ]
8430
+ };
8431
+ tracks[`camera-${index}`] = {
8432
+ target: "camera",
8433
+ property: "focus",
8434
+ composite: "replace",
8435
+ interpolation: "step",
8436
+ keyframes: [{
8437
+ at: start,
8438
+ value: chapter.frame
8439
+ }]
8440
+ };
8441
+ }
8442
+ markers.push({
8443
+ at: start,
8444
+ label: chapter.label ?? animation.name ?? chapter.animation
8445
+ });
8446
+ for (const marker of animation.markers) markers.push({
8447
+ ...marker,
8448
+ at: start + marker.at
8449
+ });
8450
+ if (chapter.pause) markers.push({
8451
+ at: end,
8452
+ pause: true
8453
+ });
8454
+ cursor = boundary;
8455
+ }
8456
+ const byTime = /* @__PURE__ */ new Map();
8457
+ for (const marker of markers) {
8458
+ const previous = byTime.get(marker.at);
8459
+ byTime.set(marker.at, {
8460
+ ...previous,
8461
+ ...marker,
8462
+ pause: Boolean(previous?.pause || marker.pause)
8463
+ });
8464
+ }
8465
+ return {
8466
+ duration: cursor,
8467
+ play: "manual",
8468
+ end: "hold",
8469
+ delay: 0,
8470
+ loop: false,
8471
+ tracks,
8472
+ markers: [...byTime.values()].sort((a, b) => a.at - b.at)
8473
+ };
8474
+ }
8475
+ function boardSequencePatch(document, id, chapters) {
8476
+ BoardIdSchema$1.parse(id);
8477
+ if (chapters.some((chapter) => chapter.animation === id)) throw new Error("A sequence cannot overwrite one of its source chapters.");
8478
+ const animation = boardSequence(document, chapters);
8479
+ const tracks = { ...animation.tracks };
8480
+ for (const key of Object.keys(document.animations[id]?.tracks ?? {})) if (!(key in tracks)) tracks[key] = null;
8481
+ const patch = { animations: { [id]: {
8482
+ ...animation,
8483
+ tracks
8484
+ } } };
8485
+ const result = applyBoardPatchToDocument(document, patch);
8486
+ if (!result.ok) throw new Error(result.diagnostics.map((entry) => `${entry.path}: ${entry.message}`).join("; "));
8487
+ return patch;
8488
+ }
8489
+ function boardPathTrack(target, points, options) {
8490
+ const { duration, at = 0, samples = 64, orient = false } = options;
8491
+ if (points.length < 2 || points.length > 1e4 || points.some((p) => !Number.isFinite(p.x) || !Number.isFinite(p.y))) throw new Error("A path needs 2 to 10000 finite points.");
8492
+ if (!Number.isFinite(duration) || duration <= 0 || !Number.isFinite(at) || at < 0) throw new Error("Path duration must be positive and start time non-negative.");
8493
+ if (!Number.isInteger(samples) || samples < 2 || samples > 1e4) throw new Error("Path samples must be an integer from 2 to 10000.");
8494
+ const segments = [];
8495
+ let total = 0;
8496
+ let previous = points[0];
8497
+ for (const point of points.slice(1)) {
8498
+ if (!previous) break;
8499
+ const start = total;
8500
+ total += Math.hypot(point.x - previous.x, point.y - previous.y);
8501
+ segments.push({
8502
+ a: previous,
8503
+ b: point,
8504
+ start,
8505
+ end: total
8506
+ });
8507
+ previous = point;
8508
+ }
8509
+ const distances = [.../* @__PURE__ */ new Set([...segments.map((segment) => segment.end), ...Array.from({ length: samples }, (_, index) => total * index / (samples - 1))])].sort((a, b) => a - b);
8510
+ let segmentIndex = 0;
8511
+ return {
8512
+ target,
8513
+ property: "position",
8514
+ composite: "replace",
8515
+ interpolation: "auto",
8516
+ orient,
8517
+ keyframes: distances.map((distance) => {
8518
+ const progress = total > 0 ? distance / total : 0;
8519
+ while (segmentIndex < segments.length - 1 && (segments[segmentIndex]?.end ?? 0) < distance) segmentIndex++;
8520
+ const segment = segments[segmentIndex];
8521
+ if (!segment) throw new Error("Path has no segments.");
8522
+ const { a, b, start, end } = segment;
8523
+ const fraction = end > start ? (distance - start) / (end - start) : 0;
8524
+ return {
8525
+ at: at + duration * progress,
8526
+ value: {
8527
+ x: a.x + (b.x - a.x) * fraction,
8528
+ y: a.y + (b.y - a.y) * fraction
8529
+ }
8530
+ };
8531
+ })
7581
8532
  };
7582
8533
  }
7583
8534
  const CURVE_SEGMENTS = 20;
@@ -7783,12 +8734,12 @@ function resolveSceneArrow(item, scene) {
7783
8734
  return resolved;
7784
8735
  }
7785
8736
  const resolvedCache = /* @__PURE__ */ new WeakMap();
7786
- function frameKey$1(frame) {
8737
+ function frameKey(frame) {
7787
8738
  return frame ? `${frame.x},${frame.y},${frame.width},${frame.height},${frame.rotation}` : "-";
7788
8739
  }
7789
8740
  function arrowKey(item, matrix, getFrame) {
7790
8741
  const { start, end } = item.props;
7791
- return `${matrix.join(",")}|${isArrowBinding(start) ? frameKey$1(getFrame(start.item)) : ""}|${isArrowBinding(end) ? frameKey$1(getFrame(end.item)) : ""}`;
8742
+ return `${matrix.join(",")}|${isArrowBinding(start) ? frameKey(getFrame(start.item)) : ""}|${isArrowBinding(end) ? frameKey(getFrame(end.item)) : ""}`;
7792
8743
  }
7793
8744
  function pathMidpoint(path) {
7794
8745
  if (path.length === 0) return worldPoint(0, 0);
@@ -8076,6 +9027,7 @@ function resolveRegion(scene, region) {
8076
9027
  return boundsOf.get(item.id) ?? null;
8077
9028
  };
8078
9029
  const visible = (item, rect) => {
9030
+ if (item.type === "effect" && item.props.kind === "trail" && item.props.follow) return true;
8079
9031
  const box = bounds(item);
8080
9032
  return box !== null && rectsIntersect(box, rect);
8081
9033
  };
@@ -8152,7 +9104,31 @@ function planBoardExport(input) {
8152
9104
  };
8153
9105
  }
8154
9106
  //#endregion
9107
+ //#region ../sdk/src/board/model/media.ts
9108
+ function getMediaResourceTitle(value) {
9109
+ if (!/^[a-z][a-z\d+.-]*:/i.test(value)) return value.split("/").filter(Boolean).pop() || value;
9110
+ try {
9111
+ const url = new URL(value);
9112
+ const title = url.pathname.split("/").filter(Boolean).pop() || url.hostname;
9113
+ try {
9114
+ return decodeURIComponent(title);
9115
+ } catch {
9116
+ return title;
9117
+ }
9118
+ } catch {
9119
+ return value.split("/").filter(Boolean).pop() || value;
9120
+ }
9121
+ }
9122
+ //#endregion
8155
9123
  //#region ../sdk/src/board/model/file-snapshot.ts
9124
+ /**
9125
+ * File-card snapshot derivation — pure, renderer-agnostic, dependency-free.
9126
+ *
9127
+ * Whoever holds the file content calls `buildFileSnapshot`; today that is the
9128
+ * web client, for cards near its viewport. Nothing here ever writes back to the
9129
+ * workspace file. The file on disk stays the single source of truth, and the
9130
+ * snapshot is a cache keyed by mtime so a stale card is detectable.
9131
+ */
8156
9132
  const DOC_EXTENSIONS = /* @__PURE__ */ new Set([
8157
9133
  "md",
8158
9134
  "mdx",
@@ -8242,7 +9218,7 @@ const MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
8242
9218
  "tgz"
8243
9219
  ]);
8244
9220
  function extensionOf(path) {
8245
- const name = path.split("/").filter(Boolean).pop() ?? path;
9221
+ const name = fileBaseName(path);
8246
9222
  const dot = name.lastIndexOf(".");
8247
9223
  if (dot <= 0) return "";
8248
9224
  return name.slice(dot + 1).toLowerCase();
@@ -8270,7 +9246,7 @@ function fileCategory(path, mimeType) {
8270
9246
  }
8271
9247
  /** Basename of a path, used as a last-resort card title. */
8272
9248
  function fileBaseName(path) {
8273
- return path.split("/").filter(Boolean).pop() ?? path;
9249
+ return getMediaResourceTitle(path);
8274
9250
  }
8275
9251
  /** Basename with a trailing extension stripped. Dotfiles keep their full name. */
8276
9252
  function fileStem(path) {
@@ -8294,7 +9270,7 @@ function filePreviewKind(snapshot) {
8294
9270
  }
8295
9271
  /** Short uppercase type label for a file card (`MD`, `JSON`, `FILE`). */
8296
9272
  function fileTypeLabel(path) {
8297
- const name = path.split("/").filter(Boolean).pop() ?? path;
9273
+ const name = fileBaseName(path);
8298
9274
  const dot = name.lastIndexOf(".");
8299
9275
  if (dot <= 0) return (name.replace(/^\./, "") || "file").toUpperCase();
8300
9276
  return name.slice(dot + 1).toUpperCase();
@@ -8467,114 +9443,6 @@ function resolveItemColor(color, fallback, colors, mode, parseCss, part = "strok
8467
9443
  return parseCss(value) ?? pickBoardColor(colors, fallback, mode)[part];
8468
9444
  }
8469
9445
  //#endregion
8470
- //#region ../sdk/src/board/model/scene.ts
8471
- function orderBoardItems(items) {
8472
- const children = /* @__PURE__ */ new Map();
8473
- for (const [id, item] of Object.entries(items)) {
8474
- const parent = item.parent && items[item.parent] && item.parent !== id ? item.parent : "";
8475
- const list = children.get(parent) ?? [];
8476
- list.push(id);
8477
- children.set(parent, list);
8478
- }
8479
- const compare = (a, b) => (items[a]?.z ?? 0) - (items[b]?.z ?? 0) || (a < b ? -1 : a > b ? 1 : 0);
8480
- for (const list of children.values()) list.sort(compare);
8481
- const order = [];
8482
- const seen = /* @__PURE__ */ new Set();
8483
- const visit = (id) => {
8484
- if (seen.has(id)) return;
8485
- seen.add(id);
8486
- order.push(id);
8487
- for (const child of children.get(id) ?? []) visit(child);
8488
- };
8489
- for (const id of children.get("") ?? []) visit(id);
8490
- for (const id of Object.keys(items)) visit(id);
8491
- return order;
8492
- }
8493
- function buildBoardScene(document, previous) {
8494
- const items = document.items;
8495
- const children = /* @__PURE__ */ new Map();
8496
- const binders = /* @__PURE__ */ new Map();
8497
- for (const [id, item] of Object.entries(items)) {
8498
- const parent = item.parent && items[item.parent] ? item.parent : "";
8499
- const list = children.get(parent) ?? [];
8500
- list.push(id);
8501
- children.set(parent, list);
8502
- for (const bound of arrowBindings(item)) binders.set(bound, [...binders.get(bound) ?? [], id]);
8503
- }
8504
- const layout = createBoardLayout((id) => items[id], (id) => [...children.get(id) ?? [], ...binders.get(id) ?? []]);
8505
- const stable = /* @__PURE__ */ new Map();
8506
- const isStable = (id, depth = 0) => {
8507
- const known = stable.get(id);
8508
- if (known !== void 0) return known;
8509
- const item = items[id];
8510
- const before = previous?.get(id);
8511
- let result = Boolean(item && before && stripSceneSource(before) === item) && depth < 64;
8512
- if (result && item?.parent && items[item.parent]) result = isStable(item.parent, depth + 1);
8513
- if (result && item) {
8514
- for (const bound of arrowBindings(item)) if (items[bound] && !isStable(bound, depth + 1)) result = false;
8515
- }
8516
- stable.set(id, result);
8517
- return result;
8518
- };
8519
- const ordered = orderBoardItems(items);
8520
- const sceneItems = [];
8521
- const byId = /* @__PURE__ */ new Map();
8522
- const indexById = /* @__PURE__ */ new Map();
8523
- for (const id of ordered) {
8524
- const item = items[id];
8525
- const scene = (isStable(id) ? previous?.get(id) : void 0) ?? toSceneItem(id, item, layout);
8526
- indexById.set(id, sceneItems.length);
8527
- sceneItems.push(scene);
8528
- byId.set(id, scene);
8529
- }
8530
- return {
8531
- items: sceneItems,
8532
- get: (id) => byId.get(id),
8533
- indexOf: (id) => indexById.get(id) ?? -1,
8534
- layout,
8535
- children: (id) => children.get(id ?? "") ?? [],
8536
- descendants: (id) => {
8537
- const result = [];
8538
- const seen = /* @__PURE__ */ new Set([id]);
8539
- const stack = [...children.get(id) ?? []];
8540
- while (stack.length) {
8541
- const next = stack.pop();
8542
- if (seen.has(next)) continue;
8543
- seen.add(next);
8544
- result.push(next);
8545
- stack.push(...children.get(next) ?? []);
8546
- }
8547
- return result;
8548
- },
8549
- binders: (id) => binders.get(id) ?? [],
8550
- opacity: (id) => cumulativeOpacity((key) => items[key], id)
8551
- };
8552
- }
8553
- function cumulativeOpacity(lookup, id) {
8554
- let value = 1;
8555
- let cursor = id;
8556
- for (let depth = 0; cursor && depth < 64; depth += 1) {
8557
- const item = lookup(cursor);
8558
- if (!item) break;
8559
- value *= item.opacity;
8560
- cursor = item.parent;
8561
- }
8562
- return value;
8563
- }
8564
- const sources = /* @__PURE__ */ new WeakMap();
8565
- function stripSceneSource(scene) {
8566
- return sources.get(scene);
8567
- }
8568
- function toSceneItem(id, item, layout) {
8569
- const scene = {
8570
- ...item,
8571
- id,
8572
- frame: layout.frame(id)
8573
- };
8574
- sources.set(scene, item);
8575
- return scene;
8576
- }
8577
- //#endregion
8578
9446
  //#region ../sdk/src/board/model/text-metrics.ts
8579
9447
  function boardFontFamily(font, stacks) {
8580
9448
  if (font === "sans" || font === "serif" || font === "mono") return stacks[font];
@@ -8619,11 +9487,14 @@ function taskArtifactPreviewUrl(artifact) {
8619
9487
  //#endregion
8620
9488
  //#region ../sdk/src/board/model/image-key.ts
8621
9489
  function imageAssetKey(item) {
8622
- if (item.type === "image") return `file:${item.props.src}`;
9490
+ if (item.type === "image") {
9491
+ const source = parseBoardMediaSource$1(item.props.src);
9492
+ return source ? `${source.kind}:${source.value}` : null;
9493
+ }
8623
9494
  if (item.type === "file") {
8624
9495
  const snapshot = item.props.snapshot;
8625
- if (snapshot?.coverPath) return `file:${snapshot.coverPath}`;
8626
9496
  if (snapshot?.coverUrl) return `url:${snapshot.coverUrl}`;
9497
+ if (snapshot?.coverPath) return `file:${snapshot.coverPath}`;
8627
9498
  return null;
8628
9499
  }
8629
9500
  if (item.type === "task") {
@@ -26041,20 +26912,122 @@ var CanvasRenderer = class extends AbstractRenderer {
26041
26912
  //#region ../../node_modules/.pnpm/pixi.js@8.20.1/node_modules/pixi.js/lib/index.mjs
26042
26913
  extensions.add(browserExt, webworkerExt);
26043
26914
  //#endregion
26044
- //#region ../sdk/src/board/render/text-measurement.ts
26045
- const BOARD_FONT_STACKS = {
26046
- sans: BOARD_FONT_STACK,
26047
- serif: "ui-serif, Georgia, \"Noto Serif CJK SC\", \"Songti SC\", serif",
26048
- mono: BOARD_MONO_FONT_STACK
26049
- };
26050
- let measureContext;
26051
- let installed = false;
26052
- function getMeasureContext() {
26053
- if (measureContext !== void 0) return measureContext;
26915
+ //#region ../sdk/src/board/render/css-color.ts
26916
+ const cache = /* @__PURE__ */ new Map();
26917
+ let canvas = null;
26918
+ function parseBoardCssPaint(value) {
26919
+ const key = value.trim();
26920
+ if (!key) return null;
26921
+ if (key.toLowerCase() === "transparent") return {
26922
+ color: 0,
26923
+ alpha: 0
26924
+ };
26925
+ const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(key)?.[1];
26926
+ if (hex) {
26927
+ const digits = hex.length <= 4 ? [...hex].map((digit) => digit + digit).join("") : hex;
26928
+ return {
26929
+ color: Number.parseInt(digits.slice(0, 6), 16),
26930
+ alpha: digits.length === 8 ? Number.parseInt(digits.slice(6), 16) / 255 : 1
26931
+ };
26932
+ }
26933
+ if (cache.has(key)) return cache.get(key) ?? null;
26934
+ let result = null;
26054
26935
  try {
26055
- measureContext = DOMAdapter.get().createCanvas(1, 1).getContext("2d") ?? null;
26936
+ canvas ??= DOMAdapter.get().createCanvas(1, 1);
26937
+ const context = canvas.getContext("2d");
26938
+ context.fillStyle = "#010203";
26939
+ context.fillStyle = key;
26940
+ const first = context.fillStyle;
26941
+ context.fillStyle = "#040506";
26942
+ context.fillStyle = key;
26943
+ if (first === context.fillStyle) {
26944
+ context.clearRect(0, 0, 1, 1);
26945
+ context.fillRect(0, 0, 1, 1);
26946
+ const [r = 0, g = 0, b = 0, a = 0] = context.getImageData(0, 0, 1, 1).data;
26947
+ result = {
26948
+ color: r << 16 | g << 8 | b,
26949
+ alpha: a / 255
26950
+ };
26951
+ }
26056
26952
  } catch {
26057
- measureContext = null;
26953
+ result = null;
26954
+ }
26955
+ if (cache.size >= 256) cache.clear();
26956
+ cache.set(key, result);
26957
+ return result;
26958
+ }
26959
+ function parseBoardCssColor(value) {
26960
+ return parseBoardCssPaint(value)?.color ?? null;
26961
+ }
26962
+ //#endregion
26963
+ //#region ../sdk/src/board/render/palette.ts
26964
+ const DARK = {
26965
+ bg: 1315860,
26966
+ surface: 2105376,
26967
+ hover: 2763306,
26968
+ border: 3815994,
26969
+ brand: 16734751,
26970
+ text: 16053492,
26971
+ muted: 9211020,
26972
+ rare: 3718648,
26973
+ epic: 10980346,
26974
+ legendary: 16096779
26975
+ };
26976
+ const LIGHT = {
26977
+ bg: 16777215,
26978
+ surface: 16053493,
26979
+ hover: 15329772,
26980
+ border: 14079706,
26981
+ brand: 15222030,
26982
+ text: 1579035,
26983
+ muted: 7041664,
26984
+ rare: 2450411,
26985
+ epic: 8141549,
26986
+ legendary: 11817737
26987
+ };
26988
+ function defaultBoardPalette(mode) {
26989
+ return { ...mode === "light" ? LIGHT : DARK };
26990
+ }
26991
+ function itemAlpha(context, color) {
26992
+ const value = typeof color === "object" && color ? color[context.colorScheme] : color;
26993
+ return value ? parseBoardCssPaint(value)?.alpha ?? 1 : 1;
26994
+ }
26995
+ function itemColor(context, color, fallback, part = "stroke") {
26996
+ if (((typeof color === "object" && color ? color[context.colorScheme] : color) ?? fallback) === "neutral" && part !== "fill") return context.palette.text;
26997
+ return resolveItemColor(color, fallback, context.colors, context.colorScheme, parseBoardCssColor, part);
26998
+ }
26999
+ //#endregion
27000
+ //#region ../sdk/src/board/render/sampling.ts
27001
+ function createBoardRenderSampler(document, animation, options = {}) {
27002
+ const colorScheme = options.colorScheme ?? "dark";
27003
+ const context = {
27004
+ colorScheme,
27005
+ colors: options.colors ?? buildFallbackShapeColors(colorScheme),
27006
+ palette: {
27007
+ ...defaultBoardPalette(colorScheme),
27008
+ ...options.palette
27009
+ }
27010
+ };
27011
+ return createBoardAnimationSampler(document, animation, { resolveColor: (color) => ({
27012
+ color: itemColor(context, color, "brand", "fill"),
27013
+ alpha: itemAlpha(context, color)
27014
+ }) });
27015
+ }
27016
+ //#endregion
27017
+ //#region ../sdk/src/board/render/text-measurement.ts
27018
+ const BOARD_FONT_STACKS = {
27019
+ sans: BOARD_FONT_STACK,
27020
+ serif: "ui-serif, Georgia, \"Noto Serif CJK SC\", \"Songti SC\", \"Noto Sans Math\", \"DejaVu Sans\", \"Noto Sans CJK SC\", \"FreeSans\", \"FreeSerif\", sans-serif",
27021
+ mono: BOARD_MONO_FONT_STACK
27022
+ };
27023
+ let measureContext;
27024
+ let installed = false;
27025
+ function getMeasureContext() {
27026
+ if (measureContext !== void 0) return measureContext;
27027
+ try {
27028
+ measureContext = DOMAdapter.get().createCanvas(1, 1).getContext("2d") ?? null;
27029
+ } catch {
27030
+ measureContext = null;
26058
27031
  }
26059
27032
  return measureContext;
26060
27033
  }
@@ -26074,38 +27047,115 @@ function ensureBoardTextMeasurement() {
26074
27047
  installBoardTextMeasurement();
26075
27048
  }
26076
27049
  //#endregion
26077
- //#region ../sdk/src/board/render/css-color.ts
26078
- const CACHE_LIMIT = 256;
26079
- const cache = /* @__PURE__ */ new Map();
26080
- let canvas = null;
26081
- function parseBoardCssColor(value) {
26082
- const key = value.trim();
26083
- if (!key) return null;
26084
- const cached = cache.get(key);
26085
- if (cached !== void 0 || cache.has(key)) return cached ?? null;
26086
- let result = null;
27050
+ //#region ../sdk/src/board/render/diagnostics.ts
27051
+ const glyphCache = /* @__PURE__ */ new Map();
27052
+ let glyphContext;
27053
+ function missingGlyph(character, font) {
27054
+ if (/\s/u.test(character)) return false;
27055
+ const key = `${font}:${character}`;
27056
+ const cached = glyphCache.get(key);
27057
+ if (cached !== void 0) return cached;
26087
27058
  try {
26088
- const direct = /^#([0-9a-f]{6})$/i.exec(key);
26089
- if (direct?.[1]) result = Number.parseInt(direct[1], 16);
26090
- else {
26091
- canvas ??= DOMAdapter.get().createCanvas(1, 1);
26092
- const context = canvas.getContext("2d");
26093
- if (context) {
26094
- context.fillStyle = "#010203";
26095
- context.fillStyle = key;
26096
- const first = String(context.fillStyle);
26097
- context.fillStyle = "#040506";
26098
- context.fillStyle = key;
26099
- const match = first === String(context.fillStyle) ? /^#([0-9a-f]{6})$/i.exec(first) : null;
26100
- if (match?.[1]) result = Number.parseInt(match[1], 16);
27059
+ glyphContext ??= DOMAdapter.get().createCanvas(64, 64).getContext("2d");
27060
+ const ctx = glyphContext;
27061
+ ctx.font = `32px ${font}`;
27062
+ const absent = "􏿿";
27063
+ if (ctx.measureText(character).width !== ctx.measureText(absent).width) {
27064
+ if (glyphCache.size >= 2048) glyphCache.clear();
27065
+ glyphCache.set(key, false);
27066
+ return false;
27067
+ }
27068
+ const pixels = (text) => {
27069
+ ctx.clearRect(0, 0, 64, 64);
27070
+ ctx.fillStyle = "#000000";
27071
+ ctx.fillText(text, 0, 40);
27072
+ return ctx.getImageData(0, 0, 64, 64).data;
27073
+ };
27074
+ const a = pixels(character);
27075
+ const b = pixels(absent);
27076
+ const missing = a.some((n) => n !== 0) && a.every((n, i) => n === b[i]);
27077
+ if (glyphCache.size >= 2048) glyphCache.clear();
27078
+ glyphCache.set(key, missing);
27079
+ return missing;
27080
+ } catch {
27081
+ return false;
27082
+ }
27083
+ }
27084
+ function measureBoardText(props) {
27085
+ ensureBoardTextMeasurement();
27086
+ const font = boardFontFamily(props.font, BOARD_FONT_STACKS);
27087
+ const characters = [...new Set(props.text)].slice(0, 512).filter((char) => missingGlyph(char, font)).join("");
27088
+ return {
27089
+ ...layoutBoardText(props),
27090
+ missingCharacters: characters
27091
+ };
27092
+ }
27093
+ function inspectBoardScene(scene, limit = 100) {
27094
+ ensureBoardTextMeasurement();
27095
+ const diagnostics = [];
27096
+ const texts = scene.items.filter((item) => item.type === "text" && scene.opacity(item.id) > .01);
27097
+ const push = (entry) => {
27098
+ if (diagnostics.length < limit) diagnostics.push(entry);
27099
+ };
27100
+ for (const item of scene.items) {
27101
+ if (diagnostics.length >= limit) break;
27102
+ if (item.type === "effect" && item.props.follow && item.props.kind !== "trail") push({
27103
+ code: "effect-follow-unused",
27104
+ item: item.id
27105
+ });
27106
+ }
27107
+ let glyphBudget = 256;
27108
+ for (const item of texts) {
27109
+ if (diagnostics.length >= limit || item.type !== "text") break;
27110
+ if (glyphBudget-- > 0) {
27111
+ const measured = measureBoardText(item.props);
27112
+ if (measured.missingCharacters) push({
27113
+ code: "glyph-unavailable",
27114
+ item: item.id,
27115
+ characters: measured.missingCharacters
27116
+ });
27117
+ }
27118
+ const bounds = itemBounds(item.frame);
27119
+ let parentId = item.parent;
27120
+ while (parentId) {
27121
+ const parent = scene.get(parentId);
27122
+ if (!parent) break;
27123
+ if (parent.type === "frame" && parent.props.clip) {
27124
+ const clip = itemBounds(parent.frame);
27125
+ if (bounds.x < clip.x - 1 || bounds.y < clip.y - 1 || bounds.x + bounds.width > clip.x + clip.width + 1 || bounds.y + bounds.height > clip.y + clip.height + 1) {
27126
+ push({
27127
+ code: "text-clipped",
27128
+ item: item.id,
27129
+ related: parentId
27130
+ });
27131
+ break;
27132
+ }
26101
27133
  }
27134
+ parentId = parent.parent;
27135
+ }
27136
+ }
27137
+ const sorted = texts.map((item) => ({
27138
+ item,
27139
+ rect: itemBounds(item.frame)
27140
+ })).sort((a, b) => a.rect.x - b.rect.x);
27141
+ let comparisons = 0;
27142
+ for (let i = 0; i < sorted.length && diagnostics.length < limit; i++) {
27143
+ const a = sorted[i];
27144
+ if (!a) continue;
27145
+ for (let j = i + 1; j < sorted.length; j++) {
27146
+ const b = sorted[j];
27147
+ if (!b) continue;
27148
+ if (b.rect.x >= a.rect.x + a.rect.width) break;
27149
+ if (++comparisons > 1e4) return diagnostics;
27150
+ if (a.item.parent === b.item.parent && rectsIntersect(a.rect, b.rect)) push({
27151
+ code: "text-overlap",
27152
+ item: a.item.id,
27153
+ related: b.item.id
27154
+ });
27155
+ if (diagnostics.length >= limit) break;
26102
27156
  }
26103
- } catch {
26104
- result = null;
26105
27157
  }
26106
- if (cache.size >= CACHE_LIMIT) cache.clear();
26107
- cache.set(key, result);
26108
- return result;
27158
+ return diagnostics;
26109
27159
  }
26110
27160
  //#endregion
26111
27161
  //#region ../sdk/src/board/render/clip.ts
@@ -26990,39 +28040,6 @@ const taskCardRenderer = {
26990
28040
  }
26991
28041
  };
26992
28042
  //#endregion
26993
- //#region ../sdk/src/board/render/palette.ts
26994
- const DARK = {
26995
- bg: 1315860,
26996
- surface: 2105376,
26997
- hover: 2763306,
26998
- border: 3815994,
26999
- brand: 16734751,
27000
- text: 16053492,
27001
- muted: 9211020,
27002
- rare: 3718648,
27003
- epic: 10980346,
27004
- legendary: 16096779
27005
- };
27006
- const LIGHT = {
27007
- bg: 16777215,
27008
- surface: 16053493,
27009
- hover: 15329772,
27010
- border: 14079706,
27011
- brand: 15222030,
27012
- text: 1579035,
27013
- muted: 7041664,
27014
- rare: 2450411,
27015
- epic: 8141549,
27016
- legendary: 11817737
27017
- };
27018
- function defaultBoardPalette(mode) {
27019
- return { ...mode === "light" ? LIGHT : DARK };
27020
- }
27021
- function itemColor(context, color, fallback, part = "stroke") {
27022
- if (((typeof color === "object" && color ? color[context.colorScheme] : color) ?? fallback) === "neutral" && part !== "fill") return context.palette.text;
27023
- return resolveItemColor(color, fallback, context.colors, context.colorScheme, parseBoardCssColor, part);
27024
- }
27025
- //#endregion
27026
28043
  //#region ../sdk/src/board/render/stroke.ts
27027
28044
  const HEAD_SPREAD = Math.PI / 6;
27028
28045
  function dashPattern(dash, width) {
@@ -27110,6 +28127,27 @@ function endAngle(path, atStart) {
27110
28127
  }
27111
28128
  return null;
27112
28129
  }
28130
+ function roundRectPoints(width, height, radius) {
28131
+ const r = Math.min(radius, width / 2, height / 2);
28132
+ if (r <= 0) return [
28133
+ worldPoint(0, 0),
28134
+ worldPoint(width, 0),
28135
+ worldPoint(width, height),
28136
+ worldPoint(0, height),
28137
+ worldPoint(0, 0)
28138
+ ];
28139
+ const corner = (cx, cy, from) => Array.from({ length: 9 }, (_, index) => {
28140
+ const angle = from + index / 8 * (Math.PI / 2);
28141
+ return worldPoint(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r);
28142
+ });
28143
+ return [
28144
+ ...corner(width - r, r, -Math.PI / 2),
28145
+ ...corner(width - r, height - r, 0),
28146
+ ...corner(r, height - r, Math.PI / 2),
28147
+ ...corner(r, r, Math.PI),
28148
+ worldPoint(width - r, 0)
28149
+ ];
28150
+ }
27113
28151
  //#endregion
27114
28152
  //#region ../sdk/src/board/render/renderers/arrow-card-renderer.ts
27115
28153
  const partsByContainer$10 = /* @__PURE__ */ new WeakMap();
@@ -27122,6 +28160,7 @@ function sync$10(container, item, context) {
27122
28160
  const { props, style } = item;
27123
28161
  const selected = context.selectedIds.has(item.id);
27124
28162
  const hovered = context.hoveredId === item.id;
28163
+ parts.line.alpha = itemAlpha(context, style.stroke);
27125
28164
  const color = itemColor(context, style.stroke, "brand");
27126
28165
  const size = style.strokeWidth ?? 2.5;
27127
28166
  syncTextResolution(parts.label, parts, context.zoom);
@@ -27235,7 +28274,7 @@ const arrowCardRenderer = {
27235
28274
  drawFarStroke(graphics, trimPolyline(resolveSceneArrow(arrow, context.scene).path, arrow.style.trim), {
27236
28275
  color: itemColor(context, arrow.style.stroke, "brand"),
27237
28276
  width: arrow.style.strokeWidth ?? 2.5,
27238
- alpha: .85
28277
+ alpha: .85 * itemAlpha(context, arrow.style.stroke)
27239
28278
  });
27240
28279
  },
27241
28280
  destroy: (container) => {
@@ -27255,7 +28294,7 @@ function sync$9(container, item, context) {
27255
28294
  const { width, height } = item.frame;
27256
28295
  const selected = context.selectedIds.has(item.id);
27257
28296
  const hovered = context.hoveredId === item.id;
27258
- const title = item.props.snapshot?.title ?? item.props.src.split("/").pop() ?? "Audio";
28297
+ const title = item.props.snapshot?.title ?? getMediaResourceTitle(item.props.src);
27259
28298
  syncTextResolution(parts.label, parts, context.zoom);
27260
28299
  if (parts.label.text !== title) parts.label.text = title;
27261
28300
  syncTextWrapWidth(parts.label, parts, Math.max(1, width - 24), false);
@@ -27430,6 +28469,7 @@ function sync$8(container, item, context) {
27430
28469
  }
27431
28470
  const stroke = parts.stroke;
27432
28471
  if (!stroke) return;
28472
+ stroke.alpha = itemAlpha(context, item.style.stroke);
27433
28473
  if (stroke instanceof Mesh) stroke.tint = color;
27434
28474
  const { box } = parts;
27435
28475
  const sx = item.frame.width / box.width;
@@ -27472,7 +28512,7 @@ const drawCardRenderer = {
27472
28512
  drawFarStroke(graphics, farStrokeSamples(trimDrawPoints(draw.props.points, draw.style.trim)).map((point) => applyMatrix$1(matrix, point)), {
27473
28513
  color: itemColor(context, draw.style.stroke, "brand"),
27474
28514
  width: draw.style.strokeWidth ?? 4,
27475
- alpha: 1
28515
+ alpha: itemAlpha(context, draw.style.stroke)
27476
28516
  });
27477
28517
  },
27478
28518
  destroy: (container) => {
@@ -27588,11 +28628,27 @@ function drawTrail(graphics, parts, item, context, color) {
27588
28628
  parts.trail.length = 0;
27589
28629
  return;
27590
28630
  }
27591
- const head = {
27592
- x: target.frame.x + target.frame.width / 2 - item.frame.x,
27593
- y: target.frame.y + target.frame.height / 2 - item.frame.y,
27594
- t: time
28631
+ const angle = item.frame.rotation * Math.PI / 180;
28632
+ const localPoint = (frame, t) => {
28633
+ const dx = frame.x + frame.width / 2 - item.frame.x - item.frame.width / 2;
28634
+ const dy = frame.y + frame.height / 2 - item.frame.y - item.frame.height / 2;
28635
+ return {
28636
+ x: dx * Math.cos(angle) + dy * Math.sin(angle) + item.frame.width / 2,
28637
+ y: -dx * Math.sin(angle) + dy * Math.cos(angle) + item.frame.height / 2,
28638
+ t
28639
+ };
27595
28640
  };
28641
+ const head = localPoint(target.frame, time);
28642
+ if (context.frameAt) {
28643
+ parts.trail.length = 0;
28644
+ const start = Math.max(0, time - item.props.life);
28645
+ const step = Math.max(1e3 / 30, item.props.life / 63);
28646
+ for (let t = Math.ceil(start / step) * step; t < time; t += step) {
28647
+ const past = context.frameAt(target.id, t);
28648
+ if (past) parts.trail.push(localPoint(past, t));
28649
+ }
28650
+ parts.trail.push(head);
28651
+ }
27596
28652
  const last = parts.trail.at(-1);
27597
28653
  if (last && (time < last.t || time - last.t > item.props.life)) parts.trail.length = 0;
27598
28654
  if (!last || Math.hypot(head.x - last.x, head.y - last.y) > .5) parts.trail.push(head);
@@ -27619,6 +28675,7 @@ function sync$7(container, item, context) {
27619
28675
  const seed = hashString(props.seed ?? item.id);
27620
28676
  const time = Math.max(0, context.time);
27621
28677
  const graphics = parts.graphics;
28678
+ graphics.alpha = itemAlpha(context, item.style.fill ?? item.style.stroke);
27622
28679
  graphics.clear();
27623
28680
  switch (props.kind) {
27624
28681
  case "particles":
@@ -27685,13 +28742,15 @@ function sync$6(container, item, context) {
27685
28742
  const selected = context.selectedIds.has(item.id);
27686
28743
  const hovered = context.hoveredId === item.id;
27687
28744
  const fill = itemColor(context, style.fill ?? style.stroke, "neutral", "fill");
27688
- const fillAlpha = style.fillOpacity ?? (style.fill ? 1 : DEFAULT_WASH);
28745
+ const fillAlpha = (style.fillOpacity ?? (style.fill ? 1 : DEFAULT_WASH)) * itemAlpha(context, style.fill ?? style.stroke);
27689
28746
  const baseStroke = itemColor(context, style.stroke, "neutral");
27690
- const stroke = selected ? context.palette.brand : baseStroke;
27691
- const alpha = selected ? 1 : hovered ? .85 : .55;
28747
+ const stroke = selected ? context.palette.brand : hovered && style.stroke ? context.palette.muted : baseStroke;
28748
+ const alpha = (selected || style.stroke ? 1 : hovered ? .85 : .55) * itemAlpha(context, style.stroke);
27692
28749
  const radius = style.radius ?? 4;
27693
28750
  const strokeWidth = style.strokeWidth ?? 1.5;
28751
+ const decorated = props.decoration !== "none";
27694
28752
  const visualSig = [
28753
+ props.decoration,
27695
28754
  frame.width,
27696
28755
  frame.height,
27697
28756
  selected,
@@ -27699,23 +28758,36 @@ function sync$6(container, item, context) {
27699
28758
  fill,
27700
28759
  fillAlpha,
27701
28760
  stroke,
28761
+ alpha,
27702
28762
  radius,
27703
- strokeWidth
28763
+ strokeWidth,
28764
+ style.dash,
28765
+ style.trim
27704
28766
  ].join("|");
27705
28767
  if (visualSig !== parts.visualSig) {
27706
28768
  parts.visualSig = visualSig;
27707
28769
  parts.box.clear();
27708
- parts.box.roundRect(0, 0, frame.width, frame.height, radius).fill({
28770
+ if (decorated) parts.box.roundRect(0, 0, frame.width, frame.height, radius).fill({
27709
28771
  color: fill,
27710
28772
  alpha: fillAlpha
27711
28773
  });
27712
- if (strokeWidth > 0 || selected) parts.box.roundRect(0, 0, frame.width, frame.height, radius).stroke({
27713
- color: stroke,
27714
- width: selected ? Math.max(2, strokeWidth) : strokeWidth,
27715
- alpha
27716
- });
28774
+ const width = selected ? Math.max(2, strokeWidth) : strokeWidth;
28775
+ const pattern = decorated ? dashPattern(style.dash, width) : null;
28776
+ const trim = decorated ? style.trim ?? 1 : 1;
28777
+ if ((decorated && strokeWidth > 0 || selected || hovered) && trim > 0) {
28778
+ if (pattern || trim < 1) tracePolyline(parts.box, trimPolyline(roundRectPoints(frame.width, frame.height, radius), trim), pattern);
28779
+ else parts.box.roundRect(0, 0, frame.width, frame.height, radius);
28780
+ parts.box.stroke({
28781
+ color: stroke,
28782
+ width,
28783
+ alpha,
28784
+ cap: pattern || trim < 1 ? "round" : "butt",
28785
+ join: "round"
28786
+ });
28787
+ }
27717
28788
  }
27718
- const title = locked ? `🔒 ${props.label || "Frame"}` : props.label || "Frame";
28789
+ parts.label.visible = decorated;
28790
+ const title = locked ? `🔒 ${props.label}`.trim() : props.label;
27719
28791
  const labelColor = style.stroke ? baseStroke : context.palette.muted;
27720
28792
  const textSig = [title, labelColor].join("|");
27721
28793
  if (textSig !== parts.textSig) {
@@ -27753,10 +28825,10 @@ const frameCardRenderer = {
27753
28825
  if (item.type === "frame") sync$6(container, item, context);
27754
28826
  },
27755
28827
  renderFar: (graphics, item, context) => {
27756
- if (item.type !== "frame") return;
28828
+ if (item.type !== "frame" || item.props.decoration === "none") return;
27757
28829
  drawFarPlate(graphics, item.frame, {
27758
28830
  fill: itemColor(context, item.style.fill ?? item.style.stroke, "neutral", "fill"),
27759
- fillAlpha: Math.max(.07, item.style.fillOpacity ?? (item.style.fill ? 1 : 0))
28831
+ fillAlpha: Math.max(.07, item.style.fillOpacity ?? (item.style.fill ? 1 : 0)) * itemAlpha(context, item.style.fill ?? item.style.stroke)
27760
28832
  });
27761
28833
  },
27762
28834
  destroy: (container) => {
@@ -27845,27 +28917,7 @@ function outlinePoints(geometry, width, height, radius) {
27845
28917
  p(0, height),
27846
28918
  p(width / 2, 0)
27847
28919
  ];
27848
- default: {
27849
- const r = Math.min(radius, width / 2, height / 2);
27850
- if (r <= 0) return [
27851
- p(0, 0),
27852
- p(width, 0),
27853
- p(width, height),
27854
- p(0, height),
27855
- p(0, 0)
27856
- ];
27857
- const corner = (cx, cy, from) => Array.from({ length: 9 }, (_, index) => {
27858
- const angle = from + index / 8 * (Math.PI / 2);
27859
- return p(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r);
27860
- });
27861
- return [
27862
- ...corner(width - r, r, -Math.PI / 2),
27863
- ...corner(width - r, height - r, 0),
27864
- ...corner(r, height - r, Math.PI / 2),
27865
- ...corner(r, r, Math.PI),
27866
- p(width - r, 0)
27867
- ];
27868
- }
28920
+ default: return roundRectPoints(width, height, radius);
27869
28921
  }
27870
28922
  }
27871
28923
  function sync$5(container, item, context) {
@@ -27878,7 +28930,8 @@ function sync$5(container, item, context) {
27878
28930
  const hovered = context.hoveredId === item.id;
27879
28931
  const stroke = itemColor(context, style.stroke ?? style.fill, "brand");
27880
28932
  const fill = itemColor(context, style.fill ?? style.stroke, "brand", "fill");
27881
- const fillOpacity = style.fillOpacity ?? (style.fill ? 1 : 0);
28933
+ const fillOpacity = (style.fillOpacity ?? (style.fill ? 1 : 0)) * itemAlpha(context, style.fill ?? style.stroke);
28934
+ const strokeAlpha = itemAlpha(context, style.stroke ?? style.fill);
27882
28935
  const strokeWidth = style.strokeWidth ?? 1.75;
27883
28936
  syncTextResolution(parts.label, parts, context.zoom);
27884
28937
  const visualSig = [
@@ -27891,6 +28944,7 @@ function sync$5(container, item, context) {
27891
28944
  fill,
27892
28945
  fillOpacity,
27893
28946
  stroke,
28947
+ strokeAlpha,
27894
28948
  strokeWidth,
27895
28949
  style.dash,
27896
28950
  style.radius,
@@ -27918,12 +28972,13 @@ function sync$5(container, item, context) {
27918
28972
  parts.shape.stroke({
27919
28973
  color: hovered && !selected ? context.palette.muted : stroke,
27920
28974
  width: lineWidth,
27921
- alpha: selected ? 1 : hovered ? .95 : .85,
28975
+ alpha: (selected ? 1 : hovered ? .95 : .85) * strokeAlpha,
27922
28976
  cap: partial || pattern ? "round" : "butt",
27923
28977
  join: "round"
27924
28978
  });
27925
28979
  }
27926
28980
  }
28981
+ parts.label.alpha = itemAlpha(context, style.stroke);
27927
28982
  const label = itemColor(context, style.stroke, "neutral", "label");
27928
28983
  const textSig = [
27929
28984
  props.text,
@@ -27985,9 +29040,9 @@ const shapeCardRenderer = {
27985
29040
  if (item.type !== "shape") return;
27986
29041
  drawFarPlate(graphics, item.frame, {
27987
29042
  fill: itemColor(context, item.style.fill ?? item.style.stroke, "brand", "fill"),
27988
- fillAlpha: Math.max(.18, item.style.fillOpacity ?? 0),
29043
+ fillAlpha: Math.max(.18, item.style.fillOpacity ?? 0) * itemAlpha(context, item.style.fill ?? item.style.stroke),
27989
29044
  accent: itemColor(context, item.style.stroke, "brand"),
27990
- accentAlpha: .9
29045
+ accentAlpha: .9 * itemAlpha(context, item.style.stroke)
27991
29046
  });
27992
29047
  },
27993
29048
  destroy: (container) => {
@@ -27996,26 +29051,27 @@ const shapeCardRenderer = {
27996
29051
  }
27997
29052
  };
27998
29053
  //#endregion
27999
- //#region ../sdk/src/board/render/renderers/sketch-card-renderer.ts
29054
+ //#region ../sdk/src/board/render/renderers/custom-card-renderer.ts
28000
29055
  const partsByContainer$4 = /* @__PURE__ */ new WeakMap();
28001
29056
  function sync$4(container, item, context) {
28002
29057
  const parts = partsByContainer$4.get(container);
28003
29058
  if (!parts) return;
28004
29059
  if (parts.id !== item.id) {
28005
- context.sketches?.release(parts.id);
29060
+ context.scripts?.release(parts.id);
28006
29061
  parts.id = item.id;
28007
29062
  }
28008
29063
  positionShell(parts.root, item);
28009
29064
  const { width, height } = item.frame;
28010
29065
  const resolution = Math.min(4, Math.max(.25, context.zoom * getBoardResolution()));
28011
- const texture = context.sketches?.frame(item, context.time, resolution) ?? null;
28012
- const error = context.sketches?.error(item.id) ?? null;
29066
+ const texture = context.scripts?.frame(item, context.time, resolution) ?? null;
29067
+ const error = context.scripts?.error(item.id) ?? null;
28013
29068
  parts.sprite.visible = Boolean(texture);
28014
29069
  if (texture && parts.sprite.texture !== texture) parts.sprite.texture = texture;
28015
29070
  parts.sprite.width = width;
28016
29071
  parts.sprite.height = height;
28017
29072
  const selected = context.selectedIds.has(item.id);
28018
- const message = texture ? "" : error ? `${item.props.src}\n${error}` : item.props.src;
29073
+ const ready = Boolean(texture) || context.scripts?.ready?.(item.id) === true;
29074
+ const message = error ? `${item.script.src}\n${error}` : ready ? "" : item.script.src;
28019
29075
  const sig = [
28020
29076
  width,
28021
29077
  height,
@@ -28028,21 +29084,21 @@ function sync$4(container, item, context) {
28028
29084
  if (sig === parts.sig) return;
28029
29085
  parts.sig = sig;
28030
29086
  parts.placeholder.clear();
28031
- if (!texture || selected) parts.placeholder.rect(0, 0, width, height).stroke({
29087
+ if (!ready || selected || error) parts.placeholder.rect(0, 0, width, height).stroke({
28032
29088
  color: selected ? context.palette.brand : context.palette.border,
28033
29089
  width: selected ? 2 : 1,
28034
29090
  alpha: .8
28035
29091
  });
28036
- parts.label.visible = !texture;
29092
+ parts.label.visible = !ready || Boolean(error);
28037
29093
  parts.label.text = message;
28038
29094
  parts.label.style.fill = error ? context.colors.rose.stroke : context.palette.muted;
28039
29095
  parts.label.style.wordWrapWidth = Math.max(40, width - 16);
28040
29096
  parts.label.position.set(width / 2, height / 2);
28041
29097
  }
28042
- const sketchCardRenderer = {
28043
- id: "sketch-card",
28044
- canRender: (item) => item.type === "sketch",
28045
- animated: true,
29098
+ const customCardRenderer = {
29099
+ id: "custom-card",
29100
+ canRender: (item) => item.type === "custom",
29101
+ animated: false,
28046
29102
  create: (item, context) => {
28047
29103
  const root = new Container();
28048
29104
  const sprite = new Sprite(Texture.EMPTY);
@@ -28068,11 +29124,11 @@ const sketchCardRenderer = {
28068
29124
  label,
28069
29125
  sig: ""
28070
29126
  });
28071
- if (item.type === "sketch") sync$4(root, item, context);
29127
+ if (item.type === "custom") sync$4(root, item, context);
28072
29128
  return root;
28073
29129
  },
28074
29130
  update: (container, item, context) => {
28075
- if (item.type === "sketch") sync$4(container, item, context);
29131
+ if (item.type === "custom") sync$4(container, item, context);
28076
29132
  },
28077
29133
  renderFar: (graphics, item, context) => {
28078
29134
  drawFarPlate(graphics, item.frame, {
@@ -28082,7 +29138,7 @@ const sketchCardRenderer = {
28082
29138
  },
28083
29139
  destroy: (container, context) => {
28084
29140
  const parts = partsByContainer$4.get(container);
28085
- if (parts) context.sketches?.release(parts.id);
29141
+ if (parts) context.scripts?.release(parts.id);
28086
29142
  parts?.root.destroy({ children: true });
28087
29143
  partsByContainer$4.delete(container);
28088
29144
  }
@@ -28102,6 +29158,39 @@ function layoutContain$1(sprite, width, height) {
28102
29158
  sprite.x = (width - sprite.width) / 2;
28103
29159
  sprite.y = (height - sprite.height) / 2;
28104
29160
  }
29161
+ function cropRect(crop) {
29162
+ if (!crop) return null;
29163
+ const w = Math.min(crop.w, 1 - crop.x);
29164
+ const h = Math.min(crop.h, 1 - crop.y);
29165
+ return w > 0 && h > 0 ? {
29166
+ x: crop.x,
29167
+ y: crop.y,
29168
+ w,
29169
+ h
29170
+ } : null;
29171
+ }
29172
+ function showTexture(parts, texture, crop) {
29173
+ const rect = texture ? cropRect(crop) : null;
29174
+ const key = rect ? `${rect.x}|${rect.y}|${rect.w}|${rect.h}` : "";
29175
+ const previous = parts.cropped;
29176
+ const reusable = previous && parts.cropSource === texture && parts.cropKey === key;
29177
+ if (texture && rect && !reusable) {
29178
+ const { frame } = texture;
29179
+ const width = Math.max(1, frame.width * rect.w);
29180
+ const height = Math.max(1, frame.height * rect.h);
29181
+ const x = frame.x + Math.min(frame.width * rect.x, frame.width - width);
29182
+ const y = frame.y + Math.min(frame.height * rect.y, frame.height - height);
29183
+ parts.cropped = new Texture({
29184
+ source: texture.source,
29185
+ frame: new Rectangle(x, y, width, height)
29186
+ });
29187
+ } else if (!rect) parts.cropped = null;
29188
+ parts.cropSource = rect ? texture : null;
29189
+ parts.cropKey = key;
29190
+ const display = parts.cropped ?? texture ?? Texture.EMPTY;
29191
+ if (parts.sprite.texture !== display) parts.sprite.texture = display;
29192
+ if (previous && previous !== parts.cropped) previous.destroy(false);
29193
+ }
28105
29194
  function sync$3(container, item, context) {
28106
29195
  const parts = partsByContainer$3.get(container);
28107
29196
  if (!parts) return;
@@ -28120,12 +29209,13 @@ function sync$3(container, item, context) {
28120
29209
  selected,
28121
29210
  hovered,
28122
29211
  texId,
29212
+ JSON.stringify(item.props.crop ?? null),
28123
29213
  failed,
28124
29214
  context.colorScheme
28125
29215
  ].join("|");
28126
29216
  if (sig === parts.sig) return;
28127
29217
  parts.sig = sig;
28128
- if (texture && parts.sprite.texture !== texture) parts.sprite.texture = texture;
29218
+ showTexture(parts, texture, item.props.crop);
28129
29219
  parts.sprite.visible = Boolean(texture);
28130
29220
  if (texture) layoutContain$1(parts.sprite, width, height);
28131
29221
  parts.placeholder.clear();
@@ -28185,7 +29275,10 @@ const imageCardRenderer = {
28185
29275
  root,
28186
29276
  sprite,
28187
29277
  placeholder,
28188
- sig: ""
29278
+ sig: "",
29279
+ cropped: null,
29280
+ cropSource: null,
29281
+ cropKey: ""
28189
29282
  });
28190
29283
  if (item.type === "image") sync$3(root, item, context);
28191
29284
  return root;
@@ -28205,7 +29298,9 @@ const imageCardRenderer = {
28205
29298
  });
28206
29299
  },
28207
29300
  destroy: (container) => {
28208
- partsByContainer$3.get(container)?.root.destroy({ children: true });
29301
+ const parts = partsByContainer$3.get(container);
29302
+ parts?.root.destroy({ children: true });
29303
+ parts?.cropped?.destroy(false);
28209
29304
  partsByContainer$3.delete(container);
28210
29305
  }
28211
29306
  };
@@ -28237,6 +29332,8 @@ function sync$2(container, item, context) {
28237
29332
  parts.body.anchor.set(anchorX, 0);
28238
29333
  parts.body.position.set(item.frame.width * anchorX, 0);
28239
29334
  syncTextResolution(parts.body, parts, context.zoom * Math.max(1, previewScale));
29335
+ parts.body.alpha = itemAlpha(context, item.style.fill);
29336
+ parts.caret.alpha = parts.body.alpha;
28240
29337
  const ink = itemColor(context, item.style.fill, "neutral");
28241
29338
  const text = revealed(props.text, props.reveal);
28242
29339
  const contentSig = [
@@ -28297,7 +29394,7 @@ const textCardRenderer = {
28297
29394
  if (item.type !== "text") return;
28298
29395
  drawFarPlate(graphics, item.frame, {
28299
29396
  fill: itemColor(context, item.style.fill, "neutral"),
28300
- fillAlpha: .35
29397
+ fillAlpha: .35 * itemAlpha(context, item.style.fill)
28301
29398
  });
28302
29399
  },
28303
29400
  destroy: (container) => {
@@ -28357,6 +29454,7 @@ const unknownCardRenderer = {
28357
29454
  create: (item, context) => {
28358
29455
  const root = new Container();
28359
29456
  const box = new Graphics();
29457
+ box.context.batchMode = "batch";
28360
29458
  const resolution = textResolutionForZoom(context.zoom);
28361
29459
  const label = new Text({
28362
29460
  text: "",
@@ -28416,7 +29514,7 @@ function sync(container, item, context) {
28416
29514
  const selected = context.selectedIds.has(item.id);
28417
29515
  const hovered = context.hoveredId === item.id;
28418
29516
  const resizing = context.resizingIds.has(item.id);
28419
- const title = item.props.snapshot?.title ?? item.props.src.split("/").pop() ?? "Video";
29517
+ const title = item.props.snapshot?.title ?? getMediaResourceTitle(item.props.src);
28420
29518
  const key = context.assetKey(item);
28421
29519
  const texture = key ? context.getTexture(key) : null;
28422
29520
  const failed = Boolean(key && !texture && context.hasError(key));
@@ -28546,7 +29644,7 @@ const builtinCardRenderers = [
28546
29644
  arrowCardRenderer,
28547
29645
  frameCardRenderer,
28548
29646
  effectCardRenderer,
28549
- sketchCardRenderer
29647
+ customCardRenderer
28550
29648
  ];
28551
29649
  /**
28552
29650
  * Pick a renderer per item: `renderers` first, then the built-ins. Items no
@@ -28562,100 +29660,968 @@ function createBoardCardRendererResolver(renderers = []) {
28562
29660
  /** The built-in renderers alone. */
28563
29661
  const getBoardCardRenderer = createBoardCardRendererResolver();
28564
29662
  //#endregion
28565
- //#region ../sdk/src/board/export/scene.ts
28566
- function buildContext(input) {
28567
- const missing = /* @__PURE__ */ new Set();
28568
- const textures = input.textures ?? /* @__PURE__ */ new Map();
28569
- return {
28570
- context: {
28571
- settings: input.settings,
28572
- scene: input.scene,
28573
- time: input.time ?? 0,
28574
- selectedIds: /* @__PURE__ */ new Set(),
28575
- hoveredId: null,
28576
- resizingIds: /* @__PURE__ */ new Set(),
28577
- palette: {
28578
- ...defaultBoardPalette(input.colorScheme),
28579
- ...input.palette
28580
- },
28581
- colors: input.colors ?? buildFallbackShapeColors(input.colorScheme),
28582
- colorScheme: input.colorScheme,
28583
- rendererType: "canvas",
28584
- zoom: input.scale,
28585
- sketches: input.sketches,
28586
- assetKey: input.assetKey ?? imageAssetKey,
28587
- getTexture: (key) => textures.get(key) ?? null,
28588
- hasError: (key) => {
28589
- if (!textures.has(key)) missing.add(key);
28590
- return false;
28591
- },
28592
- fileState: () => "ok",
28593
- acquireTexture: () => {},
28594
- releaseTexture: () => {}
28595
- },
28596
- missing
28597
- };
29663
+ //#region ../protocol/dist/board-url.js
29664
+ const BOARD_REMOTE_URL_MAX_LENGTH = 4096;
29665
+ function parseIpv4(host) {
29666
+ const parts = host.split(".").map(Number);
29667
+ return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) ? parts : null;
29668
+ }
29669
+ function isBlockedIpv4(host) {
29670
+ const parts = parseIpv4(host);
29671
+ if (!parts) return false;
29672
+ const [first, second, third] = parts;
29673
+ if (first === 0 || first === 10 || first === 127) return true;
29674
+ if (first === 100 && second >= 64 && second <= 127) return true;
29675
+ if (first === 169 && second === 254) return true;
29676
+ if (first === 172 && second >= 16 && second <= 31) return true;
29677
+ if (first === 192 && second === 168) return true;
29678
+ if (first === 192 && second === 0 && (third === 0 || third === 2)) return true;
29679
+ if (first === 192 && second === 88 && third === 99) return true;
29680
+ if (first === 198 && (second === 18 || second === 19)) return true;
29681
+ if (first === 198 && second === 51 && third === 100) return true;
29682
+ if (first === 203 && second === 0 && third === 113) return true;
29683
+ return first >= 224;
29684
+ }
29685
+ function expandIpv6(host) {
29686
+ const [head, tail, extra] = host.toLowerCase().split("::");
29687
+ if (extra !== void 0) return null;
29688
+ const headParts = head ? head.split(":").filter(Boolean) : [];
29689
+ const tailParts = tail ? tail.split(":").filter(Boolean) : [];
29690
+ const missing = 8 - headParts.length - tailParts.length;
29691
+ if (missing < 0 || tail === void 0 && missing !== 0) return null;
29692
+ const parts = [
29693
+ ...headParts,
29694
+ ...Array.from({ length: missing }, () => "0"),
29695
+ ...tailParts
29696
+ ];
29697
+ if (parts.length !== 8 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) return null;
29698
+ return parts.map((part) => part.padStart(4, "0"));
29699
+ }
29700
+ function isBlockedIpv6(host) {
29701
+ const parts = expandIpv6(host);
29702
+ if (!parts) return true;
29703
+ if (parts.every((part) => part === "0000")) return true;
29704
+ if (parts.slice(0, 7).every((part) => part === "0000") && parts[7] === "0001") return true;
29705
+ if (parts.slice(0, 5).every((part) => part === "0000") && parts[5] === "ffff") {
29706
+ const high = Number.parseInt(parts[6] ?? "0", 16);
29707
+ const low = Number.parseInt(parts[7] ?? "0", 16);
29708
+ return isBlockedIpv4(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
29709
+ }
29710
+ if (parts.slice(0, 6).every((part) => part === "0000")) return true;
29711
+ const first = Number.parseInt(parts[0] ?? "0", 16);
29712
+ if ((first & 65024) === 64512) return true;
29713
+ if ((first & 65472) === 65152 || (first & 65472) === 65216) return true;
29714
+ if ((first & 65280) === 65280) return true;
29715
+ return parts[0] === "2001" && parts[1] === "0db8";
29716
+ }
29717
+ function isPublicBoardRemoteAddress(value) {
29718
+ const address = value.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
29719
+ if (parseIpv4(address)) return !isBlockedIpv4(address);
29720
+ return address.includes(":") && !isBlockedIpv6(address);
29721
+ }
29722
+ function isBlockedHost(hostname) {
29723
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
29724
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
29725
+ if (parseIpv4(host) || host.includes(":")) return !isPublicBoardRemoteAddress(host);
29726
+ return false;
28598
29727
  }
28599
- function createBoardExportScene(input) {
28600
- const { context, missing } = buildContext(input);
28601
- const root = new Container({ label: "board-export-root" });
28602
- const outputWidth = input.world.width * input.scale;
28603
- const outputHeight = input.world.height * input.scale;
28604
- if (input.background != null) root.addChild(new Graphics().rect(0, 0, outputWidth, outputHeight).fill({
28605
- color: input.background,
28606
- alpha: 1
28607
- }));
28608
- if (input.backgroundImage) {
28609
- const { texture, fit, position, opacity } = input.backgroundImage;
28610
- if (fit === "repeat") root.addChild(new TilingSprite({
28611
- texture,
28612
- width: outputWidth,
28613
- height: outputHeight,
28614
- alpha: opacity
28615
- }));
28616
- else {
28617
- const sprite = new Sprite({
28618
- texture,
28619
- alpha: opacity
28620
- });
28621
- const scale = fit === "cover" ? Math.max(outputWidth / texture.width, outputHeight / texture.height) : Math.min(outputWidth / texture.width, outputHeight / texture.height);
28622
- sprite.width = texture.width * scale;
28623
- sprite.height = texture.height * scale;
28624
- const x = position === "left" ? 0 : position === "right" ? outputWidth - sprite.width : (outputWidth - sprite.width) / 2;
28625
- const y = position === "top" ? 0 : position === "bottom" ? outputHeight - sprite.height : (outputHeight - sprite.height) / 2;
28626
- sprite.position.set(x, y);
28627
- root.addChild(sprite);
28628
- }
28629
- }
28630
- const world = new Container({
28631
- isRenderGroup: true,
28632
- label: "board-export-world"
28633
- });
28634
- world.scale.set(input.scale);
28635
- world.position.set(-input.world.x * input.scale, -input.world.y * input.scale);
28636
- const resolve = input.renderers ?? getBoardCardRenderer;
28637
- const groups = /* @__PURE__ */ new Map();
28638
- for (const item of input.items) {
28639
- const clip = clippingAncestor(input.scene, item);
28640
- const host = clip && groups.get(clip) || world;
28641
- const container = resolve(item, context).create(item, context);
28642
- container.alpha = input.scene.opacity(item.id);
28643
- host.addChild(container);
28644
- if (isClippingFrame(item)) {
28645
- const { group } = syncClipGroup(void 0, item);
28646
- host.addChild(group);
28647
- groups.set(item.id, group);
28648
- }
29728
+ /**
29729
+ * Normalize a browser-loadable public HTTP(S) URL. This blocks explicit local
29730
+ * addresses; any future server-side fetcher must additionally validate DNS
29731
+ * resolution to defend against rebinding.
29732
+ */
29733
+ function normalizeBoardRemoteUrl(value) {
29734
+ if (typeof value !== "string") return void 0;
29735
+ const input = value.trim();
29736
+ if (!input || input.length > 4096) return void 0;
29737
+ try {
29738
+ const url = new URL(input);
29739
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
29740
+ if (url.username || url.password || isBlockedHost(url.hostname)) return;
29741
+ const normalized = url.toString();
29742
+ return normalized.length <= 4096 ? normalized : void 0;
29743
+ } catch {
29744
+ return;
28649
29745
  }
28650
- root.addChild(world);
28651
- return {
28652
- root,
28653
- missingImageKeys: [...missing],
28654
- destroy: () => root.destroy({ children: true })
28655
- };
28656
29746
  }
28657
- //#endregion
28658
- //#region ../sdk/src/board/export/index.ts
29747
+ string().max(BOARD_REMOTE_URL_MAX_LENGTH).refine((value) => normalizeBoardRemoteUrl(value) !== void 0, { message: "URL must be a public HTTP(S) URL without credentials" });
29748
+ const BOARD_CAMERA_TARGET = "camera";
29749
+ const BoardKeySchema = string().min(1).max(160).regex(/^[^\s/]+$/, "ids must not contain whitespace or '/'");
29750
+ const BoardIdSchema = BoardKeySchema.refine((value) => value !== BOARD_CAMERA_TARGET, "\"camera\" is reserved");
29751
+ const finite = number$1().finite();
29752
+ const nonNegative = finite.nonnegative();
29753
+ const ratio = finite.min(0).max(1);
29754
+ const BoardVec2Schema = object({
29755
+ x: finite,
29756
+ y: finite
29757
+ }).strict();
29758
+ const BoardSizeSchema = object({
29759
+ width: finite.positive(),
29760
+ height: finite.positive()
29761
+ }).strict();
29762
+ const BOARD_COLOR_TOKENS = [
29763
+ "brand",
29764
+ "neutral",
29765
+ "black",
29766
+ "white",
29767
+ "blue",
29768
+ "green",
29769
+ "amber",
29770
+ "violet",
29771
+ "rose"
29772
+ ];
29773
+ const CSS_NAMED_COLORS = new Set("transparent aliceblue antiquewhite aqua aquamarine azure beige bisque blanchedalmond blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray grey greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen steelblue tan teal thistle tomato turquoise violet wheat whitesmoke yellow yellowgreen".split(" "));
29774
+ function isBoardColorString(value) {
29775
+ const color = value.trim().toLowerCase();
29776
+ return BOARD_COLOR_TOKENS.includes(color) || CSS_NAMED_COLORS.has(color) || /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(color) || /^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\([^()]*\)$/.test(color);
29777
+ }
29778
+ const colorString = string().max(120).refine(isBoardColorString, "expected a palette token or a CSS color");
29779
+ const BoardColorSchema = union([colorString, object({
29780
+ light: colorString,
29781
+ dark: colorString
29782
+ }).strict()]);
29783
+ const BoardStyleSchema = object({
29784
+ fill: BoardColorSchema.optional(),
29785
+ fillOpacity: ratio.optional(),
29786
+ stroke: BoardColorSchema.optional(),
29787
+ strokeWidth: finite.min(0).max(64).optional(),
29788
+ dash: _enum([
29789
+ "solid",
29790
+ "dashed",
29791
+ "dotted"
29792
+ ]).optional(),
29793
+ radius: nonNegative.optional(),
29794
+ trim: ratio.optional()
29795
+ }).strict();
29796
+ function isSafeBoardSourcePath(value) {
29797
+ return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !/^[a-z][a-z\d+.-]*:/i.test(value.trimStart()) && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
29798
+ }
29799
+ const srcSchema = string().refine(isSafeBoardSourcePath, "src must be a relative Space file path");
29800
+ function parseBoardMediaSource(src) {
29801
+ if (isSafeBoardSourcePath(src)) return {
29802
+ kind: "file",
29803
+ value: src
29804
+ };
29805
+ return normalizeBoardRemoteUrl(src) ? {
29806
+ kind: "url",
29807
+ value: src
29808
+ } : null;
29809
+ }
29810
+ const BoardMediaSourceSchema = string().max(4096).refine((src) => parseBoardMediaSource(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a stable public HTTP(S) URL; relative Space file paths remain supported.");
29811
+ const BoardScriptSchema = object({
29812
+ src: srcSchema.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module"),
29813
+ params: record(string(), json()).default({}),
29814
+ targets: record(string().min(1).max(80), BoardIdSchema).default({}),
29815
+ seed: string().min(1).max(160).optional(),
29816
+ enabled: boolean().default(true)
29817
+ }).strict();
29818
+ const itemEnvelope = {
29819
+ script: BoardScriptSchema.optional(),
29820
+ parent: BoardIdSchema.optional(),
29821
+ z: finite.optional(),
29822
+ position: BoardVec2Schema.default({
29823
+ x: 0,
29824
+ y: 0
29825
+ }),
29826
+ rotation: finite.default(0),
29827
+ scale: union([finite, BoardVec2Schema]).default(1),
29828
+ origin: BoardVec2Schema.default({
29829
+ x: .5,
29830
+ y: .5
29831
+ }),
29832
+ opacity: ratio.default(1),
29833
+ style: BoardStyleSchema.default({}),
29834
+ locked: boolean().optional(),
29835
+ metadata: record(string(), unknown()).optional()
29836
+ };
29837
+ const boxed = (width, height) => ({
29838
+ ...itemEnvelope,
29839
+ size: BoardSizeSchema.default({
29840
+ width,
29841
+ height
29842
+ })
29843
+ });
29844
+ const unsized = { ...itemEnvelope };
29845
+ const fontSize = finite.min(2).max(512);
29846
+ const BOARD_TEXT_ALIGNS = [
29847
+ "left",
29848
+ "center",
29849
+ "right"
29850
+ ];
29851
+ const BOARD_SHAPE_GEOMETRIES = [
29852
+ "rectangle",
29853
+ "rounded",
29854
+ "ellipse",
29855
+ "diamond",
29856
+ "triangle",
29857
+ "path"
29858
+ ];
29859
+ object({
29860
+ ...boxed(480, 320),
29861
+ type: literal("frame"),
29862
+ props: object({
29863
+ label: string().max(280).default(""),
29864
+ decoration: _enum(["frame", "none"]).optional(),
29865
+ clip: boolean().default(true)
29866
+ }).strict().prefault({})
29867
+ }).strict();
29868
+ object({
29869
+ ...unsized,
29870
+ type: literal("text"),
29871
+ props: object({
29872
+ text: string().default(""),
29873
+ fontSize: fontSize.default(24),
29874
+ fontWeight: finite.min(100).max(900).default(500),
29875
+ font: string().min(1).max(120).default("sans"),
29876
+ align: _enum(BOARD_TEXT_ALIGNS).default("left"),
29877
+ lineHeight: finite.min(.5).max(4).default(4 / 3),
29878
+ width: finite.positive().optional(),
29879
+ reveal: ratio.default(1),
29880
+ caret: boolean().default(false)
29881
+ }).strict().prefault({})
29882
+ }).strict();
29883
+ object({
29884
+ ...boxed(240, 160),
29885
+ type: literal("shape"),
29886
+ props: object({
29887
+ geometry: _enum(BOARD_SHAPE_GEOMETRIES).default("rectangle"),
29888
+ path: string().min(1).max(2e5).optional(),
29889
+ text: string().default(""),
29890
+ fontSize: fontSize.default(20),
29891
+ align: _enum(BOARD_TEXT_ALIGNS).default("center")
29892
+ }).strict().refine((props) => props.geometry !== "path" || Boolean(props.path), {
29893
+ message: "path geometry requires props.path",
29894
+ path: ["path"]
29895
+ }).prefault({})
29896
+ }).strict();
29897
+ const BoardDrawPointSchema = object({
29898
+ x: finite,
29899
+ y: finite,
29900
+ p: ratio.default(.5)
29901
+ }).strict();
29902
+ object({
29903
+ ...unsized,
29904
+ type: literal("draw"),
29905
+ props: object({ points: array(BoardDrawPointSchema).min(1).max(1e5) }).strict()
29906
+ }).strict();
29907
+ const BoardArrowEndSchema = union([BoardVec2Schema, object({
29908
+ item: BoardIdSchema,
29909
+ anchor: union([
29910
+ literal("auto"),
29911
+ object({
29912
+ side: _enum([
29913
+ "top",
29914
+ "right",
29915
+ "bottom",
29916
+ "left"
29917
+ ]),
29918
+ offset: ratio.default(.5)
29919
+ }).strict(),
29920
+ object({
29921
+ x: ratio,
29922
+ y: ratio
29923
+ }).strict()
29924
+ ]).default("auto"),
29925
+ port: string().min(1).max(120).optional()
29926
+ }).strict()]);
29927
+ const BOARD_ARROW_ROUTES = [
29928
+ "straight",
29929
+ "curve",
29930
+ "orthogonal"
29931
+ ];
29932
+ object({
29933
+ ...unsized,
29934
+ type: literal("arrow"),
29935
+ props: object({
29936
+ start: BoardArrowEndSchema,
29937
+ end: BoardArrowEndSchema,
29938
+ route: _enum(BOARD_ARROW_ROUTES).default("straight"),
29939
+ bend: finite.min(-.85).max(.85).default(0),
29940
+ waypoints: array(BoardVec2Schema).max(64).default([]),
29941
+ arrowStart: boolean().default(false),
29942
+ arrowEnd: boolean().default(true),
29943
+ label: string().max(280).default(""),
29944
+ fontSize: fontSize.default(14),
29945
+ relation: string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional()
29946
+ }).strict()
29947
+ }).strict();
29948
+ const fileProps = {
29949
+ src: BoardMediaSourceSchema,
29950
+ snapshot: object({
29951
+ title: string().optional(),
29952
+ mimeType: string().optional(),
29953
+ size: nonNegative.optional(),
29954
+ mtimeMs: nonNegative.optional(),
29955
+ naturalWidth: finite.positive().optional(),
29956
+ naturalHeight: finite.positive().optional(),
29957
+ durationMs: nonNegative.optional(),
29958
+ excerpt: string().optional(),
29959
+ coverPath: string().optional(),
29960
+ coverUrl: string().optional()
29961
+ }).strict().optional()
29962
+ };
29963
+ const timedFileProps = {
29964
+ ...fileProps,
29965
+ time: nonNegative.optional()
29966
+ };
29967
+ const BoardCropSchema = object({
29968
+ x: ratio,
29969
+ y: ratio,
29970
+ w: ratio,
29971
+ h: ratio
29972
+ }).strict();
29973
+ object({
29974
+ ...boxed(640, 360),
29975
+ type: literal("image"),
29976
+ props: object({
29977
+ ...fileProps,
29978
+ crop: BoardCropSchema.optional()
29979
+ }).strict()
29980
+ }).strict();
29981
+ object({
29982
+ ...boxed(640, 360),
29983
+ type: literal("video"),
29984
+ props: object(timedFileProps).strict()
29985
+ }).strict();
29986
+ object({
29987
+ ...boxed(480, 96),
29988
+ type: literal("audio"),
29989
+ props: object(timedFileProps).strict()
29990
+ }).strict();
29991
+ object({
29992
+ ...boxed(360, 220),
29993
+ type: literal("file"),
29994
+ props: object(fileProps).strict()
29995
+ }).strict();
29996
+ const httpsUrl = string().max(4096).url();
29997
+ const artifactBase = {
29998
+ id: string().min(1).max(240),
29999
+ title: string().max(240).optional(),
30000
+ url: httpsUrl,
30001
+ mimeType: string().max(160).optional()
30002
+ };
30003
+ const BoardTaskArtifactSchema = discriminatedUnion("type", [
30004
+ object({
30005
+ ...artifactBase,
30006
+ type: literal("image"),
30007
+ naturalWidth: finite.positive().optional(),
30008
+ naturalHeight: finite.positive().optional()
30009
+ }).strict(),
30010
+ object({
30011
+ ...artifactBase,
30012
+ type: literal("video"),
30013
+ previewUrl: httpsUrl.optional(),
30014
+ durationMs: number$1().int().positive().optional(),
30015
+ naturalWidth: finite.positive().optional(),
30016
+ naturalHeight: finite.positive().optional()
30017
+ }).strict(),
30018
+ object({
30019
+ ...artifactBase,
30020
+ type: literal("audio"),
30021
+ previewUrl: httpsUrl.optional(),
30022
+ durationMs: number$1().int().positive().optional()
30023
+ }).strict(),
30024
+ object({
30025
+ id: string().min(1).max(240),
30026
+ type: literal("text"),
30027
+ title: string().max(240).optional(),
30028
+ textExcerpt: string().min(1).max(480)
30029
+ }).strict()
30030
+ ]);
30031
+ const BoardTaskSnapshotSchema = object({
30032
+ taskType: string().min(1).max(120),
30033
+ status: _enum([
30034
+ "pending",
30035
+ "running",
30036
+ "completed",
30037
+ "failed"
30038
+ ]),
30039
+ title: string().min(1).max(240),
30040
+ model: string().max(160).optional(),
30041
+ promptExcerpt: string().max(480).optional(),
30042
+ artifactCount: number$1().int().nonnegative(),
30043
+ artifacts: array(BoardTaskArtifactSchema).max(6).default([]),
30044
+ updatedAt: string().optional()
30045
+ }).strict();
30046
+ object({
30047
+ ...boxed(420, 240),
30048
+ type: literal("task"),
30049
+ props: object({
30050
+ taskRunId: string().min(1).max(160),
30051
+ snapshot: BoardTaskSnapshotSchema
30052
+ }).strict()
30053
+ }).strict();
30054
+ const BOARD_EFFECT_KINDS = [
30055
+ "particles",
30056
+ "trail",
30057
+ "impact",
30058
+ "flash",
30059
+ "glow"
30060
+ ];
30061
+ object({
30062
+ ...boxed(240, 240),
30063
+ type: literal("effect"),
30064
+ props: object({
30065
+ kind: _enum(BOARD_EFFECT_KINDS),
30066
+ rate: nonNegative.max(2e4).default(40),
30067
+ life: finite.positive().max(6e4).default(1200),
30068
+ particleSize: finite.positive().max(512).default(6),
30069
+ speed: nonNegative.max(1e4).default(120),
30070
+ direction: finite.default(-90),
30071
+ spread: finite.min(0).max(360).default(360),
30072
+ gravity: finite.default(0),
30073
+ intensity: nonNegative.max(10).default(1),
30074
+ follow: BoardIdSchema.optional(),
30075
+ seed: string().min(1).max(160).optional()
30076
+ }).strict()
30077
+ }).strict();
30078
+ object({
30079
+ ...boxed(480, 320),
30080
+ type: literal("custom"),
30081
+ script: BoardScriptSchema,
30082
+ props: object({}).strict().default({})
30083
+ }).strict();
30084
+ const BOARD_EXTENSION_TYPE_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z0-9-]+)+$/;
30085
+ object({
30086
+ ...itemEnvelope,
30087
+ size: BoardSizeSchema.default({
30088
+ width: 240,
30089
+ height: 160
30090
+ }),
30091
+ type: string().regex(BOARD_EXTENSION_TYPE_PATTERN),
30092
+ props: record(string(), unknown()).default({})
30093
+ }).strict();
30094
+ const CSS_EASE_KEYWORDS = /* @__PURE__ */ new Set([
30095
+ "linear",
30096
+ "ease",
30097
+ "ease-in",
30098
+ "ease-out",
30099
+ "ease-in-out",
30100
+ "step-start",
30101
+ "step-end"
30102
+ ]);
30103
+ function isBoardEase(value) {
30104
+ const ease = value.trim();
30105
+ if (CSS_EASE_KEYWORDS.has(ease)) return true;
30106
+ const bezier = /^cubic-bezier\(\s*([^,()]+),\s*([^,()]+),\s*([^,()]+),\s*([^,()]+)\)$/.exec(ease);
30107
+ if (bezier) {
30108
+ const [x1, , x2] = bezier.slice(1).map(Number);
30109
+ return bezier.slice(1).every((part) => Number.isFinite(Number(part))) && x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1;
30110
+ }
30111
+ return /^steps\(\s*[1-9]\d*\s*(?:,\s*(?:start|end|jump-start|jump-end|jump-none|jump-both)\s*)?\)$/.test(ease);
30112
+ }
30113
+ const BoardEaseSchema = string().max(120).refine(isBoardEase, "expected a CSS easing: linear, ease-in-out, cubic-bezier(…) or steps(…)");
30114
+ const BoardKeyframeSchema = object({
30115
+ at: nonNegative,
30116
+ value: unknown(),
30117
+ ease: BoardEaseSchema.optional()
30118
+ }).strict();
30119
+ const BoardTrackSchema = object({
30120
+ target: string().min(1).max(160),
30121
+ property: string().min(1).max(200),
30122
+ keyframes: array(BoardKeyframeSchema).min(1).max(1e5),
30123
+ composite: _enum(["replace", "add"]).default("replace"),
30124
+ interpolation: _enum([
30125
+ "auto",
30126
+ "step",
30127
+ "spline"
30128
+ ]).default("auto"),
30129
+ orient: boolean().optional()
30130
+ }).strict().superRefine((track, context) => {
30131
+ for (let index = 1; index < track.keyframes.length; index += 1) if ((track.keyframes[index]?.at ?? 0) < (track.keyframes[index - 1]?.at ?? 0)) context.addIssue({
30132
+ code: "custom",
30133
+ message: "keyframes must be ordered by at",
30134
+ path: [
30135
+ "keyframes",
30136
+ index,
30137
+ "at"
30138
+ ]
30139
+ });
30140
+ });
30141
+ const BoardMarkerSchema = object({
30142
+ at: nonNegative,
30143
+ label: string().max(280).optional(),
30144
+ pause: boolean().optional()
30145
+ }).strict();
30146
+ object({
30147
+ name: string().max(280).optional(),
30148
+ duration: finite.positive().max(864e5),
30149
+ play: _enum([
30150
+ "manual",
30151
+ "auto",
30152
+ "always"
30153
+ ]).default("manual"),
30154
+ delay: nonNegative.default(0),
30155
+ loop: boolean().default(false),
30156
+ end: _enum(["hold", "reset"]).default("hold"),
30157
+ markers: array(BoardMarkerSchema).max(1e4).default([])
30158
+ }).strict().extend({ tracks: record(BoardKeySchema, BoardTrackSchema).default({}) }).strict();
30159
+ const BOARD_ENTER_PRESETS = [
30160
+ "fade-in",
30161
+ "rise",
30162
+ "drop",
30163
+ "pop",
30164
+ "deal"
30165
+ ];
30166
+ const BoardBackgroundSchema = object({
30167
+ kind: _enum([
30168
+ "solid",
30169
+ "dots",
30170
+ "grid",
30171
+ "image"
30172
+ ]).default("dots"),
30173
+ color: BoardColorSchema.optional(),
30174
+ imageUrl: string().url().max(4096).optional(),
30175
+ fit: _enum([
30176
+ "cover",
30177
+ "contain",
30178
+ "repeat"
30179
+ ]).optional(),
30180
+ opacity: ratio.optional()
30181
+ }).strict();
30182
+ object({
30183
+ title: string().min(1).max(255).optional(),
30184
+ background: BoardBackgroundSchema.default({ kind: "dots" }),
30185
+ grid: object({
30186
+ visible: boolean().default(false),
30187
+ size: finite.min(4).max(512).default(24)
30188
+ }).strict().optional(),
30189
+ enter: object({
30190
+ preset: _enum(BOARD_ENTER_PRESETS),
30191
+ duration: finite.positive().max(1e4).optional()
30192
+ }).strict().optional()
30193
+ }).strict();
30194
+ const patchObject = record(string(), unknown());
30195
+ object({
30196
+ board: patchObject.nullable().optional(),
30197
+ items: record(BoardIdSchema, patchObject.nullable()).optional(),
30198
+ animations: record(BoardIdSchema, patchObject.and(object({ tracks: record(BoardKeySchema, patchObject.nullable()).optional() })).nullable()).optional()
30199
+ }).strict();
30200
+ function createBoardScriptModuleReader(read, timeoutMs = 15e3) {
30201
+ const cache = /* @__PURE__ */ new Map();
30202
+ const queue = [];
30203
+ let active = 0;
30204
+ let disposed = false;
30205
+ function pump() {
30206
+ while (active < 4 && queue.length) queue.shift()?.();
30207
+ }
30208
+ return {
30209
+ read(path) {
30210
+ if (disposed) return Promise.reject(/* @__PURE__ */ new Error("Module reader is disposed."));
30211
+ const cached = cache.get(path);
30212
+ if (cached) return cached;
30213
+ const promise = new Promise((resolve, reject) => {
30214
+ queue.push(() => {
30215
+ if (disposed) {
30216
+ reject(/* @__PURE__ */ new Error("Module reader is disposed."));
30217
+ return;
30218
+ }
30219
+ active++;
30220
+ let timer;
30221
+ const timeout = new Promise((_resolve, rejectRead) => {
30222
+ timer = setTimeout(() => rejectRead(/* @__PURE__ */ new Error(`Module read timed out: ${path}`)), timeoutMs);
30223
+ });
30224
+ Promise.race([Promise.resolve().then(() => read(path)), timeout]).then(resolve, reject).finally(() => {
30225
+ clearTimeout(timer);
30226
+ active--;
30227
+ pump();
30228
+ });
30229
+ });
30230
+ pump();
30231
+ });
30232
+ cache.set(path, promise);
30233
+ promise.catch(() => {
30234
+ if (cache.get(path) === promise) cache.delete(path);
30235
+ });
30236
+ return promise;
30237
+ },
30238
+ invalidate(path) {
30239
+ if (path) cache.delete(path);
30240
+ else cache.clear();
30241
+ },
30242
+ destroy() {
30243
+ disposed = true;
30244
+ cache.clear();
30245
+ for (const job of queue.splice(0)) job();
30246
+ }
30247
+ };
30248
+ }
30249
+ function resolveScriptImport(from, specifier) {
30250
+ if (!specifier.startsWith("./") && !specifier.startsWith("../")) throw new Error(`Only relative module imports are supported: ${specifier}`);
30251
+ const parts = from.split("/").slice(0, -1);
30252
+ for (const part of specifier.split("/")) {
30253
+ if (part === ".") continue;
30254
+ if (part === "..") {
30255
+ if (!parts.length) throw new Error("Module import leaves the Space.");
30256
+ parts.pop();
30257
+ } else parts.push(part);
30258
+ }
30259
+ const path = parts.join("/");
30260
+ if (!isSafeBoardSourcePath(path) || !/\.m?js$/.test(path)) throw new Error(`Invalid module path: ${path}`);
30261
+ return path;
30262
+ }
30263
+ async function loadBoardScriptSources(src, read, timeoutMs = 15e3) {
30264
+ if (!isSafeBoardSourcePath(src) || !/\.m?js$/.test(src)) throw new Error("Invalid script entry path.");
30265
+ const { init, parse } = await import("./board-kernel/lexer.js");
30266
+ await init;
30267
+ const sources = {};
30268
+ const visiting = /* @__PURE__ */ new Set();
30269
+ let bytes = 0;
30270
+ async function visit(path) {
30271
+ if (visiting.has(path)) throw new Error(`Circular module dependency: ${path}`);
30272
+ if (sources[path]) return;
30273
+ if (Object.keys(sources).length + visiting.size >= 64) throw new Error("A script may load at most 64 modules.");
30274
+ visiting.add(path);
30275
+ let timer;
30276
+ const code = await Promise.race([Promise.resolve().then(() => read(path)), new Promise((_resolve, reject) => {
30277
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Module read timed out: ${path}`)), timeoutMs);
30278
+ })]).finally(() => clearTimeout(timer));
30279
+ bytes += new TextEncoder().encode(code).byteLength;
30280
+ if (bytes > 2e6) throw new Error("Script modules exceed the 2 MB limit.");
30281
+ const [imports] = parse(code, path);
30282
+ const resolved = [];
30283
+ for (const entry of imports) {
30284
+ if (entry.d === -2) throw new Error("import.meta is not available in Board scripts.");
30285
+ if (entry.n === void 0) throw new Error("Dynamic imports must use a literal relative path.");
30286
+ const dependency = resolveScriptImport(path, entry.n);
30287
+ await visit(dependency);
30288
+ resolved.push({
30289
+ start: entry.s,
30290
+ end: entry.e,
30291
+ path: dependency,
30292
+ dynamic: entry.d >= 0
30293
+ });
30294
+ }
30295
+ sources[path] = {
30296
+ code,
30297
+ imports: resolved
30298
+ };
30299
+ visiting.delete(path);
30300
+ }
30301
+ await visit(src);
30302
+ return sources;
30303
+ }
30304
+ //#endregion
30305
+ //#region ../sdk/src/board/render/script-runtime.ts
30306
+ /** Self-contained so browser workers and Node execute exactly the same kernel. */
30307
+ function createScriptRuntime(module, initial, host) {
30308
+ if ("mount" in module) throw new Error("Board scripts do not support DOM mount(). Use draw() or native scene items.");
30309
+ let input = initial;
30310
+ let disposed = false;
30311
+ let revision = 0;
30312
+ let randomState = 2166136261;
30313
+ for (const char of initial.item.script?.seed ?? initial.id) randomState = Math.imul(randomState ^ char.charCodeAt(0), 16777619) >>> 0;
30314
+ const parameterDefaults = Object.fromEntries(Object.entries(module.parameters ?? {}).filter(([, spec]) => spec.default !== void 0).map(([key, spec]) => [key, spec.default]));
30315
+ let params = {
30316
+ ...parameterDefaults,
30317
+ ...initial.item.script?.params
30318
+ };
30319
+ const state = structuredClone(initial.snapshot?.state ?? {});
30320
+ if (initial.snapshot) randomState = initial.snapshot.random;
30321
+ let time = initial.snapshot?.time ?? 0;
30322
+ let started = Boolean(initial.snapshot);
30323
+ const stepMs = 1e3 / 60;
30324
+ let frame = {
30325
+ t: time,
30326
+ dt: 0,
30327
+ width: initial.width,
30328
+ height: initial.height,
30329
+ seed: initial.item.script?.seed ?? initial.id,
30330
+ params,
30331
+ state
30332
+ };
30333
+ const handlers = /* @__PURE__ */ new Map();
30334
+ const frames = /* @__PURE__ */ new Set();
30335
+ const cleanups = [];
30336
+ const effects = structuredClone(initial.snapshot?.effects);
30337
+ let patches = effects?.patches ?? {};
30338
+ let items = effects?.items ?? {};
30339
+ let space = effects?.space ?? "world";
30340
+ let hits = effects?.hits ?? {};
30341
+ const invalidate = () => {
30342
+ if (disposed) return;
30343
+ revision++;
30344
+ host.invalidate();
30345
+ };
30346
+ const present = (id, patch) => {
30347
+ if (patch === null) delete patches[id];
30348
+ else patches[id] = structuredClone(patch);
30349
+ invalidate();
30350
+ };
30351
+ const context = {
30352
+ self: {
30353
+ id: initial.id,
30354
+ get base() {
30355
+ return input.item;
30356
+ },
30357
+ present: (patch) => present(initial.id, patch)
30358
+ },
30359
+ get params() {
30360
+ return params;
30361
+ },
30362
+ get targets() {
30363
+ return input.item.script?.targets ?? {};
30364
+ },
30365
+ async emit(target, type, data) {
30366
+ await host.call("board.emit", {
30367
+ target,
30368
+ type,
30369
+ data
30370
+ });
30371
+ },
30372
+ state,
30373
+ get frame() {
30374
+ return frame;
30375
+ },
30376
+ random() {
30377
+ randomState = Math.imul(randomState, 1664525) + 1013904223 >>> 0;
30378
+ return randomState / 4294967296;
30379
+ },
30380
+ on(type, handler) {
30381
+ let listeners = handlers.get(type);
30382
+ if (!listeners) {
30383
+ listeners = /* @__PURE__ */ new Set();
30384
+ handlers.set(type, listeners);
30385
+ }
30386
+ listeners.add(handler);
30387
+ invalidate();
30388
+ return () => {
30389
+ listeners.delete(handler);
30390
+ invalidate();
30391
+ };
30392
+ },
30393
+ onFrame(handler) {
30394
+ frames.add(handler);
30395
+ invalidate();
30396
+ return () => {
30397
+ frames.delete(handler);
30398
+ invalidate();
30399
+ };
30400
+ },
30401
+ onDispose: (handler) => cleanups.push(handler),
30402
+ invalidate,
30403
+ get: (id) => input.items[id],
30404
+ present,
30405
+ scene(next, options) {
30406
+ items = structuredClone(next);
30407
+ space = options?.space ?? "world";
30408
+ invalidate();
30409
+ },
30410
+ hitRegions(next) {
30411
+ hits = structuredClone(next);
30412
+ invalidate();
30413
+ },
30414
+ async commit(patch) {
30415
+ await host.call("board.commit", patch);
30416
+ },
30417
+ capabilities: {
30418
+ has: (name) => (input.capabilities ?? []).includes(name),
30419
+ call: (name, value) => host.call(name, value)
30420
+ }
30421
+ };
30422
+ const setup = module.default?.(context);
30423
+ if (typeof setup === "function") cleanups.push(setup);
30424
+ return {
30425
+ get animated() {
30426
+ return frames.size > 0 || module.animated === true;
30427
+ },
30428
+ update(next) {
30429
+ input = next;
30430
+ params = {
30431
+ ...parameterDefaults,
30432
+ ...next.item.script?.params
30433
+ };
30434
+ },
30435
+ step(t) {
30436
+ if (disposed) return;
30437
+ if (!Number.isFinite(t) || t < time - .001) throw new Error("Reset the instance before seeking backwards.");
30438
+ const update = (at, dt) => {
30439
+ frame = {
30440
+ ...frame,
30441
+ t: at,
30442
+ dt,
30443
+ width: input.width,
30444
+ height: input.height,
30445
+ params: context.params
30446
+ };
30447
+ for (const handler of frames) handler(frame);
30448
+ };
30449
+ if (!started) {
30450
+ started = true;
30451
+ update(time, 0);
30452
+ }
30453
+ if (frames.size) while (time + stepMs <= t + 1e-6) {
30454
+ time += stepMs;
30455
+ update(time, stepMs);
30456
+ }
30457
+ else time = t;
30458
+ frame = {
30459
+ ...frame,
30460
+ t,
30461
+ dt: 0,
30462
+ width: input.width,
30463
+ height: input.height,
30464
+ params: context.params
30465
+ };
30466
+ },
30467
+ event(event) {
30468
+ if (disposed) return;
30469
+ const listeners = handlers.get(event.type);
30470
+ if (!listeners?.size) return;
30471
+ for (const handler of listeners) handler(event);
30472
+ invalidate();
30473
+ },
30474
+ draw(canvas) {
30475
+ const result = module.draw?.(canvas, frame);
30476
+ if (result && typeof result.then === "function") throw new Error("draw() must be synchronous.");
30477
+ },
30478
+ output() {
30479
+ return {
30480
+ parameters: module.parameters,
30481
+ space,
30482
+ patches,
30483
+ items,
30484
+ hits,
30485
+ listens: [...handlers].filter(([, callbacks]) => callbacks.size).map(([type]) => type),
30486
+ animated: this.animated,
30487
+ updating: frames.size > 0,
30488
+ version: revision
30489
+ };
30490
+ },
30491
+ snapshot(includeEffects = true, clone = true) {
30492
+ const snapshot = {
30493
+ state,
30494
+ time,
30495
+ random: randomState,
30496
+ effects: includeEffects ? {
30497
+ patches,
30498
+ items,
30499
+ hits,
30500
+ space
30501
+ } : void 0
30502
+ };
30503
+ return clone ? structuredClone(snapshot) : snapshot;
30504
+ },
30505
+ destroy() {
30506
+ disposed = true;
30507
+ for (const cleanup of cleanups.reverse()) cleanup();
30508
+ handlers.clear();
30509
+ frames.clear();
30510
+ patches = {};
30511
+ items = {};
30512
+ hits = {};
30513
+ }
30514
+ };
30515
+ }
30516
+ //#endregion
30517
+ //#region ../sdk/src/board/export/scene.ts
30518
+ function buildContext(input) {
30519
+ const missing = /* @__PURE__ */ new Set();
30520
+ const textures = input.textures ?? /* @__PURE__ */ new Map();
30521
+ return {
30522
+ context: {
30523
+ settings: input.settings,
30524
+ scene: input.scene,
30525
+ time: input.time ?? 0,
30526
+ frameAt: input.frameAt,
30527
+ selectedIds: /* @__PURE__ */ new Set(),
30528
+ hoveredId: null,
30529
+ resizingIds: /* @__PURE__ */ new Set(),
30530
+ palette: {
30531
+ ...defaultBoardPalette(input.colorScheme),
30532
+ ...input.palette
30533
+ },
30534
+ colors: input.colors ?? buildFallbackShapeColors(input.colorScheme),
30535
+ colorScheme: input.colorScheme,
30536
+ rendererType: "canvas",
30537
+ zoom: input.scale,
30538
+ scripts: input.scripts,
30539
+ assetKey: input.assetKey ?? imageAssetKey,
30540
+ getTexture: (key) => textures.get(key) ?? null,
30541
+ hasError: (key) => {
30542
+ if (!textures.has(key)) missing.add(key);
30543
+ return false;
30544
+ },
30545
+ fileState: () => "ok",
30546
+ acquireTexture: () => {},
30547
+ releaseTexture: () => {}
30548
+ },
30549
+ missing
30550
+ };
30551
+ }
30552
+ function createBoardExportScene(input) {
30553
+ const { context, missing } = buildContext(input);
30554
+ const root = new Container({ label: "board-export-root" });
30555
+ const outputWidth = input.world.width * input.scale;
30556
+ const outputHeight = input.world.height * input.scale;
30557
+ if (input.background != null) root.addChild(new Graphics().rect(0, 0, outputWidth, outputHeight).fill({
30558
+ color: input.background,
30559
+ alpha: 1
30560
+ }));
30561
+ if (input.backgroundImage) {
30562
+ const { texture, fit, position, opacity } = input.backgroundImage;
30563
+ if (fit === "repeat") root.addChild(new TilingSprite({
30564
+ texture,
30565
+ width: outputWidth,
30566
+ height: outputHeight,
30567
+ alpha: opacity
30568
+ }));
30569
+ else {
30570
+ const sprite = new Sprite({
30571
+ texture,
30572
+ alpha: opacity
30573
+ });
30574
+ const scale = fit === "cover" ? Math.max(outputWidth / texture.width, outputHeight / texture.height) : Math.min(outputWidth / texture.width, outputHeight / texture.height);
30575
+ sprite.width = texture.width * scale;
30576
+ sprite.height = texture.height * scale;
30577
+ const x = position === "left" ? 0 : position === "right" ? outputWidth - sprite.width : (outputWidth - sprite.width) / 2;
30578
+ const y = position === "top" ? 0 : position === "bottom" ? outputHeight - sprite.height : (outputHeight - sprite.height) / 2;
30579
+ sprite.position.set(x, y);
30580
+ root.addChild(sprite);
30581
+ }
30582
+ }
30583
+ const world = new Container({
30584
+ isRenderGroup: true,
30585
+ label: "board-export-world"
30586
+ });
30587
+ world.scale.set(input.scale);
30588
+ world.position.set(-input.world.x * input.scale, -input.world.y * input.scale);
30589
+ const resolve = input.renderers ?? getBoardCardRenderer;
30590
+ const groups = /* @__PURE__ */ new Map();
30591
+ function hostFor(item) {
30592
+ const clip = clippingAncestor(input.scene, item);
30593
+ const ancestor = clip ? input.scene.get(clip) : void 0;
30594
+ if (!ancestor) return world;
30595
+ let group = groups.get(ancestor.id);
30596
+ if (!group) {
30597
+ group = syncClipGroup(void 0, ancestor).group;
30598
+ hostFor(ancestor).addChild(group);
30599
+ groups.set(ancestor.id, group);
30600
+ }
30601
+ return group;
30602
+ }
30603
+ for (const item of input.items) {
30604
+ const opacity = input.scene.opacity(item.id);
30605
+ if (opacity <= 0) continue;
30606
+ const host = hostFor(item);
30607
+ const container = resolve(item, context).create(item, context);
30608
+ container.alpha = opacity;
30609
+ host.addChild(container);
30610
+ if (isClippingFrame(item) && !groups.has(item.id)) {
30611
+ const { group } = syncClipGroup(void 0, item);
30612
+ host.addChild(group);
30613
+ groups.set(item.id, group);
30614
+ }
30615
+ }
30616
+ root.addChild(world);
30617
+ return {
30618
+ root,
30619
+ missingImageKeys: [...missing],
30620
+ destroy: () => root.destroy({ children: true })
30621
+ };
30622
+ }
30623
+ //#endregion
30624
+ //#region ../sdk/src/board/export/index.ts
28659
30625
  function resolveBackground(background, palette, document, colorScheme) {
28660
30626
  if (background === "transparent") return null;
28661
30627
  if (typeof background === "number") return background;
@@ -28663,20 +30629,50 @@ function resolveBackground(background, palette, document, colorScheme) {
28663
30629
  const css = typeof declared === "object" ? declared[colorScheme] : declared;
28664
30630
  return css ? parseBoardCssColor(css) ?? palette.bg : palette.bg;
28665
30631
  }
28666
- function renderBoardExport(renderer, input, options = {}) {
30632
+ function prepareBoardExport(input, options = {}) {
28667
30633
  ensureBoardTextMeasurement();
28668
- const base = normalizeBoardDocument(input);
28669
- const document = options.at ? boardDocumentAt(base, options.at.animation ?? null, options.at.time).document : base;
28670
- const scene = buildBoardScene(document);
30634
+ const base = options.sampler ? input : normalizeBoardDocument(input);
30635
+ if (options.camera && options.region && options.region.kind !== "all") throw new Error("Camera export cannot be combined with a fixed region.");
30636
+ if (options.camera && !options.at) throw new Error("Camera export needs an animation time.");
30637
+ if (options.camera && ![options.camera.width, options.camera.height].every((n) => Number.isSafeInteger(n) && n > 0)) throw new Error("Camera dimensions must be positive integers.");
30638
+ const sampler = options.at ? options.sampler ?? createBoardRenderSampler(base, options.at.animation ?? null, options) : void 0;
30639
+ const sample = options.at ? sampler?.at(options.at.time) : void 0;
30640
+ const sampled = sample?.document ?? base;
30641
+ const document = options.scripts?.document?.(sampled, options.at?.time ?? 0) ?? sampled;
30642
+ const scene = document === sampled ? sample?.scene ?? buildBoardScene(document) : buildBoardScene(document);
30643
+ const camera = options.camera ? resolveBoardAnimationCamera(sample?.evaluation.camera ?? {}, (id) => scene.get(id)?.frame, options.camera, options.at?.time ?? 0) : void 0;
28671
30644
  const plan = planBoardExport({
28672
30645
  scene,
28673
- region: options.region ?? { kind: "all" },
28674
- scale: options.scale,
28675
- padding: options.padding,
30646
+ region: camera && options.camera ? {
30647
+ kind: "rect",
30648
+ rect: {
30649
+ x: -camera.x / camera.zoom,
30650
+ y: -camera.y / camera.zoom,
30651
+ width: options.camera.width / camera.zoom,
30652
+ height: options.camera.height / camera.zoom
30653
+ }
30654
+ } : options.region ?? { kind: "all" },
30655
+ scale: camera?.zoom ?? options.scale,
30656
+ padding: camera ? 0 : options.padding,
28676
30657
  maxEdge: options.maxEdge,
28677
30658
  maxPixels: options.maxPixels
28678
30659
  });
28679
- if (!plan) return null;
30660
+ if (plan && camera && options.camera) {
30661
+ const ratio = plan.clamped ? plan.scale / camera.zoom : 1;
30662
+ plan.width = Math.max(1, Math.floor(options.camera.width * ratio));
30663
+ plan.height = Math.max(1, Math.floor(options.camera.height * ratio));
30664
+ }
30665
+ return plan ? {
30666
+ document,
30667
+ scene,
30668
+ plan,
30669
+ sampler
30670
+ } : null;
30671
+ }
30672
+ function renderBoardExport(renderer, input, options = {}) {
30673
+ const prepared = prepareBoardExport(input, options);
30674
+ if (!prepared) return null;
30675
+ const { document, scene, plan, sampler } = prepared;
28680
30676
  const colorScheme = options.colorScheme ?? "dark";
28681
30677
  const palette = {
28682
30678
  ...defaultBoardPalette(colorScheme),
@@ -28687,13 +30683,14 @@ function renderBoardExport(renderer, input, options = {}) {
28687
30683
  scene,
28688
30684
  items: plan.items,
28689
30685
  time: options.at?.time ?? 0,
30686
+ frameAt: sampler?.frameAt,
28690
30687
  world: plan.world,
28691
30688
  scale: plan.scale,
28692
30689
  colorScheme,
28693
30690
  palette,
28694
30691
  colors: options.colors,
28695
30692
  textures: options.textures,
28696
- sketches: options.sketches,
30693
+ scripts: options.scripts,
28697
30694
  assetKey: options.assetKey,
28698
30695
  renderers: options.renderers,
28699
30696
  background: resolveBackground(options.background, palette, document, colorScheme),
@@ -28731,145 +30728,253 @@ function renderBoardExport(renderer, input, options = {}) {
28731
30728
  }
28732
30729
  }
28733
30730
  //#endregion
28734
- //#region ../sdk/src/board/export/node/sketch.ts
28735
- const MAX_EDGE = 2048;
28736
- const DEFAULT_TIMEOUT_MS = 2e3;
28737
- const DEFAULT_READ_TIMEOUT_MS = 15e3;
28738
- const MAX_PREPARE_CONCURRENCY = 4;
28739
- function transformModule(code, src) {
28740
- return `(function(module, exports) {\n${code.replace(/export\s+default\s+/g, "module.exports.default = ").replace(/export\s+(async\s+)?function\s+draw\b/g, (_match, asyncKeyword = "") => `module.exports.draw = ${asyncKeyword}function draw`).replace(/export\s+(const|let|var)\s+draw\s*=/g, "module.exports.draw =")}\n})(module, module.exports);\n//# sourceURL=${JSON.stringify(src)}`;
28741
- }
28742
- function runModule(code, src, timeoutMs) {
28743
- const module = { exports: {} };
28744
- const context = createContext({
28745
- module,
28746
- exports: module.exports,
28747
- console: {
28748
- log() {},
28749
- warn() {},
28750
- error() {}
28751
- },
28752
- fetch: void 0,
28753
- XMLHttpRequest: void 0,
28754
- WebSocket: void 0,
28755
- EventSource: void 0,
28756
- importScripts: void 0,
28757
- indexedDB: void 0,
28758
- caches: void 0,
28759
- process: void 0,
28760
- require: void 0
28761
- });
28762
- new Script(transformModule(code, src), { filename: src }).runInContext(context, { timeout: timeoutMs });
28763
- const value = module.exports.default && typeof module.exports.default === "object" ? {
28764
- ...module.exports.default,
28765
- ...module.exports
28766
- } : module.exports;
28767
- if (typeof value.draw !== "function") throw new Error(`${src} does not export draw(ctx, frame)`);
28768
- if (value.draw.constructor?.name === "AsyncFunction") throw new Error(`${src} must export a synchronous draw(ctx, frame) for Node export.`);
28769
- return {
28770
- module: value,
28771
- context
28772
- };
28773
- }
28774
- function withTimeout(promise, timeoutMs, src) {
28775
- return new Promise((resolve, reject) => {
28776
- const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${src} exceeded the ${timeoutMs}ms sketch timeout.`)), timeoutMs);
28777
- promise.then(resolve, reject).finally(() => clearTimeout(timer));
28778
- });
28779
- }
28780
- function frameKey(item, time, scale) {
28781
- return `${item.id}|${time}|${item.frame.width}|${item.frame.height}|${scale}|${item.props.seed ?? item.id}|${JSON.stringify(item.props.params)}`;
28782
- }
28783
- function frameScale(resolution, width, height) {
28784
- return Math.min(4, Math.max(.25, resolution), MAX_EDGE / Math.max(1, width), MAX_EDGE / Math.max(1, height));
28785
- }
28786
- function textureFromCanvas(canvas) {
28787
- return new Texture({ source: new ImageSource({
28788
- resource: canvas,
28789
- width: canvas.width,
28790
- height: canvas.height
28791
- }) });
28792
- }
28793
- function createNodeBoardSketchHost(renderer, options) {
30731
+ //#region ../sdk/src/board/export/node/scripts.ts
30732
+ const WORKER = `
30733
+ const {parentPort, workerData} = require('node:worker_threads');
30734
+ const {createContext, Script, SourceTextModule} = require('node:vm');
30735
+ const {createCanvas} = require(workerData.canvasModule);
30736
+ const instances = new Map();
30737
+ const graphs = new Map();
30738
+ const update = new Script('__runtime.update(JSON.parse(__inputJson)); __runtime.step(__time)');
30739
+ const draw = new Script('__runtime.draw(__drawing)');
30740
+ const output = new Script('JSON.stringify(__runtime.output())');
30741
+ const dispose = new Script('__runtime.destroy()');
30742
+ function release(id) {
30743
+ const entry=instances.get(id);instances.delete(id);
30744
+ if(entry) {try {dispose.runInContext(entry.context,{timeout:workerData.timeout});} catch {}}
30745
+ }
30746
+ async function create(data) {
30747
+ const context = createContext({console:{log(){},warn(){},error(){}},structuredClone},{codeGeneration:{strings:false,wasm:false}});
30748
+ new Script("const __name=(value,name)=>Object.defineProperty(value,'name',{value:name,configurable:true})").runInContext(context);
30749
+ const sources = graphs.get(data.src);
30750
+ if(!sources) throw new Error('Module graph is unavailable.');
30751
+ const modules = new Map();
30752
+ const resolve = (from,name) => {
30753
+ const parts=from.split('/').slice(0,-1);
30754
+ for(const part of name.split('/')) { if(part==='..') parts.pop(); else if(part!=='.') parts.push(part); }
30755
+ return parts.join('/');
30756
+ };
30757
+ const link = (name,ref) => load(resolve(ref.identifier,name));
30758
+ function load(path) {
30759
+ if(modules.has(path)) return modules.get(path);
30760
+ const source=sources[path];
30761
+ if(!source) throw new Error('Module is unavailable: '+path);
30762
+ const module=new SourceTextModule(source.code,{context,identifier:path,importModuleDynamically:async name=>{
30763
+ const child=load(resolve(path,name));
30764
+ if(child.status==='unlinked') await child.link(link);
30765
+ if(child.status==='linked') await child.evaluate({timeout:workerData.timeout});
30766
+ return child;
30767
+ }});
30768
+ modules.set(path,module);return module;
30769
+ }
30770
+ const root=load(data.src);
30771
+ await root.link(link);await root.evaluate({timeout:workerData.timeout});
30772
+ context.__module=root.namespace;
30773
+ context.__inputJson=JSON.stringify(data.input);
30774
+ new Script('globalThis.__runtime=('+workerData.runtime+')(__module,JSON.parse(__inputJson),{call:async()=>{throw new Error("Capabilities are unavailable during export.")},invalidate(){}})').runInContext(context,{timeout:workerData.timeout});
30775
+ return {context,signature:data.signature,time:-1,draw:!!root.namespace.draw,canvas:null};
30776
+ }
30777
+ parentPort.on('message',async data=>{
30778
+ try {
30779
+ if(data.release) {for(const id of data.release) release(id);return;}
30780
+ if(data.sources) graphs.set(data.src,data.sources);
30781
+ let entry=instances.get(data.input.id);
30782
+ if(!entry || entry.signature!==data.signature || data.time<entry.time) {
30783
+ release(data.input.id);
30784
+ entry=await create(data);instances.set(data.input.id,entry);
30785
+ }
30786
+ const context=entry.context;
30787
+ context.__inputJson=JSON.stringify(data.input);context.__time=data.time;
30788
+ update.runInContext(context,{timeout:workerData.timeout});entry.time=data.time;
30789
+ let png;
30790
+ if(entry.draw) {
30791
+ const width=Math.max(1,Math.round(data.input.width*data.scale));
30792
+ const height=Math.max(1,Math.round(data.input.height*data.scale));
30793
+ const canvas=entry.canvas??=createCanvas(width,height);
30794
+ canvas.width=width;canvas.height=height;
30795
+ const drawing=canvas.getContext('2d');drawing.scale(data.scale,data.scale);
30796
+ context.__drawing=drawing;draw.runInContext(context,{timeout:workerData.timeout});
30797
+ png=canvas.toBuffer('image/png');delete context.__drawing;
30798
+ }
30799
+ parentPort.postMessage({png,output:JSON.parse(output.runInContext(context,{timeout:workerData.timeout}))});
30800
+ } catch(error) { instances.delete(data.input?.id);parentPort.postMessage({error:String(error.stack||error)}); }
30801
+ });
30802
+ `;
30803
+ function createNodeBoardScriptHost(renderer, options) {
28794
30804
  const textures = /* @__PURE__ */ new Map();
30805
+ const outputs = /* @__PURE__ */ new Map();
28795
30806
  const errors = /* @__PURE__ */ new Map();
28796
- const lastErrors = /* @__PURE__ */ new Map();
28797
- const modules = /* @__PURE__ */ new Map();
28798
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
28799
- const readTimeoutMs = options.readTimeoutMs ?? DEFAULT_READ_TIMEOUT_MS;
28800
- const loadModule = (src) => {
28801
- let pending = modules.get(src);
28802
- if (!pending) {
28803
- pending = withTimeout(options.readModule(src), readTimeoutMs, src).then((code) => runModule(code, src, timeoutMs));
28804
- modules.set(src, pending);
28805
- pending.catch(() => modules.delete(src));
28806
- }
28807
- return pending;
28808
- };
28809
- const render = async (item, time, scale) => {
28810
- const key = frameKey(item, time, scale);
28811
- if (textures.has(key) || errors.has(key)) return;
28812
- try {
28813
- const runtime = await loadModule(item.props.src);
28814
- const width = Math.max(1, Math.round(item.frame.width * scale));
28815
- const height = Math.max(1, Math.round(item.frame.height * scale));
28816
- const canvas = renderer.canvasModule.createCanvas(width, height);
28817
- const context = canvas.getContext("2d");
28818
- context.scale(scale, scale);
28819
- const frame = {
28820
- ...item.props,
28821
- t: time,
28822
- width: item.frame.width,
28823
- height: item.frame.height,
28824
- seed: item.props.seed ?? item.id
28825
- };
28826
- runtime.context.__boardDraw = runtime.module.draw;
28827
- runtime.context.__boardDrawContext = context;
28828
- runtime.context.__boardFrame = frame;
28829
- try {
28830
- const drawResult = new Script("__boardDraw(__boardDrawContext, __boardFrame)", { filename: item.props.src }).runInContext(runtime.context, { timeout: timeoutMs });
28831
- if (drawResult && typeof drawResult.then === "function") throw new Error(`${item.props.src} must export a synchronous draw(ctx, frame) for Node export.`);
28832
- } finally {
28833
- delete runtime.context.__boardDraw;
28834
- delete runtime.context.__boardDrawContext;
28835
- delete runtime.context.__boardFrame;
30807
+ const sources = /* @__PURE__ */ new Map();
30808
+ const modules = createBoardScriptModuleReader(options.readModule, options.readTimeoutMs);
30809
+ const lanes = Array.from({ length: 4 }, () => ({
30810
+ worker: null,
30811
+ tail: Promise.resolve(),
30812
+ graphs: /* @__PURE__ */ new Set()
30813
+ }));
30814
+ const assignments = /* @__PURE__ */ new Map();
30815
+ let disposed = false;
30816
+ let nextLane = 0;
30817
+ const timeout = options.timeoutMs ?? 2e3;
30818
+ const key = (id, time) => `${id}|${time}`;
30819
+ function stop(lane) {
30820
+ const worker = lane.worker;
30821
+ lane.worker = null;
30822
+ lane.graphs.clear();
30823
+ worker?.terminate();
30824
+ }
30825
+ function sample(input, graph, time, scale) {
30826
+ let lane = assignments.get(input.id);
30827
+ if (!lane) {
30828
+ lane = lanes[nextLane++ % lanes.length];
30829
+ assignments.set(input.id, lane);
30830
+ }
30831
+ const selected = lane;
30832
+ const task = selected.tail.then(() => new Promise((resolve, reject) => {
30833
+ if (disposed) {
30834
+ reject(/* @__PURE__ */ new Error("Script host is disposed."));
30835
+ return;
28836
30836
  }
28837
- textures.set(key, textureFromCanvas(canvas));
28838
- lastErrors.delete(item.id);
28839
- } catch (error) {
28840
- errors.set(key, error instanceof Error ? error.message : String(error));
28841
- lastErrors.set(item.id, error instanceof Error ? error.message : String(error));
28842
- }
28843
- };
30837
+ if (!Number.isFinite(time) || time < 0 || time > 6e5) {
30838
+ reject(/* @__PURE__ */ new Error("Script export time must be between 0 and 600000 ms."));
30839
+ return;
30840
+ }
30841
+ const freshWorker = !selected.worker;
30842
+ selected.worker ??= new Worker(WORKER, {
30843
+ eval: true,
30844
+ execArgv: ["--experimental-vm-modules", "--disable-warning=ExperimentalWarning"],
30845
+ env: {},
30846
+ resourceLimits: { maxOldGenerationSizeMb: 128 },
30847
+ workerData: {
30848
+ runtime: createScriptRuntime.toString(),
30849
+ timeout,
30850
+ canvasModule: fileURLToPath(import.meta.resolve("@napi-rs/canvas"))
30851
+ }
30852
+ });
30853
+ const worker = selected.worker;
30854
+ if (freshWorker) {
30855
+ const stopped = (error) => {
30856
+ if (selected.worker !== worker) return;
30857
+ if (selected.reject) selected.reject(error);
30858
+ else stop(selected);
30859
+ };
30860
+ worker.on("error", stopped);
30861
+ worker.on("exit", () => stopped(/* @__PURE__ */ new Error("Script worker stopped.")));
30862
+ }
30863
+ const cleanup = () => {
30864
+ clearTimeout(timer);
30865
+ worker.off("message", message);
30866
+ selected.reject = void 0;
30867
+ };
30868
+ const failure = (error) => {
30869
+ cleanup();
30870
+ stop(selected);
30871
+ reject(error);
30872
+ };
30873
+ const message = (value) => {
30874
+ cleanup();
30875
+ if (value.error) reject(new Error(value.error));
30876
+ else resolve(value);
30877
+ };
30878
+ const timer = setTimeout(() => failure(/* @__PURE__ */ new Error("Script export timed out.")), Math.max(timeout, 15e3));
30879
+ selected.reject = failure;
30880
+ worker.once("message", message);
30881
+ const src = input.item.script?.src ?? "";
30882
+ worker.postMessage({
30883
+ input,
30884
+ src,
30885
+ signature: JSON.stringify(input.item.script),
30886
+ sources: selected.graphs.has(src) ? void 0 : graph,
30887
+ time,
30888
+ scale
30889
+ });
30890
+ selected.graphs.add(src);
30891
+ }));
30892
+ selected.tail = task.catch(() => void 0);
30893
+ return task;
30894
+ }
28844
30895
  return {
28845
- frame(item, time, resolution) {
28846
- const scale = frameScale(resolution, item.frame.width, item.frame.height);
28847
- return textures.get(frameKey(item, time, scale)) ?? null;
30896
+ ready: (id) => [...outputs.values()].some((at) => at.has(id)),
30897
+ frame: (item, time) => textures.get(key(item.id, time)) ?? null,
30898
+ error: (id) => errors.get(id) ?? null,
30899
+ release() {},
30900
+ document(document, time) {
30901
+ const at = outputs.get(time);
30902
+ if (!at?.size) return document;
30903
+ const items = { ...document.items };
30904
+ for (const [id, item] of composeBoardScriptItems(document, buildBoardScene(document), at)) items[id] = item;
30905
+ return {
30906
+ ...document,
30907
+ items
30908
+ };
30909
+ },
30910
+ clearFrames() {
30911
+ for (const texture of textures.values()) texture.destroy(true);
30912
+ textures.clear();
30913
+ outputs.clear();
30914
+ errors.clear();
28848
30915
  },
28849
- error: (id) => lastErrors.get(id) ?? null,
28850
- release: () => {},
28851
30916
  async prepare(items, times, zoom) {
28852
- const sketches = items.filter((item) => item.type === "sketch");
28853
- const jobs = [];
28854
- for (const item of sketches) {
28855
- const scale = frameScale(zoom, item.frame.width, item.frame.height);
28856
- for (const time of times) jobs.push(() => render(item, time, scale));
30917
+ const scripted = items.filter((item) => item.script?.enabled);
30918
+ const alive = new Set(scripted.map((item) => item.id));
30919
+ for (const [id, lane] of assignments) if (!alive.has(id)) {
30920
+ lane.worker?.postMessage({ release: [id] });
30921
+ assignments.delete(id);
28857
30922
  }
28858
- let next = 0;
28859
- const worker = async () => {
28860
- while (next < jobs.length) {
28861
- const job = jobs[next++];
28862
- if (job) await job();
30923
+ if (!scripted.length) return;
30924
+ const all = Object.fromEntries(items.map(({ id, frame: _frame, ...item }) => [id, item]));
30925
+ await Promise.all(scripted.map(async (item) => {
30926
+ const base = all[item.id];
30927
+ const src = item.script?.src;
30928
+ if (!base || !src) return;
30929
+ try {
30930
+ let graph = sources.get(src);
30931
+ if (!graph) {
30932
+ graph = loadBoardScriptSources(src, modules.read, options.readTimeoutMs);
30933
+ sources.set(src, graph);
30934
+ }
30935
+ const resolved = await graph;
30936
+ const width = "size" in item ? item.size.width : item.frame.width;
30937
+ const height = "size" in item ? item.size.height : item.frame.height;
30938
+ const scale = Math.min(4, Math.max(.25, zoom), 2048 / Math.max(1, width, height));
30939
+ const scoped = Object.fromEntries([...boardScriptScope({ items: all }, item.id)].flatMap((id) => all[id] ? [[id, all[id]]] : []));
30940
+ for (const time of times) {
30941
+ const result = await sample({
30942
+ id: item.id,
30943
+ item: base,
30944
+ items: scoped,
30945
+ width,
30946
+ height
30947
+ }, resolved, time, scale);
30948
+ if (disposed) return;
30949
+ if (result.png) {
30950
+ const frameKey = key(item.id, time);
30951
+ textures.get(frameKey)?.destroy(true);
30952
+ textures.set(frameKey, await renderer.decodeImage(result.png));
30953
+ }
30954
+ let at = outputs.get(time);
30955
+ if (!at) {
30956
+ at = /* @__PURE__ */ new Map();
30957
+ outputs.set(time, at);
30958
+ }
30959
+ at.set(item.id, result.output);
30960
+ }
30961
+ } catch (error) {
30962
+ errors.set(item.id, error instanceof Error ? error.message : String(error));
28863
30963
  }
28864
- };
28865
- await Promise.all(Array.from({ length: Math.min(MAX_PREPARE_CONCURRENCY, jobs.length) }, worker));
30964
+ }));
28866
30965
  },
28867
30966
  destroy() {
30967
+ disposed = true;
30968
+ for (const lane of lanes) {
30969
+ lane.reject?.(/* @__PURE__ */ new Error("Script host is disposed."));
30970
+ stop(lane);
30971
+ }
28868
30972
  for (const texture of textures.values()) texture.destroy(true);
28869
30973
  textures.clear();
28870
- errors.clear();
28871
- lastErrors.clear();
28872
- modules.clear();
30974
+ outputs.clear();
30975
+ sources.clear();
30976
+ modules.destroy();
30977
+ assignments.clear();
28873
30978
  }
28874
30979
  };
28875
30980
  }
@@ -28999,10 +31104,10 @@ const MIME = {
28999
31104
  webp: "image/webp"
29000
31105
  };
29001
31106
  function exportBoardImageBytes(renderer, document, options = {}) {
29002
- const { format = "png", quality = .92, textures, sketches, backgroundImage, ...rest } = options;
31107
+ const { format = "png", quality = .92, textures, scripts, backgroundImage, ...rest } = options;
29003
31108
  const result = renderBoardExport(renderer.renderer, document, {
29004
31109
  ...rest,
29005
- ...sketches ? { sketches } : {},
31110
+ ...scripts ? { scripts } : {},
29006
31111
  ...textures ? { textures } : {},
29007
31112
  ...backgroundImage ? { backgroundImage: {
29008
31113
  ...backgroundImage,
@@ -29011,13 +31116,18 @@ function exportBoardImageBytes(renderer, document, options = {}) {
29011
31116
  });
29012
31117
  if (!result) return null;
29013
31118
  const canvas = result.canvas;
29014
- const bytes = format === "png" ? canvas.toBuffer(MIME.png) : canvas.toBuffer(MIME[format], Math.round(quality * 100));
29015
- return {
29016
- bytes: new Uint8Array(bytes),
29017
- plan: result.plan,
29018
- warnings: result.warnings,
29019
- format
29020
- };
31119
+ try {
31120
+ const bytes = format === "png" ? canvas.toBuffer(MIME.png) : canvas.toBuffer(MIME[format], Math.round(quality * 100));
31121
+ return {
31122
+ bytes: new Uint8Array(bytes),
31123
+ plan: result.plan,
31124
+ warnings: result.warnings,
31125
+ format
31126
+ };
31127
+ } finally {
31128
+ canvas.width = 1;
31129
+ canvas.height = 1;
31130
+ }
29021
31131
  }
29022
31132
  //#endregion
29023
- export { boardDocumentAt, boardImageKeySource, buildBoardScene, createNodeBoardRenderer, createNodeBoardSketchHost, exportBoardImageBytes, imageAssetKey, parseBoardDocument, planBoardExport, selectBoardExportAssets };
31133
+ export { BOARD_SCRIPT_DRAWING_EXAMPLE, boardDocumentAt, boardImageKeySource, boardPathTrack, boardSequence, boardSequencePatch, buildBoardScene, createBoardAnimationSampler, createBoardRenderSampler, createNodeBoardRenderer, createNodeBoardScriptHost, exportBoardImageBytes, hasBoardScripts, imageAssetKey, inspectBoardScene, measureBoardText, parseBoardDocument, planBoardExport, prepareBoardExport, selectBoardExportAssets };