@hirokisakabe/pom 0.1.12 → 0.2.0
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 +28 -582
- package/dist/calcYogaLayout/calcYogaLayout.js +12 -0
- package/dist/inputSchema.d.ts +268 -2
- package/dist/inputSchema.d.ts.map +1 -1
- package/dist/inputSchema.js +53 -1
- package/dist/renderPptx/renderPptx.d.ts.map +1 -1
- package/dist/renderPptx/renderPptx.js +784 -0
- package/dist/toPositioned/toPositioned.d.ts.map +1 -1
- package/dist/toPositioned/toPositioned.js +45 -0
- package/dist/types.d.ts +399 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +134 -0
- package/package.json +11 -2
- package/dist/parsePptx/convertChart.d.ts +0 -8
- package/dist/parsePptx/convertChart.d.ts.map +0 -1
- package/dist/parsePptx/convertChart.js +0 -78
- package/dist/parsePptx/convertImage.d.ts +0 -8
- package/dist/parsePptx/convertImage.d.ts.map +0 -1
- package/dist/parsePptx/convertImage.js +0 -13
- package/dist/parsePptx/convertShape.d.ts +0 -7
- package/dist/parsePptx/convertShape.d.ts.map +0 -1
- package/dist/parsePptx/convertShape.js +0 -137
- package/dist/parsePptx/convertTable.d.ts +0 -7
- package/dist/parsePptx/convertTable.d.ts.map +0 -1
- package/dist/parsePptx/convertTable.js +0 -46
- package/dist/parsePptx/convertText.d.ts +0 -7
- package/dist/parsePptx/convertText.d.ts.map +0 -1
- package/dist/parsePptx/convertText.js +0 -32
- package/dist/parsePptx/index.d.ts +0 -23
- package/dist/parsePptx/index.d.ts.map +0 -1
- package/dist/parsePptx/index.js +0 -114
- package/dist/parsePptx/parseHtml.d.ts +0 -22
- package/dist/parsePptx/parseHtml.d.ts.map +0 -1
- package/dist/parsePptx/parseHtml.js +0 -53
- package/dist/parsePptx/types.d.ts +0 -15
- package/dist/parsePptx/types.d.ts.map +0 -1
- package/dist/parsePptx/types.js +0 -1
- package/dist/parsePptx/units.d.ts +0 -13
- package/dist/parsePptx/units.d.ts.map +0 -1
- package/dist/parsePptx/units.js +0 -19
|
@@ -201,6 +201,790 @@ export function renderPptx(pages, slidePx) {
|
|
|
201
201
|
slide.addChart(node.chartType, chartData, chartOptions);
|
|
202
202
|
break;
|
|
203
203
|
}
|
|
204
|
+
case "timeline": {
|
|
205
|
+
const direction = node.direction ?? "horizontal";
|
|
206
|
+
const items = node.items;
|
|
207
|
+
const itemCount = items.length;
|
|
208
|
+
if (itemCount === 0)
|
|
209
|
+
break;
|
|
210
|
+
const defaultColor = "1D4ED8"; // blue
|
|
211
|
+
const nodeRadius = 12; // px
|
|
212
|
+
const lineWidth = 4; // px
|
|
213
|
+
if (direction === "horizontal") {
|
|
214
|
+
// 水平タイムライン
|
|
215
|
+
const lineY = node.y + node.h / 2;
|
|
216
|
+
const startX = node.x + nodeRadius;
|
|
217
|
+
const endX = node.x + node.w - nodeRadius;
|
|
218
|
+
const lineLength = endX - startX;
|
|
219
|
+
// メインの線を描画
|
|
220
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
221
|
+
x: pxToIn(startX),
|
|
222
|
+
y: pxToIn(lineY),
|
|
223
|
+
w: pxToIn(lineLength),
|
|
224
|
+
h: 0,
|
|
225
|
+
line: { color: "E2E8F0", width: pxToPt(lineWidth) },
|
|
226
|
+
});
|
|
227
|
+
// 各アイテムを描画
|
|
228
|
+
items.forEach((item, index) => {
|
|
229
|
+
const progress = itemCount === 1 ? 0.5 : index / (itemCount - 1);
|
|
230
|
+
const cx = startX + lineLength * progress;
|
|
231
|
+
const cy = lineY;
|
|
232
|
+
const color = item.color ?? defaultColor;
|
|
233
|
+
// ノード(円)を描画
|
|
234
|
+
slide.addShape(pptx.ShapeType.ellipse, {
|
|
235
|
+
x: pxToIn(cx - nodeRadius),
|
|
236
|
+
y: pxToIn(cy - nodeRadius),
|
|
237
|
+
w: pxToIn(nodeRadius * 2),
|
|
238
|
+
h: pxToIn(nodeRadius * 2),
|
|
239
|
+
fill: { color },
|
|
240
|
+
line: { type: "none" },
|
|
241
|
+
});
|
|
242
|
+
// 日付を上に表示
|
|
243
|
+
slide.addText(item.date, {
|
|
244
|
+
x: pxToIn(cx - 60),
|
|
245
|
+
y: pxToIn(cy - nodeRadius - 40),
|
|
246
|
+
w: pxToIn(120),
|
|
247
|
+
h: pxToIn(24),
|
|
248
|
+
fontSize: pxToPt(12),
|
|
249
|
+
fontFace: "Noto Sans JP",
|
|
250
|
+
color: "64748B",
|
|
251
|
+
align: "center",
|
|
252
|
+
valign: "bottom",
|
|
253
|
+
});
|
|
254
|
+
// タイトルを下に表示
|
|
255
|
+
slide.addText(item.title, {
|
|
256
|
+
x: pxToIn(cx - 60),
|
|
257
|
+
y: pxToIn(cy + nodeRadius + 8),
|
|
258
|
+
w: pxToIn(120),
|
|
259
|
+
h: pxToIn(24),
|
|
260
|
+
fontSize: pxToPt(14),
|
|
261
|
+
fontFace: "Noto Sans JP",
|
|
262
|
+
color: "1E293B",
|
|
263
|
+
bold: true,
|
|
264
|
+
align: "center",
|
|
265
|
+
valign: "top",
|
|
266
|
+
});
|
|
267
|
+
// 説明を表示
|
|
268
|
+
if (item.description) {
|
|
269
|
+
slide.addText(item.description, {
|
|
270
|
+
x: pxToIn(cx - 60),
|
|
271
|
+
y: pxToIn(cy + nodeRadius + 32),
|
|
272
|
+
w: pxToIn(120),
|
|
273
|
+
h: pxToIn(32),
|
|
274
|
+
fontSize: pxToPt(11),
|
|
275
|
+
fontFace: "Noto Sans JP",
|
|
276
|
+
color: "64748B",
|
|
277
|
+
align: "center",
|
|
278
|
+
valign: "top",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
// 垂直タイムライン
|
|
285
|
+
const lineX = node.x + 40;
|
|
286
|
+
const startY = node.y + nodeRadius;
|
|
287
|
+
const endY = node.y + node.h - nodeRadius;
|
|
288
|
+
const lineLength = endY - startY;
|
|
289
|
+
// メインの線を描画
|
|
290
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
291
|
+
x: pxToIn(lineX),
|
|
292
|
+
y: pxToIn(startY),
|
|
293
|
+
w: 0,
|
|
294
|
+
h: pxToIn(lineLength),
|
|
295
|
+
line: { color: "E2E8F0", width: pxToPt(lineWidth) },
|
|
296
|
+
});
|
|
297
|
+
// 各アイテムを描画
|
|
298
|
+
items.forEach((item, index) => {
|
|
299
|
+
const progress = itemCount === 1 ? 0.5 : index / (itemCount - 1);
|
|
300
|
+
const cx = lineX;
|
|
301
|
+
const cy = startY + lineLength * progress;
|
|
302
|
+
const color = item.color ?? defaultColor;
|
|
303
|
+
// ノード(円)を描画
|
|
304
|
+
slide.addShape(pptx.ShapeType.ellipse, {
|
|
305
|
+
x: pxToIn(cx - nodeRadius),
|
|
306
|
+
y: pxToIn(cy - nodeRadius),
|
|
307
|
+
w: pxToIn(nodeRadius * 2),
|
|
308
|
+
h: pxToIn(nodeRadius * 2),
|
|
309
|
+
fill: { color },
|
|
310
|
+
line: { type: "none" },
|
|
311
|
+
});
|
|
312
|
+
// 日付を左上に表示
|
|
313
|
+
slide.addText(item.date, {
|
|
314
|
+
x: pxToIn(cx + nodeRadius + 16),
|
|
315
|
+
y: pxToIn(cy - nodeRadius - 4),
|
|
316
|
+
w: pxToIn(100),
|
|
317
|
+
h: pxToIn(20),
|
|
318
|
+
fontSize: pxToPt(12),
|
|
319
|
+
fontFace: "Noto Sans JP",
|
|
320
|
+
color: "64748B",
|
|
321
|
+
align: "left",
|
|
322
|
+
valign: "bottom",
|
|
323
|
+
});
|
|
324
|
+
// タイトルを右に表示
|
|
325
|
+
slide.addText(item.title, {
|
|
326
|
+
x: pxToIn(cx + nodeRadius + 16),
|
|
327
|
+
y: pxToIn(cy - 4),
|
|
328
|
+
w: pxToIn(node.w - 80),
|
|
329
|
+
h: pxToIn(24),
|
|
330
|
+
fontSize: pxToPt(14),
|
|
331
|
+
fontFace: "Noto Sans JP",
|
|
332
|
+
color: "1E293B",
|
|
333
|
+
bold: true,
|
|
334
|
+
align: "left",
|
|
335
|
+
valign: "top",
|
|
336
|
+
});
|
|
337
|
+
// 説明を表示
|
|
338
|
+
if (item.description) {
|
|
339
|
+
slide.addText(item.description, {
|
|
340
|
+
x: pxToIn(cx + nodeRadius + 16),
|
|
341
|
+
y: pxToIn(cy + 20),
|
|
342
|
+
w: pxToIn(node.w - 80),
|
|
343
|
+
h: pxToIn(32),
|
|
344
|
+
fontSize: pxToPt(11),
|
|
345
|
+
fontFace: "Noto Sans JP",
|
|
346
|
+
color: "64748B",
|
|
347
|
+
align: "left",
|
|
348
|
+
valign: "top",
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
case "matrix": {
|
|
356
|
+
const items = node.items;
|
|
357
|
+
const axes = node.axes;
|
|
358
|
+
const quadrants = node.quadrants;
|
|
359
|
+
const defaultItemColor = "1D4ED8"; // blue
|
|
360
|
+
const itemSize = 24; // px
|
|
361
|
+
const lineWidth = 2; // px
|
|
362
|
+
const axisColor = "E2E8F0";
|
|
363
|
+
// マトリクスの描画領域(パディングを考慮)
|
|
364
|
+
const padding = 60; // 軸ラベル用の余白
|
|
365
|
+
const areaX = node.x + padding;
|
|
366
|
+
const areaY = node.y + padding;
|
|
367
|
+
const areaW = node.w - padding * 2;
|
|
368
|
+
const areaH = node.h - padding * 2;
|
|
369
|
+
// 中心座標
|
|
370
|
+
const centerX = areaX + areaW / 2;
|
|
371
|
+
const centerY = areaY + areaH / 2;
|
|
372
|
+
// === 1. 十字線(軸線)を描画 ===
|
|
373
|
+
// 横線(X軸)
|
|
374
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
375
|
+
x: pxToIn(areaX),
|
|
376
|
+
y: pxToIn(centerY),
|
|
377
|
+
w: pxToIn(areaW),
|
|
378
|
+
h: 0,
|
|
379
|
+
line: { color: axisColor, width: pxToPt(lineWidth) },
|
|
380
|
+
});
|
|
381
|
+
// 縦線(Y軸)
|
|
382
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
383
|
+
x: pxToIn(centerX),
|
|
384
|
+
y: pxToIn(areaY),
|
|
385
|
+
w: 0,
|
|
386
|
+
h: pxToIn(areaH),
|
|
387
|
+
line: { color: axisColor, width: pxToPt(lineWidth) },
|
|
388
|
+
});
|
|
389
|
+
// === 2. 軸ラベルを描画 ===
|
|
390
|
+
// X軸ラベル(下部中央)
|
|
391
|
+
slide.addText(axes.x, {
|
|
392
|
+
x: pxToIn(centerX - 60),
|
|
393
|
+
y: pxToIn(areaY + areaH + 8),
|
|
394
|
+
w: pxToIn(120),
|
|
395
|
+
h: pxToIn(24),
|
|
396
|
+
fontSize: pxToPt(12),
|
|
397
|
+
fontFace: "Noto Sans JP",
|
|
398
|
+
color: "64748B",
|
|
399
|
+
align: "center",
|
|
400
|
+
valign: "top",
|
|
401
|
+
});
|
|
402
|
+
// Y軸ラベル(左部中央)
|
|
403
|
+
slide.addText(axes.y, {
|
|
404
|
+
x: pxToIn(node.x + 4),
|
|
405
|
+
y: pxToIn(centerY - 12),
|
|
406
|
+
w: pxToIn(48),
|
|
407
|
+
h: pxToIn(24),
|
|
408
|
+
fontSize: pxToPt(12),
|
|
409
|
+
fontFace: "Noto Sans JP",
|
|
410
|
+
color: "64748B",
|
|
411
|
+
align: "center",
|
|
412
|
+
valign: "middle",
|
|
413
|
+
});
|
|
414
|
+
// === 3. 象限ラベルを描画 ===
|
|
415
|
+
if (quadrants) {
|
|
416
|
+
const quadrantFontSize = 11;
|
|
417
|
+
const quadrantColor = "94A3B8"; // slate-400
|
|
418
|
+
const quadrantW = areaW / 2 - 20;
|
|
419
|
+
const quadrantH = 48;
|
|
420
|
+
// 左上
|
|
421
|
+
slide.addText(quadrants.topLeft, {
|
|
422
|
+
x: pxToIn(areaX + 10),
|
|
423
|
+
y: pxToIn(areaY + 10),
|
|
424
|
+
w: pxToIn(quadrantW),
|
|
425
|
+
h: pxToIn(quadrantH),
|
|
426
|
+
fontSize: pxToPt(quadrantFontSize),
|
|
427
|
+
fontFace: "Noto Sans JP",
|
|
428
|
+
color: quadrantColor,
|
|
429
|
+
align: "left",
|
|
430
|
+
valign: "top",
|
|
431
|
+
});
|
|
432
|
+
// 右上
|
|
433
|
+
slide.addText(quadrants.topRight, {
|
|
434
|
+
x: pxToIn(centerX + 10),
|
|
435
|
+
y: pxToIn(areaY + 10),
|
|
436
|
+
w: pxToIn(quadrantW),
|
|
437
|
+
h: pxToIn(quadrantH),
|
|
438
|
+
fontSize: pxToPt(quadrantFontSize),
|
|
439
|
+
fontFace: "Noto Sans JP",
|
|
440
|
+
color: quadrantColor,
|
|
441
|
+
align: "right",
|
|
442
|
+
valign: "top",
|
|
443
|
+
});
|
|
444
|
+
// 左下
|
|
445
|
+
slide.addText(quadrants.bottomLeft, {
|
|
446
|
+
x: pxToIn(areaX + 10),
|
|
447
|
+
y: pxToIn(centerY + areaH / 2 - quadrantH - 10),
|
|
448
|
+
w: pxToIn(quadrantW),
|
|
449
|
+
h: pxToIn(quadrantH),
|
|
450
|
+
fontSize: pxToPt(quadrantFontSize),
|
|
451
|
+
fontFace: "Noto Sans JP",
|
|
452
|
+
color: quadrantColor,
|
|
453
|
+
align: "left",
|
|
454
|
+
valign: "bottom",
|
|
455
|
+
});
|
|
456
|
+
// 右下
|
|
457
|
+
slide.addText(quadrants.bottomRight, {
|
|
458
|
+
x: pxToIn(centerX + 10),
|
|
459
|
+
y: pxToIn(centerY + areaH / 2 - quadrantH - 10),
|
|
460
|
+
w: pxToIn(quadrantW),
|
|
461
|
+
h: pxToIn(quadrantH),
|
|
462
|
+
fontSize: pxToPt(quadrantFontSize),
|
|
463
|
+
fontFace: "Noto Sans JP",
|
|
464
|
+
color: quadrantColor,
|
|
465
|
+
align: "right",
|
|
466
|
+
valign: "bottom",
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
// === 4. アイテムをプロット ===
|
|
470
|
+
for (const item of items) {
|
|
471
|
+
// 座標変換: (0,0)=左下, (1,1)=右上
|
|
472
|
+
// x: 0 -> areaX, 1 -> areaX + areaW
|
|
473
|
+
// y: 0 -> areaY + areaH, 1 -> areaY (反転)
|
|
474
|
+
const itemX = areaX + item.x * areaW;
|
|
475
|
+
const itemY = areaY + (1 - item.y) * areaH; // Y軸反転
|
|
476
|
+
const itemColor = item.color ?? defaultItemColor;
|
|
477
|
+
// 円を描画
|
|
478
|
+
slide.addShape(pptx.ShapeType.ellipse, {
|
|
479
|
+
x: pxToIn(itemX - itemSize / 2),
|
|
480
|
+
y: pxToIn(itemY - itemSize / 2),
|
|
481
|
+
w: pxToIn(itemSize),
|
|
482
|
+
h: pxToIn(itemSize),
|
|
483
|
+
fill: { color: itemColor },
|
|
484
|
+
line: { type: "none" },
|
|
485
|
+
});
|
|
486
|
+
// ラベルを描画(円の上)
|
|
487
|
+
slide.addText(item.label, {
|
|
488
|
+
x: pxToIn(itemX - 50),
|
|
489
|
+
y: pxToIn(itemY - itemSize / 2 - 20),
|
|
490
|
+
w: pxToIn(100),
|
|
491
|
+
h: pxToIn(18),
|
|
492
|
+
fontSize: pxToPt(11),
|
|
493
|
+
fontFace: "Noto Sans JP",
|
|
494
|
+
color: "1E293B",
|
|
495
|
+
bold: true,
|
|
496
|
+
align: "center",
|
|
497
|
+
valign: "bottom",
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
case "tree": {
|
|
503
|
+
const layout = node.layout ?? "vertical";
|
|
504
|
+
const nodeShape = node.nodeShape ?? "rect";
|
|
505
|
+
const nodeWidth = node.nodeWidth ?? 120;
|
|
506
|
+
const nodeHeight = node.nodeHeight ?? 40;
|
|
507
|
+
const levelGap = node.levelGap ?? 60;
|
|
508
|
+
const siblingGap = node.siblingGap ?? 20;
|
|
509
|
+
const connectorStyle = node.connectorStyle ?? {};
|
|
510
|
+
const defaultColor = "1D4ED8";
|
|
511
|
+
// サブツリーの幅/高さを計算
|
|
512
|
+
function calculateSubtreeSize(item) {
|
|
513
|
+
if (!item.children || item.children.length === 0) {
|
|
514
|
+
return { width: nodeWidth, height: nodeHeight };
|
|
515
|
+
}
|
|
516
|
+
const childSizes = item.children.map(calculateSubtreeSize);
|
|
517
|
+
if (layout === "vertical") {
|
|
518
|
+
const childrenWidth = childSizes.reduce((sum, s) => sum + s.width, 0) +
|
|
519
|
+
siblingGap * (childSizes.length - 1);
|
|
520
|
+
const childrenHeight = Math.max(...childSizes.map((s) => s.height));
|
|
521
|
+
return {
|
|
522
|
+
width: Math.max(nodeWidth, childrenWidth),
|
|
523
|
+
height: nodeHeight + levelGap + childrenHeight,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
const childrenHeight = childSizes.reduce((sum, s) => sum + s.height, 0) +
|
|
528
|
+
siblingGap * (childSizes.length - 1);
|
|
529
|
+
const childrenWidth = Math.max(...childSizes.map((s) => s.width));
|
|
530
|
+
return {
|
|
531
|
+
width: nodeWidth + levelGap + childrenWidth,
|
|
532
|
+
height: Math.max(nodeHeight, childrenHeight),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// ツリーレイアウトを計算
|
|
537
|
+
function calculateTreeLayout(item, x, y) {
|
|
538
|
+
const subtreeSize = calculateSubtreeSize(item);
|
|
539
|
+
const layoutNode = {
|
|
540
|
+
item,
|
|
541
|
+
x: 0,
|
|
542
|
+
y: 0,
|
|
543
|
+
width: nodeWidth,
|
|
544
|
+
height: nodeHeight,
|
|
545
|
+
children: [],
|
|
546
|
+
};
|
|
547
|
+
if (layout === "vertical") {
|
|
548
|
+
// ノードを中央上部に配置
|
|
549
|
+
layoutNode.x = x + subtreeSize.width / 2 - nodeWidth / 2;
|
|
550
|
+
layoutNode.y = y;
|
|
551
|
+
// 子ノードを配置
|
|
552
|
+
if (item.children && item.children.length > 0) {
|
|
553
|
+
const childSizes = item.children.map(calculateSubtreeSize);
|
|
554
|
+
const totalChildWidth = childSizes.reduce((sum, s) => sum + s.width, 0) +
|
|
555
|
+
siblingGap * (childSizes.length - 1);
|
|
556
|
+
let childX = x + subtreeSize.width / 2 - totalChildWidth / 2;
|
|
557
|
+
const childY = y + nodeHeight + levelGap;
|
|
558
|
+
for (let i = 0; i < item.children.length; i++) {
|
|
559
|
+
const child = item.children[i];
|
|
560
|
+
const childLayout = calculateTreeLayout(child, childX, childY);
|
|
561
|
+
layoutNode.children.push(childLayout);
|
|
562
|
+
childX += childSizes[i].width + siblingGap;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
// horizontal: ノードを左中央に配置
|
|
568
|
+
layoutNode.x = x;
|
|
569
|
+
layoutNode.y = y + subtreeSize.height / 2 - nodeHeight / 2;
|
|
570
|
+
// 子ノードを配置
|
|
571
|
+
if (item.children && item.children.length > 0) {
|
|
572
|
+
const childSizes = item.children.map(calculateSubtreeSize);
|
|
573
|
+
const totalChildHeight = childSizes.reduce((sum, s) => sum + s.height, 0) +
|
|
574
|
+
siblingGap * (childSizes.length - 1);
|
|
575
|
+
const childX = x + nodeWidth + levelGap;
|
|
576
|
+
let childY = y + subtreeSize.height / 2 - totalChildHeight / 2;
|
|
577
|
+
for (let i = 0; i < item.children.length; i++) {
|
|
578
|
+
const child = item.children[i];
|
|
579
|
+
const childLayout = calculateTreeLayout(child, childX, childY);
|
|
580
|
+
layoutNode.children.push(childLayout);
|
|
581
|
+
childY += childSizes[i].height + siblingGap;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return layoutNode;
|
|
586
|
+
}
|
|
587
|
+
// 接続線を描画
|
|
588
|
+
function drawConnector(parent, child, style) {
|
|
589
|
+
const lineColor = style.color ?? "333333";
|
|
590
|
+
const lineWidth = style.width ?? 2;
|
|
591
|
+
if (layout === "vertical") {
|
|
592
|
+
// 親の下端中央から子の上端中央へ
|
|
593
|
+
const parentCenterX = parent.x + parent.width / 2;
|
|
594
|
+
const parentBottomY = parent.y + parent.height;
|
|
595
|
+
const childCenterX = child.x + child.width / 2;
|
|
596
|
+
const childTopY = child.y;
|
|
597
|
+
const midY = (parentBottomY + childTopY) / 2;
|
|
598
|
+
// 垂直線(親から中間点まで)
|
|
599
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
600
|
+
x: pxToIn(parentCenterX),
|
|
601
|
+
y: pxToIn(parentBottomY),
|
|
602
|
+
w: 0,
|
|
603
|
+
h: pxToIn(midY - parentBottomY),
|
|
604
|
+
line: { color: lineColor, width: pxToPt(lineWidth) },
|
|
605
|
+
});
|
|
606
|
+
// 水平線(中間点で)
|
|
607
|
+
const minX = Math.min(parentCenterX, childCenterX);
|
|
608
|
+
const maxX = Math.max(parentCenterX, childCenterX);
|
|
609
|
+
if (maxX > minX) {
|
|
610
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
611
|
+
x: pxToIn(minX),
|
|
612
|
+
y: pxToIn(midY),
|
|
613
|
+
w: pxToIn(maxX - minX),
|
|
614
|
+
h: 0,
|
|
615
|
+
line: { color: lineColor, width: pxToPt(lineWidth) },
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
// 垂直線(中間点から子まで)
|
|
619
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
620
|
+
x: pxToIn(childCenterX),
|
|
621
|
+
y: pxToIn(midY),
|
|
622
|
+
w: 0,
|
|
623
|
+
h: pxToIn(childTopY - midY),
|
|
624
|
+
line: { color: lineColor, width: pxToPt(lineWidth) },
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
// 親の右端中央から子の左端中央へ
|
|
629
|
+
const parentRightX = parent.x + parent.width;
|
|
630
|
+
const parentCenterY = parent.y + parent.height / 2;
|
|
631
|
+
const childLeftX = child.x;
|
|
632
|
+
const childCenterY = child.y + child.height / 2;
|
|
633
|
+
const midX = (parentRightX + childLeftX) / 2;
|
|
634
|
+
// 水平線(親から中間点まで)
|
|
635
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
636
|
+
x: pxToIn(parentRightX),
|
|
637
|
+
y: pxToIn(parentCenterY),
|
|
638
|
+
w: pxToIn(midX - parentRightX),
|
|
639
|
+
h: 0,
|
|
640
|
+
line: { color: lineColor, width: pxToPt(lineWidth) },
|
|
641
|
+
});
|
|
642
|
+
// 垂直線(中間点で)
|
|
643
|
+
const minY = Math.min(parentCenterY, childCenterY);
|
|
644
|
+
const maxY = Math.max(parentCenterY, childCenterY);
|
|
645
|
+
if (maxY > minY) {
|
|
646
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
647
|
+
x: pxToIn(midX),
|
|
648
|
+
y: pxToIn(minY),
|
|
649
|
+
w: 0,
|
|
650
|
+
h: pxToIn(maxY - minY),
|
|
651
|
+
line: { color: lineColor, width: pxToPt(lineWidth) },
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
// 水平線(中間点から子まで)
|
|
655
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
656
|
+
x: pxToIn(midX),
|
|
657
|
+
y: pxToIn(childCenterY),
|
|
658
|
+
w: pxToIn(childLeftX - midX),
|
|
659
|
+
h: 0,
|
|
660
|
+
line: { color: lineColor, width: pxToPt(lineWidth) },
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// ノードを描画
|
|
665
|
+
function drawTreeNode(layoutNode, shape, defaultNodeColor) {
|
|
666
|
+
const color = layoutNode.item.color ?? defaultNodeColor;
|
|
667
|
+
const shapeType = (() => {
|
|
668
|
+
switch (shape) {
|
|
669
|
+
case "rect":
|
|
670
|
+
return pptx.ShapeType.rect;
|
|
671
|
+
case "roundRect":
|
|
672
|
+
return pptx.ShapeType.roundRect;
|
|
673
|
+
case "ellipse":
|
|
674
|
+
return pptx.ShapeType.ellipse;
|
|
675
|
+
}
|
|
676
|
+
})();
|
|
677
|
+
// ノードの背景
|
|
678
|
+
slide.addShape(shapeType, {
|
|
679
|
+
x: pxToIn(layoutNode.x),
|
|
680
|
+
y: pxToIn(layoutNode.y),
|
|
681
|
+
w: pxToIn(layoutNode.width),
|
|
682
|
+
h: pxToIn(layoutNode.height),
|
|
683
|
+
fill: { color },
|
|
684
|
+
line: { color: "333333", width: pxToPt(1) },
|
|
685
|
+
});
|
|
686
|
+
// ノードのラベル
|
|
687
|
+
slide.addText(layoutNode.item.label, {
|
|
688
|
+
x: pxToIn(layoutNode.x),
|
|
689
|
+
y: pxToIn(layoutNode.y),
|
|
690
|
+
w: pxToIn(layoutNode.width),
|
|
691
|
+
h: pxToIn(layoutNode.height),
|
|
692
|
+
fontSize: pxToPt(12),
|
|
693
|
+
fontFace: "Noto Sans JP",
|
|
694
|
+
color: "FFFFFF",
|
|
695
|
+
align: "center",
|
|
696
|
+
valign: "middle",
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
// すべての接続線を再帰的に描画
|
|
700
|
+
function drawAllConnectors(layoutNode) {
|
|
701
|
+
for (const child of layoutNode.children) {
|
|
702
|
+
drawConnector(layoutNode, child, connectorStyle);
|
|
703
|
+
drawAllConnectors(child);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// すべてのノードを再帰的に描画
|
|
707
|
+
function drawAllNodes(layoutNode) {
|
|
708
|
+
drawTreeNode(layoutNode, nodeShape, defaultColor);
|
|
709
|
+
for (const child of layoutNode.children) {
|
|
710
|
+
drawAllNodes(child);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// ツリーのサイズを計算
|
|
714
|
+
const treeSize = calculateSubtreeSize(node.data);
|
|
715
|
+
// 描画領域内の中央に配置
|
|
716
|
+
const offsetX = node.x + (node.w - treeSize.width) / 2;
|
|
717
|
+
const offsetY = node.y + (node.h - treeSize.height) / 2;
|
|
718
|
+
// レイアウト計算
|
|
719
|
+
const rootLayout = calculateTreeLayout(node.data, offsetX, offsetY);
|
|
720
|
+
// 描画(接続線を先に、ノードを後に描画)
|
|
721
|
+
drawAllConnectors(rootLayout);
|
|
722
|
+
drawAllNodes(rootLayout);
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
case "flow": {
|
|
726
|
+
const direction = node.direction ?? "horizontal";
|
|
727
|
+
const nodeWidth = node.nodeWidth ?? 120;
|
|
728
|
+
const nodeHeight = node.nodeHeight ?? 60;
|
|
729
|
+
const nodeGap = node.nodeGap ?? 80;
|
|
730
|
+
const connectorStyle = node.connectorStyle ?? {};
|
|
731
|
+
const defaultColor = "1D4ED8";
|
|
732
|
+
const layouts = new Map();
|
|
733
|
+
const nodeCount = node.nodes.length;
|
|
734
|
+
// ノードのレイアウトを計算
|
|
735
|
+
if (direction === "horizontal") {
|
|
736
|
+
// 水平方向: 左から右へ均等配置
|
|
737
|
+
const totalWidth = nodeCount * nodeWidth + (nodeCount - 1) * nodeGap;
|
|
738
|
+
const startX = node.x + (node.w - totalWidth) / 2;
|
|
739
|
+
const centerY = node.y + node.h / 2;
|
|
740
|
+
node.nodes.forEach((item, index) => {
|
|
741
|
+
const w = item.width ?? nodeWidth;
|
|
742
|
+
const h = item.height ?? nodeHeight;
|
|
743
|
+
layouts.set(item.id, {
|
|
744
|
+
id: item.id,
|
|
745
|
+
x: startX + index * (nodeWidth + nodeGap) + (nodeWidth - w) / 2,
|
|
746
|
+
y: centerY - h / 2,
|
|
747
|
+
width: w,
|
|
748
|
+
height: h,
|
|
749
|
+
item,
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
// 垂直方向: 上から下へ均等配置
|
|
755
|
+
const totalHeight = nodeCount * nodeHeight + (nodeCount - 1) * nodeGap;
|
|
756
|
+
const startY = node.y + (node.h - totalHeight) / 2;
|
|
757
|
+
const centerX = node.x + node.w / 2;
|
|
758
|
+
node.nodes.forEach((item, index) => {
|
|
759
|
+
const w = item.width ?? nodeWidth;
|
|
760
|
+
const h = item.height ?? nodeHeight;
|
|
761
|
+
layouts.set(item.id, {
|
|
762
|
+
id: item.id,
|
|
763
|
+
x: centerX - w / 2,
|
|
764
|
+
y: startY +
|
|
765
|
+
index * (nodeHeight + nodeGap) +
|
|
766
|
+
(nodeHeight - h) / 2,
|
|
767
|
+
width: w,
|
|
768
|
+
height: h,
|
|
769
|
+
item,
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
// 接続線を描画(ノードより先に描画して背面に配置)
|
|
774
|
+
for (const conn of node.connections) {
|
|
775
|
+
const fromLayout = layouts.get(conn.from);
|
|
776
|
+
const toLayout = layouts.get(conn.to);
|
|
777
|
+
if (!fromLayout || !toLayout)
|
|
778
|
+
continue;
|
|
779
|
+
const lineColor = conn.color ?? connectorStyle.color ?? "333333";
|
|
780
|
+
const lineWidth = connectorStyle.width ?? 2;
|
|
781
|
+
const arrowType = connectorStyle.arrowType ?? "triangle";
|
|
782
|
+
let startX, startY, endX, endY;
|
|
783
|
+
if (direction === "horizontal") {
|
|
784
|
+
// 水平: 右端から左端へ
|
|
785
|
+
startX = fromLayout.x + fromLayout.width;
|
|
786
|
+
startY = fromLayout.y + fromLayout.height / 2;
|
|
787
|
+
endX = toLayout.x;
|
|
788
|
+
endY = toLayout.y + toLayout.height / 2;
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
// 垂直: 下端から上端へ
|
|
792
|
+
startX = fromLayout.x + fromLayout.width / 2;
|
|
793
|
+
startY = fromLayout.y + fromLayout.height;
|
|
794
|
+
endX = toLayout.x + toLayout.width / 2;
|
|
795
|
+
endY = toLayout.y;
|
|
796
|
+
}
|
|
797
|
+
// 直線接続(シンプルなケース)
|
|
798
|
+
const isHorizontalLine = Math.abs(startY - endY) < 1;
|
|
799
|
+
const isVerticalLine = Math.abs(startX - endX) < 1;
|
|
800
|
+
if (isHorizontalLine || isVerticalLine) {
|
|
801
|
+
// 直線で描画
|
|
802
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
803
|
+
x: pxToIn(Math.min(startX, endX)),
|
|
804
|
+
y: pxToIn(Math.min(startY, endY)),
|
|
805
|
+
w: pxToIn(Math.abs(endX - startX)),
|
|
806
|
+
h: pxToIn(Math.abs(endY - startY)),
|
|
807
|
+
line: {
|
|
808
|
+
color: lineColor,
|
|
809
|
+
width: pxToPt(lineWidth),
|
|
810
|
+
endArrowType: arrowType,
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
// L字型接続
|
|
816
|
+
const midX = (startX + endX) / 2;
|
|
817
|
+
const midY = (startY + endY) / 2;
|
|
818
|
+
if (direction === "horizontal") {
|
|
819
|
+
// 水平→垂直→水平
|
|
820
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
821
|
+
x: pxToIn(startX),
|
|
822
|
+
y: pxToIn(startY),
|
|
823
|
+
w: pxToIn(midX - startX),
|
|
824
|
+
h: 0,
|
|
825
|
+
line: { color: lineColor, width: pxToPt(lineWidth) },
|
|
826
|
+
});
|
|
827
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
828
|
+
x: pxToIn(midX),
|
|
829
|
+
y: pxToIn(Math.min(startY, endY)),
|
|
830
|
+
w: 0,
|
|
831
|
+
h: pxToIn(Math.abs(endY - startY)),
|
|
832
|
+
line: { color: lineColor, width: pxToPt(lineWidth) },
|
|
833
|
+
});
|
|
834
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
835
|
+
x: pxToIn(midX),
|
|
836
|
+
y: pxToIn(endY),
|
|
837
|
+
w: pxToIn(endX - midX),
|
|
838
|
+
h: 0,
|
|
839
|
+
line: {
|
|
840
|
+
color: lineColor,
|
|
841
|
+
width: pxToPt(lineWidth),
|
|
842
|
+
endArrowType: arrowType,
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
// 垂直→水平→垂直
|
|
848
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
849
|
+
x: pxToIn(startX),
|
|
850
|
+
y: pxToIn(startY),
|
|
851
|
+
w: 0,
|
|
852
|
+
h: pxToIn(midY - startY),
|
|
853
|
+
line: { color: lineColor, width: pxToPt(lineWidth) },
|
|
854
|
+
});
|
|
855
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
856
|
+
x: pxToIn(Math.min(startX, endX)),
|
|
857
|
+
y: pxToIn(midY),
|
|
858
|
+
w: pxToIn(Math.abs(endX - startX)),
|
|
859
|
+
h: 0,
|
|
860
|
+
line: { color: lineColor, width: pxToPt(lineWidth) },
|
|
861
|
+
});
|
|
862
|
+
slide.addShape(pptx.ShapeType.line, {
|
|
863
|
+
x: pxToIn(endX),
|
|
864
|
+
y: pxToIn(midY),
|
|
865
|
+
w: 0,
|
|
866
|
+
h: pxToIn(endY - midY),
|
|
867
|
+
line: {
|
|
868
|
+
color: lineColor,
|
|
869
|
+
width: pxToPt(lineWidth),
|
|
870
|
+
endArrowType: arrowType,
|
|
871
|
+
},
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// ラベルを描画
|
|
876
|
+
if (conn.label) {
|
|
877
|
+
const labelX = (startX + endX) / 2;
|
|
878
|
+
const labelY = (startY + endY) / 2;
|
|
879
|
+
slide.addText(conn.label, {
|
|
880
|
+
x: pxToIn(labelX - 30),
|
|
881
|
+
y: pxToIn(labelY - 10),
|
|
882
|
+
w: pxToIn(60),
|
|
883
|
+
h: pxToIn(20),
|
|
884
|
+
fontSize: pxToPt(10),
|
|
885
|
+
fontFace: "Noto Sans JP",
|
|
886
|
+
color: "64748B",
|
|
887
|
+
align: "center",
|
|
888
|
+
valign: "middle",
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
// ノードを描画
|
|
893
|
+
for (const item of node.nodes) {
|
|
894
|
+
const layout = layouts.get(item.id);
|
|
895
|
+
if (!layout)
|
|
896
|
+
continue;
|
|
897
|
+
const fillColor = item.color ?? defaultColor;
|
|
898
|
+
const textColor = item.textColor ?? "FFFFFF";
|
|
899
|
+
// 図形を描画
|
|
900
|
+
slide.addText(item.text, {
|
|
901
|
+
x: pxToIn(layout.x),
|
|
902
|
+
y: pxToIn(layout.y),
|
|
903
|
+
w: pxToIn(layout.width),
|
|
904
|
+
h: pxToIn(layout.height),
|
|
905
|
+
shape: item.shape,
|
|
906
|
+
fill: { color: fillColor },
|
|
907
|
+
line: { color: "333333", width: pxToPt(1) },
|
|
908
|
+
fontSize: pxToPt(14),
|
|
909
|
+
fontFace: "Noto Sans JP",
|
|
910
|
+
color: textColor,
|
|
911
|
+
align: "center",
|
|
912
|
+
valign: "middle",
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
case "processArrow": {
|
|
918
|
+
const direction = node.direction ?? "horizontal";
|
|
919
|
+
const steps = node.steps;
|
|
920
|
+
const stepCount = steps.length;
|
|
921
|
+
if (stepCount === 0)
|
|
922
|
+
break;
|
|
923
|
+
const defaultColor = "4472C4"; // PowerPoint標準の青
|
|
924
|
+
const defaultTextColor = "FFFFFF";
|
|
925
|
+
const itemWidth = node.itemWidth ?? 150;
|
|
926
|
+
const itemHeight = node.itemHeight ?? 60;
|
|
927
|
+
const gap = node.gap ?? -15; // 負の値でシェブロンを重ねる
|
|
928
|
+
if (direction === "horizontal") {
|
|
929
|
+
// 水平方向のプロセスアロー
|
|
930
|
+
const totalWidth = stepCount * itemWidth + (stepCount - 1) * gap;
|
|
931
|
+
const startX = node.x + (node.w - totalWidth) / 2;
|
|
932
|
+
const centerY = node.y + node.h / 2;
|
|
933
|
+
steps.forEach((step, index) => {
|
|
934
|
+
const stepX = startX + index * (itemWidth + gap);
|
|
935
|
+
const stepY = centerY - itemHeight / 2;
|
|
936
|
+
const fillColor = step.color?.replace("#", "") ?? defaultColor;
|
|
937
|
+
const textColor = step.textColor?.replace("#", "") ?? defaultTextColor;
|
|
938
|
+
// 最初のステップは homePlate、以降は chevron
|
|
939
|
+
const shapeType = index === 0 ? pptx.ShapeType.homePlate : pptx.ShapeType.chevron;
|
|
940
|
+
slide.addText(step.label, {
|
|
941
|
+
x: pxToIn(stepX),
|
|
942
|
+
y: pxToIn(stepY),
|
|
943
|
+
w: pxToIn(itemWidth),
|
|
944
|
+
h: pxToIn(itemHeight),
|
|
945
|
+
shape: shapeType,
|
|
946
|
+
fill: { color: fillColor },
|
|
947
|
+
line: { type: "none" },
|
|
948
|
+
fontSize: pxToPt(node.fontPx ?? 14),
|
|
949
|
+
fontFace: "Noto Sans JP",
|
|
950
|
+
color: textColor,
|
|
951
|
+
bold: node.bold ?? false,
|
|
952
|
+
align: "center",
|
|
953
|
+
valign: "middle",
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
// 垂直方向のプロセスアロー
|
|
959
|
+
const totalHeight = stepCount * itemHeight + (stepCount - 1) * gap;
|
|
960
|
+
const startY = node.y + (node.h - totalHeight) / 2;
|
|
961
|
+
const centerX = node.x + node.w / 2;
|
|
962
|
+
steps.forEach((step, index) => {
|
|
963
|
+
const stepX = centerX - itemWidth / 2;
|
|
964
|
+
const stepY = startY + index * (itemHeight + gap);
|
|
965
|
+
const fillColor = step.color?.replace("#", "") ?? defaultColor;
|
|
966
|
+
const textColor = step.textColor?.replace("#", "") ?? defaultTextColor;
|
|
967
|
+
// 垂直方向では pentagon を使用(下向き矢印風)
|
|
968
|
+
const shapeType = index === 0 ? pptx.ShapeType.rect : pptx.ShapeType.pentagon;
|
|
969
|
+
slide.addText(step.label, {
|
|
970
|
+
x: pxToIn(stepX),
|
|
971
|
+
y: pxToIn(stepY),
|
|
972
|
+
w: pxToIn(itemWidth),
|
|
973
|
+
h: pxToIn(itemHeight),
|
|
974
|
+
shape: shapeType,
|
|
975
|
+
fill: { color: fillColor },
|
|
976
|
+
line: { type: "none" },
|
|
977
|
+
fontSize: pxToPt(node.fontPx ?? 14),
|
|
978
|
+
fontFace: "Noto Sans JP",
|
|
979
|
+
color: textColor,
|
|
980
|
+
bold: node.bold ?? false,
|
|
981
|
+
align: "center",
|
|
982
|
+
valign: "middle",
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
204
988
|
}
|
|
205
989
|
}
|
|
206
990
|
renderNode(data);
|