@things-factory/figure-ui 10.1.4 → 10.1.6

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.
Files changed (79) hide show
  1. package/client/graphql/index.ts +122 -15
  2. package/client/modeller/figure-ask.ts +5 -5
  3. package/client/modeller/figure-canvas.ts +1 -1
  4. package/client/modeller/figure-capabilities.ts +372 -0
  5. package/client/modeller/figure-history-panel.ts +477 -0
  6. package/client/modeller/figure-inspector.ts +392 -76
  7. package/client/modeller/figure-parts.ts +15 -15
  8. package/client/modeller/figure-preview.ts +2 -2
  9. package/client/modeller/figure-report.ts +102 -37
  10. package/client/modeller/figure-settings.ts +20 -20
  11. package/client/modeller/figure-side.ts +54 -8
  12. package/client/modeller/figure-source.test.ts +15 -1
  13. package/client/modeller/figure-source.ts +31 -15
  14. package/client/modeller/figure-thumbnail.ts +49 -4
  15. package/client/modeller/maturity.ts +10 -10
  16. package/client/pages/figure-list-page.ts +450 -227
  17. package/client/pages/figure-modeller-page.ts +282 -43
  18. package/client/types.ts +42 -0
  19. package/client/viewparts/figure-thumb.ts +172 -0
  20. package/dist-client/graphql/index.d.ts +31 -4
  21. package/dist-client/graphql/index.js +102 -12
  22. package/dist-client/graphql/index.js.map +1 -1
  23. package/dist-client/modeller/figure-ask.js +5 -5
  24. package/dist-client/modeller/figure-ask.js.map +1 -1
  25. package/dist-client/modeller/figure-canvas.js +1 -1
  26. package/dist-client/modeller/figure-canvas.js.map +1 -1
  27. package/dist-client/modeller/figure-capabilities.d.ts +42 -0
  28. package/dist-client/modeller/figure-capabilities.js +344 -0
  29. package/dist-client/modeller/figure-capabilities.js.map +1 -0
  30. package/dist-client/modeller/figure-history-panel.d.ts +56 -0
  31. package/dist-client/modeller/figure-history-panel.js +490 -0
  32. package/dist-client/modeller/figure-history-panel.js.map +1 -0
  33. package/dist-client/modeller/figure-inspector.d.ts +84 -0
  34. package/dist-client/modeller/figure-inspector.js +379 -75
  35. package/dist-client/modeller/figure-inspector.js.map +1 -1
  36. package/dist-client/modeller/figure-parts.js +15 -15
  37. package/dist-client/modeller/figure-parts.js.map +1 -1
  38. package/dist-client/modeller/figure-preview.js +2 -2
  39. package/dist-client/modeller/figure-preview.js.map +1 -1
  40. package/dist-client/modeller/figure-report.d.ts +18 -1
  41. package/dist-client/modeller/figure-report.js +105 -37
  42. package/dist-client/modeller/figure-report.js.map +1 -1
  43. package/dist-client/modeller/figure-settings.js +19 -19
  44. package/dist-client/modeller/figure-settings.js.map +1 -1
  45. package/dist-client/modeller/figure-side.d.ts +7 -0
  46. package/dist-client/modeller/figure-side.js +66 -7
  47. package/dist-client/modeller/figure-side.js.map +1 -1
  48. package/dist-client/modeller/figure-source.d.ts +15 -8
  49. package/dist-client/modeller/figure-source.js +16 -7
  50. package/dist-client/modeller/figure-source.js.map +1 -1
  51. package/dist-client/modeller/figure-thumbnail.js +40 -4
  52. package/dist-client/modeller/figure-thumbnail.js.map +1 -1
  53. package/dist-client/modeller/maturity.js +10 -10
  54. package/dist-client/modeller/maturity.js.map +1 -1
  55. package/dist-client/pages/figure-list-page.d.ts +45 -25
  56. package/dist-client/pages/figure-list-page.js +439 -247
  57. package/dist-client/pages/figure-list-page.js.map +1 -1
  58. package/dist-client/pages/figure-modeller-page.d.ts +54 -0
  59. package/dist-client/pages/figure-modeller-page.js +279 -43
  60. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  61. package/dist-client/tsconfig.tsbuildinfo +1 -1
  62. package/dist-client/types.d.ts +42 -0
  63. package/dist-client/types.js.map +1 -1
  64. package/dist-client/viewparts/figure-thumb.d.ts +29 -0
  65. package/dist-client/viewparts/figure-thumb.js +168 -0
  66. package/dist-client/viewparts/figure-thumb.js.map +1 -0
  67. package/dist-server/tsconfig.tsbuildinfo +1 -1
  68. package/package.json +9 -5
  69. package/test/i18n-prefix-guard.test.ts +129 -0
  70. package/test/korean-register-guard.test.ts +83 -0
  71. package/translations/en.json +307 -249
  72. package/translations/ja.json +275 -249
  73. package/translations/ko.json +307 -249
  74. package/translations/ms.json +275 -249
  75. package/translations/zh.json +275 -249
  76. package/client/viewparts/figure-card.ts +0 -226
  77. package/dist-client/viewparts/figure-card.d.ts +0 -24
  78. package/dist-client/viewparts/figure-card.js +0 -221
  79. package/dist-client/viewparts/figure-card.js.map +0 -1
