agentcss 0.0.0-stage → 0.11.2

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.
Files changed (274) hide show
  1. package/01-tokens/README.md +236 -0
  2. package/01-tokens/behavior/dismissable.js +132 -0
  3. package/01-tokens/behavior/emit.js +76 -0
  4. package/01-tokens/behavior/focus-return.js +81 -0
  5. package/01-tokens/behavior/roving.js +152 -0
  6. package/01-tokens/behavior/typeahead.js +124 -0
  7. package/01-tokens/focus-ring-demo.html +133 -0
  8. package/01-tokens/focus-ring.css +102 -0
  9. package/01-tokens/page.css +244 -0
  10. package/01-tokens/polyfill.js +116 -0
  11. package/01-tokens/responsive-demo.html +171 -0
  12. package/01-tokens/scrollbar-demo.html +194 -0
  13. package/01-tokens/scrollbar.css +179 -0
  14. package/01-tokens/theme-toggle.js +103 -0
  15. package/01-tokens/tokens.css +972 -0
  16. package/01-tokens/typography.css +156 -0
  17. package/02-primitives/badge/README.md +71 -0
  18. package/02-primitives/badge/badge.css +141 -0
  19. package/02-primitives/badge/demo.html +101 -0
  20. package/02-primitives/button/README.md +99 -0
  21. package/02-primitives/button/button.css +353 -0
  22. package/02-primitives/button/demo.html +200 -0
  23. package/02-primitives/card/README.md +145 -0
  24. package/02-primitives/card/card.css +246 -0
  25. package/02-primitives/card/demo.html +170 -0
  26. package/02-primitives/choice/README.md +161 -0
  27. package/02-primitives/choice/choice.css +395 -0
  28. package/02-primitives/choice/demo.html +277 -0
  29. package/02-primitives/combobox/README.md +79 -0
  30. package/02-primitives/combobox/combobox.css +223 -0
  31. package/02-primitives/combobox/combobox.js +485 -0
  32. package/02-primitives/combobox/demo.html +109 -0
  33. package/02-primitives/date-range/README.md +100 -0
  34. package/02-primitives/date-range/date-range.css +206 -0
  35. package/02-primitives/date-range/date-range.js +301 -0
  36. package/02-primitives/date-range/demo.html +104 -0
  37. package/02-primitives/input/README.md +170 -0
  38. package/02-primitives/input/demo.html +246 -0
  39. package/02-primitives/input/input.css +239 -0
  40. package/02-primitives/popover/README.md +80 -0
  41. package/02-primitives/popover/demo.html +150 -0
  42. package/02-primitives/popover/popover.css +187 -0
  43. package/02-primitives/popover/popover.js +331 -0
  44. package/02-primitives/progress/README.md +82 -0
  45. package/02-primitives/progress/demo.html +154 -0
  46. package/02-primitives/progress/progress.css +172 -0
  47. package/02-primitives/select/README.md +95 -0
  48. package/02-primitives/select/demo.html +121 -0
  49. package/02-primitives/select/select.css +151 -0
  50. package/02-primitives/select/select.js +549 -0
  51. package/02-primitives/separator/README.md +63 -0
  52. package/02-primitives/separator/demo.html +84 -0
  53. package/02-primitives/separator/separator.css +73 -0
  54. package/02-primitives/skeleton/README.md +69 -0
  55. package/02-primitives/skeleton/demo.html +110 -0
  56. package/02-primitives/skeleton/skeleton.css +96 -0
  57. package/02-primitives/switch/README.md +65 -0
  58. package/02-primitives/switch/demo.html +115 -0
  59. package/02-primitives/switch/switch.css +370 -0
  60. package/03-patterns/accordion/README.md +121 -0
  61. package/03-patterns/accordion/accordion.css +453 -0
  62. package/03-patterns/accordion/accordion.js +369 -0
  63. package/03-patterns/accordion/demo.html +223 -0
  64. package/03-patterns/content/README.md +103 -0
  65. package/03-patterns/content/content.css +286 -0
  66. package/03-patterns/content/demo.html +88 -0
  67. package/03-patterns/drawer/README.md +79 -0
  68. package/03-patterns/drawer/demo.html +101 -0
  69. package/03-patterns/drawer/drawer.css +129 -0
  70. package/03-patterns/drawer/drawer.js +258 -0
  71. package/03-patterns/dropdown/README.md +78 -0
  72. package/03-patterns/dropdown/demo.html +101 -0
  73. package/03-patterns/dropdown/dropdown.css +135 -0
  74. package/03-patterns/dropdown/dropdown.js +451 -0
  75. package/03-patterns/form-validation/README.md +147 -0
  76. package/03-patterns/form-validation/demo.html +280 -0
  77. package/03-patterns/form-validation/form-validation.css +263 -0
  78. package/03-patterns/list/README.md +150 -0
  79. package/03-patterns/list/demo.html +225 -0
  80. package/03-patterns/list/flip.js +201 -0
  81. package/03-patterns/list/list.css +162 -0
  82. package/03-patterns/nav/README.md +225 -0
  83. package/03-patterns/nav/demo.html +131 -0
  84. package/03-patterns/nav/nav.css +314 -0
  85. package/03-patterns/nav/nav.js +430 -0
  86. package/03-patterns/nav/toc.js +111 -0
  87. package/03-patterns/overlay/README.md +196 -0
  88. package/03-patterns/overlay/demo.html +241 -0
  89. package/03-patterns/overlay/overlay.css +319 -0
  90. package/03-patterns/overlay/overlay.js +563 -0
  91. package/03-patterns/pagination/README.md +114 -0
  92. package/03-patterns/pagination/demo.html +165 -0
  93. package/03-patterns/pagination/pagination.css +195 -0
  94. package/03-patterns/pagination/pagination.js +395 -0
  95. package/03-patterns/reveal/README.md +91 -0
  96. package/03-patterns/reveal/demo.html +106 -0
  97. package/03-patterns/reveal/reveal.css +93 -0
  98. package/03-patterns/reveal/reveal.js +172 -0
  99. package/03-patterns/states/README.md +141 -0
  100. package/03-patterns/states/demo.html +240 -0
  101. package/03-patterns/states/states.css +252 -0
  102. package/03-patterns/tabs/README.md +84 -0
  103. package/03-patterns/tabs/demo.html +139 -0
  104. package/03-patterns/tabs/tabs.css +172 -0
  105. package/03-patterns/tabs/tabs.js +307 -0
  106. package/03-patterns/tooltip/README.md +96 -0
  107. package/03-patterns/tooltip/demo.html +78 -0
  108. package/03-patterns/tooltip/tooltip.css +89 -0
  109. package/03-patterns/tooltip/tooltip.js +288 -0
  110. package/03-patterns/tree/README.md +94 -0
  111. package/03-patterns/tree/demo.html +138 -0
  112. package/03-patterns/tree/tree.css +113 -0
  113. package/03-patterns/tree/tree.js +456 -0
  114. package/04-recipes/analysis-report/README.md +80 -0
  115. package/04-recipes/analysis-report/analysis-report.css +187 -0
  116. package/04-recipes/analysis-report/demo.html +274 -0
  117. package/04-recipes/data-showcase/README.md +45 -0
  118. package/04-recipes/data-showcase/demo.html +247 -0
  119. package/04-recipes/detail-page/README.md +93 -0
  120. package/04-recipes/detail-page/demo.html +55 -0
  121. package/04-recipes/detail-page/detail-page.css +190 -0
  122. package/04-recipes/detail-page/full.html +60 -0
  123. package/04-recipes/detail-page/null.html +60 -0
  124. package/04-recipes/detail-page/partial.html +60 -0
  125. package/04-recipes/detail-page/verify-skeleton.py +19 -0
  126. package/04-recipes/longform/README.md +93 -0
  127. package/04-recipes/longform/agc-memory.svg +4 -0
  128. package/04-recipes/longform/content.html +610 -0
  129. package/04-recipes/longform/content.md +51 -0
  130. package/04-recipes/longform/longform.html +627 -0
  131. package/04-recipes/report/README.md +176 -0
  132. package/04-recipes/report/demo.html +355 -0
  133. package/04-recipes/report/report.css +364 -0
  134. package/04-recipes/table/README.md +103 -0
  135. package/04-recipes/table/demo.html +455 -0
  136. package/04-recipes/table/table.css +372 -0
  137. package/04-recipes/tierA-tokens.css +189 -0
  138. package/09-assets/README.md +74 -0
  139. package/09-assets/bar/README.md +94 -0
  140. package/09-assets/bar/bar.css +166 -0
  141. package/09-assets/bar/bar.js +320 -0
  142. package/09-assets/bar/demo.html +176 -0
  143. package/09-assets/echarts-adapter/demo.html +192 -0
  144. package/09-assets/echarts-adapter/echarts-adapter.js +277 -0
  145. package/09-assets/model-viewer/README.md +155 -0
  146. package/09-assets/model-viewer/demo.html +105 -0
  147. package/09-assets/model-viewer/model-viewer.css +143 -0
  148. package/09-assets/model-viewer/model-viewer.js +423 -0
  149. package/09-assets/scientific-plot/README.md +94 -0
  150. package/09-assets/scientific-plot/demo.html +252 -0
  151. package/09-assets/scientific-plot/scientific-plot.css +141 -0
  152. package/09-assets/scientific-plot/scientific-plot.js +610 -0
  153. package/09-assets/sparkline/README.md +153 -0
  154. package/09-assets/sparkline/demo.html +163 -0
  155. package/09-assets/sparkline/sparkline.css +158 -0
  156. package/09-assets/sparkline/sparkline.js +508 -0
  157. package/API.md +332 -0
  158. package/CHANGELOG.md +3116 -0
  159. package/CODE_OF_CONDUCT.md +64 -0
  160. package/CONTRIBUTING.md +89 -0
  161. package/ERRORS.md +1658 -0
  162. package/LICENSE +21 -0
  163. package/README.md +491 -2
  164. package/SECURITY.md +72 -0
  165. package/START-HERE.md +164 -0
  166. package/SUPPORT.md +109 -0
  167. package/adapters/presentation/README.md +113 -0
  168. package/adapters/presentation/check.js +101 -0
  169. package/adapters/presentation/deck.schema.json +74 -0
  170. package/adapters/presentation/to-doc.js +138 -0
  171. package/ai/START-HERE.md +104 -0
  172. package/ai/behaviors.json +380 -0
  173. package/ai/behaviors.schema.json +61 -0
  174. package/ai/capabilities.json +56 -0
  175. package/ai/cli.js +387 -0
  176. package/ai/components.json +1944 -0
  177. package/ai/components.meta.json +168 -0
  178. package/ai/components.schema.json +220 -0
  179. package/ai/contract.schema.json +147 -0
  180. package/ai/deprecations.json +55 -0
  181. package/ai/deprecations.schema.json +85 -0
  182. package/ai/motion-policy.json +22 -0
  183. package/ai/patch.js +323 -0
  184. package/ai/patch.schema.json +63 -0
  185. package/ai/profiles.json +92 -0
  186. package/ai/tokens.audience.json +55 -0
  187. package/ai/tokens.audience.schema.json +73 -0
  188. package/ai/tokens.json +762 -0
  189. package/ai/tokens.schema.json +58 -0
  190. package/ai/tokens.tree.json +752 -0
  191. package/ai/tokens.tree.schema.json +56 -0
  192. package/ai/validate.js +383 -0
  193. package/dist/01-tokens/behavior/dismissable.js +60 -0
  194. package/dist/01-tokens/behavior/emit.js +21 -0
  195. package/dist/01-tokens/behavior/focus-return.js +31 -0
  196. package/dist/01-tokens/behavior/roving.js +77 -0
  197. package/dist/01-tokens/behavior/typeahead.js +61 -0
  198. package/dist/01-tokens/focus-ring.css +1 -0
  199. package/dist/01-tokens/page.css +1 -0
  200. package/dist/01-tokens/polyfill.js +78 -0
  201. package/dist/01-tokens/scrollbar.css +1 -0
  202. package/dist/01-tokens/theme-toggle.js +61 -0
  203. package/dist/01-tokens/tokens.css +1 -0
  204. package/dist/01-tokens/typography.css +1 -0
  205. package/dist/02-primitives/badge/badge.css +1 -0
  206. package/dist/02-primitives/button/button.css +1 -0
  207. package/dist/02-primitives/card/card.css +1 -0
  208. package/dist/02-primitives/choice/choice.css +1 -0
  209. package/dist/02-primitives/combobox/combobox.css +1 -0
  210. package/dist/02-primitives/combobox/combobox.js +439 -0
  211. package/dist/02-primitives/date-range/date-range.css +1 -0
  212. package/dist/02-primitives/date-range/date-range.js +267 -0
  213. package/dist/02-primitives/input/input.css +1 -0
  214. package/dist/02-primitives/popover/popover.css +1 -0
  215. package/dist/02-primitives/popover/popover.js +257 -0
  216. package/dist/02-primitives/progress/progress.css +1 -0
  217. package/dist/02-primitives/select/select.css +1 -0
  218. package/dist/02-primitives/select/select.js +449 -0
  219. package/dist/02-primitives/separator/separator.css +1 -0
  220. package/dist/02-primitives/skeleton/skeleton.css +1 -0
  221. package/dist/02-primitives/switch/switch.css +1 -0
  222. package/dist/03-patterns/accordion/accordion.css +1 -0
  223. package/dist/03-patterns/accordion/accordion.js +219 -0
  224. package/dist/03-patterns/content/content.css +1 -0
  225. package/dist/03-patterns/drawer/drawer.css +1 -0
  226. package/dist/03-patterns/drawer/drawer.js +229 -0
  227. package/dist/03-patterns/dropdown/dropdown.css +1 -0
  228. package/dist/03-patterns/dropdown/dropdown.js +365 -0
  229. package/dist/03-patterns/form-validation/form-validation.css +1 -0
  230. package/dist/03-patterns/list/flip.js +117 -0
  231. package/dist/03-patterns/list/list.css +1 -0
  232. package/dist/03-patterns/nav/nav.css +1 -0
  233. package/dist/03-patterns/nav/nav.js +322 -0
  234. package/dist/03-patterns/nav/toc.js +76 -0
  235. package/dist/03-patterns/overlay/overlay.css +1 -0
  236. package/dist/03-patterns/overlay/overlay.js +398 -0
  237. package/dist/03-patterns/pagination/pagination.css +1 -0
  238. package/dist/03-patterns/pagination/pagination.js +273 -0
  239. package/dist/03-patterns/reveal/reveal.css +1 -0
  240. package/dist/03-patterns/reveal/reveal.js +138 -0
  241. package/dist/03-patterns/states/states.css +1 -0
  242. package/dist/03-patterns/tabs/tabs.css +1 -0
  243. package/dist/03-patterns/tabs/tabs.js +242 -0
  244. package/dist/03-patterns/tooltip/tooltip.css +1 -0
  245. package/dist/03-patterns/tooltip/tooltip.js +213 -0
  246. package/dist/03-patterns/tree/tree.css +1 -0
  247. package/dist/03-patterns/tree/tree.js +383 -0
  248. package/dist/04-recipes/analysis-report/analysis-report.css +1 -0
  249. package/dist/04-recipes/detail-page/detail-page.css +1 -0
  250. package/dist/04-recipes/report/report.css +1 -0
  251. package/dist/04-recipes/table/table.css +1 -0
  252. package/dist/04-recipes/tierA-tokens.css +1 -0
  253. package/dist/09-assets/bar/bar.css +1 -0
  254. package/dist/09-assets/bar/bar.js +222 -0
  255. package/dist/09-assets/echarts-adapter/echarts-adapter.js +185 -0
  256. package/dist/09-assets/model-viewer/model-viewer.css +1 -0
  257. package/dist/09-assets/model-viewer/model-viewer.js +320 -0
  258. package/dist/09-assets/scientific-plot/scientific-plot.css +1 -0
  259. package/dist/09-assets/scientific-plot/scientific-plot.js +568 -0
  260. package/dist/09-assets/sparkline/sparkline.css +1 -0
  261. package/dist/09-assets/sparkline/sparkline.js +265 -0
  262. package/dist/COSTS.md +56 -0
  263. package/docs/AI-CONTRACT.md +197 -0
  264. package/docs/BACKLOG.md +127 -0
  265. package/docs/BENCHMARK.md +252 -0
  266. package/docs/GUIDANCE.md +154 -0
  267. package/docs/INVARIANT.md +372 -0
  268. package/docs/LAYER-BASELINE.md +158 -0
  269. package/docs/RELEASING.md +275 -0
  270. package/docs/RFC-THEME-SCOPE.md +141 -0
  271. package/docs/ROADMAP.md +182 -0
  272. package/index.html +82 -0
  273. package/package.json +107 -4
  274. package/types/index.d.ts +310 -0
