paintress 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -573,6 +573,322 @@ const DocumentSchema = DocumentObjectSchema.superRefine((doc, ctx) => {
573
573
  });
574
574
  /** .ptr 文档:pen_profile + x-paintress 必填 */
575
575
  const PtrDocumentSchema = DocumentSchema.and(z.object({ [X_PAINTRESS_KEY]: XPaintress }).passthrough());
576
+ // src/doc/presets.ts
577
+ const PRESET_TYPES = [
578
+ "color",
579
+ "radius",
580
+ "spacing",
581
+ "fontSize",
582
+ "font"
583
+ ];
584
+ function isPresetType(v) {
585
+ return typeof v === "string" && PRESET_TYPES.includes(v);
586
+ }
587
+ /**
588
+ * 属性 → 期望的 token 种类。**唯一映射表**:audit、establish、validate、写入提示都从这里取,
589
+ * 各处不得再抄一份(2026-11-15:`fontSize` 曾因两处映射不一致而无法 token 化)。
590
+ *
591
+ * 刻意不含 `lineHeight`(它是 fontSize 的倍数,塞进任何数字种类都是错语义);
592
+ * `fontWeight` 同理(等独立种类,见 `.agents/memory/FUTURE.md`)。
593
+ */
594
+ const PROP_TOKEN_KIND = {
595
+ fill: "color",
596
+ stroke: "color",
597
+ color: "color",
598
+ effect: "color",
599
+ cornerRadius: "radius",
600
+ gap: "spacing",
601
+ padding: "spacing",
602
+ strokeWidth: "spacing",
603
+ letterSpacing: "spacing",
604
+ fontSize: "fontSize",
605
+ fontFamily: "font"
606
+ };
607
+ /** 种类的人读名(校验与审计的提示语用) */
608
+ const PRESET_TYPE_LABEL = {
609
+ color: "颜色",
610
+ radius: "倒角",
611
+ spacing: "间距",
612
+ fontSize: "字号",
613
+ font: "字体族"
614
+ };
615
+ /** 值是不是一个数字字串(老 `font` token 靠它识别字号;纯字体族名不会全是数字) */
616
+ function isNumericPresetValue(value) {
617
+ return typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value));
618
+ }
619
+ function isRecord$7(v) {
620
+ return typeof v === "object" && v !== null && !Array.isArray(v);
621
+ }
622
+ /** 文档里携带的预设组(`x-paintress.presets.groups`);坏结构当没有 */
623
+ function presetGroupsOf(doc) {
624
+ const meta = isRecord$7(doc) ? doc["x-paintress"] : void 0;
625
+ const presets = isRecord$7(meta) ? meta["presets"] : void 0;
626
+ const groups = isRecord$7(presets) ? presets["groups"] : void 0;
627
+ if (!isRecord$7(groups)) return {};
628
+ const out = {};
629
+ for (const [name, raw] of Object.entries(groups)) {
630
+ if (!isRecord$7(raw)) continue;
631
+ const target = typeof raw["target"] === "string" ? raw["target"] : "";
632
+ const props = {};
633
+ const rawProps = raw["props"];
634
+ if (isRecord$7(rawProps)) {
635
+ for (const [k, v] of Object.entries(rawProps)) if (typeof v === "string" && v.trim() !== "") props[k] = v;
636
+ }
637
+ if (target === "" || Object.keys(props).length === 0) continue;
638
+ out[name] = {
639
+ target,
640
+ props
641
+ };
642
+ }
643
+ return out;
644
+ }
645
+ /**
646
+ * 渲染前把节点上的 `preset: '<name>'` 铺开:组属性打底,**节点自己的属性优先**。
647
+ * 返回新树(不改输入,保存永远写原文档);缺失/类型不符的引用只警告、不猜。
648
+ */
649
+ function applyPresetRefs(nodes, groups, warnings) {
650
+ if (Object.keys(groups).length === 0) return nodes;
651
+ const walk = (list, where) => list.map((raw, i) => {
652
+ if (!isRecord$7(raw)) return raw;
653
+ let node = raw;
654
+ const name = node["preset"];
655
+ const label = `${String(node["type"] ?? "?")}${typeof node["name"] === "string" && node["name"] !== "" ? `: ${String(node["name"])}` : ""}`;
656
+ if (typeof name === "string" && name !== "") {
657
+ const group = groups[name];
658
+ if (group === void 0) warnings.push(`${label}: preset "${name}" 不存在`);
659
+ else if (group.target !== node["type"]) {
660
+ if (!Object.entries(group.props).every(([key, value]) => node[key] === value)) warnings.push(`${label}: preset "${name}" 适用于 ${group.target},节点是 ${String(node["type"])}`);
661
+ } else node = {
662
+ ...group.props,
663
+ ...node
664
+ };
665
+ }
666
+ const kids = node["children"];
667
+ if (Array.isArray(kids)) node = {
668
+ ...node,
669
+ children: walk(kids, `${where}/${i}`)
670
+ };
671
+ return node;
672
+ });
673
+ return walk(nodes, "children");
674
+ }
675
+ // src/doc/fonts.ts
676
+ const FONT_SOURCE_KINDS = [
677
+ "web",
678
+ "css",
679
+ "file",
680
+ "system",
681
+ "bundle"
682
+ ];
683
+ function isFontSourceKind(v) {
684
+ return typeof v === "string" && FONT_SOURCE_KINDS.includes(v);
685
+ }
686
+ /**
687
+ * paintress 自己的字体观:不带字体文件,只给 jsDelivr 上的 fontsource CSS。
688
+ * 版本钉死,换镜像/自托管只改这里与 seed 数据。
689
+ */
690
+ const BUILTIN_FONT_LINKS = ["https://cdn.jsdelivr.net/npm/@fontsource-variable/inter@5.3.0/wght.css", "https://cdn.jsdelivr.net/npm/@fontsource-variable/noto-sans-sc@5.3.1/wght.css"];
691
+ /** 内置网络字体 CSS;`PAINTRESS_NO_WEB_FONTS=1` 时跳过(CI / 纯离线) */
692
+ function webFontLinks() {
693
+ return process.env["PAINTRESS_NO_WEB_FONTS"] === "1" ? [] : [...BUILTIN_FONT_LINKS];
694
+ }
695
+ /** 无项目默认时的内置栈;系统字体只是栈尾兵底 */
696
+ const BUILTIN_FONT_STACK = "\"Inter Variable\", \"Noto Sans SC Variable\", system-ui, -apple-system, \"Segoe UI\", sans-serif";
697
+ const GENERIC_FAMILIES = /* @__PURE__ */ new Set([
698
+ "sans-serif",
699
+ "serif",
700
+ "monospace",
701
+ "cursive",
702
+ "fantasy",
703
+ "system-ui",
704
+ "ui-monospace",
705
+ "ui-sans-serif",
706
+ "ui-serif",
707
+ "ui-rounded",
708
+ "emoji",
709
+ "math",
710
+ "fangsong"
711
+ ]);
712
+ function isGenericFamily(family) {
713
+ return GENERIC_FAMILIES.has(family.trim().toLowerCase());
714
+ }
715
+ function isRecord$6(v) {
716
+ return typeof v === "object" && v !== null && !Array.isArray(v);
717
+ }
718
+ /** `'Inter, "Noto Sans SC"'` → `['Inter', 'Noto Sans SC']`(去引号、去空项) */
719
+ function splitFontStack(stack) {
720
+ return stack.split(",").map((part) => part.trim().replace(/^["']|["']$/g, "").trim()).filter((part) => part !== "");
721
+ }
722
+ function quoteFamily(family) {
723
+ return /[\s]/.test(family) ? `"${family}"` : family;
724
+ }
725
+ /**
726
+ * 真字体在前、替代在后、`sans-serif` 兜底。替代值可以是 stack。
727
+ * 已经出现过的 family 不重复加(先出现的那一层优先,语义与 CSS 一致)。
728
+ */
729
+ function stackWithPreviews(stack, previews) {
730
+ const production = splitFontStack(stack);
731
+ const seen = /* @__PURE__ */ new Set();
732
+ const out = [];
733
+ const push = (family) => {
734
+ const key = family.toLowerCase();
735
+ if (seen.has(key)) return;
736
+ seen.add(key);
737
+ out.push(quoteFamily(family));
738
+ };
739
+ for (const family of production) {
740
+ push(family);
741
+ const preview = previews[family];
742
+ if (preview === void 0) continue;
743
+ for (const entry of splitFontStack(preview)) push(entry);
744
+ }
745
+ if (!production.some((family) => isGenericFamily(family))) out.push("sans-serif");
746
+ return out.join(", ");
747
+ }
748
+ /** 节点显式写了的字体族(不展开 `$token`;token 的解析由渲染器完成) */
749
+ function stackFamilies(stack) {
750
+ return splitFontStack(stack).filter((family) => !isGenericFamily(family));
751
+ }
752
+ /** 解析 `x-paintress.fonts`;坏结构当空,不让一份手写文件把渲染搞崩 */
753
+ function fontRegistryOf(doc) {
754
+ const meta = isRecord$6(doc) ? doc["x-paintress"] : void 0;
755
+ const fonts = isRecord$6(meta) ? meta["fonts"] : void 0;
756
+ if (!isRecord$6(fonts)) return { families: {} };
757
+ const rawFamilies = isRecord$6(fonts["families"]) ? fonts["families"] : {};
758
+ const families = {};
759
+ for (const [family, raw] of Object.entries(rawFamilies)) {
760
+ if (!isRecord$6(raw)) continue;
761
+ const sourceRaw = isRecord$6(raw["source"]) ? raw["source"] : void 0;
762
+ if (sourceRaw === void 0 || !isFontSourceKind(sourceRaw["kind"])) continue;
763
+ const target = [
764
+ sourceRaw["target"],
765
+ sourceRaw["url"],
766
+ sourceRaw["path"]
767
+ ].find((v) => typeof v === "string");
768
+ const source = {
769
+ kind: sourceRaw["kind"],
770
+ ...target !== void 0 ? { target } : {}
771
+ };
772
+ const metricsRaw = isRecord$6(raw["metrics"]) ? raw["metrics"] : void 0;
773
+ const metrics = {};
774
+ if (metricsRaw) {
775
+ for (const key of [
776
+ "sizeAdjust",
777
+ "ascentOverride",
778
+ "descentOverride",
779
+ "lineGapOverride"
780
+ ]) if (typeof metricsRaw[key] === "string" && metricsRaw[key] !== "") metrics[key] = metricsRaw[key];
781
+ }
782
+ families[family] = {
783
+ source,
784
+ ...typeof raw["preview"] === "string" && raw["preview"] !== "" ? { preview: raw["preview"] } : {},
785
+ ...Object.keys(metrics).length > 0 ? { metrics } : {},
786
+ ...typeof raw["note"] === "string" && raw["note"] !== "" ? { note: raw["note"] } : {}
787
+ };
788
+ }
789
+ return {
790
+ ...typeof fonts["default"] === "string" && fonts["default"] !== "" ? { default: fonts["default"] } : {},
791
+ families
792
+ };
793
+ }
794
+ function varString(variables, key) {
795
+ const def = variables?.[key];
796
+ if (!isRecord$6(def)) return void 0;
797
+ const value = def["value"];
798
+ return typeof value === "string" ? value : void 0;
799
+ }
800
+ /** 默认栈:`default` 是 token key 就解析成值,否则按 family 用;未配置用内置栈 */
801
+ function resolveDefaultStack(registry, doc) {
802
+ const raw = registry.default;
803
+ if (raw === void 0 || raw === "") return BUILTIN_FONT_STACK;
804
+ const fromToken = varString(doc?.variables, raw);
805
+ if (fromToken !== void 0) return fromToken;
806
+ return raw;
807
+ }
808
+ const FORMAT_BY_EXT = {
809
+ ".woff2": "woff2",
810
+ ".woff": "woff",
811
+ ".ttf": "truetype",
812
+ ".otf": "opentype"
813
+ };
814
+ function fontFormat(target) {
815
+ const dot = target.lastIndexOf(".");
816
+ if (dot < 0) return void 0;
817
+ return FORMAT_BY_EXT[target.slice(dot).toLowerCase()];
818
+ }
819
+ /**
820
+ * 由注册表生成渲染计划。`resolveUrl` 决定 URL 的可达形式:
821
+ * 查看器给 `/files/<project>/<path>`,截图给 `file:///<abs>`,导出可以给 data URI。
822
+ */
823
+ function planFonts(registry, resolveUrl, doc) {
824
+ const links = webFontLinks();
825
+ const faces = [];
826
+ const previews = {};
827
+ for (const [family, entry] of Object.entries(registry.families)) {
828
+ const { source } = entry;
829
+ if (source.kind === "web" && source.target !== void 0) links.push(source.target);
830
+ else if (source.kind === "css" && source.target !== void 0) {
831
+ const url = resolveUrl(source);
832
+ if (url !== null) links.push(url);
833
+ } else if (source.kind === "file" && source.target !== void 0) {
834
+ const url = resolveUrl(source);
835
+ if (url !== null) {
836
+ const format = fontFormat(source.target);
837
+ faces.push({
838
+ family,
839
+ src: url,
840
+ ...format !== void 0 ? { format } : {}
841
+ });
842
+ }
843
+ }
844
+ if (entry.preview !== void 0) {
845
+ const first = splitFontStack(entry.preview)[0];
846
+ if (entry.metrics !== void 0 && first !== void 0 && !isGenericFamily(first)) {
847
+ faces.push({
848
+ family: `Preview: ${family}`,
849
+ src: `local("${first}")`,
850
+ metrics: entry.metrics
851
+ });
852
+ previews[family] = `"Preview: ${family}", ${entry.preview}`;
853
+ } else previews[family] = entry.preview;
854
+ }
855
+ }
856
+ return {
857
+ links,
858
+ faces,
859
+ previews,
860
+ defaultStack: resolveDefaultStack(registry, doc)
861
+ };
862
+ }
863
+ /** 字体缺口:没写 family 的文本、没登记的 family —— MCP 的 doc_info / validate / hints 共用 */
864
+ function textFontGaps(doc, registry) {
865
+ const ids = [];
866
+ const families = /* @__PURE__ */ new Set();
867
+ let textNodes = 0;
868
+ const walk = (nodes) => {
869
+ for (const raw of nodes) {
870
+ if (!isRecord$6(raw)) continue;
871
+ if (raw["type"] === "text") {
872
+ const ff = raw["fontFamily"];
873
+ if (typeof ff !== "string" || ff.trim() === "") {
874
+ textNodes += 1;
875
+ if (ids.length < 10 && typeof raw["id"] === "string") ids.push(raw["id"]);
876
+ } else if (!ff.startsWith("$")) for (const family of stackFamilies(ff)) families.add(family);
877
+ }
878
+ const kids = raw["children"];
879
+ if (Array.isArray(kids)) walk(kids);
880
+ }
881
+ };
882
+ walk(doc.children);
883
+ const list = [...families];
884
+ return {
885
+ textNodes,
886
+ ids,
887
+ families: list,
888
+ unregistered: list.filter((family) => registry.families[family] === void 0),
889
+ defaultStack: resolveDefaultStack(registry, doc)
890
+ };
891
+ }
576
892
  // src/doc/id.ts
577
893
  /** id 生成:5 位 base62,禁 '/'。与既有文档 id 风格一致(如 zae8o、bi8Au)。 */
578
894
  const ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
@@ -596,6 +912,14 @@ function collectIds(children, out = []) {
596
912
  return out;
597
913
  }
598
914
  // src/doc/validate.ts
915
+ /** 一个属性值里出现的所有 `$token` 引用(结构化填充、数组、逐边对象都要覆盖) */
916
+ function refsIn(value, out = []) {
917
+ if (typeof value === "string") {
918
+ if (VARIABLE_NAME.test(value)) out.push(value.slice(1));
919
+ } else if (Array.isArray(value)) for (const entry of value) refsIn(entry, out);
920
+ else if (value !== null && typeof value === "object") for (const entry of Object.values(value)) refsIn(entry, out);
921
+ return out;
922
+ }
599
923
  /** 结构校验(schema)+ 语义校验(重复 id、悬空 ref、未知类型、未解析变量) */
600
924
  function validateDoc(input, opts = {}) {
601
925
  const errors = [];
@@ -614,6 +938,11 @@ function validateDoc(input, opts = {}) {
614
938
  };
615
939
  }
616
940
  const doc = parsed.data;
941
+ const presetGroups = {
942
+ ...opts.presetGroups ?? {},
943
+ ...presetGroupsOf(doc)
944
+ };
945
+ const presetKinds = opts.presetKinds ?? {};
617
946
  const allIds = collectIds(doc.children);
618
947
  const seen = /* @__PURE__ */ new Set();
619
948
  const dup = /* @__PURE__ */ new Set();
@@ -643,11 +972,53 @@ function validateDoc(input, opts = {}) {
643
972
  path: here,
644
973
  message: `ref 指向不存在的组件 id "${String(rec["ref"])}"`
645
974
  });
975
+ const presetName = rec["preset"];
976
+ if (typeof presetName === "string" && presetName !== "") {
977
+ const group = presetGroups[presetName];
978
+ if (group === void 0) warnings.push({
979
+ level: "warning",
980
+ path: here,
981
+ message: `preset "${presetName}" 不存在`
982
+ });
983
+ else if (group.target !== type) warnings.push({
984
+ level: "warning",
985
+ path: here,
986
+ message: `preset "${presetName}" 适用于 ${group.target},这里是 ${String(type)}`
987
+ });
988
+ }
646
989
  const kids = rec["children"];
647
990
  if (Array.isArray(kids)) walk(kids, `${here}/children`);
991
+ for (const [prop, value] of Object.entries(rec)) {
992
+ if (prop === "children") continue;
993
+ const expected = PROP_TOKEN_KIND[prop];
994
+ if (expected === void 0) continue;
995
+ for (const ref of refsIn(value)) {
996
+ const actual = presetKinds[ref];
997
+ if (actual === void 0 || actual === expected) continue;
998
+ warnings.push({
999
+ level: "warning",
1000
+ path: here,
1001
+ message: `${prop} 需要${PRESET_TYPE_LABEL[expected]} token,$${ref} 是${PRESET_TYPE_LABEL[actual]}(类型 ${actual})`
1002
+ });
1003
+ }
1004
+ }
648
1005
  }
649
1006
  };
