@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.
- package/client/graphql/index.ts +36 -3
- package/client/modeller/figure-animations.ts +712 -0
- package/client/modeller/figure-ask.ts +5 -5
- package/client/modeller/figure-canvas.ts +1 -1
- package/client/modeller/figure-capabilities.ts +372 -0
- package/client/modeller/figure-history-panel.ts +171 -18
- package/client/modeller/figure-inspector.ts +439 -76
- package/client/modeller/figure-parts.ts +15 -15
- package/client/modeller/figure-preview.ts +2 -2
- package/client/modeller/figure-report.ts +102 -37
- package/client/modeller/figure-settings.ts +20 -20
- package/client/modeller/figure-side.ts +91 -15
- package/client/modeller/figure-source.test.ts +15 -1
- package/client/modeller/figure-source.ts +31 -15
- package/client/modeller/figure-thumbnail.ts +49 -4
- package/client/modeller/maturity.ts +10 -10
- package/client/pages/figure-list-page.ts +204 -66
- package/client/pages/figure-modeller-page.ts +158 -42
- package/client/types.ts +31 -0
- package/client/viewparts/figure-thumb.ts +250 -0
- package/dist-client/graphql/index.d.ts +8 -1
- package/dist-client/graphql/index.js +33 -3
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-animations.d.ts +93 -0
- package/dist-client/modeller/figure-animations.js +655 -0
- package/dist-client/modeller/figure-animations.js.map +1 -0
- package/dist-client/modeller/figure-ask.js +5 -5
- package/dist-client/modeller/figure-ask.js.map +1 -1
- package/dist-client/modeller/figure-canvas.js +1 -1
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-capabilities.d.ts +42 -0
- package/dist-client/modeller/figure-capabilities.js +344 -0
- package/dist-client/modeller/figure-capabilities.js.map +1 -0
- package/dist-client/modeller/figure-history-panel.d.ts +15 -1
- package/dist-client/modeller/figure-history-panel.js +165 -18
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +98 -0
- package/dist-client/modeller/figure-inspector.js +425 -75
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-parts.js +15 -15
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +2 -2
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +18 -1
- package/dist-client/modeller/figure-report.js +105 -37
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-settings.js +19 -19
- package/dist-client/modeller/figure-settings.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +24 -0
- package/dist-client/modeller/figure-side.js +94 -14
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +15 -8
- package/dist-client/modeller/figure-source.js +16 -7
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/figure-thumbnail.js +40 -4
- package/dist-client/modeller/figure-thumbnail.js.map +1 -1
- package/dist-client/modeller/maturity.js +10 -10
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/pages/figure-list-page.d.ts +11 -4
- package/dist-client/pages/figure-list-page.js +201 -63
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +33 -0
- package/dist-client/pages/figure-modeller-page.js +156 -42
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +29 -0
- package/dist-client/types.js.map +1 -1
- package/dist-client/viewparts/figure-thumb.d.ts +29 -0
- package/dist-client/viewparts/figure-thumb.js +242 -0
- package/dist-client/viewparts/figure-thumb.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/test/i18n-prefix-guard.test.ts +171 -0
- package/test/korean-register-guard.test.ts +83 -0
- package/translations/en.json +351 -264
- package/translations/ja.json +276 -264
- package/translations/ko.json +351 -264
- package/translations/ms.json +276 -264
- package/translations/zh.json +276 -264
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import '@material/web/icon/icon.js'
|
|
2
2
|
|
|
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
|
|
|
6
7
|
import { i18next, localize } from '@operato/i18n'
|
|
7
8
|
import { ScrollbarStyles } from '@operato/styles'
|
|
@@ -14,7 +15,8 @@ import {
|
|
|
14
15
|
SIZING_RULES,
|
|
15
16
|
trianglesOf
|
|
16
17
|
} from '@hatiolab/figure-model'
|
|
17
|
-
import
|
|
18
|
+
import { ANCHOR_RULES, PART_ROLES, REPEAT_LIMIT, anchorOfPart, repeatPlanOfPart } from '@hatiolab/figure-model'
|
|
19
|
+
import type { AnchorRule, Axis, FigurePart, MaterialPreset, PartRole, PrimitiveKind } from '@hatiolab/figure-model'
|
|
18
20
|
import { activePalette } from '@hatiolab/things-scene'
|
|
19
21
|
|
|
20
22
|
import { partToFigure, partFromFigure } from './figure-source.js'
|
|
@@ -87,12 +89,21 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
87
89
|
font-size: 0.76rem;
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
/*
|
|
93
|
+
── 자리를 아끼는 판 ──────────────────────────────────────────────────────
|
|
94
|
+
속성은 앞으로 더 늘어난다(능력 · 포트 · 애니메이션 · 기준정보). 지금 여백을 그대로 두면
|
|
95
|
+
그것들이 들어올 자리가 없다. 절 여백을 6/10 으로, 제목 아래를 4px 로 줄였다 — 글자 크기와
|
|
96
|
+
칸 높이는 건드리지 않는다(작게 만드는 것이 아니라 **빈 곳을 줄이는 것**이다).
|
|
97
|
+
*/
|
|
90
98
|
section {
|
|
91
|
-
padding:
|
|
99
|
+
padding: 6px 10px;
|
|
92
100
|
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
93
101
|
}
|
|
94
102
|
h3 {
|
|
95
|
-
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
gap: 4px;
|
|
106
|
+
margin: 0 0 4px 0;
|
|
96
107
|
font-size: var(--ui-t-micro, 11px);
|
|
97
108
|
font-weight: 700;
|
|
98
109
|
letter-spacing: 0.04em;
|
|
@@ -101,12 +112,47 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
label {
|
|
104
|
-
display:
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: 3px;
|
|
105
118
|
margin-bottom: 2px;
|
|
106
119
|
font-size: 0.68rem;
|
|
107
120
|
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
108
121
|
}
|
|
109
122
|
|
|
123
|
+
/*
|
|
124
|
+
도움말 물음표. **글자 줄을 하나 먹지 않는다** — 라벨·제목 줄의 남는 폭에 얹힌다.
|
|
125
|
+
아이콘만 두고 테두리를 안 두는 이유는 이것이 누를 것이라기보다 「더 있다」는 표시라서다.
|
|
126
|
+
*/
|
|
127
|
+
button[help] {
|
|
128
|
+
display: inline-flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
padding: 0;
|
|
131
|
+
margin: 0;
|
|
132
|
+
color: var(--md-sys-color-outline);
|
|
133
|
+
background: none;
|
|
134
|
+
border: none;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
}
|
|
137
|
+
button[help] md-icon {
|
|
138
|
+
--md-icon-size: 13px;
|
|
139
|
+
font-size: 13px;
|
|
140
|
+
}
|
|
141
|
+
button[help]:hover,
|
|
142
|
+
button[help][aria-expanded='true'] {
|
|
143
|
+
color: var(--md-sys-color-primary);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* 편 도움말. 라벨 아래 한 칸 들여 붙인다 — 무엇에 딸린 말인지 보이게. */
|
|
147
|
+
p[hint] {
|
|
148
|
+
margin: 2px 0 4px 0;
|
|
149
|
+
padding-left: 6px;
|
|
150
|
+
font-size: 0.68rem;
|
|
151
|
+
line-height: 1.5;
|
|
152
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
153
|
+
border-left: 2px solid var(--md-sys-color-outline-variant);
|
|
154
|
+
}
|
|
155
|
+
|
|
110
156
|
input,
|
|
111
157
|
select {
|
|
112
158
|
width: 100%;
|
|
@@ -174,7 +220,8 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
174
220
|
div[field] + div[field],
|
|
175
221
|
div[triple] + div[field],
|
|
176
222
|
div[field] + div[triple] {
|
|
177
|
-
|
|
223
|
+
/* 칸 사이 7 → 5. 칸 자체는 그대로 두고 사이만 좁힌다. */
|
|
224
|
+
margin-top: 5px;
|
|
178
225
|
}
|
|
179
226
|
|
|
180
227
|
/*
|
|
@@ -483,6 +530,13 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
483
530
|
/** 보드. 위치를 보드 중심 기준으로 바꾸려면 이것이 있어야 한다. */
|
|
484
531
|
/** 도형의 표시 이름. 정본은 저작면이 들고, 여기서는 보여 주고 바꾼다(`figure-rename` 으로 알린다). */
|
|
485
532
|
@property({ type: String }) figureName = ''
|
|
533
|
+
/** 분류 하나 — 팔레트에서 묶는 기준. */
|
|
534
|
+
@property({ type: String }) figureCategory?: string
|
|
535
|
+
/** 태그 여럿 — 분류로는 못 하는 찾기. */
|
|
536
|
+
@property({ type: Array }) figureTags?: string[]
|
|
537
|
+
|
|
538
|
+
/** 펴 둔 도움말. 부품을 바꿔도 유지된다 — 매번 다시 펴게 하지 않는다. */
|
|
539
|
+
@state() private opened = new Set<string>()
|
|
486
540
|
|
|
487
541
|
@property({ type: Object }) board?: BoardModel
|
|
488
542
|
/** 보드 위의 부품들. 씬 모델 그대로다. */
|
|
@@ -500,8 +554,8 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
500
554
|
return html`
|
|
501
555
|
${this.renderFigure()}
|
|
502
556
|
<section>
|
|
503
|
-
<h3>${i18next.t('label.properties')}</h3>
|
|
504
|
-
<p quiet>${i18next.t('text.select-a-part-to-edit')}</p>
|
|
557
|
+
<h3>${i18next.t('figure.label.properties')}</h3>
|
|
558
|
+
<p quiet>${i18next.t('figure.text.select-a-part-to-edit')}</p>
|
|
505
559
|
</section>
|
|
506
560
|
`
|
|
507
561
|
}
|
|
@@ -509,12 +563,125 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
509
563
|
// 여기부터는 사용자 용어다 — X·Y·Z, 위치는 부품 중심, 회전은 도(degree).
|
|
510
564
|
const shown = partToFigure(part, this.board)
|
|
511
565
|
|
|
566
|
+
/*
|
|
567
|
+
역할을 먼저 묻는다.
|
|
568
|
+
|
|
569
|
+
자리(slot)로 지정한 부품은 **그려지지 않는다.** 그 사실을 모른 채 재질부터 고르면 안 쓰일
|
|
570
|
+
색을 고르게 된다. 그리고 자리는 도형이 능력을 질 때만 뜻이 있으므로, 능력이 없으면 이
|
|
571
|
+
묶음은 아예 나오지 않는다 — 해 둬도 아무 일도 안 일어나는 칸을 보여 주지 않는다.
|
|
572
|
+
*/
|
|
573
|
+
return html`
|
|
574
|
+
${this.renderFigure()} ${this.renderIdentity(shown)} ${this.renderRole(shown)}
|
|
575
|
+
${this.renderPlacement(shown)} ${this.renderShape(shown)} ${this.renderMaterial(shown)}
|
|
576
|
+
${this.renderBehaviour(shown)}
|
|
577
|
+
`
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* ── 이 부품이 능력에서 맡는 자리 ────────────────────────────────────────────
|
|
582
|
+
*
|
|
583
|
+
* 「능력」 탭이 도형 전체가 무엇을 하는지 정하고, 어느 부품이 그것을 받쳐 주는지는 여기서
|
|
584
|
+
* 정한다. **부품을 고치는 곳이 한 군데여야** 하기 때문이다 — 목록을 따로 두면 같은 부품을
|
|
585
|
+
* 두 화면에서 고치게 되고, 저작자는 어느 쪽이 이기는지 매번 생각해야 한다.
|
|
586
|
+
*
|
|
587
|
+
* 칸 수는 `repeat` 가 있으면 받지 않는다. 반복하는 자리의 칸 수는 **인스턴스 크기**가 정한다 —
|
|
588
|
+
* 컨베이어가 길어지면 실을 자리가 늘어나는 것이 그것이고, 여기 적은 값은 안 쓰인다.
|
|
589
|
+
*/
|
|
590
|
+
private renderRole(part: FigurePart) {
|
|
591
|
+
if (!this.board?.capabilities?.length) return nothing
|
|
592
|
+
|
|
593
|
+
const roles = part.capability?.roles ?? []
|
|
594
|
+
const repeats = part.sizing === 'repeat' && !!part.repeat
|
|
595
|
+
|
|
512
596
|
return html`
|
|
513
|
-
|
|
514
|
-
|
|
597
|
+
<section>
|
|
598
|
+
<h3>${i18next.t('figure.label.role-in-capability')}${this.help('figure.text.anchors-are-not-drawn')}</h3>
|
|
599
|
+
|
|
600
|
+
${(PART_ROLES as readonly PartRole[]).map(
|
|
601
|
+
one => html`
|
|
602
|
+
<label toggle>
|
|
603
|
+
<input
|
|
604
|
+
type="checkbox"
|
|
605
|
+
.checked=${live(roles.includes(one))}
|
|
606
|
+
@change=${(e: Event) => this.toggleRole(one, (e.target as HTMLInputElement).checked)}
|
|
607
|
+
/>
|
|
608
|
+
${i18next.t('figure.label.role-' + one)}
|
|
609
|
+
</label>
|
|
610
|
+
`
|
|
611
|
+
)}
|
|
612
|
+
${roles.length === 0 ? html`<p quiet>${i18next.t('figure.text.role-none-means-drawn')}</p>` : nothing}
|
|
613
|
+
|
|
614
|
+
${roles.includes('slot')
|
|
615
|
+
? html`
|
|
616
|
+
<div field>
|
|
617
|
+
<label>${i18next.t('figure.label.accepts')}${this.help('figure.text.accepts-explained')}</label>
|
|
618
|
+
<input
|
|
619
|
+
.value=${(part.capability?.accepts ?? []).join(', ')}
|
|
620
|
+
placeholder=${i18next.t('figure.text.accepts-anything')}
|
|
621
|
+
@change=${(e: Event) => this.setAccepts((e.target as HTMLInputElement).value)}
|
|
622
|
+
/>
|
|
623
|
+
</div>
|
|
624
|
+
<div field>
|
|
625
|
+
<label>${i18next.t('figure.label.slot-capacity')}</label>
|
|
626
|
+
${repeats
|
|
627
|
+
? html`<p quiet>${i18next.t('figure.text.capacity-from-repeat')}</p>`
|
|
628
|
+
: this.number('', part.capability?.capacity ?? 1, value => this.setCapacity(value), 1)}
|
|
629
|
+
</div>
|
|
630
|
+
`
|
|
631
|
+
: nothing}
|
|
632
|
+
</section>
|
|
515
633
|
`
|
|
516
634
|
}
|
|
517
635
|
|
|
636
|
+
/**
|
|
637
|
+
* 역할 하나를 켜고 끈다.
|
|
638
|
+
*
|
|
639
|
+
* **여럿을 함께 맡을 수 있다.** AGV 의 상판은 물건을 놓는 자리이면서 그 자리로 물건이
|
|
640
|
+
* 들어오고 나간다. 그래서 하나를 고르는 선택 상자가 아니라 확인 상자다.
|
|
641
|
+
*
|
|
642
|
+
* 마지막 역할을 끄면 `capability` 를 통째로 지운다. 빈 배열을 남기면 형식이 오류로 보고,
|
|
643
|
+
* 저작자가 보기에는 아무 역할도 없는 평범한 부품인데 발행이 막힌다.
|
|
644
|
+
*/
|
|
645
|
+
private toggleRole(role: PartRole, on: boolean) {
|
|
646
|
+
this.change(part => {
|
|
647
|
+
const roles = (part.capability?.roles ?? []).filter(one => one !== role)
|
|
648
|
+
if (on) roles.push(role)
|
|
649
|
+
|
|
650
|
+
if (roles.length === 0) {
|
|
651
|
+
const { capability, ...rest } = part
|
|
652
|
+
return rest as FigurePart
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// 형식이 적은 순서를 지킨다 — 켠 순서대로 두면 같은 뜻이 다른 값으로 저장된다.
|
|
656
|
+
const ordered = (PART_ROLES as readonly PartRole[]).filter(one => roles.includes(one))
|
|
657
|
+
return { ...part, capability: { ...part.capability, roles: ordered } }
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private setAccepts(raw: string) {
|
|
662
|
+
const accepts = raw
|
|
663
|
+
.split(',')
|
|
664
|
+
.map(one => one.trim())
|
|
665
|
+
.filter(one => one.length > 0)
|
|
666
|
+
|
|
667
|
+
this.change(part => {
|
|
668
|
+
const spec = { ...part.capability, roles: part.capability?.roles ?? ['slot' as PartRole] }
|
|
669
|
+
// 빈 목록은 제한하지 않는다는 뜻이다. 빈 배열을 저장하면 아무것도 못 받는 것으로 읽힌다.
|
|
670
|
+
if (accepts.length > 0) spec.accepts = accepts
|
|
671
|
+
else delete spec.accepts
|
|
672
|
+
return { ...part, capability: spec as NonNullable<FigurePart['capability']> }
|
|
673
|
+
})
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private setCapacity(value: number) {
|
|
677
|
+
this.change(part => {
|
|
678
|
+
const spec = { ...part.capability, roles: part.capability?.roles ?? ['slot' as PartRole] }
|
|
679
|
+
if (Number.isFinite(value) && value >= 1) spec.capacity = Math.floor(value)
|
|
680
|
+
else delete spec.capacity
|
|
681
|
+
return { ...part, capability: spec as NonNullable<FigurePart['capability']> }
|
|
682
|
+
})
|
|
683
|
+
}
|
|
684
|
+
|
|
518
685
|
/**
|
|
519
686
|
* 도형 자체 — 이름과 타입.
|
|
520
687
|
*
|
|
@@ -531,12 +698,12 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
531
698
|
private renderFigure() {
|
|
532
699
|
return html`
|
|
533
700
|
<section>
|
|
534
|
-
<h3>${i18next.t('label.figure')}</h3>
|
|
701
|
+
<h3>${i18next.t('figure.label.figure')}</h3>
|
|
535
702
|
<div field>
|
|
536
|
-
<label>${i18next.t('label.name')}</label>
|
|
703
|
+
<label>${i18next.t('figure.label.name')}</label>
|
|
537
704
|
<input
|
|
538
705
|
.value=${this.figureName}
|
|
539
|
-
placeholder=${i18next.t('text.figure-display-name')}
|
|
706
|
+
placeholder=${i18next.t('figure.text.figure-display-name')}
|
|
540
707
|
@change=${(e: Event) =>
|
|
541
708
|
this.dispatchEvent(
|
|
542
709
|
new CustomEvent('figure-rename', {
|
|
@@ -548,8 +715,8 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
548
715
|
/>
|
|
549
716
|
</div>
|
|
550
717
|
<div field>
|
|
551
|
-
<label>${i18next.t('label.type')}</label>
|
|
552
|
-
<code fixed title=${i18next.t('text.type-name-cannot-change')}>
|
|
718
|
+
<label>${i18next.t('figure.label.figure-type')}${this.help('figure.text.type-name-cannot-change')}</label>
|
|
719
|
+
<code fixed title=${i18next.t('figure.text.type-name-cannot-change')}>
|
|
553
720
|
<md-icon>lock</md-icon>${this.board?.figureType ?? ''}
|
|
554
721
|
</code>
|
|
555
722
|
</div>
|
|
@@ -557,30 +724,71 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
557
724
|
왜 못 고치는지 **적어 둔다.** 전에는 tooltip 에만 있었고, 그러면 눌러 보고 안 되는 것을
|
|
558
725
|
알게 된다 — 화면이 안 되는 이유를 말하지 않으면 사용자가 자기 실수라고 생각한다.
|
|
559
726
|
-->
|
|
560
|
-
|
|
727
|
+
|
|
728
|
+
<div field>
|
|
729
|
+
<label>${i18next.t('figure.label.category')}${this.help('figure.text.category-explained')}</label>
|
|
730
|
+
<input
|
|
731
|
+
.value=${this.figureCategory ?? ''}
|
|
732
|
+
placeholder=${i18next.t('figure.text.category-placeholder')}
|
|
733
|
+
@change=${(e: Event) => this.tellCatalog({ category: (e.target as HTMLInputElement).value.trim() || undefined })}
|
|
734
|
+
/>
|
|
735
|
+
</div>
|
|
736
|
+
|
|
737
|
+
<div field>
|
|
738
|
+
<label>${i18next.t('figure.label.tags')}${this.help('figure.text.tags-explained')}</label>
|
|
739
|
+
<input
|
|
740
|
+
.value=${(this.figureTags ?? []).join(', ')}
|
|
741
|
+
placeholder=${i18next.t('figure.text.tags-placeholder')}
|
|
742
|
+
@change=${(e: Event) => this.tellTags((e.target as HTMLInputElement).value)}
|
|
743
|
+
/>
|
|
744
|
+
</div>
|
|
561
745
|
</section>
|
|
562
746
|
`
|
|
563
747
|
}
|
|
564
748
|
|
|
749
|
+
/**
|
|
750
|
+
* 태그 줄을 목록으로.
|
|
751
|
+
*
|
|
752
|
+
* 쉼표로 가르고 `#` 은 떼어 낸다 — 사람은 `#반출` 이라고 적고, 저장되는 것은 `반출` 이다.
|
|
753
|
+
* 겹친 것은 한 번만 남긴다. 빈 줄은 **빈 배열**이지 `undefined` 가 아니다 — 전부 지우려는
|
|
754
|
+
* 저작자가 지울 방법을 잃지 않게.
|
|
755
|
+
*/
|
|
756
|
+
private tellTags(raw: string) {
|
|
757
|
+
const tags = [
|
|
758
|
+
...new Set(
|
|
759
|
+
raw
|
|
760
|
+
.split(',')
|
|
761
|
+
.map(one => one.trim().replace(/^#+/, '').trim())
|
|
762
|
+
.filter(one => one.length > 0)
|
|
763
|
+
)
|
|
764
|
+
]
|
|
765
|
+
|
|
766
|
+
this.tellCatalog({ tags })
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** 카탈로그 값이 바뀌었다고 알린다. 정본은 저작면이 든다 — 여기서는 알리기만 한다. */
|
|
770
|
+
private tellCatalog(patch: { category?: string; tags?: string[] }) {
|
|
771
|
+
this.dispatchEvent(new CustomEvent('figure-catalog', { detail: patch, bubbles: true, composed: true }))
|
|
772
|
+
}
|
|
773
|
+
|
|
565
774
|
private renderIdentity(part: FigurePart) {
|
|
566
775
|
return html`
|
|
567
776
|
<section>
|
|
568
|
-
<h3>${i18next.t('label.part')}</h3>
|
|
777
|
+
<h3>${i18next.t('figure.label.part')}</h3>
|
|
569
778
|
<div field>
|
|
570
|
-
<label>${i18next.t('label.name')}</label>
|
|
779
|
+
<label>${i18next.t('figure.label.name')}${this.help('figure.text.renaming-a-part-breaks-bindings')}</label>
|
|
571
780
|
<input
|
|
572
781
|
.value=${part.name}
|
|
573
782
|
@change=${(e: Event) => this.put({ name: (e.target as HTMLInputElement).value })}
|
|
574
783
|
/>
|
|
575
|
-
<p warn>${i18next.t('text.renaming-a-part-breaks-bindings')}</p>
|
|
576
784
|
</div>
|
|
577
785
|
<div field>
|
|
578
|
-
<label>${i18next.t('label.shape')}</label>
|
|
786
|
+
<label>${i18next.t('figure.label.shape')}</label>
|
|
579
787
|
<select @change=${(e: Event) => this.changePrimitive((e.target as HTMLSelectElement).value as PrimitiveKind)}>
|
|
580
788
|
${PRIMITIVE_KINDS.map(
|
|
581
789
|
kind => html`
|
|
582
790
|
<option value=${kind} ?selected=${kind === part.primitive}>
|
|
583
|
-
${i18next.t('label.primitive-' + kind)}
|
|
791
|
+
${i18next.t('figure.label.primitive-' + kind)}
|
|
584
792
|
</option>
|
|
585
793
|
`
|
|
586
794
|
)}
|
|
@@ -596,9 +804,9 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
596
804
|
|
|
597
805
|
return html`
|
|
598
806
|
<section>
|
|
599
|
-
<h3>${i18next.t('label.placement')}</h3>
|
|
807
|
+
<h3>${i18next.t('figure.label.placement')}</h3>
|
|
600
808
|
|
|
601
|
-
<label>${i18next.t('label.position')}</label>
|
|
809
|
+
<label>${i18next.t('figure.label.part-position')}</label>
|
|
602
810
|
<div triple>
|
|
603
811
|
${(['x', 'y', 'z'] as const).map(axis =>
|
|
604
812
|
this.number(axis.toUpperCase(), t.position[axis], value =>
|
|
@@ -608,7 +816,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
608
816
|
</div>
|
|
609
817
|
|
|
610
818
|
<div field>
|
|
611
|
-
<label>${i18next.t('label.size')}</label>
|
|
819
|
+
<label>${i18next.t('figure.label.part-size')}</label>
|
|
612
820
|
<div triple>
|
|
613
821
|
${(['x', 'y', 'z'] as const).map(axis =>
|
|
614
822
|
this.number(
|
|
@@ -622,16 +830,16 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
622
830
|
</div>
|
|
623
831
|
|
|
624
832
|
<div field>
|
|
625
|
-
<label>${i18next.t('label.tools')}</label>
|
|
833
|
+
<label>${i18next.t('figure.label.tools')}</label>
|
|
626
834
|
<div tools>
|
|
627
|
-
<button title=${i18next.t('text.center-on-x')} @click=${() => this.ask('center', 'x')}>
|
|
628
|
-
${i18next.t('button.center-x')}
|
|
835
|
+
<button title=${i18next.t('figure.text.center-on-x')} @click=${() => this.ask('center', 'x')}>
|
|
836
|
+
${i18next.t('figure.button.center-x')}
|
|
629
837
|
</button>
|
|
630
|
-
<button title=${i18next.t('text.center-on-z')} @click=${() => this.ask('center', 'z')}>
|
|
631
|
-
${i18next.t('button.center-z')}
|
|
838
|
+
<button title=${i18next.t('figure.text.center-on-z')} @click=${() => this.ask('center', 'z')}>
|
|
839
|
+
${i18next.t('figure.button.center-z')}
|
|
632
840
|
</button>
|
|
633
|
-
<button title=${i18next.t('text.put-on-floor')} @click=${() => this.ask('floor', 'y')}>
|
|
634
|
-
${i18next.t('button.to-floor')}
|
|
841
|
+
<button title=${i18next.t('figure.text.put-on-floor')} @click=${() => this.ask('floor', 'y')}>
|
|
842
|
+
${i18next.t('figure.button.to-floor')}
|
|
635
843
|
</button>
|
|
636
844
|
<span sep></span>
|
|
637
845
|
${(['x', 'z'] as const).map(axis => {
|
|
@@ -640,22 +848,22 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
640
848
|
return html`
|
|
641
849
|
<button
|
|
642
850
|
?disabled=${!able}
|
|
643
|
-
title=${i18next.t(able ? 'text.mirror-explained' : 'text.mirror-would-overlap')}
|
|
851
|
+
title=${i18next.t(able ? 'figure.text.mirror-explained' : 'figure.text.mirror-would-overlap')}
|
|
644
852
|
@click=${() => this.ask('mirror', axis)}
|
|
645
853
|
>
|
|
646
|
-
${i18next.t('button.mirror-' + axis)}
|
|
854
|
+
${i18next.t('figure.button.mirror-' + axis)}
|
|
647
855
|
</button>
|
|
648
856
|
`
|
|
649
857
|
})}
|
|
650
858
|
<span sep></span>
|
|
651
|
-
<button title=${i18next.t('text.snap-explained')} @click=${() => this.ask('snap', 'x')}>
|
|
652
|
-
${i18next.t('button.snap')}
|
|
859
|
+
<button title=${i18next.t('figure.text.snap-explained')} @click=${() => this.ask('snap', 'x')}>
|
|
860
|
+
${i18next.t('figure.button.snap')}
|
|
653
861
|
</button>
|
|
654
862
|
</div>
|
|
655
863
|
</div>
|
|
656
864
|
|
|
657
865
|
<div field>
|
|
658
|
-
<label>${i18next.t('label.rotation-degrees')}</label>
|
|
866
|
+
<label>${i18next.t('figure.label.rotation-degrees')}</label>
|
|
659
867
|
<div triple>
|
|
660
868
|
${(['x', 'y', 'z'] as const).map(axis =>
|
|
661
869
|
this.number(axis.toUpperCase(), rotation[axis] ?? 0, value =>
|
|
@@ -676,7 +884,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
676
884
|
|
|
677
885
|
return html`
|
|
678
886
|
<section>
|
|
679
|
-
<h3>${i18next.t('label.shape-detail')}</h3>
|
|
887
|
+
<h3>${i18next.t('figure.label.shape-detail')}</h3>
|
|
680
888
|
${segmented ? this.renderSegments(part) : nothing}
|
|
681
889
|
${segmented ? this.renderKeepRound(part) : nothing}
|
|
682
890
|
${extruded ? this.renderExtruded(part) : nothing}
|
|
@@ -700,11 +908,11 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
700
908
|
|
|
701
909
|
return html`
|
|
702
910
|
<div field>
|
|
703
|
-
<label>${i18next.t('label.corner-round')}</label>
|
|
911
|
+
<label>${i18next.t('figure.label.corner-round')}</label>
|
|
704
912
|
${this.number('', part.shape?.round ?? 0, value => reshape({ round: value }))}
|
|
705
913
|
</div>
|
|
706
914
|
<div field>
|
|
707
|
-
<label>${i18next.t('label.hollow-wall')}</label>
|
|
915
|
+
<label>${i18next.t('figure.label.hollow-wall')}</label>
|
|
708
916
|
${this.number('', wall, value =>
|
|
709
917
|
reshape({ hollow: value > 0 ? { ...(hollow ?? {}), wall: value } : undefined })
|
|
710
918
|
)}
|
|
@@ -712,7 +920,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
712
920
|
${wall > 0
|
|
713
921
|
? html`
|
|
714
922
|
<div field>
|
|
715
|
-
<label>${i18next.t('label.hollow-floor')}</label>
|
|
923
|
+
<label>${i18next.t('figure.label.hollow-floor')}</label>
|
|
716
924
|
${this.number('', hollow?.floor ?? wall, value => reshape({ hollow: { ...hollow, wall, floor: value } }))}
|
|
717
925
|
</div>
|
|
718
926
|
`
|
|
@@ -739,15 +947,15 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
739
947
|
|
|
740
948
|
return html`
|
|
741
949
|
<div field>
|
|
742
|
-
<label>${i18next.t('label.keep-round')}</label>
|
|
950
|
+
<label>${i18next.t('figure.label.keep-round')}</label>
|
|
743
951
|
<div choices>
|
|
744
952
|
<button choice ?on=${on} @click=${() => this.put({ keepRound: undefined })}>
|
|
745
|
-
<b>${i18next.t('text.keep-round-on')}</b>
|
|
746
|
-
<small>${i18next.t('text.keep-round-cost')}</small>
|
|
953
|
+
<b>${i18next.t('figure.text.keep-round-on')}</b>
|
|
954
|
+
<small>${i18next.t('figure.text.keep-round-cost')}</small>
|
|
747
955
|
</button>
|
|
748
956
|
<button choice ?on=${!on} @click=${() => this.put({ keepRound: false })}>
|
|
749
|
-
<b>${i18next.t('text.keep-round-off')}</b>
|
|
750
|
-
<small>${i18next.t('text.keep-round-merged')}</small>
|
|
957
|
+
<b>${i18next.t('figure.text.keep-round-off')}</b>
|
|
958
|
+
<small>${i18next.t('figure.text.keep-round-merged')}</small>
|
|
751
959
|
</button>
|
|
752
960
|
</div>
|
|
753
961
|
</div>
|
|
@@ -759,7 +967,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
759
967
|
|
|
760
968
|
return html`
|
|
761
969
|
<div field>
|
|
762
|
-
<label>${i18next.t('label.segments')}</label>
|
|
970
|
+
<label>${i18next.t('figure.label.segments')}</label>
|
|
763
971
|
<div segments>
|
|
764
972
|
${SEGMENT_PRESETS.map(preset => {
|
|
765
973
|
const triangles = trianglesOf({ ...part, segments: preset })
|
|
@@ -767,14 +975,14 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
767
975
|
<button seg ?on=${current === preset} @click=${() => this.put({ segments: preset })}>
|
|
768
976
|
${this.silhouette(preset)}
|
|
769
977
|
<b>${preset}</b>
|
|
770
|
-
<small>${i18next.t('text.n-triangles', { count: triangles })}</small>
|
|
978
|
+
<small>${i18next.t('figure.text.n-triangles', { count: triangles })}</small>
|
|
771
979
|
</button>
|
|
772
980
|
`
|
|
773
981
|
})}
|
|
774
982
|
</div>
|
|
775
983
|
|
|
776
984
|
<details free>
|
|
777
|
-
<summary>${i18next.t('text.other-segment-count')}</summary>
|
|
985
|
+
<summary>${i18next.t('figure.text.other-segment-count')}</summary>
|
|
778
986
|
${this.number('', current, value => this.put({ segments: value }), 3)}
|
|
779
987
|
</details>
|
|
780
988
|
</div>
|
|
@@ -837,37 +1045,37 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
837
1045
|
|
|
838
1046
|
return html`
|
|
839
1047
|
<section>
|
|
840
|
-
<h3>${i18next.t('label.material')}</h3>
|
|
1048
|
+
<h3>${i18next.t('figure.label.part-material')}</h3>
|
|
841
1049
|
|
|
842
1050
|
<div field>
|
|
843
|
-
<label>${i18next.t('label.palette-token')}</label>
|
|
1051
|
+
<label>${i18next.t('figure.label.palette-token')}</label>
|
|
844
1052
|
<div swatches>
|
|
845
1053
|
${this.swatches(painted, material.token)}
|
|
846
1054
|
</div>
|
|
847
1055
|
${lit.length
|
|
848
1056
|
? html`
|
|
849
|
-
<label group>${i18next.t('label.status-colors')}</label>
|
|
1057
|
+
<label group>${i18next.t('figure.label.status-colors')}</label>
|
|
850
1058
|
<div swatches>
|
|
851
1059
|
${this.swatches(lit, material.token)}
|
|
852
1060
|
</div>
|
|
853
1061
|
`
|
|
854
1062
|
: ''}
|
|
855
|
-
<span token>${material.token ?? i18next.t('label.no-color-chosen')}</span>
|
|
1063
|
+
<span token>${material.token ?? i18next.t('figure.label.no-color-chosen')}</span>
|
|
856
1064
|
</div>
|
|
857
1065
|
|
|
858
1066
|
<div field>
|
|
859
|
-
<label>${i18next.t('label.preset')}</label>
|
|
1067
|
+
<label>${i18next.t('figure.label.preset')}</label>
|
|
860
1068
|
<select
|
|
861
1069
|
@change=${(e: Event) => {
|
|
862
1070
|
const value = (e.target as HTMLSelectElement).value
|
|
863
1071
|
this.putMaterial({ preset: (value || undefined) as MaterialPreset | undefined })
|
|
864
1072
|
}}
|
|
865
1073
|
>
|
|
866
|
-
<option value="" ?selected=${!material.preset}>${i18next.t('label.none')}</option>
|
|
1074
|
+
<option value="" ?selected=${!material.preset}>${i18next.t('figure.label.none')}</option>
|
|
867
1075
|
${MATERIAL_PRESETS.map(
|
|
868
1076
|
preset => html`
|
|
869
1077
|
<option value=${preset} ?selected=${preset === material.preset}>
|
|
870
|
-
${i18next.t('label.preset-' + preset)}
|
|
1078
|
+
${i18next.t('figure.label.preset-' + preset)}
|
|
871
1079
|
</option>
|
|
872
1080
|
`
|
|
873
1081
|
)}
|
|
@@ -881,7 +1089,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
881
1089
|
.checked=${material.flatShading ?? true}
|
|
882
1090
|
@change=${(e: Event) => this.putMaterial({ flatShading: (e.target as HTMLInputElement).checked })}
|
|
883
1091
|
/>
|
|
884
|
-
${i18next.t('label.flat-shading')}
|
|
1092
|
+
${i18next.t('figure.label.flat-shading')}
|
|
885
1093
|
</label>
|
|
886
1094
|
<label toggle>
|
|
887
1095
|
<input
|
|
@@ -889,12 +1097,11 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
889
1097
|
.checked=${!!material.transparent}
|
|
890
1098
|
@change=${(e: Event) => this.putMaterial({ transparent: (e.target as HTMLInputElement).checked })}
|
|
891
1099
|
/>
|
|
892
|
-
${i18next.t('label.transparent')}
|
|
1100
|
+
${i18next.t('figure.label.transparent')}
|
|
893
1101
|
</label>
|
|
894
1102
|
|
|
895
1103
|
<div field>
|
|
896
|
-
<label>${i18next.t('label.emissive')}</label>
|
|
897
|
-
<p cost>${i18next.t('text.emissive-explained')}</p>
|
|
1104
|
+
<label>${i18next.t('figure.label.emissive')}${this.help('figure.text.emissive-explained')}</label>
|
|
898
1105
|
|
|
899
1106
|
<div slider>
|
|
900
1107
|
<input
|
|
@@ -912,13 +1119,13 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
912
1119
|
${glow
|
|
913
1120
|
? html`
|
|
914
1121
|
<div field>
|
|
915
|
-
<label>${i18next.t('label.emissive-color')}</label>
|
|
1122
|
+
<label>${i18next.t('figure.label.emissive-color')}</label>
|
|
916
1123
|
<div swatches>
|
|
917
1124
|
<button
|
|
918
1125
|
swatch
|
|
919
1126
|
same
|
|
920
1127
|
?on=${!glow.token}
|
|
921
|
-
title=${i18next.t('label.same-as-off')}
|
|
1128
|
+
title=${i18next.t('figure.label.same-as-off')}
|
|
922
1129
|
style="--swatch:${(material.token && palette[material.token]) || palette['palette.neutral']}"
|
|
923
1130
|
@click=${() => this.putMaterial({ emissive: { ...glow, token: undefined } })}
|
|
924
1131
|
></button>
|
|
@@ -934,7 +1141,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
934
1141
|
`
|
|
935
1142
|
)}
|
|
936
1143
|
</div>
|
|
937
|
-
<span token>${glow.token ?? i18next.t('label.same-as-off')}</span>
|
|
1144
|
+
<span token>${glow.token ?? i18next.t('figure.label.same-as-off')}</span>
|
|
938
1145
|
</div>
|
|
939
1146
|
|
|
940
1147
|
<label toggle>
|
|
@@ -944,9 +1151,9 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
944
1151
|
@change=${(e: Event) =>
|
|
945
1152
|
this.putMaterial({ emissive: { ...glow, on: (e.target as HTMLInputElement).checked } })}
|
|
946
1153
|
/>
|
|
947
|
-
${i18next.t('label.on-by-default')}
|
|
1154
|
+
${i18next.t('figure.label.on-by-default')}
|
|
948
1155
|
</label>
|
|
949
|
-
|
|
1156
|
+
${this.help('figure.text.on-by-default-explained')}
|
|
950
1157
|
`
|
|
951
1158
|
: ''}
|
|
952
1159
|
|
|
@@ -977,27 +1184,23 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
977
1184
|
|
|
978
1185
|
return html`
|
|
979
1186
|
<section>
|
|
980
|
-
<h3>${i18next.t('label.sizing-rule')}</h3>
|
|
1187
|
+
<h3>${i18next.t('figure.label.sizing-rule')}${this.help('figure.text.sizing-' + sizing + '-explained')}</h3>
|
|
981
1188
|
<select @change=${(e: Event) => this.put({ sizing: (e.target as HTMLSelectElement).value as never })}>
|
|
982
1189
|
${SIZING_RULES.map(
|
|
983
1190
|
rule => html`
|
|
984
|
-
<option value=${rule} ?selected=${rule === sizing}>${i18next.t('label.sizing-' + rule)}</option>
|
|
1191
|
+
<option value=${rule} ?selected=${rule === sizing}>${i18next.t('figure.label.sizing-' + rule)}</option>
|
|
985
1192
|
`
|
|
986
1193
|
)}
|
|
987
1194
|
</select>
|
|
988
|
-
|
|
1195
|
+
${sizing === 'scale' ? nothing : this.help('figure.text.sizing-costs-one-draw-call')}
|
|
1196
|
+
${sizing === 'repeat' ? this.help('figure.text.repeat-edits-the-unit') : nothing}
|
|
989
1197
|
|
|
990
|
-
${
|
|
991
|
-
? nothing
|
|
992
|
-
: html`<p cost>${i18next.t('text.sizing-costs-one-draw-call')}</p>`}
|
|
993
|
-
${sizing === 'repeat'
|
|
994
|
-
? html`<p cost>${i18next.t('text.repeat-edits-the-unit')}</p>`
|
|
995
|
-
: nothing}
|
|
1198
|
+
${this.renderAnchors(part)}
|
|
996
1199
|
|
|
997
1200
|
${sizing === 'repeat'
|
|
998
1201
|
? html`
|
|
999
1202
|
<div field>
|
|
1000
|
-
<label>${i18next.t('label.repeat-axis')}</label>
|
|
1203
|
+
<label>${i18next.t('figure.label.repeat-axis')}</label>
|
|
1001
1204
|
<select
|
|
1002
1205
|
@change=${(e: Event) =>
|
|
1003
1206
|
this.put({ repeat: { ...repeat, axis: (e.target as HTMLSelectElement).value as Axis } })}
|
|
@@ -1008,16 +1211,176 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
1008
1211
|
</select>
|
|
1009
1212
|
</div>
|
|
1010
1213
|
<div field>
|
|
1011
|
-
<label>${i18next.t('label.repeat-pitch')}</label>
|
|
1214
|
+
<label>${i18next.t('figure.label.repeat-pitch')}${this.help('figure.text.repeat-pitch-explained')}</label>
|
|
1012
1215
|
${this.number('', repeat.pitch, value => this.put({ repeat: { ...repeat, pitch: value } }), 1)}
|
|
1013
|
-
<p cost>${i18next.t('text.repeat-pitch-explained')}</p>
|
|
1014
1216
|
</div>
|
|
1217
|
+
${this.renderRepeatCount(part)}
|
|
1015
1218
|
`
|
|
1016
1219
|
: nothing}
|
|
1017
1220
|
</section>
|
|
1018
1221
|
`
|
|
1019
1222
|
}
|
|
1020
1223
|
|
|
1224
|
+
/**
|
|
1225
|
+
* ── 몇 개가 되나 ──────────────────────────────────────────────────────────
|
|
1226
|
+
*
|
|
1227
|
+
* 저작 화면은 축과 피치만 받고 있었다. 형식이 가진 반복 설정이 그 둘뿐이므로 편집할 것은
|
|
1228
|
+
* 다 있었는데, **결과인 개수를 어디에도 표시하지 않았다.** 피치 300 을 적은 저작자가 몇 개가
|
|
1229
|
+
* 되는지 알려면 미리보기를 세어야 했다.
|
|
1230
|
+
*
|
|
1231
|
+
* 개수는 그리는 쪽이 쓰는 `repeatPlan` 을 그대로 불러 얻는다. 여기서 다시 계산하면 화면이
|
|
1232
|
+
* 말하는 개수와 실제로 그려지는 개수가 달라진다.
|
|
1233
|
+
*
|
|
1234
|
+
* ## 두 가지 반복이 있다
|
|
1235
|
+
*
|
|
1236
|
+
* 컨베이어의 롤러는 길어지면 **개수가 늘고** 한 칸 간격은 그대로다. 셀트레이의 셀은 커지면
|
|
1237
|
+
* **셀이 커지고** 개수는 그대로다 — 몇 셀짜리 트레이냐는 화면 크기와 무관하다. 그 갈림을
|
|
1238
|
+
* 이 축의 anchor 가 정하는데 화면에는 그 말이 없었다. 그래서 어느 쪽인지 함께 적는다.
|
|
1239
|
+
*/
|
|
1240
|
+
private renderRepeatCount(part: FigurePart) {
|
|
1241
|
+
const board = this.board
|
|
1242
|
+
if (!board) return nothing
|
|
1243
|
+
|
|
1244
|
+
const base = { x: board.width, y: board.depth, z: board.height }
|
|
1245
|
+
const plan = repeatPlanOfPart(part, { x: 1, y: 1, z: 1 }, base)
|
|
1246
|
+
if (!plan) return nothing
|
|
1247
|
+
|
|
1248
|
+
const grows = anchorOfPart(part, base, plan.axis) !== 'scale'
|
|
1249
|
+
|
|
1250
|
+
return html`
|
|
1251
|
+
<div field>
|
|
1252
|
+
<label>${i18next.t('figure.label.repeat-count')}</label>
|
|
1253
|
+
<p quiet>
|
|
1254
|
+
${i18next.t('figure.text.repeat-count-now', { count: plan.count })}
|
|
1255
|
+
${plan.count >= REPEAT_LIMIT
|
|
1256
|
+
? html`<br />${i18next.t('figure.text.repeat-count-capped', { limit: REPEAT_LIMIT })}`
|
|
1257
|
+
: ''}
|
|
1258
|
+
<br />${i18next.t(grows ? 'figure.text.repeat-grows-with-instance' : 'figure.text.repeat-keeps-count')}
|
|
1259
|
+
</p>
|
|
1260
|
+
</div>
|
|
1261
|
+
`
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* ── 어느 면을 붙잡나 ──────────────────────────────────────────────────────
|
|
1266
|
+
*
|
|
1267
|
+
* **이 자리가 없었다.** 형식은 축마다 `anchor` 를 지고 다니고(`figure-source.ts` 가 왕복시킨다),
|
|
1268
|
+
* 발행 게이트는 「valve-wheel 의 anchor.z 를 max 로 두라」고 사유까지 말하는데, 저작면에는 그
|
|
1269
|
+
* 값을 바꿀 자리가 없었다 — 고치라는 말만 있고 고칠 문이 없었다.
|
|
1270
|
+
*
|
|
1271
|
+
* ## 「자동」이 기본이다
|
|
1272
|
+
*
|
|
1273
|
+
* 형식에서 `anchor` 는 적지 않아도 된다. 안 적으면 **가까운 면**을 붙잡고, 표본 열넷 중 어느
|
|
1274
|
+
* 것도 이 값을 적지 않고 제대로 선다. 그래서 축마다 첫 항목이 「자동」이고, 지금 자동이 무엇으로
|
|
1275
|
+
* 읽히는지를 **함께 보여 준다** — 그것을 모르면 무엇을 바꾸는지 모른 채 고르게 된다.
|
|
1276
|
+
*
|
|
1277
|
+
* 자동이 무엇인지는 형식에게 묻는다(`anchorOfPart`). 여기서 「위 절반이면 max」를 다시 세면
|
|
1278
|
+
* 화면이 말하는 면과 실제로 그려지는 면이 갈린다.
|
|
1279
|
+
*
|
|
1280
|
+
* ## `scale` 인 부품에도 보여 준다
|
|
1281
|
+
*
|
|
1282
|
+
* 처음에는 감췄다 — 형식 주석에 「`sizing` 이 `scale` 인 부품에는 뜻이 없다」고 적혀 있어서다.
|
|
1283
|
+
* **그 문장은 기본값에 대한 것이다.** `anchorOf` 는 적힌 값을 가장 먼저 보고(`if (said) return
|
|
1284
|
+
* said`), 그 뒤에야 `sizing` 을 본다. 즉 `scale` 부품에도 면을 적으면 그것이 이긴다.
|
|
1285
|
+
*
|
|
1286
|
+
* 감춰 두면 어긋남이 난다: 발행 게이트가 막는 MIXER 의 `valve-wheel` 이 `scale` 부품이고,
|
|
1287
|
+
* 게이트의 사유는 「그 부품의 anchor.z 를 max 로 두라」다. 고치라는 자리에 문을 닫아 둔 셈이었다.
|
|
1288
|
+
*
|
|
1289
|
+
* 대신 `scale` 일 때는 자동이 무엇인지가 「함께 커진다」로 보이므로, 저작자가 그것을 바꾸는
|
|
1290
|
+
* 것임을 알 수 있다.
|
|
1291
|
+
*/
|
|
1292
|
+
/**
|
|
1293
|
+
* ── 도움말은 자리를 먹지 않는다 ─────────────────────────────────────────────
|
|
1294
|
+
*
|
|
1295
|
+
* 설명 문단이 열 줄 있었고 그 줄들이 패널 높이의 상당 부분이었다. 속성은 앞으로 더 늘어난다
|
|
1296
|
+
* (능력 · 포트 · 애니메이션 · 기준정보) — 늘 보이는 설명은 그 자리를 먼저 먹는다.
|
|
1297
|
+
*
|
|
1298
|
+
* 그래서 물음표로 접는다. **지우지 않는다** — 그 문장들은 「이걸 고르면 무엇이 달라지나」를
|
|
1299
|
+
* 말하는 것이고, 없으면 닫힌 목록을 고를 수 없다. 한 번 읽으면 되는 것이라 접어 두는 것이 맞다.
|
|
1300
|
+
*
|
|
1301
|
+
* 편 것은 **부품을 바꿔도 그대로 둔다**(`opened` 는 부품에 딸린 상태가 아니다). 같은 것을
|
|
1302
|
+
* 계속 고치는 사람이 매번 다시 펴야 하면 접은 것이 되레 방해가 된다.
|
|
1303
|
+
*/
|
|
1304
|
+
private help(key: string) {
|
|
1305
|
+
const shown = this.opened.has(key)
|
|
1306
|
+
|
|
1307
|
+
return html`
|
|
1308
|
+
<button
|
|
1309
|
+
help
|
|
1310
|
+
aria-label=${i18next.t('figure.label.help')}
|
|
1311
|
+
aria-expanded=${shown ? 'true' : 'false'}
|
|
1312
|
+
@click=${() => {
|
|
1313
|
+
shown ? this.opened.delete(key) : this.opened.add(key)
|
|
1314
|
+
this.requestUpdate()
|
|
1315
|
+
}}
|
|
1316
|
+
>
|
|
1317
|
+
<md-icon>${shown ? 'help' : 'help_outline'}</md-icon>
|
|
1318
|
+
</button>
|
|
1319
|
+
${shown ? html`<p hint>${i18next.t(key)}</p>` : nothing}
|
|
1320
|
+
`
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
private renderAnchors(part: FigurePart) {
|
|
1324
|
+
const base = this.board
|
|
1325
|
+
? { x: this.board.width, y: this.board.height, z: this.board.depth }
|
|
1326
|
+
: { x: 0, y: 0, z: 0 }
|
|
1327
|
+
|
|
1328
|
+
return html`
|
|
1329
|
+
<div field>
|
|
1330
|
+
<!-- 「anchor.x」로 적힌 사유를 화면에서 찾을 수 있게 칸 이름에 그 낱말을 남긴다 -->
|
|
1331
|
+
<label>${i18next.t('figure.label.anchor')} (anchor)${this.help('figure.text.anchor-explained')}</label>
|
|
1332
|
+
${(['x', 'y', 'z'] as const).map(axis => {
|
|
1333
|
+
const said = part.anchor?.[axis]
|
|
1334
|
+
const auto = anchorOfPart(part, base, axis)
|
|
1335
|
+
|
|
1336
|
+
/*
|
|
1337
|
+
**면 이름은 축마다 다르다.** 처음에는 min 을 「아래·왼쪽·뒤 면」으로 한 줄에 다 적었는데,
|
|
1338
|
+
x 줄에서 「아래」와 「뒤」는 관계가 없어 읽는 사람이 무엇을 고르는지 몰랐다.
|
|
1339
|
+
|
|
1340
|
+
그리고 이름씨가 아니라 **동작**으로 적는다 — 「오른쪽 면」이 아니라 「오른쪽 면에 붙어
|
|
1341
|
+
있는다」다. 「anchor.x 를 max 로 둔다」가 무슨 뜻인지 모르겠다는 말이 그 자리에서 나왔다.
|
|
1342
|
+
*/
|
|
1343
|
+
/*
|
|
1344
|
+
**값 이름을 함께 보인다.** 동작만 적었더니(「오른쪽 면에 붙어 있는다») 발행 게이트가
|
|
1345
|
+
「anchor.x 를 max 로 두라」고 말할 때 화면에서 그 낱말을 찾을 수 없었다 — 사용자가
|
|
1346
|
+
그 자리에서 지적했다.
|
|
1347
|
+
|
|
1348
|
+
`max` 는 정본에 저장되는 이름이고 서버 오류·형식 문서·게이트 사유가 다 그 이름을 쓴다.
|
|
1349
|
+
숨기면 안 되는 종류다. 동작이 먼저고 이름이 괄호로 따라온다.
|
|
1350
|
+
*/
|
|
1351
|
+
const nameOf = (rule: string) =>
|
|
1352
|
+
`${i18next.t(rule === 'min' || rule === 'max' ? `figure.label.anchor-${rule}-${axis}` : `figure.label.anchor-${rule}`)} (${rule})`
|
|
1353
|
+
|
|
1354
|
+
return html`
|
|
1355
|
+
<div cell>
|
|
1356
|
+
<span axis>${axis}</span>
|
|
1357
|
+
<select
|
|
1358
|
+
@change=${(e: Event) => {
|
|
1359
|
+
const value = (e.target as HTMLSelectElement).value
|
|
1360
|
+
const anchor = { ...(part.anchor ?? {}) }
|
|
1361
|
+
/* 「자동」은 칸을 지우는 것이다 — 빈 값을 넣어 두면 형식이 그것을 값으로 읽는다. */
|
|
1362
|
+
if (value) {
|
|
1363
|
+
anchor[axis] = value as AnchorRule
|
|
1364
|
+
} else {
|
|
1365
|
+
delete anchor[axis]
|
|
1366
|
+
}
|
|
1367
|
+
this.put({ anchor: Object.keys(anchor).length ? anchor : undefined })
|
|
1368
|
+
}}
|
|
1369
|
+
>
|
|
1370
|
+
<option value="" ?selected=${!said}>
|
|
1371
|
+
${i18next.t('figure.label.anchor-auto', { rule: nameOf(auto) })}
|
|
1372
|
+
</option>
|
|
1373
|
+
${ANCHOR_RULES.map(
|
|
1374
|
+
rule => html`<option value=${rule} ?selected=${rule === said}>${nameOf(rule)}</option>`
|
|
1375
|
+
)}
|
|
1376
|
+
</select>
|
|
1377
|
+
</div>
|
|
1378
|
+
`
|
|
1379
|
+
})}
|
|
1380
|
+
</div>
|
|
1381
|
+
`
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1021
1384
|
/**
|
|
1022
1385
|
* 숫자 칸.
|
|
1023
1386
|
*
|