可移植 SVG 图元

编写并分享静态 SVG 图元,带安全的 Ganter 绑定和有类型的连接点。

以 Markdown 查看

Ganter 图元把图形和实时数据分开。图形就是普通的静态 SVG。每一层数值映射和每一段动画都归 Ganter Lab 自己管,所以导入进来的文件跑不了脚本、事件处理程序、内嵌的动画代码或者网络请求。本页是这套编写模型的完整参考:一个图元声明什么、导入接受和拒绝什么,以及图元怎样在工作站之间流转。

一个图元在图形之上声明三样东西:

  • 可绑定区域:有名字的输入(填充、描边、运动、文字、可见性),放置之后的仪表画面实例再从一个 Logic 地址或者一个固定值把它们喂起来。
  • 连接点:周边上有类型的连接位置,好让仪表画面在图元之间走管道、风管、导线和轴。
  • 建议:可选的、明确摆好姿态的组合方式,说明它和别的图元怎么拼。

系统图元与我的图元

目录有两种来源,凡是列出图元的地方都能按它筛选:

来源 引用 它是什么
系统 system:<stable-key> 随应用一起发布的内置图元库。可以查看、可以导出,但绝不能就地编辑。
我的 user:<guid> 你创建、导入或者复制出来的图元。可以编辑,也可以删除。

每一个系统图元都提供复制到“我的图元”:这个副本是一份冻结下来、独立归你所有的用户素材(图形、视图框、适配、占位尺寸、可绑定区域、类型配置、连接点和建议一起复制过去),所以后来某次应用更新改了原件,它照样按你选定的样子工作。删除一个用户图元会先问一句;还引用着它的仪表画面显示一个占位图形,而不是就此坏掉。

造一个图元的几条路

  • 导入一个文件Logic > Assets > 图元 > 导入图元接受一个普通的 .svg,或者一个完整的 .ganter-symbol.json 包。每一次导入都造一个素材,各有各的身份;它绝不覆盖已有的图元,撞名时靠加后缀(“ 2”“ 3”……)解决,而不是失败。选择器在读任何字节之前就拒绝超过 1 MB 的文件。裸 SVG 导入之后,应用会报出它移除了多少处不安全的内容(如果有的话)。
  • 在应用里编写新建用户图元会打开一个编辑器,用来填名称、SVG 标记、仪表画面布局(适配和初始占位尺寸)以及那些可绑定区域;图元一旦存在,合法的更改就自动保存。连接点的几何位置在应用里有意可编辑:有类型的连接点只能由一个包提供,而由包定义的连接点在后续编辑中原样只读保留。
  • 问一个 AI:两条路走的是完全同一份约定。通过内置的 MCP 端点symbol_validate 工具把导入空跑一遍,symbol_import 把一个新素材存下来;两者的说明里都带着完整的约定。而在一个够不着应用的外部对话里,图元面板上的复制对话指令会复制一段自成一体的提示词;把返回的 SVG 或者包存成文件,再在这里导入。

关于它周围那个资源工作区,见 Logic

备好一个普通的 SVG

给你想绑定的每一个部件一个稳定的 SVG id。导入之后,这些 id 会出现在可绑定区域的目标下拉框里。id 和可绑定区域的名称共用一套写法:首字符是字母或者 _,后面接字母、数字、_.: 或者 -,最长 128 个字符。

可选的 data-ganter-* 属性,是一个图元唯一可以携带的 Ganter 专有标记:

属性 写在哪里 它声明什么
data-ganter-pivot="x y" 任何可绑定的元素 旋转、缩放和持续旋转这三种可绑定区域所用的确切支点(视图框坐标)。这三种的目标元素上必须有它。
data-ganter-spin-ratio 一个持续旋转元素的子元素 一个看得见的子转子的相对转速。有限、非零,绝对值不超过 100;负值表示反向。除非这个元素也声明了自己的支点,否则它会被移除。
data-ganter-role 任何元素 给编写工具用的一个稳定语义角色。仅供参考。
data-ganter-fit-width <text> / <tspan> 文字可绑定区域的一个可选的限宽显示:绑上去的文字量出来比这么多视图框单位还宽时,字号会缩小以便放得下。
data-ganter-fit-min-font-size <text> / <tspan> 缩小时允许到的最小字号。再小就显示回退文字,而完整的值仍留在无障碍名称和提示文字里。
data-ganter-fit-fallback <text> / <tspan> 连最小字号都放不下这个值时显示的那段文字。

