入口:https://toyosatomiminomiko.github.io/miko_graphcalc/
GraphCalc 的当前入口是 index.html,它加载 src/main.ts,再由
DslApp 驱动 .miko DSL.
这是符号计算引擎(Symbolic Computation Engine)和Plotter, 作为计算机代数系统(Computer Algebra System, CAS)是不完全的.
源码 textarea
-> Rust/WASM 解析器 parse_miko()
-> AstProgram
-> DslCompiler.compileScene()
-> SceneIR
-> Plotter / CameraManager / DslIntegralRenderer
-> Three.js 场景
- 内置示例菜单:
source code窗口标题栏的"示例"按钮打开分组清单 (求导 / 偏导 与 其他主题),选中即整段替换编辑器源码并立即运行. 示例文本在构建期由import.meta.glob(..., { query: '?raw' })从example/*.miko内联进 bundle(运行时不 fetch,离线可用),example/仍是唯一真相源;清单与文件集的一一对应由src/ui/examples/exampleCatalog.test.ts守住.载入走"全选 +execCommand('insertText')"覆盖而不是直接赋值,浏览器原生撤销栈得以 保留,一次 Ctrl+Z 就能退回载入前手写的代码(Chromium 152 / Firefox 155 实测;撤销环节的取舍见src/ui/examples/replaceEditorSource.ts) param:参数面板与实时刷新;param φ = 0 in cyclic [-3.14159, 3.14159, 0.01]显式声明循环类系数(球坐标方位角这类圆周量),越界值按区间长度回绕到[min, max)而不是夹到端点;不写cyclic的参数一律按普通参数处理. 每条滑块行末端的reset把该参数退回in前的声明值(param a = 1 in [...]里的1),单项复位不必重按"运行";已经停在该值上时按钮置灰curve/surface/vector_field/point/vector:基础几何对象- 对象相加:
curve/surface的表达式可以按名引用同类对象,+就是逐点 函数相加(curve c3 = c1 + c2即 y = f1(x) + f2(x),surface s3 = s1 + s2即 z = f1(x,y) + f2(x,y));加减乘除按普通表达式优先级生效,链式相加 (curve c4 = c3 + c1)与任意声明顺序都成立,成环报错.引用在归一化之前 展开成被引用对象自己的表达式与区间:没有显式range时定义域取被引用 对象区间的交集(x 区间 / x-y 矩形,交集为空报错),显式range优先; curve 只能引用 curve,surface 只能引用 surface,引用体积/点/向量/implicit 对象或derivative等产物会报错而不是悄悄变成自由参数.相加结果照常参与region/integral/derivative;示例example/object_addition.miko region:面积图形(两条曲线围成的 x 型带状区域,绘制在 z=0 平面),可 作为二重积分的积分域;边界曲线只允许不带静态变换/动画的纯函数曲线matrix/transform:对象场景变换animation:单矩阵动画片段,可通过对象animation = [...]绑定并顺序播放derivative:求导语句,把符号求导结果做成一个新对象并画出整条导数 函数曲线/曲面(curve -> curve 求 x 导,surface -> surface 求 x/y 偏导); 语法derivative 名称 = derivative(源对象 [, 变量]),函数名用全名不缩写; 示例example/derivative_graph.mikoimplicit:f(x,y)=0(二维等值线)或f(x,y,z)=0(三维等值面)的隐式 标量场,dim 由表达式里出现的坐标变量推断,level选项给出方程右端 (缺省 0).V1 只有方程本体参与gradient/derivative分析,本体的 marching squares/cubes 采网渲染留到后续- 隐式场求导:体积对象(
sphere)与implicit对象没有解出因变量,derivative(sphere|implicit)求的是梯度 ∇f,产物是一个vector_field(复用向量场的采样/渲染管线);gradient g = grad(sphere|implicit) at [x, y, z]在空间点取 ∇f,先沿梯度牛顿投影到等值面再画点/法向/切平面 (3Dat语法上至少两个坐标,第三个缺省按 0 补;∇f=0 处报错). 分析点也可显式写球坐标at spherical(θ, φ)(r 取球体半径)或at spherical(r, θ, φ);θ/φ 约定由numericConfig.analysis. sphericalAngleConvention全局配置(默认 physics:θ 从 +Z 量起;可切 math),结果列表同时回显[r, θ, φ].示例example/sphere_gradient.miko gradient/divergence/curl/laplacian:点分析(求导/偏导经这些 微分分析算子暴露:一元求导 = curve 的 gradient,偏导 = surface 的 gradient,div/curl = 向量场的一阶偏导组合,laplacian = 标量场的∇²f = f_xx + f_yy + f_zz;用户文档见docs/derivatives-guide.md)gradient的show元素:通用point/normal;曲面(偏导)与三维 隐式场/球体可加tangent_plane画切平面,一元曲线与二维隐式曲线可加tangent画切线;曲线求导与二维隐式曲线不写show时默认画[point, normal, tangent],让切线始终可见,其余分析默认[point, normal].示例集见example/README.md: 一元求导example/derivative_curve.miko与求导法则对照example/derivative_rules.miko,偏导example/partial_derivative_surface.miko, 散度/旋度example/divergence_vector_field.miko与example/curl_vector_field.miko, 拉普拉斯example/laplacian_scalar_field.miko与调和场对照example/laplacian_harmonic.mikointegral:数值积分 + 黎曼/梯形/辛普森/勒贝格可视化,方法为trapezoid/simpson/lebesgue,以及黎曼系列riemann:left/riemann:right/riemann:mid;裸写riemann等价于riemann:left. 被积函数缺省是源对象表达式(curve 的 y=f(x),surface 的 z=f(x,y)); region/solid 源可用选项integrand指定任意被积函数(缺省"1", 即求区域面积/体积),变量一律为世界坐标 x/y/z- 积分域:1D
curve区间,2Dsurface矩形,2Dregion面积图形, 3D 体积实体(sphere/box/cylinder/cone/frustum);IR 用显式dim/domainKind描述域,不再用 range 长度推断维度 - 2D 黎曼端点方法(right/mid)已在二维曲面矩形域放开,采样端 = 方法端, 数值与可视化同源;region 域二重积分(累次 B1 / 网格指示 B2),体积域 三重积分(世界网格 C1 / 轴向切片 C2),解析式对拍见 math_rs 单元测试
sphere/box/cylinder/cone/frustum:透明体积图形;cylinder/cone/frustum统一映射为同一个conicIR 类型intersection:求交.曲线参与的求交得到离散交点,曲面/体积参与的求交得到空间交线; 支持 曲线∩曲线,曲线∩曲面,曲线∩体积,曲面∩曲面,曲面∩体积,体积∩体积
相机状态不进入 DSL:透视/正交与旋转锁定由右侧 UI 开关控制,
camera:view 按钮只负责预设视角.
界面形态是"3D 视口铺满 + 七个浮动窗口 + 顶部任务栏":#viewport(Three.js
画布)仍然铺满 #app,七个窗口悬在它上面,桌面空白处照常可以转视角.任务栏是
紧贴 #app 上沿的一条通栏带,窗口几何被夹在它下面,所以它不会被任何窗口遮住.
任务栏(顶部通栏):窗口按钮 ... 全部还原
source code(源码) 参数(参数滑块) 视图(视图控件)
过程(递等式) 诊断(报错 / 警告) 实体对象(实体一栏)
求值对象(六个求值子列表)
- 窗口外壳(
.window/ 标题栏 / 正文 / 八根缩放手柄)与任务栏由库声明式装配 (库miko_ui的mountDesktop/WindowFrame/Dock):index.html里只有一个空#app,窗口层 / 吸附预览 / 任务栏 / 每个窗口的正文容器都由库建; 加窗口只改UI_CONFIG.window.windows一处. - 能力:拖动标题栏移动,八向缩放,最小化(再点任务栏按钮恢复),最大化(填满任务栏 之下的工作区),边缘吸附(左/右半屏,拖到顶部任务栏附近最大化)与窗口间磁吸. 没有关闭与全屏:没有真正的进程可关,关闭与最小化在观感上就是同一件事; 全屏与最大化的差别也只剩"遮不遮任务栏",而任务栏不该被遮.
- 状态与写入点:
WindowManager持有geometry/state/restore/focused/zIndex;几何的唯一写入点是_applyGeometry(逐条setProperty,不碰z-index),状态的唯一写入点是_applyState(所有.window类名,inert/aria-hidden,任务栏按钮的激活态与淡化),z-index只有focus()写. - 隐藏态(最小化)用
opacity: 0+inert+aria-hidden,不用display: none:编辑器行号与高亮层在隐藏期间必须仍能量到尺寸,否则恢复后 对齐会整体错乱. #window-layer整层pointer-events: none(只有.window自己auto), 这是"空桌面处仍能转 3D"的前提;任务栏那条带自己收指针--它占的是工作区 之上的预留带,工作区里没有任何东西需要让路.- 窗口外壳的两个尺寸(任务栏高度
dockReserve,标题栏高度headerHeight)在 挂载时由库从DesktopConfig写到#app的 CSS 变量上,是运行期唯一来源;miko_ui的styles/tokens.css里的同名值只是没有 JS 时的兜底. - 布局不落 localStorage:刷新后回到默认几何,与"界面偏好不落本地存储"的 既有约定一致;设计取舍与逐条理由见 面板窗口化设计计划.
代码区字体,KaTeX 字号与窗口外壳常量不做运行时设置界面,也不落 localStorage:
唯一真相源是 src/config/uiConfig.ts,启动时由 src/app/applyUiConfig.ts
写成 :root 上的 CSS 变量,再由应用层的四份样式表(css/base.css /
panels.css / editor.css / process.css)与 miko_ui 的 styles/ 下的库
样式表(控件 widgets.css,桌面窗口系统 desktop.css,编辑器外壳
editor.css,反馈条目 feedback.css)的 var() 消费.
领域类型图例(--kind-*:曲线/球体/区域...各是什么色)走同一条机制,但真相源
是一张 TS 注册表:src/config/kindPalette.ts 的键取自领域
(SceneObject['kind'] 与分析的 op),由 src/app/applyKindPalette.ts 写成
:root 变量,与 applyUiConfig() 一起在 new DslApp() 之前调用.放 TS 而不写成
CSS 变量的理由是缺一项要能被机器拦住:css/*.css 里每个 var(--kind-*) 与
注册表的键必须互相覆盖(两个方向都查,见 src/config/cssPalette.test.ts),
所以"新增一类对象却忘了配色"的表现是测试红,而不是徽章悄悄没有底色.应用的样式表
里因此一个颜色字面量都没有.这套图例归应用而不是库:库是通用的,不认识"旋转体"
这类概念(--kind-* 曾经借住在库的 styles/tokens.css 里,已收回).
样式表按两层加载,顺序即层叠顺序(入口在 src/main.ts):
- 库层:
import 'miko_ui/styles.css'一行拿到库的全部默认样式,内部顺序 (token -> 滚动条 -> 控件 -> 桌面 -> 编辑器外壳)由库自己的styles.css决定, 应用不插手;库以后加样式表,应用入口不用改; - 应用层:
css/base.css->panels.css->editor.css->process.css,只写应用自己的类 / id / 页面级规则.
滚动条是一条单独的规定:库的 styles/scrollbar.css 是唯一来源,会滚动的
容器挂库公开的 .ui-scrollbar 类即得同一种细滚动条(尺寸 --scrollbar-size,
颜色 --color-scrollbar*,都在库的 token 层),应用侧一条滚动条规则都不写 --
挂类的地方是 src/app/appViews.ts(#params-panel / #view-controls /
源码窗口正文根 #editor-panel / 实体与求值窗口正文根 .object-panel /
诊断窗口的正文根,即库那颗 .message-area),
src/app/windowChrome.ts(示例浮层,库的 createMenu 会把它补成会滚动的
.menu-panel),src/ui/process/ProcessPanel.ts,src/ui/evaluation/evaluationDom.ts
与 integralItem.ts.库自己的组件不挂这个类:菜单件与滚动条规定互不认识,要不要
用由消费者决定.库侧这条规定有独立守卫(miko_ui 的
test/scrollbarStyles.test.ts):只认 .ui-scrollbar,不认识任何组件,也不被任何
组件引用.
滑条属于窗口正文根,不属于窗口里的某个元素:七个窗口里有六个的正文根就是"一个
按顺序往下摞,超出就自己滚的列容器",overflow 与 min-height: 0 都只在这一层
(#params-panel / #view-controls / #editor-panel / .object-panel /
诊断的 .message-area;过程窗口是它自己的几个滚动块 -- 页头不该跟着滚).这一条过去
在实体 / 求值 / 诊断三个窗口上是反的 -- 正文根是个 overflow: hidden 的宿主,真正
滚的是里面那张"库的 .message-area"卡片,滑条于是缩在窗口内的元素里,还要靠逐层的
min-height: 0 把滚动顶下去.源码窗口同样:滑条原来在 .code-editor 里的 textarea
上,现在编辑器随内容长高长宽,由 #editor-panel 滚(库侧口径见 miko_ui README 的
"窗口正文的排布"与 styles/editor.css 的"滚动归属").
整层压而不是逐份交错:交错时"谁赢"由"文件排在第几位"决定,而不是"这块样式归谁
负责".踩过的坑是应用层的 .row-visibility-btn 被排在它后面的库 widgets.css
盖掉,在应用里改 background 完全无效而且不报错.所以应用层不许出现"只由库的类
构成"的选择器,也不许整组照抄库的按钮基线 :where(.ui-button) -- 要改外观就改库
(或给节点加一个应用自有的变体类,只写增量).这条界限由
src/config/styleLayers.test.ts 断言,配色纪律由
src/config/cssPalette.test.ts 断言.
UI_CONFIG.editor:fontFamily/fontSize/lineHeight/tabSize, 作用于左面板源码编辑区(textarea,行号栏与源码高亮层共用同一组值);gutterMinWidth是行号槽宽下限;UI_CONFIG.formula.katexFontSize:对象列表里 KaTeX 公式的字号, 单位 em,基准是.object-expr的 16px;UI_CONFIG.panel:只剩参数区与视图控件的内容下限 (paramsMinHeight/viewControlsMinHeight),由 CSS 消费;UI_CONFIG.window:桌面窗口化的全部几何与常量--七个窗口的标题/Dock 文案/ 默认几何锚点/最小尺寸,标题栏按钮清单(只有最小化与最大化),edgeKeep/edgeGap/headerMinVisible/dockReserve/headerHeight, 三层容器的z-index与吸附阈值.窗口几何不进 CSS(窗口是 JS 建的, 不存在"CSS 首帧"),默认几何由WindowGeometry.resolveDefaultGeometry()按当前桌面尺寸算出 px,由WindowManager写成行内样式;两个外壳尺寸 (--window-header-height与--dock-reserve)在挂载时由库从这份配置 写到#app,库样式表里的同名值只是没有 JS 时的兜底.
改完刷新页面即可(库自己的样式改完想立刻看到,见"构建"一节的本地联调
miko_ui).库样式表(miko_ui 的 styles/tokens.css)的 :root
兜底只负责脚本执行前的首帧,必须与 UI_CONFIG 保持一致--这条约定由
applyUiConfig.test.ts 逐字断言,
只改 uiConfig.ts 或只改 CSS 都会先失败在测试上,不会静默闪一帧旧样式.
行号槽宽不写死:EditorLineNumbers 按当前字体与最大行号位数动态写入
--code-gutter-width.
源码高亮不引入编辑器组件:着色后的源码渲染在 textarea 背后的
#dsl-editor-highlight 层里,textarea 只把文字设为透明(光标/选区/撤销/IME
仍由原生 textarea 负责).透明与显示由 EditorHighlight 在首次渲染成功后加上的
is-highlighted 类同时开关,脚本没跑时它就是一个普通输入框.
两者同处库的 .code-editor-input 网格的一格(grid-area: 1 / 1),尺寸
由布局保证一致;编辑器自己不滚,滑条归源码窗口的正文根 #editor-panel(库侧
见 styles/editor.css 的"滚动归属"),所以历史上那条"textarea 的滚动条要占位,
高亮层不占,两边最大滚动偏移差一个滚动条厚度,抄 scrollTop 会被夹住"的坑
不存在了(EditorHighlight.sync / EditorLineNumbers.sync 保留,写成恒 0,
消费者把 textarea 改回固定尺寸时仍然对齐).
分词与配色见 src/editor/dslHighlight.ts 与 css/editor.css;关键字表由
dslHighlight.test.ts 直接读 src/compiler/compiler_rs/src/miko.pest 校验,
语法文件新增枚举值不会漏.
语法:
intersection 名称 = intersection(对象A, 对象B) {
color = "#ffffff"; // 可选,默认取调色板
segments = 96; // 可选,采样分辨率,最大 256
};
求交结果自动按组合区分:
- 曲线参与的求交(
intersection(c1, s1),intersection(c1, c2),intersection(c1, S))渲染为交点; - 曲面/体积参与的求交(
intersection(s1, s2),intersection(s1, S),intersection(S, B))渲染为三维交线.
体积对象包括球体,方块和旋转体(圆柱/圆锥/圆台).旋转体的交线包含
侧面与上下底面,和它的数学体积定义一致.求交坐标会计入对象的静态
transform,但暂不支持带动画的对象,也不支持 point / vector /
vector_field 参与求交.
示例:
intersection X = intersection(s1, s2) {
color = "#ffffff";
segments = 96;
};
点对象(例如默认源码开头的 point P = [0, 0, 0])的全局样式由
右侧"视图"面板的"点"区域控制:可在"设定大小"与"按比例缩放"
两种模式间切换(二者是同一控制量的不同表达,切换时保持当前实际
大小不变),并可切换为不可见;设置作用于场景中的所有点对象.
XYZ 坐标轴使用 Three.js Line2 绘制,线宽以像素为单位, 可在右侧"视图"面板中调整(最小 1px).
"坐标轴向上"可在 X / Y / Z 三个轴间切换,决定哪个轴的正方向 在视口中朝上,默认 Z(数学/工程习惯);习惯 Y 向上(图形工具)的 用户可自行切换,ViewCube 的上/前/右预设视角会随该设置调整.
网格与坐标轴刻度同样使用 Line2 系列绘制:大刻度线粗而亮, 小刻度线细而暗;右侧"视图"面板可分别开关网格/刻度, 并调整大/小刻度线宽.刻度默认按整数步长排布,显示普通数值; 打开"π 单位"开关后,网格与刻度整体改按 π/2 间隔重排,大刻度落在 π 的整数倍上,刻度数字相应显示为 π/2,π,3π/2 ...(刻度位置与 数字始终一致,不是给整数刻度换标签).刻度数字与 XYZ 轴标签共用同一字体与 缩放设置,随刻度开关一起显隐;"坐标轴"面板的 X/Y/Z 标签开关 可单独隐藏某条轴的标签,并同时隐藏该轴的刻度数字.网格包含 XZ/XY/YZ 三个坐标平面,各有独立开关,同一行排列.
系数名不限于 a/b/c.只要不是 sin/pi/e 等内置符号,
k/omega/theta 这类标识符都会被识别为自由参数.
角度默认使用弧度,和 rotate(pi / 4) 保持一致.需要普通角度时可写
rotate(deg(180));deg() 在 Rust 符号归一化阶段展开为 x * pi / 180.
jacobian:解析器接受,编译器会抛出"暂未实现"- 向量场的逐分量拉普拉斯
∇²F(laplacian作用于vector_field): 编译器会抛出"逐分量拉普拉斯 ∇²F 暂不实现";标量场的laplacian已支持(见docs/derivatives-guide.md§5) scalar/vector张量声明:编译器会抛出"暂未实现"- 积分源必须引用已存在的
curve/surface/region或体积对象; 体积域integral(S)不接受range选项(域 = 渲染出的世界实体) region边界曲线带静态transform或animation:编译期报错region区域本体 V1 不支持变换/动画,不接受未知选项- 对象相加(按名引用同类对象)的目标必须是已声明的
curve/surface对象语句:derivative产出的曲线/曲面暂不能被引用(它是语句产物而不是 对象声明),intersection/integral/gradient等产物同理,引用会编译期 报错并说明是哪种产物 box/cone/cylinder/frustum的gradient/derivative:隐式函数是 max 型分段函数,暂未支持(报"暂不支持 ... 体积对象");当前隐式场源只有sphere与implicitimplicit对象不接受transform/animation(方程写在世界坐标里), 本体也不参与求交/积分(V1 只有方程,没有自己的网格)- 隐式场梯度分析里的静态
transform:与 curve/surface 的既有分析一致, 分析在对象局部坐标里进行,不套用对象的静态变换
项目根目录执行:
npm run build该命令会依次执行:
一. npm run lint:rs / npm run clean:Rust fmt + clippy,然后清空旧的
dist/ 与 src/generated/
二. npm run build:wasm:分别重建 src/math/math_rs/
src/compiler/compiler_rs/src/render/render_rs
三个 Rust crate,并把产物输出到对应的 src/generated/* 目录
源码 vs 产物的对应关系:
src/*/{math_rs,compiler_rs,render_rs}是 Rust 源码(唯一权威,含数值/编译/渲染内核);src/generated/*是它们wasm-pack构建后生成的 JS/TS 绑定与.wasm产物,且整个目录在.gitignore中被忽略(*),不提交进仓库.两者同名同树,但不要手工修改或 直接搜索/导入src/generated/*里的生成文件(唯一例外是手写粘合层src/wasm/,见下节);改内核只改src/*_rs,再跑npm run build:wasm重新生成.手写代码统一从
src/wasm/的粘合入口导入:wasm/init.ts(主线程 初始化),wasm/workerRuntime.ts(Worker 侧消息壳),wasm/matrixOps.ts(矩阵后端);生成产物只被这三个文件与各*Worker直接引用.
三. npm run test(vitest),最后 npm run build:app:先 npm run typecheck
(tsc --noEmit),再 vite build
四. 生产/CI 统一入口是根目录的 bash ./build.sh:依次执行 npm ci,决定这次用
哪份 miko_ui(不带参数时本机默认本地副本,CI 默认 npm,见下面的"本地联调"与
版本同步),再跑上面整条 build:all
(Rust lint,清理旧产物与 WASM 构建,前端/Rust 测试,前端类型检查与打包),
每个阶段都有日志输出;GitHub Actions 只调用这一个脚本,不再重复编排各步骤.
脚本本体是 scripts/build.py(build.sh 只是 exec 它的壳,入口不变),所以构建
需要 python3(GitHub Actions 的 ubuntu-latest 自带);--ui / MIKO_UI_* 的
语义与旧版兼容,版本同步的策略矩阵见 scripts/buildlib.py 的
decide_ui_sync().
miko_ui(网页 UI 库)是 npm 依赖,不在这里. 它是独立仓库 ToyosatomiminoMiko/miko_ui, 发布在 npm registry 上.根package.json里一条"miko_ui": "^0.1.6",npm ci/npm install直接从 registry 装好,和three没有区别: 本仓库里没有取库的脚本,没有preinstall,也没有.cache/缓存.库的检查 (边界守卫 / typecheck / vitest)与发布都在库自己的仓库里跑,本仓库不构建库; 源码里的import ... from 'miko_ui'解析到的是 npm 包里的dist/构建产物 (纯 ESM,自带类型声明).库的运行时依赖是@preact/signals-core与katex两项 --katex由库自带(落在库的dependencies里,不是可选 peer),本应用的package.json里没有它,源码与测试里也不出现它:LaTeX 的排版,样式 (katex/dist/katex.min.css),公式字号与测试替身渲染器全归库的公式件 (createFormulaElement/installDomStub()).因此vite.config.ts的resolve.dedupe只剩@preact/signals-core(它必须全程只有一份实例),应用 CSS 里也没有.katex选择器.这条边界 由src/config/dependencyBoundary.test.ts的五条断言守着:声明里没有,代码里 没有,dedupe 里没有,样式里没有(字号只经库的令牌--katex-font-size,且 恰好一处写入 -- 库默认是 1.21em,本应用显式要 1.5em/24px),以及装到的那一份库 自己把 katex 放在dependencies.公式边界(三段,2026-10 定): ① LaTeX 的排版实现只能在库(上一条: 应用不声明/不 import katex);② 数值与向量的文本口径也只能在库 -- 应用侧 只做"选档位"(构造库的 ValueText 实例),不实现格式化(自己算科学计数法,自己 拼
\times10^{n}都算另起一套);公式用的两档实例集中在src/math/latexText.ts(LATEX_NUMBER_TEXT/LATEX_VECTOR_TEXT);③ 公式的领域语义 (∇/∫/∂/区域不等式/ODE 解族)留在编译器(compiler/dsl/latex.ts/evaluationLatex.ts/analyses.ts)-- 那是数学,不是 UI:搬进库会让库认识@/contract的 IR,直接撞上库自己的domain-imports守卫.①由src/config/dependencyBoundary.test.ts守,②③由src/config/latexBoundary.test.ts守(Rust 符号引擎自产的 LaTeX 不在射程内, 见该文件头).版本前提(2026-10 核对,未闭环): 最后一条依赖库"把 katex 收进
dependencies"的那一版.本地工作副本已就位,但 npm 上的latest(0.1.10)仍是 旧的可选 peer 版本(npm install miko_ui@0.1.10实测只装@preact/signals-core,没有 katex),而且它的dist/formula/FormulaView.js是静态import katex from 'katex'+import 'katex/dist/katex.min.css';它连本 应用已经在用的numberText/ValueText出口都还没有(同一批未发布改动).应用侧 既然已经不声明 katex,这条路的表现是构建绿,页面白屏:Vite 8 对解析不到的 import 不报错,产物入口会变成throw Error('Could not resolve "katex" imported by "miko_ui". Is it installed?'). 所以在库发布那一版之前,--ui npm那条路(含 GitHub Pages)拿不到可用产物; 本地bash ./build.sh(默认用工作副本)不受影响.发布后把根package.json的miko_ui升到那一版即可(npm install miko_ui@<版本>会一并刷新 lock -- 现在 lock 钉的 0.1.6 里连 katex 条目都没有).每次构建对齐最新版(
--ui npm):build.sh --ui npm在npm ci之后,流水线 之前跑一步版本同步 -- 拿 npm 的latest与已装版本比,不一致就npm install miko_ui@latest并更新package.json/package-lock.json.所以 GitHub Pages 每次部署用的都是库的最新发布版(deploy.yml显式调bash ./build.sh --ui npm);本地用 npm 那一版跑完记得把这两个文件提交. 策略由MIKO_UI_SYNC控制:
auto(默认):落后就更新;check:落后就失败,不改文件(只校验);off:完全按 lock 构建,跳过同步.查不到
latest(断网 / npm 不可用)时本机警告并沿用 lock,CI(CI=true,或显式MIKO_UI_REQUIRE_LATEST=1)明确失败 -- 部署出去的不能是"说不清哪一版"的产物. 注意npm run build(=build:all)不经过build.sh,它按 lock 构建,不会自动 更新;要手动升到最新就是npm install miko_ui@latest后提交 lock.
上面说的是发布形态:CI 与 GitHub Pages 只能走 npm.但改库的时候不必"改一行 ->
发一版 -> 回来 npm install":build.sh 有一个开关,换的只是"这次构建用哪份 UI".
不带开关时:本机默认本地工作副本,CI 默认 npm(CI 非空即视为 CI),所以两边
都可以只写 bash ./build.sh;CI 上显式要 local 会被直接拒绝.
bash ./build.sh # 本机: 本地 miko_ui 工作副本; CI: npm 版 + 对齐 latest
bash ./build.sh --ui npm # 本机也想用 npm 上发布的那一版(显式覆盖)
bash ./build.sh --ui local # CI 上这么写会被拒绝(绝不部署工作副本)
bash ./build.sh --help # 说明默认(本地)模式做三件事,顺序固定:
- 跳过"把
miko_ui对齐到 npmlatest"那一步(它的意义就是对齐 npm,与"用本地 副本"直接冲突); - 在本地副本里跑它自己的
npm run build:dist重建dist/-- 应用的运行时 import 的是库的dist/index.js,tsc读的是dist/index.d.ts,所以改了库的src/必须重新出 dist.本地模式下不用再挂tsc --watch,build.sh会把这件事做掉; - 把
node_modules/miko_ui换成指向副本的符号链接,并清掉node_modules/.vite里按 npm 包预打包的旧产物(不清会"改了库但页面没动").
链接会一直留着,所以构建完自己跑 npx vite,看到的就是本地库(本地服务不由
build.sh 起).库的 styles/*.css 是原样发布的,npx vite 下改完零构建立即生效;
库的 src/ 改完要再跑一次 bash ./build.sh 才会进 dist/.
全程不碰 package.json / package-lock.json,对 gh-pages 与 CI 没有影响;反过来
也不能把 file:../... 写进 package.json:CI 上没有那个路径,Pages 构建会直接
失败.也正因为这一点,CI 上不带参数时默认就是 npm(不会误用工作副本),显式写
--ui local 则被直接拒绝.想在本机回到 npm 版就是
bash ./build.sh --ui npm(它开头是 npm ci,会把链接换成 npm 版);
deploy.yml 里的 --ui npm 因此只是显式冗余,留着或去掉都一样.
工作副本默认在 ../__projects_web/miko_ui,换位置设 MIKO_UI_DIR:
MIKO_UI_DIR=/path/to/miko_ui bash ./build.sh一个必须知道的差异:本地 npm test 也会跟着用工作副本
(cssPalette.test.ts / editorStyles.test.ts 直接读库的 styles/),所以"本地绿"
不等于"CI 绿":库改了还没发版时,CI 仍按 npm 上那一版跑.底层就是
scripts/dev_ui_link.sh,只想快速链接而不跑整条流水线时可以单独调用它
(bash scripts/dev_ui_link.sh link|unlink|status,本体是 scripts/dev_ui_link.py).
只重新生成 WASM(不动其余步骤):
npm run build:wasm
npm run typecheck先说结论:GraphCalc 目前是有架构的,只是它被拆成了四条并行的线--编译/渲染/异步计算/UI 控制,最后由一个比较胖的编排器 DslApp.ts 缝在一起.你觉得"看不懂",通常是因为同步编译和异步计算/渲染这两条时间线混在一个文件里.
rust部分使用 WASM bindings generator 绑定生成器 (wasm_bindgen)
源码是唯一真相源.数据从 textarea 开始,经过一次编译变成纯数据 SceneIR,渲染层只消费这份数据,不反向修改 DSL.
源码 textarea
│ run()
▼
parseMiko() -> Rust pest 解析 -> AstProgram
│
▼
DslCompiler.compileScene()
├─ getOrBuildStaticScene() // 缓存:params / matrix / transform / animation / blueprint
├─ materializeObject() // 用当前参数生成 SceneObject
├─ compileAnalyses() // gradient / divergence / curl / laplacian
├─ compileIntegralTask() // integral 任务
└─ compileIntersections() // intersection 任务(数值交给 Worker)
│
▼
SceneIR(纯数据,不含 three.js/DOM)
│
├─ Plotter -> 各种 Renderer -> THREE 场景
├─ AnalysisRenderer -> 分析可视化
├─ DslIntegralRenderer -> 积分计算与可视化
├─ IntersectionRenderer -> 求交 Worker 调度与交线渲染
└─ ParamPanel / ObjectList / Diagnostics
关键边界文件:
- contract/ast.ts:解析结果
AstProgram - contract/ir.ts:编译结果
SceneIR - DslCompiler.ts:AST 到 SceneIR 的编排入口
- SceneStore.ts:当前会话的 AST/显隐/动画起点等状态
- CompileController.ts:解析与重新编译的调度
- RenderController.ts:场景/相机/异步采样编排
- DslApp.ts:装配层 + rAF 主循环 + 参数刷新入口
- Plotter.ts:对象 id 到渲染器的路由门面
- ComputeFacade.ts:数值计算门面
(曲线采样/积分);
compute/index.ts是 compute 层统一入口
依赖严格单向:main -> app -> ui -> render -> compute/compiler -> math/wasm -> contract/config/core,
同层之间只允许向后引用,全仓库无环(测试文件引用 testing/ 不计入生产依赖).
src/contract/ 零依赖叶子:跨层数据契约(ast.ts 解析产物 / ir.ts 编译产物 /
view.ts 视图值域 / evaluation.ts 求值细节展示数据)
src/config/ 零依赖叶子:数值/渲染/UI 默认值(含 SphericalAngleConvention)
src/core/ 零依赖通用原语:LatestRequestExecutor(+RequestClient)
src/math/ 纯数学(同步,无 DOM,无 Worker)
CoordinateSystem / latexNumber / paramValue
adapters/ 系数/求交的纯数据转换
matrix/ 行主序 Mat4 与矩阵运算接口(原 tensor/,无张量)
math_rs/ Rust 数值内核(表达式求值/采样/积分/求交)
src/wasm/ 手写 WASM 粘合层:init(主线程)/ workerRuntime(Worker 壳)/ matrixOps(矩阵后端)
src/generated/ wasm-pack 产物(被 .gitignore 忽略,勿手改)
src/compute/ AST/IR 表达式 -> Worker + Rust/WASM 的数值结果
scheduling/ 与领域无关的 Worker 调度(ComputeWorkerClient)
domain/ curve / surface / vectorField / integral / intersection 编组
index.ts compute 层公共面(只导出跨层需要的东西,内部调度不外泄)
ComputeFacade 曲线采样 + 积分门面;dispose() 收口 5 个领域 dispose*
src/compiler/ AST -> IR(dsl/ 编译管线,parser/ WASM 解析,errors/text 纯工具)
src/render/ 只消费 IR;渲染层不再自行解析表达式
core/ 场景/相机/动画/渲染器与轴刻度
visualization/ 网格与积分可视化
src/ui/ 应用侧界面:只声明"有什么"(DOM 结构)与"干什么"(行为)
entity/ 实体列表(实体对象窗口):行结构 + 列表装配
evaluation/ 求值列表(求值对象窗口):分析/积分/求交/求解/原函数/微分方程
objects/ 两个对象列表的装配与回调接线
params/ 参数面板:滑块取值口径与写回时机
process/ 过程窗口:递等式步骤列表与三级披露判据
view/ 视图窗口:元素清单(viewSpec)+ 状态源(viewState)+ 解释器(ViewPanel)
examples/ 示例目录与载入(示例清单数据 + 编辑器写入)
src/app/ 控制与编排
src/testing/ 测试基建(domStub / setupWasm / matrixOps),不被生产代码引用
src/ui/ 里没有样式:控件词表(token / 开关 / 滑块 / 数字框 / 按钮 /
浮层 / 行原语)在库 miko_ui 的 widgets/ 与 shared/,主题与默认外观在
库的 styles/,桌面窗口系统在库的 desktop/,编辑器外壳在库的 editor/,
反馈件(消息区容器与诊断条目)与公式排版在库的 feedback/ / formula/.应用侧只保留
应用自有的类名(对象行/求值行/过程步骤/面板容器)与其布局,见
界面样式配置.
数值求值链路(math_rs::eval_core::CompiledEvaluator)在构造期把符号解析成
槽位(symbolic::eval::SymBinding),求值期零字符串/零哈希/零分配;旧
HashMap<String, f64> 查表版保留为测试参照物并用逐点对拍守住语义.
index.html 加载 src/main.ts,后者只做:
new DslApp().start()DslApp.start() 会先搭建场景/相机/渲染器/控制器,然后进入每帧渲染循环,最后调用一次 run().
run() 做的事情是:
- DslApp 清空诊断,取消待刷新的参数帧,然后交给 CompileController.
- CompileController 增加运行序号并异步调用
parseMiko(editor.value)(Rust pest 解析成 AST);返回后若序号过期或已销毁则直接丢弃. - 提交 AST 与 WASM 矩阵后端到 SceneStore.
- 调用
compileScene(ast, {}, matrixOps)生成 SceneIR. - 用 SceneIR 更新参数面板,并交给 RenderController 触发绘制/异步采样.
一次完整运行只解析一次源码.之后拖参数滑块不会重新解析源码,而是复用同一个 currentAst.
滑块变化路径在 DslApp.ts 里的 _scheduleRefresh / _refreshObjects:
滑块 input
-> requestAnimationFrame 合并多个变化
-> compileScene(currentAst, paramPanelController.getValues())
-> renderController.applyScene(scene, changedParams)
-> 只重绘依赖了这些参数的对象
这里有个很重要的机制:对象会携带 coefficients,说明它引用了哪些参数._objectDependsOnParams 据此判断哪些对象需要真正重采样;不相关的对象只更新引用,不重新生成几何体.
积分和曲面等重计算也遵循这个规则:只有依赖了变化参数的对象,其关联积分才重新算.
这是最容易看漏的部分.编译器产出的是表达式字符串和系数,真正的数值采样在 Web Worker + Rust/WASM 里完成.
| 内容 | 渲染/调用入口 | Worker | Rust/WASM | 返回 |
|---|---|---|---|---|
| 曲线采样 | CurveRenderer |
CurveWorker |
math_rs.sample_curve |
顶点数组 |
| 曲面采样 | SurfaceRenderer -> SurfaceMesh |
SurfaceWorker |
render_rs.sample_and_process_surface |
位置/颜色/法线/索引 |
| 向量场采样 | VectorFieldRenderer |
VectorFieldWorker |
math_rs.sample_vector_field |
向量数组 |
| 数值积分 | DslIntegralRenderer -> ComputeFacade |
IntegralWorker |
math_rs.integrate1d/2d,带域 integrate_region(2D 区域)/integrate_solid(3D 实体) |
积分值/样本 |
| 求交 | IntersectionRenderer |
IntersectionWorker |
math_rs.intersect_pair |
交点/交线折线 |
这些链路都使用 LatestRequestExecutor:同一时间最多一个请求真正在跑,高频拖动滑块时,旧请求会被标记为 superseded,只保留最新请求.这是防止 Worker 积压的关键.
求交编译只产出 IntersectionTask(引用对象,颜色,segments),数值内核在
math_rs::intersection_core;IntersectionRenderer 用任务输入指纹做增量缓存,
参数无关的刷新不重算,只有隐藏求交本身才移除,结果回来后只重建对应任务的
geometry.求交结果按独立求值对象处理:隐藏某个参与面并不会隐藏交线.
- 业务数据(参数/对象列表/诊断/积分结果):DslApp 与 RenderController 直接注入回调,不走事件总线;调用链在编译/应用代码里就能看清.
- 视图控件(相机/坐标轴/网格/点样式):面板把控件绑到
viewState的一组 signal 上(三层见下),RenderController 用effect订阅并落到 SceneManager/CameraManager/Plotter.
原来的 9 个视图控制器与 EventBus(连同 contract/events.ts)已经整条下线:
状态只有 viewState 一份,没有"先声明后接线"的 dead event key 可留.
视图窗口(相机/预置视角/点/坐标轴/曲面)的装配固定成四层:
RENDER_CONFIG ──► src/ui/view/viewState.ts 唯一状态源(signal / 派生信号 / 计算值)
│ value: Signal<...>
▼
UI_CONFIG.view ──► src/ui/view/viewSpec.ts 元素清单:分组 / 顺序 / 行标签 / 控件种类 / 绑定的状态键
│ VIEW_BLOCKS
▼
src/ui/view/ViewPanel.ts 解释器:按 kind 建控件 + 统一持有与拆卸
│ effect
▼
RenderController 订阅状态,推到 CameraManager / Plotter / SceneManager
"视图窗口里有什么"只有一处:viewSpec.ts 的表(顺序即显示顺序).它只声明
结构,数值仍引用 UI_CONFIG.view(可调范围/步长/ViewCube 名单),渲染默认值仍
只在 RENDER_CONFIG,viewState.ts 仍是状态与派生(半径↔倍数换算,下限保护),
状态到渲染器的接线仍是 RenderController 的 effect.这几样各有硬理由分居:
render/core 的文件直接读 RENDER_CONFIG(并进 UI 配置等于让渲染层依赖界面),
而换算与下限保护是函数不是数据.表里那两行"内容随状态变"的(相机行 / 点的大小
行)是 custom 行,构建函数就放在表里,不会又散回装配代码.
清单是数据,所以另有一条守卫 src/ui/view/viewSpec.test.ts:表里的键必须是真实
存在的状态字段;ViewState 的每个字段必须被清单引用或被 RenderController
订阅(否则是"只加信号不接线"的孤儿);同一个字段不许被两行主绑定.
库的 widgets/ 是这套东西的词汇表(createSwitch / createSegmented /
createSlider / createNumberField / createButton / createPopover /
行与分组):只负责 DOM 结构与可访问性,不认识渲染侧,也不读配置.应用侧
ParamPanelController 复用同一批件,只保留取值口径与写回时机这类业务语义;
示例菜单复用 createMenu(分组/当前项/开合态/aria-expanded/点外部关闭),
自己只留"渲染什么"与"选中后干什么";对象行/求值行也改用库的建元素原语
create_element,两栏共用的行外壳与显隐按钮在库的 shared/rowDom.ts.
行外壳(.object-row / .row-main / .row-actions)与显隐按钮只要库给了
默认样式,应用侧就不再写第二份 -- 这条由 src/config/styleLayers.test.ts 守.
miko_ui 的控件类名(.segmented / .control-group / .slider-field 等)
与样式都在库里;应用侧的 css/panels.css 只留应用自有的行/栏/面板类
(.object-sublist / .kind-* / .object-expr / .eval-* ...)与其布局.
控件的可调范围与选项(min/step/ViewCube 名单)收在 UI_CONFIG.view:
它是只有 TS 消费的界面参数(与 UI_CONFIG.window 的窗口几何同类),不进
applyUiConfig 的 CSS 变量表,css/base.css 里因此没有第二份副本;渲染默认值
仍只在 RENDER_CONFIG.控制器判"越界"时读的是同一份 UI_CONFIG.view.
装配出来的控件不再需要外部 dispose:每个控件恰好交给一个持有者(视图面板交
createViewPanel 的句柄,参数行交面板控制器,浮层交示例菜单控制器),由持有者
统一解绑.新增一个视图控件因此是:src/ui/view/viewSpec.ts 加一行(结构+文案+
绑定的键)+ src/ui/view/viewState.ts 加信号(或复用)+ 需要出图时在
RenderController 接一条 effect;只有新增一种控件种类才动 ViewPanel.ts
的解释器.#view-controls 这个宿主由 src/app/appViews.ts 建,index.html 里
除 #app 外没有任何宿主 id.
另外,曲线/曲面/向量场的 Worker 采样失败现在统一经
render/core/samplingErrors.ts 上报,RenderController 转成诊断区错误;
没有"曲线悄悄走主线程兜底,曲面直接消失"的不一致路径.
GraphCalc 于 2026.09.13.20:15:00 正式立项