paintress 0.5.2 → 0.7.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.
@@ -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,8 +912,16 @@ 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
- function validateDoc(input) {
924
+ function validateDoc(input, opts = {}) {
601
925
  const errors = [];
602
926
  const warnings = [];
603
927
  const parsed = DocumentSchema.safeParse(input);
@@ -614,6 +938,11 @@ function validateDoc(input) {
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,12 +972,60 @@ function validateDoc(input) {
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");
651
- const varNames = new Set(doc.variables ? Object.keys(doc.variables) : []);
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
+ }
1022
+ const localNames = new Set(doc.variables ? Object.keys(doc.variables) : []);
1023
+ const varNames = /* @__PURE__ */ new Set([...localNames, ...opts.knownVars ?? []]);
1024
+ for (const name of opts.presetVars ?? []) if (localNames.has(name)) warnings.push({
1025
+ level: "warning",
1026
+ path: "<variables>",
1027
+ message: `本地变量 $${name} 覆盖了项目预设(在查看器里调预设不会影响这份文件)`
1028
+ });
652
1029
  const varRefs = /* @__PURE__ */ new Set();
653
1030
  const scan = (obj) => {
654
1031
  if (typeof obj === "string" && VARIABLE_NAME.test(obj)) varRefs.add(obj.slice(1));
@@ -656,11 +1033,40 @@ function validateDoc(input) {
656
1033
  else if (obj && typeof obj === "object") Object.values(obj).forEach(scan);
657
1034
  };
658
1035
  scan(doc.children);
1036
+ for (const group of Object.values(presetGroups)) for (const value of Object.values(group.props)) scan(value);
659
1037
  for (const name of varRefs) if (!varNames.has(name)) warnings.push({
660
1038
  level: "warning",
661
1039
  path: "<variables>",
662
1040
  message: `字段引用了未定义的变量 $${name}`
663
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
+ }
664
1070
  return {
665
1071
  ok: errors.length === 0,
666
1072
  errors,
@@ -927,7 +1333,7 @@ function batch(doc, ops) {
927
1333
  return clone;
928
1334
  }
929
1335
  // src/render/vars.ts
930
- function isRecord$3(v) {
1336
+ function isRecord$5(v) {
931
1337
  return typeof v === "object" && v !== null && !Array.isArray(v);
932
1338
  }
933
1339
  function asVarValue(v) {
@@ -941,7 +1347,7 @@ function asVarValue(v) {
941
1347
  */
942
1348
  function resolveThemed(v, theme) {
943
1349
  if (Array.isArray(v)) {
944
- const entries = v.filter(isRecord$3);
1350
+ const entries = v.filter(isRecord$5);
945
1351
  if (entries.length === 0) return void 0;
946
1352
  let best = null;
947
1353
  for (const entry of entries) {
@@ -953,7 +1359,7 @@ function resolveThemed(v, theme) {
953
1359
  };
954
1360
  continue;
955
1361
  }
956
- if (!isRecord$3(t)) continue;
1362
+ if (!isRecord$5(t)) continue;
957
1363
  const keys = Object.keys(t);
958
1364
  if (!keys.every((k) => theme[k] === String(t[k]))) continue;
959
1365
  if (best === null || keys.length > best.score) best = {
@@ -970,7 +1376,7 @@ function makeVarResolver(doc, warnings, theme = {}) {
970
1376
  const make = (scope) => {
971
1377
  const lookup = (name) => {
972
1378
  const def = vars[name];
973
- if (!isRecord$3(def)) return void 0;
1379
+ if (!isRecord$5(def)) return void 0;
974
1380
  return resolveThemed(def["value"], scope);
975
1381
  };
976
1382
  return {
@@ -1021,7 +1427,7 @@ function makeVarResolver(doc, warnings, theme = {}) {
1021
1427
  return make(theme);
1022
1428
  }
1023
1429
  // src/render/expand.ts
1024
- function isRecord$2(v) {
1430
+ function isRecord$4(v) {
1025
1431
  return typeof v === "object" && v !== null && !Array.isArray(v);
1026
1432
  }
1027
1433
  function brokenRefPlaceholder(refNode, reason, warnings) {
@@ -1042,7 +1448,7 @@ function indexById(children) {
1042
1448
  const map = /* @__PURE__ */ new Map();
1043
1449
  const walk = (nodes) => {
1044
1450
  for (const n of nodes) {
1045
- if (!isRecord$2(n)) continue;
1451
+ if (!isRecord$4(n)) continue;
1046
1452
  const id = n["id"];
1047
1453
  if (typeof id === "string") map.set(id, n);
1048
1454
  const kids = n["children"];
@@ -1059,7 +1465,7 @@ function findByPath(root, segments) {
1059
1465
  for (const seg of segments) {
1060
1466
  const kids = container["children"];
1061
1467
  if (!Array.isArray(kids)) return null;
1062
- found = kids.find((k) => isRecord$2(k) && k["id"] === seg) ?? null;
1468
+ found = kids.find((k) => isRecord$4(k) && k["id"] === seg) ?? null;
1063
1469
  if (!found) return null;
1064
1470
  container = found;
1065
1471
  }
@@ -1070,7 +1476,7 @@ function findByPath(root, segments) {
1070
1476
  }
1071
1477
  function applyDescendants(expandedRoot, descendants, warnings, refId) {
1072
1478
  for (const [path, overrideRaw] of Object.entries(descendants)) {
1073
- if (!isRecord$2(overrideRaw)) continue;
1479
+ if (!isRecord$4(overrideRaw)) continue;
1074
1480
  const hit = findByPath(expandedRoot, path.split("/").filter((s) => s !== ""));
1075
1481
  if (!hit) {
1076
1482
  warnings.push(`ref ${refId} 的 descendants 路径 "${path}" 未命中,已忽略`);
@@ -1090,7 +1496,7 @@ function applyDescendants(expandedRoot, descendants, warnings, refId) {
1090
1496
  /** 递归展开一个节点(就地修改克隆树) */
1091
1497
  function expandNode(node, index, stack, warnings) {
1092
1498
  const kids = node["children"];
1093
- 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);
1094
1500
  if (node["type"] !== "ref") return node;
1095
1501
  const refId = typeof node["id"] === "string" ? node["id"] : "?";
1096
1502
  const target = typeof node["ref"] === "string" ? node["ref"] : "";
@@ -1102,13 +1508,13 @@ function expandNode(node, index, stack, warnings) {
1102
1508
  const clone = structuredClone(component);
1103
1509
  clone["id"] = node["id"];
1104
1510
  if (typeof node["name"] === "string") clone["name"] = node["name"];
1105
- 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);
1106
1512
  for (const [k, v] of Object.entries(node)) {
1107
1513
  if (k === "type" || k === "ref" || k === "descendants" || k === "id" || k === "name") continue;
1108
1514
  clone[k] = structuredClone(v);
1109
1515
  }
1110
1516
  const desc = node["descendants"];
1111
- if (isRecord$2(desc)) applyDescendants(clone, desc, warnings, refId);
1517
+ if (isRecord$4(desc)) applyDescendants(clone, desc, warnings, refId);
1112
1518
  return clone;
1113
1519
  } finally {
1114
1520
  stack.delete(target);
@@ -1120,21 +1526,72 @@ function expandRefs(doc, warnings) {
1120
1526
  return structuredClone(doc.children).map((n) => expandNode(n, index, /* @__PURE__ */ new Set(), warnings));
1121
1527
  }
1122
1528
  // src/render/style.ts
1123
- function isRecord$1(v) {
1529
+ function isRecord$3(v) {
1124
1530
  return typeof v === "object" && v !== null && !Array.isArray(v);
1125
1531
  }
1126
1532
  function colorFromFillEntry(entry, vars) {
1127
1533
  if (typeof entry === "string") return vars.resolve(entry);
1128
- if (isRecord$1(entry)) {
1534
+ if (isRecord$3(entry)) {
1129
1535
  const c = entry["color"];
1130
1536
  if (typeof c === "string") return vars.resolve(c);
1131
1537
  }
1132
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
+ }
1133
1590
  function stopsToCssList(colors, vars) {
1134
1591
  if (!Array.isArray(colors) || colors.length === 0) return void 0;
1135
1592
  const parts = [];
1136
1593
  for (const c of colors) {
1137
- if (!isRecord$1(c)) continue;
1594
+ if (!isRecord$3(c)) continue;
1138
1595
  const col = colorFromFillEntry(c["color"], vars);
1139
1596
  const pos = vars.resolveNum(c["position"]);
1140
1597
  if (col === void 0) continue;
@@ -1156,7 +1613,7 @@ function penAngleToCss(deg) {
1156
1613
  return `${(((90 - deg) % 360 + 360) % 360).toFixed(1)}deg`;
1157
1614
  }
1158
1615
  function normCenter(c, vars) {
1159
- if (isRecord$1(c)) {
1616
+ if (isRecord$3(c)) {
1160
1617
  const rx = vars?.resolveNum(c["x"]) ?? (typeof c["x"] === "number" ? c["x"] : void 0);
1161
1618
  const ry = vars?.resolveNum(c["y"]) ?? (typeof c["y"] === "number" ? c["y"] : void 0);
1162
1619
  if (rx !== void 0 && ry !== void 0) return {
@@ -1175,7 +1632,7 @@ function normCenter(c, vars) {
1175
1632
  * 另一边按正方形处理(与 Pencil 的径向渐变一致)。
1176
1633
  */
1177
1634
  function normSize(size, vars) {
1178
- if (!isRecord$1(size)) return null;
1635
+ if (!isRecord$3(size)) return null;
1179
1636
  const rw = vars.resolveNum(size["width"]);
1180
1637
  const rh = vars.resolveNum(size["height"]);
1181
1638
  if (rw === void 0 && rh === void 0) return null;
@@ -1190,11 +1647,11 @@ function fillsToCss(fillsRaw, vars, warnings, what) {
1190
1647
  const bgImages = [];
1191
1648
  let bgColor;
1192
1649
  for (const entry of entries) {
1193
- if (typeof entry === "string" || isRecord$1(entry) && entry["type"] === void 0) {
1650
+ if (typeof entry === "string" || isRecord$3(entry) && entry["type"] === void 0) {
1194
1651
  bgColor = colorFromFillEntry(entry, vars);
1195
1652
  continue;
1196
1653
  }
1197
- if (!isRecord$1(entry)) continue;
1654
+ if (!isRecord$3(entry)) continue;
1198
1655
  if (vars.resolveBool(entry["enabled"]) === false) continue;
1199
1656
  const kind = entry["type"];
1200
1657
  if (kind === "color") bgColor = colorFromFillEntry(entry, vars);
@@ -1225,11 +1682,17 @@ function fillsToCss(fillsRaw, vars, warnings, what) {
1225
1682
  decls.push(["background-repeat", "no-repeat"]);
1226
1683
  } else warnings.push(`${what}: 图像填充缺少 url,已跳过`);
1227
1684
  } else if (kind === "mesh_gradient") {
1228
- const first = colorFromFillEntry(entry["colors"]?.[0], vars);
1229
- if (first) {
1230
- bgColor = first;
1231
- warnings.push(`${what}: mesh_gradient 降级为首个顶点色`);
1232
- } 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
+ }
1233
1696
  } else if (kind === "shader") warnings.push(`${what}: shader 填充暂不支持渲染,已跳过`);
1234
1697
  }
1235
1698
  if (bgImages.length > 0) decls.unshift(["background-image", bgImages.join(", ")]);
@@ -1250,7 +1713,7 @@ function fillsToCss(fillsRaw, vars, warnings, what) {
1250
1713
  function strokeToCss(node, vars, warnings, what) {
1251
1714
  const stroke = node["stroke"];
1252
1715
  if (stroke === void 0) return [];
1253
- 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));
1254
1717
  if (enabled.length === 0) return [];
1255
1718
  const firstColor = enabled.map((e) => colorFromFillEntry(e, vars)).find((c) => c !== void 0);
1256
1719
  if (firstColor === void 0) return [];
@@ -1259,8 +1722,8 @@ function strokeToCss(node, vars, warnings, what) {
1259
1722
  const alignRaw = node["strokeAlignment"];
1260
1723
  const align = alignRaw === "outer" || alignRaw === "center" ? alignRaw : "inner";
1261
1724
  const widthRaw = node["strokeWidth"];
1262
- if (isRecord$1(widthRaw) && align !== "inner") warnings.push(`${what}: 逐边描边宽度 + strokeAlignment=${align} 暂按 inner(CSS border)渲染`);
1263
- 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") {
1264
1727
  const w = vars.resolveNum(widthRaw) ?? 1;
1265
1728
  decls.push(["outline", `${w}px solid ${firstColor}`]);
1266
1729
  decls.push(["outline-offset", align === "outer" ? "0" : `${-w / 2}px`]);
@@ -1270,7 +1733,7 @@ function strokeToCss(node, vars, warnings, what) {
1270
1733
  if (typeof widthRaw === "number") {
1271
1734
  decls.push(["border-width", `${widthRaw}px`]);
1272
1735
  decls.push(["border-style", "solid"]);
1273
- } else if (isRecord$1(widthRaw)) {
1736
+ } else if (isRecord$3(widthRaw)) {
1274
1737
  const sides = [];
1275
1738
  for (const [k, cssName] of [
1276
1739
  ["top", "border-top"],
@@ -1300,10 +1763,10 @@ function effectsToCss(effectsRaw, vars, warnings, what) {
1300
1763
  let blurPx;
1301
1764
  let bgBlurPx;
1302
1765
  for (const e of entries) {
1303
- if (!isRecord$1(e)) continue;
1766
+ if (!isRecord$3(e)) continue;
1304
1767
  if (e["type"] === "shadow") {
1305
1768
  const inner = e["shadowType"] === "inner";
1306
- const off = isRecord$1(e["offset"]) ? e["offset"] : {};
1769
+ const off = isRecord$3(e["offset"]) ? e["offset"] : {};
1307
1770
  const x = vars.resolveNum(off["x"]) ?? 0;
1308
1771
  const y = vars.resolveNum(off["y"]) ?? 0;
1309
1772
  const b = vars.resolveNum(e["blur"]) ?? 0;
@@ -1319,12 +1782,12 @@ function effectsToCss(effectsRaw, vars, warnings, what) {
1319
1782
  return decls;
1320
1783
  }
1321
1784
  /** 文本样式;文本颜色来自 fill(实文件如此:text 节点 fill 即字色) */
1322
- function textToCss(node, vars, warnings) {
1785
+ function textToCss(node, vars, warnings, fontPreviews) {
1323
1786
  const decls = [];
1324
1787
  const num = (k) => vars.resolveNum(node[k]);
1325
1788
  const str = (k) => typeof node[k] === "string" ? vars.resolve(node[k]) : void 0;
1326
1789
  const ff = str("fontFamily");
1327
- if (ff) decls.push(["font-family", ff]);
1790
+ if (ff) decls.push(["font-family", fontPreviews === void 0 ? ff : stackWithPreviews(ff, fontPreviews)]);
1328
1791
  const fs = num("fontSize");
1329
1792
  if (fs !== void 0) decls.push(["font-size", `${fs}px`]);
1330
1793
  const fw = str("fontWeight");
@@ -1501,6 +1964,198 @@ function resolveIconName(library, name) {
1501
1964
  if (direct !== void 0) return direct;
1502
1965
  return name;
1503
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
+ }
1504
2159
  // src/render/html.ts
1505
2160
  function escapeHtml(s) {
1506
2161
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1508,7 +2163,7 @@ function escapeHtml(s) {
1508
2163
  function escapeAttr(s) {
1509
2164
  return escapeHtml(s).replace(/"/g, "&quot;");
1510
2165
  }
1511
- function isRecord(v) {
2166
+ function isRecord$1(v) {
1512
2167
  return typeof v === "object" && v !== null && !Array.isArray(v);
1513
2168
  }
1514
2169
  /** 'fit_content(120)' → 120(sizing 字符串内的回退值) */
@@ -1655,9 +2310,75 @@ function boardAttrs(node, vars) {
1655
2310
  `data-board-h="${numStr(box.h)}"`
1656
2311
  ];
1657
2312
  }
1658
- 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) {
1659
2380
  const type = String(node["type"] ?? "?");
1660
- const scoped = isRecord(node["theme"]) ? vars.withTheme(node["theme"]) : vars;
2381
+ const scoped = isRecord$1(node["theme"]) ? vars.withTheme(node["theme"]) : vars;
1661
2382
  if (scoped.resolveBool(node["enabled"]) === false) return "";
1662
2383
  const label = typeof node["name"] === "string" && node["name"] !== "" ? `${type}: ${String(node["name"])}` : type;
1663
2384
  const positioned = parentLayout === "none" || node["layoutPosition"] === "absolute";
@@ -1674,13 +2395,15 @@ function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
1674
2395
  if (parentLayout !== "none" && !positioned) css.push(["flex-shrink", "0"]);
1675
2396
  css.push(...sizeDecls(node["width"], node["height"], positioned, parentLayout, scoped, warnings, label));
1676
2397
  css.push(...commonToCss(node, scoped));
1677
- if (type !== "text") css.push(...fillsToCss(node["fill"], scoped, warnings, label));
1678
- 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));
1679
2402
  css.push(...effectsToCss(node["effect"], scoped, warnings, label));
1680
2403
  let inner = "";
1681
2404
  switch (type) {
1682
2405
  case "text":
1683
- css.push(...textToCss(node, scoped, warnings));
2406
+ css.push(...textToCss(node, scoped, warnings, fonts?.previews));
1684
2407
  inner = escapeHtml(typeof node["content"] === "string" ? scoped.resolve(node["content"]) : "");
1685
2408
  break;
1686
2409
  case "icon":
@@ -1704,19 +2427,28 @@ function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
1704
2427
  inner = escapeHtml(typeof node["content"] === "string" ? scoped.resolve(node["content"]) : "");
1705
2428
  break;
1706
2429
  }
1707
- 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}`);
1708
2439
  css.push(["border-radius", "50%"]);
1709
- const inner2 = scoped.resolveNum(node["innerRadius"]);
1710
- if (inner2 !== void 0 && inner2 > 0) warnings.push(`${label}: innerRadius 环形暂按实心椭圆渲染`);
1711
- const sweep = scoped.resolveNum(node["sweepAngle"]);
1712
- if (sweep !== void 0 && sweep !== 360) warnings.push(`${label}: 弧形(sweepAngle=${sweep})暂按整圆渲染`);
1713
2440
  break;
1714
- }
1715
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
+ }
1716
2448
  const count = scoped.resolveNum(node["polygonCount"]);
1717
2449
  const n = count ?? 3;
1718
2450
  if (count === void 0) warnings.push(`${label}: polygonCount 缺省按 3 渲染`);
1719
- if (node["cornerRadius"] !== void 0) warnings.push(`${label}: 多边形圆角暂忽略`);
2451
+ if (plan !== null && "reason" in plan) warnings.push(`${label}: ${plan.reason}`);
1720
2452
  css.push(["clip-path", polygonClip(n)]);
1721
2453
  break;
1722
2454
  }
@@ -1738,7 +2470,7 @@ function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
1738
2470
  if (type === "group" && node["effect"] !== void 0) {}
1739
2471
  }
1740
2472
  const kidsRaw = node["children"];
1741
- 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("") : "";
1742
2474
  return openTag(node, css.map(([p, v]) => `${p}:${v}`).join(";"), extraAttrs) + inner + childrenHtml + "</div>";
1743
2475
  }
1744
2476
  const KNOWN_RENDER_TYPES = /* @__PURE__ */ new Set([
@@ -1760,7 +2492,7 @@ function computeCanvas(nodes, vars) {
1760
2492
  let maxW = 0;
1761
2493
  let maxH = 0;
1762
2494
  for (const n of nodes) {
1763
- if (!isRecord(n)) continue;
2495
+ if (!isRecord$1(n)) continue;
1764
2496
  const box = nodeBox(n, vars);
1765
2497
  maxW = Math.max(maxW, box.x + box.w);
1766
2498
  maxH = Math.max(maxH, box.y + box.h);
@@ -1773,7 +2505,7 @@ function computeCanvas(nodes, vars) {
1773
2505
  function boardBoxes(nodes, vars) {
1774
2506
  const out = [];
1775
2507
  for (const n of nodes) {
1776
- if (!isRecord(n)) continue;
2508
+ if (!isRecord$1(n)) continue;
1777
2509
  const box = nodeBox(n, vars);
1778
2510
  const name = typeof n["name"] === "string" && n["name"] !== "" ? n["name"] : void 0;
1779
2511
  out.push({
@@ -1788,12 +2520,93 @@ function boardBoxes(nodes, vars) {
1788
2520
  return out;
1789
2521
  }
1790
2522
  /** 渲染整棵(已展开的)树:顶层节点在画布上绝对定位,并带画布元数据 */
1791
- function renderTree(nodes, vars, warnings) {
1792
- return nodes.filter(isRecord).map((n) => {
1793
- 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);
1794
2526
  }).join("");
1795
2527
  }
1796
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
+ }
1797
2610
  /**
1798
2611
  * 渲染入口(决策 2/3/4/5 的兑现):
1799
2612
  * 变量内联解析 → ref 展开 → 树 → HTML(每节点 data-node-id)。
@@ -1802,11 +2615,23 @@ function renderTree(nodes, vars, warnings) {
1802
2615
  function renderDocument(doc, opts = {}) {
1803
2616
  const warnings = [];
1804
2617
  const vars = makeVarResolver(doc, warnings);
1805
- const expanded = expandRefs(doc, warnings);
1806
- const canvas = computeCanvas(expanded, vars);
1807
- 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);
1808
2629
  const title = opts.title ?? "paintress render";
1809
- 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)) : "";
1810
2635
  return {
1811
2636
  html: [
1812
2637
  "<!DOCTYPE html>",
@@ -1816,14 +2641,17 @@ function renderDocument(doc, opts = {}) {
1816
2641
  `<title>${escapeAttr(title)}</title>`,
1817
2642
  "<style>",
1818
2643
  "* { box-sizing: border-box; margin: 0; padding: 0; }",
1819
- "body { margin: 0; background: #ffffff; }",
1820
- ".ptr-canvas { position: relative; }",
2644
+ `body { margin: 0; background: ${opts.transparent === true ? "transparent" : "#ffffff"}; }`,
2645
+ `.ptr-canvas { position: relative; font-family: ${defaultStack}; }`,
2646
+ ...faces,
1821
2647
  "</style>",
1822
2648
  "</head>",
1823
2649
  "<body>",
1824
2650
  `<div class="ptr-canvas" style="width:${canvas.width}px;height:${canvas.height}px">`,
1825
2651
  body,
1826
2652
  "</div>",
2653
+ fontLoaderScript(links),
2654
+ probe,
1827
2655
  "</body>",
1828
2656
  "</html>",
1829
2657
  ""
@@ -1833,4 +2661,4 @@ function renderDocument(doc, opts = {}) {
1833
2661
  boards
1834
2662
  };
1835
2663
  }
1836
- 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 };