@things-factory/figure-ui 10.1.5 → 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.
- package/client/graphql/index.ts +35 -3
- 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 +392 -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 +37 -9
- 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 +152 -66
- package/client/pages/figure-modeller-page.ts +260 -43
- package/client/types.ts +25 -0
- package/client/viewparts/figure-thumb.ts +172 -0
- package/dist-client/graphql/index.d.ts +8 -1
- package/dist-client/graphql/index.js +32 -3
- package/dist-client/graphql/index.js.map +1 -1
- 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 +84 -0
- package/dist-client/modeller/figure-inspector.js +379 -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 +1 -0
- package/dist-client/modeller/figure-side.js +36 -8
- 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 +1 -4
- package/dist-client/pages/figure-list-page.js +150 -63
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +45 -0
- package/dist-client/pages/figure-modeller-page.js +259 -43
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +23 -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 +168 -0
- package/dist-client/viewparts/figure-thumb.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -5
- package/test/i18n-prefix-guard.test.ts +129 -0
- package/test/korean-register-guard.test.ts +83 -0
- package/translations/en.json +307 -264
- package/translations/ja.json +275 -264
- package/translations/ko.json +307 -264
- package/translations/ms.json +275 -264
- package/translations/zh.json +275 -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
|
/*
|
|
@@ -484,6 +531,9 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
484
531
|
/** 도형의 표시 이름. 정본은 저작면이 들고, 여기서는 보여 주고 바꾼다(`figure-rename` 으로 알린다). */
|
|
485
532
|
@property({ type: String }) figureName = ''
|
|
486
533
|
|
|
534
|
+
/** 펴 둔 도움말. 부품을 바꿔도 유지된다 — 매번 다시 펴게 하지 않는다. */
|
|
535
|
+
@state() private opened = new Set<string>()
|
|
536
|
+
|
|
487
537
|
@property({ type: Object }) board?: BoardModel
|
|
488
538
|
/** 보드 위의 부품들. 씬 모델 그대로다. */
|
|
489
539
|
@property({ type: Array }) parts: PartModel[] = []
|
|
@@ -500,8 +550,8 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
500
550
|
return html`
|
|
501
551
|
${this.renderFigure()}
|
|
502
552
|
<section>
|
|
503
|
-
<h3>${i18next.t('label.properties')}</h3>
|
|
504
|
-
<p quiet>${i18next.t('text.select-a-part-to-edit')}</p>
|
|
553
|
+
<h3>${i18next.t('figure.label.properties')}</h3>
|
|
554
|
+
<p quiet>${i18next.t('figure.text.select-a-part-to-edit')}</p>
|
|
505
555
|
</section>
|
|
506
556
|
`
|
|
507
557
|
}
|
|
@@ -509,12 +559,125 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
509
559
|
// 여기부터는 사용자 용어다 — X·Y·Z, 위치는 부품 중심, 회전은 도(degree).
|
|
510
560
|
const shown = partToFigure(part, this.board)
|
|
511
561
|
|
|
562
|
+
/*
|
|
563
|
+
역할을 먼저 묻는다.
|
|
564
|
+
|
|
565
|
+
자리(slot)로 지정한 부품은 **그려지지 않는다.** 그 사실을 모른 채 재질부터 고르면 안 쓰일
|
|
566
|
+
색을 고르게 된다. 그리고 자리는 도형이 능력을 질 때만 뜻이 있으므로, 능력이 없으면 이
|
|
567
|
+
묶음은 아예 나오지 않는다 — 해 둬도 아무 일도 안 일어나는 칸을 보여 주지 않는다.
|
|
568
|
+
*/
|
|
569
|
+
return html`
|
|
570
|
+
${this.renderFigure()} ${this.renderIdentity(shown)} ${this.renderRole(shown)}
|
|
571
|
+
${this.renderPlacement(shown)} ${this.renderShape(shown)} ${this.renderMaterial(shown)}
|
|
572
|
+
${this.renderBehaviour(shown)}
|
|
573
|
+
`
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* ── 이 부품이 능력에서 맡는 자리 ────────────────────────────────────────────
|
|
578
|
+
*
|
|
579
|
+
* 「능력」 탭이 도형 전체가 무엇을 하는지 정하고, 어느 부품이 그것을 받쳐 주는지는 여기서
|
|
580
|
+
* 정한다. **부품을 고치는 곳이 한 군데여야** 하기 때문이다 — 목록을 따로 두면 같은 부품을
|
|
581
|
+
* 두 화면에서 고치게 되고, 저작자는 어느 쪽이 이기는지 매번 생각해야 한다.
|
|
582
|
+
*
|
|
583
|
+
* 칸 수는 `repeat` 가 있으면 받지 않는다. 반복하는 자리의 칸 수는 **인스턴스 크기**가 정한다 —
|
|
584
|
+
* 컨베이어가 길어지면 실을 자리가 늘어나는 것이 그것이고, 여기 적은 값은 안 쓰인다.
|
|
585
|
+
*/
|
|
586
|
+
private renderRole(part: FigurePart) {
|
|
587
|
+
if (!this.board?.capabilities?.length) return nothing
|
|
588
|
+
|
|
589
|
+
const roles = part.capability?.roles ?? []
|
|
590
|
+
const repeats = part.sizing === 'repeat' && !!part.repeat
|
|
591
|
+
|
|
512
592
|
return html`
|
|
513
|
-
|
|
514
|
-
|
|
593
|
+
<section>
|
|
594
|
+
<h3>${i18next.t('figure.label.role-in-capability')}${this.help('figure.text.anchors-are-not-drawn')}</h3>
|
|
595
|
+
|
|
596
|
+
${(PART_ROLES as readonly PartRole[]).map(
|
|
597
|
+
one => html`
|
|
598
|
+
<label toggle>
|
|
599
|
+
<input
|
|
600
|
+
type="checkbox"
|
|
601
|
+
.checked=${live(roles.includes(one))}
|
|
602
|
+
@change=${(e: Event) => this.toggleRole(one, (e.target as HTMLInputElement).checked)}
|
|
603
|
+
/>
|
|
604
|
+
${i18next.t('figure.label.role-' + one)}
|
|
605
|
+
</label>
|
|
606
|
+
`
|
|
607
|
+
)}
|
|
608
|
+
${roles.length === 0 ? html`<p quiet>${i18next.t('figure.text.role-none-means-drawn')}</p>` : nothing}
|
|
609
|
+
|
|
610
|
+
${roles.includes('slot')
|
|
611
|
+
? html`
|
|
612
|
+
<div field>
|
|
613
|
+
<label>${i18next.t('figure.label.accepts')}${this.help('figure.text.accepts-explained')}</label>
|
|
614
|
+
<input
|
|
615
|
+
.value=${(part.capability?.accepts ?? []).join(', ')}
|
|
616
|
+
placeholder=${i18next.t('figure.text.accepts-anything')}
|
|
617
|
+
@change=${(e: Event) => this.setAccepts((e.target as HTMLInputElement).value)}
|
|
618
|
+
/>
|
|
619
|
+
</div>
|
|
620
|
+
<div field>
|
|
621
|
+
<label>${i18next.t('figure.label.slot-capacity')}</label>
|
|
622
|
+
${repeats
|
|
623
|
+
? html`<p quiet>${i18next.t('figure.text.capacity-from-repeat')}</p>`
|
|
624
|
+
: this.number('', part.capability?.capacity ?? 1, value => this.setCapacity(value), 1)}
|
|
625
|
+
</div>
|
|
626
|
+
`
|
|
627
|
+
: nothing}
|
|
628
|
+
</section>
|
|
515
629
|
`
|
|
516
630
|
}
|
|
517
631
|
|
|
632
|
+
/**
|
|
633
|
+
* 역할 하나를 켜고 끈다.
|
|
634
|
+
*
|
|
635
|
+
* **여럿을 함께 맡을 수 있다.** AGV 의 상판은 물건을 놓는 자리이면서 그 자리로 물건이
|
|
636
|
+
* 들어오고 나간다. 그래서 하나를 고르는 선택 상자가 아니라 확인 상자다.
|
|
637
|
+
*
|
|
638
|
+
* 마지막 역할을 끄면 `capability` 를 통째로 지운다. 빈 배열을 남기면 형식이 오류로 보고,
|
|
639
|
+
* 저작자가 보기에는 아무 역할도 없는 평범한 부품인데 발행이 막힌다.
|
|
640
|
+
*/
|
|
641
|
+
private toggleRole(role: PartRole, on: boolean) {
|
|
642
|
+
this.change(part => {
|
|
643
|
+
const roles = (part.capability?.roles ?? []).filter(one => one !== role)
|
|
644
|
+
if (on) roles.push(role)
|
|
645
|
+
|
|
646
|
+
if (roles.length === 0) {
|
|
647
|
+
const { capability, ...rest } = part
|
|
648
|
+
return rest as FigurePart
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// 형식이 적은 순서를 지킨다 — 켠 순서대로 두면 같은 뜻이 다른 값으로 저장된다.
|
|
652
|
+
const ordered = (PART_ROLES as readonly PartRole[]).filter(one => roles.includes(one))
|
|
653
|
+
return { ...part, capability: { ...part.capability, roles: ordered } }
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private setAccepts(raw: string) {
|
|
658
|
+
const accepts = raw
|
|
659
|
+
.split(',')
|
|
660
|
+
.map(one => one.trim())
|
|
661
|
+
.filter(one => one.length > 0)
|
|
662
|
+
|
|
663
|
+
this.change(part => {
|
|
664
|
+
const spec = { ...part.capability, roles: part.capability?.roles ?? ['slot' as PartRole] }
|
|
665
|
+
// 빈 목록은 제한하지 않는다는 뜻이다. 빈 배열을 저장하면 아무것도 못 받는 것으로 읽힌다.
|
|
666
|
+
if (accepts.length > 0) spec.accepts = accepts
|
|
667
|
+
else delete spec.accepts
|
|
668
|
+
return { ...part, capability: spec as NonNullable<FigurePart['capability']> }
|
|
669
|
+
})
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private setCapacity(value: number) {
|
|
673
|
+
this.change(part => {
|
|
674
|
+
const spec = { ...part.capability, roles: part.capability?.roles ?? ['slot' as PartRole] }
|
|
675
|
+
if (Number.isFinite(value) && value >= 1) spec.capacity = Math.floor(value)
|
|
676
|
+
else delete spec.capacity
|
|
677
|
+
return { ...part, capability: spec as NonNullable<FigurePart['capability']> }
|
|
678
|
+
})
|
|
679
|
+
}
|
|
680
|
+
|
|
518
681
|
/**
|
|
519
682
|
* 도형 자체 — 이름과 타입.
|
|
520
683
|
*
|
|
@@ -531,12 +694,12 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
531
694
|
private renderFigure() {
|
|
532
695
|
return html`
|
|
533
696
|
<section>
|
|
534
|
-
<h3>${i18next.t('label.figure')}</h3>
|
|
697
|
+
<h3>${i18next.t('figure.label.figure')}</h3>
|
|
535
698
|
<div field>
|
|
536
|
-
<label>${i18next.t('label.name')}</label>
|
|
699
|
+
<label>${i18next.t('figure.label.name')}</label>
|
|
537
700
|
<input
|
|
538
701
|
.value=${this.figureName}
|
|
539
|
-
placeholder=${i18next.t('text.figure-display-name')}
|
|
702
|
+
placeholder=${i18next.t('figure.text.figure-display-name')}
|
|
540
703
|
@change=${(e: Event) =>
|
|
541
704
|
this.dispatchEvent(
|
|
542
705
|
new CustomEvent('figure-rename', {
|
|
@@ -548,8 +711,8 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
548
711
|
/>
|
|
549
712
|
</div>
|
|
550
713
|
<div field>
|
|
551
|
-
<label>${i18next.t('label.type')}</label>
|
|
552
|
-
<code fixed title=${i18next.t('text.type-name-cannot-change')}>
|
|
714
|
+
<label>${i18next.t('figure.label.figure-type')}${this.help('figure.text.type-name-cannot-change')}</label>
|
|
715
|
+
<code fixed title=${i18next.t('figure.text.type-name-cannot-change')}>
|
|
553
716
|
<md-icon>lock</md-icon>${this.board?.figureType ?? ''}
|
|
554
717
|
</code>
|
|
555
718
|
</div>
|
|
@@ -557,7 +720,6 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
557
720
|
왜 못 고치는지 **적어 둔다.** 전에는 tooltip 에만 있었고, 그러면 눌러 보고 안 되는 것을
|
|
558
721
|
알게 된다 — 화면이 안 되는 이유를 말하지 않으면 사용자가 자기 실수라고 생각한다.
|
|
559
722
|
-->
|
|
560
|
-
<p quiet>${i18next.t('text.type-name-cannot-change')}</p>
|
|
561
723
|
</section>
|
|
562
724
|
`
|
|
563
725
|
}
|
|
@@ -565,22 +727,21 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
565
727
|
private renderIdentity(part: FigurePart) {
|
|
566
728
|
return html`
|
|
567
729
|
<section>
|
|
568
|
-
<h3>${i18next.t('label.part')}</h3>
|
|
730
|
+
<h3>${i18next.t('figure.label.part')}</h3>
|
|
569
731
|
<div field>
|
|
570
|
-
<label>${i18next.t('label.name')}</label>
|
|
732
|
+
<label>${i18next.t('figure.label.name')}${this.help('figure.text.renaming-a-part-breaks-bindings')}</label>
|
|
571
733
|
<input
|
|
572
734
|
.value=${part.name}
|
|
573
735
|
@change=${(e: Event) => this.put({ name: (e.target as HTMLInputElement).value })}
|
|
574
736
|
/>
|
|
575
|
-
<p warn>${i18next.t('text.renaming-a-part-breaks-bindings')}</p>
|
|
576
737
|
</div>
|
|
577
738
|
<div field>
|
|
578
|
-
<label>${i18next.t('label.shape')}</label>
|
|
739
|
+
<label>${i18next.t('figure.label.shape')}</label>
|
|
579
740
|
<select @change=${(e: Event) => this.changePrimitive((e.target as HTMLSelectElement).value as PrimitiveKind)}>
|
|
580
741
|
${PRIMITIVE_KINDS.map(
|
|
581
742
|
kind => html`
|
|
582
743
|
<option value=${kind} ?selected=${kind === part.primitive}>
|
|
583
|
-
${i18next.t('label.primitive-' + kind)}
|
|
744
|
+
${i18next.t('figure.label.primitive-' + kind)}
|
|
584
745
|
</option>
|
|
585
746
|
`
|
|
586
747
|
)}
|
|
@@ -596,9 +757,9 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
596
757
|
|
|
597
758
|
return html`
|
|
598
759
|
<section>
|
|
599
|
-
<h3>${i18next.t('label.placement')}</h3>
|
|
760
|
+
<h3>${i18next.t('figure.label.placement')}</h3>
|
|
600
761
|
|
|
601
|
-
<label>${i18next.t('label.position')}</label>
|
|
762
|
+
<label>${i18next.t('figure.label.part-position')}</label>
|
|
602
763
|
<div triple>
|
|
603
764
|
${(['x', 'y', 'z'] as const).map(axis =>
|
|
604
765
|
this.number(axis.toUpperCase(), t.position[axis], value =>
|
|
@@ -608,7 +769,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
608
769
|
</div>
|
|
609
770
|
|
|
610
771
|
<div field>
|
|
611
|
-
<label>${i18next.t('label.size')}</label>
|
|
772
|
+
<label>${i18next.t('figure.label.part-size')}</label>
|
|
612
773
|
<div triple>
|
|
613
774
|
${(['x', 'y', 'z'] as const).map(axis =>
|
|
614
775
|
this.number(
|
|
@@ -622,16 +783,16 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
622
783
|
</div>
|
|
623
784
|
|
|
624
785
|
<div field>
|
|
625
|
-
<label>${i18next.t('label.tools')}</label>
|
|
786
|
+
<label>${i18next.t('figure.label.tools')}</label>
|
|
626
787
|
<div tools>
|
|
627
|
-
<button title=${i18next.t('text.center-on-x')} @click=${() => this.ask('center', 'x')}>
|
|
628
|
-
${i18next.t('button.center-x')}
|
|
788
|
+
<button title=${i18next.t('figure.text.center-on-x')} @click=${() => this.ask('center', 'x')}>
|
|
789
|
+
${i18next.t('figure.button.center-x')}
|
|
629
790
|
</button>
|
|
630
|
-
<button title=${i18next.t('text.center-on-z')} @click=${() => this.ask('center', 'z')}>
|
|
631
|
-
${i18next.t('button.center-z')}
|
|
791
|
+
<button title=${i18next.t('figure.text.center-on-z')} @click=${() => this.ask('center', 'z')}>
|
|
792
|
+
${i18next.t('figure.button.center-z')}
|
|
632
793
|
</button>
|
|
633
|
-
<button title=${i18next.t('text.put-on-floor')} @click=${() => this.ask('floor', 'y')}>
|
|
634
|
-
${i18next.t('button.to-floor')}
|
|
794
|
+
<button title=${i18next.t('figure.text.put-on-floor')} @click=${() => this.ask('floor', 'y')}>
|
|
795
|
+
${i18next.t('figure.button.to-floor')}
|
|
635
796
|
</button>
|
|
636
797
|
<span sep></span>
|
|
637
798
|
${(['x', 'z'] as const).map(axis => {
|
|
@@ -640,22 +801,22 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
640
801
|
return html`
|
|
641
802
|
<button
|
|
642
803
|
?disabled=${!able}
|
|
643
|
-
title=${i18next.t(able ? 'text.mirror-explained' : 'text.mirror-would-overlap')}
|
|
804
|
+
title=${i18next.t(able ? 'figure.text.mirror-explained' : 'figure.text.mirror-would-overlap')}
|
|
644
805
|
@click=${() => this.ask('mirror', axis)}
|
|
645
806
|
>
|
|
646
|
-
${i18next.t('button.mirror-' + axis)}
|
|
807
|
+
${i18next.t('figure.button.mirror-' + axis)}
|
|
647
808
|
</button>
|
|
648
809
|
`
|
|
649
810
|
})}
|
|
650
811
|
<span sep></span>
|
|
651
|
-
<button title=${i18next.t('text.snap-explained')} @click=${() => this.ask('snap', 'x')}>
|
|
652
|
-
${i18next.t('button.snap')}
|
|
812
|
+
<button title=${i18next.t('figure.text.snap-explained')} @click=${() => this.ask('snap', 'x')}>
|
|
813
|
+
${i18next.t('figure.button.snap')}
|
|
653
814
|
</button>
|
|
654
815
|
</div>
|
|
655
816
|
</div>
|
|
656
817
|
|
|
657
818
|
<div field>
|
|
658
|
-
<label>${i18next.t('label.rotation-degrees')}</label>
|
|
819
|
+
<label>${i18next.t('figure.label.rotation-degrees')}</label>
|
|
659
820
|
<div triple>
|
|
660
821
|
${(['x', 'y', 'z'] as const).map(axis =>
|
|
661
822
|
this.number(axis.toUpperCase(), rotation[axis] ?? 0, value =>
|
|
@@ -676,7 +837,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
676
837
|
|
|
677
838
|
return html`
|
|
678
839
|
<section>
|
|
679
|
-
<h3>${i18next.t('label.shape-detail')}</h3>
|
|
840
|
+
<h3>${i18next.t('figure.label.shape-detail')}</h3>
|
|
680
841
|
${segmented ? this.renderSegments(part) : nothing}
|
|
681
842
|
${segmented ? this.renderKeepRound(part) : nothing}
|
|
682
843
|
${extruded ? this.renderExtruded(part) : nothing}
|
|
@@ -700,11 +861,11 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
700
861
|
|
|
701
862
|
return html`
|
|
702
863
|
<div field>
|
|
703
|
-
<label>${i18next.t('label.corner-round')}</label>
|
|
864
|
+
<label>${i18next.t('figure.label.corner-round')}</label>
|
|
704
865
|
${this.number('', part.shape?.round ?? 0, value => reshape({ round: value }))}
|
|
705
866
|
</div>
|
|
706
867
|
<div field>
|
|
707
|
-
<label>${i18next.t('label.hollow-wall')}</label>
|
|
868
|
+
<label>${i18next.t('figure.label.hollow-wall')}</label>
|
|
708
869
|
${this.number('', wall, value =>
|
|
709
870
|
reshape({ hollow: value > 0 ? { ...(hollow ?? {}), wall: value } : undefined })
|
|
710
871
|
)}
|
|
@@ -712,7 +873,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
712
873
|
${wall > 0
|
|
713
874
|
? html`
|
|
714
875
|
<div field>
|
|
715
|
-
<label>${i18next.t('label.hollow-floor')}</label>
|
|
876
|
+
<label>${i18next.t('figure.label.hollow-floor')}</label>
|
|
716
877
|
${this.number('', hollow?.floor ?? wall, value => reshape({ hollow: { ...hollow, wall, floor: value } }))}
|
|
717
878
|
</div>
|
|
718
879
|
`
|
|
@@ -739,15 +900,15 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
739
900
|
|
|
740
901
|
return html`
|
|
741
902
|
<div field>
|
|
742
|
-
<label>${i18next.t('label.keep-round')}</label>
|
|
903
|
+
<label>${i18next.t('figure.label.keep-round')}</label>
|
|
743
904
|
<div choices>
|
|
744
905
|
<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>
|
|
906
|
+
<b>${i18next.t('figure.text.keep-round-on')}</b>
|
|
907
|
+
<small>${i18next.t('figure.text.keep-round-cost')}</small>
|
|
747
908
|
</button>
|
|
748
909
|
<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>
|
|
910
|
+
<b>${i18next.t('figure.text.keep-round-off')}</b>
|
|
911
|
+
<small>${i18next.t('figure.text.keep-round-merged')}</small>
|
|
751
912
|
</button>
|
|
752
913
|
</div>
|
|
753
914
|
</div>
|
|
@@ -759,7 +920,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
759
920
|
|
|
760
921
|
return html`
|
|
761
922
|
<div field>
|
|
762
|
-
<label>${i18next.t('label.segments')}</label>
|
|
923
|
+
<label>${i18next.t('figure.label.segments')}</label>
|
|
763
924
|
<div segments>
|
|
764
925
|
${SEGMENT_PRESETS.map(preset => {
|
|
765
926
|
const triangles = trianglesOf({ ...part, segments: preset })
|
|
@@ -767,14 +928,14 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
767
928
|
<button seg ?on=${current === preset} @click=${() => this.put({ segments: preset })}>
|
|
768
929
|
${this.silhouette(preset)}
|
|
769
930
|
<b>${preset}</b>
|
|
770
|
-
<small>${i18next.t('text.n-triangles', { count: triangles })}</small>
|
|
931
|
+
<small>${i18next.t('figure.text.n-triangles', { count: triangles })}</small>
|
|
771
932
|
</button>
|
|
772
933
|
`
|
|
773
934
|
})}
|
|
774
935
|
</div>
|
|
775
936
|
|
|
776
937
|
<details free>
|
|
777
|
-
<summary>${i18next.t('text.other-segment-count')}</summary>
|
|
938
|
+
<summary>${i18next.t('figure.text.other-segment-count')}</summary>
|
|
778
939
|
${this.number('', current, value => this.put({ segments: value }), 3)}
|
|
779
940
|
</details>
|
|
780
941
|
</div>
|
|
@@ -837,37 +998,37 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
837
998
|
|
|
838
999
|
return html`
|
|
839
1000
|
<section>
|
|
840
|
-
<h3>${i18next.t('label.material')}</h3>
|
|
1001
|
+
<h3>${i18next.t('figure.label.part-material')}</h3>
|
|
841
1002
|
|
|
842
1003
|
<div field>
|
|
843
|
-
<label>${i18next.t('label.palette-token')}</label>
|
|
1004
|
+
<label>${i18next.t('figure.label.palette-token')}</label>
|
|
844
1005
|
<div swatches>
|
|
845
1006
|
${this.swatches(painted, material.token)}
|
|
846
1007
|
</div>
|
|
847
1008
|
${lit.length
|
|
848
1009
|
? html`
|
|
849
|
-
<label group>${i18next.t('label.status-colors')}</label>
|
|
1010
|
+
<label group>${i18next.t('figure.label.status-colors')}</label>
|
|
850
1011
|
<div swatches>
|
|
851
1012
|
${this.swatches(lit, material.token)}
|
|
852
1013
|
</div>
|
|
853
1014
|
`
|
|
854
1015
|
: ''}
|
|
855
|
-
<span token>${material.token ?? i18next.t('label.no-color-chosen')}</span>
|
|
1016
|
+
<span token>${material.token ?? i18next.t('figure.label.no-color-chosen')}</span>
|
|
856
1017
|
</div>
|
|
857
1018
|
|
|
858
1019
|
<div field>
|
|
859
|
-
<label>${i18next.t('label.preset')}</label>
|
|
1020
|
+
<label>${i18next.t('figure.label.preset')}</label>
|
|
860
1021
|
<select
|
|
861
1022
|
@change=${(e: Event) => {
|
|
862
1023
|
const value = (e.target as HTMLSelectElement).value
|
|
863
1024
|
this.putMaterial({ preset: (value || undefined) as MaterialPreset | undefined })
|
|
864
1025
|
}}
|
|
865
1026
|
>
|
|
866
|
-
<option value="" ?selected=${!material.preset}>${i18next.t('label.none')}</option>
|
|
1027
|
+
<option value="" ?selected=${!material.preset}>${i18next.t('figure.label.none')}</option>
|
|
867
1028
|
${MATERIAL_PRESETS.map(
|
|
868
1029
|
preset => html`
|
|
869
1030
|
<option value=${preset} ?selected=${preset === material.preset}>
|
|
870
|
-
${i18next.t('label.preset-' + preset)}
|
|
1031
|
+
${i18next.t('figure.label.preset-' + preset)}
|
|
871
1032
|
</option>
|
|
872
1033
|
`
|
|
873
1034
|
)}
|
|
@@ -881,7 +1042,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
881
1042
|
.checked=${material.flatShading ?? true}
|
|
882
1043
|
@change=${(e: Event) => this.putMaterial({ flatShading: (e.target as HTMLInputElement).checked })}
|
|
883
1044
|
/>
|
|
884
|
-
${i18next.t('label.flat-shading')}
|
|
1045
|
+
${i18next.t('figure.label.flat-shading')}
|
|
885
1046
|
</label>
|
|
886
1047
|
<label toggle>
|
|
887
1048
|
<input
|
|
@@ -889,12 +1050,11 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
889
1050
|
.checked=${!!material.transparent}
|
|
890
1051
|
@change=${(e: Event) => this.putMaterial({ transparent: (e.target as HTMLInputElement).checked })}
|
|
891
1052
|
/>
|
|
892
|
-
${i18next.t('label.transparent')}
|
|
1053
|
+
${i18next.t('figure.label.transparent')}
|
|
893
1054
|
</label>
|
|
894
1055
|
|
|
895
1056
|
<div field>
|
|
896
|
-
<label>${i18next.t('label.emissive')}</label>
|
|
897
|
-
<p cost>${i18next.t('text.emissive-explained')}</p>
|
|
1057
|
+
<label>${i18next.t('figure.label.emissive')}${this.help('figure.text.emissive-explained')}</label>
|
|
898
1058
|
|
|
899
1059
|
<div slider>
|
|
900
1060
|
<input
|
|
@@ -912,13 +1072,13 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
912
1072
|
${glow
|
|
913
1073
|
? html`
|
|
914
1074
|
<div field>
|
|
915
|
-
<label>${i18next.t('label.emissive-color')}</label>
|
|
1075
|
+
<label>${i18next.t('figure.label.emissive-color')}</label>
|
|
916
1076
|
<div swatches>
|
|
917
1077
|
<button
|
|
918
1078
|
swatch
|
|
919
1079
|
same
|
|
920
1080
|
?on=${!glow.token}
|
|
921
|
-
title=${i18next.t('label.same-as-off')}
|
|
1081
|
+
title=${i18next.t('figure.label.same-as-off')}
|
|
922
1082
|
style="--swatch:${(material.token && palette[material.token]) || palette['palette.neutral']}"
|
|
923
1083
|
@click=${() => this.putMaterial({ emissive: { ...glow, token: undefined } })}
|
|
924
1084
|
></button>
|
|
@@ -934,7 +1094,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
934
1094
|
`
|
|
935
1095
|
)}
|
|
936
1096
|
</div>
|
|
937
|
-
<span token>${glow.token ?? i18next.t('label.same-as-off')}</span>
|
|
1097
|
+
<span token>${glow.token ?? i18next.t('figure.label.same-as-off')}</span>
|
|
938
1098
|
</div>
|
|
939
1099
|
|
|
940
1100
|
<label toggle>
|
|
@@ -944,9 +1104,9 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
944
1104
|
@change=${(e: Event) =>
|
|
945
1105
|
this.putMaterial({ emissive: { ...glow, on: (e.target as HTMLInputElement).checked } })}
|
|
946
1106
|
/>
|
|
947
|
-
${i18next.t('label.on-by-default')}
|
|
1107
|
+
${i18next.t('figure.label.on-by-default')}
|
|
948
1108
|
</label>
|
|
949
|
-
|
|
1109
|
+
${this.help('figure.text.on-by-default-explained')}
|
|
950
1110
|
`
|
|
951
1111
|
: ''}
|
|
952
1112
|
|
|
@@ -977,27 +1137,23 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
977
1137
|
|
|
978
1138
|
return html`
|
|
979
1139
|
<section>
|
|
980
|
-
<h3>${i18next.t('label.sizing-rule')}</h3>
|
|
1140
|
+
<h3>${i18next.t('figure.label.sizing-rule')}${this.help('figure.text.sizing-' + sizing + '-explained')}</h3>
|
|
981
1141
|
<select @change=${(e: Event) => this.put({ sizing: (e.target as HTMLSelectElement).value as never })}>
|
|
982
1142
|
${SIZING_RULES.map(
|
|
983
1143
|
rule => html`
|
|
984
|
-
<option value=${rule} ?selected=${rule === sizing}>${i18next.t('label.sizing-' + rule)}</option>
|
|
1144
|
+
<option value=${rule} ?selected=${rule === sizing}>${i18next.t('figure.label.sizing-' + rule)}</option>
|
|
985
1145
|
`
|
|
986
1146
|
)}
|
|
987
1147
|
</select>
|
|
988
|
-
|
|
1148
|
+
${sizing === 'scale' ? nothing : this.help('figure.text.sizing-costs-one-draw-call')}
|
|
1149
|
+
${sizing === 'repeat' ? this.help('figure.text.repeat-edits-the-unit') : nothing}
|
|
989
1150
|
|
|
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}
|
|
1151
|
+
${this.renderAnchors(part)}
|
|
996
1152
|
|
|
997
1153
|
${sizing === 'repeat'
|
|
998
1154
|
? html`
|
|
999
1155
|
<div field>
|
|
1000
|
-
<label>${i18next.t('label.repeat-axis')}</label>
|
|
1156
|
+
<label>${i18next.t('figure.label.repeat-axis')}</label>
|
|
1001
1157
|
<select
|
|
1002
1158
|
@change=${(e: Event) =>
|
|
1003
1159
|
this.put({ repeat: { ...repeat, axis: (e.target as HTMLSelectElement).value as Axis } })}
|
|
@@ -1008,16 +1164,176 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
1008
1164
|
</select>
|
|
1009
1165
|
</div>
|
|
1010
1166
|
<div field>
|
|
1011
|
-
<label>${i18next.t('label.repeat-pitch')}</label>
|
|
1167
|
+
<label>${i18next.t('figure.label.repeat-pitch')}${this.help('figure.text.repeat-pitch-explained')}</label>
|
|
1012
1168
|
${this.number('', repeat.pitch, value => this.put({ repeat: { ...repeat, pitch: value } }), 1)}
|
|
1013
|
-
<p cost>${i18next.t('text.repeat-pitch-explained')}</p>
|
|
1014
1169
|
</div>
|
|
1170
|
+
${this.renderRepeatCount(part)}
|
|
1015
1171
|
`
|
|
1016
1172
|
: nothing}
|
|
1017
1173
|
</section>
|
|
1018
1174
|
`
|
|
1019
1175
|
}
|
|
1020
1176
|
|
|
1177
|
+
/**
|
|
1178
|
+
* ── 몇 개가 되나 ──────────────────────────────────────────────────────────
|
|
1179
|
+
*
|
|
1180
|
+
* 저작 화면은 축과 피치만 받고 있었다. 형식이 가진 반복 설정이 그 둘뿐이므로 편집할 것은
|
|
1181
|
+
* 다 있었는데, **결과인 개수를 어디에도 표시하지 않았다.** 피치 300 을 적은 저작자가 몇 개가
|
|
1182
|
+
* 되는지 알려면 미리보기를 세어야 했다.
|
|
1183
|
+
*
|
|
1184
|
+
* 개수는 그리는 쪽이 쓰는 `repeatPlan` 을 그대로 불러 얻는다. 여기서 다시 계산하면 화면이
|
|
1185
|
+
* 말하는 개수와 실제로 그려지는 개수가 달라진다.
|
|
1186
|
+
*
|
|
1187
|
+
* ## 두 가지 반복이 있다
|
|
1188
|
+
*
|
|
1189
|
+
* 컨베이어의 롤러는 길어지면 **개수가 늘고** 한 칸 간격은 그대로다. 셀트레이의 셀은 커지면
|
|
1190
|
+
* **셀이 커지고** 개수는 그대로다 — 몇 셀짜리 트레이냐는 화면 크기와 무관하다. 그 갈림을
|
|
1191
|
+
* 이 축의 anchor 가 정하는데 화면에는 그 말이 없었다. 그래서 어느 쪽인지 함께 적는다.
|
|
1192
|
+
*/
|
|
1193
|
+
private renderRepeatCount(part: FigurePart) {
|
|
1194
|
+
const board = this.board
|
|
1195
|
+
if (!board) return nothing
|
|
1196
|
+
|
|
1197
|
+
const base = { x: board.width, y: board.depth, z: board.height }
|
|
1198
|
+
const plan = repeatPlanOfPart(part, { x: 1, y: 1, z: 1 }, base)
|
|
1199
|
+
if (!plan) return nothing
|
|
1200
|
+
|
|
1201
|
+
const grows = anchorOfPart(part, base, plan.axis) !== 'scale'
|
|
1202
|
+
|
|
1203
|
+
return html`
|
|
1204
|
+
<div field>
|
|
1205
|
+
<label>${i18next.t('figure.label.repeat-count')}</label>
|
|
1206
|
+
<p quiet>
|
|
1207
|
+
${i18next.t('figure.text.repeat-count-now', { count: plan.count })}
|
|
1208
|
+
${plan.count >= REPEAT_LIMIT
|
|
1209
|
+
? html`<br />${i18next.t('figure.text.repeat-count-capped', { limit: REPEAT_LIMIT })}`
|
|
1210
|
+
: ''}
|
|
1211
|
+
<br />${i18next.t(grows ? 'figure.text.repeat-grows-with-instance' : 'figure.text.repeat-keeps-count')}
|
|
1212
|
+
</p>
|
|
1213
|
+
</div>
|
|
1214
|
+
`
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* ── 어느 면을 붙잡나 ──────────────────────────────────────────────────────
|
|
1219
|
+
*
|
|
1220
|
+
* **이 자리가 없었다.** 형식은 축마다 `anchor` 를 지고 다니고(`figure-source.ts` 가 왕복시킨다),
|
|
1221
|
+
* 발행 게이트는 「valve-wheel 의 anchor.z 를 max 로 두라」고 사유까지 말하는데, 저작면에는 그
|
|
1222
|
+
* 값을 바꿀 자리가 없었다 — 고치라는 말만 있고 고칠 문이 없었다.
|
|
1223
|
+
*
|
|
1224
|
+
* ## 「자동」이 기본이다
|
|
1225
|
+
*
|
|
1226
|
+
* 형식에서 `anchor` 는 적지 않아도 된다. 안 적으면 **가까운 면**을 붙잡고, 표본 열넷 중 어느
|
|
1227
|
+
* 것도 이 값을 적지 않고 제대로 선다. 그래서 축마다 첫 항목이 「자동」이고, 지금 자동이 무엇으로
|
|
1228
|
+
* 읽히는지를 **함께 보여 준다** — 그것을 모르면 무엇을 바꾸는지 모른 채 고르게 된다.
|
|
1229
|
+
*
|
|
1230
|
+
* 자동이 무엇인지는 형식에게 묻는다(`anchorOfPart`). 여기서 「위 절반이면 max」를 다시 세면
|
|
1231
|
+
* 화면이 말하는 면과 실제로 그려지는 면이 갈린다.
|
|
1232
|
+
*
|
|
1233
|
+
* ## `scale` 인 부품에도 보여 준다
|
|
1234
|
+
*
|
|
1235
|
+
* 처음에는 감췄다 — 형식 주석에 「`sizing` 이 `scale` 인 부품에는 뜻이 없다」고 적혀 있어서다.
|
|
1236
|
+
* **그 문장은 기본값에 대한 것이다.** `anchorOf` 는 적힌 값을 가장 먼저 보고(`if (said) return
|
|
1237
|
+
* said`), 그 뒤에야 `sizing` 을 본다. 즉 `scale` 부품에도 면을 적으면 그것이 이긴다.
|
|
1238
|
+
*
|
|
1239
|
+
* 감춰 두면 어긋남이 난다: 발행 게이트가 막는 MIXER 의 `valve-wheel` 이 `scale` 부품이고,
|
|
1240
|
+
* 게이트의 사유는 「그 부품의 anchor.z 를 max 로 두라」다. 고치라는 자리에 문을 닫아 둔 셈이었다.
|
|
1241
|
+
*
|
|
1242
|
+
* 대신 `scale` 일 때는 자동이 무엇인지가 「함께 커진다」로 보이므로, 저작자가 그것을 바꾸는
|
|
1243
|
+
* 것임을 알 수 있다.
|
|
1244
|
+
*/
|
|
1245
|
+
/**
|
|
1246
|
+
* ── 도움말은 자리를 먹지 않는다 ─────────────────────────────────────────────
|
|
1247
|
+
*
|
|
1248
|
+
* 설명 문단이 열 줄 있었고 그 줄들이 패널 높이의 상당 부분이었다. 속성은 앞으로 더 늘어난다
|
|
1249
|
+
* (능력 · 포트 · 애니메이션 · 기준정보) — 늘 보이는 설명은 그 자리를 먼저 먹는다.
|
|
1250
|
+
*
|
|
1251
|
+
* 그래서 물음표로 접는다. **지우지 않는다** — 그 문장들은 「이걸 고르면 무엇이 달라지나」를
|
|
1252
|
+
* 말하는 것이고, 없으면 닫힌 목록을 고를 수 없다. 한 번 읽으면 되는 것이라 접어 두는 것이 맞다.
|
|
1253
|
+
*
|
|
1254
|
+
* 편 것은 **부품을 바꿔도 그대로 둔다**(`opened` 는 부품에 딸린 상태가 아니다). 같은 것을
|
|
1255
|
+
* 계속 고치는 사람이 매번 다시 펴야 하면 접은 것이 되레 방해가 된다.
|
|
1256
|
+
*/
|
|
1257
|
+
private help(key: string) {
|
|
1258
|
+
const shown = this.opened.has(key)
|
|
1259
|
+
|
|
1260
|
+
return html`
|
|
1261
|
+
<button
|
|
1262
|
+
help
|
|
1263
|
+
aria-label=${i18next.t('figure.label.help')}
|
|
1264
|
+
aria-expanded=${shown ? 'true' : 'false'}
|
|
1265
|
+
@click=${() => {
|
|
1266
|
+
shown ? this.opened.delete(key) : this.opened.add(key)
|
|
1267
|
+
this.requestUpdate()
|
|
1268
|
+
}}
|
|
1269
|
+
>
|
|
1270
|
+
<md-icon>${shown ? 'help' : 'help_outline'}</md-icon>
|
|
1271
|
+
</button>
|
|
1272
|
+
${shown ? html`<p hint>${i18next.t(key)}</p>` : nothing}
|
|
1273
|
+
`
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private renderAnchors(part: FigurePart) {
|
|
1277
|
+
const base = this.board
|
|
1278
|
+
? { x: this.board.width, y: this.board.height, z: this.board.depth }
|
|
1279
|
+
: { x: 0, y: 0, z: 0 }
|
|
1280
|
+
|
|
1281
|
+
return html`
|
|
1282
|
+
<div field>
|
|
1283
|
+
<!-- 「anchor.x」로 적힌 사유를 화면에서 찾을 수 있게 칸 이름에 그 낱말을 남긴다 -->
|
|
1284
|
+
<label>${i18next.t('figure.label.anchor')} (anchor)${this.help('figure.text.anchor-explained')}</label>
|
|
1285
|
+
${(['x', 'y', 'z'] as const).map(axis => {
|
|
1286
|
+
const said = part.anchor?.[axis]
|
|
1287
|
+
const auto = anchorOfPart(part, base, axis)
|
|
1288
|
+
|
|
1289
|
+
/*
|
|
1290
|
+
**면 이름은 축마다 다르다.** 처음에는 min 을 「아래·왼쪽·뒤 면」으로 한 줄에 다 적었는데,
|
|
1291
|
+
x 줄에서 「아래」와 「뒤」는 관계가 없어 읽는 사람이 무엇을 고르는지 몰랐다.
|
|
1292
|
+
|
|
1293
|
+
그리고 이름씨가 아니라 **동작**으로 적는다 — 「오른쪽 면」이 아니라 「오른쪽 면에 붙어
|
|
1294
|
+
있는다」다. 「anchor.x 를 max 로 둔다」가 무슨 뜻인지 모르겠다는 말이 그 자리에서 나왔다.
|
|
1295
|
+
*/
|
|
1296
|
+
/*
|
|
1297
|
+
**값 이름을 함께 보인다.** 동작만 적었더니(「오른쪽 면에 붙어 있는다») 발행 게이트가
|
|
1298
|
+
「anchor.x 를 max 로 두라」고 말할 때 화면에서 그 낱말을 찾을 수 없었다 — 사용자가
|
|
1299
|
+
그 자리에서 지적했다.
|
|
1300
|
+
|
|
1301
|
+
`max` 는 정본에 저장되는 이름이고 서버 오류·형식 문서·게이트 사유가 다 그 이름을 쓴다.
|
|
1302
|
+
숨기면 안 되는 종류다. 동작이 먼저고 이름이 괄호로 따라온다.
|
|
1303
|
+
*/
|
|
1304
|
+
const nameOf = (rule: string) =>
|
|
1305
|
+
`${i18next.t(rule === 'min' || rule === 'max' ? `figure.label.anchor-${rule}-${axis}` : `figure.label.anchor-${rule}`)} (${rule})`
|
|
1306
|
+
|
|
1307
|
+
return html`
|
|
1308
|
+
<div cell>
|
|
1309
|
+
<span axis>${axis}</span>
|
|
1310
|
+
<select
|
|
1311
|
+
@change=${(e: Event) => {
|
|
1312
|
+
const value = (e.target as HTMLSelectElement).value
|
|
1313
|
+
const anchor = { ...(part.anchor ?? {}) }
|
|
1314
|
+
/* 「자동」은 칸을 지우는 것이다 — 빈 값을 넣어 두면 형식이 그것을 값으로 읽는다. */
|
|
1315
|
+
if (value) {
|
|
1316
|
+
anchor[axis] = value as AnchorRule
|
|
1317
|
+
} else {
|
|
1318
|
+
delete anchor[axis]
|
|
1319
|
+
}
|
|
1320
|
+
this.put({ anchor: Object.keys(anchor).length ? anchor : undefined })
|
|
1321
|
+
}}
|
|
1322
|
+
>
|
|
1323
|
+
<option value="" ?selected=${!said}>
|
|
1324
|
+
${i18next.t('figure.label.anchor-auto', { rule: nameOf(auto) })}
|
|
1325
|
+
</option>
|
|
1326
|
+
${ANCHOR_RULES.map(
|
|
1327
|
+
rule => html`<option value=${rule} ?selected=${rule === said}>${nameOf(rule)}</option>`
|
|
1328
|
+
)}
|
|
1329
|
+
</select>
|
|
1330
|
+
</div>
|
|
1331
|
+
`
|
|
1332
|
+
})}
|
|
1333
|
+
</div>
|
|
1334
|
+
`
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1021
1337
|
/**
|
|
1022
1338
|
* 숫자 칸.
|
|
1023
1339
|
*
|