@things-factory/figure-ui 10.1.5 → 10.1.7

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 +36 -3
  2. package/client/modeller/figure-animations.ts +712 -0
  3. package/client/modeller/figure-ask.ts +5 -5
  4. package/client/modeller/figure-canvas.ts +1 -1
  5. package/client/modeller/figure-capabilities.ts +372 -0
  6. package/client/modeller/figure-history-panel.ts +171 -18
  7. package/client/modeller/figure-inspector.ts +439 -76
  8. package/client/modeller/figure-parts.ts +15 -15
  9. package/client/modeller/figure-preview.ts +2 -2
  10. package/client/modeller/figure-report.ts +102 -37
  11. package/client/modeller/figure-settings.ts +20 -20
  12. package/client/modeller/figure-side.ts +91 -15
  13. package/client/modeller/figure-source.test.ts +15 -1
  14. package/client/modeller/figure-source.ts +31 -15
  15. package/client/modeller/figure-thumbnail.ts +49 -4
  16. package/client/modeller/maturity.ts +10 -10
  17. package/client/pages/figure-list-page.ts +204 -66
  18. package/client/pages/figure-modeller-page.ts +158 -42
  19. package/client/types.ts +31 -0
  20. package/client/viewparts/figure-thumb.ts +250 -0
  21. package/dist-client/graphql/index.d.ts +8 -1
  22. package/dist-client/graphql/index.js +33 -3
  23. package/dist-client/graphql/index.js.map +1 -1
  24. package/dist-client/modeller/figure-animations.d.ts +93 -0
  25. package/dist-client/modeller/figure-animations.js +655 -0
  26. package/dist-client/modeller/figure-animations.js.map +1 -0
  27. package/dist-client/modeller/figure-ask.js +5 -5
  28. package/dist-client/modeller/figure-ask.js.map +1 -1
  29. package/dist-client/modeller/figure-canvas.js +1 -1
  30. package/dist-client/modeller/figure-canvas.js.map +1 -1
  31. package/dist-client/modeller/figure-capabilities.d.ts +42 -0
  32. package/dist-client/modeller/figure-capabilities.js +344 -0
  33. package/dist-client/modeller/figure-capabilities.js.map +1 -0
  34. package/dist-client/modeller/figure-history-panel.d.ts +15 -1
  35. package/dist-client/modeller/figure-history-panel.js +165 -18
  36. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  37. package/dist-client/modeller/figure-inspector.d.ts +98 -0
  38. package/dist-client/modeller/figure-inspector.js +425 -75
  39. package/dist-client/modeller/figure-inspector.js.map +1 -1
  40. package/dist-client/modeller/figure-parts.js +15 -15
  41. package/dist-client/modeller/figure-parts.js.map +1 -1
  42. package/dist-client/modeller/figure-preview.js +2 -2
  43. package/dist-client/modeller/figure-preview.js.map +1 -1
  44. package/dist-client/modeller/figure-report.d.ts +18 -1
  45. package/dist-client/modeller/figure-report.js +105 -37
  46. package/dist-client/modeller/figure-report.js.map +1 -1
  47. package/dist-client/modeller/figure-settings.js +19 -19
  48. package/dist-client/modeller/figure-settings.js.map +1 -1
  49. package/dist-client/modeller/figure-side.d.ts +24 -0
  50. package/dist-client/modeller/figure-side.js +94 -14
  51. package/dist-client/modeller/figure-side.js.map +1 -1
  52. package/dist-client/modeller/figure-source.d.ts +15 -8
  53. package/dist-client/modeller/figure-source.js +16 -7
  54. package/dist-client/modeller/figure-source.js.map +1 -1
  55. package/dist-client/modeller/figure-thumbnail.js +40 -4
  56. package/dist-client/modeller/figure-thumbnail.js.map +1 -1
  57. package/dist-client/modeller/maturity.js +10 -10
  58. package/dist-client/modeller/maturity.js.map +1 -1
  59. package/dist-client/pages/figure-list-page.d.ts +11 -4
  60. package/dist-client/pages/figure-list-page.js +201 -63
  61. package/dist-client/pages/figure-list-page.js.map +1 -1
  62. package/dist-client/pages/figure-modeller-page.d.ts +33 -0
  63. package/dist-client/pages/figure-modeller-page.js +156 -42
  64. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  65. package/dist-client/tsconfig.tsbuildinfo +1 -1
  66. package/dist-client/types.d.ts +29 -0
  67. package/dist-client/types.js.map +1 -1
  68. package/dist-client/viewparts/figure-thumb.d.ts +29 -0
  69. package/dist-client/viewparts/figure-thumb.js +242 -0
  70. package/dist-client/viewparts/figure-thumb.js.map +1 -0
  71. package/dist-server/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +6 -5
  73. package/test/i18n-prefix-guard.test.ts +171 -0
  74. package/test/korean-register-guard.test.ts +83 -0
  75. package/translations/en.json +351 -264
  76. package/translations/ja.json +276 -264
  77. package/translations/ko.json +351 -264
  78. package/translations/ms.json +276 -264
  79. package/translations/zh.json +276 -264
@@ -1,10 +1,12 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
2
  import '@material/web/icon/icon.js';
3
3
  import { css, html, LitElement, nothing, svg } from 'lit';
4
- import { customElement, property } from 'lit/decorators.js';
4
+ import { customElement, property, state } from 'lit/decorators.js';
5
+ import { live } from 'lit/directives/live.js';
5
6
  import { i18next, localize } from '@operato/i18n';
6
7
  import { ScrollbarStyles } from '@operato/styles';
7
8
  import { EXTRUDE_PRIMITIVES, MATERIAL_PRESETS, PRIMITIVE_KINDS, SEGMENTED_PRIMITIVES, SEGMENT_PRESETS, SIZING_RULES, trianglesOf } from '@hatiolab/figure-model';
9
+ import { ANCHOR_RULES, PART_ROLES, REPEAT_LIMIT, anchorOfPart, repeatPlanOfPart } from '@hatiolab/figure-model';
8
10
  import { activePalette } from '@hatiolab/things-scene';