@@ -0,0 +1,423 @@
1
+ /*
2
+ * model-viewer.js — 惰性加载的 3D 模型查看器(零依赖 · ES5)
3
+ *
4
+ * ============================================================================
5
+ * ⭐ 设计依据:项目需求
6
+ * ============================================================================
7
+ * 「就放一到两个素材上去就可以了」
8
+ * 「体积和加载的问题,**一定要快**」
9
+ * 「因为这个模块我们可能以后很久都不会去动它,**必须一次性就做对**」
10
+ *
11
+ * ⇒ 三条硬指标(本文件逐条对应):
12
+ * ① **首屏 0 字节 three** —— 点"加载模型"才 `<script>` 注入。
13
+ * 绝对不用 `<script src="three.min.js">` 写在 HTML 里(那是 592KB 白给)。
14
+ * ② **失败必须降级** —— 库没来 / WebGL 不支持 / 模型 404
15
+ * ⇒ 全部显示**明确的中文提示 + 重试按钮**,**绝不白屏**。
16
+ * ③ **令牌驱动** —— 颜色全部走 `var(--*)`,换主题跟着变。
17
+ *
18
+ * ============================================================================
19
+ * 🔴 为什么**不把 three 打包进来**
20
+ * ============================================================================
21
+ * · three.min.js 592KB,本库的硬约束是「零依赖、可拷贝即用」
22
+ * · GLTFLoader / OrbitControls 在素材里是 **ES6 class**(实测),
23
+ * 而本库硬约束是「**JS 一律 ES5**」(精简档 的 WebView 版本不确定)
24
+ * ⇒ **three 属于主项目的责任**,本库只提供**接入规范 + 降级 UI**。
25
+ *
26
+ * ⚠️ 这不是"甩锅",是**分层**:本库管"怎么正确地接",
27
+ * 库本身由主项目按需引入(可以是 CDN、可以是 npm、可以是本地文件)。
28
+ *
29
+ * ============================================================================
30
+ * 用法
31
+ * ============================================================================
32
+ * <div class="mv" data-mv="models/fox.glb"
33
+ * data-mv-height="320"
34
+ * data-mv-three="/vendor/three.min.js"
35
+ * data-mv-gltf="/vendor/GLTFLoader.js"
36
+ * data-mv-orbit="/vendor/OrbitControls.js"></div>
37
+ * <script src="09-assets/model-viewer/model-viewer.js"></script>
38
+ * <script>ModelViewer.update();</script>
39
+ */
40
+ (function () {
41
+ 'use strict';
42
+
43
+ /* ---------- 小工具 ---------- */
44
+
45
+ function el(tag, cls, text) {
46
+ var n = document.createElement(tag);
47
+ if (cls) n.className = cls;
48
+ if (text !== undefined && text !== null) n.textContent = text;
49
+ return n;
50
+ }
51
+
52
+ function hasWebGL() {
53
+ try {
54
+ var c = document.createElement('canvas');
55
+ return !!(c.getContext('webgl2') || c.getContext('webgl') ||
56
+ c.getContext('experimental-webgl'));
57
+ } catch (e) { return false; }
58
+ }
59
+
60
+ /** 注入一个脚本并等它加载完(惰性加载的核心) */
61
+ function loadScript(src) {
62
+ return new Promise(function (resolve, reject) {
63
+ if (!src) { resolve(); return; }
64
+ var s = document.createElement('script');
65
+ s.src = src;
66
+ s.async = false; // 保持顺序 ⇒ three 必须在 loader 之前
67
+ s.onload = function () { resolve(); };
68
+ s.onerror = function () {
69
+ reject(new Error('脚本加载失败:' + src));
70
+ };
71
+ document.head.appendChild(s);
72
+ });
73
+ }
74
+
75
+ function fmtKB(bytes) {
76
+ return Math.round(bytes / 1024) + ' KB';
77
+ }
78
+
79
+ /* ---------- 每个实例 ---------- */
80
+
81
+ function Viewer(node) {
82
+ this.node = node;
83
+ /* 🔴 支持 URL 查询参数覆盖(便于测试降级路径,不必改代码):
84
+ ?mv=NOPE.glb&three=vendor/x.js ⇒ 模拟「模型/引擎 404」 */
85
+ var q = (location.search || '').replace(/^\?/, '');
86
+ function qp(k, dflt) {
87
+ var m = new RegExp('(?:^|&)' + k + '=([^&]*)').exec(q);
88
+ return m ? decodeURIComponent(m[1]) : dflt;
89
+ }
90
+ this.qOverride = !!q;
91
+ this.src = qp('mv', node.getAttribute('data-mv') || '');
92
+ this.h = parseInt(node.getAttribute('data-mv-height') || '320', 10);
93
+ this.threeSrc = qp('three', node.getAttribute('data-mv-three') || '');
94
+ this.gltfSrc = qp('gltf', node.getAttribute('data-mv-gltf') || '');
95
+ this.orbitSrc = qp('orbit', node.getAttribute('data-mv-orbit') || '');
96
+ this.instances = {}; // three 实例(切主题时重建)
97
+ this.state = 'idle';
98
+ this.build();
99
+ }
100
+
101
+ Viewer.prototype.build = function () {
102
+ var self = this;
103
+ var n = this.node;
104
+ n.classList.add('mv');
105
+
106
+ // 🔴 无论如何都先画"占位 + 说明",
107
+ // 绝不留白屏(硬要求)。
108
+ this.ui = el('div', 'mv__ui');
109
+ this.ui.appendChild(this.makeIdle());
110
+ n.appendChild(this.ui);
111
+
112
+ // 加载按钮
113
+ this.btn = el('button', 'mv__btn', '加载 3D 模型');
114
+ this.btn.type = 'button';
115
+ this.btn.addEventListener('click', function () { self.load(); });
116
+ this.ui.appendChild(this.btn);
117
+
118
+ this.hint = el('p', 'mv__hint');
119
+ this.hint.innerHTML = '模型 <code>' + (this.src.split('/').pop() || '—') +
120
+ '</code> 会<b>点击后才下载</b>,首屏不占流量。';
121
+ this.ui.appendChild(this.hint);
122
+
123
+ if (!hasWebGL()) {
124
+ this.fail('这台设备的浏览器不支持 WebGL,无法显示 3D 模型。');
125
+ this.btn.disabled = true;
126
+ }
127
+ };
128
+
129
+ Viewer.prototype.makeIdle = function () {
130
+ var box = el('div', 'mv__stage');
131
+ box.appendChild(el('div', 'mv__ph', '3D 模型'));
132
+ return box;
133
+ };
134
+
135
+ /** 统一的失败呈现:中文说明 + 可重试,绝不白屏 */
136
+ Viewer.prototype.fail = function (msg) {
137
+ if (this.timer) { clearTimeout(this.timer); this.timer = null; }
138
+ this.state = 'error';
139
+ this.ui.innerHTML = '';
140
+ var s = el('div', 'mv__stage mv__stage--err');
141
+ s.appendChild(el('div', 'mv__ph', '3D 模型'));
142
+ var p = el('p', 'mv__err', msg);
143
+ s.appendChild(p);
144
+ this.ui.appendChild(s);
145
+ if (this.btn) this.ui.appendChild(this.btn);
146
+ if (this.btn) {
147
+ this.btn.disabled = false;
148
+ this.btn.textContent = '重试';
149
+ }
150
+ };
151
+
152
+ Viewer.prototype.status = function (msg) {
153
+ if (this.statusNode) { this.statusNode.textContent = msg; }
154
+ };
155
+
156
+ /** 真正的加载:先进度,再注入库,最后建场景 */
157
+ Viewer.prototype.load = function () {
158
+ var self = this;
159
+ if (this.state === 'loading' || this.state === 'ok') return;
160
+ this.state = 'loading';
161
+
162
+ this.ui.innerHTML = '';
163
+ var stage = el('div', 'mv__stage');
164
+ this.statusNode = el('p', 'mv__status', '正在准备…');
165
+ stage.appendChild(this.statusNode);
166
+ this.ui.appendChild(stage);
167
+
168
+ var t0 = (window.performance && performance.now) ? performance.now() : Date.now();
169
+ this.status('正在准备 3D 引擎…');
170
+ this.boot(null, t0, 0);
171
+
172
+ /* 🔴🔴 **超时兜底**(实测:服务器上会卡死在 loading)
173
+ 现象:模型 404 时,服务器返回 `404 + Content-Type 为空`,
174
+ three 的 FileLoader **既不触发 onError 也不触发 onLoad**
175
+ ⇒ 界面永远停在「正在准备…」⇒ **看起来像白屏**。
176
+
177
+ ⚠️ 本地(http)时同样的 404 **会**正常触发 onError
178
+ ⇒ 又是一次「本地过、线上挂」。
179
+
180
+ ⇒ 无论如何,**超过 20 秒没有结果就判失败**,
181
+ 给出可操作提示,绝不让用户盯着一个不动的进度条。 */
182
+ var self0 = this;
183
+ this.timer = setTimeout(function () {
184
+ if (self0.state === 'loading') {
185
+ self0.fail('加载超时(20 秒)。请检查:' +
186
+ '① 模型路径是否正确:' + self0.src +
187
+ '② 服务器是否能返回该文件(404 时也应给明确提示)');
188
+ }
189
+ }, 20000);
190
+ };
191
+
192
+ Viewer.prototype.boot = function (buf, t0, bytes) {
193
+ var self = this;
194
+ // 🔴 顺序必须是 three → GLTFLoader → OrbitControls
195
+ loadScript(this.threeSrc)
196
+ .then(function () { return loadScript(self.gltfSrc); })
197
+ .then(function () { return loadScript(self.orbitSrc); })
198
+ .then(function () { self.initScene(buf, t0, bytes); })
199
+ .catch(function (err) {
200
+ self.fail('3D 引擎加载失败:' + (err && err.message ? err.message : err) +
201
+ '。请检查 three.js / GLTFLoader.js / OrbitControls.js 的路径。');
202
+ });
203
+ };
204
+
205
+ Viewer.prototype.initScene = function (buf, t0, bytes) {
206
+ var self = this;
207
+ if (typeof window.THREE === 'undefined') {
208
+ this.fail('three.js 没有正确加载(THREE 未定义)。请检查 data-mv-three 的路径。');
209
+ return;
210
+ }
211
+
212
+ var W = this.node.clientWidth || 320;
213
+ var H = this.h;
214
+
215
+ // 场景
216
+ var scene = new THREE.Scene();
217
+ var camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 100);
218
+ camera.position.set(0, 0.6, 2.4);
219
+
220
+ var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
221
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); // 🔴 上限 2,控性能
222
+ renderer.setSize(W, H);
223
+ renderer.setClearColor(0x000000, 0);
224
+ this.ui.innerHTML = '';
225
+ this.node.appendChild(renderer.domElement);
226
+ this.statusNode = null;
227
+
228
+ // 光照(三点光 = 通行做法,不是单一环境光)
229
+ scene.add(new THREE.AmbientLight(0xffffff, 0.45));
230
+ var key = new THREE.DirectionalLight(0xffffff, 0.9);
231
+ key.position.set(1, 1.6, 1.4);
232
+ scene.add(key);
233
+ var rim = new THREE.DirectionalLight(0xffffff, 0.35);
234
+ rim.position.set(-1.2, 0.4, -1);
235
+ scene.add(rim);
236
+
237
+ // 控制器
238
+ /* 🔴 实测纠正:three 官方 `examples/js` 里的控制器
239
+ 是挂在 **THREE 上**的(`THREE.OrbitControls`),**不是全局**。
240
+ 我原来写 `window.OrbitControls` ⇒ undefined ⇒ 拖不动。
241
+ 两个来源都试一遍,兼容不同发行版。 */
242
+ var OrbitCtor = (window.THREE && window.THREE.OrbitControls) || window.OrbitControls;
243
+ var controls = null;
244
+ if (OrbitCtor) {
245
+ controls = new OrbitCtor(camera, renderer.domElement);
246
+ controls.enableDamping = true;
247
+ controls.dampingFactor = 0.08;
248
+ controls.enablePan = false; // 移动端禁止平移,避免误触
249
+ controls.minDistance = 1.1;
250
+ controls.maxDistance = 4.5;
251
+ controls.target.set(0, 0.35, 0);
252
+ }
253
+
254
+ // 🔴 同上:GLTFLoader 也挂在 THREE 上
255
+ var GLCtor = (window.THREE && window.THREE.GLTFLoader) || window.GLTFLoader;
256
+ if (!GLCtor) {
257
+ this.fail('GLTFLoader 没有正确加载(THREE.GLTFLoader 未定义)。请检查 data-mv-gltf 的路径。');
258
+ return;
259
+ }
260
+ /* 🔴 **关键修复**(本地能跑、服务器 https 下报
261
+ 「模型解析失败:Failed to fetch」——实测定位):
262
+
263
+ 原来用 `loader.parse(arrayBuffer, '')`:
264
+ three 内部对 glb 里的贴图走 **`fetch`(blob: / 数据 URI)**,
265
+ 在某些 https / CSP 环境会被拦 ⇒ 直接抛 "Failed to fetch"。
266
+
267
+ ⇒ 改用 `loader.load(url, onLoad, onProgress, onError)`:
268
+ three 用自己的 **FileLoader(XHR)** 取文件,**不经过 fetch**
269
+ ⇒ 顺带**白送真实下载进度**(onProgress 有 loaded/total)。
270
+
271
+ ⚠️ 这是"本地过、线上挂"的典型:**本地 http 掩盖了协议差异**。 */
272
+ var loader = new GLCtor();
273
+
274
+ /* 🔴🔴 **改用「一次 GET,校验 + 复用同一份 buffer」**
275
+
276
+ ❌ 先试了 HEAD 预检 + three.load(),**结果请求了两次 fox.glb**
277
+ (HEAD 一次 + GET 一次)⇒ 实测服务器对同一资源第二次拿不到
278
+ ⇒ 界面卡在 loading。
279
+ ⇒ 教训:**预检请求不能"只用一次"**。
280
+ 正确做法:**自己 GET 一次**(能看 status / Content-Type /
281
+ 拿到的是不是 HTML),**并把这份 buffer 直接交给 three.parse**。
282
+
283
+ ⚠️ 本地 http 不会暴露这个问题(两次都 200)⇒ 又一次「本地过、线上挂」。 */
284
+ var self1 = this;
285
+ var xhr = new XMLHttpRequest();
286
+ xhr.responseType = 'arraybuffer';
287
+ xhr.open('GET', this.src, true);
288
+ xhr.onprogress = function (e) {
289
+ if (e.lengthComputable && e.total) {
290
+ self1.lastBytes = e.total;
291
+ self1.status('正在下载模型 ' + Math.round(e.loaded / e.total * 100) + '%');
292
+ }
293
+ };
294
+ xhr.onload = function () {
295
+ if (xhr.status >= 400) {
296
+ self1.fail('模型不存在(HTTP ' + xhr.status + '):' + self1.src);
297
+ return;
298
+ }
299
+ var buf = xhr.response;
300
+ // 🔴 校验 magic:glb 必须是 "glTF" 开头
301
+ if (!buf || buf.byteLength < 12) {
302
+ self1.fail('模型文件是空的或损坏了:' + self1.src);
303
+ return;
304
+ }
305
+ /* 🔴 这里必须用 **Uint8Array** 读字节。
306
+ ArrayBuffer 直接下标取的是「字符」,不是字节 ⇒ 读出来是 0。 */
307
+ var b0 = new Uint8Array(buf, 0, 4);
308
+ var head = String.fromCharCode(b0[0], b0[1], b0[2], b0[3]);
309
+ if (head !== 'glTF') {
310
+ self1.fail('这不是 glb 文件(缺少 glTF 标识,收到的是「' + head + '」)。' +
311
+ '服务器可能返回了 HTML 错误页。');
312
+ return;
313
+ }
314
+ doLoad(buf);
315
+ };
316
+ xhr.onerror = function () { self1.fail('无法访问模型:' + self1.src); };
317
+ xhr.send();
318
+
319
+ function doLoad(buf) {
320
+ loader.parse(buf, '', function (gltf) {
321
+ var model = gltf.scene;
322
+ // 🔴 自动居中 + 按包围盒缩放到合适大小 —— 免得每个模型都要手调相机
323
+ var box = new THREE.Box3().setFromObject(model);
324
+ var size = box.getSize(new THREE.Vector3());
325
+ var center = box.getCenter(new THREE.Vector3());
326
+ model.position.sub(center);
327
+ var maxDim = Math.max(size.x, size.y, size.z) || 1;
328
+ model.scale.setScalar(1.6 / maxDim);
329
+ box.setFromObject(model);
330
+ var c2 = box.getCenter(new THREE.Vector3());
331
+ model.position.sub(new THREE.Vector3(c2.x, box.min.y + (box.max.y - box.min.y) * 0.45, c2.z));
332
+ scene.add(model);
333
+
334
+ if (self.timer) { clearTimeout(self.timer); self.timer = null; }
335
+ self.state = 'ok';
336
+ self.instances = { scene: scene, camera: camera, renderer: renderer,
337
+ model: model, controls: controls };
338
+
339
+ // 渲染循环(只在可见时跑 —— 省电,见 README 的 TODO)
340
+ var last = 0;
341
+ function loop(t) {
342
+ self.raf = requestAnimationFrame(loop);
343
+ // 🔴 页面不可见时**停渲染**(iOS 切后台会掉到 1fps,浪费电量)
344
+ if (document.hidden) return;
345
+ if (controls) controls.update();
346
+ renderer.render(scene, camera);
347
+ last = t;
348
+ }
349
+ self.raf = requestAnimationFrame(loop);
350
+
351
+ self.ui.innerHTML = '';
352
+ var cap = el('p', 'mv__cap');
353
+ var dt = Math.round(((window.performance && performance.now) ? performance.now() : Date.now()) - t0);
354
+ cap.innerHTML = '模型 <b>' + fmtKB(bytes || self.lastBytes || 0) + '</b> · 引擎就绪耗时 <b>' + dt + 'ms</b>' +
355
+ (controls ? ' · 拖动可旋转,滚轮/双指可缩放' : ' · 未加载控制器,无法拖动');
356
+ self.node.appendChild(cap);
357
+ },
358
+ // onProgress ⇒ 真实进度
359
+ function (evt) {
360
+ if (evt && evt.lengthComputable && evt.total) {
361
+ self.lastBytes = evt.total;
362
+ self.status('正在下载模型 ' + Math.round(evt.loaded / evt.total * 100) + '%');
363
+ }
364
+ },
365
+ // onError
366
+ function (err) {
367
+ var msg = (err && err.message) ? err.message : (err || '未知错误');
368
+ /* 🔴 **实测定位的"本地过、线上挂"**:
369
+ 服务器的 CSP 是
370
+ img-src 'self' data: https: ← 没有 blob:
371
+ connect-src 'self' https: ← 没有 blob:
372
+ 而 three 的 GLTFLoader 对 **内嵌贴图会创建 Blob URL**
373
+ (PNG 太大,不能用 data URI)⇒ 被 CSP 拦 ⇒ "Failed to fetch"。
374
+
375
+ ⚠️ 本地 http 没有 CSP ⇒ 完全看不出来(我第一次就踩了)。
376
+
377
+ ⇒ 这里给**可操作的**提示,而不是把 "Failed to fetch" 抛给用户。 */
378
+ if (msg.indexOf('fetch') > -1 || msg.indexOf('Failed') > -1) {
379
+ self.fail('模型加载失败:服务器的 Content-Security-Policy 缺少 blob:,' +
380
+ '内嵌贴图被浏览器拦下了。需要在 CSP 的 img-src 与 connect-src 里加上 blob:。');
381
+ } else {
382
+ self.fail('模型加载失败:' + msg + '。请确认 ' + self.src + ' 已部署且可访问。');
383
+ }
384
+ });
385
+ } // ← doLoad 结束
386
+ };
387
+
388
+ /* ---------- 对外 API ---------- */
389
+
390
+ var ModelViewer = {
391
+ /** 扫描并初始化页面里的所有查看器 */
392
+ update: function (root) {
393
+ var list = (root || document).querySelectorAll('[data-mv]');
394
+ for (var i = 0; i < list.length; i++) {
395
+ if (list[i].__mv) continue;
396
+ list[i].__mv = new Viewer(list[i]);
397
+ }
398
+ },
399
+ /** 换主题后调用:three 里的颜色跟随令牌,需要重建 */
400
+ refresh: function (root) {
401
+ var list = (root || document).querySelectorAll('[data-mv]');
402
+ for (var i = 0; i < list.length; i++) {
403
+ var v = list[i].__mv;
404
+ if (!v || v.state !== 'ok') continue;
405
+ if (v.raf) cancelAnimationFrame(v.raf);
406
+ if (v.instances && v.instances.renderer) {
407
+ try { v.instances.renderer.dispose(); } catch (e) {}
408
+ }
409
+ list[i].innerHTML = '';
410
+ list[i].__mv = new Viewer(list[i]);
411
+ }
412
+ },
413
+ };
414
+
415
+ if (typeof window !== 'undefined') window.ModelViewer = ModelViewer;
416
+
417
+ // @autoinit: [data-mv] —— 扫属性自动 update()(详见 API.md「自动初始化」)
418
+ if (document.readyState === 'loading') {
419
+ document.addEventListener('DOMContentLoaded', function () { ModelViewer.update(); });
420
+ } else {
421
+ ModelViewer.update();
422
+ }
423
+ })();
@@ -0,0 +1,94 @@
1
+ # scientific-plot · 二维科学绘图
2
+
3
+ 原生 SVG 画的二维坐标系绘图模块。**零第三方运行时依赖**,ES5,和 `09-assets/sparkline` 同级。
4
+
5
+ ## 它和 BI 图表不是一回事
6
+
7
+ | | BI / 营运图表(`sparkline`、`echarts-adapter`) | 科学绘图(本模块) |
8
+ |---|---|---|
9
+ | 回答的问题 | 「这个月比上个月好还是差」 | 「这些测量值落在哪、误差多大、有没有超模型」 |
10
+ | 坐标 | 多为分类轴 | **数值轴**,线性 / 对数显式声明 |
11
+ | 数据语义 | 趋势 | 值 + **不确定性**(误差棒 / 区间带) |
12
+ | 缺失 | 少见 | 常见,且**必须与零区分** |
13
+
14
+ 两者共享设计令牌、字体、容器与响应式基础,
15
+ 但**不共享**坐标与数据格式规则 ⇒ 本模块不依赖 BI 图表的内部实现。
16
+
17
+ ## 用法
18
+
19
+ ```html
20
+ <link rel="stylesheet" href="01-tokens/tokens.css">
21
+ <link rel="stylesheet" href="09-assets/scientific-plot/scientific-plot.css">
22
+ <script src="09-assets/scientific-plot/scientific-plot.js"></script>
23
+ <div id="fig"></div>
24
+ <script>
25
+ ScientificPlot.render(document.getElementById('fig'), {
26
+ width: 680, height: 340,
27
+ title: '阻尼振荡位移', // 进 aria-label
28
+ x: { label: '时间', unit: 's', scale: 'linear', domain: [0, 4] },
29
+ y: { label: '位移', unit: 'm', scale: 'linear', domain: [-1.1, 1.1] },
30
+ series: [{ type: 'function', fn: function (t) {
31
+ return Math.exp(-0.35 * t) * Math.cos(2 * Math.PI * 1.2 * t);
32
+ }, domain: [0, 4], samples: 400 }],
33
+ refs: [{ axis: 'y', value: 0 }]
34
+ });
35
+ </script>
36
+ ```
37
+
38
+ ## API
39
+
40
+ `ScientificPlot.render(el, spec)` 返回 `{ svg, x, y, ticks, series }`(比例尺对象可用于后续交互)。
41
+
42
+ **坐标轴** `spec.x` / `spec.y`
43
+
44
+ | 字段 | 说明 |
45
+ |---|---|
46
+ | `label` | 轴名。**不给会退化成 "x"/"y"**,正式图请给 |
47
+ | `unit` | 单位,会拼到轴名后(如「时间 / s」) |
48
+ | `scale` | `'linear'`(默认)或 `'log'` |
49
+ | `domain` | `[min, max]`,必填。对数轴必须 `> 0`,否则**抛错** |
50
+
51
+ **数据系列** `spec.series[]`
52
+
53
+ | `type` | `data` 形状 | 说明 |
54
+ |---|---|---|
55
+ | `line` | `[[x, y], ...]` | 折线;`null` 处**断开** |
56
+ | `scatter` | `[[x, y], ...]` | 散点 |
57
+ | `function` | 传 `fn` + `domain` + `samples` | 采样成点后画线,**不做表达式求值** |
58
+ | `errorbar` | `[[x, y, yerr], ...]` 或 `[[x, y, lo, hi], ...]` | 误差棒,只画给定边界 |
59
+ | `band` | `[[x, lo, hi], ...]` | 置信区间带(填充多边形) |
60
+
61
+ 其他:`refs[]`(参考线 `{axis, value, label}`)、`annotations[]`(点标注 `{x, y, text}`)、
62
+ `grid`(默认开)、`truncated`(轴截断时置 `true`,图上会打提示)、`margin`、`className`。
63
+
64
+ ## 它**不**做什么(诚实边界)
65
+
66
+ - **不计算任何统计量**:均值、标准误、标准差、置信区间都由调用方算好传进来。
67
+ 本模块也**不宣称**显著性 —— 那是调用方的判断,不是绘图的职责。
68
+ - **不做表达式求值**:函数曲线只接受真函数。传字符串会被忽略,**绝不 `eval`**。
69
+ - **不做插值 / 回归 / 平滑**:只连你给的点。
70
+ - **不做**直方图 / 箱线图 / 小提琴图 / 热力图 / 等高线 —— 这些留待后续,
71
+ 确有需要时接入成熟绘图库,不打包进本库必选运行时。
72
+ - **不做大规模交互**(缩放、框选、十字线)—— 第一版聚焦"画对"。
73
+
74
+ ## 颜色与线型
75
+
76
+ 分类色 5 个(`--accent / --danger / --success / --warning / --info`,与 `echarts-adapter` 同一套)。
77
+ **第 6 条曲线不会循环回第 1 个颜色**,而是切换线型(虚线 / 点线…)——
78
+ 因为两条同色曲线读者分不清谁是谁。
79
+
80
+ ## 预览
81
+
82
+ ```bash
83
+ # 仓库根起静态服务
84
+ python -m http.server 8000
85
+ # 打开
86
+ http://127.0.0.1:8000/09-assets/scientific-plot/demo.html
87
+ ```
88
+
89
+ ## 已知限制
90
+
91
+ - 未在 iOS / Android 真机 WebView 上验证(与库整体一致)。
92
+ - 刻度标签未做"相邻标签重叠时自动抽稀"(对数轴跨大数量级时已按 10 的幂简化)。
93
+ - 无数据点级别的无障碍替代(图表整体有 `aria-label`,逐点值需调用方在页面上另给文本等价说明,
94
+ demo.html 第 5 节演示了做法)。