形状工作台 · 给引擎加一个新形状

想加一个新形状,
该动哪几行?

形状系统有 5 个注册点,其中 4 个是机械的。 真正不能猜的只有一件事:你的形状有没有凹口 —— 因为它直接决定必须用哪种 silhouette,而选错了凸包会把你的凹口静默吃掉。

5 个注册点 1 个不能猜 0 依赖

STEP 0

先过决策树:三个问题定 silhouette

别先写代码。先回答下面三个问题 —— 它会告诉你 silhouette.kind 填什么。

Q1你的形状有凹口 / 凹槽 / 内陷吗?
有凹
绝对不能用 solid —— 它的轮廓算法是凸包(pi()), 在数学上不可能产生凹形,你的凹口会被直接填平。继续看 Q2。
完全凸(像球 / 蛋 / 盾)
用 solid 最省事,也是最常见的一类。
{ kind:"solid", samples: Jt(mt("名")), balls:[], isLoop:false }
Q2(只有凹形状要看)它是「有明确厚度的一块板」,还是「鼓起来的一个包」?
有厚度的板 / 柱(方、六角、星、齿轮、房子)
→ extrusion:把轮廓沿 ±z 挤出,边缘用 bevel 收圆。
{ kind:"extrusion", rings: Nv(samples, halfDepth, bevel) }
像枕头 / 云 / 心那样鼓起来
→ loft:轮廓在赤道,向上向下各收成一个点。
{ kind:"loft", rings: zv(samples, depth) }
Q3它其实是一串球 / 一堆球吗?
是:药丸、豆子、胶囊
不生成路径,改填 balls,轮廓由射线和圆求交算出。
{ kind:"solid", samples:[], balls: jl(名,"x"), isLoop:false }
不是
回到 Q1 的结论。
下面是工作台 —— 拖顶点画形状,它会实时跑凸包、告诉你有没有踩到这条红线, 并吐出 5 处注册代码。

WORKBENCH

工作台:拖顶点 → 拿到代码

拖动白点改形状,拖动形状本体不动。点一个顶点可以单独调它自己的圆角 —— 引擎的 ac() 支持逐顶点圆角数组,这是画齿轮 / 花瓣类形状的关键技巧。

拖动圆点改形状 · 点顶点选中后可单独调它的圆角

RECIPE

五步配方

1

写路径生成器,注册进 Mv

只吃 2~3 个数字。用 4 个图元拼:eh(正多边形)、ac(圆角多边形)、 ar(极坐标函数)、th(圆并集)。 坐标以设计画布中心 x = 114.2705 为原点,y 轴向下为正。

moon: () => ac([[x-80,x+60], [x+40,x+90], …], 18),
// 规则多边形一行搞定:
moon: () => eh(114, 6, 20, Math.PI/6),
2

把它变成点列:mt() + Jt()

路径字符串先被 is() 采样成点,再由 Jt() 归一化成 3D 点(打平 z=0)。 这一步不用你写,但要知道点是采样出来的,所以路径本身决定最终精度。

mt("moon")        // = is(Kr("moon").path, 4) → 每 4 单位采一个点
Jt(mt("moon"))    // → [{x:(px-x)/x, y:-(py-x)/x, z:0}, …]
3

写描述符,注册进 Iv —— 这里选 silhouette

按 STEP 0 的决策树填。同时必须定义 seat,否则眼睛会飘在形状外面。

// 凸 → 最省事
moon: () => qn(1.17, "moon")            // seat 自动 = wrap

// 凹 + 有厚度
moon: () => oh(1.17, Jt(mt("moon")), .36, .22)
// seat = { kind:"front", faceZ:.36 }

// 凹 + 鼓起来
moon: () => zl(1.17, Jt(mt("moon")), .62)
// seat = { kind:"loft", surfaceZ: Dv(rings) }

// 球链
moon: () => ({ size:1.17,
  silhouette:{ kind:"solid", samples:[], balls: jl("moon","x"), isLoop:false },
  seat:{ kind:"wrap", wrapR:…, wrapY0:…, wrapY1:…, radiusAt:… } })
4

(可选)注册 3D 实体 iS[name]

只有开 isProof 走软件光照时才用得到 —— 正常 SVG 渲染完全不需要。 但不填的话那套 3D 着色器就渲不出你的形状。

moon: { kind: "revolve" },   // 旋转体
// 可选:revolve / loft / extrusion / roundedSlab / pill / inscribedBalls / crystal
5

注册数据 + UI

三处:shapes.names、shapes.eyeMetrics, 以及 UI 的形状清单与文案。

// grok-recipes.json
"names": [ …, "moon" ],
"eyeMetrics": { "moon": { "width":.21, "height":.46, "gap":.47, "shiftY":.12 } }

// bundle 里的 UI 清单(Hw)与别名(wN)
Hw = [ …, "moon" ];
function wN(e) { switch (e) { /* blob→sphere, flower→starFlower, … */ } }

CHECKLIST

注册点清单

位置 必填 做什么 不填会怎样
Mv[name]必填 路径生成器,返回 SVG path 字符串直接报错
Iv[name]必填 描述符 {size, silhouette, seat}直接报错
Iv[name].seat必填 眼睛落座方式(front / wrap / wrapX / loft) 眼睛飘在体外或不动
shapes.names必填 白名单 —— bodyAlias() 不在名单里就回退成 sphere 静默变成圆球
shapes.eyeMetrics建议 眼睛的 width / height / gap / shiftY,逐形状微调 用默认值,眼睛比例不对
iS[name]可选 3D 实体 kind,只给 isProof 光照用 开 proof 时渲不出来
Hw / wN / 文案UI 需要 选形状面板的清单、UI 名到引擎名的别名、三语文案 用户选不到 / 名字对不上
最容易踩的坑:shapes.names 漏了。 bodyAlias() 是这样的:shapeNames.includes(t) ? t : "sphere" —— 名字不在名单里不报错,静默回退成球。你会以为几何写错了,其实是名单没加。

GOTCHAS

几个会浪费你半天的坑

想跳过全部这些?如果你的项目只需要「凸形状 + 圆的组合」,最省事的路线是: 凸形状全用 ar 极坐标公式(超椭圆 El 一个公式就覆盖方、圆角方、 宝石、蛋),凹形状全用 th(圆的并集)—— 两条公式足以覆盖绝大多数有机形状。

本页几何实现逐行对应原 bundle 的 Mv/Iv/Zv/ac/eh/th/Nv/zv/pi, 见 js/lib-shapes.js;生成的代码里坐标是设计画布坐标,可直接粘进 Mv 表。 原理细节见 形状系统解剖。
Grok 与 x.ai 商标归其所有者。本项目仅用于研究与学习。