9
11
  import { partToFigure, partFromFigure } from './figure-source.js';
10
12
  import { canMirror } from './part-edits.js';
@@ -54,6 +56,8 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
54
56
  /** 보드. 위치를 보드 중심 기준으로 바꾸려면 이것이 있어야 한다. */
55
57
  /** 도형의 표시 이름. 정본은 저작면이 들고, 여기서는 보여 주고 바꾼다(`figure-rename` 으로 알린다). */
56
58
  this.figureName = '';
59
+ /** 펴 둔 도움말. 부품을 바꿔도 유지된다 — 매번 다시 펴게 하지 않는다. */
60
+ this.opened = new Set();
57
61
  /** 보드 위의 부품들. 씬 모델 그대로다. */
58
62
  this.parts = [];
59
63
  this.selected = -1;
@@ -82,12 +86,21 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
82
86
  font-size: 0.76rem;
83
87
  }
84
88
 
89
+ /*
90
+ ── 자리를 아끼는 판 ──────────────────────────────────────────────────────
91
+ 속성은 앞으로 더 늘어난다(능력 · 포트 · 애니메이션 · 기준정보). 지금 여백을 그대로 두면
92
+ 그것들이 들어올 자리가 없다. 절 여백을 6/10 으로, 제목 아래를 4px 로 줄였다 — 글자 크기와
93
+ 칸 높이는 건드리지 않는다(작게 만드는 것이 아니라 **빈 곳을 줄이는 것**이다).
94
+ */
85
95
  section {
86
- padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
96
+ padding: 6px 10px;
87
97
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
88
98
  }
89
99
  h3 {
90
- margin: 0 0 6px 0;
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 4px;
103
+ margin: 0 0 4px 0;
91
104
  font-size: var(--ui-t-micro, 11px);
92
105
  font-weight: 700;
93
106
  letter-spacing: 0.04em;
@@ -96,12 +109,47 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
96
109
  }
97
110
 
98
111
  label {
99
- display: block;
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 3px;
100
115
  margin-bottom: 2px;
101
116
  font-size: 0.68rem;
102
117
  color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
103
118
  }
104
119
 
120
+ /*
121
+ 도움말 물음표. **글자 줄을 하나 먹지 않는다** — 라벨·제목 줄의 남는 폭에 얹힌다.
122
+ 아이콘만 두고 테두리를 안 두는 이유는 이것이 누를 것이라기보다 「더 있다」는 표시라서다.
123
+ */
124
+ button[help] {
125
+ display: inline-flex;
126
+ align-items: center;
127
+ padding: 0;
128
+ margin: 0;
129
+ color: var(--md-sys-color-outline);
130
+ background: none;
131
+ border: none;
132
+ cursor: pointer;
133
+ }
134
+ button[help] md-icon {
135
+ --md-icon-size: 13px;
136
+ font-size: 13px;
137
+ }
138
+ button[help]:hover,
139
+ button[help][aria-expanded='true'] {
140
+ color: var(--md-sys-color-primary);
141
+ }
142
+
143
+ /* 편 도움말. 라벨 아래 한 칸 들여 붙인다 — 무엇에 딸린 말인지 보이게. */
144
+ p[hint] {
145
+ margin: 2px 0 4px 0;
146
+ padding-left: 6px;
147
+ font-size: 0.68rem;
148
+ line-height: 1.5;
149
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
150
+ border-left: 2px solid var(--md-sys-color-outline-variant);
151
+ }
152
+
105
153
  input,
106
154
  select {
107
155
  width: 100%;
@@ -169,7 +217,8 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
169
217
  div[field] + div[field],
170
218
  div[triple] + div[field],
171
219
  div[field] + div[triple] {
172
- margin-top: 7px;
220
+ /* 칸 사이 7 → 5. 칸 자체는 그대로 두고 사이만 좁힌다. */
221
+ margin-top: 5px;
173
222
  }
174
223
 
175
224
  /*
@@ -484,18 +533,126 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
484
533
  return html `
485
534
  ${this.renderFigure()}
486
535
  <section>
487
- <h3>${i18next.t('label.properties')}</h3>
488
- <p quiet>${i18next.t('text.select-a-part-to-edit')}</p>
536
+ <h3>${i18next.t('figure.label.properties')}</h3>
537
+ <p quiet>${i18next.t('figure.text.select-a-part-to-edit')}</p>
489
538
  </section>
490
539
  `;
491
540
  }
492
541
  // 여기부터는 사용자 용어다 — X·Y·Z, 위치는 부품 중심, 회전은 도(degree).
493
542
  const shown = partToFigure(part, this.board);
543
+ /*
544
+ 역할을 먼저 묻는다.
545
+
546
+ 자리(slot)로 지정한 부품은 **그려지지 않는다.** 그 사실을 모른 채 재질부터 고르면 안 쓰일
547
+ 색을 고르게 된다. 그리고 자리는 도형이 능력을 질 때만 뜻이 있으므로, 능력이 없으면 이
548
+ 묶음은 아예 나오지 않는다 — 해 둬도 아무 일도 안 일어나는 칸을 보여 주지 않는다.
549
+ */
550
+ return html `
551
+ ${this.renderFigure()} ${this.renderIdentity(shown)} ${this.renderRole(shown)}
552
+ ${this.renderPlacement(shown)} ${this.renderShape(shown)} ${this.renderMaterial(shown)}
553
+ ${this.renderBehaviour(shown)}
554
+ `;
555
+ }
556
+ /**
557
+ * ── 이 부품이 능력에서 맡는 자리 ────────────────────────────────────────────
558
+ *
559
+ * 「능력」 탭이 도형 전체가 무엇을 하는지 정하고, 어느 부품이 그것을 받쳐 주는지는 여기서
560
+ * 정한다. **부품을 고치는 곳이 한 군데여야** 하기 때문이다 — 목록을 따로 두면 같은 부품을
561
+ * 두 화면에서 고치게 되고, 저작자는 어느 쪽이 이기는지 매번 생각해야 한다.
562
+ *
563
+ * 칸 수는 `repeat` 가 있으면 받지 않는다. 반복하는 자리의 칸 수는 **인스턴스 크기**가 정한다 —
564
+ * 컨베이어가 길어지면 실을 자리가 늘어나는 것이 그것이고, 여기 적은 값은 안 쓰인다.
565
+ */
566
+ renderRole(part) {
567
+ if (!this.board?.capabilities?.length)
568
+ return nothing;
569
+ const roles = part.capability?.roles ?? [];
570
+ const repeats = part.sizing === 'repeat' && !!part.repeat;
494
571
  return html `
495
- ${this.renderFigure()} ${this.renderIdentity(shown)} ${this.renderPlacement(shown)}
496
- ${this.renderShape(shown)} ${this.renderMaterial(shown)} ${this.renderBehaviour(shown)}
572
+ <section>
573
+ <h3>${i18next.t('figure.label.role-in-capability')}${this.help('figure.text.anchors-are-not-drawn')}</h3>
574
+
575
+ ${PART_ROLES.map(one => html `
576
+ <label toggle>
577
+ <input
578
+ type="checkbox"
579
+ .checked=${live(roles.includes(one))}
580
+ @change=${(e) => this.toggleRole(one, e.target.checked)}
581
+ />
582
+ ${i18next.t('figure.label.role-' + one)}
583
+ </label>
584
+ `)}
585
+ ${roles.length === 0 ? html `<p quiet>${i18next.t('figure.text.role-none-means-drawn')}</p>` : nothing}
586
+
587
+ ${roles.includes('slot')
588
+ ? html `
589
+ <div field>
590
+ <label>${i18next.t('figure.label.accepts')}${this.help('figure.text.accepts-explained')}</label>
591
+ <input
592
+ .value=${(part.capability?.accepts ?? []).join(', ')}
593
+ placeholder=${i18next.t('figure.text.accepts-anything')}
594
+ @change=${(e) => this.setAccepts(e.target.value)}
595
+ />
596
+ </div>
597
+ <div field>
598
+ <label>${i18next.t('figure.label.slot-capacity')}</label>
599
+ ${repeats
600
+ ? html `<p quiet>${i18next.t('figure.text.capacity-from-repeat')}</p>`
601
+ : this.number('', part.capability?.capacity ?? 1, value => this.setCapacity(value), 1)}
602
+ </div>
603
+ `
604
+ : nothing}
605
+ </section>
497
606
  `;
498
607
  }
