lan-low-code-cli 2.9.0 → 2.9.1
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.
- package/README.md +9 -5
- package/dist/commands/flow.js +4 -4
- package/dist/generator/flow/compiler.js +26 -7
- package/dist/generator/flow/layout.js +164 -73
- package/dist/index.js +1 -1
- package/package.json +2 -1
- package/skills/lan-low-code-cli/SKILL.md +1 -1
- package/skills/lan-low-code-cli/SKILL.zh-CN.md +1 -1
- package/skills/lan-low-code-cli/references/cli-contract.md +8 -5
- package/skills/lan-low-code-cli/references/cli-contract.zh-CN.md +6 -4
package/README.md
CHANGED
|
@@ -350,13 +350,17 @@ llc flow delete --id <flowId> --yes
|
|
|
350
350
|
|
|
351
351
|
**布局**由编译器生成,不要手写 `drawAttr.bounds`:
|
|
352
352
|
|
|
353
|
-
- `--layout ide
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
353
|
+
- `--layout ide`(默认):**dagre 分层布局**(Sugiyama:分层 → 层内排序 → 坐标分配)。
|
|
354
|
+
主链以 `--center-x`(默认 679)居中,`BranchNode` 56×56、其余 180×38;分支后继位于下一层,
|
|
355
|
+
父节点居中于子节点之上,汇聚节点居中于多个父节点下方;层间距 `--rank-padding`(默认 52),
|
|
356
|
+
同层间距 60。嵌套分支场景实测交叉数 0、线段总长比旧算法短约 16%。
|
|
357
|
+
- `--layout layered`:旧版「rank + lane」算法,复刻 IDE 手工「左右插入」习惯(分支后继与分支节点同层、
|
|
358
|
+
左右对称)。保留用于兼容历史流程,分支嵌套时更容易出现长线和交叉。
|
|
357
359
|
- `--layout passthrough`:沿用 spec 里的 `bounds`(用于回写平台导出的流程)。
|
|
358
360
|
- `passthrough` 之外的模式下,`relative:false`、节点尺寸、连线 `sourceRef/targetRef` 与
|
|
359
|
-
`formulaConfig.branchFormulas`(引用真实连线 ID
|
|
361
|
+
`formulaConfig.branchFormulas`(引用真实连线 ID)均自动生成;分支出线按实际相对位置写
|
|
362
|
+
`exitX=0/1`,多出边/多入边写 `exitX/entryX=0.25/0.75`,减少线条挤在同一点。
|
|
363
|
+
- 布局依赖:`@dagrejs/dagre`(MIT)。CLI 其余逻辑仍不依赖任何前端包。
|
|
360
364
|
|
|
361
365
|
**Schema 注入**:`--resolve-schema` 会为引用数据模型的节点调 `getSourceJsonSchema` /
|
|
362
366
|
`getProcessDataOutJsonSchema` 补齐 `extraAttr.context_outputParam` / `context_bindFormId`。
|
package/dist/commands/flow.js
CHANGED
|
@@ -290,7 +290,7 @@ function registerFlowCommand(program) {
|
|
|
290
290
|
.command('compile')
|
|
291
291
|
.description('把 Flow Spec 编译为可直接提交的 fdDesign(含编排布局与离线校验)')
|
|
292
292
|
.requiredOption('--spec <@file|json>', 'Flow Spec:@文件路径 或内联 JSON 字符串')
|
|
293
|
-
.option('--layout <mode>', '布局模式:ide
|
|
293
|
+
.option('--layout <mode>', '布局模式:ide(默认,dagre 分层)| layered(旧版同层分支)| passthrough(沿用已有坐标)')
|
|
294
294
|
.option('--center-x <n>', '主链中心 x,默认 679(对应 1920 宽屏)')
|
|
295
295
|
.option('--rank-padding <n>', '层间距增量,默认 52(层距 = 该层最高节点 + 52)')
|
|
296
296
|
.option('--lane-gap <n>', '泳道间距覆盖值,默认 2 × 层距')
|
|
@@ -319,7 +319,7 @@ function registerFlowCommand(program) {
|
|
|
319
319
|
.command('validate')
|
|
320
320
|
.description('只做离线校验(图结构 / 必填 / 编码 / 布局重叠),不输出设计')
|
|
321
321
|
.requiredOption('--spec <@file|json>', 'Flow Spec:@文件路径 或内联 JSON 字符串')
|
|
322
|
-
.option('--layout <mode>', '布局模式:ide
|
|
322
|
+
.option('--layout <mode>', '布局模式:ide(默认,dagre 分层)| layered(旧版同层分支)| passthrough(沿用坐标)')
|
|
323
323
|
.option('--center-x <n>', '主链中心 x')
|
|
324
324
|
.option('--rank-padding <n>', '层间距增量')
|
|
325
325
|
.option('--lane-gap <n>', '泳道间距')
|
|
@@ -579,7 +579,7 @@ function registerFlowCommand(program) {
|
|
|
579
579
|
.option('--enable', '成功后调用 startup 启用')
|
|
580
580
|
.option('--resolve-schema', '联网补齐 extraAttr.context_*(SchemaResolver)')
|
|
581
581
|
.option('--strict-schema', 'Schema 拉取失败时升级为错误')
|
|
582
|
-
.option('--layout <mode>', '布局模式:ide
|
|
582
|
+
.option('--layout <mode>', '布局模式:ide(默认,dagre 分层)| layered(旧版同层分支)| passthrough(沿用坐标)')
|
|
583
583
|
.option('--center-x <n>', '主链中心 x')
|
|
584
584
|
.option('--lane-gap <n>', '泳道间距')
|
|
585
585
|
.option('--min-gap <n>', '最小边缘间隙')
|
|
@@ -687,7 +687,7 @@ function registerFlowCommand(program) {
|
|
|
687
687
|
.option('--no-ignore-bounds', '比较时也包含 drawAttr.bounds')
|
|
688
688
|
.option('--resolve-schema', '对比前联网补齐 extraAttr.context_*(与发布时的选项保持一致)')
|
|
689
689
|
.option('--strict-schema', 'Schema 拉取失败时升级为错误')
|
|
690
|
-
.option('--layout <mode>', '布局模式:ide
|
|
690
|
+
.option('--layout <mode>', '布局模式:ide(默认,dagre 分层)| layered(旧版同层分支)| passthrough(沿用坐标)')
|
|
691
691
|
.option('--center-x <n>', '主链中心 x')
|
|
692
692
|
.option('--json', '以 JSON 输出差异')).action(async (opts) => {
|
|
693
693
|
const spec = readJsonInput(String(opts.spec), '--spec');
|
|
@@ -322,15 +322,34 @@ function compileGraph(spec, ctx) {
|
|
|
322
322
|
});
|
|
323
323
|
}
|
|
324
324
|
const nodes = normalized.graph.nodes.map((node) => buildNode(node, layout.bounds, ctx, branchDefaults, branchFormulas, diagnostics));
|
|
325
|
+
const incomingCount = new Map();
|
|
326
|
+
const outgoingCount = new Map();
|
|
327
|
+
for (const edge of normalized.graph.edges) {
|
|
328
|
+
incomingCount.set(edge.to, (incomingCount.get(edge.to) ?? 0) + 1);
|
|
329
|
+
outgoingCount.set(edge.from, (outgoingCount.get(edge.from) ?? 0) + 1);
|
|
330
|
+
}
|
|
325
331
|
const lines = normalized.graph.edges.map((edge) => {
|
|
326
|
-
//
|
|
332
|
+
// 多出边/多入边按实际相对位置设置锚点,避免多条线挤在节点同一点。
|
|
333
|
+
// 条件分支:左右侧出(exitX=0/1);汇聚:左/右 1/4 处入(entryX=0.25/0.75)。
|
|
327
334
|
const source = nodeById.get(edge.from);
|
|
328
|
-
const target = nodeById.get(edge.to);
|
|
329
335
|
const isBranchOut = source?.type === 'BranchNode';
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
336
|
+
const sourceBounds = layout.bounds[edge.from];
|
|
337
|
+
const targetBounds = layout.bounds[edge.to];
|
|
338
|
+
const sourceCenterX = sourceBounds ? sourceBounds.x + sourceBounds.width / 2 : undefined;
|
|
339
|
+
const targetCenterX = targetBounds ? targetBounds.x + targetBounds.width / 2 : undefined;
|
|
340
|
+
const leftOfTarget = sourceCenterX !== undefined && targetCenterX !== undefined ? sourceCenterX < targetCenterX : true;
|
|
341
|
+
let style = '';
|
|
342
|
+
if (isBranchOut) {
|
|
343
|
+
const exitX = sourceCenterX !== undefined && targetCenterX !== undefined
|
|
344
|
+
? (targetCenterX < sourceCenterX ? 0 : targetCenterX > sourceCenterX ? 1 : 0.5)
|
|
345
|
+
: 0.5;
|
|
346
|
+
style = `line;exitX=${exitX};exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;`;
|
|
347
|
+
}
|
|
348
|
+
else if ((outgoingCount.get(edge.from) ?? 0) > 1 || (incomingCount.get(edge.to) ?? 0) > 1) {
|
|
349
|
+
const exitX = (outgoingCount.get(edge.from) ?? 0) > 1 ? (leftOfTarget ? 0.25 : 0.75) : 0.5;
|
|
350
|
+
const entryX = (incomingCount.get(edge.to) ?? 0) > 1 ? (leftOfTarget ? 0.25 : 0.75) : 0.5;
|
|
351
|
+
style = `line;exitX=${exitX};exitY=1;exitDx=0;exitDy=0;entryX=${entryX};entryY=0;entryDx=0;entryDy=0;`;
|
|
352
|
+
}
|
|
334
353
|
return {
|
|
335
354
|
id: lineIdOf.get(edge),
|
|
336
355
|
name: edge.name ?? '',
|
|
@@ -342,7 +361,7 @@ function compileGraph(spec, ctx) {
|
|
|
342
361
|
// 仅 --emit-waypoints 时额外写入 wayPoints(非 IDE 原生行为)。
|
|
343
362
|
drawAttr: ctx.options.emitWayPoints
|
|
344
363
|
? { style, wayPoints: [] }
|
|
345
|
-
:
|
|
364
|
+
: style ? { style } : {},
|
|
346
365
|
};
|
|
347
366
|
});
|
|
348
367
|
const design = {
|
|
@@ -2,22 +2,26 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* 动作流编排布局引擎
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - 种子 mxgraph/utils.tsx:93-135:N1 在 (C-90, 80)、N2 在 (C-90, 166),L1 直连
|
|
8
|
-
* - 向下插入 R4(mxgraph/utils.tsx:422-465,1408-1428):distance = h_prev + 52,
|
|
9
|
-
* cy_new = cy_prev + distance,下游整体 y += distance
|
|
10
|
-
* - 左右插入 R5(mxgraph/utils.tsx:442-458,568-584):与源节点同层中心线,
|
|
11
|
-
* x = C - w/2 ± 2*distance
|
|
12
|
-
* - 无整图自动布局、无后端布局服务;坐标是持久化契约,IDE 回显原样使用
|
|
5
|
+
* 坐标是持久化契约:IDE 回显 `drawAttr.bounds` 时原样使用,不会重算,
|
|
6
|
+
* 也没有整图自动布局或后端布局服务,因此坐标质量直接决定连线是否可读。
|
|
13
7
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* 两种模式:
|
|
9
|
+
* - `ide`(默认):dagre 分层布局(Sugiyama:分层 → 层内排序 → 坐标分配)。
|
|
10
|
+
* 分支后继位于下一层,父节点由 dagre 居中于子节点之上,汇聚节点居中于多个父节点下方,
|
|
11
|
+
* 能显著减少长边与交叉。dagre 是 mxGraph 生态常用的布局库(MIT)。
|
|
12
|
+
* - `layered`:旧版「rank + lane」算法,复刻 IDE 手工「左右插入」习惯——
|
|
13
|
+
* 分支后继与分支节点同层、左右对称。保留用于兼容历史流程,但分支多时线段较乱。
|
|
14
|
+
* - `passthrough`:原样沿用节点 bounds(UI 导出流程回写)。
|
|
15
|
+
*
|
|
16
|
+
* 尺寸与坐标依据:
|
|
17
|
+
* - styleSheet.ts:339-347:BranchNode 56×56,其余 180×38
|
|
18
|
+
* - mxgraph/utils.tsx:93-135:主画布起始 y=80
|
|
16
19
|
*/
|
|
17
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.DEFAULT_CENTER_X = exports.MIN_GAP = exports.RANK_PADDING = exports.START_Y = exports.NODE_SIZE = void 0;
|
|
21
|
+
exports.DEFAULT_NODE_GAP = exports.DEFAULT_CENTER_X = exports.MIN_GAP = exports.RANK_PADDING = exports.START_Y = exports.NODE_SIZE = void 0;
|
|
19
22
|
exports.nodeSize = nodeSize;
|
|
20
23
|
exports.layoutGraph = layoutGraph;
|
|
24
|
+
const dagre_1 = require("@dagrejs/dagre");
|
|
21
25
|
/** 与 styleSheet.ts nodeBounds 一致 */
|
|
22
26
|
exports.NODE_SIZE = {
|
|
23
27
|
BranchNode: { width: 56, height: 56 },
|
|
@@ -25,76 +29,192 @@ exports.NODE_SIZE = {
|
|
|
25
29
|
};
|
|
26
30
|
/** 主画布开始节点的 y(mxgraph/utils.tsx:130) */
|
|
27
31
|
exports.START_Y = 80;
|
|
28
|
-
/**
|
|
32
|
+
/** 层间距增量:distance = h_prev + 52 */
|
|
29
33
|
exports.RANK_PADDING = 52;
|
|
30
34
|
/**
|
|
31
35
|
* 最小边缘间隙,默认 40。
|
|
32
36
|
*
|
|
33
37
|
* IDE 常规编排的垂直间隙由 R4 推导:gap = 52 + (h_prev - h_new)/2;
|
|
34
38
|
* 38 高节点后紧跟 56 高 BranchNode 时最小为 52 - (56-38)/2 = 43px。
|
|
35
|
-
* 因此不能照搬 mxHierarchicalLayout 的 interRankCellSpacing=50(该值只在
|
|
36
|
-
* 折叠/展开时生效),否则正常分支流程会被误判为布局重叠。
|
|
37
39
|
*/
|
|
38
40
|
exports.MIN_GAP = 40;
|
|
39
41
|
/** W=1920 时 (1920-562)/2;CLI 无浏览器宽度,可用 --center-x 覆盖 */
|
|
40
42
|
exports.DEFAULT_CENTER_X = 679;
|
|
43
|
+
/** dagre 同层节点默认水平间距 */
|
|
44
|
+
exports.DEFAULT_NODE_GAP = 60;
|
|
41
45
|
function nodeSize(nodeType) {
|
|
42
46
|
return exports.NODE_SIZE[nodeType] ?? exports.NODE_SIZE.general;
|
|
43
47
|
}
|
|
44
48
|
const round = (v) => Math.round(v * 100) / 100;
|
|
45
49
|
function layoutGraph(graph, options = {}) {
|
|
46
50
|
const mode = options.mode ?? 'ide';
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
if (mode === 'passthrough')
|
|
52
|
+
return layoutPassthrough(graph);
|
|
53
|
+
if (mode === 'layered')
|
|
54
|
+
return layoutLayered(graph, options);
|
|
55
|
+
return layoutDagre(graph, options);
|
|
56
|
+
}
|
|
57
|
+
/* ------------------------------------------------------------------ 重叠 / 负坐标检查 */
|
|
58
|
+
function checkBounds(graph, bounds, minGap) {
|
|
50
59
|
const diagnostics = [];
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
60
|
+
const placed = graph.nodes.filter((node) => bounds[node.id]);
|
|
61
|
+
for (let i = 0; i < placed.length; i++) {
|
|
62
|
+
for (let j = i + 1; j < placed.length; j++) {
|
|
63
|
+
const a = bounds[placed[i].id];
|
|
64
|
+
const b = bounds[placed[j].id];
|
|
65
|
+
const horizontal = a.x < b.x + b.width + minGap && b.x < a.x + a.width + minGap;
|
|
66
|
+
const vertical = a.y < b.y + b.height + minGap && b.y < a.y + a.height + minGap;
|
|
67
|
+
if (horizontal && vertical) {
|
|
58
68
|
diagnostics.push({
|
|
59
69
|
level: 'error',
|
|
60
|
-
code: '
|
|
61
|
-
message:
|
|
62
|
-
nodeId:
|
|
70
|
+
code: 'LAYOUT_OVERLAP',
|
|
71
|
+
message: `节点 ${placed[i].id} 与 ${placed[j].id} 的布局重叠或间距不足 ${minGap}px`,
|
|
72
|
+
nodeId: placed[j].id,
|
|
63
73
|
});
|
|
64
|
-
continue;
|
|
65
74
|
}
|
|
66
|
-
bounds[node.id] = {
|
|
67
|
-
x: given.x,
|
|
68
|
-
y: given.y,
|
|
69
|
-
width: size.width,
|
|
70
|
-
height: size.height,
|
|
71
|
-
relative: given.relative ?? false,
|
|
72
|
-
};
|
|
73
75
|
}
|
|
74
|
-
return { bounds, rankCenters: [], diagnostics };
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
+
for (const node of placed) {
|
|
78
|
+
const b = bounds[node.id];
|
|
79
|
+
if (b.x < 0 || b.y < 0) {
|
|
80
|
+
diagnostics.push({
|
|
81
|
+
level: 'warn',
|
|
82
|
+
code: 'LAYOUT_NEGATIVE',
|
|
83
|
+
message: `节点 ${node.id} 坐标为负 (${b.x}, ${b.y}),IDE 中可能显示不全`,
|
|
84
|
+
nodeId: node.id,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return diagnostics;
|
|
89
|
+
}
|
|
90
|
+
/* ------------------------------------------------------------------ passthrough */
|
|
91
|
+
function layoutPassthrough(graph) {
|
|
92
|
+
const diagnostics = [];
|
|
93
|
+
const bounds = {};
|
|
94
|
+
for (const node of graph.nodes) {
|
|
95
|
+
const size = nodeSize(node.type);
|
|
96
|
+
const given = node.bounds;
|
|
97
|
+
if (!given || typeof given.x !== 'number' || typeof given.y !== 'number') {
|
|
98
|
+
diagnostics.push({
|
|
99
|
+
level: 'error',
|
|
100
|
+
code: 'LAYOUT_MISSING_BOUNDS',
|
|
101
|
+
message: `passthrough 模式要求节点提供 bounds.{x,y}:${node.id}`,
|
|
102
|
+
nodeId: node.id,
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
bounds[node.id] = {
|
|
107
|
+
x: given.x,
|
|
108
|
+
y: given.y,
|
|
109
|
+
width: size.width,
|
|
110
|
+
height: size.height,
|
|
111
|
+
relative: given.relative ?? false,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { bounds, rankCenters: [], diagnostics };
|
|
115
|
+
}
|
|
116
|
+
/* ------------------------------------------------------------------ dagre(默认) */
|
|
117
|
+
function layoutDagre(graph, options) {
|
|
118
|
+
const centerX = options.centerX ?? exports.DEFAULT_CENTER_X;
|
|
119
|
+
const minGap = options.minGap ?? exports.MIN_GAP;
|
|
120
|
+
const diagnostics = [];
|
|
121
|
+
const g = new dagre_1.graphlib.Graph({ multigraph: true });
|
|
122
|
+
g.setGraph({
|
|
123
|
+
rankdir: 'TB',
|
|
124
|
+
nodesep: options.nodeGap ?? exports.DEFAULT_NODE_GAP,
|
|
125
|
+
ranksep: options.rankPadding ?? exports.RANK_PADDING,
|
|
126
|
+
marginx: 0,
|
|
127
|
+
marginy: 0,
|
|
128
|
+
});
|
|
129
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
130
|
+
for (const node of graph.nodes) {
|
|
131
|
+
const size = nodeSize(node.type);
|
|
132
|
+
g.setNode(node.id, { width: size.width, height: size.height });
|
|
133
|
+
}
|
|
134
|
+
graph.edges.forEach((edge, index) => {
|
|
135
|
+
if (!g.hasNode(edge.from) || !g.hasNode(edge.to))
|
|
136
|
+
return;
|
|
137
|
+
g.setEdge(edge.from, edge.to, {}, `e${index}`);
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
(0, dagre_1.layout)(g);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
diagnostics.push({
|
|
144
|
+
level: 'warn',
|
|
145
|
+
code: 'LAYOUT_DAGRE_FAILED',
|
|
146
|
+
message: `dagre 布局失败,已回退到 layered:${err.message.split('\n')[0]}`,
|
|
147
|
+
});
|
|
148
|
+
return layoutLayered(graph, options);
|
|
149
|
+
}
|
|
150
|
+
const start = graph.nodes.find((node) => node._isStart) ?? graph.nodes[0];
|
|
151
|
+
const startLabel = start ? g.node(start.id) : undefined;
|
|
152
|
+
const startSize = start ? nodeSize(start.type) : exports.NODE_SIZE.general;
|
|
153
|
+
const startX = startLabel && typeof startLabel.x === 'number' ? startLabel.x : undefined;
|
|
154
|
+
const startY = startLabel && typeof startLabel.y === 'number' ? startLabel.y : undefined;
|
|
155
|
+
const offsetX = startX !== undefined ? centerX - startX : 0;
|
|
156
|
+
const offsetY = startY !== undefined ? exports.START_Y - (startY - startSize.height / 2) : exports.START_Y;
|
|
157
|
+
const raw = new Map();
|
|
158
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
159
|
+
for (const node of graph.nodes) {
|
|
160
|
+
const label = g.node(node.id);
|
|
161
|
+
if (!label || typeof label.x !== 'number' || typeof label.y !== 'number')
|
|
162
|
+
continue;
|
|
163
|
+
const size = nodeSize(node.type);
|
|
164
|
+
const x = label.x + offsetX - size.width / 2;
|
|
165
|
+
const y = label.y + offsetY - size.height / 2;
|
|
166
|
+
raw.set(node.id, { x, y });
|
|
167
|
+
minX = Math.min(minX, x);
|
|
168
|
+
}
|
|
169
|
+
// 负坐标会显示不全:整体右移
|
|
170
|
+
const shiftX = Number.isFinite(minX) && minX < 0 ? 20 - minX : 0;
|
|
171
|
+
const bounds = {};
|
|
172
|
+
const centers = new Set();
|
|
173
|
+
for (const node of graph.nodes) {
|
|
174
|
+
const point = raw.get(node.id);
|
|
175
|
+
if (!point)
|
|
176
|
+
continue;
|
|
177
|
+
const size = nodeSize(node.type);
|
|
178
|
+
bounds[node.id] = {
|
|
179
|
+
x: round(point.x + shiftX),
|
|
180
|
+
y: round(point.y),
|
|
181
|
+
width: size.width,
|
|
182
|
+
height: size.height,
|
|
183
|
+
relative: false,
|
|
184
|
+
};
|
|
185
|
+
centers.add(Math.round(point.y + size.height / 2));
|
|
186
|
+
}
|
|
187
|
+
diagnostics.push(...checkBounds(graph, bounds, minGap));
|
|
188
|
+
return { bounds, rankCenters: [...centers].sort((a, b) => a - b), diagnostics };
|
|
189
|
+
}
|
|
190
|
+
/* ------------------------------------------------------------------ 旧版 layered */
|
|
191
|
+
function layoutLayered(graph, options) {
|
|
192
|
+
const centerX = options.centerX ?? exports.DEFAULT_CENTER_X;
|
|
193
|
+
const padding = options.rankPadding ?? exports.RANK_PADDING;
|
|
194
|
+
const minGap = options.minGap ?? exports.MIN_GAP;
|
|
195
|
+
const bounds = {};
|
|
196
|
+
const maxRank = graph.nodes.reduce((max, node) => Math.max(max, node._rank), 0);
|
|
77
197
|
// 每层取最高节点,决定「该层到下一层」的间距:38→90、56→108
|
|
78
198
|
const rankHeight = [];
|
|
79
199
|
for (let r = 0; r <= maxRank; r++) {
|
|
80
|
-
const heights = graph.nodes.filter((
|
|
200
|
+
const heights = graph.nodes.filter((node) => node._rank === r).map((node) => nodeSize(node.type).height);
|
|
81
201
|
rankHeight[r] = heights.length > 0 ? Math.max(...heights) : exports.NODE_SIZE.general.height;
|
|
82
202
|
}
|
|
83
203
|
const gapOf = (r) => (rankHeight[r] ?? exports.NODE_SIZE.general.height) + padding;
|
|
84
|
-
// 中心线:cy(0) = 80 + h_start/2
|
|
204
|
+
// 中心线:cy(0) = 80 + h_start/2;cy(r) = cy(r-1) + gap(r-1)
|
|
85
205
|
const rankCenters = [];
|
|
86
|
-
const startNode = graph.nodes.find((
|
|
206
|
+
const startNode = graph.nodes.find((node) => node._isStart);
|
|
87
207
|
rankCenters[0] = exports.START_Y + (startNode ? nodeSize(startNode.type).height : exports.NODE_SIZE.general.height) / 2;
|
|
88
208
|
for (let r = 1; r <= maxRank; r++) {
|
|
89
209
|
rankCenters[r] = rankCenters[r - 1] + gapOf(r - 1);
|
|
90
210
|
}
|
|
91
211
|
for (const node of graph.nodes) {
|
|
92
212
|
const size = nodeSize(node.type);
|
|
93
|
-
const
|
|
213
|
+
const rank = node._rank;
|
|
94
214
|
// R5 的 ±2*distance:distance 取当前层最高节点对应的层间距
|
|
95
|
-
const laneGap = options.laneGap ?? 2 * gapOf(
|
|
215
|
+
const laneGap = options.laneGap ?? 2 * gapOf(rank);
|
|
96
216
|
const x = centerX - size.width / 2 + node._lane * laneGap;
|
|
97
|
-
const y = (rankCenters[
|
|
217
|
+
const y = (rankCenters[rank] ?? rankCenters[rankCenters.length - 1]) - size.height / 2;
|
|
98
218
|
bounds[node.id] = {
|
|
99
219
|
x: round(x),
|
|
100
220
|
y: round(y),
|
|
@@ -103,34 +223,5 @@ function layoutGraph(graph, options = {}) {
|
|
|
103
223
|
relative: false,
|
|
104
224
|
};
|
|
105
225
|
}
|
|
106
|
-
|
|
107
|
-
const placed = graph.nodes.filter((n) => bounds[n.id]);
|
|
108
|
-
for (let i = 0; i < placed.length; i++) {
|
|
109
|
-
for (let j = i + 1; j < placed.length; j++) {
|
|
110
|
-
const a = bounds[placed[i].id];
|
|
111
|
-
const b = bounds[placed[j].id];
|
|
112
|
-
const horizontal = a.x < b.x + b.width + minGap && b.x < a.x + a.width + minGap;
|
|
113
|
-
const vertical = a.y < b.y + b.height + minGap && b.y < a.y + a.height + minGap;
|
|
114
|
-
if (horizontal && vertical) {
|
|
115
|
-
diagnostics.push({
|
|
116
|
-
level: 'error',
|
|
117
|
-
code: 'LAYOUT_OVERLAP',
|
|
118
|
-
message: `节点 ${placed[i].id} 与 ${placed[j].id} 的布局重叠或间距不足 ${minGap}px`,
|
|
119
|
-
nodeId: placed[j].id,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
for (const node of placed) {
|
|
125
|
-
const b = bounds[node.id];
|
|
126
|
-
if (b.x < 0 || b.y < 0) {
|
|
127
|
-
diagnostics.push({
|
|
128
|
-
level: 'warn',
|
|
129
|
-
code: 'LAYOUT_NEGATIVE',
|
|
130
|
-
message: `节点 ${node.id} 坐标为负 (${b.x}, ${b.y}),IDE 中可能显示不全;可增大 --center-x / --lane-gap`,
|
|
131
|
-
nodeId: node.id,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return { bounds, rankCenters, diagnostics };
|
|
226
|
+
return { bounds, rankCenters, diagnostics: checkBounds(graph, bounds, minGap) };
|
|
136
227
|
}
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const locale_1 = require("./commands/locale");
|
|
|
12
12
|
const operation_1 = require("./commands/operation");
|
|
13
13
|
const template_1 = require("./commands/template");
|
|
14
14
|
const logger_1 = require("./utils/logger");
|
|
15
|
-
const VERSION = '2.9.
|
|
15
|
+
const VERSION = '2.9.1';
|
|
16
16
|
const program = new commander_1.Command();
|
|
17
17
|
exports.program = program;
|
|
18
18
|
program
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lan-low-code-cli",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.1",
|
|
4
4
|
"description": "lan-low-code cli:低代码表单引擎命令行工具(创建表单、获取可用字段、自动完善表单字段)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"low-code",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"pack:skill": "node scripts/pack.js --target skill"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@dagrejs/dagre": "1.1.5",
|
|
32
33
|
"commander": "^11.1.0"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lan-low-code-cli
|
|
3
|
-
version: 2.9.
|
|
3
|
+
version: 2.9.1
|
|
4
4
|
author: Pig George
|
|
5
5
|
description: Operate the Landray low-code form engine through the `llc` CLI. Use when the user asks to create, inspect, modify, publish, or delete low-code apps and forms; generate field configs; configure layouts (grid/tabs), field permissions, options, i18n, or control actions (onChange/onLoad); or run pre-publish validation. Also use for troubleshooting form loading/rendering errors.
|
|
6
6
|
tags:
|
|
@@ -271,11 +271,14 @@ Application-scoped action flows. Every write command needs `--yes` and an explic
|
|
|
271
271
|
|
|
272
272
|
- `--code` must match `^[a-zA-Z0-9_]{1,30}$`; node `id` `^[0-9a-zA-Z_]{2,32}$`. Start/End nodes
|
|
273
273
|
and their edges are auto-added; disconnected roots/leaves are wired automatically.
|
|
274
|
-
- **Layout**: `ide` (default)
|
|
275
|
-
`--center-x` (default `679`), `BranchNode` 56×56, branch successors on
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
274
|
+
- **Layout**: `ide` (default) uses **dagre** (Sugiyama: layering → ordering → coordinates). Main chain
|
|
275
|
+
centred on `--center-x` (default `679`), `BranchNode` 56×56 and others 180×38, branch successors on
|
|
276
|
+
the next rank, parents centred over children, joins centred under parents, rank gap
|
|
277
|
+
`--rank-padding` (default 52), same-rank gap 60. `layered` keeps the legacy rank+lane algorithm
|
|
278
|
+
(branch successors on the same rank, left/right symmetric); `passthrough` keeps `bounds` from the
|
|
279
|
+
spec. `drawAttr.bounds` uses top-left `x/y` with `relative:false` and is persisted verbatim by the
|
|
280
|
+
IDE (it never recomputes coordinates). Branch edges get `exitX=0/1` from the actual relative x;
|
|
281
|
+
multi-in/multi-out edges get `exitX`/`entryX` 0.25/0.75 to avoid sharing one anchor point.
|
|
279
282
|
- Branch handling: `defaultTargetRef` comes from the edge with `"default": true`; a
|
|
280
283
|
`formulaConfig.branchFormulas` referencing the real line IDs is generated automatically, and branch
|
|
281
284
|
edges get `exitX=0/1` anchor styles so they do not overlap in the designer.
|
|
@@ -260,10 +260,12 @@ llc operation detach --form-id <fdId> --operation-id <operationId> #
|
|
|
260
260
|
|
|
261
261
|
- `--code` 需匹配 `^[a-zA-Z0-9_]{1,30}$`;节点 `id` 需匹配 `^[0-9a-zA-Z_]{2,32}$`。
|
|
262
262
|
开始/结束节点及其连线自动补齐,无入边/无出边的业务节点自动接线。
|
|
263
|
-
- **布局**:`ide
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
`
|
|
263
|
+
- **布局**:`ide`(默认)使用 **dagre**(Sugiyama:分层 → 层内排序 → 坐标分配)。主链以
|
|
264
|
+
`--center-x`(默认 `679`)居中,`BranchNode` 56×56、其余 180×38;分支后继位于下一层,父节点
|
|
265
|
+
居中于子节点之上,汇聚节点居中于多个父节点下方;层间距 `--rank-padding`(默认 52),同层间距 60。
|
|
266
|
+
`layered` 保留旧版 rank+lane 算法(分支后继与分支节点同层、左右对称);`passthrough` 沿用 spec
|
|
267
|
+
中的 `bounds`。`drawAttr.bounds` 是左上角 `x/y` 且带 `relative:false`,IDE **原样持久化、不会重算坐标**。
|
|
268
|
+
分支出线按实际相对位置写 `exitX=0/1`;多出边/多入边写 `exitX`/`entryX` 0.25/0.75,避免共用一个锚点。
|
|
267
269
|
- 分支处理:`defaultTargetRef` 取自 `"default": true` 的出边;自动生成引用真实连线 ID 的
|
|
268
270
|
`formulaConfig.branchFormulas`;分支连线写入 `exitX=0/1` 锚点,避免 IDE 内两条线重叠。
|
|
269
271
|
- `--resolve-schema` 通过 `getSourceJsonSchema` / `getProcessDataOutJsonSchema` 补齐
|