Repository navigation
Expand file tree
/
Copy pathce-preview.js
More file actions
6288 lines (6172 loc) · 281 KB
/
Copy pathce-preview.js
File metadata and controls
6288 lines (6172 loc) · 281 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* ChoTenEditor Minecraft 风格预览渲染器
* 依赖(全部可选, 缺失时自动降级): window.CEMCAssets / window.electronAPI.mc / window.I18N / jsyaml
*
* 能力:
* 1) MC 位图字体引擎: 读取原版 font provider(reference/bitmap/space), 支持
* MiniMessage、旧版 § 颜色码、CraftEngine 自定义标签
* (image / shift / global / i18n / l10n / expr / random / arg / var / papi / bubble 等)
* 2) 物品/方块图标: 平面物品贴图 + 由原版模型 JSON 生成的等轴测 3D 方块
* 3) 场景合成: 聊天栏 / 物品悬浮提示(lore) / 原版容器 GUI (9x1~9x6) / 字体图像总览
* 4) 模型场景 (item-model): 物品/方块模型按 display.<context> (ItemTransform)
* 完整展开渲染, 支持视角旋转与俯仰 —— 「物品模型的完全预览」
* 5) 家具场景编辑: 碰撞箱/座位/元素的可拖拽手柄 + 反投影 API (预览内编辑)
*
* 对外 API: window.CEPreview
*/
(function () {
'use strict';
var root = (typeof window !== 'undefined') ? window
: (typeof globalThis !== 'undefined' ? globalThis : this);
if (root.CEPreview) return;
var VERSION = 1;
var LINE_HEIGHT = 9; // MC 文本行高 (Font.lineHeight = 9)
var SHADOW_FACTOR = 0.25; // MC 阴影 = 原色 * 0.25 (ARGB.scaleRGB(color, 0.25f))
var CACHE_CAP = 400;
var COS30 = Math.cos(Math.PI / 6);
var SIN30 = Math.sin(Math.PI / 6);
// ---------------- MiniMessage 具名颜色 (与原版 16 色一致) ----------------
var NAMED_COLORS = {
black: '#000000', dark_blue: '#0000AA', dark_green: '#00AA00', dark_aqua: '#00AAAA',
dark_red: '#AA0000', dark_purple: '#AA00AA', gold: '#FFAA00', gray: '#AAAAAA',
grey: '#AAAAAA', dark_gray: '#555555', dark_grey: '#555555', blue: '#5555FF',
green: '#55FF55', aqua: '#55FFFF', red: '#FF5555', light_purple: '#FF55FF',
yellow: '#FFFF55', white: '#FFFFFF'
};
// ---------------- 旧版 § 颜色码 ----------------
var LEGACY_COLORS = {
'0': '#000000', '1': '#0000AA', '2': '#00AA00', '3': '#00AAAA', '4': '#AA0000',
'5': '#AA00AA', '6': '#FFAA00', '7': '#AAAAAA', '8': '#555555', '9': '#5555FF',
'a': '#55FF55', 'b': '#55FFFF', 'c': '#FF5555', 'd': '#FF55FF', 'e': '#FFFF55',
'f': '#FFFFFF'
};
var LEGACY_FORMATS = { l: 'bold', o: 'italic', n: 'underlined', m: 'strikethrough', k: 'obfuscated' };
// ---------------- 标签命名空间 ----------------
// MiniMessage(Adventure) 和 CraftEngine 都用 <>, 必须分开识别与开关:
// MM 标签只影响样式/交互 (颜色、装饰、点击悬浮...)
// CE 标签会展开成实际内容 (图像、变量、计算、偏移...)
// 装饰标签的短名/长名都映射到同一个样式键 —— 之前直接把标签名当键写,
// 于是 <i>/<b>/<u>/<st>/<obf> 全部静默失效 (样式键其实叫 italic/bold/...)。
var DECOR_ALIASES = {
bold: 'bold', b: 'bold',
italic: 'italic', em: 'italic', i: 'italic',
underlined: 'underlined', u: 'underlined',
strikethrough: 'strikethrough', st: 'strikethrough',
obfuscated: 'obfuscated', obf: 'obfuscated'
};
// MiniMessage 里「有语义但不改变外观」的标签: 预览中直接吃掉 (点击/悬浮/插入)
var MM_OPAQUE = { click: 1, hover: 1, insertion: 1 };
// MiniMessage 里需要显示成灰色占位符的动态标签 (预览无法求值)
var MM_PLACEHOLDER = { key: 1, selector: 1, score: 1, nbt: 1 };
// MiniMessage 的翻译标签 (CE 用的是 i18n / l10n)
var MM_TRANSLATE = { lang: 1, translate: 1 };
// CraftEngine 扩展标签 (见 CE wiki: reference/text_format)
var CE_TAGS = {
shift: 1, image: 1, global: 1, i18n: 1, l10n: 1, expr: 1, random: 1,
arg: 1, viewer_arg: 1, var: 1, papi: 1, viewer_papi: 1, rel_papi: 1,
head_texture: 1, bubble: 1, nameplate: 1, background: 1
};
// ---------------- 选项 / 状态 ----------------
var options = {
mcRoot: null,
lang: 'zh_cn',
shadow: true,
resolveTags: true, // 总开关
resolveMiniMessage: true, // MiniMessage(Adventure) 标签
resolveCeTags: true, // CraftEngine 扩展标签
resolveGlobals: true,
resolveImages: true,
// 原版「强制 Unicode 字体」(Force Unicode Font): true = 缺失字形也强制走 unifont
// (此处即系统合成的 unifont 风格点阵), false = 保持默认 (同样回退合成, 见 glyphFor)。
// 两个模式当前都回退到合成字形, 区别在于: 强制模式下 ASCII 也按 unicode 页渲染。
forceUnicode: false
};
var _projectData = { images: {}, globals: {}, emojis: {}, langs: {}, furniture: {}, items: {}, blocks: {} };
// 当前场景视图俯仰 (度): 30 = 等轴测 (缺省), 90 = 正俯视, -90 = 正仰视。
// scenePitch 与 sceneViewYaw 一起构成视图旋转, 家具场景/模型场景共用;
// 每个场景渲染入口都会按 scene.pitch 重新赋值 (缺省 30)。
var scenePitch = 30;
var _fonts = null; // minecraft:default glyph map
var _fontMaps = Object.create(null); // resource id -> glyph map
var _fontMapLoaded = Object.create(null); // provider-loaded maps; image registration may be provisional
var _fontMapPromises = Object.create(null); // in-flight provider loads by resource id
var _fontPromise = null;
var _fontSettled = false; // 是否已基于资源就绪完成一次真实加载
var _readyFired = false;
var _readyListeners = [];
var _activeFile = null;
var _projectCacheKey = null;
var _imgCache = new Map();
var _jsonCache = new Map();
var _modelCache = new Map();
// CE 运行时生成的模型 (configuration 里 model.generation 声明、资源包上无 json):
// loadModelChain 磁盘未命中时从这里合成 {parent, textures} —— 与 CE 生成行为一致。
var _runtimeModels = Object.create(null);
function registerRuntimeModel(id, modelJson, defaultNs) {
if (!id || !modelJson || typeof modelJson !== 'object') return false;
var s = normalizeResourceId(id, defaultNs);
_runtimeModels[s] = modelJson;
_modelCache.delete(s);
return true;
}
function runtimeModelOf(id, defaultNs) {
return _runtimeModels[normalizeResourceId(id, defaultNs)] || null;
}
var _warnings = [];
// ---------------- 小工具 ----------------
function warn(msg) {
if (_warnings.indexOf(msg) === -1) _warnings.push(msg);
}
function t(key, fb, params) {
var v = null;
try {
if (root.I18N && root.I18N.t) { v = root.I18N.t(key); if (v === key) v = null; }
} catch (e) { v = null; }
if (v == null) v = fb != null ? fb : key;
if (params) v = String(v).replace(/\{(\w+)\}/g, function (m, n) { return params[n] != null ? params[n] : m; });
return v;
}
function cacheSet(map, k, v) {
if (map.size >= CACHE_CAP) {
var first = map.keys().next();
if (!first.done) map.delete(first.value);
}
map.set(k, v);
return v;
}
function isObj(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
function clamp(v, a, b) { return v < a ? a : (v > b ? b : v); }
// ---------- 颜色 ----------
function parseColor(s) {
if (typeof s !== 'string') return null;
var v = s.trim();
if (NAMED_COLORS[v]) return hexToRgb(NAMED_COLORS[v]);
var m = /^#([0-9a-f]{6})$/i.exec(v);
if (m) return hexToRgb('#' + m[1]);
m = /^#([0-9a-f]{3})$/i.exec(v);
if (m) {
var h = m[1];
return hexToRgb('#' + h[0] + h[0] + h[1] + h[1] + h[2] + h[2]);
}
m = /^rgba?\(([^)]+)\)$/i.exec(v);
if (m) {
var p = m[1].split(',').map(function (x) { return parseFloat(x); });
return { r: p[0] | 0, g: p[1] | 0, b: p[2] | 0, a: p.length > 3 ? p[3] : 1 };
}
return null;
}
function hexToRgb(hex) {
var n = parseInt(hex.slice(1), 16);
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255, a: 1 };
}
function rgbCss(c, alpha) {
if (!c) return '#FFFFFF';
var a = alpha == null ? (c.a == null ? 1 : c.a) : alpha;
return a >= 1 ? 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')'
: 'rgba(' + c.r + ',' + c.g + ',' + c.b + ',' + a + ')';
}
function scaleColor(c, f) {
return { r: Math.round(c.r * f), g: Math.round(c.g * f), b: Math.round(c.b * f), a: c.a == null ? 1 : c.a };
}
function lerpColor(a, b, k) {
return {
r: Math.round(a.r + (b.r - a.r) * k),
g: Math.round(a.g + (b.g - a.g) * k),
b: Math.round(a.b + (b.b - a.b) * k),
a: 1
};
}
function hslToRgb(h, s, l) {
h = ((h % 360) + 360) % 360 / 360;
var r, g, b;
if (s === 0) { r = g = b = l; }
else {
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
var hue = function (tt) {
if (tt < 0) tt += 1;
if (tt > 1) tt -= 1;
if (tt < 1 / 6) return p + (q - p) * 6 * tt;
if (tt < 1 / 2) return q;
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
return p;
};
r = hue(h + 1 / 3); g = hue(h); b = hue(h - 1 / 3);
}
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255), a: 1 };
}
// ---------------- 内置回退字形 (5x7 点阵, 覆盖 ASCII 0x20-0x7E) ----------------
// 每个字符 5 ?x 7 ? 每位丢? ?5 个字符编?(0-9A-V ?base32 行?
var FALLBACK_ROWS = {
'A': '0E11 11 1F11 11 11', 'B': '1E11 11 1E11 11 1E', 'C': '0E11 10 10 10 11 0E',
'D': '1E11 11 11 11 11 1E', 'E': '1F10 10 1E10 10 1F', 'F': '1F10 10 1E10 10 10',
'G': '0E11 10 1711 11 0E', 'H': '11 11 11 1F11 11 11', 'I': '1F04 04 04 04 04 1F',
'J': '07 02 02 02 12 0C', 'K': '11 12 14 1814 12 11', 'L': '10 10 10 10 10 1F',
'M': '11 1B15 15 11 11 11', 'N': '11 1915 1311 11 11', 'O': '0E11 11 11 11 11 0E',
'P': '1E11 11 1E10 10 10', 'Q': '0E11 11 11 1512 0D', 'R': '1E11 11 1E14 12 11',
'S': '0F10 10 0E01 01 1E', 'T': '1F04 04 04 04 04 04', 'U': '11 11 11 11 11 11 0E',
'V': '11 11 11 11 11 0A04', 'W': '11 11 11 1515 1B11', 'X': '11 11 0A04 0A11 11',
'Y': '11 11 0A04 04 04 04', 'Z': '1F01 02 04 08 10 1F'
};
function buildFallbackFont() {
var glyphs = Object.create(null);
var mk = function (ch, rows) {
// rows: 7 个数? 每个 5 bit
var px = [];
for (var y = 0; y < 7; y++) {
var row = rows[y] | 0;
var line = [];
for (var x = 0; x < 5; x++) line.push(((row >> (4 - x)) & 1) ? 1 : 0);
px.push(line);
}
glyphs[ch.codePointAt(0)] = { type: 'fallback', px: px, w: 5, h: 7, advance: 6, ascent: 7 };
};
// 数字与字母的点阵数据 (紧凑字面?
var G = {
'0': [14, 17, 19, 21, 25, 17, 14], '1': [4, 12, 4, 4, 4, 4, 14],
'2': [14, 17, 1, 2, 4, 8, 31], '3': [31, 2, 4, 2, 1, 17, 14],
'4': [2, 6, 10, 18, 31, 2, 2], '5': [31, 16, 30, 1, 1, 17, 14],
'6': [6, 8, 16, 30, 17, 17, 14], '7': [31, 1, 2, 4, 8, 8, 8],
'8': [14, 17, 17, 14, 17, 17, 14], '9': [14, 17, 17, 15, 1, 2, 12],
'A': [14, 17, 17, 31, 17, 17, 17], 'B': [30, 17, 17, 30, 17, 17, 30],
'C': [14, 17, 16, 16, 16, 17, 14], 'D': [30, 17, 17, 17, 17, 17, 30],
'E': [31, 16, 16, 30, 16, 16, 31], 'F': [31, 16, 16, 30, 16, 16, 16],
'G': [14, 17, 16, 23, 17, 17, 15], 'H': [17, 17, 17, 31, 17, 17, 17],
'I': [14, 4, 4, 4, 4, 4, 14], 'J': [7, 2, 2, 2, 2, 18, 12],
'K': [17, 18, 20, 24, 20, 18, 17], 'L': [16, 16, 16, 16, 16, 16, 31],
'M': [17, 27, 21, 21, 17, 17, 17], 'N': [17, 25, 21, 19, 17, 17, 17],
'O': [14, 17, 17, 17, 17, 17, 14], 'P': [30, 17, 17, 30, 16, 16, 16],
'Q': [14, 17, 17, 17, 21, 18, 13], 'R': [30, 17, 17, 30, 20, 18, 17],
'S': [15, 16, 16, 14, 1, 1, 30], 'T': [31, 4, 4, 4, 4, 4, 4],
'U': [17, 17, 17, 17, 17, 17, 14], 'V': [17, 17, 17, 17, 17, 10, 4],
'W': [17, 17, 17, 21, 21, 27, 17], 'X': [17, 17, 10, 4, 10, 17, 17],
'Y': [17, 17, 10, 4, 4, 4, 4], 'Z': [31, 1, 2, 4, 8, 16, 31],
'!': [4, 4, 4, 4, 4, 0, 4], '?': [14, 17, 1, 2, 4, 0, 4],
'.': [0, 0, 0, 0, 0, 0, 4], ',': [0, 0, 0, 0, 0, 4, 8],
':': [0, 0, 4, 0, 0, 4, 0], ';': [0, 0, 4, 0, 0, 4, 8],
'-': [0, 0, 0, 31, 0, 0, 0], '_': [0, 0, 0, 0, 0, 0, 31],
'+': [0, 4, 4, 31, 4, 4, 0], '=': [0, 0, 31, 0, 31, 0, 0],
'/': [1, 2, 2, 4, 8, 8, 16], '\\': [16, 8, 8, 4, 2, 2, 1],
'*': [0, 10, 4, 31, 4, 10, 0], '#': [10, 10, 31, 10, 31, 10, 10],
'(': [2, 4, 8, 8, 8, 4, 2], ')': [8, 4, 2, 2, 2, 4, 8],
'[': [14, 8, 8, 8, 8, 8, 14], ']': [14, 2, 2, 2, 2, 2, 14],
'<': [2, 4, 8, 16, 8, 4, 2], '>': [8, 4, 2, 1, 2, 4, 8],
"'": [4, 4, 8, 0, 0, 0, 0], '"': [10, 10, 20, 0, 0, 0, 0],
'%': [25, 26, 2, 4, 8, 11, 19], '&': [12, 18, 20, 8, 21, 18, 13],
'@': [14, 17, 23, 21, 23, 16, 14], '$': [4, 15, 20, 14, 5, 30, 4],
'~': [0, 0, 8, 21, 2, 0, 0], '^': [4, 10, 17, 0, 0, 0, 0],
'|': [4, 4, 4, 4, 4, 4, 4], '{': [6, 4, 4, 8, 4, 4, 6], '}': [12, 4, 4, 2, 4, 4, 12]
};
Object.keys(G).forEach(function (ch) { mk(ch, G[ch]); });
glyphs[32] = { type: 'space', w: 0, h: 0, advance: 4, ascent: 7 };
return glyphs;
}
// ---------------- 原版字体加载 ----------------
function mcApi() {
var a = root.electronAPI;
return a && a.mc ? a.mc : null;
}
function readText(p) {
var m = mcApi();
if (!m || !m.readText) return Promise.resolve(null);
return m.readText(p).then(function (r) { return (r && r.success) ? r.content : null; }).catch(function () { return null; });
}
function readDataUrl(p) {
var m = mcApi();
if (!m || !m.readBinary) return Promise.resolve(null);
return m.readBinary(p).then(function (r) { return (r && r.success) ? r.dataUrl : null; }).catch(function () { return null; });
}
function loadImagePath(p) {
if (!p) return Promise.resolve(null);
if (_imgCache.has(p)) return _imgCache.get(p);
var pr = readDataUrl(p).then(function (url) {
if (!url || typeof Image === 'undefined') return null;
return new Promise(function (res) {
var im = new Image();
im.onload = function () { res(im); };
im.onerror = function () { res(null); };
im.src = url;
});
}).catch(function () { return null; });
return cacheSet(_imgCache, p, pr);
}
function loadJsonPath(p) {
if (!p) return Promise.resolve(null);
if (_jsonCache.has(p)) return _jsonCache.get(p);
var pr = readText(p).then(function (txt) {
if (!txt) return null;
try { return JSON.parse(txt); } catch (e) { return null; }
}).catch(function () { return null; });
return cacheSet(_jsonCache, p, pr);
}
// .mcmeta (动画/纹理元数据)。
// 注意两种命名都存在, 必须都试:
// · 资源包惯例: <name>.mcmeta (去掉 .png)
// · 原版 gui sprite: <name>.png.mcmeta (保留 .png —— Mojang 自己的命名)
// 先试保留 .png 的写法 (原版贴图), 再退回去 .png 的写法。
async function loadTextureMeta(texturePath) {
var p = String(texturePath || '');
if (!p) return null;
var withPng = /\.png$/i.test(p) ? p + '.mcmeta' : p + '.mcmeta';
var m1 = await loadJsonPath(withPng);
if (m1) return m1;
if (/\.png$/i.test(p)) {
var m2 = await loadJsonPath(p.replace(/\.png$/i, '') + '.mcmeta');
if (m2) return m2;
}
return null;
}
// 带动画 (.mcmeta animation) 的贴图是纵向帧条: 整张直接画会把所有帧叠在一起。
// 这里按 animation 声明 (或宽高推断) 裁出单帧, 供静态图标/模型 UV 使用。
// 返回 { img, sw, sh, sx, sy }; 非动画贴图返回 null (调用方按整图绘制)。
async function spriteFrameOf(img, texturePath) {
try {
if (!img || !img.width || !img.height) return null;
var meta = await loadTextureMeta(texturePath);
var anim = meta && meta.animation;
if (!anim) return null;
var fw = anim.width != null ? anim.width : img.width; // mc 帧宽默认 = 贴图宽
var fh = anim.height != null ? anim.height : img.width; // 帧高默认 = 帧宽 (纵向条)
if (!(fw > 0) || !(fh > 0) || fw > img.width || fh > img.height) return null;
// 帧数 = 总高 / 帧高; interpolate 等参数只影响播放, 静态预览取第一帧即可
var frames = Math.floor(img.height / fh + 1e-6);
if (frames <= 1) return null;
return { img: img, sw: fw, sh: fh, sx: 0, sy: 0 };
} catch (e) { return null; }
}
function assets() { return root.CEMCAssets || null; }
function activeNamespace() {
var A = assets();
try { return A && A.currentNamespace ? A.currentNamespace() : null; } catch (e) { return null; }
}
function normalizeResourceId(id, defaultNs) {
var s = String(id == null ? '' : id).trim().replace(/\\/g, '/');
if (!s) return '';
var i = s.indexOf(':');
if (i > 0) return s;
var ns = defaultNs || activeNamespace() || 'minecraft';
return String(ns).replace(/:$/, '') + ':' + s;
}
function resourceNamespace(id, fallback) {
var s = String(id || '');
var i = s.indexOf(':');
return i > 0 ? s.slice(0, i) : (fallback || activeNamespace() || 'minecraft');
}
function resourcePath(id) {
var s = String(id || '');
var i = s.indexOf(':');
return i > 0 ? s.slice(i + 1) : s;
}
function resolvePath(kind, id) {
var A = assets();
return A && A.resolve ? A.resolve(kind, id) : null;
}
// 同一个资源 id 可能在多个资源包里都有(当前工程包 / 其它包 / 原版),
// 只取第一个路径会读错包(<image:...> 变成「找不到图片」小红块、字体回退点阵)。
// 这里返回全部候选路径,由调用方按顺序尝试读取。
function resolveCandidates(kind, id) {
var A = assets();
if (A && A.resolveCandidates) {
var list = A.resolveCandidates(kind, id);
if (list && list.length) return list;
}
var p = resolvePath(kind, id);
return p ? [p] : [];
}
function nsDirsOf(ns) {
var A = assets();
ns = ns || 'minecraft';
if (A && A.nsDirs) { var l = A.nsDirs(ns); if (l && l.length) return l; }
if (A && A.nsDir) { var d0 = A.nsDir(ns); if (d0) return [d0]; }
return [];
}
// 依次尝试候选路径,返回第一个真正读到的图片
async function loadImageAny(kind, id) {
var cands = resolveCandidates(kind, id);
for (var i = 0; i < cands.length; i++) {
var img = await loadImagePath(cands[i]);
if (img) return img;
}
return null;
}
// 带路径的加载: 命中哪个候选就返回它 (供 .mcmeta 按「贴图路径 + .mcmeta」查找)
async function loadImageAnyWithPath(kind, id) {
var cands = resolveCandidates(kind, id);
for (var i = 0; i < cands.length; i++) {
var img = await loadImagePath(cands[i]);
if (img) return { img: img, path: cands[i] };
}
return null;
}
// 贴图加载 (动画感知): 命中图 + 若是 .mcmeta 动画帧条则裁出第一帧。
// 返回 { img, sw, sh, sx, sy } 或 null; sw/sh/sx/sy 缺省 = 整图。
async function loadTextureFrame(kind, id) {
var hit = await loadImageAnyWithPath(kind, id);
if (!hit) return null;
var fr = await spriteFrameOf(hit.img, hit.path);
if (fr) return fr;
var img = hit.img;
return { img: img, sw: img.width, sh: img.height, sx: 0, sy: 0 };
}
async function loadJsonAny(kind, id) {
var cands = resolveCandidates(kind, id);
for (var i = 0; i < cands.length; i++) {
var j = await loadJsonPath(cands[i]);
if (j) return j;
}
return null;
}
// provider ?file 字段?namespace:path (可带 textures/ 前缀)
function fontTextureCandidates(fileRef, defaultNs) {
var s = String(fileRef || '');
var i = s.indexOf(':');
var ns = i === -1 ? (defaultNs || 'minecraft') : s.slice(0, i);
var p = i === -1 ? s : s.slice(i + 1);
p = p.replace(/^textures\//, '');
p = p.replace(/\.png$/i, '');
return resolveCandidates('texture', ns + ':' + p);
}
function fontTexturePath(fileRef, defaultNs) {
return fontTextureCandidates(fileRef, defaultNs)[0] || null;
}
function fontJsonPath(id) {
return resolvePath('font', normalizeResourceId(id, 'minecraft:default'.split(':')[0]));
}
function loadFontMap(fontId, addFallback) {
var id = normalizeResourceId(fontId, 'minecraft');
if (!id) id = 'minecraft:default';
if (_fontMapLoaded[id]) return Promise.resolve(_fontMaps[id]);
if (_fontMapPromises[id]) return _fontMapPromises[id];
var promise = (async function () {
var provisional = _fontMaps[id] || null;
var glyphs = Object.create(null);
var seenFonts = Object.create(null);
var chain = [];
try { await expandFont(id, 0, seenFonts, chain, resourceNamespace(id, 'minecraft')); } catch (e) { warn('font-load-failed: ' + (e && e.message)); }
for (var i = 0; i < chain.length; i++) {
var prov = chain[i];
if (prov.type === 'space') {
var adv = prov.advances || {};
Object.keys(adv).forEach(function (ch) {
var cp = ch.codePointAt(0);
glyphs[cp] = { type: 'space', w: 0, h: 0, advance: adv[ch] | 0, ascent: 7 };
});
} else if (prov.type === 'bitmap') {
await loadBitmapProvider(prov, glyphs, resourceNamespace(id));
}
}
if (addFallback !== false) {
var fb = buildFallbackFont();
Object.keys(fb).forEach(function (k) { if (!glyphs[k]) glyphs[k] = fb[k]; });
}
// Image registration may have created a provisional map before providers load.
// Provider glyphs stay authoritative; only fill genuinely missing codepoints.
if (provisional) Object.keys(provisional).forEach(function (k) {
if (!glyphs[k]) glyphs[k] = provisional[k];
});
_fontMaps[id] = glyphs;
_fontMapLoaded[id] = true;
delete _fontMapPromises[id];
return glyphs;
})();
_fontMapPromises[id] = promise;
return promise;
}
function loadFontData() {
if (_fontPromise) return _fontPromise;
var A = assets();
if (A && A.status && A.status().state !== 'ready' && !_fontSettled) {
warn('assets-not-ready');
if (A.onReady) A.onReady(function () { _fontSettled = true; _fonts = null; _fontMaps = Object.create(null); _fontMapLoaded = Object.create(null); _fontMapPromises = Object.create(null); _fontPromise = null; });
return Promise.resolve(buildFallbackFont());
}
_fontSettled = true;
_fontPromise = (async function () {
var glyphs = await loadFontMap('minecraft:default');
_fonts = glyphs;
await loadUnihex(); // 缺字合成数据, 不阻塞主字体表返回
var ids = [];
try { ids = A && A.listFor ? (A.listFor('fonts') || []) : []; } catch (e) { ids = []; }
for (var i = 0; i < ids.length; i++) {
if (normalizeResourceId(ids[i], 'minecraft') === 'minecraft:default') continue;
await loadFontMap(ids[i]);
}
return glyphs;
})();
return _fontPromise;
}
async function expandFont(fontId, depth, seenFonts, out, defaultNs) {
var id = normalizeResourceId(fontId, defaultNs || resourceNamespace(fontId, 'minecraft'));
if (depth > 4 || seenFonts[id]) return;
seenFonts[id] = 1;
var json = await loadJsonAny('font', id);
if (!json || !Array.isArray(json.providers)) return;
var ns = resourceNamespace(id, defaultNs || 'minecraft');
for (var i = 0; i < json.providers.length; i++) {
var prov = json.providers[i];
if (!prov || typeof prov !== 'object') continue;
if (prov.type === 'reference') {
await expandFont(prov.id, depth + 1, seenFonts, out, ns);
} else {
prov = Object.assign({}, prov);
prov._fontNamespace = ns;
out.push(prov);
}
}
}
async function loadBitmapProvider(prov, glyphs, defaultNs) {
var cands = fontTextureCandidates(prov.file, prov._fontNamespace || defaultNs || 'minecraft');
var img = null;
for (var ci = 0; ci < cands.length && !img; ci++) img = await loadImagePath(cands[ci]);
if (!img) { warn('missing-font-texture: ' + prov.file); return; }
var chars = Array.isArray(prov.chars) ? prov.chars : null;
if (!chars || !chars.length) return;
// 注意: 原版的 chars 用 codePoints() 统计列数 (代理对算 1 个),
// 直接用 String.length 会把 U+1F300 之类的字符算成 2 列,
// 导致格子宽度被腰斩、整个 provider 的字形都被压窄。
var rowCount = chars.length;
var rows = [];
var colCount = 0;
for (var i = 0; i < rowCount; i++) {
var cps = decodeChars(chars[i]);
rows.push(cps);
colCount = Math.max(colCount, cps.length);
}
if (!colCount) return;
var cellW = img.width / colCount;
var cellH = img.height / rowCount;
// 原版 BitmapProvider 的定义: `height` 缺省值是 8 (不是格子高度),
// 渲染时按 scale = height / 格子高度 缩放。CE 生成的 provider 一定会写 height,
// 但第三方字体包常常省略, 之前用格子高度兜底会让字形整体偏大/偏小。
var rawH = prov.height != null ? Number(prov.height) : 8;
if (!isFinite(rawH)) { warn('invalid-font-height: ' + prov.height); rawH = 8; }
var ascent = prov.ascent != null ? Number(prov.ascent) : rawH - 1;
if (!isFinite(ascent)) { warn('invalid-font-ascent: ' + prov.ascent); ascent = rawH - 1; }
var offsetOnly = imageOffsetOnly(rawH, ascent);
var renderH = imageRenderHeight(rawH, 8);
if (rawH === 0) warn('invalid-font-height: ' + prov.height);
var scale = cellH > 0 ? renderH / cellH : 1;
var rawScale = cellH > 0 ? rawH / cellH : rawH;
// 整张图一次取出像? 避免每个字形都做丢?canvas 操作
var atlas = atlasPixels(img);
for (var r = 0; r < rowCount; r++) {
var row = rows[r]; // 已解码的码位数组
for (var c = 0; c < row.length; c++) {
var cp = row[c];
if (!cp) continue; // \u0000 = 无字?
if (glyphs[cp]) continue; // 先出现的 provider 优先
var x0 = c * cellW;
var y0 = r * cellH;
var trimmed = atlas ? trimmedWidth(atlas, x0, y0, cellW, cellH) : 0;
glyphs[cp] = {
type: 'bitmap', img: img,
sx: x0, sy: y0, sw: cellW, sh: cellH,
w: Math.round(cellW * scale), h: Math.round(renderH),
advance: mcGlyphAdvance(trimmed, rawScale),
ascent: ascent,
offsetOnly: offsetOnly,
rawHeight: rawH,
renderHeight: renderH
};
}
}
}
// 把整张贴图读进内?(只做丢?canvas 操作)
function atlasPixels(img) {
try {
if (typeof document === 'undefined') return null;
var cv = document.createElement('canvas');
cv.width = img.width;
cv.height = img.height;
var ctx = cv.getContext('2d');
ctx.imageSmoothingEnabled = false;
ctx.drawImage(img, 0, 0);
return { data: ctx.getImageData(0, 0, cv.width, cv.height).data, w: cv.width, h: cv.height };
} catch (e) {
warn('atlas-read-failed: ' + (e && e.message));
return null;
}
}
function trimmedWidth(atlas, x0, y0, w, h) {
var x1 = Math.min(atlas.w, Math.ceil(x0 + w));
var y1 = Math.min(atlas.h, Math.ceil(y0 + h));
var sx = Math.max(0, Math.floor(x0));
var sy = Math.max(0, Math.floor(y0));
for (var x = x1 - 1; x >= sx; x--) {
for (var y = sy; y < y1; y++) {
if (atlas.data[(y * atlas.w + x) * 4 + 3] > 0) return x - sx + 1;
}
}
return 0;
}
// 把一行 chars 解成码位数组 (支持混合的 \\uXXXX 转义串, 并合并 UTF-16 代理对)
function decodeChars(str) {
var s = String(str == null ? '' : str);
var units = [];
for (var i = 0; i < s.length;) {
if (s.charAt(i) === '\\' && s.charAt(i + 1) === 'u' && /^[0-9a-fA-F]{4}$/.test(s.slice(i + 2, i + 6))) {
units.push(parseInt(s.slice(i + 2, i + 6), 16));
i += 6;
continue;
}
var cp = s.codePointAt(i);
units.push(cp);
i += cp > 0xFFFF ? 2 : 1;
}
return units;
}
// 统计一行 chars 的码位数 (CE / MC 都按码位分列)
function countCodepoints(str) {
return decodeChars(str).length;
}
// 用采样法估算字形实际占用的像素宽?(旧字形实? 保留给外部调试使?
function measureTrimmed(img, x0, y0, w, h) {
if (!img) return 0;
try {
if (typeof document === 'undefined') return 0;
var cv = document.createElement('canvas');
cv.width = Math.max(1, Math.ceil(w));
cv.height = Math.max(1, Math.ceil(h));
var ctx = cv.getContext('2d');
ctx.drawImage(img, x0, y0, w, h, 0, 0, cv.width, cv.height);
var data = ctx.getImageData(0, 0, cv.width, cv.height).data;
for (var x = cv.width - 1; x >= 0; x--) {
for (var y = 0; y < cv.height; y++) {
if (data[(y * cv.width + x) * 4 + 3] > 0) return x + 1;
}
}
} catch (e) { /* tainted canvas ?*/ }
return 0;
}
// ---------------- 文本解析 ----------------
// 产出 item 列表: {kind:'glyph'|'space'|'image'|'break', cp, style, ...}
function cloneStyle(s) {
return {
color: s.color, bold: s.bold, italic: s.italic, underlined: s.underlined,
strikethrough: s.strikethrough, obfuscated: s.obfuscated,
font: s.font, shadow: s.shadow, gradId: s.gradId
};
}
function defaultStyle() {
return { color: { r: 255, g: 255, b: 255, a: 1 }, shadow: true };
}
/**
* 解析带标签的文本, 产出样式化字形序列
* @returns {items: Array, width:number, height:number}
*/
function parseText(text, opts) {
var o = opts || {};
var resolveTags = o.resolveTags !== false && options.resolveTags !== false;
// 两个命名空间各自可关: 关掉时标签按普通文本原样显示 (方便看到原始配置)
var mmOn = resolveTags && o.resolveMiniMessage !== false && options.resolveMiniMessage !== false;
var ceOn = resolveTags && o.resolveCeTags !== false && options.resolveCeTags !== false;
o = Object.assign({}, o, { mmOn: mmOn, ceOn: ceOn });
var items = [];
var style = o.style ? cloneStyle(o.style) : defaultStyle();
if (o.baseColor) style.color = o.baseColor;
var grads = {}; // gradId -> {type:'gradient'|'rainbow', stops, phase, reversed}
var gradSeq = 0;
var fontStack = [];
var lines = []; // 行高累计
var lineH = LINE_HEIGHT;
var lineCount = 1;
var s = String(text == null ? '' : text);
var i = 0;
var guard = 0;
while (i < s.length && guard++ < 200000) {
var ch = s[i];
// ---- 转义 ----
if (ch === '\\' && i + 1 < s.length && (s[i + 1] === '<' || s[i + 1] === '\\' || s[i + 1] === '&')) {
pushGlyph(items, s[i + 1], style);
i += 2; continue;
}
// ---- 旧版 § 颜色码 ----
if (ch === '\u00a7' && i + 1 < s.length) {
var code = s[i + 1].toLowerCase();
if (LEGACY_COLORS[code]) {
style.color = hexToRgb(LEGACY_COLORS[code]);
style.bold = style.italic = style.underlined = style.strikethrough = style.obfuscated = false;
i += 2; continue;
}
if (LEGACY_FORMATS[code]) { style[LEGACY_FORMATS[code]] = true; i += 2; continue; }
if (code === 'r') { style = defaultStyle(); i += 2; continue; }
}
// ---- & 颜色码 (部分配置文件使用) ----
if (ch === '&' && i + 1 < s.length && /^[0-9a-fk-or]$/i.test(s[i + 1])) {
var c2 = s[i + 1].toLowerCase();
if (LEGACY_COLORS[c2]) { style.color = hexToRgb(LEGACY_COLORS[c2]); i += 2; continue; }
if (LEGACY_FORMATS[c2]) { style[LEGACY_FORMATS[c2]] = true; i += 2; continue; }
if (c2 === 'r') { style = defaultStyle(); i += 2; continue; }
}
// ---- 换行 ----
if (ch === '\n') {
items.push({ kind: 'break' });
lineCount++;
i++; continue;
}
// ---- MiniMessage / CE 标签 ----
if (ch === '<' && resolveTags) {
var close = findTagEnd(s, i);
if (close > i) {
var raw = s.slice(i + 1, close);
var handled = applyTag(raw, style, grads, function (g) { gradSeq = Math.max(gradSeq, g); },
items, o, fontStack);
if (handled) {
if (handled === 'break') { lineCount++; }
if (handled === 'consumed') { /* nothing */ }
i = close + 1;
continue;
}
}
}
// ---- 普通字符 ----
var cp = s.codePointAt(i);
var clen = cp > 0xFFFF ? 2 : 1;
pushGlyph(items, s.slice(i, i + clen), style);
i += clen;
}
// ---- 渐变着色 ----
applyGradients(items, grads);
// ---- 测量 ----
var font = glyphsSync();
var width = 0, maxW = 0, maxH = lineH;
for (var k = 0; k < items.length; k++) {
var it = items[k];
if (it.kind === 'break') { maxW = Math.max(maxW, width); width = 0; maxH += lineH; continue; }
width += itemAdvance(it);
}
maxW = Math.max(maxW, width);
return { items: items, width: maxW, height: maxH, lines: Math.max(1, maxH / lineH) };
}
function pushGlyph(items, str, style) {
var cp = str.codePointAt(0);
items.push({ kind: 'glyph', cp: cp, ch: str, style: cloneStyle(style), gradId: style.gradId });
}
function findTagEnd(s, start) {
// <tag> / <tag:arg> / <tag:'a:b'> ; 只在遇到 ' 时跳?
var quote = null;
for (var i = start + 1; i < s.length; i++) {
var c = s[i];
if (c === '\n') return -1;
if (quote) { if (c === quote) quote = null; continue; }
if (c === "'" || c === '"') { quote = c; continue; }
if (c === '>') return i;
if (c === '<') return -1;
}
return -1;
}
// 将内?MiniMessage 样式?(?"'<!shadow><white>'") 应用到临时样?
function styleFromFormat(fmt, base) {
var st = cloneStyle(base);
if (!fmt) return st;
var inner = String(fmt).trim();
if ((inner.charAt(0) === "'" && inner.charAt(inner.length - 1) === "'") ||
(inner.charAt(0) === '"' && inner.charAt(inner.length - 1) === '"')) {
inner = inner.slice(1, -1);
}
var re = /<([^<>]*)>/g;
var m;
while ((m = re.exec(inner)) !== null) {
var nm = m[1].replace(/^\//, '');
var negated = nm.charAt(0) === '!';
if (negated) nm = nm.slice(1);
var low = nm.toLowerCase();
var deco = DECOR_ALIASES[low];
if (deco) { st[deco] = !negated; continue; }
if (low === 'shadow') { st.shadow = !negated; continue; }
if (NAMED_COLORS[low]) { if (!negated) st.color = hexToRgb(NAMED_COLORS[low]); continue; }
var hex = parseColor(low);
if (hex && /^#/.test(low)) { if (!negated) st.color = hex; continue; }
}
return st;
}
var _gradCounter = 0;
/**
* 处理一个标签
* @returns {string|undefined|boolean} 处理结果: 'break' 表示换行, true 表示已消费
*/
// 把「无法求值的动态标签」显示成灰色占位符, 让用户看得见这里有什么
function pushPlaceholder(items, text, style) {
var st = cloneStyle(style);
st.color = hexToRgb('#7F7F7F');
st.shadow = false;
for (var i = 0; i < text.length; i++) pushGlyph(items, text[i], st);
}
function applyTag(raw, style, grads, bump, items, o, fontStack) {
var name = raw;
var arg = '';
var ci = raw.indexOf(':');
if (ci !== -1) { name = raw.slice(0, ci); arg = raw.slice(ci + 1); }
name = name.trim();
var closing = false, negated = false;
// </i> / <!i> / <!/i> 三种关闭写法都要认 (前缀顺序任意)
for (var pass = 0; pass < 2; pass++) {
if (name.charAt(0) === '/') { closing = true; name = name.slice(1); }
else if (name.charAt(0) === '!') { negated = true; name = name.slice(1); }
else break;
}
var low = name.toLowerCase();
var off = closing || negated;
var mm = o.mmOn !== false; // MiniMessage 命名空间是否启用
var ce = o.ceOn !== false; // CraftEngine 命名空间是否启用
// ==================== MiniMessage ====================
// 装饰: <i>/<italic>/<em> ... ; <!i>/</i> = 去掉该样式
var deco = DECOR_ALIASES[low];
if (deco) {
if (!mm) return false;
style[deco] = !off;
return true;
}
if (low === 'reset') {
if (!mm) return false;
var d = defaultStyle();
Object.keys(d).forEach(function (k) { style[k] = d[k]; });
style.bold = style.italic = style.underlined = style.strikethrough = style.obfuscated = false;
style.gradId = null;
return true;
}
if (low === 'newline' || low === 'br') {
if (!mm) return false;
items.push({ kind: 'break' });
return 'break';
}
// 阴影: <shadow> / <!shadow> / </shadow> / <shadow:#rrggbb>
if (low === 'shadow') {
if (!mm) return false;
style.shadow = !off;
if (!off && arg) { var sc = parseColor(unquote(arg)); if (sc) style.shadowColor = sc; }
return true;
}
// 颜色
if (low === 'color' || low === 'colour') {
if (!mm) return false;
if (off) { style.color = null; return true; }
var c = parseColor(unquote(arg));
if (c) style.color = c;
return true;
}
if (NAMED_COLORS[low]) {
if (!mm) return false;
style.color = off ? null : hexToRgb(NAMED_COLORS[low]);
return true;
}
var hx = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(low);
if (hx) {
if (!mm) return false;
style.color = off ? null : parseColor(low);
return true;
}
if (low === 'font') {
if (!mm) return false;
if (off) {
style.font = fontStack && fontStack.length ? fontStack.pop() : null;
} else {
if (fontStack) fontStack.push(style.font || null);
style.font = unquote(arg);
}
return true;
}
// 渐变 / 彩虹 / 过渡
if (low === 'gradient' || low === 'rainbow' || low === 'transition') {
if (!mm) return false;
if (off) { style.gradId = null; return true; }
var id = '$g' + (++_gradCounter);
if (low === 'rainbow') {
grads[id] = { type: 'rainbow', phase: parseFloat(unquote(arg)) || 0 };
} else {
var stops = splitArgs(arg).map(function (x) { return parseColor(unquote(x)); }).filter(Boolean);
grads[id] = { type: low === 'transition' ? 'transition' : 'gradient', stops: stops.length ? stops : [hexToRgb('#FFFFFF')] };
}
style.gradId = id;
return true;
}
// 点击 / 悬浮 / 插入: 改变的是交互而非外观, 预览里直接消费掉
if (MM_OPAQUE[low]) {
if (!mm) return false;
return true;
}
// 翻译: 先查工程译文, 查不到就灰占位
if (MM_TRANSLATE[low]) {
if (!mm) return false;
var tval = langLookup(unquote(arg));
if (tval != null) { parseTextInto(tval, style, items, grads, o); return true; }
pushPlaceholder(items, '<' + low + ':' + arg + '>', style);
return true;
}
// 键位 / 选择器 / 计分板 / NBT: 预览无法求值 → 灰占位
if (MM_PLACEHOLDER[low]) {
if (!mm) return false;
pushPlaceholder(items, '<' + low + ':' + arg + '>', style);
return true;
}
// ==================== CraftEngine ====================
if (!ce) return false;
if (low === 'shift') {
var px = null;
if (/^-?\d+(\.\d+)?$/.test(unquote(arg))) px = parseFloat(unquote(arg));
else if (options.mcRoot) px = null;
if (px != null && isFinite(px)) {
items.push({ kind: 'shift', dx: px, style: cloneStyle(style) });
return true;
}
// 无法解析时按 0 处理 (offset-characters 未配?)
items.push({ kind: 'shift', dx: 0, style: cloneStyle(style) });
return true;
}
if (low === 'image') {
var parts = splitArgs(arg).map(function (x) { return unquote(x); });
var imgId = parts.length >= 2 ? parts[0] + ':' + parts[1] : (parts[0] || '');
var row = parts.length >= 4 ? parseInt(parts[2], 10) : null;
var col = parts.length >= 4 ? parseInt(parts[3], 10) : null;
var fmt = parts.length >= 4 ? parts[4] : (parts.length === 3 ? parts[2] : null);
var st2 = fmt ? styleFromFormat(fmt, style) : cloneStyle(style);
var info = options.resolveImages === false ? null : imageGlyph(imgId, row, col);
items.push({
kind: 'image', id: imgId, row: row, col: col,
info: info, style: st2, tag: '<image:' + arg + '>'
});
return true;
}
if (low === 'global') {
var ga = splitArgs(arg).map(function (x) { return x; });
var gid = unquote(ga[0] || '');
if (options.resolveGlobals !== false && _projectData.globals[gid] != null) {
var body = String(_projectData.globals[gid]);
// 支持 <arg:0> 等索引参?
var args = ga.slice(1).map(function (x) { return unquote(x); });
body = body.replace(/<arg:(\d+)>/g, function (m, idx) {
var v = args[parseInt(idx, 10)];
return v != null ? v : m;
});
var sub = parseTextInto(body, style, items, grads, o);
return true;
}
// 未定? 原样保留
return false;
}
// CE 的 i18n(服务端语言) / l10n(客户端语言); MiniMessage 的 lang/translate 已在上方处理
if (low === 'i18n' || low === 'l10n') {
var key = unquote(arg);
var val = langLookup(key);
if (val != null) { parseTextInto(val, style, items, grads, o); return true; }
return false;
}
if (low === 'expr') {
var ea = splitArgs(arg);
var fmt = unquote(ea[0]);
var expr = ea.length > 1 ? unquote(ea.slice(1).join(':')) : '';
var r = evalExpr(expr);
if (r !== null) {
var txt = fmt === 'bool' ? (r ? 'true' : 'false')
: (fmt ? formatNumber(r, fmt) : String(Math.round(r * 100) / 100));
parseTextInto(txt, style, items, grads, o);
return true;
}
return false;
}
if (low === 'random') {
var ra = splitArgs(arg);
var rid = unquote(ra[0] || 'random');
var val2 = randomRoll(rid, ra.slice(1).map(function (x) { return unquote(x); }));
if (val2 !== null) { parseTextInto(val2, style, items, grads, o); return true; }
return false;
}
if (low === 'arg' || low === 'viewer_arg' || low === 'var' || low === 'papi' ||
low === 'viewer_papi' || low === 'rel_papi') {