608
+ /**
609
+ * 역할 하나를 켜고 끈다.
610
+ *
611
+ * **여럿을 함께 맡을 수 있다.** AGV 의 상판은 물건을 놓는 자리이면서 그 자리로 물건이
612
+ * 들어오고 나간다. 그래서 하나를 고르는 선택 상자가 아니라 확인 상자다.
613
+ *
614
+ * 마지막 역할을 끄면 `capability` 를 통째로 지운다. 빈 배열을 남기면 형식이 오류로 보고,
615
+ * 저작자가 보기에는 아무 역할도 없는 평범한 부품인데 발행이 막힌다.
616
+ */
617
+ toggleRole(role, on) {
618
+ this.change(part => {
619
+ const roles = (part.capability?.roles ?? []).filter(one => one !== role);
620
+ if (on)
621
+ roles.push(role);
622
+ if (roles.length === 0) {
623
+ const { capability, ...rest } = part;
624
+ return rest;
625
+ }
626
+ // 형식이 적은 순서를 지킨다 — 켠 순서대로 두면 같은 뜻이 다른 값으로 저장된다.
627
+ const ordered = PART_ROLES.filter(one => roles.includes(one));
628
+ return { ...part, capability: { ...part.capability, roles: ordered } };
629
+ });
630
+ }
631
+ setAccepts(raw) {
632
+ const accepts = raw
633
+ .split(',')
634
+ .map(one => one.trim())
635
+ .filter(one => one.length > 0);
636
+ this.change(part => {
637
+ const spec = { ...part.capability, roles: part.capability?.roles ?? ['slot'] };
638
+ // 빈 목록은 제한하지 않는다는 뜻이다. 빈 배열을 저장하면 아무것도 못 받는 것으로 읽힌다.
639
+ if (accepts.length > 0)
640
+ spec.accepts = accepts;
641
+ else
642
+ delete spec.accepts;
643
+ return { ...part, capability: spec };
644
+ });
645
+ }
646
+ setCapacity(value) {
647
+ this.change(part => {
648
+ const spec = { ...part.capability, roles: part.capability?.roles ?? ['slot'] };
649
+ if (Number.isFinite(value) && value >= 1)
650
+ spec.capacity = Math.floor(value);
651
+ else
652
+ delete spec.capacity;
653
+ return { ...part, capability: spec };
654
+ });
655
+ }
499
656
  /**
500
657
  * 도형 자체 — 이름과 타입.
501
658
  *
@@ -512,12 +669,12 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
512
669
  renderFigure() {
513
670
  return html `
514
671
  <section>
515
- <h3>${i18next.t('label.figure')}</h3>
672
+ <h3>${i18next.t('figure.label.figure')}</h3>
516
673
  <div field>
517
- <label>${i18next.t('label.name')}</label>
674
+ <label>${i18next.t('figure.label.name')}</label>
518
675
  <input
519
676
  .value=${this.figureName}
520
- placeholder=${i18next.t('text.figure-display-name')}
677
+ placeholder=${i18next.t('figure.text.figure-display-name')}
521
678
  @change=${(e) => this.dispatchEvent(new CustomEvent('figure-rename', {
522
679
  detail: { name: e.target.value },
523
680
  bubbles: true,
@@ -526,8 +683,8 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
526
683
  />
527
684
  </div>
528
685
  <div field>
529
- <label>${i18next.t('label.type')}</label>
530
- <code fixed title=${i18next.t('text.type-name-cannot-change')}>
686
+ <label>${i18next.t('figure.label.figure-type')}${this.help('figure.text.type-name-cannot-change')}</label>
687
+ <code fixed title=${i18next.t('figure.text.type-name-cannot-change')}>
531
688
  <md-icon>lock</md-icon>${this.board?.figureType ?? ''}
532
689
  </code>
533
690
  </div>
@@ -535,28 +692,64 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
535
692
  왜 못 고치는지 **적어 둔다.** 전에는 tooltip 에만 있었고, 그러면 눌러 보고 안 되는 것을
536
693
  알게 된다 — 화면이 안 되는 이유를 말하지 않으면 사용자가 자기 실수라고 생각한다.
537
694
  -->
538
- <p quiet>${i18next.t('text.type-name-cannot-change')}</p>
695
+
696
+ <div field>
697
+ <label>${i18next.t('figure.label.category')}${this.help('figure.text.category-explained')}</label>
698
+ <input
699
+ .value=${this.figureCategory ?? ''}
700
+ placeholder=${i18next.t('figure.text.category-placeholder')}
701
+ @change=${(e) => this.tellCatalog({ category: e.target.value.trim() || undefined })}
702
+ />
703
+ </div>
704
+
705
+ <div field>
706
+ <label>${i18next.t('figure.label.tags')}${this.help('figure.text.tags-explained')}</label>
707
+ <input
708
+ .value=${(this.figureTags ?? []).join(', ')}
709
+ placeholder=${i18next.t('figure.text.tags-placeholder')}
710
+ @change=${(e) => this.tellTags(e.target.value)}
711
+ />
712
+ </div>
539
713
  </section>
540
714
  `;
541
715
  }
716
+ /**
717
+ * 태그 줄을 목록으로.
718
+ *
719
+ * 쉼표로 가르고 `#` 은 떼어 낸다 — 사람은 `#반출` 이라고 적고, 저장되는 것은 `반출` 이다.
720
+ * 겹친 것은 한 번만 남긴다. 빈 줄은 **빈 배열**이지 `undefined` 가 아니다 — 전부 지우려는
721
+ * 저작자가 지울 방법을 잃지 않게.
722
+ */
723
+ tellTags(raw) {
724
+ const tags = [
725
+ ...new Set(raw
726
+ .split(',')
727
+ .map(one => one.trim().replace(/^#+/, '').trim())
728
+ .filter(one => one.length > 0))
729
+ ];
730
+ this.tellCatalog({ tags });
731
+ }
732
+ /** 카탈로그 값이 바뀌었다고 알린다. 정본은 저작면이 든다 — 여기서는 알리기만 한다. */
733
+ tellCatalog(patch) {
734
+ this.dispatchEvent(new CustomEvent('figure-catalog', { detail: patch, bubbles: true, composed: true }));
735
+ }
542
736
  renderIdentity(part) {
543
737
  return html `
544
738
  <section>
545
- <h3>${i18next.t('label.part')}</h3>
739
+ <h3>${i18next.t('figure.label.part')}</h3>
546
740
  <div field>
547
- <label>${i18next.t('label.name')}</label>
741
+ <label>${i18next.t('figure.label.name')}${this.help('figure.text.renaming-a-part-breaks-bindings')}</label>
548
742
  <input
549
743
  .value=${part.name}
550
744
  @change=${(e) => this.put({ name: e.target.value })}
551
745
  />
552
- <p warn>${i18next.t('text.renaming-a-part-breaks-bindings')}</p>
553
746
  </div>
554
747
  <div field>
555
- <label>${i18next.t('label.shape')}</label>
748
+ <label>${i18next.t('figure.label.shape')}</label>
556
749
  <select @change=${(e) => this.changePrimitive(e.target.value)}>
557
750
  ${PRIMITIVE_KINDS.map(kind => html `
558
751
  <option value=${kind} ?selected=${kind === part.primitive}>
559
- ${i18next.t('label.primitive-' + kind)}
752
+ ${i18next.t('figure.label.primitive-' + kind)}
560
753
  </option>
561
754
  `)}
562
755
  </select>
@@ -569,31 +762,31 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
569
762
  const rotation = t.rotation ?? {};
570
763
  return html `
571
764
  <section>
572
- <h3>${i18next.t('label.placement')}</h3>
765
+ <h3>${i18next.t('figure.label.placement')}</h3>
573
766
 
574
- <label>${i18next.t('label.position')}</label>
767
+ <label>${i18next.t('figure.label.part-position')}</label>
575
768
  <div triple>
576
769
  ${['x', 'y', 'z'].map(axis => this.number(axis.toUpperCase(), t.position[axis], value => this.putTransform({ position: { ...t.position, [axis]: value } })))}
577
770
  </div>
578
771
 
579
772
  <div field>
580
- <label>${i18next.t('label.size')}</label>
773
+ <label>${i18next.t('figure.label.part-size')}</label>
581
774
  <div triple>
582
775
  ${['x', 'y', 'z'].map(axis => this.number(axis.toUpperCase(), t.size[axis], value => this.putTransform({ size: { ...t.size, [axis]: value } }), 1))}
583
776
  </div>
584
777
  </div>
585
778
 
586
779
  <div field>
587
- <label>${i18next.t('label.tools')}</label>
780
+ <label>${i18next.t('figure.label.tools')}</label>
588
781
  <div tools>
589
- <button title=${i18next.t('text.center-on-x')} @click=${() => this.ask('center', 'x')}>
590
- ${i18next.t('button.center-x')}
782
+ <button title=${i18next.t('figure.text.center-on-x')} @click=${() => this.ask('center', 'x')}>
783
+ ${i18next.t('figure.button.center-x')}
591
784
  </button>
592
- <button title=${i18next.t('text.center-on-z')} @click=${() => this.ask('center', 'z')}>
593
- ${i18next.t('button.center-z')}
785
+ <button title=${i18next.t('figure.text.center-on-z')} @click=${() => this.ask('center', 'z')}>
786
+ ${i18next.t('figure.button.center-z')}
594
787
  </button>
595
- <button title=${i18next.t('text.put-on-floor')} @click=${() => this.ask('floor', 'y')}>
596
- ${i18next.t('button.to-floor')}
788
+ <button title=${i18next.t('figure.text.put-on-floor')} @click=${() => this.ask('floor', 'y')}>
789
+ ${i18next.t('figure.button.to-floor')}
597
790
  </button>
598
791
  <span sep></span>
599
792
  ${['x', 'z'].map(axis => {
@@ -602,22 +795,22 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
602
795
  return html `
603
796
  <button
604
797
  ?disabled=${!able}
605
- title=${i18next.t(able ? 'text.mirror-explained' : 'text.mirror-would-overlap')}
798
+ title=${i18next.t(able ? 'figure.text.mirror-explained' : 'figure.text.mirror-would-overlap')}
606
799
  @click=${() => this.ask('mirror', axis)}
607
800
  >
608
- ${i18next.t('button.mirror-' + axis)}
801
+ ${i18next.t('figure.button.mirror-' + axis)}
609
802
  </button>
610
803
  `;
611
804
  })}
612
805
  <span sep></span>
613
- <button title=${i18next.t('text.snap-explained')} @click=${() => this.ask('snap', 'x')}>
614
- ${i18next.t('button.snap')}
806
+ <button title=${i18next.t('figure.text.snap-explained')} @click=${() => this.ask('snap', 'x')}>
807
+ ${i18next.t('figure.button.snap')}
615
808
  </button>
616
809
  </div>
617
810
  </div>
618
811
 
619
812
  <div field>
620
- <label>${i18next.t('label.rotation-degrees')}</label>
813
+ <label>${i18next.t('figure.label.rotation-degrees')}</label>
621
814
  <div triple>
622
815
  ${['x', 'y', 'z'].map(axis => this.number(axis.toUpperCase(), rotation[axis] ?? 0, value => this.putTransform({ rotation: { ...rotation, [axis]: value } })))}
623
816
  </div>
@@ -633,7 +826,7 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
633
826
  return nothing;
634
827
  return html `
635
828
  <section>
636
- <h3>${i18next.t('label.shape-detail')}</h3>
829
+ <h3>${i18next.t('figure.label.shape-detail')}</h3>
637
830
  ${segmented ? this.renderSegments(part) : nothing}
638
831
  ${segmented ? this.renderKeepRound(part) : nothing}
639
832
  ${extruded ? this.renderExtruded(part) : nothing}
@@ -653,17 +846,17 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
653
846
  const reshape = (over) => this.put({ shape: { ...(part.shape ?? {}), ...over } });
654
847
  return html `
655
848
  <div field>
656
- <label>${i18next.t('label.corner-round')}</label>
849
+ <label>${i18next.t('figure.label.corner-round')}</label>
657
850
  ${this.number('', part.shape?.round ?? 0, value => reshape({ round: value }))}
658
851
  </div>
659
852
  <div field>
660
- <label>${i18next.t('label.hollow-wall')}</label>
853
+ <label>${i18next.t('figure.label.hollow-wall')}</label>
661
854
  ${this.number('', wall, value => reshape({ hollow: value > 0 ? { ...(hollow ?? {}), wall: value } : undefined }))}
662
855
  </div>
663
856
  ${wall > 0
664
857
  ? html `
665
858
  <div field>
666
- <label>${i18next.t('label.hollow-floor')}</label>
859
+ <label>${i18next.t('figure.label.hollow-floor')}</label>
667
860
  ${this.number('', hollow?.floor ?? wall, value => reshape({ hollow: { ...hollow, wall, floor: value } }))}
668
861
  </div>
669
862
  `
@@ -688,15 +881,15 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
688
881
  const on = part.keepRound !== false;
689
882
  return html `
690
883
  <div field>
691
- <label>${i18next.t('label.keep-round')}</label>
884
+ <label>${i18next.t('figure.label.keep-round')}</label>
692
885
  <div choices>
693
886
  <button choice ?on=${on} @click=${() => this.put({ keepRound: undefined })}>
694
- <b>${i18next.t('text.keep-round-on')}</b>
695
- <small>${i18next.t('text.keep-round-cost')}</small>
887
+ <b>${i18next.t('figure.text.keep-round-on')}</b>
888
+ <small>${i18next.t('figure.text.keep-round-cost')}</small>
696
889
  </button>
697
890
  <button choice ?on=${!on} @click=${() => this.put({ keepRound: false })}>
698
- <b>${i18next.t('text.keep-round-off')}</b>
699
- <small>${i18next.t('text.keep-round-merged')}</small>
891
+ <b>${i18next.t('figure.text.keep-round-off')}</b>
892
+ <small>${i18next.t('figure.text.keep-round-merged')}</small>
700
893
  </button>
701
894
  </div>
702
895
  </div>
@@ -706,7 +899,7 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
706
899
  const current = part.segments ?? SEGMENT_PRESETS[1];
707
900
  return html `
708
901
  <div field>
709
- <label>${i18next.t('label.segments')}</label>
902
+ <label>${i18next.t('figure.label.segments')}</label>
710
903
  <div segments>
711
904
  ${SEGMENT_PRESETS.map(preset => {
712
905
  const triangles = trianglesOf({ ...part, segments: preset });
@@ -714,14 +907,14 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
714
907
  <button seg ?on=${current === preset} @click=${() => this.put({ segments: preset })}>
715
908
  ${this.silhouette(preset)}
716
909
  <b>${preset}</b>
717
- <small>${i18next.t('text.n-triangles', { count: triangles })}</small>
910
+ <small>${i18next.t('figure.text.n-triangles', { count: triangles })}</small>
718
911
  </button>
719
912
  `;
720
913
  })}
721
914
  </div>
722
915
 
723
916
  <details free>
724
- <summary>${i18next.t('text.other-segment-count')}</summary>
917
+ <summary>${i18next.t('figure.text.other-segment-count')}</summary>
725
918
  ${this.number('', current, value => this.put({ segments: value }), 3)}
726
919
  </details>
727
920
  </div>
@@ -775,36 +968,36 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
775
968
  const glow = material.emissive;
776
969
  return html `
777
970
  <section>
778
- <h3>${i18next.t('label.material')}</h3>
971
+ <h3>${i18next.t('figure.label.part-material')}</h3>
779
972
 
780
973
  <div field>
781
- <label>${i18next.t('label.palette-token')}</label>
974
+ <label>${i18next.t('figure.label.palette-token')}</label>
782
975
  <div swatches>
783
976
  ${this.swatches(painted, material.token)}
784
977
  </div>
785
978
  ${lit.length
786
979
  ? html `
787
- <label group>${i18next.t('label.status-colors')}</label>
980
+ <label group>${i18next.t('figure.label.status-colors')}</label>
788
981
  <div swatches>
789
982
  ${this.swatches(lit, material.token)}
790
983
  </div>
791
984
  `
792
985
  : ''}
793
- <span token>${material.token ?? i18next.t('label.no-color-chosen')}</span>
986
+ <span token>${material.token ?? i18next.t('figure.label.no-color-chosen')}</span>
794
987
  </div>
795
988
 
796
989
  <div field>
797
- <label>${i18next.t('label.preset')}</label>
990
+ <label>${i18next.t('figure.label.preset')}</label>
798
991
  <select
799
992
  @change=${(e) => {
800
993
  const value = e.target.value;
801
994
  this.putMaterial({ preset: (value || undefined) });
802
995
  }}
803
996
  >
804
- <option value="" ?selected=${!material.preset}>${i18next.t('label.none')}</option>
997
+ <option value="" ?selected=${!material.preset}>${i18next.t('figure.label.none')}</option>
805
998
  ${MATERIAL_PRESETS.map(preset => html `
806
999
  <option value=${preset} ?selected=${preset === material.preset}>
807
- ${i18next.t('label.preset-' + preset)}
1000
+ ${i18next.t('figure.label.preset-' + preset)}
808
1001
  </option>
809
1002
  `)}
810
1003
  </select>
@@ -817,7 +1010,7 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
817
1010
  .checked=${material.flatShading ?? true}
818
1011
  @change=${(e) => this.putMaterial({ flatShading: e.target.checked })}
819
1012
  />
820
- ${i18next.t('label.flat-shading')}
1013
+ ${i18next.t('figure.label.flat-shading')}
821
1014
  </label>
822
1015
  <label toggle>
823
1016
  <input
@@ -825,12 +1018,11 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
825
1018
  .checked=${!!material.transparent}
826
1019
  @change=${(e) => this.putMaterial({ transparent: e.target.checked })}
827
1020
  />
828
- ${i18next.t('label.transparent')}
1021
+ ${i18next.t('figure.label.transparent')}
829
1022
  </label>
830
1023
 
831
1024
  <div field>
832
- <label>${i18next.t('label.emissive')}</label>
833
- <p cost>${i18next.t('text.emissive-explained')}</p>
1025
+ <label>${i18next.t('figure.label.emissive')}${this.help('figure.text.emissive-explained')}</label>
834
1026
 
835
1027
  <div slider>
836
1028
  <input
@@ -848,13 +1040,13 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
848
1040
  ${glow
849
1041
  ? html `
850
1042
  <div field>
851
- <label>${i18next.t('label.emissive-color')}</label>
1043
+ <label>${i18next.t('figure.label.emissive-color')}</label>
852
1044
  <div swatches>
853
1045
  <button
854
1046
  swatch
855
1047
  same
856
1048
  ?on=${!glow.token}
857
- title=${i18next.t('label.same-as-off')}
1049
+ title=${i18next.t('figure.label.same-as-off')}
858
1050
  style="--swatch:${(material.token && palette[material.token]) || palette['palette.neutral']}"
859
1051
  @click=${() => this.putMaterial({ emissive: { ...glow, token: undefined } })}
860
1052
  ></button>
@@ -868,7 +1060,7 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
868
1060
  ></button>
869
1061
  `)}
870
1062
  </div>
871
- <span token>${glow.token ?? i18next.t('label.same-as-off')}</span>
1063
+ <span token>${glow.token ?? i18next.t('figure.label.same-as-off')}</span>
872
1064
  </div>
873
1065
 
874
1066
  <label toggle>
@@ -877,9 +1069,9 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
877
1069
  .checked=${!!glow.on}
878
1070
  @change=${(e) => this.putMaterial({ emissive: { ...glow, on: e.target.checked } })}
879
1071
  />
880
- ${i18next.t('label.on-by-default')}
1072
+ ${i18next.t('figure.label.on-by-default')}
881
1073
  </label>
882
- <p cost>${i18next.t('text.on-by-default-explained')}</p>
1074
+ ${this.help('figure.text.on-by-default-explained')}
883
1075
  `
884
1076
  : ''}
885
1077
 
@@ -908,25 +1100,21 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
908
1100
  const repeat = part.repeat ?? { axis: 'x', pitch: 0 };
909
1101
  return html `
910
1102
  <section>
911
- <h3>${i18next.t('label.sizing-rule')}</h3>
1103
+ <h3>${i18next.t('figure.label.sizing-rule')}${this.help('figure.text.sizing-' + sizing + '-explained')}</h3>
912
1104
  <select @change=${(e) => this.put({ sizing: e.target.value })}>
913
1105
  ${SIZING_RULES.map(rule => html `
914
- <option value=${rule} ?selected=${rule === sizing}>${i18next.t('label.sizing-' + rule)}</option>
1106
+ <option value=${rule} ?selected=${rule === sizing}>${i18next.t('figure.label.sizing-' + rule)}</option>
915
1107
  `)}
916
1108
  </select>
917
- <p warn>${i18next.t('text.sizing-' + sizing + '-explained')}</p>
1109
+ ${sizing === 'scale' ? nothing : this.help('figure.text.sizing-costs-one-draw-call')}
1110
+ ${sizing === 'repeat' ? this.help('figure.text.repeat-edits-the-unit') : nothing}
918
1111
 
919
- ${sizing === 'scale'
920
- ? nothing
921
- : html `<p cost>${i18next.t('text.sizing-costs-one-draw-call')}</p>`}
922
- ${sizing === 'repeat'
923
- ? html `<p cost>${i18next.t('text.repeat-edits-the-unit')}</p>`
924
- : nothing}
1112
+ ${this.renderAnchors(part)}
925
1113
 
926
1114
  ${sizing === 'repeat'
927
1115
  ? html `
928
1116
  <div field>
929
- <label>${i18next.t('label.repeat-axis')}</label>
1117
+ <label>${i18next.t('figure.label.repeat-axis')}</label>
930
1118
  <select
931
1119
  @change=${(e) => this.put({ repeat: { ...repeat, axis: e.target.value } })}
932
1120
  >
@@ -934,15 +1122,165 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
934
1122
  </select>
935
1123
  </div>
936
1124
  <div field>
937
- <label>${i18next.t('label.repeat-pitch')}</label>
1125
+ <label>${i18next.t('figure.label.repeat-pitch')}${this.help('figure.text.repeat-pitch-explained')}</label>
938
1126
  ${this.number('', repeat.pitch, value => this.put({ repeat: { ...repeat, pitch: value } }), 1)}
939
- <p cost>${i18next.t('text.repeat-pitch-explained')}</p>
940
1127
  </div>
1128
+ ${this.renderRepeatCount(part)}
941
1129
  `
942
1130
  : nothing}
943
1131
  </section>
944
1132
  `;
945
1133
  }
1134
+ /**
1135
+ * ── 몇 개가 되나 ──────────────────────────────────────────────────────────
1136
+ *
1137
+ * 저작 화면은 축과 피치만 받고 있었다. 형식이 가진 반복 설정이 그 둘뿐이므로 편집할 것은
1138
+ * 다 있었는데, **결과인 개수를 어디에도 표시하지 않았다.** 피치 300 을 적은 저작자가 몇 개가
1139
+ * 되는지 알려면 미리보기를 세어야 했다.
1140
+ *
1141
+ * 개수는 그리는 쪽이 쓰는 `repeatPlan` 을 그대로 불러 얻는다. 여기서 다시 계산하면 화면이
1142
+ * 말하는 개수와 실제로 그려지는 개수가 달라진다.
1143
+ *
1144
+ * ## 두 가지 반복이 있다
1145
+ *
1146
+ * 컨베이어의 롤러는 길어지면 **개수가 늘고** 한 칸 간격은 그대로다. 셀트레이의 셀은 커지면
1147
+ * **셀이 커지고** 개수는 그대로다 — 몇 셀짜리 트레이냐는 화면 크기와 무관하다. 그 갈림을
1148
+ * 이 축의 anchor 가 정하는데 화면에는 그 말이 없었다. 그래서 어느 쪽인지 함께 적는다.
1149
+ */
1150
+ renderRepeatCount(part) {
1151
+ const board = this.board;
1152
+ if (!board)
1153
+ return nothing;
1154
+ const base = { x: board.width, y: board.depth, z: board.height };
1155
+ const plan = repeatPlanOfPart(part, { x: 1, y: 1, z: 1 }, base);
1156
+ if (!plan)
1157
+ return nothing;
1158
+ const grows = anchorOfPart(part, base, plan.axis) !== 'scale';
1159
+ return html `
1160
+ <div field>
1161
+ <label>${i18next.t('figure.label.repeat-count')}</label>
1162
+ <p quiet>
1163
+ ${i18next.t('figure.text.repeat-count-now', { count: plan.count })}
1164
+ ${plan.count >= REPEAT_LIMIT
1165
+ ? html `<br />${i18next.t('figure.text.repeat-count-capped', { limit: REPEAT_LIMIT })}`
1166
+ : ''}
1167
+ <br />${i18next.t(grows ? 'figure.text.repeat-grows-with-instance' : 'figure.text.repeat-keeps-count')}
1168
+ </p>
1169
+ </div>
1170
+ `;
1171
+ }
1172
+ /**
1173
+ * ── 어느 면을 붙잡나 ──────────────────────────────────────────────────────
1174
+ *
1175
+ * **이 자리가 없었다.** 형식은 축마다 `anchor` 를 지고 다니고(`figure-source.ts` 가 왕복시킨다),
1176
+ * 발행 게이트는 「valve-wheel 의 anchor.z 를 max 로 두라」고 사유까지 말하는데, 저작면에는 그
1177
+ * 값을 바꿀 자리가 없었다 — 고치라는 말만 있고 고칠 문이 없었다.
1178
+ *
1179
+ * ## 「자동」이 기본이다
1180
+ *
1181
+ * 형식에서 `anchor` 는 적지 않아도 된다. 안 적으면 **가까운 면**을 붙잡고, 표본 열넷 중 어느
1182
+ * 것도 이 값을 적지 않고 제대로 선다. 그래서 축마다 첫 항목이 「자동」이고, 지금 자동이 무엇으로
1183
+ * 읽히는지를 **함께 보여 준다** — 그것을 모르면 무엇을 바꾸는지 모른 채 고르게 된다.
1184
+ *
1185
+ * 자동이 무엇인지는 형식에게 묻는다(`anchorOfPart`). 여기서 「위 절반이면 max」를 다시 세면
1186
+ * 화면이 말하는 면과 실제로 그려지는 면이 갈린다.
1187
+ *
1188
+ * ## `scale` 인 부품에도 보여 준다
1189
+ *
1190
+ * 처음에는 감췄다 — 형식 주석에 「`sizing` 이 `scale` 인 부품에는 뜻이 없다」고 적혀 있어서다.
1191
+ * **그 문장은 기본값에 대한 것이다.** `anchorOf` 는 적힌 값을 가장 먼저 보고(`if (said) return
1192
+ * said`), 그 뒤에야 `sizing` 을 본다. 즉 `scale` 부품에도 면을 적으면 그것이 이긴다.
1193
+ *
1194
+ * 감춰 두면 어긋남이 난다: 발행 게이트가 막는 MIXER 의 `valve-wheel` 이 `scale` 부품이고,
1195
+ * 게이트의 사유는 「그 부품의 anchor.z 를 max 로 두라」다. 고치라는 자리에 문을 닫아 둔 셈이었다.
1196
+ *
1197
+ * 대신 `scale` 일 때는 자동이 무엇인지가 「함께 커진다」로 보이므로, 저작자가 그것을 바꾸는
1198
+ * 것임을 알 수 있다.
1199
+ */
1200
+ /**
1201
+ * ── 도움말은 자리를 먹지 않는다 ─────────────────────────────────────────────
1202
+ *
1203
+ * 설명 문단이 열 줄 있었고 그 줄들이 패널 높이의 상당 부분이었다. 속성은 앞으로 더 늘어난다
1204
+ * (능력 · 포트 · 애니메이션 · 기준정보) — 늘 보이는 설명은 그 자리를 먼저 먹는다.
1205
+ *
1206
+ * 그래서 물음표로 접는다. **지우지 않는다** — 그 문장들은 「이걸 고르면 무엇이 달라지나」를
1207
+ * 말하는 것이고, 없으면 닫힌 목록을 고를 수 없다. 한 번 읽으면 되는 것이라 접어 두는 것이 맞다.
1208
+ *
1209
+ * 편 것은 **부품을 바꿔도 그대로 둔다**(`opened` 는 부품에 딸린 상태가 아니다). 같은 것을
1210
+ * 계속 고치는 사람이 매번 다시 펴야 하면 접은 것이 되레 방해가 된다.
1211
+ */
1212
+ help(key) {
1213
+ const shown = this.opened.has(key);
1214
+ return html `
1215
+ <button
1216
+ help
1217
+ aria-label=${i18next.t('figure.label.help')}
1218
+ aria-expanded=${shown ? 'true' : 'false'}
1219
+ @click=${() => {
1220
+ shown ? this.opened.delete(key) : this.opened.add(key);
1221
+ this.requestUpdate();
1222
+ }}
1223
+ >
1224
+ <md-icon>${shown ? 'help' : 'help_outline'}</md-icon>
1225
+ </button>
1226
+ ${shown ? html `<p hint>${i18next.t(key)}</p>` : nothing}
1227
+ `;
1228
+ }
1229
+ renderAnchors(part) {
1230
+ const base = this.board
1231
+ ? { x: this.board.width, y: this.board.height, z: this.board.depth }
1232
+ : { x: 0, y: 0, z: 0 };
1233
+ return html `
1234
+ <div field>
1235
+ <!-- 「anchor.x」로 적힌 사유를 화면에서 찾을 수 있게 칸 이름에 그 낱말을 남긴다 -->
1236
+ <label>${i18next.t('figure.label.anchor')} (anchor)${this.help('figure.text.anchor-explained')}</label>
1237
+ ${['x', 'y', 'z'].map(axis => {
1238
+ const said = part.anchor?.[axis];
1239
+ const auto = anchorOfPart(part, base, axis);
1240
+ /*
1241
+ **면 이름은 축마다 다르다.** 처음에는 min 을 「아래·왼쪽·뒤 면」으로 한 줄에 다 적었는데,
1242
+ x 줄에서 「아래」와 「뒤」는 관계가 없어 읽는 사람이 무엇을 고르는지 몰랐다.
1243
+
1244
+ 그리고 이름씨가 아니라 **동작**으로 적는다 — 「오른쪽 면」이 아니라 「오른쪽 면에 붙어
1245
+ 있는다」다. 「anchor.x 를 max 로 둔다」가 무슨 뜻인지 모르겠다는 말이 그 자리에서 나왔다.
1246
+ */
1247
+ /*
1248
+ **값 이름을 함께 보인다.** 동작만 적었더니(「오른쪽 면에 붙어 있는다») 발행 게이트가
1249
+ 「anchor.x 를 max 로 두라」고 말할 때 화면에서 그 낱말을 찾을 수 없었다 — 사용자가
1250
+ 그 자리에서 지적했다.
1251
+
1252
+ `max` 는 정본에 저장되는 이름이고 서버 오류·형식 문서·게이트 사유가 다 그 이름을 쓴다.
1253
+ 숨기면 안 되는 종류다. 동작이 먼저고 이름이 괄호로 따라온다.
1254
+ */
1255
+ const nameOf = (rule) => `${i18next.t(rule === 'min' || rule === 'max' ? `figure.label.anchor-${rule}-${axis}` : `figure.label.anchor-${rule}`)} (${rule})`;
1256
+ return html `
1257
+ <div cell>
1258
+ <span axis>${axis}</span>
1259
+ <select
1260
+ @change=${(e) => {
1261
+ const value = e.target.value;
1262
+ const anchor = { ...(part.anchor ?? {}) };
1263
+ /* 「자동」은 칸을 지우는 것이다 — 빈 값을 넣어 두면 형식이 그것을 값으로 읽는다. */
1264
+ if (value) {
1265
+ anchor[axis] = value;
1266
+ }
1267
+ else {
1268
+ delete anchor[axis];
1269
+ }
1270
+ this.put({ anchor: Object.keys(anchor).length ? anchor : undefined });
1271
+ }}
1272
+ >
1273
+ <option value="" ?selected=${!said}>
1274
+ ${i18next.t('figure.label.anchor-auto', { rule: nameOf(auto) })}
1275
+ </option>
1276
+ ${ANCHOR_RULES.map(rule => html `<option value=${rule} ?selected=${rule === said}>${nameOf(rule)}</option>`)}
1277
+ </select>
1278
+ </div>
1279
+ `;
1280
+ })}
1281
+ </div>
1282
+ `;
1283
+ }
946
1284
  /**
947
1285
  * 숫자 칸.
948
1286
  *
@@ -1047,6 +1385,18 @@ __decorate([
1047
1385
  property({ type: String }),
1048
1386
  __metadata("design:type", Object)
1049
1387
  ], FigureInspector.prototype, "figureName", void 0);
1388
+ __decorate([
1389
+ property({ type: String }),
1390
+ __metadata("design:type", String)
1391
+ ], FigureInspector.prototype, "figureCategory", void 0);
1392
+ __decorate([
1393
+ property({ type: Array }),
1394
+ __metadata("design:type", Array)
1395
+ ], FigureInspector.prototype, "figureTags", void 0);
1396
+ __decorate([
1397
+ state(),
1398
+ __metadata("design:type", Object)
1399
+ ], FigureInspector.prototype, "opened", void 0);
1050
1400
  __decorate([
1051
1401
  property({ type: Object }),
1052
1402
  __metadata("design:type", Object)