@diagrammo/dgmo 0.6.1 → 0.6.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.
- package/.claude/commands/dgmo.md +76 -0
- package/dist/cli.cjs +160 -159
- package/dist/index.cjs +780 -147
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +780 -147
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +33 -50
- package/package.json +4 -3
- package/src/c4/layout.ts +68 -5
- package/src/cli.ts +124 -2
- package/src/er/classify.ts +206 -0
- package/src/er/layout.ts +259 -94
- package/src/er/renderer.ts +231 -17
- package/src/infra/layout.ts +60 -13
- package/src/infra/renderer.ts +375 -32
- package/src/initiative-status/layout.ts +46 -30
- package/.claude/skills/dgmo-chart/SKILL.md +0 -141
- package/.claude/skills/dgmo-flowchart/SKILL.md +0 -61
- package/.claude/skills/dgmo-generate/SKILL.md +0 -59
- package/.claude/skills/dgmo-sequence/SKILL.md +0 -104
package/src/er/layout.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import dagre from '@dagrejs/dagre';
|
|
2
|
-
import type { ParsedERDiagram, ERTable } from './types';
|
|
2
|
+
import type { ParsedERDiagram, ERTable, ERRelationship } from './types';
|
|
3
3
|
|
|
4
4
|
// ============================================================
|
|
5
5
|
// Layout types
|
|
@@ -42,6 +42,9 @@ const MEMBER_LINE_HEIGHT = 18;
|
|
|
42
42
|
const COMPARTMENT_PADDING_Y = 8;
|
|
43
43
|
const SEPARATOR_HEIGHT = 1;
|
|
44
44
|
|
|
45
|
+
const HALF_MARGIN = 30;
|
|
46
|
+
const COMP_GAP = 60; // gap between packed components
|
|
47
|
+
|
|
45
48
|
// ============================================================
|
|
46
49
|
// Node sizing
|
|
47
50
|
// ============================================================
|
|
@@ -52,7 +55,6 @@ function computeNodeDimensions(table: ERTable): {
|
|
|
52
55
|
headerHeight: number;
|
|
53
56
|
columnsHeight: number;
|
|
54
57
|
} {
|
|
55
|
-
// Width: max of table name, column text lengths
|
|
56
58
|
let maxTextLen = table.name.length;
|
|
57
59
|
for (const col of table.columns) {
|
|
58
60
|
let colText = col.name;
|
|
@@ -61,7 +63,6 @@ function computeNodeDimensions(table: ERTable): {
|
|
|
61
63
|
maxTextLen = Math.max(maxTextLen, colText.length);
|
|
62
64
|
}
|
|
63
65
|
const width = Math.max(MIN_WIDTH, maxTextLen * CHAR_WIDTH + PADDING_X);
|
|
64
|
-
|
|
65
66
|
const headerHeight = HEADER_BASE;
|
|
66
67
|
|
|
67
68
|
let columnsHeight = 0;
|
|
@@ -72,12 +73,210 @@ function computeNodeDimensions(table: ERTable): {
|
|
|
72
73
|
SEPARATOR_HEIGHT;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
const height =
|
|
76
|
-
headerHeight + columnsHeight + (columnsHeight === 0 ? 4 : 0);
|
|
77
|
-
|
|
76
|
+
const height = headerHeight + columnsHeight + (columnsHeight === 0 ? 4 : 0);
|
|
78
77
|
return { width, height, headerHeight, columnsHeight };
|
|
79
78
|
}
|
|
80
79
|
|
|
80
|
+
// ============================================================
|
|
81
|
+
// Connected component detection (undirected BFS)
|
|
82
|
+
// ============================================================
|
|
83
|
+
|
|
84
|
+
function findConnectedComponents(
|
|
85
|
+
tableIds: string[],
|
|
86
|
+
relationships: ERRelationship[]
|
|
87
|
+
): string[][] {
|
|
88
|
+
const adj = new Map<string, Set<string>>();
|
|
89
|
+
for (const id of tableIds) adj.set(id, new Set());
|
|
90
|
+
for (const rel of relationships) {
|
|
91
|
+
adj.get(rel.source)?.add(rel.target);
|
|
92
|
+
adj.get(rel.target)?.add(rel.source);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const visited = new Set<string>();
|
|
96
|
+
const components: string[][] = [];
|
|
97
|
+
|
|
98
|
+
for (const id of tableIds) {
|
|
99
|
+
if (visited.has(id)) continue;
|
|
100
|
+
const comp: string[] = [];
|
|
101
|
+
const queue = [id];
|
|
102
|
+
while (queue.length > 0) {
|
|
103
|
+
const cur = queue.shift()!;
|
|
104
|
+
if (visited.has(cur)) continue;
|
|
105
|
+
visited.add(cur);
|
|
106
|
+
comp.push(cur);
|
|
107
|
+
for (const nb of adj.get(cur) ?? []) {
|
|
108
|
+
if (!visited.has(nb)) queue.push(nb);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
components.push(comp);
|
|
112
|
+
}
|
|
113
|
+
return components;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================================
|
|
117
|
+
// Per-component layout (independent dagre run)
|
|
118
|
+
// ============================================================
|
|
119
|
+
|
|
120
|
+
interface ComponentLayout {
|
|
121
|
+
/** Node center positions relative to this component's top-left origin */
|
|
122
|
+
nodePositions: Map<
|
|
123
|
+
string,
|
|
124
|
+
{ x: number; y: number; width: number; height: number; headerHeight: number; columnsHeight: number }
|
|
125
|
+
>;
|
|
126
|
+
/** Edge waypoints keyed by relationship lineNumber, relative to origin */
|
|
127
|
+
edgePoints: Map<number, { x: number; y: number }[]>;
|
|
128
|
+
width: number;
|
|
129
|
+
height: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function layoutComponent(
|
|
133
|
+
tables: ERTable[],
|
|
134
|
+
rels: ERRelationship[],
|
|
135
|
+
dimMap: Map<string, { width: number; height: number; headerHeight: number; columnsHeight: number }>
|
|
136
|
+
): ComponentLayout {
|
|
137
|
+
const nodePositions = new Map<
|
|
138
|
+
string,
|
|
139
|
+
{ x: number; y: number; width: number; height: number; headerHeight: number; columnsHeight: number }
|
|
140
|
+
>();
|
|
141
|
+
const edgePoints = new Map<number, { x: number; y: number }[]>();
|
|
142
|
+
|
|
143
|
+
if (tables.length === 1) {
|
|
144
|
+
// Single node — skip dagre
|
|
145
|
+
const dims = dimMap.get(tables[0].id)!;
|
|
146
|
+
nodePositions.set(tables[0].id, { x: dims.width / 2, y: dims.height / 2, ...dims });
|
|
147
|
+
return { nodePositions, edgePoints, width: dims.width, height: dims.height };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const g = new dagre.graphlib.Graph({ multigraph: true });
|
|
151
|
+
g.setGraph({ rankdir: 'LR', nodesep: 40, ranksep: 80, edgesep: 20 });
|
|
152
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
153
|
+
|
|
154
|
+
for (const table of tables) {
|
|
155
|
+
const dims = dimMap.get(table.id)!;
|
|
156
|
+
g.setNode(table.id, { width: dims.width, height: dims.height });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Use lineNumber as edge name to support multigraph (multiple edges between same pair)
|
|
160
|
+
for (const rel of rels) {
|
|
161
|
+
g.setEdge(rel.source, rel.target, { label: rel.label ?? '' }, String(rel.lineNumber));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
dagre.layout(g);
|
|
165
|
+
|
|
166
|
+
// Compute bounding box (dagre coordinates)
|
|
167
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
168
|
+
|
|
169
|
+
for (const table of tables) {
|
|
170
|
+
const pos = g.node(table.id);
|
|
171
|
+
const dims = dimMap.get(table.id)!;
|
|
172
|
+
minX = Math.min(minX, pos.x - dims.width / 2);
|
|
173
|
+
minY = Math.min(minY, pos.y - dims.height / 2);
|
|
174
|
+
maxX = Math.max(maxX, pos.x + dims.width / 2);
|
|
175
|
+
maxY = Math.max(maxY, pos.y + dims.height / 2);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const rel of rels) {
|
|
179
|
+
const ed = g.edge(rel.source, rel.target, String(rel.lineNumber));
|
|
180
|
+
for (const pt of ed?.points ?? []) {
|
|
181
|
+
minX = Math.min(minX, pt.x);
|
|
182
|
+
minY = Math.min(minY, pt.y);
|
|
183
|
+
maxX = Math.max(maxX, pt.x);
|
|
184
|
+
maxY = Math.max(maxY, pt.y);
|
|
185
|
+
}
|
|
186
|
+
if (rel.label && (ed?.points ?? []).length > 0) {
|
|
187
|
+
const pts = ed!.points;
|
|
188
|
+
const mid = pts[Math.floor(pts.length / 2)];
|
|
189
|
+
const hw = (rel.label.length * 7 + 8) / 2;
|
|
190
|
+
minX = Math.min(minX, mid.x - hw);
|
|
191
|
+
maxX = Math.max(maxX, mid.x + hw);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Normalize positions to component origin (0, 0)
|
|
196
|
+
for (const table of tables) {
|
|
197
|
+
const pos = g.node(table.id);
|
|
198
|
+
const dims = dimMap.get(table.id)!;
|
|
199
|
+
nodePositions.set(table.id, {
|
|
200
|
+
x: pos.x - minX,
|
|
201
|
+
y: pos.y - minY,
|
|
202
|
+
...dims,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
for (const rel of rels) {
|
|
206
|
+
const ed = g.edge(rel.source, rel.target, String(rel.lineNumber));
|
|
207
|
+
edgePoints.set(
|
|
208
|
+
rel.lineNumber,
|
|
209
|
+
(ed?.points ?? []).map((pt: { x: number; y: number }) => ({ x: pt.x - minX, y: pt.y - minY }))
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
nodePositions,
|
|
215
|
+
edgePoints,
|
|
216
|
+
width: Math.max(0, maxX - minX),
|
|
217
|
+
height: Math.max(0, maxY - minY),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ============================================================
|
|
222
|
+
// Row packing (Next Fit Decreasing Height)
|
|
223
|
+
// ============================================================
|
|
224
|
+
|
|
225
|
+
interface PackedComponent {
|
|
226
|
+
compIds: string[];
|
|
227
|
+
compLayout: ComponentLayout;
|
|
228
|
+
offsetX: number;
|
|
229
|
+
offsetY: number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function packComponents(
|
|
233
|
+
items: Array<{ compIds: string[]; compLayout: ComponentLayout }>
|
|
234
|
+
): PackedComponent[] {
|
|
235
|
+
if (items.length === 0) return [];
|
|
236
|
+
|
|
237
|
+
// Sort: multi-node components (have relationships, more interesting) first,
|
|
238
|
+
// then isolated single-node components. Within each group, sort by height descending.
|
|
239
|
+
const sorted = [...items].sort((a, b) => {
|
|
240
|
+
const aConnected = a.compIds.length > 1 ? 1 : 0;
|
|
241
|
+
const bConnected = b.compIds.length > 1 ? 1 : 0;
|
|
242
|
+
if (aConnected !== bConnected) return bConnected - aConnected;
|
|
243
|
+
return b.compLayout.height - a.compLayout.height;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Target width: sqrt of total content area × aspect factor (~1.5 = slightly landscape)
|
|
247
|
+
const totalArea = items.reduce(
|
|
248
|
+
(s, c) => s + (c.compLayout.width || MIN_WIDTH) * (c.compLayout.height || HEADER_BASE),
|
|
249
|
+
0
|
|
250
|
+
);
|
|
251
|
+
const targetW = Math.max(
|
|
252
|
+
Math.sqrt(totalArea) * 1.5,
|
|
253
|
+
sorted[0].compLayout.width // at least as wide as the widest component
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const placements: PackedComponent[] = [];
|
|
257
|
+
let curX = 0;
|
|
258
|
+
let curY = 0;
|
|
259
|
+
let rowH = 0;
|
|
260
|
+
|
|
261
|
+
for (const item of sorted) {
|
|
262
|
+
const w = item.compLayout.width || MIN_WIDTH;
|
|
263
|
+
const h = item.compLayout.height || HEADER_BASE;
|
|
264
|
+
|
|
265
|
+
// Wrap to next row if this item doesn't fit (but always place if row is empty)
|
|
266
|
+
if (curX > 0 && curX + w > targetW) {
|
|
267
|
+
curY += rowH + COMP_GAP;
|
|
268
|
+
curX = 0;
|
|
269
|
+
rowH = 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
placements.push({ compIds: item.compIds, compLayout: item.compLayout, offsetX: curX, offsetY: curY });
|
|
273
|
+
curX += w + COMP_GAP;
|
|
274
|
+
rowH = Math.max(rowH, h);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return placements;
|
|
278
|
+
}
|
|
279
|
+
|
|
81
280
|
// ============================================================
|
|
82
281
|
// Layout engine
|
|
83
282
|
// ============================================================
|
|
@@ -87,130 +286,96 @@ export function layoutERDiagram(parsed: ParsedERDiagram): ERLayoutResult {
|
|
|
87
286
|
return { nodes: [], edges: [], width: 0, height: 0 };
|
|
88
287
|
}
|
|
89
288
|
|
|
90
|
-
|
|
91
|
-
g.setGraph({
|
|
92
|
-
rankdir: 'TB',
|
|
93
|
-
nodesep: 60,
|
|
94
|
-
ranksep: 80,
|
|
95
|
-
edgesep: 20,
|
|
96
|
-
});
|
|
97
|
-
g.setDefaultEdgeLabel(() => ({}));
|
|
98
|
-
|
|
99
|
-
// Compute dimensions and add nodes
|
|
289
|
+
// ── 1. Node dimensions ──────────────────────────────────────────────────────
|
|
100
290
|
const dimMap = new Map<
|
|
101
291
|
string,
|
|
102
|
-
{
|
|
103
|
-
width: number;
|
|
104
|
-
height: number;
|
|
105
|
-
headerHeight: number;
|
|
106
|
-
columnsHeight: number;
|
|
107
|
-
}
|
|
292
|
+
{ width: number; height: number; headerHeight: number; columnsHeight: number }
|
|
108
293
|
>();
|
|
109
|
-
|
|
110
294
|
for (const table of parsed.tables) {
|
|
111
|
-
|
|
112
|
-
dimMap.set(table.id, dims);
|
|
113
|
-
g.setNode(table.id, {
|
|
114
|
-
label: table.name,
|
|
115
|
-
width: dims.width,
|
|
116
|
-
height: dims.height,
|
|
117
|
-
});
|
|
295
|
+
dimMap.set(table.id, computeNodeDimensions(table));
|
|
118
296
|
}
|
|
119
297
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
298
|
+
// ── 2. Find connected components ────────────────────────────────────────────
|
|
299
|
+
const compIdSets = findConnectedComponents(
|
|
300
|
+
parsed.tables.map((t) => t.id),
|
|
301
|
+
parsed.relationships
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// ── 3. Layout each component independently ──────────────────────────────────
|
|
305
|
+
const tableById = new Map(parsed.tables.map((t) => [t.id, t]));
|
|
306
|
+
|
|
307
|
+
const componentItems = compIdSets.map((ids) => {
|
|
308
|
+
const tables = ids.map((id) => tableById.get(id)!);
|
|
309
|
+
const rels = parsed.relationships.filter((r) => ids.includes(r.source));
|
|
310
|
+
return { compIds: ids, compLayout: layoutComponent(tables, rels, dimMap) };
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ── 4. Pack component bounding boxes into rows ───────────────────────────────
|
|
314
|
+
const packed = packComponents(componentItems);
|
|
315
|
+
|
|
316
|
+
// Build lookup: tableId → packed placement
|
|
317
|
+
const placementByTableId = new Map<string, PackedComponent>();
|
|
318
|
+
for (const p of packed) {
|
|
319
|
+
for (const id of p.compIds) placementByTableId.set(id, p);
|
|
123
320
|
}
|
|
124
321
|
|
|
125
|
-
//
|
|
126
|
-
|
|
322
|
+
// Build lookup: lineNumber → packed placement (for edges)
|
|
323
|
+
const placementByRelLine = new Map<number, PackedComponent>();
|
|
324
|
+
for (const p of packed) {
|
|
325
|
+
for (const lineNum of p.compLayout.edgePoints.keys()) {
|
|
326
|
+
placementByRelLine.set(lineNum, p);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
127
329
|
|
|
128
|
-
//
|
|
330
|
+
// ── 5. Assemble final layout ─────────────────────────────────────────────────
|
|
129
331
|
const layoutNodes: ERLayoutNode[] = parsed.tables.map((table) => {
|
|
130
|
-
const
|
|
131
|
-
const
|
|
332
|
+
const p = placementByTableId.get(table.id)!;
|
|
333
|
+
const pos = p.compLayout.nodePositions.get(table.id)!;
|
|
132
334
|
return {
|
|
133
335
|
...table,
|
|
134
|
-
x: pos.x,
|
|
135
|
-
y: pos.y,
|
|
136
|
-
width:
|
|
137
|
-
height:
|
|
138
|
-
headerHeight:
|
|
139
|
-
columnsHeight:
|
|
336
|
+
x: pos.x + p.offsetX + HALF_MARGIN,
|
|
337
|
+
y: pos.y + p.offsetY + HALF_MARGIN,
|
|
338
|
+
width: pos.width,
|
|
339
|
+
height: pos.height,
|
|
340
|
+
headerHeight: pos.headerHeight,
|
|
341
|
+
columnsHeight: pos.columnsHeight,
|
|
140
342
|
};
|
|
141
343
|
});
|
|
142
344
|
|
|
143
|
-
// Extract edge waypoints
|
|
144
345
|
const layoutEdges: ERLayoutEdge[] = parsed.relationships.map((rel) => {
|
|
145
|
-
const
|
|
346
|
+
const p = placementByRelLine.get(rel.lineNumber);
|
|
347
|
+
const pts = p?.compLayout.edgePoints.get(rel.lineNumber) ?? [];
|
|
146
348
|
return {
|
|
147
349
|
source: rel.source,
|
|
148
350
|
target: rel.target,
|
|
149
351
|
cardinality: rel.cardinality,
|
|
150
|
-
points:
|
|
352
|
+
points: pts.map((pt) => ({
|
|
353
|
+
x: pt.x + (p?.offsetX ?? 0) + HALF_MARGIN,
|
|
354
|
+
y: pt.y + (p?.offsetY ?? 0) + HALF_MARGIN,
|
|
355
|
+
})),
|
|
151
356
|
label: rel.label,
|
|
152
357
|
lineNumber: rel.lineNumber,
|
|
153
358
|
};
|
|
154
359
|
});
|
|
155
360
|
|
|
156
|
-
//
|
|
157
|
-
let minX = Infinity;
|
|
158
|
-
let minY = Infinity;
|
|
361
|
+
// ── 6. Total canvas dimensions ───────────────────────────────────────────────
|
|
159
362
|
let maxX = 0;
|
|
160
363
|
let maxY = 0;
|
|
161
364
|
for (const node of layoutNodes) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const top = node.y - node.height / 2;
|
|
165
|
-
const bottom = node.y + node.height / 2;
|
|
166
|
-
if (left < minX) minX = left;
|
|
167
|
-
if (right > maxX) maxX = right;
|
|
168
|
-
if (top < minY) minY = top;
|
|
169
|
-
if (bottom > maxY) maxY = bottom;
|
|
170
|
-
}
|
|
171
|
-
for (const edge of layoutEdges) {
|
|
172
|
-
for (const pt of edge.points) {
|
|
173
|
-
if (pt.x < minX) minX = pt.x;
|
|
174
|
-
if (pt.x > maxX) maxX = pt.x;
|
|
175
|
-
if (pt.y < minY) minY = pt.y;
|
|
176
|
-
if (pt.y > maxY) maxY = pt.y;
|
|
177
|
-
}
|
|
178
|
-
// Edge labels extend ~50px from midpoint
|
|
179
|
-
if (edge.label && edge.points.length > 0) {
|
|
180
|
-
const midPt = edge.points[Math.floor(edge.points.length / 2)];
|
|
181
|
-
const labelHalfW = (edge.label.length * 7 + 8) / 2;
|
|
182
|
-
if (midPt.x + labelHalfW > maxX) maxX = midPt.x + labelHalfW;
|
|
183
|
-
if (midPt.x - labelHalfW < minX) minX = midPt.x - labelHalfW;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Padding for cardinality markers (~25px) + edge labels
|
|
188
|
-
const EDGE_MARGIN = 60;
|
|
189
|
-
const HALF_MARGIN = EDGE_MARGIN / 2;
|
|
190
|
-
|
|
191
|
-
// Shift all nodes and edges so content starts at HALF_MARGIN (breathing room on all sides)
|
|
192
|
-
const shiftX = -minX + HALF_MARGIN;
|
|
193
|
-
const shiftY = -minY + HALF_MARGIN;
|
|
194
|
-
for (const node of layoutNodes) {
|
|
195
|
-
node.x += shiftX;
|
|
196
|
-
node.y += shiftY;
|
|
365
|
+
maxX = Math.max(maxX, node.x + node.width / 2);
|
|
366
|
+
maxY = Math.max(maxY, node.y + node.height / 2);
|
|
197
367
|
}
|
|
198
368
|
for (const edge of layoutEdges) {
|
|
199
369
|
for (const pt of edge.points) {
|
|
200
|
-
pt.x
|
|
201
|
-
pt.y
|
|
370
|
+
maxX = Math.max(maxX, pt.x);
|
|
371
|
+
maxY = Math.max(maxY, pt.y);
|
|
202
372
|
}
|
|
203
373
|
}
|
|
204
|
-
maxX += shiftX;
|
|
205
|
-
maxY += shiftY;
|
|
206
|
-
|
|
207
|
-
const totalWidth = maxX + HALF_MARGIN;
|
|
208
|
-
const totalHeight = maxY + HALF_MARGIN;
|
|
209
374
|
|
|
210
375
|
return {
|
|
211
376
|
nodes: layoutNodes,
|
|
212
377
|
edges: layoutEdges,
|
|
213
|
-
width:
|
|
214
|
-
height:
|
|
378
|
+
width: maxX + HALF_MARGIN,
|
|
379
|
+
height: maxY + HALF_MARGIN,
|
|
215
380
|
};
|
|
216
381
|
}
|