连接点不住在 SVG 里,也不对准 SVG 元素的 id。叫做 data-ganter-port 或者 data-ganter-port-* 的属性不属于这个格式:裸 SVG 导入会把它们移除,而带着它们的严格包会被拒绝。

裸 SVG 导入永远得到一个合法的只有视觉部分的图元:零个连接点、零个可绑定区域。检视面板会说明没有可连接的连接点,因为角色、类型、朝外方向和内部路径都没有声明。Ganter 不从文件名、类别或者图形去猜这些字段,也不提供内部的连接点或路径编辑器。

净化器接受什么

同一套 SVG 策略适用于每一条路(文件导入、应用内编辑器、MCP、包)。它有意比 SVG 小一圈:

  • 保留:惰性的几何和结构,也就是 svggdefssymbolusepathrectcircleellipselinepolylinepolygontexttspantitledescclipPathmasklinearGradientradialGradientstoppatternmarker,连同表现属性(填充、描边、不透明度、变换、样式)和 ARIA 标签。
  • 移除或拒绝<script><style><foreignObject>、事件处理属性、内嵌动画,以及任何外部的或者 data:/file:/http(s) 的 URL。只有内部的 url(#id)href="#id" 引用留得下来。
  • 上限:标记最大 512 KB、最多 4096 个元素。

安全的上色值是十六进制颜色、CSS 颜色名、rgb()/rgba()/hsl()/hsla(),以及应用的主题变量 var(--color-<name>)(例如 var(--color-success)var(--color-symbol-off))。一份图形靠主题变量适应浅色和深色:发布一份引用变量的图,永远不要发两个主题版本。

违规怎么处理,取决于走的是哪条路:裸 SVG 会被修补(不安全的内容被移除,每一次移除都作为一条警告报出来);是严格的,会点名第一批违规并拒绝,而不是悄悄替它修补。

可移植的 v2 包

导出一个图元会造出一个 .ganter-symbol.json 文件。外壳的每一个字段都是必填的;那三个约定数组可以为空,但必须存在:

字段 要求
$schema 严格是 https://ganterlab.com/schemas/ganter-symbol-v2.schema.json
format 严格是 ganter-symbol
version 严格是 2。版本 1 的包会被拒绝,而不是交给一个旧版解析器。
name 显示名称。导入时可以覆盖它,撞名时自动加后缀。
svg 净化过的静态 SVG,它的根元素带着一个和 viewBox 字段相等的 viewBox
viewBox minX minY width height:四个有限数,宽和高为正。
fit containstretchstretch-x 或者 stretch-y
footprint { "width": 1..24, "height": 1..12 },也就是建议的仪表画面格数。
slots 可绑定区域声明的数组(见下)。
typeConfigurations 共用的连接点类型选择的数组(见下)。
terminals 最多 16 个完整连接点的数组(见下)。
suggestions 按顺序排列的编写好的组合方式的数组(见下)。

公开的 Ganter symbol v2 JSON Schema 校验传输用的那层外壳。导入程序随后做 JSON Schema 表达不了的那些语义检查:SVG 目标是否存在、内部路径的几何、建议的兼容性。包在每一个方面都是严格的:不安全的标记、未知字段、数组里的空条目,以及旧的 ports[]anchors[]profileconnectionStyle 约定,都会被拒绝而不是被修补。

包里从来不含工作站地址、仪表画面绑定、某个实例的类型选择、数据库 GUID 或者用户身份。带着 Logic 地址的可绑定区域甚至无法导出:地址属于放置好的那个实例,不属于这个可复用的图元。

尺寸、视图框和适配

视图框是这个图元自然的坐标空间。连接点和内部路径都在它里面声明,而它必须和 SVG 根自己的 viewBox 完全一致;两个视图框对不上的包会被拒绝。非正方形的视图框完全支持,所以一条长长的输送带不必挤在一个正方形里。

适配方式说的是图形怎样使用操作员画出来的那个仪表画面矩形:

适配 行为
contain 保持长宽比;图形在格子矩形里留边显示。这是默认值。
stretch 两个方向都填满矩形,必要时变形。
stretch-x 横向拉伸,竖向保持自然比例。
stretch-y 竖向拉伸,横向保持自然比例。

占位尺寸是图元第一次放置时的初始大小,以仪表画面的格数计:宽 1 到 24,高 1 到 12(编辑器默认 4 × 4)。它是一个建议,不是一条约束;操作员之后可以随意调整大小。

有两道各自独立的关卡限制一次导入的大小:文件选择器在读文件之前就拒绝超过 1 MB 的东西,而净化器拒绝超过 512 KB 或者超过 4096 个元素的标记。

可绑定区域:绑定的那层接口

一个可绑定区域声明一路输入:它驱动哪个元素、对它做什么,以及输入值怎样映射到输出。放置好的仪表画面实例之后为每一个可绑定区域挑定信号源(一个 Logic 地址或者一个固定值);图元本身从不保存信号源。

每一个可绑定区域带着:

字段 含义
name 显示给仪表画面作者看的参数名。写法见上;在这个图元的各个可绑定区域之间不重名。
elementId 一个已存在的 SVG 元素的 id。同一个元素在每个通道上最多接一个可绑定区域,所以两个区域不会为同一个元素的填充打架。
kind 下面十三种之一。
inMininMax 输入范围:映射到 outMin/outMax 的那两个值。有限数;连续的那几种用它,fill/stroke 把它当阈值中点用。
outMinoutMax 按这一种的单位给出的输出范围(度、视图框单位、0..1、度每秒)。有限数。
outMinSecondaryoutMaxSecondary 可选的第二路输出轴,只用于 translatescale:主输出是 X,第二路是 Y。要么两个都给,要么都不给。
onColoroffColor fill/stroke 用的安全上色值(阈值两侧的两种状态)。对 fillColor/strokeColor 来说,onColor 是检视面板的预览颜色。

那十三种可绑定区域:

种类 它驱动什么 备注
rotate 旋转元素;输入线性映射成角度。 目标必须声明 data-ganter-pivot
opacity 元素的不透明度;输入映射到 0..1。
fill 在输入范围中点以下用 offColor 上填充色,到达或超过中点用 onColor。Bool 输入直接切换。 对准可以填充的形状(不含 <line>)。
visible 按值的真假显示或者隐藏元素。
text 用格式化之后的值替换元素的文字内容。 只对准 <text>/<tspan>;配上 data-ganter-fit-* 属性就是一个限宽的数值显示。
translate 以视图框单位移动元素;主输出是 X,可选的第二路是 Y。
scale 缩放元素;主输出是 X,第二路是 Y(不给就是等比缩放)。 目标必须声明 data-ganter-pivot
stroke 按和 fill 一样的阈值规则给描边上色。
spin 随时间持续旋转;输入映射成角速度,单位是度每秒。带着 data-ganter-spin-ratio 的子元素按声明的比例一起转。 目标必须声明 data-ganter-pivot。运动由宿主调度,与操作系统的动画偏好无关。
spinEnabled 暂停或者恢复同一个元素上的 spin 区域,而不隐藏它。 要求同一个元素上有一个 spin 区域。
fillColor 直接用绑上来的颜色值设置填充(一个颜色素材、一个颜色变量,或者一个安全的 CSS 颜色)。
strokeColor 直接用绑上来的颜色值设置描边。
strokeWidth 直接设置描边宽度,以 SVG 用户单位计,0.5 到 32;不接受单位后缀,也不接受 CSS。 输入不合法、解析不到或者质量不好时,恢复编写时的 stroke-width;重置、重新挂载和释放同样如此。

这些可绑定区域给图形上样式;它们从不给仪表画面的连线走向上样式,那些走向的颜色和宽度是按连线类型全局定的。

绑定归谁所有

图元声明的是参数和允许的类型选择,不是实时信号源,也不是某个实例的选择。放下一个图元组件之后,操作员把每一个可绑定区域映射到一个 Logic 地址或者一个固定值,并在那个实例上为每一个可配置的连接点挑定类型。这些选择留在工作站里,绝不会随可复用的图元一起导出。没有映射的可绑定区域保持中性:元素保留它编写时的外观。

全局位置与连线类型

每一个连接点都引用固定的十六个周边锚点目录里的一个位置。包里只存 anchorId;它从不重复、也不覆盖坐标。这些位置,以视图框的归一化比例表示:

  • 上边:top-left (0, 0)、top-25 (0.25, 0)、top-50 (0.5, 0)、top-75 (0.75, 0)、top-right (1, 0);
  • 右边:right-25 (1, 0.25)、right-50 (1, 0.5)、right-75 (1, 0.75);
  • 下边,顺时针:bottom-right (1, 1)、bottom-75 (0.75, 1)、bottom-50 (0.5, 1)、bottom-25 (0.25, 1)、bottom-left (0, 1);
  • 左边,继续顺时针:left-75 (0, 0.75)、left-50 (0, 0.5)、left-25 (0, 0.25)。

这些是周边上的位置,不是一个 5×5 的网格。一个图元既不能新增位置,也不能存自己的锚点坐标。

那八种全局连线类型是 liquidgasair-ductelectricalsignalnetworkmaterialmechanical-shaft。请用 Logic > Assets > 连线类型配置每一种类型对应的颜色和显示描边宽度。图元和连线保留的是稳定的类型 id,所以一次全局更改会给用到它的每一条走向重新上样式。包从不复制颜色或者宽度,也没有小、标准、大之类的走向变体。

两个端点在解析出同一种类型时构成一条已定义的连线。连接点的角色有助于编写和复核,但它本身并不禁止某种接线方式。

共用的类型选择

一个能配合不止一种介质工作的图元,声明一份共用的类型配置,而不是把自己的图形复制一遍:

{
  "id": "process-type",
  "name": "Process type",
  "allowedTypes": ["liquid", "gas"],
  "defaultType": "liquid"
}

id 是一个稳定的键:小写字母和数字,中间用单个短横线连接,首尾都是字母或数字(大写会被拒绝)。allowedTypes 非空,装的是各不相同的全局类型 id;defaultType 必须属于它;name 必填。每一个引用 process-type 的连接点,都跟着放置好的那个仪表画面实例上做出的同一个选择走。一份定义可以带着好几份互不相干的配置,比如工艺介质和放空介质,而别的连接点仍然是固定的。

完整的连接点

每一个可连接的连接点都声明一个全局位置、一个角色、恰好一个固定类型或者一份共用配置、一个原始朝向下的朝外方向,以及一条内部到达路径:

{
  "anchorId": "left-50",
  "role": "input",
  "typeConfiguration": "process-type",
  "direction": "west",
  "internalPath": [
    { "x": 18, "y": 50 },
    { "x": 36, "y": 50 },
    { "x": 48, "y": 62 }
  ]
}

那些规则,全部在导入时强制执行:

  • 最多 16 个连接点,每一个锚点 id 最多用一次。
  • roleinputoutput 或者 bidirectionaldirectionnortheastsouth 或者 west
  • type(一种全局连线类型)和 typeConfiguration(一个已声明的配置 id)恰好给一个,不能都给,也不能都不给。
  • 全局锚点换算到 SVG 自然视图框之后,就是 internalPath 隐含的起点;不要把它当作第一个点再写一遍。坐标是视图框里的绝对值,不是归一化比例,而且路径至少有一个点。
  • 每一段都留在视图框内,长度非零,并且是水平、竖直或者正好 45°;相邻两段的转角最大 90°。
  • 连接点要求 SVG 本身带有视图框。

仪表画面把内部路径画在设备图形下面,随着缩放、四分之一圈旋转和翻转一起变换,并保留全局连线类型的显示宽度,而不是让它跟着图元一起缩放,这样一条连续的线就从一个本体内部一直画到另一个本体内部。

一个连接点是要么全给、要么不给。带着残缺连接点的严格包是不合法的;只有视觉部分的包用一个空的 terminals 数组代替。任何缺口都绝不会被猜着填上。

按顺序排列的建议

一条建议记下一种有意为之的组合方式,连目标的确切姿态一起记:

{
  "sourceAnchorId": "right-75",
  "targetSymbol": "system:capping-station",
  "targetAnchorId": "left-75",
  "targetRotationDegrees": 0,
  "targetFlipHorizontal": false,
  "targetFlipVertical": false,
  "connectionType": "material"
}

校验:sourceAnchorId 必须是这个图元自己的某个连接点;targetSymbol 必须是一个带来源限定的引用,而在一个可移植的包里它只能是 system:<stable-key>(用户 GUID 只在它所属的那台工作站里有意义),并且点名的那个图元要在系统目录里存在、且在 targetAnchorId 上有一个连接点;旋转是 090180 或者 270,两个翻转标志都必填;每一条建议都必须唯一。connectionType 只有在两端相等的固定类型已经把场景说清楚时才可以省略;只要有一端是可配置的,它就必填,并且那时它必须在两端都被允许。

建议是有方向的,数组的顺序定优先级,但它们绝不是普通仪表画面连线的白名单。suggestions 数组为空时,检视面板会说明没有建议的组合方式,而不是自己编一个目录里的搭档出来。

导入会拒绝什么

这是一份拒绝清单,好让一次失败的导入可以读出来,而不用猜。对裸 SVG 来说只有第一组适用,而其中只有大小上限、XML 不良构和缺少 <svg> 根会真的拒绝导入,不被允许的标记则是带一条警告修补掉。对一个包来说,下面的一切都是硬拒绝。

图形

  • 文件超过 1 MB、标记超过 512 KB,或者元素超过 4096 个。
  • XML 不良构,或者根不是一个净化过的 <svg>
  • 在包里:任何按白名单本来要移除的标记(脚本、事件处理属性、外部引用、不被允许的元素或属性)。
  • viewBox 不是四个有限数、宽高不为正,或者和 SVG 根的不一致;包里干脆没有根视图框。

外壳

  • schema、format 或者 version 不是 v2 约定的那些值;任何地方出现未知字段;缺少 namefitfootprint 或者那三个数组中的任何一个;数组里出现空条目。
  • fit 不在那四种模式里;占位尺寸超出 1–24 × 1–12。

可绑定区域

  • 名称或者元素 id 不合写法;可绑定区域重名;目标 id 在 SVG 里不存在;目标元素接不了的种类(文字类用在非文字元素上、填充类用在 <line> 上)。
  • rotatespin 或者 scale 区域的目标没有有限的 data-ganter-pivotspinEnabled 区域所在的元素上没有 spin 区域。
  • 同一个元素的同一个通道上有两个区域。除了 fillfillColor 共用填充通道、strokestrokeColor 共用描边通道之外,每一种自成一个通道,所以一个元素不能接两次旋转,正如它不能接两次填充。
  • 范围里出现非有限数;第二路输出只给了一端,或者出现在 translate/scale 之外的种类上;不安全的上色值;带着 Logic 地址的可绑定区域。

连接

  • 超过 16 个连接点;锚点 id 重复或者未知;一个连接点同时给了或者都没给 type/typeConfiguration;未知的类型或配置 id;不合法的内部路径(跑出视图框、长度为零或者角度不对的线段、转角超过 90°)。
  • 类型配置的 id 不合法、缺少名称、允许类型重复或未知,或者默认值不在它自己的列表里。
  • 一条建议的源连接点不存在、目标不是带着所点名连接点的系统图元、姿态不是四分之一圈的整数倍,或者连线类型在两端不被同时接受。

MCP 端点上的 symbol_validate 会把这次导入原样空跑一遍,报出每一条错误、警告和说明,什么都不存下来。

打包与分享

导出图元包(系统图元和用户图元一样可用)把 <name>.ganter-symbol.json 下载下来,里面是净化过的图形和完整的约定:$schemaformatversionnamesvgviewBoxfitfootprintslots[]typeConfigurations[]terminals[]suggestions[],那三个连接数组即使为空也在。绝不随之走的东西有:工作站地址、单个实例的类型选择、数据库身份、用户身份,以及这台工作站的连线类型颜色和宽度。

在收下它的那台工作站上,同一个文件走导入图元(或者走 MCP 的 symbol_import),落地成一个新的用户图元,连接点一个不少。因为类型身份是一个稳定的 id、而外观是全局的,导入进来的图元,它的走向立刻跟上收下它那台工作站自己的连线类型样式。