650
1007
  walk(doc.children, "children");
1008
+ for (const [name, group] of Object.entries(presetGroups)) for (const [prop, value] of Object.entries(group.props)) if (!VARIABLE_NAME.test(value)) warnings.push({
1009
+ level: "warning",
1010
+ path: `<preset:${name}>`,
1011
+ message: `preset 组里的 ${prop} 是字面量 "${value}",应当写成 $token 引用`
1012
+ });
1013
+ else {
1014
+ const expected = PROP_TOKEN_KIND[prop];
1015
+ const actual = presetKinds[value.slice(1)];
1016
+ if (expected !== void 0 && actual !== void 0 && actual !== expected) warnings.push({
1017
+ level: "warning",
1018
+ path: `<preset:${name}>`,
1019
+ message: `${prop} 需要${PRESET_TYPE_LABEL[expected]} token,${value} 是${PRESET_TYPE_LABEL[actual]}(类型 ${actual})`
1020
+ });
1021
+ }
651
1022
  const localNames = new Set(doc.variables ? Object.keys(doc.variables) : []);
652
1023
  const varNames = /* @__PURE__ */ new Set([...localNames, ...opts.knownVars ?? []]);
653
1024
  for (const name of opts.presetVars ?? []) if (localNames.has(name)) warnings.push({
@@ -662,11 +1033,40 @@ function validateDoc(input, opts = {}) {
662
1033
  else if (obj && typeof obj === "object") Object.values(obj).forEach(scan);
663
1034
  };
664
1035
  scan(doc.children);
1036
+ for (const group of Object.values(presetGroups)) for (const value of Object.values(group.props)) scan(value);
665
1037
  for (const name of varRefs) if (!varNames.has(name)) warnings.push({
666
1038
  level: "warning",
667
1039
  path: "<variables>",
668
1040
  message: `字段引用了未定义的变量 $${name}`
669
1041
  });
1042
+ if (opts.fontRegistry !== void 0) {
1043
+ const gaps = textFontGaps(doc, opts.fontRegistry);
1044
+ if (gaps.textNodes > 0) {
1045
+ const sample = gaps.ids.length > 0 ? `(如 ${gaps.ids.join("、")})` : "";
1046
+ warnings.push({
1047
+ level: "warning",
1048
+ path: "<fonts>",
1049
+ message: `${gaps.textNodes} 个 text 节点没有 fontFamily,当前用默认字体 ${gaps.defaultStack}${sample}`
1050
+ });
1051
+ }
1052
+ for (const family of gaps.unregistered) warnings.push({
1053
+ level: "warning",
1054
+ path: "<fonts>",
1055
+ message: `字体未登记:${family}(paintress_font add)`
1056
+ });
1057
+ for (const [family, entry] of Object.entries(opts.fontRegistry.families)) if ((entry.source.kind === "system" || entry.source.kind === "bundle") && entry.preview === void 0) warnings.push({
1058
+ level: "warning",
1059
+ path: `<font:${family}>`,
1060
+ message: `${family} 是 ${entry.source.kind} 来源,没有 preview:截图侧可能不可用`
1061
+ });
1062
+ const def = opts.fontRegistry.default;
1063
+ const known = new Set(opts.knownVars ?? []);
1064
+ if (def !== void 0 && opts.fontRegistry.families[def] === void 0 && !known.has(def)) warnings.push({
1065
+ level: "warning",
1066
+ path: "<fonts>",
1067
+ message: `默认字体 ${def} 没有登记`
1068
+ });
1069
+ }
670
1070
  return {
671
1071
  ok: errors.length === 0,
672
1072
  errors,
@@ -933,7 +1333,7 @@ function batch(doc, ops) {
933
1333
  return clone;
934
1334
  }
935
1335
  // src/render/vars.ts
936
- function isRecord$3(v) {
1336
+ function isRecord$5(v) {
937
1337
  return typeof v === "object" && v !== null && !Array.isArray(v);
938
1338
  }
939
1339
  function asVarValue(v) {
@@ -947,7 +1347,7 @@ function asVarValue(v) {
947
1347
  */
948
1348
  function resolveThemed(v, theme) {
949
1349
  if (Array.isArray(v)) {
950
- const entries = v.filter(isRecord$3);
1350
+ const entries = v.filter(isRecord$5);
951
1351
  if (entries.length === 0) return void 0;
952
1352
  let best = null;
953
1353
  for (const entry of entries) {
@@ -959,7 +1359,7 @@ function resolveThemed(v, theme) {
959
1359
  };
960
1360
  continue;
961
1361
  }
962
- if (!isRecord$3(t)) continue;
1362
+ if (!isRecord$5(t)) continue;
963
1363
  const keys = Object.keys(t);
964
1364
  if (!keys.every((k) => theme[k] === String(t[k]))) continue;
965
1365
  if (best === null || keys.length > best.score) best = {
@@ -976,7 +1376,7 @@ function makeVarResolver(doc, warnings, theme = {}) {
976
1376
  const make = (scope) => {
977
1377
  const lookup = (name) => {
978
1378
  const def = vars[name];
979
- if (!isRecord$3(def)) return void 0;
1379
+ if (!isRecord$5(def)) return void 0;
980
1380
  return resolveThemed(def["value"], scope);
981
1381
  };
982
1382
  return {
@@ -1027,7 +1427,7 @@ function makeVarResolver(doc, warnings, theme = {}) {
1027
1427
  return make(theme);
1028
1428
  }
1029
1429
  // src/render/expand.ts
1030
- function isRecord$2(v) {
1430
+ function isRecord$4(v) {
1031
1431
  return typeof v === "object" && v !== null && !Array.isArray(v);
1032
1432
  }
1033
1433
  function brokenRefPlaceholder(refNode, reason, warnings) {
@@ -1048,7 +1448,7 @@ function indexById(children) {
1048
1448
  const map = /* @__PURE__ */ new Map();
1049
1449
  const walk = (nodes) => {
1050
1450
  for (const n of nodes) {
1051
- if (!isRecord$2(n)) continue;
1451
+ if (!isRecord$4(n)) continue;
1052
1452
  const id = n["id"];
1053
1453
  if (typeof id === "string") map.set(id, n);
1054
1454
  const kids = n["children"];
@@ -1065,7 +1465,7 @@ function findByPath(root, segments) {
1065
1465
  for (const seg of segments) {
1066
1466
  const kids = container["children"];
1067
1467
  if (!Array.isArray(kids)) return null;
1068
- found = kids.find((k) => isRecord$2(k) && k["id"] === seg) ?? null;
1468
+ found = kids.find((k) => isRecord$4(k) && k["id"] === seg) ?? null;
1069
1469
  if (!found) return null;
1070
1470
  container = found;
1071
1471
  }
@@ -1076,7 +1476,7 @@ function findByPath(root, segments) {
1076
1476
  }
1077
1477
  function applyDescendants(expandedRoot, descendants, warnings, refId) {
1078
1478
  for (const [path, overrideRaw] of Object.entries(descendants)) {
1079
- if (!isRecord$2(overrideRaw)) continue;
1479
+ if (!isRecord$4(overrideRaw)) continue;
1080
1480
  const hit = findByPath(expandedRoot, path.split("/").filter((s) => s !== ""));
1081
1481
  if (!hit) {
1082
1482
  warnings.push(`ref ${refId} 的 descendants 路径 "${path}" 未命中,已忽略`);
@@ -1096,7 +1496,7 @@ function applyDescendants(expandedRoot, descendants, warnings, refId) {
1096
1496
  /** 递归展开一个节点(就地修改克隆树) */
1097
1497
  function expandNode(node, index, stack, warnings) {
1098
1498
  const kids = node["children"];
1099
- if (Array.isArray(kids)) node["children"] = kids.map((k) => isRecord$2(k) ? expandNode(k, index, stack, warnings) : k);
1499
+ if (Array.isArray(kids)) node["children"] = kids.map((k) => isRecord$4(k) ? expandNode(k, index, stack, warnings) : k);
1100
1500
  if (node["type"] !== "ref") return node;
1101
1501
  const refId = typeof node["id"] === "string" ? node["id"] : "?";
1102
1502
  const target = typeof node["ref"] === "string" ? node["ref"] : "";
@@ -1108,13 +1508,13 @@ function expandNode(node, index, stack, warnings) {
1108
1508
  const clone = structuredClone(component);
1109
1509
  clone["id"] = node["id"];
1110
1510
  if (typeof node["name"] === "string") clone["name"] = node["name"];
1111
- if (Array.isArray(clone["children"])) clone["children"] = clone["children"].map((k) => isRecord$2(k) ? expandNode(k, index, stack, warnings) : k);
1511
+ if (Array.isArray(clone["children"])) clone["children"] = clone["children"].map((k) => isRecord$4(k) ? expandNode(k, index, stack, warnings) : k);
1112
1512
  for (const [k, v] of Object.entries(node)) {
1113
1513
  if (k === "type" || k === "ref" || k === "descendants" || k === "id" || k === "name") continue;
1114
1514
  clone[k] = structuredClone(v);
1115
1515
  }
1116
1516
  const desc = node["descendants"];
1117
- if (isRecord$2(desc)) applyDescendants(clone, desc, warnings, refId);
1517
+ if (isRecord$4(desc)) applyDescendants(clone, desc, warnings, refId);
1118
1518
  return clone;
1119
1519
  } finally {
1120
1520
  stack.delete(target);
@@ -1126,21 +1526,72 @@ function expandRefs(doc, warnings) {
1126
1526
  return structuredClone(doc.children).map((n) => expandNode(n, index, /* @__PURE__ */ new Set(), warnings));
1127
1527
  }
1128
1528
  // src/render/style.ts
1129
- function isRecord$1(v) {
1529
+ function isRecord$3(v) {
1130
1530
  return typeof v === "object" && v !== null && !Array.isArray(v);
1131
1531
  }
1132
1532
  function colorFromFillEntry(entry, vars) {
1133
1533
  if (typeof entry === "string") return vars.resolve(entry);
1134
- if (isRecord$1(entry)) {
1534
+ if (isRecord$3(entry)) {
1135
1535
  const c = entry["color"];
1136
1536
  if (typeof c === "string") return vars.resolve(c);
1137
1537
  }
1138
1538
  }
1539
+ /** 平均色:网格渐变的底色(解析不了就回落首个色) */
1540
+ function averageColor(colors) {
1541
+ let r = 0;
1542
+ let g = 0;
1543
+ let b = 0;
1544
+ let n = 0;
1545
+ for (const c of colors) {
1546
+ const hex = /^#((?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}))$/.exec(c.trim());
1547
+ if (hex === null) continue;
1548
+ let raw = hex[1];
1549
+ if (raw.length === 3) raw = raw.split("").map((ch) => ch + ch).join("");
1550
+ r += parseInt(raw.slice(0, 2), 16);
1551
+ g += parseInt(raw.slice(2, 4), 16);
1552
+ b += parseInt(raw.slice(4, 6), 16);
1553
+ n += 1;
1554
+ }
1555
+ if (n === 0) return colors[0] ?? "#000000";
1556
+ return `rgb(${Math.round(r / n)} ${Math.round(g / n)} ${Math.round(b / n)})`;
1557
+ }
1558
+ /**
1559
+ * mesh_gradient 近似:每个网格点一层径向渐变(该点颜色 → 透明),
1560
+ * 底下垫一层平均色。浏览器按预乘 alpha 插值,叠出来就是平滑的网格混色。
1561
+ * `columns × rows` 缺省、颜色数不足或网格过大时返回 null(调用方按首个顶点色降级)。
1562
+ */
1563
+ function meshLayers(entry, vars) {
1564
+ const cols = vars.resolveNum(entry["columns"]);
1565
+ const rows = vars.resolveNum(entry["rows"]);
1566
+ const raw = entry["colors"];
1567
+ if (cols === void 0 || rows === void 0 || !Array.isArray(raw)) return null;
1568
+ const c = Math.round(cols);
1569
+ const r = Math.round(rows);
1570
+ if (c < 1 || r < 1) return null;
1571
+ const colors = raw.map((x) => colorFromFillEntry(x, vars));
1572
+ if (colors.length < c * r || colors.some((x) => x === void 0)) return null;
1573
+ const cells = c * r;
1574
+ if (cells > 144) return null;
1575
+ const images = [];
1576
+ for (let row = 0; row < r; row++) for (let col = 0; col < c; col++) {
1577
+ const color = colors[row * c + col];
1578
+ const cx = (col + .5) / c * 100;
1579
+ const cy = (row + .5) / r * 100;
1580
+ const sx = 100 / c * 1.4;
1581
+ const sy = 100 / r * 1.4;
1582
+ images.push(`radial-gradient(ellipse ${sx.toFixed(2)}% ${sy.toFixed(2)}% at ${cx.toFixed(2)}% ${cy.toFixed(2)}%, ${color} 0%, transparent 72%)`);
1583
+ }
1584
+ return {
1585
+ images,
1586
+ base: averageColor(colors.filter((x) => x !== void 0)),
1587
+ cells
1588
+ };
1589
+ }
1139
1590
  function stopsToCssList(colors, vars) {
1140
1591
  if (!Array.isArray(colors) || colors.length === 0) return void 0;
1141
1592
  const parts = [];
1142
1593
  for (const c of colors) {
1143
- if (!isRecord$1(c)) continue;
1594
+ if (!isRecord$3(c)) continue;
1144
1595
  const col = colorFromFillEntry(c["color"], vars);
1145
1596
  const pos = vars.resolveNum(c["position"]);
1146
1597
  if (col === void 0) continue;
@@ -1162,7 +1613,7 @@ function penAngleToCss(deg) {
1162
1613
  return `${(((90 - deg) % 360 + 360) % 360).toFixed(1)}deg`;
1163
1614
  }
1164
1615
  function normCenter(c, vars) {
1165
- if (isRecord$1(c)) {
1616
+ if (isRecord$3(c)) {
1166
1617
  const rx = vars?.resolveNum(c["x"]) ?? (typeof c["x"] === "number" ? c["x"] : void 0);
1167
1618
  const ry = vars?.resolveNum(c["y"]) ?? (typeof c["y"] === "number" ? c["y"] : void 0);
1168
1619
  if (rx !== void 0 && ry !== void 0) return {
@@ -1181,7 +1632,7 @@ function normCenter(c, vars) {
1181
1632
  * 另一边按正方形处理(与 Pencil 的径向渐变一致)。
1182
1633
  */
1183
1634
  function normSize(size, vars) {
1184
- if (!isRecord$1(size)) return null;
1635
+ if (!isRecord$3(size)) return null;
1185
1636
  const rw = vars.resolveNum(size["width"]);
1186
1637
  const rh = vars.resolveNum(size["height"]);
1187
1638
  if (rw === void 0 && rh === void 0) return null;
@@ -1196,11 +1647,11 @@ function fillsToCss(fillsRaw, vars, warnings, what) {
1196
1647
  const bgImages = [];
1197
1648
  let bgColor;
1198
1649
  for (const entry of entries) {
1199
- if (typeof entry === "string" || isRecord$1(entry) && entry["type"] === void 0) {
1650
+ if (typeof entry === "string" || isRecord$3(entry) && entry["type"] === void 0) {
1200
1651
  bgColor = colorFromFillEntry(entry, vars);
1201
1652
  continue;
1202
1653
  }
1203
- if (!isRecord$1(entry)) continue;
1654
+ if (!isRecord$3(entry)) continue;
1204
1655
  if (vars.resolveBool(entry["enabled"]) === false) continue;
1205
1656
  const kind = entry["type"];
1206
1657
  if (kind === "color") bgColor = colorFromFillEntry(entry, vars);
@@ -1231,11 +1682,17 @@ function fillsToCss(fillsRaw, vars, warnings, what) {
1231
1682
  decls.push(["background-repeat", "no-repeat"]);
1232
1683
  } else warnings.push(`${what}: 图像填充缺少 url,已跳过`);
1233
1684
  } else if (kind === "mesh_gradient") {
1234
- const first = colorFromFillEntry(entry["colors"]?.[0], vars);
1235
- if (first) {
1236
- bgColor = first;
1237
- warnings.push(`${what}: mesh_gradient 降级为首个顶点色`);
1238
- } else warnings.push(`${what}: mesh_gradient 无法降级,已跳过`);
1685
+ const mesh = meshLayers(entry, vars);
1686
+ if (mesh !== null) {
1687
+ bgImages.push(...mesh.images);
1688
+ if (bgColor === void 0) bgColor = mesh.base;
1689
+ } else {
1690
+ const first = colorFromFillEntry(entry["colors"]?.[0], vars);
1691
+ if (first) {
1692
+ bgColor = first;
1693
+ warnings.push(`${what}: mesh_gradient 网格不完整(或超过 144 格),降级为首个顶点色`);
1694
+ } else warnings.push(`${what}: mesh_gradient 无法降级,已跳过`);
1695
+ }
1239
1696
  } else if (kind === "shader") warnings.push(`${what}: shader 填充暂不支持渲染,已跳过`);
1240
1697
  }
1241
1698
  if (bgImages.length > 0) decls.unshift(["background-image", bgImages.join(", ")]);
@@ -1256,7 +1713,7 @@ function fillsToCss(fillsRaw, vars, warnings, what) {
1256
1713
  function strokeToCss(node, vars, warnings, what) {
1257
1714
  const stroke = node["stroke"];
1258
1715
  if (stroke === void 0) return [];
1259
- const enabled = (Array.isArray(stroke) ? stroke : [stroke]).filter((e) => !(isRecord$1(e) && e["enabled"] === false));
1716
+ const enabled = (Array.isArray(stroke) ? stroke : [stroke]).filter((e) => !(isRecord$3(e) && e["enabled"] === false));
1260
1717
  if (enabled.length === 0) return [];
1261
1718
  const firstColor = enabled.map((e) => colorFromFillEntry(e, vars)).find((c) => c !== void 0);
1262
1719
  if (firstColor === void 0) return [];
@@ -1265,8 +1722,8 @@ function strokeToCss(node, vars, warnings, what) {
1265
1722
  const alignRaw = node["strokeAlignment"];
1266
1723
  const align = alignRaw === "outer" || alignRaw === "center" ? alignRaw : "inner";
1267
1724
  const widthRaw = node["strokeWidth"];
1268
- if (isRecord$1(widthRaw) && align !== "inner") warnings.push(`${what}: 逐边描边宽度 + strokeAlignment=${align} 暂按 inner(CSS border)渲染`);
1269
- if (!isRecord$1(widthRaw) && align !== "inner") {
1725
+ if (isRecord$3(widthRaw) && align !== "inner") warnings.push(`${what}: 逐边描边宽度 + strokeAlignment=${align} 暂按 inner(CSS border)渲染`);
1726
+ if (!isRecord$3(widthRaw) && align !== "inner") {
1270
1727
  const w = vars.resolveNum(widthRaw) ?? 1;
1271
1728
  decls.push(["outline", `${w}px solid ${firstColor}`]);
1272
1729
  decls.push(["outline-offset", align === "outer" ? "0" : `${-w / 2}px`]);
@@ -1276,7 +1733,7 @@ function strokeToCss(node, vars, warnings, what) {
1276
1733
  if (typeof widthRaw === "number") {
1277
1734
  decls.push(["border-width", `${widthRaw}px`]);
1278
1735
  decls.push(["border-style", "solid"]);
1279
- } else if (isRecord$1(widthRaw)) {
1736
+ } else if (isRecord$3(widthRaw)) {
1280
1737
  const sides = [];
1281
1738
  for (const [k, cssName] of [
1282
1739
  ["top", "border-top"],
@@ -1306,10 +1763,10 @@ function effectsToCss(effectsRaw, vars, warnings, what) {
1306
1763
  let blurPx;
1307
1764
  let bgBlurPx;
1308
1765
  for (const e of entries) {
1309
- if (!isRecord$1(e)) continue;
1766
+ if (!isRecord$3(e)) continue;
1310
1767
  if (e["type"] === "shadow") {
1311
1768
  const inner = e["shadowType"] === "inner";
1312
- const off = isRecord$1(e["offset"]) ? e["offset"] : {};
1769
+ const off = isRecord$3(e["offset"]) ? e["offset"] : {};
1313
1770
  const x = vars.resolveNum(off["x"]) ?? 0;
1314
1771
  const y = vars.resolveNum(off["y"]) ?? 0;
1315
1772
  const b = vars.resolveNum(e["blur"]) ?? 0;
@@ -1325,12 +1782,12 @@ function effectsToCss(effectsRaw, vars, warnings, what) {
1325
1782
  return decls;
1326
1783
  }
1327
1784
  /** 文本样式;文本颜色来自 fill(实文件如此:text 节点 fill 即字色) */
1328
- function textToCss(node, vars, warnings) {
1785
+ function textToCss(node, vars, warnings, fontPreviews) {
1329
1786
  const decls = [];
1330
1787
  const num = (k) => vars.resolveNum(node[k]);
1331
1788
  const str = (k) => typeof node[k] === "string" ? vars.resolve(node[k]) : void 0;
1332
1789
  const ff = str("fontFamily");
1333
- if (ff) decls.push(["font-family", ff]);
1790
+ if (ff) decls.push(["font-family", fontPreviews === void 0 ? ff : stackWithPreviews(ff, fontPreviews)]);
1334
1791
  const fs = num("fontSize");
1335
1792
  if (fs !== void 0) decls.push(["font-size", `${fs}px`]);
1336
1793
  const fw = str("fontWeight");
@@ -1507,6 +1964,198 @@ function resolveIconName(library, name) {
1507
1964
  if (direct !== void 0) return direct;
1508
1965
  return name;
1509
1966
  }
1967
+ // src/render/geometry.ts
1968
+ /**
1969
+ * 圆弧 / 圆环 / 圆角多边形的几何(C1、C2)。
1970
+ *
1971
+ * 只产出**路径字符串**,坐标系是画板像素(viewBox = `0 0 w h`),
1972
+ * 这样描边宽度不会被非等比缩放拉变形。尺寸拿不到数值时调用方退回旧渲染(+ 警告),
1973
+ * 不在几何层猜尺寸。
1974
+ *
1975
+ * 角度约定(format/spec.yaml §nodes.ellipse):0° 在正右、逆时针为正、sweep 正逆负顺;
1976
+ * 而 SVG 的 y 轴向下、sweep-flag=1 是顺时针,所以正向扇形的 flag 是 0。
1977
+ */
1978
+ function isRecord$2(v) {
1979
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1980
+ }
1981
+ /** 角度 → 椭圆上的点(度,0° 正右,逆时针为正;渲染坐标系 y 向下) */
1982
+ function ellipsePoint(cx, cy, rx, ry, deg) {
1983
+ const a = deg * Math.PI / 180;
1984
+ return {
1985
+ x: cx + rx * Math.cos(a),
1986
+ y: cy - ry * Math.sin(a)
1987
+ };
1988
+ }
1989
+ function fmt(n) {
1990
+ return String(Math.round(n * 1e3) / 1e3);
1991
+ }
1992
+ function arcTo(rx, ry, large, ccw, p) {
1993
+ return `A ${fmt(rx)} ${fmt(ry)} 0 ${large ? 1 : 0} ${ccw ? 0 : 1} ${fmt(p.x)} ${fmt(p.y)}`;
1994
+ }
1995
+ /** 是否需要走 SVG(整圆实心仍旧用 div 渲染,保证既有产物像素不变) */
1996
+ function needsEllipseSvg(input) {
1997
+ const sweep = input.sweepAngle ?? 360;
1998
+ return (input.innerRadius ?? 0) > 0 || Math.abs(sweep) < 360;
1999
+ }
2000
+ /**
2001
+ * 椭圆形状的 SVG path(px 坐标)。
2002
+ * - inner=0 + 整圆:不在本函数处理(调用方用 div)
2003
+ * - 整圆 + inner>0:外圆 + 内圆(evenodd)
2004
+ * - 部分弧:扇形(inner=0)或扇环(inner>0)
2005
+ */
2006
+ function ellipsePath(input) {
2007
+ const w = Math.max(1, input.width);
2008
+ const h = Math.max(1, input.height);
2009
+ const cx = w / 2;
2010
+ const cy = h / 2;
2011
+ const rx = w / 2;
2012
+ const ry = h / 2;
2013
+ const start = input.startAngle ?? 0;
2014
+ const sweepRaw = input.sweepAngle ?? 360;
2015
+ const sweep = Math.max(-360, Math.min(360, sweepRaw));
2016
+ const inner = Math.max(0, Math.min(1, input.innerRadius ?? 0));
2017
+ const full = Math.abs(sweep) >= 360;
2018
+ if (sweep === 0) return "";
2019
+ if (full) {
2020
+ const rix = rx * inner;
2021
+ const riy = ry * inner;
2022
+ if (inner <= 0) return `M ${fmt(cx - rx)} ${fmt(cy)} ${arcTo(rx, ry, false, true, {
2023
+ x: cx + rx,
2024
+ y: cy
2025
+ })} ${arcTo(rx, ry, false, true, {
2026
+ x: cx - rx,
2027
+ y: cy
2028
+ })} Z`;
2029
+ return `${`M ${fmt(cx - rx)} ${fmt(cy)} ${arcTo(rx, ry, false, true, {
2030
+ x: cx + rx,
2031
+ y: cy
2032
+ })} ${arcTo(rx, ry, false, true, {
2033
+ x: cx - rx,
2034
+ y: cy
2035
+ })} Z`} ${`M ${fmt(cx - rix)} ${fmt(cy)} ${arcTo(rix, riy, false, true, {
2036
+ x: cx + rix,
2037
+ y: cy
2038
+ })} ${arcTo(rix, riy, false, true, {
2039
+ x: cx - rix,
2040
+ y: cy
2041
+ })} Z`}`;
2042
+ }
2043
+ const a0 = start;
2044
+ const a1 = start + sweep;
2045
+ const large = Math.abs(sweep) > 180;
2046
+ const ccw = sweep > 0;
2047
+ const p0 = ellipsePoint(cx, cy, rx, ry, a0);
2048
+ const p1 = ellipsePoint(cx, cy, rx, ry, a1);
2049
+ if (inner <= 0) return `M ${fmt(cx)} ${fmt(cy)} L ${fmt(p0.x)} ${fmt(p0.y)} ${arcTo(rx, ry, large, ccw, p1)} Z`;
2050
+ const rix = rx * inner;
2051
+ const riy = ry * inner;
2052
+ const q0 = ellipsePoint(cx, cy, rix, riy, a0);
2053
+ const q1 = ellipsePoint(cx, cy, rix, riy, a1);
2054
+ return [
2055
+ `M ${fmt(p0.x)} ${fmt(p0.y)}`,
2056
+ arcTo(rx, ry, large, ccw, p1),
2057
+ `L ${fmt(q1.x)} ${fmt(q1.y)}`,
2058
+ arcTo(rix, riy, large, !ccw, q0),
2059
+ "Z"
2060
+ ].join(" ");
2061
+ }
2062
+ /** 多边形顶点:−90° 起、按屏幕顺时针排,和旧 clip-path 的位置一致 */
2063
+ function polygonPoints(input) {
2064
+ const w = Math.max(1, input.width);
2065
+ const h = Math.max(1, input.height);
2066
+ const n = Math.max(3, Math.round(input.count ?? 3));
2067
+ const cx = w / 2;
2068
+ const cy = h / 2;
2069
+ const rx = w / 2;
2070
+ const ry = h / 2;
2071
+ const pts = [];
2072
+ for (let i = 0; i < n; i++) {
2073
+ const a = (-90 + 360 / n * i) * Math.PI / 180;
2074
+ pts.push({
2075
+ x: cx + rx * Math.cos(a),
2076
+ y: cy + ry * Math.sin(a)
2077
+ });
2078
+ }
2079
+ return pts;
2080
+ }
2081
+ /**
2082
+ * 圆角多边形:每个顶点按内切圆半径 r 退让 d = r / tan(内角/2),
2083
+ * 用二次贝塞尔过顶点圆角。d 被限制在相邻边长的 45% 以内,避免小边被吃穿。
2084
+ */
2085
+ function polygonPath(input) {
2086
+ const pts = polygonPoints(input);
2087
+ const n = pts.length;
2088
+ const r = Math.max(0, input.cornerRadius ?? 0);
2089
+ if (r <= 0) return `M ${pts.map((p) => `${fmt(p.x)} ${fmt(p.y)}`).join(" L ")} Z`;
2090
+ const parts = [];
2091
+ for (let i = 0; i < n; i++) {
2092
+ const prev = pts[(i - 1 + n) % n];
2093
+ const cur = pts[i];
2094
+ const next = pts[(i + 1) % n];
2095
+ const v0 = {
2096
+ x: prev.x - cur.x,
2097
+ y: prev.y - cur.y
2098
+ };
2099
+ const v1 = {
2100
+ x: next.x - cur.x,
2101
+ y: next.y - cur.y
2102
+ };
2103
+ const len0 = Math.hypot(v0.x, v0.y) || 1;
2104
+ const len1 = Math.hypot(v1.x, v1.y) || 1;
2105
+ const cos = Math.max(-1, Math.min(1, (v0.x * v1.x + v0.y * v1.y) / (len0 * len1)));
2106
+ const interior = Math.acos(cos);
2107
+ const dWant = interior > 1e-6 ? r / Math.tan(interior / 2) : Number.POSITIVE_INFINITY;
2108
+ const d = Math.min(dWant, len0 * .45, len1 * .45);
2109
+ const a = {
2110
+ x: cur.x + v0.x / len0 * d,
2111
+ y: cur.y + v0.y / len0 * d
2112
+ };
2113
+ const b = {
2114
+ x: cur.x + v1.x / len1 * d,
2115
+ y: cur.y + v1.y / len1 * d
2116
+ };
2117
+ parts.push(i === 0 ? `M ${fmt(a.x)} ${fmt(a.y)}` : `L ${fmt(a.x)} ${fmt(a.y)}`);
2118
+ parts.push(`Q ${fmt(cur.x)} ${fmt(cur.y)} ${fmt(b.x)} ${fmt(b.y)}`);
2119
+ }
2120
+ parts.push("Z");
2121
+ return parts.join(" ");
2122
+ }
2123
+ /** 顶层实色(填充列表里最后一层生效的 color);有渐变/图片/关闭层里夹着色时返回只认纯色的那一层 */
2124
+ function solidFillColor(fill, vars) {
2125
+ const entries = Array.isArray(fill) ? fill : fill === void 0 ? [] : [fill];
2126
+ let color = null;
2127
+ for (const entry of entries) {
2128
+ if (typeof entry === "string") {
2129
+ color = vars.resolve(entry);
2130
+ continue;
2131
+ }
2132
+ if (!isRecord$2(entry)) continue;
2133
+ if (vars.resolveBool(entry["enabled"]) === false) continue;
2134
+ if (entry["type"] !== "color") return null;
2135
+ const c = entry["color"];
2136
+ if (typeof c === "string") color = vars.resolve(c);
2137
+ }
2138
+ return color;
2139
+ }
2140
+ /** 单层、单色、非逐边的描边(否则返回 null,调用方降级) */
2141
+ function simpleStroke(node, vars) {
2142
+ const raw = node["stroke"];
2143
+ if (raw === void 0) return null;
2144
+ const enabled = (Array.isArray(raw) ? raw : [raw]).filter((e) => !(isRecord$2(e) && vars.resolveBool(e["enabled"]) === false));
2145
+ if (enabled.length !== 1) return null;
2146
+ const entry = enabled[0];
2147
+ const widthRaw = node["strokeWidth"];
2148
+ if (isRecord$2(widthRaw)) return null;
2149
+ let color = null;
2150
+ if (typeof entry === "string") color = vars.resolve(entry);
2151
+ else if (isRecord$2(entry) && entry["type"] === "color" && typeof entry["color"] === "string") color = vars.resolve(entry["color"]);
2152
+ if (color === null) return null;
2153
+ const width = vars.resolveNum(widthRaw) ?? 1;
2154
+ return {
2155
+ color,
2156
+ width
2157
+ };
2158
+ }
1510
2159
  // src/render/html.ts
1511
2160
  function escapeHtml(s) {
1512
2161
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1514,7 +2163,7 @@ function escapeHtml(s) {
1514
2163
  function escapeAttr(s) {
1515
2164
  return escapeHtml(s).replace(/"/g, "&quot;");
1516
2165
  }
1517
- function isRecord(v) {
2166
+ function isRecord$1(v) {
1518
2167
  return typeof v === "object" && v !== null && !Array.isArray(v);
1519
2168
  }
1520
2169
  /** 'fit_content(120)' → 120(sizing 字符串内的回退值) */
@@ -1661,9 +2310,75 @@ function boardAttrs(node, vars) {
1661
2310
  `data-board-h="${numStr(box.h)}"`
1662
2311
  ];
1663
2312
  }
1664
- function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
2313
+ function planSvgShape(node, type, vars) {
2314
+ if (type === "ellipse") {
2315
+ const sweep = vars.resolveNum(node["sweepAngle"]);
2316
+ const inner = vars.resolveNum(node["innerRadius"]);
2317
+ const start = vars.resolveNum(node["startAngle"]);
2318
+ if (!needsEllipseSvg({
2319
+ width: 1,
2320
+ height: 1,
2321
+ ...start !== void 0 ? { startAngle: start } : {},
2322
+ ...sweep !== void 0 ? { sweepAngle: sweep } : {},
2323
+ ...inner !== void 0 ? { innerRadius: inner } : {}
2324
+ })) return null;
2325
+ const w = numericSize(node["width"], vars);
2326
+ const h = numericSize(node["height"], vars);
2327
+ if (w === void 0 || h === void 0) return { reason: "弧形/圆环需要显式宽高,暂按整圆渲染" };
2328
+ const rawFill = node["fill"];
2329
+ const fill = rawFill === void 0 ? null : solidFillColor(rawFill, vars);
2330
+ if (rawFill !== void 0 && fill === null) return { reason: "弧形/圆环只支持纯色填充,暂按整圆/实心渲染" };
2331
+ const stroke = simpleStroke(node, vars);
2332
+ return { shape: {
2333
+ path: ellipsePath({
2334
+ width: w,
2335
+ height: h,
2336
+ ...start !== void 0 ? { startAngle: start } : {},
2337
+ ...sweep !== void 0 ? { sweepAngle: sweep } : {},
2338
+ ...inner !== void 0 ? { innerRadius: inner } : {}
2339
+ }),
2340
+ width: w,
2341
+ height: h,
2342
+ fill,
2343
+ stroke,
2344
+ evenodd: (inner ?? 0) > 0
2345
+ } };
2346
+ }
2347
+ if (type === "polygon") {
2348
+ const radius = vars.resolveNum(node["cornerRadius"]);
2349
+ if (radius === void 0 || radius <= 0) return null;
2350
+ const w = numericSize(node["width"], vars);
2351
+ const h = numericSize(node["height"], vars);
2352
+ if (w === void 0 || h === void 0) return { reason: "多边形圆角需要显式宽高,暂忽略圆角" };
2353
+ const rawFill = node["fill"];
2354
+ const fill = rawFill === void 0 ? null : solidFillColor(rawFill, vars);
2355
+ if (rawFill !== void 0 && fill === null) return { reason: "多边形圆角只支持纯色填充,暂忽略圆角" };
2356
+ const count = vars.resolveNum(node["polygonCount"]);
2357
+ return { shape: {
2358
+ path: polygonPath({
2359
+ width: w,
2360
+ height: h,
2361
+ ...count !== void 0 ? { count } : {},
2362
+ cornerRadius: radius
2363
+ }),
2364
+ width: w,
2365
+ height: h,
2366
+ fill,
2367
+ stroke: simpleStroke(node, vars),
2368
+ evenodd: false
2369
+ } };
2370
+ }
2371
+ return null;
2372
+ }
2373
+ function svgShapeHtml(shape) {
2374
+ const fill = shape.fill === null ? " fill=\"none\"" : ` fill="${escapeAttr(shape.fill)}"`;
2375
+ const rule = shape.evenodd ? " fill-rule=\"evenodd\"" : "";
2376
+ const stroke = shape.stroke === null ? "" : ` stroke="${escapeAttr(shape.stroke.color)}" stroke-width="${numStr(shape.stroke.width)}"`;
2377
+ return `<svg viewBox="0 0 ${numStr(shape.width)} ${numStr(shape.height)}" width="100%" height="100%" preserveAspectRatio="none" style="display:block;width:100%;height:100%"><path d="${escapeAttr(shape.path)}"${fill}${rule}${stroke}/></svg>`;
2378
+ }
2379
+ function renderNode(node, parentLayout, vars, warnings, extraAttrs = [], fonts) {
1665
2380
  const type = String(node["type"] ?? "?");
1666
- const scoped = isRecord(node["theme"]) ? vars.withTheme(node["theme"]) : vars;
2381
+ const scoped = isRecord$1(node["theme"]) ? vars.withTheme(node["theme"]) : vars;
1667
2382
  if (scoped.resolveBool(node["enabled"]) === false) return "";
1668
2383
  const label = typeof node["name"] === "string" && node["name"] !== "" ? `${type}: ${String(node["name"])}` : type;
1669
2384
  const positioned = parentLayout === "none" || node["layoutPosition"] === "absolute";
@@ -1680,13 +2395,15 @@ function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
1680
2395
  if (parentLayout !== "none" && !positioned) css.push(["flex-shrink", "0"]);
1681
2396
  css.push(...sizeDecls(node["width"], node["height"], positioned, parentLayout, scoped, warnings, label));
1682
2397
  css.push(...commonToCss(node, scoped));
1683
- if (type !== "text") css.push(...fillsToCss(node["fill"], scoped, warnings, label));
1684
- css.push(...strokeToCss(node, scoped, warnings, label));
2398
+ const plan = planSvgShape(node, type, scoped);
2399
+ const svgShape = plan !== null && "shape" in plan ? plan.shape : null;
2400
+ if (type !== "text" && svgShape === null) css.push(...fillsToCss(node["fill"], scoped, warnings, label));
2401
+ if (svgShape === null) css.push(...strokeToCss(node, scoped, warnings, label));
1685
2402
  css.push(...effectsToCss(node["effect"], scoped, warnings, label));
1686
2403
  let inner = "";
1687
2404
  switch (type) {
1688
2405
  case "text":
1689
- css.push(...textToCss(node, scoped, warnings));
2406
+ css.push(...textToCss(node, scoped, warnings, fonts?.previews));
1690
2407
  inner = escapeHtml(typeof node["content"] === "string" ? scoped.resolve(node["content"]) : "");
1691
2408
  break;
1692
2409
  case "icon":
@@ -1710,19 +2427,28 @@ function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
1710
2427
  inner = escapeHtml(typeof node["content"] === "string" ? scoped.resolve(node["content"]) : "");
1711
2428
  break;
1712
2429
  }
1713
- case "ellipse": {
2430
+ case "ellipse":
2431
+ if (svgShape !== null) {
2432
+ inner = svgShapeHtml(svgShape);
2433
+ if (scoped.resolveNum(node["sweepAngle"]) === 0) warnings.push(`${label}: sweepAngle=0,空弧不绘制`);
2434
+ const alignment = node["strokeAlignment"];
2435
+ if (svgShape.stroke !== null && (alignment === "inner" || alignment === "outer")) warnings.push(`${label}: strokeAlignment=${alignment} 在 SVG 形状上暂按 center 渲染`);
2436
+ break;
2437
+ }
2438
+ if (plan !== null && "reason" in plan) warnings.push(`${label}: ${plan.reason}`);
1714
2439
  css.push(["border-radius", "50%"]);
1715
- const inner2 = scoped.resolveNum(node["innerRadius"]);
1716
- if (inner2 !== void 0 && inner2 > 0) warnings.push(`${label}: innerRadius 环形暂按实心椭圆渲染`);
1717
- const sweep = scoped.resolveNum(node["sweepAngle"]);
1718
- if (sweep !== void 0 && sweep !== 360) warnings.push(`${label}: 弧形(sweepAngle=${sweep})暂按整圆渲染`);
1719
2440
  break;
1720
- }
1721
2441
  case "polygon": {
2442
+ if (svgShape !== null) {
2443
+ inner = svgShapeHtml(svgShape);
2444
+ const alignment = node["strokeAlignment"];
2445
+ if (svgShape.stroke !== null && (alignment === "inner" || alignment === "outer")) warnings.push(`${label}: strokeAlignment=${alignment} 在 SVG 形状上暂按 center 渲染`);
2446
+ break;
2447
+ }
1722
2448
  const count = scoped.resolveNum(node["polygonCount"]);
1723
2449
  const n = count ?? 3;
1724
2450
  if (count === void 0) warnings.push(`${label}: polygonCount 缺省按 3 渲染`);
1725
- if (node["cornerRadius"] !== void 0) warnings.push(`${label}: 多边形圆角暂忽略`);
2451
+ if (plan !== null && "reason" in plan) warnings.push(`${label}: ${plan.reason}`);
1726
2452
  css.push(["clip-path", polygonClip(n)]);
1727
2453
  break;
1728
2454
  }
@@ -1744,7 +2470,7 @@ function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
1744
2470
  if (type === "group" && node["effect"] !== void 0) {}
1745
2471
  }
1746
2472
  const kidsRaw = node["children"];
1747
- const childrenHtml = Array.isArray(kidsRaw) ? kidsRaw.filter(isRecord).map((k) => renderNode(k, effective, scoped, warnings)).join("") : "";
2473
+ const childrenHtml = Array.isArray(kidsRaw) ? kidsRaw.filter(isRecord$1).map((k) => renderNode(k, effective, scoped, warnings, [], fonts)).join("") : "";
1748
2474
  return openTag(node, css.map(([p, v]) => `${p}:${v}`).join(";"), extraAttrs) + inner + childrenHtml + "</div>";
1749
2475
  }
1750
2476
  const KNOWN_RENDER_TYPES = /* @__PURE__ */ new Set([
@@ -1766,7 +2492,7 @@ function computeCanvas(nodes, vars) {
1766
2492
  let maxW = 0;
1767
2493
  let maxH = 0;
1768
2494
  for (const n of nodes) {
1769
- if (!isRecord(n)) continue;
2495
+ if (!isRecord$1(n)) continue;
1770
2496
  const box = nodeBox(n, vars);
1771
2497
  maxW = Math.max(maxW, box.x + box.w);
1772
2498
  maxH = Math.max(maxH, box.y + box.h);
@@ -1779,7 +2505,7 @@ function computeCanvas(nodes, vars) {
1779
2505
  function boardBoxes(nodes, vars) {
1780
2506
  const out = [];
1781
2507
  for (const n of nodes) {
1782
- if (!isRecord(n)) continue;
2508
+ if (!isRecord$1(n)) continue;
1783
2509
  const box = nodeBox(n, vars);
1784
2510
  const name = typeof n["name"] === "string" && n["name"] !== "" ? n["name"] : void 0;
1785
2511
  out.push({
@@ -1794,12 +2520,93 @@ function boardBoxes(nodes, vars) {
1794
2520
  return out;
1795
2521
  }
1796
2522
  /** 渲染整棵(已展开的)树:顶层节点在画布上绝对定位,并带画布元数据 */
1797
- function renderTree(nodes, vars, warnings) {
1798
- return nodes.filter(isRecord).map((n) => {
1799
- return renderNode(n, "none", vars, warnings, boardAttrs(n, isRecord(n["theme"]) ? vars.withTheme(n["theme"]) : vars));
2523
+ function renderTree(nodes, vars, warnings, fonts) {
2524
+ return nodes.filter(isRecord$1).map((n) => {
2525
+ return renderNode(n, "none", vars, warnings, boardAttrs(n, isRecord$1(n["theme"]) ? vars.withTheme(n["theme"]) : vars), fonts);
1800
2526
  }).join("");
1801
2527
  }
1802
2528
  // src/render/entry.ts
2529
+ function metricsCss(metrics) {
2530
+ if (metrics === void 0) return "";
2531
+ const parts = [];
2532
+ if (metrics.sizeAdjust !== void 0) parts.push(`size-adjust:${metrics.sizeAdjust}`);
2533
+ if (metrics.ascentOverride !== void 0) parts.push(`ascent-override:${metrics.ascentOverride}`);
2534
+ if (metrics.descentOverride !== void 0) parts.push(`descent-override:${metrics.descentOverride}`);
2535
+ if (metrics.lineGapOverride !== void 0) parts.push(`line-gap-override:${metrics.lineGapOverride}`);
2536
+ return parts.length > 0 ? `${parts.join(";")};` : "";
2537
+ }
2538
+ function faceCss(face) {
2539
+ const src = face.src.startsWith("local(") ? face.src : `url("${face.src}")`;
2540
+ const format = face.format !== void 0 ? ` format("${face.format}")` : "";
2541
+ return `@font-face{font-family:${JSON.stringify(face.family)};src:${src}${format};${metricsCss(face.metrics)}}`;
2542
+ }
2543
+ function isRecord(v) {
2544
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2545
+ }
2546
+ /** 探针要检查的 family:注册表里的、默认栈里的、文档里写死的 */
2547
+ function collectProbeFamilies(nodes, plan) {
2548
+ const set = /* @__PURE__ */ new Set();
2549
+ for (const family of Object.keys(plan?.previews ?? {})) set.add(family);
2550
+ for (const family of splitFontStack(plan?.defaultStack ?? "\"Inter Variable\", \"Noto Sans SC Variable\", system-ui, -apple-system, \"Segoe UI\", sans-serif")) if (!isGenericFamily(family)) set.add(family);
2551
+ const walk = (list) => {
2552
+ for (const raw of list) {
2553
+ if (!isRecord(raw)) continue;
2554
+ if (raw["type"] === "text" && typeof raw["fontFamily"] === "string" && !raw["fontFamily"].startsWith("$")) {
2555
+ for (const family of splitFontStack(raw["fontFamily"])) if (!isGenericFamily(family)) set.add(family);
2556
+ }
2557
+ const kids = raw["children"];
2558
+ if (Array.isArray(kids)) walk(kids);
2559
+ }
2560
+ };
2561
+ walk(nodes);
2562
+ return [...set];
2563
+ }
2564
+ /**
2565
+ * 字体 CSS 异步加载:外链样式表会阻塞 load(测试与首屏都不该等 CDN),
2566
+ * 于是用脚本注入;`window.__paintressFontsReady` 给自己人(探针 / 截图)一个等待点。
2567
+ */
2568
+ function fontLoaderScript(links) {
2569
+ if (links.length === 0) return "";
2570
+ return [
2571
+ "<script>",
2572
+ "(function(){",
2573
+ `var hrefs=${JSON.stringify(links).replace(/</g, "\\u003c")};`,
2574
+ "var loads=hrefs.map(function(href){return new Promise(function(resolve){",
2575
+ "var l=document.createElement(\"link\");l.rel=\"stylesheet\";l.href=href;l.onload=resolve;l.onerror=resolve;document.head.appendChild(l);",
2576
+ "});});",
2577
+ "window.__paintressFontsReady=Promise.all(loads).then(function(){",
2578
+ "return document.fonts&&document.fonts.ready?document.fonts.ready:null;});",
2579
+ "})();",
2580
+ "<\/script>"
2581
+ ].join("\n");
2582
+ }
2583
+ /**
2584
+ * 字体可用性探针(宽度法):`document.fonts.check()` 对缺失的系统字体也返回 true,
2585
+ * 只有「候选 family 与缺失基线的宽度是否相同」能区分。
2586
+ */
2587
+ function probeScript(families) {
2588
+ if (families.length === 0) return "";
2589
+ return [
2590
+ "<script type=\"application/json\" id=\"ptr-fonts\">",
2591
+ JSON.stringify(families).replace(/</g, "\\u003c").replace(/[\u2028\u2029]/g, (ch) => "\\u" + ch.charCodeAt(0).toString(16)),
2592
+ "<\/script>",
2593
+ "<script>",
2594
+ "(function(){",
2595
+ "var el=document.getElementById(\"ptr-fonts\");if(!el||window.parent===window)return;",
2596
+ "var families=[];try{families=JSON.parse(el.textContent||\"[]\")}catch(e){return}",
2597
+ "if(!families.length)return;",
2598
+ "var c=document.createElement(\"canvas\").getContext(\"2d\");",
2599
+ "var text=\"漢字Abc123 光伴\";",
2600
+ "function w(f,b){c.font='32px \"'+f+'\", '+b;return c.measureText(text).width}",
2601
+ "function missing(){var sb=w(\"__paintress_missing__\",\"serif\"),mb=w(\"__paintress_missing__\",\"monospace\");",
2602
+ "var out=[];for(var i=0;i<families.length;i++){var f=families[i];",
2603
+ "if(Math.abs(w(f,\"serif\")-sb)<0.5&&Math.abs(w(f,\"monospace\")-mb)<0.5)out.push(f)}return out}",
2604
+ "function send(){var m=missing();var data={type:\"paintress-fonts\",checked:families,missing:m};window.parent.postMessage(data,\"*\");if(window.PAINTRESS_FONT_URL){fetch(window.PAINTRESS_FONT_URL,{method:\"POST\",headers:{\"content-type\":\"application/json\"},body:JSON.stringify({checked:families,missing:m})}).catch(function(){})}}",
2605
+ "Promise.resolve(window.__paintressFontsReady).then(function(){setTimeout(send,50)});",
2606
+ "})();",
2607
+ "<\/script>"
2608
+ ].join("\n");
2609
+ }
1803
2610
  /**
1804
2611
  * 渲染入口(决策 2/3/4/5 的兑现):
1805
2612
  * 变量内联解析 → ref 展开 → 树 → HTML(每节点 data-node-id)。
@@ -1808,11 +2615,23 @@ function renderTree(nodes, vars, warnings) {
1808
2615
  function renderDocument(doc, opts = {}) {
1809
2616
  const warnings = [];
1810
2617
  const vars = makeVarResolver(doc, warnings);
1811
- const expanded = expandRefs(doc, warnings);
1812
- const canvas = computeCanvas(expanded, vars);
1813
- const boards = boardBoxes(expanded, vars);
2618
+ const groups = presetGroupsOf(doc);
2619
+ const seeded = applyPresetRefs(doc.children, groups, warnings);
2620
+ const expanded = expandRefs({
2621
+ ...doc,
2622
+ children: seeded
2623
+ }, warnings);
2624
+ const postWarnings = [];
2625
+ const withPresets = applyPresetRefs(expanded, groups, postWarnings);
2626
+ for (const message of postWarnings) if (!warnings.includes(message)) warnings.push(message);
2627
+ const canvas = computeCanvas(withPresets, vars);
2628
+ const boards = boardBoxes(withPresets, vars);
1814
2629
  const title = opts.title ?? "paintress render";
1815
- const body = renderTree(expanded, vars, warnings);
2630
+ const body = renderTree(withPresets, vars, warnings, { previews: opts.fonts?.previews ?? {} });
2631
+ const links = [...new Set(opts.fonts?.links ?? [])];
2632
+ const defaultStack = stackWithPreviews(opts.fonts?.defaultStack ?? "\"Inter Variable\", \"Noto Sans SC Variable\", system-ui, -apple-system, \"Segoe UI\", sans-serif", opts.fonts?.previews ?? {});
2633
+ const faces = (opts.fonts?.faces ?? []).map(faceCss);
2634
+ const probe = opts.probeFonts === true ? probeScript(collectProbeFamilies(withPresets, opts.fonts)) : "";
1816
2635
  return {
1817
2636
  html: [
1818
2637
  "<!DOCTYPE html>",
@@ -1823,13 +2642,16 @@ function renderDocument(doc, opts = {}) {
1823
2642
  "<style>",
1824
2643
  "* { box-sizing: border-box; margin: 0; padding: 0; }",
1825
2644
  `body { margin: 0; background: ${opts.transparent === true ? "transparent" : "#ffffff"}; }`,
1826
- ".ptr-canvas { position: relative; }",
2645
+ `.ptr-canvas { position: relative; font-family: ${defaultStack}; }`,
2646
+ ...faces,
1827
2647
  "</style>",
1828
2648
  "</head>",
1829
2649
  "<body>",
1830
2650
  `<div class="ptr-canvas" style="width:${canvas.width}px;height:${canvas.height}px">`,
1831
2651
  body,
1832
2652
  "</div>",
2653
+ fontLoaderScript(links),
2654
+ probe,
1833
2655
  "</body>",
1834
2656
  "</html>",
1835
2657
  ""
@@ -1839,4 +2661,4 @@ function renderDocument(doc, opts = {}) {
1839
2661
  boards
1840
2662
  };
1841
2663
  }
1842
- export { RefSchema as $, genId as A, StrokeAlign as At, EllipseSchema as B, CornerRadius as Bt, update as C, GradientType as Ct, serializeDoc as D, MainAxis as Dt, saveDoc as E, LayoutDir as Et, PtrDocumentSchema as F, TextGrowth as Ft, KNOWN_NODE_TYPES as G, VARIABLE_NAME as Gt, GroupSchema as H, Padding as Ht, VariableDef as I, Affine as It, NodeSchema as J, LayoutBlock as K, VariableRef as Kt, XPaintress as L, BoolOrVar as Lt, DocumentSchema as M, StrokeJoin as Mt, PEN_DOC_VERSION as N, TextAlign as Nt, validateDoc as O, PositionMode as Ot, PTR_SPEC_VERSION as P, TextAlignVertical as Pt, RectangleSchema as Q, X_PAINTRESS_KEY as R, Color as Rt, remove as S, FillRule as St, loadDoc as T, ImageFit as Tt, IconFontSchema as U, Sizing as Ut, FrameSchema as V, NumOrVar as Vt, IconSchema as W, StringOrVar as Wt, PathSchema as X, NoteSchema as Y, PolygonSchema as Z, buildIndex as _, SolidFill as _t, escapeHtml as a, BlurEffect as at, move as b, BlendMode as bt, commonToCss as c, FillEntry as ct, fillsToCss as d, GraphicsBlock as dt, ScriptSchema as et, strokeToCss as f, ImageFill as ft, batch as g, ShadowEffect as gt, makeVarResolver as h, ShaderFill as ht, escapeAttr as i, BackgroundBlurEffect as it, DocumentObjectSchema as j, StrokeCap as jt, collectIds as k, ShadowKind as kt, cornerRadiusToCss as l, Fills as lt, expandRefs as m, ShaderEffect as mt, boardBoxes as n, UnknownNodeSchema as nt, renderTree as o, EffectEntry as ot, textToCss as p, MeshGradientFill as pt, NodeBase as q, VERSION as qt, computeCanvas as r, schemaForType as rt, getLucideIcon as s, Effects as st, renderDocument as t, TextSchema as tt, effectsToCss as u, GradientFill as ut, get as v, StrokeBlock as vt, FileChangedError as w, IconLibrary as wt, query as x, CrossAxis as xt, insert as y, StrokeWidth as yt, BrowserSchema as z, ColorOrVar as zt };
2664
+ export { IconFontSchema as $, Sizing as $t, genId as A, BlendMode as At, presetGroupsOf as B, StrokeAlign as Bt, update as C, MeshGradientFill as Ct, serializeDoc as D, SolidFill as Dt, saveDoc as E, ShadowEffect as Et, splitFontStack as F, ImageFit as Ft, PtrDocumentSchema as G, TextGrowth as Gt, DocumentSchema as H, StrokeJoin as Ht, textFontGaps as I, LayoutDir as It, X_PAINTRESS_KEY as J, Color as Jt, VariableDef as K, Affine as Kt, PROP_TOKEN_KIND as L, MainAxis as Lt, isFontSourceKind as M, FillRule as Mt, isGenericFamily as N, GradientType as Nt, validateDoc as O, StrokeBlock as Ot, planFonts as P, IconLibrary as Pt, GroupSchema as Q, Padding as Qt, isNumericPresetValue as R, PositionMode as Rt, remove as S, ImageFill as St, loadDoc as T, ShaderFill as Tt, PEN_DOC_VERSION as U, TextAlign as Ut, DocumentObjectSchema as V, StrokeCap as Vt, PTR_SPEC_VERSION as W, TextAlignVertical as Wt, EllipseSchema as X, CornerRadius as Xt, BrowserSchema as Y, ColorOrVar as Yt, FrameSchema as Z, NumOrVar as Zt, buildIndex as _, Effects as _t, escapeHtml as a, NoteSchema as at, move as b, GradientFill as bt, commonToCss as c, RectangleSchema as ct, fillsToCss as d, TextSchema as dt, StringOrVar as en, IconSchema as et, strokeToCss as f, UnknownNodeSchema as ft, batch as g, EffectEntry as gt, makeVarResolver as h, BlurEffect as ht, escapeAttr as i, NodeSchema as it, fontRegistryOf as j, CrossAxis as jt, collectIds as k, StrokeWidth as kt, cornerRadiusToCss as l, RefSchema as lt, expandRefs as m, BackgroundBlurEffect as mt, boardBoxes as n, VariableRef as nn, LayoutBlock as nt, renderTree as o, PathSchema as ot, textToCss as p, schemaForType as pt, XPaintress as q, BoolOrVar as qt, computeCanvas as r, VERSION as rn, NodeBase as rt, getLucideIcon as s, PolygonSchema as st, renderDocument as t, VARIABLE_NAME as tn, KNOWN_NODE_TYPES as tt, effectsToCss as u, ScriptSchema as ut, get as v, FillEntry as vt, FileChangedError as w, ShaderEffect as wt, query as x, GraphicsBlock as xt, insert as y, Fills as yt, isPresetType as z, ShadowKind as zt };