@@ -324,49 +324,49 @@ export class FigureParts extends localize(i18next)(LitElement) {
324
324
 
325
325
  return html`
326
326
  <section>
327
- <h3>${i18next.t('label.shapes')}</h3>
327
+ <h3>${i18next.t('figure.label.shapes')}</h3>
328
328
  <div palette>
329
329
  ${PRIMITIVE_KINDS.map(
330
330
  kind => html`
331
331
  <button
332
332
  shape
333
333
  ?disabled=${full}
334
- title=${full ? i18next.t('text.part-limit-reached') : i18next.t('text.add-a-shape')}
334
+ title=${full ? i18next.t('figure.text.part-limit-reached') : i18next.t('figure.text.add-a-shape')}
335
335
  @click=${() => this.ask('add-part', { primitive: kind })}
336
336
  >
337
337
  <md-icon>${SHAPE_ICON[kind]}</md-icon>
338
- ${i18next.t('label.primitive-' + kind)}
338
+ ${i18next.t('figure.label.primitive-' + kind)}
339
339
  </button>
340
340
  `
341
341
  )}
342
342
  </div>
343
343
  ${full
344
- ? html`<p full>${i18next.t('text.part-limit-reached-short', { level, limit })}</p>`
344
+ ? html`<p full>${i18next.t('figure.text.part-limit-reached-short', { level, limit })}</p>`
345
345
  : ''}
346
346
  </section>
347
347
 
348
348
  <section list>
349
349
  <h3>
350
- ${i18next.t('label.parts')}
351
- <span room ?full=${full} title=${i18next.t('text.limit-comes-from-level')}>
352
- ${i18next.t('text.n-of-limit', { used: parts.length, limit })}
350
+ ${i18next.t('figure.label.parts')}
351
+ <span room ?full=${full} title=${i18next.t('figure.text.limit-comes-from-level')}>
352
+ ${i18next.t('figure.text.n-of-limit', { used: parts.length, limit })}
353
353
  </span>
354
354
  ${this.renderShowAll(parts)}
355
355
  </h3>
356
356
 
357
357
  <div level>
358
- <label for="detail-level">${i18next.t('label.detail-level')}</label>
358
+ <label for="detail-level">${i18next.t('figure.label.detail-level')}</label>
359
359
  <select
360
360
  id="detail-level"
361
- title=${i18next.t('text.detail-level-explained')}
361
+ title=${i18next.t('figure.text.detail-level-explained')}
362
362
  @change=${(e: Event) =>
363
363
  this.ask('change-detail-level', { level: (e.target as HTMLSelectElement).value })}
364
364
  >
365
365
  ${DETAIL_LEVELS.map(
366
366
  one => html`
367
367
  <option value=${one} ?selected=${one === level}>
368
- ${one} ${i18next.t('label.detail-' + one)} ·
369
- ${i18next.t('text.n-parts-at-most', { limit: PART_LIMIT[one] })}
368
+ ${one} ${i18next.t('figure.label.detail-' + one)} ·
369
+ ${i18next.t('figure.text.n-parts-at-most', { limit: PART_LIMIT[one] })}
370
370
  </option>
371
371
  `
372
372
  )}
@@ -374,7 +374,7 @@ export class FigureParts extends localize(i18next)(LitElement) {
374
374
  </div>
375
375
 
376
376
  ${parts.length === 0
377
- ? html`<p quiet>${i18next.t('text.pick-a-shape-above-to-start')}</p>`
377
+ ? html`<p quiet>${i18next.t('figure.text.pick-a-shape-above-to-start')}</p>`
378
378
  : html`
379
379
  <ol>
380
380
  ${parts.map((part, index) => this.renderRow(part, index, parts.length))}
@@ -403,7 +403,7 @@ export class FigureParts extends localize(i18next)(LitElement) {
403
403
  eye
404
404
  all
405
405
  ?off=${!anyHidden}
406
- title=${i18next.t(anyHidden ? 'text.show-all-parts' : 'text.hide-all-parts')}
406
+ title=${i18next.t(anyHidden ? 'figure.text.show-all-parts' : 'figure.text.hide-all-parts')}
407
407
  @click=${(e: Event) => {
408
408
  e.stopPropagation()
409
409
  this.ask('set-all-parts-hidden', { hidden: !anyHidden })
@@ -423,7 +423,7 @@ export class FigureParts extends localize(i18next)(LitElement) {
423
423
  <button
424
424
  eye
425
425
  ?off=${off}
426
- title=${i18next.t(off ? 'text.show-this-part' : 'text.hide-this-part')}
426
+ title=${i18next.t(off ? 'figure.text.show-this-part' : 'figure.text.hide-this-part')}
427
427
  @click=${(e: Event) => {
428
428
  // 눌렀을 때 줄 고르기가 함께 일어나지 않게 막는다 — 가리려던 것이지 고르려던 것이 아니다
429
429
  e.stopPropagation()
@@ -453,7 +453,7 @@ export class FigureParts extends localize(i18next)(LitElement) {
453
453
  <button
454
454
  op=${name}
455
455
  ?disabled=${!enabled}
456
- title=${i18next.t('button.' + name)}
456
+ title=${i18next.t('figure.button.' + name)}
457
457
  @click=${(e: Event) => {
458
458
  e.stopPropagation()
459
459
  if (enabled) this.ask(name, detail)
@@ -263,7 +263,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
263
263
  private standUp() {
264
264
  const source = this.source
265
265
  if (!source || source.parts.length === 0) {
266
- this.cannot = i18next.t('text.preview-needs-a-part')
266
+ this.cannot = i18next.t('figure.text.preview-needs-a-part')
267
267
  this.teardown()
268
268
  return
269
269
  }
@@ -273,7 +273,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
273
273
 
274
274
  const { errors } = validate(source)
275
275
  if (errors.length > 0) {
276
- this.cannot = i18next.t('text.preview-blocked-by-errors')
276
+ this.cannot = i18next.t('figure.text.preview-blocked-by-errors')
277
277
  this.teardown()
278
278
  return
279
279
  }
@@ -6,7 +6,7 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js'
6
6
 
7
7
  import { i18next, localize } from '@operato/i18n'
8
8
  import { ScrollbarStyles } from '@operato/styles'
9
- import { compile, costCurveSvg, costOf, scoreOf, validate } from '@hatiolab/figure-model'
9
+ import { compile, costCurveSvg, costOf, scoreOf, sizingReport, validate } from '@hatiolab/figure-model'
10
10
  import type { FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model'
11
11
 
12
12
  import { capacityOf } from './scene-budget.js'
@@ -440,6 +440,23 @@ export class FigureReport extends localize(i18next)(LitElement) {
440
440
  @state() private expanded = false
441
441
  @state() private errors: { code: string; message: string; at?: string }[] = []
442
442
  @state() private violations: { code: string; message: string }[] = []
443
+ /**
444
+ * 크기를 바꿔 봤을 때 갈라지는가 — **발행을 막는 것**이다.
445
+ *
446
+ * 이력 탭이 같은 판정을 서버에서 받아 발행 문을 잠그지만, 그것은 **저장된 정본**에 대한
447
+ * 것이라 저장하기 전에는 답하지 못한다. 여기서는 저작 중인 정본을 그대로 재므로, 부품을
448
+ * 옮기는 그 자리에서 갈라짐이 생겼는지 사라졌는지 보인다 — 저장하고 탭을 옮겨 확인하게
449
+ * 두면 고칠 때가 이미 지나 있다.
450
+ */
451
+ @state() private sizing: { code: string; message: string; at?: string; blocking: boolean }[] = []
452
+ /**
453
+ * 아직 부품이 하나도 없나 — **저작 전이지 형식을 어긴 것이 아니다.**
454
+ *
455
+ * 형식은 부품 0개를 오류로 본다(`missing-parts`). 발행 관점에서는 맞다. 그런데 새로 만들기를
456
+ * 누른 직후가 바로 그 상태이므로, 그것을 그대로 오류로 그리면 **아무것도 하지 않았는데 붉은
457
+ * 화면**을 보게 된다. 그래서 이 자리에서 둘을 가른다.
458
+ */
459
+ @state() private bare = false
443
460
 
444
461
  willUpdate(changed: Map<string, unknown>) {
445
462
  if (changed.has('source')) {
@@ -448,10 +465,17 @@ export class FigureReport extends localize(i18next)(LitElement) {
448
465
  }
449
466
 
450
467
  render() {
451
- if (!this.source) {
468
+ /*
469
+ 부품이 없으면 이 자리는 **판정이 아니라 안내**다. 이미 있던 「부품을 추가하면 나옵니다」가
470
+ 정확히 그 말이므로 그것을 쓴다 — 같은 상태에 문장을 둘 두지 않는다.
471
+
472
+ 아래 셈들(머리·축·한도·추이)은 부품이 있어야 뜻이 있고, 재지 않은 값을 들고 그리면 0 으로
473
+ 보인다. 0 은 「나쁘다」로 읽히므로 그 자리에 두지 않는다.
474
+ */
475
+ if (!this.source || this.bare) {
452
476
  return html`
453
477
  <section>
454
- <p quiet>${i18next.t('text.report-appears-when-you-add-a-part')}</p>
478
+ <p quiet>${i18next.t('figure.text.report-appears-when-you-add-a-part')}</p>
455
479
  </section>
456
480
  `
457
481
  }
@@ -484,34 +508,34 @@ export class FigureReport extends localize(i18next)(LitElement) {
484
508
 
485
509
  return html`
486
510
  <section>
487
- <h3>${i18next.t('label.maturity')}</h3>
488
- <p lead>${i18next.t('label.axis-scale')} — ${i18next.t('text.how-many-fit')}</p>
511
+ <h3>${i18next.t('figure.label.maturity')}</h3>
512
+ <p lead>${i18next.t('figure.label.axis-scale')} — ${i18next.t('figure.text.how-many-fit')}</p>
489
513
  <div verdict>
490
514
  <span capacity>${capacity.instances.toLocaleString()}</span>
491
- <span unit>${i18next.t('text.instances-unit')}</span>
515
+ <span unit>${i18next.t('figure.text.instances-unit')}</span>
492
516
  <span
493
517
  grade
494
518
  ?tight=${score.grade === 'C'}
495
519
  ?over=${score.grade === 'D'}
496
- title=${i18next.t('text.grade-explained')}
520
+ title=${i18next.t('figure.text.grade-explained')}
497
521
  >
498
522
  ${score.grade}
499
- <small>${i18next.t('text.headroom-percent', { percent: Math.round(score.score) })}</small>
523
+ <small>${i18next.t('figure.text.headroom-percent', { percent: Math.round(score.score) })}</small>
500
524
  </span>
501
525
  </div>
502
526
  <p because>
503
- ${i18next.t('text.grade-decided-by')}
504
- <strong>${i18next.t('label.budget-' + score.tightest.key)}</strong>
527
+ ${i18next.t('figure.text.grade-decided-by')}
528
+ <strong>${i18next.t('figure.label.budget-' + score.tightest.key)}</strong>
505
529
  — ${score.tightest.used} / ${score.tightest.limit}
506
530
  (${Math.round((score.tightest.used / score.tightest.limit) * 100)}%)
507
531
  </p>
508
532
  ${this.renderGradeScale(score.grade)}
509
533
  <p basis>
510
- ${i18next.t('text.bound-by-' + capacity.bound, {
534
+ ${i18next.t('figure.text.bound-by-' + capacity.bound, {
511
535
  per: capacity.per.toLocaleString(),
512
536
  budget: capacity.budget.toLocaleString()
513
537
  })}
514
- <em>${i18next.t('text.budget-not-measured-yet')}</em>
538
+ <em>${i18next.t('figure.text.budget-not-measured-yet')}</em>
515
539
  </p>
516
540
  </section>
517
541
  `
@@ -536,7 +560,7 @@ export class FigureReport extends localize(i18next)(LitElement) {
536
560
 
537
561
  return html`
538
562
  <p scale>
539
- <span>${i18next.t('text.grade-scale-lead')}</span>
563
+ <span>${i18next.t('figure.text.grade-scale-lead')}</span>
540
564
  ${bounds.map(
541
565
  ([grade, bound]) => html`<span ?now=${grade === now}><b>${grade}</b> ${bound}</span>`
542
566
  )}
@@ -568,7 +592,7 @@ export class FigureReport extends localize(i18next)(LitElement) {
568
592
 
569
593
  return html`
570
594
  <section>
571
- <h3>${i18next.t('label.by-axis')}</h3>
595
+ <h3>${i18next.t('figure.label.by-axis')}</h3>
572
596
  <div radar>${this.renderRadar(all.filter(axis => axis.ratio !== undefined))}</div>
573
597
  ${this.renderUnmeasured(all.filter(axis => axis.ratio === undefined))}
574
598
  <ul axes>
@@ -624,7 +648,7 @@ export class FigureReport extends localize(i18next)(LitElement) {
624
648
  const anchor = x < cx - 3 ? 'end' : x > cx + 3 ? 'start' : 'middle'
625
649
  return svg`
626
650
  <text x=${x.toFixed(1)} y=${(y + 1.8).toFixed(1)} text-anchor=${anchor}>
627
- ${i18next.t('label.axis-' + axis.key + '-short')}
651
+ ${i18next.t('figure.label.axis-' + axis.key + '-short')}
628
652
  </text>
629
653
  `
630
654
  })}
@@ -644,21 +668,21 @@ export class FigureReport extends localize(i18next)(LitElement) {
644
668
 
645
669
  return html`
646
670
  <p unmeasured>
647
- ${i18next.t('text.axes-not-measurable-yet', {
648
- axes: axes.map(axis => i18next.t('label.axis-' + axis.key)).join(' · ')
671
+ ${i18next.t('figure.text.axes-not-measurable-yet', {
672
+ axes: axes.map(axis => i18next.t('figure.label.axis-' + axis.key)).join(' · ')
649
673
  })}
650
674
  </p>
651
675
  `
652
676
  }
653
677
 
654
678
  private renderAxis(axis: MaturityAxis) {
655
- const name = i18next.t('label.axis-' + axis.key)
679
+ const name = i18next.t('figure.label.axis-' + axis.key)
656
680
 
657
681
  if (axis.blocked) {
658
682
  return html`
659
683
  <li blocked>
660
684
  <span>${name}</span>
661
- <span reading>${i18next.t('text.not-measurable-yet')}</span>
685
+ <span reading>${i18next.t('figure.text.not-measurable-yet')}</span>
662
686
  <em>${i18next.t(axis.blocked)}</em>
663
687
  </li>
664
688
  `
@@ -688,7 +712,7 @@ export class FigureReport extends localize(i18next)(LitElement) {
688
712
  * 그것을 그대로 실으면 다른 언어 사용자에게 한국어가 나온다.
689
713
  *
690
714
  * `budgets` 는 **배열**이다. `Object.entries` 로 돌면 키가 인덱스(0·1·2…)가 되어
691
- * `label.budget-0` 같은 없는 키를 찾는다 — 화면을 띄워 보고서야 드러났다.
715
+ * `figure.label.budget-0` 같은 없는 키를 찾는다 — 화면을 띄워 보고서야 드러났다.
692
716
  * 배열을 그대로 돌고 `use.key` 를 쓴다.
693
717
  *
694
718
  * 그래서 **안정된 `key`** 로 이 화면의 i18n 을 찾는다. `key` 는 열거형이라 형식의
@@ -701,14 +725,14 @@ export class FigureReport extends localize(i18next)(LitElement) {
701
725
 
702
726
  return html`
703
727
  <section>
704
- <h3>${i18next.t('label.budgets')}</h3>
728
+ <h3>${i18next.t('figure.label.budgets')}</h3>
705
729
  <ul budgets>
706
730
  ${score.budgets.map(use => {
707
731
  const ratio = Math.min(1, use.ratio)
708
732
  const tight = use.key === score.tightest.key
709
733
  return html`
710
734
  <li>
711
- <span>${i18next.t('label.budget-' + use.key)}</span>
735
+ <span>${i18next.t('figure.label.budget-' + use.key)}</span>
712
736
  <span>${use.used} / ${use.limit}</span>
713
737
  <div bar><i ?tight=${tight} style="width:${Math.round(ratio * 100)}%"></i></div>
714
738
  </li>
@@ -725,21 +749,21 @@ export class FigureReport extends localize(i18next)(LitElement) {
725
749
 
726
750
  return html`
727
751
  <section>
728
- <h3>${i18next.t('label.weight')}</h3>
752
+ <h3>${i18next.t('figure.label.weight')}</h3>
729
753
  <dl metrics>
730
- <dt>${i18next.t('label.triangles')}</dt>
754
+ <dt>${i18next.t('figure.label.triangles')}</dt>
731
755
  <dd>${cost.triangles.toLocaleString()}</dd>
732
756
 
733
- <dt>${i18next.t('label.triangles-at-100')}</dt>
757
+ <dt>${i18next.t('figure.label.triangles-at-100')}</dt>
734
758
  <dd>${cost.at.triangles.toLocaleString()}</dd>
735
759
 
736
- <dt>${i18next.t('label.draw-calls-at-100')}</dt>
760
+ <dt>${i18next.t('figure.label.draw-calls-at-100')}</dt>
737
761
  <dd>${cost.at.drawCalls.toLocaleString()}</dd>
738
762
 
739
- <dt>${i18next.t('label.distinct-shapes')}</dt>
763
+ <dt>${i18next.t('figure.label.distinct-shapes')}</dt>
740
764
  <dd>${cost.distinctShapes}</dd>
741
765
 
742
- <dt>${i18next.t('label.distinct-materials')}</dt>
766
+ <dt>${i18next.t('figure.label.distinct-materials')}</dt>
743
767
  <dd>${cost.distinctMaterials}</dd>
744
768
  </dl>
745
769
  </section>
@@ -762,14 +786,14 @@ export class FigureReport extends localize(i18next)(LitElement) {
762
786
 
763
787
  return html`
764
788
  <section>
765
- <h3>${i18next.t('label.reuse-trend')}</h3>
789
+ <h3>${i18next.t('figure.label.reuse-trend')}</h3>
766
790
  ${many
767
791
  ? html`
768
792
  <dl metrics>
769
- <dt>${i18next.t('label.triangles-at-10k')}</dt>
793
+ <dt>${i18next.t('figure.label.triangles-at-10k')}</dt>
770
794
  <dd>${many.at.triangles.toLocaleString()}</dd>
771
795
 
772
- <dt>${i18next.t('label.draw-calls-at-10k')}</dt>
796
+ <dt>${i18next.t('figure.label.draw-calls-at-10k')}</dt>
773
797
  <dd>${many.at.drawCalls.toLocaleString()}</dd>
774
798
  </dl>
775
799
  `
@@ -777,7 +801,7 @@ export class FigureReport extends localize(i18next)(LitElement) {
777
801
  ${this.curve
778
802
  ? html`
779
803
  <button expand @click=${() => (this.expanded = true)}>
780
- ${i18next.t('button.see-the-trend-chart')}
804
+ ${i18next.t('figure.button.see-the-trend-chart')}
781
805
  </button>
782
806
  `
783
807
  : nothing}
@@ -794,7 +818,7 @@ export class FigureReport extends localize(i18next)(LitElement) {
794
818
  <figure @click=${(e: Event) => e.stopPropagation()}>
795
819
  ${unsafeSVG(this.curve)}
796
820
  <figcaption>
797
- <button close @click=${() => (this.expanded = false)}>${i18next.t('button.close')}</button>
821
+ <button close @click=${() => (this.expanded = false)}>${i18next.t('figure.button.close')}</button>
798
822
  </figcaption>
799
823
  </figure>
800
824
  </div>
@@ -809,19 +833,27 @@ export class FigureReport extends localize(i18next)(LitElement) {
809
833
  * 검사가 돌았는지 알 수 없다.
810
834
  */
811
835
  private renderFindings() {
812
- if (this.errors.length === 0 && this.violations.length === 0) {
836
+ if (this.errors.length === 0 && this.violations.length === 0 && this.sizing.length === 0) {
813
837
  return html`
814
838
  <section>
815
- <h3>${i18next.t('label.findings')}</h3>
816
- <p quiet>${i18next.t('text.no-findings')}</p>
839
+ <h3>${i18next.t('figure.label.findings')}</h3>
840
+ <p quiet>${i18next.t('figure.text.no-findings')}</p>
817
841
  </section>
818
842
  `
819
843
  }
820
844
 
821
845
  return html`
822
846
  <section>
823
- <h3>${i18next.t('label.findings')}</h3>
847
+ <h3>${i18next.t('figure.label.findings')}</h3>
824
848
  <ul findings>
849
+ ${this.sizing.map(
850
+ finding => html`
851
+ <li ?violation=${!finding.blocking}>
852
+ <md-icon>call_split</md-icon>
853
+ <span>${finding.message} ${finding.at ? html`<code at>${finding.at}</code>` : ''}</span>
854
+ </li>
855
+ `
856
+ )}
825
857
  ${this.errors.map(
826
858
  e => html`
827
859
  <li>
@@ -854,6 +886,24 @@ export class FigureReport extends localize(i18next)(LitElement) {
854
886
  this.curve = ''
855
887
  this.errors = []
856
888
  this.violations = []
889
+ this.sizing = []
890
+ return
891
+ }
892
+
893
+ /*
894
+ 부품이 없으면 판정을 아예 하지 않는다 — 잴 것이 없다. 「무엇을 고쳐야 하나」가 아니라
895
+ 「무엇부터 놓나」인 상태이고, 그 안내는 부품 목록이 이미 하고 있다.
896
+ */
897
+ this.bare = !source.parts?.length
898
+ if (this.bare) {
899
+ this.cost = undefined
900
+ this.score = undefined
901
+ this.many = undefined
902
+ this.capacity = undefined
903
+ this.curve = ''
904
+ this.errors = []
905
+ this.violations = []
906
+ this.sizing = []
857
907
  return
858
908
  }
859
909
 
@@ -868,6 +918,7 @@ export class FigureReport extends localize(i18next)(LitElement) {
868
918
  this.many = undefined
869
919
  this.capacity = undefined
870
920
  this.curve = ''
921
+ this.sizing = []
871
922
  this.dispatchEvent(
872
923
  new CustomEvent('verdict-changed', { detail: {}, bubbles: true, composed: true })
873
924
  )
@@ -875,6 +926,20 @@ export class FigureReport extends localize(i18next)(LitElement) {
875
926
  }
876
927
 
877
928
  const blueprint = compile(source)
929
+
930
+ /*
931
+ 크기를 바꿔 보는 일곱 조합. 판정 규칙은 `figure-model` 의 `gate.ts` 가 정한 것을 그대로
932
+ 쓴다 — 무엇이 막는 것인지 화면이 다시 정하면 서버와 다른 말을 하게 된다.
933
+ */
934
+ const report = sizingReport(blueprint)
935
+ this.sizing = report.findings.map(finding => ({
936
+ code: finding.code,
937
+ message: finding.message,
938
+ /* 숫자가 아니라 자리를 말한다 — 갈라져 나온 부품 이름이 고칠 자리다. */
939
+ at: finding.clumps.map(clump => [...clump].sort().join('+')).join(' | ') || undefined,
940
+ blocking: finding.code === 'sizing-splits'
941
+ }))
942
+
878
943
  this.cost = costOf(blueprint, 100)
879
944
  // 1만 개는 figure-model 에 다시 묻는다 — 화면이 곱셈으로 짐작하지 않는다.
880
945
  this.many = costOf(blueprint, 10000)
@@ -159,34 +159,34 @@ export class FigureSettings extends localize(i18next)(LitElement) {
159
159
 
160
160
  return html`
161
161
  <section>
162
- <h3>${i18next.t('label.guides')}</h3>
163
- <p quiet>${i18next.t('text.view-settings-are-not-saved')}</p>
162
+ <h3>${i18next.t('figure.label.guides')}</h3>
163
+ <p quiet>${i18next.t('figure.text.view-settings-are-not-saved')}</p>
164
164
 
165
- ${this.toggle('grid', view.grid, i18next.t('label.coordinate-ground'), i18next.t('text.coordinate-ground-explained'))}
166
- ${this.toggle('gridLabels', view.gridLabels, i18next.t('label.axis-labels'))}
167
- ${this.toggle('floor', view.floor, i18next.t('label.floor'), i18next.t('text.floor-explained'))}
165
+ ${this.toggle('grid', view.grid, i18next.t('figure.label.coordinate-ground'), i18next.t('figure.text.coordinate-ground-explained'))}
166
+ ${this.toggle('gridLabels', view.gridLabels, i18next.t('figure.label.axis-labels'))}
167
+ ${this.toggle('floor', view.floor, i18next.t('figure.label.scene-floor'), i18next.t('figure.text.floor-explained'))}
168
168
  </section>
169
169
 
170
170
  <section>
171
- <h3>${i18next.t('label.environment')}</h3>
171
+ <h3>${i18next.t('figure.label.environment')}</h3>
172
172
 
173
173
  <label field>
174
- <span>${i18next.t('label.preset')}</span>
174
+ <span>${i18next.t('figure.label.preset')}</span>
175
175
  <select
176
176
  .value=${view.environment}
177
177
  @change=${(e: Event) => this.put({ environment: (e.target as HTMLSelectElement).value as EnvironmentName })}
178
178
  >
179
179
  ${ENVIRONMENTS.map(
180
180
  name => html`<option value=${name} ?selected=${view.environment === name}>
181
- ${i18next.t(`label.environment-${name}`)}
181
+ ${i18next.t(`figure.label.environment-${name}`)}
182
182
  </option>`
183
183
  )}
184
184
  </select>
185
185
  </label>
186
- <p quiet>${i18next.t('text.environment-explained')}</p>
186
+ <p quiet>${i18next.t('figure.text.environment-explained')}</p>
187
187
 
188
188
  <label field>
189
- <span>${i18next.t('label.sky-color')}</span>
189
+ <span>${i18next.t('figure.label.sky-color')}</span>
190
190
  <input
191
191
  type="color"
192
192
  .value=${view.skyColor}
@@ -196,30 +196,30 @@ export class FigureSettings extends localize(i18next)(LitElement) {
196
196
  </section>
197
197
 
198
198
  <section>
199
- <h3>${i18next.t('label.key-light')}</h3>
199
+ <h3>${i18next.t('figure.label.key-light')}</h3>
200
200
 
201
- ${this.slider('dirLightIntensity', view.dirLightIntensity, i18next.t('label.intensity'), 0, 5, 0.1)}
201
+ ${this.slider('dirLightIntensity', view.dirLightIntensity, i18next.t('figure.label.intensity'), 0, 5, 0.1)}
202
202
  ${this.toggle(
203
203
  'dirLightFollowCamera',
204
204
  view.dirLightFollowCamera,
205
- i18next.t('label.follow-camera'),
206
- i18next.t('text.follow-camera-explained')
205
+ i18next.t('figure.label.follow-camera'),
206
+ i18next.t('figure.text.follow-camera-explained')
207
207
  )}
208
- ${this.toggle('dirShadowEnabled', view.dirShadowEnabled, i18next.t('label.cast-shadow'))}
208
+ ${this.toggle('dirShadowEnabled', view.dirShadowEnabled, i18next.t('figure.label.cast-shadow'))}
209
209
  ${view.dirLightIntensity === 0 && view.hemiIntensity === 0
210
- ? html`<p quiet>${i18next.t('text.all-lights-off')}</p>`
210
+ ? html`<p quiet>${i18next.t('figure.text.all-lights-off')}</p>`
211
211
  : ''}
212
212
  </section>
213
213
 
214
214
  <section>
215
- <h3>${i18next.t('label.hemisphere-light')}</h3>
215
+ <h3>${i18next.t('figure.label.hemisphere-light')}</h3>
216
216
 
217
- ${this.slider('hemiIntensity', view.hemiIntensity, i18next.t('label.intensity'), 0, 8, 0.1)}
218
- <p quiet>${i18next.t('text.hemisphere-light-explained')}</p>
217
+ ${this.slider('hemiIntensity', view.hemiIntensity, i18next.t('figure.label.intensity'), 0, 8, 0.1)}
218
+ <p quiet>${i18next.t('figure.text.hemisphere-light-explained')}</p>
219
219
  </section>
220
220
 
221
221
  <section>
222
- <button reset @click=${() => this.reset()}>${i18next.t('button.reset-view')}</button>
222
+ <button reset @click=${() => this.reset()}>${i18next.t('figure.button.reset-view')}</button>
223
223
  </section>
224
224
  `
225
225
  }
@@ -1,6 +1,8 @@
1
1
  import './figure-inspector.js'
2
+ import './figure-capabilities.js'
2
3
  import './figure-report.js'
3
4
  import './figure-settings.js'
5
+ import './figure-history-panel.js'
4
6
 
5
7
  import { css, html, LitElement } from 'lit'
6
8
  import { customElement, property, state } from 'lit/decorators.js'
@@ -12,7 +14,7 @@ import type { BoardModel, PartModel } from './figure-source.js'
12
14
  import { DEFAULT_VIEW } from './figure-view.js'
13
15
  import type { ViewSettings } from './figure-view.js'
14
16
 
15
- type Tab = 'properties' | 'verdict' | 'view'
17
+ type Tab = 'properties' | 'capability' | 'verdict' | 'view' | 'history'
16
18
 
17
19
  /**
18
20
  * 저작면 오른쪽 칸 — 속성과 판정을 탭으로 겹쳐 둔다.
@@ -64,10 +66,19 @@ export class FigureSide extends localize(i18next)(LitElement) {
64
66
  }
65
67
  button[tab] {
66
68
  flex: 1;
69
+ /*
70
+ **탭 이름은 한 줄이다.** 배지를 이름 옆에 나란히 두었더니 넷이 좁은 칸을 나눠 쓰면서
71
+ 이름이 접혀 「성숙 / 도」가 되었다. 이름이 접히면 그 줄이 두 줄이 되고 네 탭의 높이가
72
+ 서로 달라진다.
73
+
74
+ 그래서 배지는 자리를 차지하지 않고 **이름 위에 겹친다**(아래 span[badge]). 개수는
75
+ 곁들이는 것이고 이름이 먼저다.
76
+ */
77
+ position: relative;
67
78
  display: flex;
68
79
  align-items: center;
69
80
  justify-content: center;
70
- gap: 6px;
81
+ white-space: nowrap;
71
82
  padding: 8px 6px;
72
83
  font: var(--label-font, inherit);
73
84
  font-size: 0.76rem;
@@ -87,14 +98,25 @@ export class FigureSide extends localize(i18next)(LitElement) {
87
98
  border-bottom-color: var(--md-sys-color-primary);
88
99
  }
89
100
 
90
- /* 판정을 덮어 두어도 개수는 여기 남는다 */
101
+ /*
102
+ 판정을 덮어 두어도 개수는 여기 남는다.
103
+
104
+ 자리를 먹지 않게 오른쪽 위에 겹쳐 둔다 — 탭 이름을 밀지 않으므로 이름이 접히지 않는다.
105
+ 숫자가 커져도(1,200 같은 것) 이름 위로 넘칠 뿐 줄을 바꾸지 않는다.
106
+ */
91
107
  span[badge] {
92
- padding: 1px 7px;
93
- font-size: 0.72rem;
108
+ position: absolute;
109
+ /* 이름 위에 겹치되 위로 붙지 않게 — 탭 가운데 글자와 눈높이를 조금 맞춘다 */
110
+ top: 5px;
111
+ right: 2px;
112
+ padding: 0 5px;
113
+ font-size: 0.62rem;
94
114
  font-weight: 700;
115
+ line-height: 1.5;
95
116
  color: var(--md-sys-color-on-primary-container);
96
117
  background-color: var(--md-sys-color-primary-container);
97
118
  border-radius: 999em;
119
+ pointer-events: none;
98
120
  }
99
121
 
100
122
  figure-inspector,
@@ -117,6 +139,11 @@ export class FigureSide extends localize(i18next)(LitElement) {
117
139
  곳이라 변환된 FigureSource 를 받는다. 변환은 모델러 페이지가 한 번만 해서 내려보낸다.
118
140
  */
119
141
  @property({ type: String }) figureName = ''
142
+ /** 이력 탭이 발행하고 되돌리는 데 필요한 것들. 정본은 저작면이 든다. */
143
+ @property({ type: String }) figureId?: string
144
+ @property({ type: String }) figureState?: 'draft' | 'released'
145
+ @property({ type: Number }) figureVersion = 0
146
+ @property({ type: Boolean }) dirty = false
120
147
 
121
148
  @property({ type: Object }) board?: BoardModel
122
149
  @property({ type: Array }) parts: PartModel[] = []
@@ -132,9 +159,11 @@ export class FigureSide extends localize(i18next)(LitElement) {
132
159
  render() {
133
160
  return html`
134
161
  <div tabs>
135
- ${this.renderTab('properties', i18next.t('label.properties'))}
136
- ${this.renderTab('verdict', i18next.t('label.maturity'), this.instances)}
137
- ${this.renderTab('view', i18next.t('label.view'))}
162
+ ${this.renderTab('properties', i18next.t('figure.label.properties'))}
163
+ ${this.renderTab('capability', i18next.t('figure.label.tab-capability'))}
164
+ ${this.renderTab('verdict', i18next.t('figure.label.maturity'), this.instances)}
165
+ ${this.renderTab('view', i18next.t('figure.label.tab-environment'))}
166
+ ${this.renderTab('history', i18next.t('figure.label.history'))}
138
167
  </div>
139
168
 
140
169
  <figure-inspector
@@ -145,6 +174,12 @@ export class FigureSide extends localize(i18next)(LitElement) {
145
174
  .selected=${this.selected}
146
175
  ></figure-inspector>
147
176
 
177
+ <figure-capabilities
178
+ ?off=${this.tab !== 'capability'}
179
+ .board=${this.board}
180
+ .parts=${this.parts}
181
+ ></figure-capabilities>
182
+
148
183
  <figure-report
149
184
  ?off=${this.tab !== 'verdict'}
150
185
  .source=${this.source}
@@ -152,6 +187,17 @@ export class FigureSide extends localize(i18next)(LitElement) {
152
187
  ></figure-report>
153
188
 
154
189
  <figure-settings ?off=${this.tab !== 'view'} .view=${this.view}></figure-settings>
190
+
191
+ <!--
192
+ 이력은 감춰도 DOM 에 남긴다 — 다른 탭들과 같은 규칙이다. 떼었다 붙이면 판본을 다시 부른다.
193
+ -->
194
+ <figure-history-panel
195
+ ?off=${this.tab !== 'history'}
196
+ .figureId=${this.figureId}
197
+ .figureState=${this.figureState}
198
+ .figureVersion=${this.figureVersion}
199
+ .dirty=${this.dirty}
200
+ ></figure-history-panel>
155
201
  `
156
202
  }
157
203