@things-factory/figure-ui 10.1.43 → 10.1.45
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/modeller/figure-v3-links.ts +303 -0
- package/client/modeller/modeller-route.ts +6 -21
- package/client/modeller/v3-editor.ts +44 -3
- package/client/modeller/v3-gizmo.ts +13 -2
- package/client/modeller/v3-links.ts +138 -0
- package/client/pages/figure-v3-modeller-page.ts +2 -5
- package/client/route.ts +2 -6
- package/dist-client/modeller/figure-v3-links.d.ts +42 -0
- package/dist-client/modeller/figure-v3-links.js +306 -0
- package/dist-client/modeller/figure-v3-links.js.map +1 -0
- package/dist-client/modeller/modeller-route.d.ts +4 -11
- package/dist-client/modeller/modeller-route.js +5 -22
- package/dist-client/modeller/modeller-route.js.map +1 -1
- package/dist-client/modeller/v3-editor.d.ts +5 -0
- package/dist-client/modeller/v3-editor.js +46 -2
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-gizmo.js +11 -1
- package/dist-client/modeller/v3-gizmo.js.map +1 -1
- package/dist-client/modeller/v3-links.d.ts +54 -0
- package/dist-client/modeller/v3-links.js +110 -0
- package/dist-client/modeller/v3-links.js.map +1 -0
- package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -5
- package/dist-client/pages/figure-v3-modeller-page.js +2 -5
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +2 -5
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/ai-proposal-contract.test.ts +0 -88
- package/test/figure-source.test.ts +0 -102
- package/test/i18n-prefix-guard.test.ts +0 -24
- package/test/modeller-route.test.ts +14 -25
- package/test/part-edits.test.ts +0 -7
- package/test/scene-budget.test.ts +0 -7
- package/test/v3-gizmo.test.ts +10 -0
- package/test/v3-links.test.ts +123 -0
- package/things-factory.config.js +2 -5
- package/translations/en.json +6 -0
- package/translations/ja.json +11 -1
- package/translations/ko.json +6 -0
- package/translations/ms.json +11 -1
- package/translations/zh.json +11 -1
- package/client/modeller/angle-dial.ts +0 -70
- package/client/modeller/base-box.ts +0 -175
- package/client/modeller/figure-animations.ts +0 -1249
- package/client/modeller/figure-canvas.ts +0 -567
- package/client/modeller/figure-capabilities.ts +0 -379
- package/client/modeller/figure-inspector.ts +0 -2208
- package/client/modeller/figure-parts.ts +0 -479
- package/client/modeller/figure-preview.ts +0 -598
- package/client/modeller/figure-report.ts +0 -966
- package/client/modeller/figure-settings.ts +0 -328
- package/client/modeller/figure-side.ts +0 -272
- package/client/modeller/figure-thumbnail.ts +0 -123
- package/client/modeller/figure-view.ts +0 -152
- package/client/modeller/joint-edits.ts +0 -262
- package/client/modeller/joint-gizmo.ts +0 -180
- package/client/modeller/maturity.ts +0 -288
- package/client/modeller/new-figure-entry.ts +0 -4
- package/client/modeller/pose-defaults.ts +0 -67
- package/client/pages/figure-modeller-page.ts +0 -2082
- package/dist-client/modeller/angle-dial.d.ts +0 -32
- package/dist-client/modeller/angle-dial.js +0 -41
- package/dist-client/modeller/angle-dial.js.map +0 -1
- package/dist-client/modeller/base-box.d.ts +0 -71
- package/dist-client/modeller/base-box.js +0 -110
- package/dist-client/modeller/base-box.js.map +0 -1
- package/dist-client/modeller/figure-animations.d.ts +0 -159
- package/dist-client/modeller/figure-animations.js +0 -1132
- package/dist-client/modeller/figure-animations.js.map +0 -1
- package/dist-client/modeller/figure-canvas.d.ts +0 -150
- package/dist-client/modeller/figure-canvas.js +0 -506
- package/dist-client/modeller/figure-canvas.js.map +0 -1
- package/dist-client/modeller/figure-capabilities.d.ts +0 -42
- package/dist-client/modeller/figure-capabilities.js +0 -349
- package/dist-client/modeller/figure-capabilities.js.map +0 -1
- package/dist-client/modeller/figure-inspector.d.ts +0 -329
- package/dist-client/modeller/figure-inspector.js +0 -2095
- package/dist-client/modeller/figure-inspector.js.map +0 -1
- package/dist-client/modeller/figure-parts.d.ts +0 -64
- package/dist-client/modeller/figure-parts.js +0 -478
- package/dist-client/modeller/figure-parts.js.map +0 -1
- package/dist-client/modeller/figure-preview.d.ts +0 -138
- package/dist-client/modeller/figure-preview.js +0 -549
- package/dist-client/modeller/figure-preview.js.map +0 -1
- package/dist-client/modeller/figure-report.d.ts +0 -165
- package/dist-client/modeller/figure-report.js +0 -952
- package/dist-client/modeller/figure-report.js.map +0 -1
- package/dist-client/modeller/figure-settings.d.ts +0 -39
- package/dist-client/modeller/figure-settings.js +0 -317
- package/dist-client/modeller/figure-settings.js.map +0 -1
- package/dist-client/modeller/figure-side.d.ts +0 -78
- package/dist-client/modeller/figure-side.js +0 -317
- package/dist-client/modeller/figure-side.js.map +0 -1
- package/dist-client/modeller/figure-thumbnail.d.ts +0 -3
- package/dist-client/modeller/figure-thumbnail.js +0 -95
- package/dist-client/modeller/figure-thumbnail.js.map +0 -1
- package/dist-client/modeller/figure-view.d.ts +0 -118
- package/dist-client/modeller/figure-view.js +0 -78
- package/dist-client/modeller/figure-view.js.map +0 -1
- package/dist-client/modeller/joint-edits.d.ts +0 -49
- package/dist-client/modeller/joint-edits.js +0 -223
- package/dist-client/modeller/joint-edits.js.map +0 -1
- package/dist-client/modeller/joint-gizmo.d.ts +0 -34
- package/dist-client/modeller/joint-gizmo.js +0 -158
- package/dist-client/modeller/joint-gizmo.js.map +0 -1
- package/dist-client/modeller/maturity.d.ts +0 -55
- package/dist-client/modeller/maturity.js +0 -212
- package/dist-client/modeller/maturity.js.map +0 -1
- package/dist-client/modeller/new-figure-entry.d.ts +0 -2
- package/dist-client/modeller/new-figure-entry.js +0 -5
- package/dist-client/modeller/new-figure-entry.js.map +0 -1
- package/dist-client/modeller/pose-defaults.d.ts +0 -9
- package/dist-client/modeller/pose-defaults.js +0 -35
- package/dist-client/modeller/pose-defaults.js.map +0 -1
- package/dist-client/pages/figure-modeller-page.d.ts +0 -419
- package/dist-client/pages/figure-modeller-page.js +0 -2060
- package/dist-client/pages/figure-modeller-page.js.map +0 -1
- package/test/angle-dial.test.ts +0 -78
- package/test/base-box.test.ts +0 -235
- package/test/joint-edits.test.ts +0 -165
- package/test/joint-gizmo.test.ts +0 -174
- package/test/joint-needle.test.ts +0 -162
- package/test/modeller-undo-redo.test.ts +0 -33
- package/test/new-figure-entry.test.ts +0 -15
- package/test/pose-defaults.test.ts +0 -71
- package/test/preview-board-depth.test.ts +0 -27
|
@@ -1,1132 +0,0 @@
|
|
|
1
|
-
import { __decorate, __metadata } from "tslib";
|
|
2
|
-
import { css, html, LitElement, nothing } from 'lit';
|
|
3
|
-
import { customElement, property, state } from 'lit/decorators.js';
|
|
4
|
-
import { live } from 'lit/directives/live.js';
|
|
5
|
-
import { i18next, localize } from '@operato/i18n';
|
|
6
|
-
import { ScrollbarStyles } from '@operato/styles';
|
|
7
|
-
import { AXES, CHANNEL_PATHS, INTERPOLATIONS, LIMITS } from '@hatiolab/figure-model';
|
|
8
|
-
/** A channel that drives a joint names the joint and carries one number per key (ADR-0066). */
|
|
9
|
-
function isJointChannel(channel) {
|
|
10
|
-
return channel.path === undefined;
|
|
11
|
-
}
|
|
12
|
-
/*
|
|
13
|
-
레시피의 `end` 가 곧 그 범위의 끝 자세다 — 리프트가 100mm 올라가는 곡선이면 범위도
|
|
14
|
-
0~100mm 다. 둘이 어긋나면 저작자가 1200 을 주고도 100 만 올라가는 것을 보게 된다.
|
|
15
|
-
*/
|
|
16
|
-
const MOTION_RECIPES = [
|
|
17
|
-
{ id: 'conveyor', name: 'conveyor-running', kind: 'clip', path: 'rotation', end: { x: 360, y: 0, z: 0 } },
|
|
18
|
-
{
|
|
19
|
-
id: 'gate',
|
|
20
|
-
name: 'gate-open',
|
|
21
|
-
kind: 'parameter',
|
|
22
|
-
path: 'rotation',
|
|
23
|
-
end: { x: 0, y: 90, z: 0 },
|
|
24
|
-
pivot: { x: 0, y: 0, z: 0 },
|
|
25
|
-
range: { unit: '%', min: 0, max: 100 }
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
id: 'pusher',
|
|
29
|
-
name: 'pusher-position',
|
|
30
|
-
kind: 'parameter',
|
|
31
|
-
path: 'translation',
|
|
32
|
-
end: { x: 100, y: 0, z: 0 },
|
|
33
|
-
range: { unit: 'mm', min: 0, max: 100 }
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
id: 'lift',
|
|
37
|
-
name: 'lift-height',
|
|
38
|
-
kind: 'parameter',
|
|
39
|
-
path: 'translation',
|
|
40
|
-
end: { x: 0, y: 100, z: 0 },
|
|
41
|
-
range: { unit: 'mm', min: 0, max: 100 }
|
|
42
|
-
}
|
|
43
|
-
];
|
|
44
|
-
/** 새 파라미터의 범위. 단위는 저작자가 고쳐 쓴다 — 형식은 단위를 해석하지 않는다. */
|
|
45
|
-
const NEW_RANGE = { unit: 'mm', min: 0, max: 1000 };
|
|
46
|
-
/** 새 clip 의 키 둘. **하나로 만들지 않는다** — 키 하나는 움직임이 아니라 자세다. */
|
|
47
|
-
const NEW_KEYS = [
|
|
48
|
-
{ at: 0, value: { x: 0, y: 0, z: 0 } },
|
|
49
|
-
{ at: 1, value: { x: 0, y: 0, z: 0 } }
|
|
50
|
-
];
|
|
51
|
-
/** `scale` 만 1 에서 시작한다 — 0 은 부품을 지운다. */
|
|
52
|
-
function startValue(path) {
|
|
53
|
-
return path === 'scale' ? { x: 1, y: 1, z: 1 } : { x: 0, y: 0, z: 0 };
|
|
54
|
-
}
|
|
55
|
-
function keysFor(path) {
|
|
56
|
-
return NEW_KEYS.map(key => ({ at: key.at, value: startValue(path) }));
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* 이미 쓴 이름을 비켜 간다.
|
|
60
|
-
*
|
|
61
|
-
* **두 목록을 함께 본다.** 인스턴스는 이름 하나로 값을 주므로 애니메이션과 파라미터가
|
|
62
|
-
* 같은 이름을 가지면 어느 쪽에 준 것인지 알 수 없고, 형식이 그것을 오류로 막는다.
|
|
63
|
-
*/
|
|
64
|
-
function freeName(taken, stem) {
|
|
65
|
-
const used = new Set(taken.map(one => one.name));
|
|
66
|
-
if (!used.has(stem))
|
|
67
|
-
return stem;
|
|
68
|
-
for (let n = 2;; n++)
|
|
69
|
-
if (!used.has(`${stem}-${n}`))
|
|
70
|
-
return `${stem}-${n}`;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* 파라미터 범위에 적히는 단위 — **권하는 것이지 가두는 것이 아니다.**
|
|
74
|
-
*
|
|
75
|
-
* 관절이 만드는 파라미터가 `deg` 와 `mm` 을 쓰고(회전과 직선), 그리퍼·문 같은 값은 `%` 를 쓴다.
|
|
76
|
-
* 속도는 `mm/s` 와 `rpm`, 시간은 `s` 다. 이 여섯이 지금 쓰이는 전부이고, 목록에 없는 말을 적는
|
|
77
|
-
* 길은 그대로 열려 있다.
|
|
78
|
-
*/
|
|
79
|
-
const PARAMETER_UNITS = ['deg', 'mm', '%', 'mm/s', 'rpm', 's'];
|
|
80
|
-
/** 목록 대신 손으로 적겠다는 표. `select` 의 값으로 쓴다. */
|
|
81
|
-
const UNIT_TYPE_IT = '__type-it';
|
|
82
|
-
let FigureAnimations = class FigureAnimations extends localize(i18next)(LitElement) {
|
|
83
|
-
constructor() {
|
|
84
|
-
super(...arguments);
|
|
85
|
-
this.parts = [];
|
|
86
|
-
/**
|
|
87
|
-
* 펼쳐 둔 것. 이름이 아니라 **목록과 자리**로 잡는다 — 이름은 편집 중에 바뀐다.
|
|
88
|
-
*
|
|
89
|
-
* 목록이 둘이므로 자리 하나로는 모자란다. `clip:0` 과 `parameter:0` 은 다른 것이다.
|
|
90
|
-
*/
|
|
91
|
-
this.open = '';
|
|
92
|
-
this.recipeTarget = '';
|
|
93
|
-
/** 단위를 손으로 적는 중인 파라미터. 목록에 없는 말을 적으려고 연 칸이다. */
|
|
94
|
-
this.typingUnit = new Set();
|
|
95
|
-
}
|
|
96
|
-
static { this.styles = [
|
|
97
|
-
ScrollbarStyles,
|
|
98
|
-
css `
|
|
99
|
-
:host {
|
|
100
|
-
display: flex;
|
|
101
|
-
flex-direction: column;
|
|
102
|
-
min-height: 0;
|
|
103
|
-
overflow-y: auto;
|
|
104
|
-
background-color: var(--md-sys-color-surface-container-lowest);
|
|
105
|
-
color: var(--md-sys-color-on-surface);
|
|
106
|
-
font: var(--label-font, inherit);
|
|
107
|
-
font-size: 0.76rem;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
section {
|
|
111
|
-
display: flex;
|
|
112
|
-
flex-direction: column;
|
|
113
|
-
gap: var(--spacing-medium, 8px);
|
|
114
|
-
padding: var(--spacing-large, 12px);
|
|
115
|
-
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/* 다른 탭들과 같은 눈금이다 */
|
|
119
|
-
h3 {
|
|
120
|
-
margin: 0 0 2px 0;
|
|
121
|
-
font-size: var(--ui-t-micro, 11px);
|
|
122
|
-
font-weight: 700;
|
|
123
|
-
letter-spacing: 0.04em;
|
|
124
|
-
text-transform: uppercase;
|
|
125
|
-
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
p[quiet] {
|
|
129
|
-
margin: 0;
|
|
130
|
-
font-size: 0.72rem;
|
|
131
|
-
line-height: 1.5;
|
|
132
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
div[recipes] { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
|
136
|
-
button[recipe] { min-height: 54px; align-items: flex-start; flex-direction: column; text-align: left; }
|
|
137
|
-
button[recipe] strong { font-size: 0.74rem; }
|
|
138
|
-
button[recipe] span { color: var(--md-sys-color-on-surface-variant); font-size: 0.68rem; line-height: 1.35; }
|
|
139
|
-
|
|
140
|
-
/* ── clip ─────────────────────────────────────────────────────── */
|
|
141
|
-
|
|
142
|
-
div[clip] {
|
|
143
|
-
display: flex;
|
|
144
|
-
flex-direction: column;
|
|
145
|
-
gap: var(--spacing-medium, 8px);
|
|
146
|
-
padding: 8px;
|
|
147
|
-
border: 1px solid var(--md-sys-color-outline-variant);
|
|
148
|
-
border-radius: 4px;
|
|
149
|
-
}
|
|
150
|
-
div[clip][on] {
|
|
151
|
-
border-color: var(--md-sys-color-primary);
|
|
152
|
-
background-color: var(--md-sys-color-surface-container-low);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
div[head] {
|
|
156
|
-
display: flex;
|
|
157
|
-
align-items: center;
|
|
158
|
-
gap: 6px;
|
|
159
|
-
}
|
|
160
|
-
div[head] input[name] {
|
|
161
|
-
flex: 1;
|
|
162
|
-
min-width: 0;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/* ── channel ──────────────────────────────────────────────────── */
|
|
166
|
-
|
|
167
|
-
div[channel] {
|
|
168
|
-
display: flex;
|
|
169
|
-
flex-direction: column;
|
|
170
|
-
gap: 6px;
|
|
171
|
-
padding: 6px 8px;
|
|
172
|
-
border-left: 2px solid var(--md-sys-color-outline-variant);
|
|
173
|
-
background-color: var(--md-sys-color-surface-container-lowest);
|
|
174
|
-
}
|
|
175
|
-
div[channel][orphan] {
|
|
176
|
-
border-left-color: var(--md-sys-color-error);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
div[row] {
|
|
180
|
-
display: flex;
|
|
181
|
-
align-items: center;
|
|
182
|
-
gap: 6px;
|
|
183
|
-
}
|
|
184
|
-
div[row] > label {
|
|
185
|
-
flex: none;
|
|
186
|
-
min-width: 3.2em;
|
|
187
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
188
|
-
}
|
|
189
|
-
/*
|
|
190
|
-
칸이 줄어들 줄 알아야 한다. 숫자 입력칸은 제 기본 너비(20자 남짓)를 갖고 있고 flex 는
|
|
191
|
-
기본 크기 아래로는 안 줄이므로, 범위처럼 한 줄에 셋이 서는 줄은 패널 밖으로 밀려
|
|
192
|
-
오른쪽 칸이 잘렸다.
|
|
193
|
-
*/
|
|
194
|
-
div[row] > input {
|
|
195
|
-
flex: 1 1 0;
|
|
196
|
-
min-width: 0;
|
|
197
|
-
}
|
|
198
|
-
/* 단위는 「mm」 정도의 짧은 말이라 숫자 칸보다 좁게 둔다. 고르는 칸도 같은 폭이다. */
|
|
199
|
-
div[row] > input[unit],
|
|
200
|
-
div[row] > select[unit] {
|
|
201
|
-
flex: 0 1 5.5em;
|
|
202
|
-
min-width: 0;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/* ── keyframe ─────────────────────────────────────────────────── */
|
|
206
|
-
|
|
207
|
-
table[keys] {
|
|
208
|
-
width: 100%;
|
|
209
|
-
border-collapse: collapse;
|
|
210
|
-
}
|
|
211
|
-
table[keys] th {
|
|
212
|
-
padding: 0 2px 2px;
|
|
213
|
-
font-size: 0.68rem;
|
|
214
|
-
font-weight: 600;
|
|
215
|
-
text-align: left;
|
|
216
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
217
|
-
}
|
|
218
|
-
table[keys] td {
|
|
219
|
-
padding: 1px 2px;
|
|
220
|
-
}
|
|
221
|
-
table[keys] input {
|
|
222
|
-
width: 100%;
|
|
223
|
-
min-width: 0;
|
|
224
|
-
}
|
|
225
|
-
table[keys] td[drop] {
|
|
226
|
-
width: 1.6em;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
input,
|
|
230
|
-
select {
|
|
231
|
-
box-sizing: border-box;
|
|
232
|
-
padding: 3px 5px;
|
|
233
|
-
font: inherit;
|
|
234
|
-
font-size: 0.74rem;
|
|
235
|
-
color: var(--md-sys-color-on-surface);
|
|
236
|
-
background-color: var(--md-sys-color-surface-container-lowest);
|
|
237
|
-
border: 1px solid var(--md-sys-color-outline-variant);
|
|
238
|
-
border-radius: 3px;
|
|
239
|
-
}
|
|
240
|
-
input:focus,
|
|
241
|
-
select:focus {
|
|
242
|
-
outline: none;
|
|
243
|
-
border-color: var(--md-sys-color-primary);
|
|
244
|
-
}
|
|
245
|
-
input[bad] {
|
|
246
|
-
border-color: var(--md-sys-color-error);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
button {
|
|
250
|
-
display: inline-flex;
|
|
251
|
-
align-items: center;
|
|
252
|
-
gap: 3px;
|
|
253
|
-
padding: 3px 8px;
|
|
254
|
-
font: inherit;
|
|
255
|
-
font-size: 0.72rem;
|
|
256
|
-
color: var(--md-sys-color-on-surface);
|
|
257
|
-
background-color: var(--md-sys-color-surface-container);
|
|
258
|
-
border: 1px solid var(--md-sys-color-outline-variant);
|
|
259
|
-
border-radius: 3px;
|
|
260
|
-
cursor: pointer;
|
|
261
|
-
}
|
|
262
|
-
button:hover:not(:disabled) {
|
|
263
|
-
background-color: var(--md-sys-color-surface-container-high);
|
|
264
|
-
}
|
|
265
|
-
button:disabled {
|
|
266
|
-
opacity: 0.4;
|
|
267
|
-
cursor: default;
|
|
268
|
-
}
|
|
269
|
-
button[plain] {
|
|
270
|
-
padding: 1px 4px;
|
|
271
|
-
background: none;
|
|
272
|
-
border: none;
|
|
273
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
274
|
-
}
|
|
275
|
-
button[plain]:hover:not(:disabled) {
|
|
276
|
-
color: var(--md-sys-color-error);
|
|
277
|
-
background: none;
|
|
278
|
-
}
|
|
279
|
-
button md-icon {
|
|
280
|
-
font-size: 14px;
|
|
281
|
-
width: 14px;
|
|
282
|
-
height: 14px;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/* ── 예산 ─────────────────────────────────────────────────────── */
|
|
286
|
-
|
|
287
|
-
div[budget] {
|
|
288
|
-
display: flex;
|
|
289
|
-
align-items: baseline;
|
|
290
|
-
gap: 6px;
|
|
291
|
-
}
|
|
292
|
-
div[budget] strong {
|
|
293
|
-
font-size: 0.86rem;
|
|
294
|
-
font-weight: 700;
|
|
295
|
-
}
|
|
296
|
-
div[budget][over] strong {
|
|
297
|
-
color: var(--md-sys-color-error);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
p[warn] {
|
|
301
|
-
margin: 0;
|
|
302
|
-
font-size: 0.7rem;
|
|
303
|
-
line-height: 1.5;
|
|
304
|
-
color: var(--md-sys-color-error);
|
|
305
|
-
}
|
|
306
|
-
`
|
|
307
|
-
]; }
|
|
308
|
-
get clips() {
|
|
309
|
-
return this.draft?.animations ?? [];
|
|
310
|
-
}
|
|
311
|
-
get parameters() {
|
|
312
|
-
return this.draft?.parameters ?? [];
|
|
313
|
-
}
|
|
314
|
-
/** 두 목록의 이름을 한자리에서 본다 — 인스턴스가 이름 하나로 값을 준다. */
|
|
315
|
-
get allNames() {
|
|
316
|
-
return [...this.clips, ...this.parameters];
|
|
317
|
-
}
|
|
318
|
-
channelsOf(kind, at) {
|
|
319
|
-
return (kind === 'clip' ? this.clips[at]?.channels : this.parameters[at]?.clip?.channels) ?? [];
|
|
320
|
-
}
|
|
321
|
-
/** 부품 이름 — 채널이 이것을 가리킨다. */
|
|
322
|
-
get partNames() {
|
|
323
|
-
return this.parts.map(part => part.name).filter(name => !!name);
|
|
324
|
-
}
|
|
325
|
-
/** Joints a channel can drive instead of a part (ADR-0066). */
|
|
326
|
-
get joints() {
|
|
327
|
-
return this.draft?.joints ?? [];
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* 몇 부품이 움직이나 — 채널이 가리키는 **서로 다른 부품**의 수.
|
|
331
|
-
*
|
|
332
|
-
* clip 수가 아니다. 한 부품을 세 clip 이 움직여도 그 부품 하나가 단독 묶음이 될 뿐이다.
|
|
333
|
-
* 청사진이 묶음을 가르는 기준과 같은 셈이어야 화면의 수와 판정의 수가 맞는다.
|
|
334
|
-
*/
|
|
335
|
-
get movingParts() {
|
|
336
|
-
const moving = new Set();
|
|
337
|
-
for (const clip of this.clips)
|
|
338
|
-
for (const channel of clip.channels)
|
|
339
|
-
moving.add(channel.target);
|
|
340
|
-
for (const parameter of this.parameters) {
|
|
341
|
-
for (const channel of parameter.clip?.channels ?? [])
|
|
342
|
-
moving.add(channel.target);
|
|
343
|
-
}
|
|
344
|
-
return moving.size;
|
|
345
|
-
}
|
|
346
|
-
render() {
|
|
347
|
-
if (!this.draft)
|
|
348
|
-
return html ``;
|
|
349
|
-
const nameless = this.partNames.length === 0;
|
|
350
|
-
return html `
|
|
351
|
-
<section>
|
|
352
|
-
<h3>${i18next.t('figure.label.animation')}</h3>
|
|
353
|
-
</section>
|
|
354
|
-
|
|
355
|
-
${this.renderRecipes()}
|
|
356
|
-
|
|
357
|
-
<section>
|
|
358
|
-
<h3>${i18next.t('figure.label.parameters')}</h3>
|
|
359
|
-
<p quiet>${i18next.t('figure.text.parameter-is-a-value-not-a-timeline')}</p>
|
|
360
|
-
${this.parameters.map((parameter, at) => this.renderParameter(parameter, at))}
|
|
361
|
-
<button @click=${() => this.addParameter()} ?disabled=${nameless}>
|
|
362
|
-
<md-icon>add</md-icon>${i18next.t('figure.button.add-parameter')}
|
|
363
|
-
</button>
|
|
364
|
-
</section>
|
|
365
|
-
|
|
366
|
-
<section>
|
|
367
|
-
<h3>${i18next.t('figure.label.clips')}</h3>
|
|
368
|
-
<p quiet>${i18next.t('figure.text.clip-is-time-running')}</p>
|
|
369
|
-
${this.clips.map((clip, at) => this.renderClip(clip, at))}
|
|
370
|
-
<button @click=${() => this.addClip()} ?disabled=${nameless}>
|
|
371
|
-
<md-icon>add</md-icon>${i18next.t('figure.button.add-clip')}
|
|
372
|
-
</button>
|
|
373
|
-
${nameless ? html `<p quiet>${i18next.t('figure.text.animation-needs-a-named-part')}</p>` : nothing}
|
|
374
|
-
</section>
|
|
375
|
-
|
|
376
|
-
${this.movingParts === 0 ? nothing : this.renderBudget()}
|
|
377
|
-
`;
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* 빈 화면 — **이 탭이 무엇인지 가르치는 자리다.**
|
|
381
|
-
*
|
|
382
|
-
* 「clip 을 추가하세요」라고만 쓰면 처음 여는 사람은 clip 이 무엇인지 모른 채 단추를 누른다.
|
|
383
|
-
* 그래서 무엇을 만들 수 있는지 예를 들고, 그것이 어떻게 움직이는지(사실이 들어온다)를 적는다.
|
|
384
|
-
*/
|
|
385
|
-
renderRecipes() {
|
|
386
|
-
return html `
|
|
387
|
-
<section>
|
|
388
|
-
<h3>${i18next.t('figure.label.motion-intent')}</h3>
|
|
389
|
-
<p quiet>${i18next.t('figure.text.motion-intent-help')}</p>
|
|
390
|
-
${this.partNames.length
|
|
391
|
-
? html `<select .value=${this.recipeTarget} @change=${(e) => (this.recipeTarget = e.target.value)}>
|
|
392
|
-
${this.partNames.map(name => html `<option value=${name}>${name}</option>`)}
|
|
393
|
-
</select>
|
|
394
|
-
<div recipes>
|
|
395
|
-
${MOTION_RECIPES.map(recipe => html `
|
|
396
|
-
<button recipe @click=${() => this.addRecipe(recipe)}>
|
|
397
|
-
<strong>${i18next.t('figure.label.recipe-' + recipe.id)}</strong>
|
|
398
|
-
<span>${i18next.t('figure.text.recipe-' + recipe.id)}</span>
|
|
399
|
-
</button>
|
|
400
|
-
`)}
|
|
401
|
-
</div>`
|
|
402
|
-
: html `<p quiet>${i18next.t('figure.text.animation-needs-a-named-part')}</p>`}
|
|
403
|
-
</section>
|
|
404
|
-
`;
|
|
405
|
-
}
|
|
406
|
-
/** 이름이 다른 것과 겹치나. 두 목록을 함께 본다 — 인스턴스는 이름 하나로 값을 준다. */
|
|
407
|
-
taken(kind, at, name) {
|
|
408
|
-
const mine = kind === 'clip' ? this.clips[at] : this.parameters[at];
|
|
409
|
-
return this.allNames.some(one => one !== mine && one.name === name);
|
|
410
|
-
}
|
|
411
|
-
renderClip(clip, at) {
|
|
412
|
-
const key = `clip:${at}`;
|
|
413
|
-
const on = this.open === key;
|
|
414
|
-
const duplicate = this.taken('clip', at, clip.name);
|
|
415
|
-
const nameless = !clip.name?.trim();
|
|
416
|
-
return html `
|
|
417
|
-
<div clip ?on=${on}>
|
|
418
|
-
<div head>
|
|
419
|
-
<button plain @click=${() => (this.open = on ? '' : key)} title=${i18next.t('figure.label.channels')}>
|
|
420
|
-
<md-icon>${on ? 'expand_more' : 'chevron_right'}</md-icon>
|
|
421
|
-
</button>
|
|
422
|
-
<input
|
|
423
|
-
name
|
|
424
|
-
.value=${live(clip.name ?? '')}
|
|
425
|
-
?bad=${duplicate || nameless}
|
|
426
|
-
placeholder=${i18next.t('figure.label.clip-name')}
|
|
427
|
-
title=${duplicate ? i18next.t('figure.text.clip-name-taken') : i18next.t('figure.text.clip-name-is-how-it-is-driven')}
|
|
428
|
-
@change=${(e) => this.renameClip(at, e.target.value)}
|
|
429
|
-
/>
|
|
430
|
-
<button plain @click=${() => this.removeClip(at)} title=${i18next.t('figure.button.remove')}>
|
|
431
|
-
<md-icon>close</md-icon>
|
|
432
|
-
</button>
|
|
433
|
-
</div>
|
|
434
|
-
|
|
435
|
-
${duplicate ? html `<p warn>${i18next.t('figure.text.clip-name-taken')}</p>` : nothing}
|
|
436
|
-
${nameless ? html `<p warn>${i18next.t('figure.text.clip-needs-a-name')}</p>` : nothing}
|
|
437
|
-
${on ? this.renderChannels('clip', at) : nothing}
|
|
438
|
-
</div>
|
|
439
|
-
`;
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* 파라미터 하나 — **이름·범위·자세.**
|
|
443
|
-
*
|
|
444
|
-
* 범위가 화면에 있어야 하는 이유는 그것이 **인스턴스와 주고받는 말**이기 때문이다.
|
|
445
|
-
* 곡선은 0~1 이고 인스턴스는 mm 로 준다. 그 사이를 런타임이 맵하는데, 무엇부터 무엇
|
|
446
|
-
* 까지인지 저작자가 정하지 않으면 값을 줄 수가 없다.
|
|
447
|
-
*/
|
|
448
|
-
renderParameter(parameter, at) {
|
|
449
|
-
const key = `parameter:${at}`;
|
|
450
|
-
const on = this.open === key;
|
|
451
|
-
const duplicate = this.taken('parameter', at, parameter.name);
|
|
452
|
-
const nameless = !parameter.name?.trim();
|
|
453
|
-
const range = parameter.range ?? NEW_RANGE;
|
|
454
|
-
const backwards = !(range.max > range.min);
|
|
455
|
-
const slowness = parameter.clip?.duration ?? 0;
|
|
456
|
-
return html `
|
|
457
|
-
<div clip ?on=${on}>
|
|
458
|
-
<div head>
|
|
459
|
-
<button plain @click=${() => (this.open = on ? '' : key)} title=${i18next.t('figure.label.channels')}>
|
|
460
|
-
<md-icon>${on ? 'expand_more' : 'chevron_right'}</md-icon>
|
|
461
|
-
</button>
|
|
462
|
-
<input
|
|
463
|
-
name
|
|
464
|
-
.value=${live(parameter.name ?? '')}
|
|
465
|
-
?bad=${duplicate || nameless}
|
|
466
|
-
placeholder=${i18next.t('figure.label.parameter-name')}
|
|
467
|
-
title=${duplicate
|
|
468
|
-
? i18next.t('figure.text.clip-name-taken')
|
|
469
|
-
: i18next.t('figure.text.parameter-name-is-how-a-value-arrives')}
|
|
470
|
-
@change=${(e) => this.renameParameter(at, e.target.value)}
|
|
471
|
-
/>
|
|
472
|
-
<button plain @click=${() => this.removeParameter(at)} title=${i18next.t('figure.button.remove')}>
|
|
473
|
-
<md-icon>close</md-icon>
|
|
474
|
-
</button>
|
|
475
|
-
</div>
|
|
476
|
-
|
|
477
|
-
${duplicate ? html `<p warn>${i18next.t('figure.text.clip-name-taken')}</p>` : nothing}
|
|
478
|
-
${nameless ? html `<p warn>${i18next.t('figure.text.parameter-needs-a-name')}</p>` : nothing}
|
|
479
|
-
|
|
480
|
-
<div row>
|
|
481
|
-
<label>${i18next.t('figure.label.parameter-range')}</label>
|
|
482
|
-
<input
|
|
483
|
-
type="number"
|
|
484
|
-
step="1"
|
|
485
|
-
?bad=${backwards}
|
|
486
|
-
.value=${live(String(range.min))}
|
|
487
|
-
aria-label="min"
|
|
488
|
-
@change=${(e) => this.setRange(at, { min: Number(e.target.value) || 0 })}
|
|
489
|
-
/>
|
|
490
|
-
<input
|
|
491
|
-
type="number"
|
|
492
|
-
step="1"
|
|
493
|
-
?bad=${backwards}
|
|
494
|
-
.value=${live(String(range.max))}
|
|
495
|
-
aria-label="max"
|
|
496
|
-
@change=${(e) => this.setRange(at, { max: Number(e.target.value) || 0 })}
|
|
497
|
-
/>
|
|
498
|
-
${this.renderUnit(at, range.unit ?? '')}
|
|
499
|
-
</div>
|
|
500
|
-
${backwards ? html `<p warn>${i18next.t('figure.text.range-needs-room')}</p>` : nothing}
|
|
501
|
-
|
|
502
|
-
<div row>
|
|
503
|
-
<label>${i18next.t('figure.label.parameter-default')}</label>
|
|
504
|
-
<input
|
|
505
|
-
type="number"
|
|
506
|
-
step="1"
|
|
507
|
-
.value=${live(String(parameter.default ?? range.min))}
|
|
508
|
-
@change=${(e) => this.setDefault(at, Number(e.target.value) || 0)}
|
|
509
|
-
/>
|
|
510
|
-
</div>
|
|
511
|
-
|
|
512
|
-
<div row>
|
|
513
|
-
<label>${i18next.t('figure.label.parameter-duration')}</label>
|
|
514
|
-
<input
|
|
515
|
-
type="number"
|
|
516
|
-
step="0.1"
|
|
517
|
-
min="0"
|
|
518
|
-
?bad=${slowness < 0}
|
|
519
|
-
.value=${live(parameter.clip?.duration === undefined ? '' : String(parameter.clip.duration))}
|
|
520
|
-
placeholder="0"
|
|
521
|
-
aria-label="duration"
|
|
522
|
-
@change=${(e) => this.setDuration(at, e.target.value)}
|
|
523
|
-
/>
|
|
524
|
-
</div>
|
|
525
|
-
<p quiet>${i18next.t('figure.text.parameter-duration-empty-is-instant')}</p>
|
|
526
|
-
|
|
527
|
-
${on ? this.renderChannels('parameter', at) : nothing}
|
|
528
|
-
</div>
|
|
529
|
-
`;
|
|
530
|
-
}
|
|
531
|
-
renderChannels(kind, at) {
|
|
532
|
-
const channels = this.channelsOf(kind, at);
|
|
533
|
-
return html `
|
|
534
|
-
<p quiet>${i18next.t(kind === 'clip' ? 'figure.text.clip-is-time-running' : 'figure.text.parameter-curve-is-the-value-axis')}</p>
|
|
535
|
-
|
|
536
|
-
${channels.length === 0 ? html `<p warn>${i18next.t('figure.text.clip-needs-a-channel')}</p>` : nothing}
|
|
537
|
-
${channels.map((channel, j) => this.renderChannel(kind, channel, at, j))}
|
|
538
|
-
|
|
539
|
-
<button @click=${() => this.addChannel(kind, at)}>
|
|
540
|
-
<md-icon>add</md-icon>${i18next.t('figure.button.add-channel')}
|
|
541
|
-
</button>
|
|
542
|
-
`;
|
|
543
|
-
}
|
|
544
|
-
/**
|
|
545
|
-
* 채널 하나 — 어느 부품의 어느 속성을 움직이나.
|
|
546
|
-
*
|
|
547
|
-
* 없는 부품을 가리키는 채널은 붉게 낸다. 부품 이름을 바꾸면 그 순간 이렇게 되는데,
|
|
548
|
-
* 검증은 저장할 때 말하고 여기서는 **그 자리에서** 보여 준다.
|
|
549
|
-
*/
|
|
550
|
-
renderChannel(kind, channel, at, j) {
|
|
551
|
-
const joint = this.joints.find(one => one.name === channel.target);
|
|
552
|
-
const orphan = !this.partNames.includes(channel.target) && !joint;
|
|
553
|
-
const target = html `
|
|
554
|
-
<div row>
|
|
555
|
-
<label>${i18next.t('figure.label.channel-target')}</label>
|
|
556
|
-
<select
|
|
557
|
-
.value=${live(channel.target ?? '')}
|
|
558
|
-
@change=${(e) => this.retarget(kind, at, j, e.target.value)}
|
|
559
|
-
>
|
|
560
|
-
${orphan ? html `<option value=${channel.target}>${channel.target}</option>` : nothing}
|
|
561
|
-
<optgroup label=${i18next.t('figure.label.channel-target-parts')}>
|
|
562
|
-
${this.partNames.map(name => html `<option value=${name}>${name}</option>`)}
|
|
563
|
-
</optgroup>
|
|
564
|
-
${this.joints.length > 0
|
|
565
|
-
? html `<optgroup label=${i18next.t('figure.label.channel-target-joints')}>
|
|
566
|
-
${this.joints.map(one => html `<option value=${one.name}>${one.name}</option>`)}
|
|
567
|
-
</optgroup>`
|
|
568
|
-
: nothing}
|
|
569
|
-
</select>
|
|
570
|
-
<button plain @click=${() => this.removeChannel(kind, at, j)} title=${i18next.t('figure.button.remove')}>
|
|
571
|
-
<md-icon>close</md-icon>
|
|
572
|
-
</button>
|
|
573
|
-
</div>
|
|
574
|
-
`;
|
|
575
|
-
/* A joint channel has no path or pivot: the joint says what moves. Each key is the joint coordinate. */
|
|
576
|
-
if (isJointChannel(channel)) {
|
|
577
|
-
const unit = joint?.type === 'prismatic' ? 'mm' : 'deg';
|
|
578
|
-
return html `
|
|
579
|
-
<div channel ?orphan=${orphan}>
|
|
580
|
-
${target} ${orphan ? html `<p warn>${i18next.t('figure.text.channel-target-is-gone')}</p>` : nothing}
|
|
581
|
-
<p quiet>${i18next.t('figure.text.joint-channel-value-' + unit)}</p>
|
|
582
|
-
${this.renderJointKeys(kind, channel, at, j, joint?.limits)}
|
|
583
|
-
</div>
|
|
584
|
-
`;
|
|
585
|
-
}
|
|
586
|
-
const turning = channel.path === 'rotation';
|
|
587
|
-
return html `
|
|
588
|
-
<div channel ?orphan=${orphan}>
|
|
589
|
-
${target}
|
|
590
|
-
|
|
591
|
-
${orphan ? html `<p warn>${i18next.t('figure.text.channel-target-is-gone')}</p>` : nothing}
|
|
592
|
-
|
|
593
|
-
<div row>
|
|
594
|
-
<label>${i18next.t('figure.label.channel-path')}</label>
|
|
595
|
-
<select
|
|
596
|
-
.value=${live(channel.path)}
|
|
597
|
-
@change=${(e) => this.setPath(kind, at, j, e.target.value)}
|
|
598
|
-
>
|
|
599
|
-
${CHANNEL_PATHS.map(path => html `<option value=${path}>${i18next.t('figure.label.path-' + path)}</option>`)}
|
|
600
|
-
</select>
|
|
601
|
-
<select
|
|
602
|
-
.value=${live(channel.interpolation ?? 'linear')}
|
|
603
|
-
title=${i18next.t('figure.text.interpolation-explained')}
|
|
604
|
-
@change=${(e) => this.setChannel(kind, at, j, { interpolation: e.target.value })}
|
|
605
|
-
>
|
|
606
|
-
${INTERPOLATIONS.map(one => html `<option value=${one}>${i18next.t('figure.label.interpolation-' + one)}</option>`)}
|
|
607
|
-
</select>
|
|
608
|
-
</div>
|
|
609
|
-
|
|
610
|
-
<p quiet>${i18next.t('figure.text.path-' + channel.path + '-unit')}</p>
|
|
611
|
-
|
|
612
|
-
${turning ? this.renderPivot(kind, channel, at, j) : nothing}
|
|
613
|
-
${this.renderKeys(kind, channel, at, j)}
|
|
614
|
-
</div>
|
|
615
|
-
`;
|
|
616
|
-
}
|
|
617
|
-
/**
|
|
618
|
-
* 회전 중심 — **`rotation` 에서만 뜻이 있다.**
|
|
619
|
-
*
|
|
620
|
-
* 그래서 경로가 rotation 일 때만 낸다. 언제나 내면 저작자가 옮김·배율에도 적고, 검증이
|
|
621
|
-
* 저장할 때 「쓰이지 않는다」고 알린다. 안 보이면 적을 일이 없다.
|
|
622
|
-
*/
|
|
623
|
-
renderPivot(kind, channel, at, j) {
|
|
624
|
-
const pivot = channel.pivot;
|
|
625
|
-
const on = pivot !== undefined;
|
|
626
|
-
return html `
|
|
627
|
-
<div row>
|
|
628
|
-
<label>${i18next.t('figure.label.channel-pivot')}</label>
|
|
629
|
-
<button
|
|
630
|
-
@click=${() => this.setChannel(kind, at, j, { pivot: on ? undefined : { x: 0, y: 0, z: 0 } })}
|
|
631
|
-
title=${i18next.t('figure.text.pivot-defaults-to-the-part-centre')}
|
|
632
|
-
>
|
|
633
|
-
${i18next.t(on ? 'figure.button.pivot-clear' : 'figure.button.pivot-set')}
|
|
634
|
-
</button>
|
|
635
|
-
${on
|
|
636
|
-
? AXES.map(axis => html `
|
|
637
|
-
<input
|
|
638
|
-
type="number"
|
|
639
|
-
step="1"
|
|
640
|
-
.value=${live(String(pivot[axis]))}
|
|
641
|
-
aria-label=${axis}
|
|
642
|
-
@change=${(e) => this.setChannel(kind, at, j, {
|
|
643
|
-
pivot: { ...pivot, [axis]: Number(e.target.value) || 0 }
|
|
644
|
-
})}
|
|
645
|
-
/>
|
|
646
|
-
`)
|
|
647
|
-
: nothing}
|
|
648
|
-
</div>
|
|
649
|
-
`;
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* 키프레임 표.
|
|
653
|
-
*
|
|
654
|
-
* **둘 미만으로 못 내려간다.** 하나뿐이면 움직이는 것이 아니라 자세 하나이고, 형식이
|
|
655
|
-
* 오류로 막는다. 저장하고 나서 알려 주는 것보다 지우는 단추를 죽이는 것이 낫다.
|
|
656
|
-
*
|
|
657
|
-
* 시각이 오름차순이어야 하는 것도 형식의 규칙이다. 여기서는 어긋난 줄을 붉게 내고 값을
|
|
658
|
-
* 고치지는 않는다 — 저작자가 3 을 1 로 고치던 중일 수 있다.
|
|
659
|
-
*/
|
|
660
|
-
/**
|
|
661
|
-
* Keys of a joint channel: a position and one joint coordinate each. A value outside the joint's limits
|
|
662
|
-
* is marked and left as typed; the format refuses it on save.
|
|
663
|
-
*/
|
|
664
|
-
renderJointKeys(kind, channel, at, j, limits) {
|
|
665
|
-
const keys = channel.keys ?? [];
|
|
666
|
-
const onValues = kind === 'parameter';
|
|
667
|
-
const beyond = (key) => !!limits && (key.value < limits.min || key.value > limits.max);
|
|
668
|
-
return html `
|
|
669
|
-
<table keys>
|
|
670
|
-
<tr>
|
|
671
|
-
<th>${i18next.t(onValues ? 'figure.label.key-at-value' : 'figure.label.key-at')}</th>
|
|
672
|
-
<th>${i18next.t('figure.label.joint-value')}</th>
|
|
673
|
-
<th></th>
|
|
674
|
-
</tr>
|
|
675
|
-
${keys.map((key, k) => {
|
|
676
|
-
const backwards = k > 0 && key.at <= (keys[k - 1].at ?? 0);
|
|
677
|
-
return html `
|
|
678
|
-
<tr>
|
|
679
|
-
<td>
|
|
680
|
-
<input
|
|
681
|
-
type="number"
|
|
682
|
-
step=${onValues ? '0.05' : '0.1'}
|
|
683
|
-
min="0"
|
|
684
|
-
max=${onValues ? '1' : nothing}
|
|
685
|
-
?bad=${backwards || key.at < 0 || (onValues && key.at > 1)}
|
|
686
|
-
.value=${live(String(key.at))}
|
|
687
|
-
@change=${(e) => this.setJointKey(kind, at, j, k, { at: Math.max(0, Number(e.target.value) || 0) })}
|
|
688
|
-
/>
|
|
689
|
-
</td>
|
|
690
|
-
<td>
|
|
691
|
-
<input
|
|
692
|
-
type="number"
|
|
693
|
-
step="1"
|
|
694
|
-
?bad=${beyond(key)}
|
|
695
|
-
.value=${live(String(key.value))}
|
|
696
|
-
title=${beyond(key) ? i18next.t('figure.text.joint-value-beyond-limits') : ''}
|
|
697
|
-
@change=${(e) => this.setJointKey(kind, at, j, k, { value: Number(e.target.value) || 0 })}
|
|
698
|
-
/>
|
|
699
|
-
</td>
|
|
700
|
-
<td drop>
|
|
701
|
-
<button
|
|
702
|
-
plain
|
|
703
|
-
?disabled=${keys.length <= 2}
|
|
704
|
-
title=${keys.length <= 2 ? i18next.t('figure.text.two-keys-at-least') : i18next.t('figure.button.remove')}
|
|
705
|
-
@click=${() => this.removeKey(kind, at, j, k)}
|
|
706
|
-
>
|
|
707
|
-
<md-icon>close</md-icon>
|
|
708
|
-
</button>
|
|
709
|
-
</td>
|
|
710
|
-
</tr>
|
|
711
|
-
`;
|
|
712
|
-
})}
|
|
713
|
-
</table>
|
|
714
|
-
${keys.some(beyond) ? html `<p warn>${i18next.t('figure.text.joint-value-beyond-limits')}</p>` : nothing}
|
|
715
|
-
<button @click=${() => this.addKey(kind, at, j)}>
|
|
716
|
-
<md-icon>add</md-icon>${i18next.t('figure.button.add-key')}
|
|
717
|
-
</button>
|
|
718
|
-
`;
|
|
719
|
-
}
|
|
720
|
-
renderKeys(kind, channel, at, j) {
|
|
721
|
-
const keys = channel.keys ?? [];
|
|
722
|
-
/*
|
|
723
|
-
Keys on a parameter curve sit on the 0..1 value span (ADR-0051). A key above 1 is unreachable and
|
|
724
|
-
the format refuses it. The screen marks it and leaves the number alone: rewriting what the author
|
|
725
|
-
typed would hide what they wrote.
|
|
726
|
-
*/
|
|
727
|
-
const onValues = kind === 'parameter';
|
|
728
|
-
const outside = (key) => onValues && key.at > 1;
|
|
729
|
-
return html `
|
|
730
|
-
<table keys>
|
|
731
|
-
<tr>
|
|
732
|
-
<th>${i18next.t(onValues ? 'figure.label.key-at-value' : 'figure.label.key-at')}</th>
|
|
733
|
-
${AXES.map(axis => html `<th>${axis}</th>`)}
|
|
734
|
-
<th></th>
|
|
735
|
-
</tr>
|
|
736
|
-
${keys.map((key, k) => {
|
|
737
|
-
const backwards = k > 0 && key.at <= (keys[k - 1].at ?? 0);
|
|
738
|
-
return html `
|
|
739
|
-
<tr>
|
|
740
|
-
<td>
|
|
741
|
-
<input
|
|
742
|
-
type="number"
|
|
743
|
-
step=${onValues ? '0.05' : '0.1'}
|
|
744
|
-
min="0"
|
|
745
|
-
max=${onValues ? '1' : nothing}
|
|
746
|
-
?bad=${backwards || key.at < 0 || outside(key)}
|
|
747
|
-
.value=${live(String(key.at))}
|
|
748
|
-
title=${backwards
|
|
749
|
-
? i18next.t('figure.text.keys-must-rise')
|
|
750
|
-
: outside(key)
|
|
751
|
-
? i18next.t('figure.text.parameter-key-within-span')
|
|
752
|
-
: ''}
|
|
753
|
-
@change=${(e) => this.setKey(kind, at, j, k, { at: Math.max(0, Number(e.target.value) || 0) })}
|
|
754
|
-
/>
|
|
755
|
-
</td>
|
|
756
|
-
${AXES.map(axis => html `
|
|
757
|
-
<td>
|
|
758
|
-
<input
|
|
759
|
-
type="number"
|
|
760
|
-
step=${channel.path === 'scale' ? '0.1' : '1'}
|
|
761
|
-
.value=${live(String(key.value[axis]))}
|
|
762
|
-
aria-label=${axis}
|
|
763
|
-
@change=${(e) => this.setKey(kind, at, j, k, {
|
|
764
|
-
value: { ...key.value, [axis]: Number(e.target.value) || 0 }
|
|
765
|
-
})}
|
|
766
|
-
/>
|
|
767
|
-
</td>
|
|
768
|
-
`)}
|
|
769
|
-
<td drop>
|
|
770
|
-
<button
|
|
771
|
-
plain
|
|
772
|
-
?disabled=${keys.length <= 2}
|
|
773
|
-
title=${keys.length <= 2 ? i18next.t('figure.text.two-keys-at-least') : i18next.t('figure.button.remove')}
|
|
774
|
-
@click=${() => this.removeKey(kind, at, j, k)}
|
|
775
|
-
>
|
|
776
|
-
<md-icon>close</md-icon>
|
|
777
|
-
</button>
|
|
778
|
-
</td>
|
|
779
|
-
</tr>
|
|
780
|
-
`;
|
|
781
|
-
})}
|
|
782
|
-
</table>
|
|
783
|
-
|
|
784
|
-
${keys.some((key, k) => k > 0 && key.at <= (keys[k - 1].at ?? 0))
|
|
785
|
-
? html `<p warn>${i18next.t('figure.text.keys-must-rise')}</p>`
|
|
786
|
-
: nothing}
|
|
787
|
-
${keys.some(outside) ? html `<p warn>${i18next.t('figure.text.parameter-key-within-span')}</p>` : nothing}
|
|
788
|
-
|
|
789
|
-
<button @click=${() => this.addKey(kind, at, j)}>
|
|
790
|
-
<md-icon>add</md-icon>${i18next.t('figure.button.add-key')}
|
|
791
|
-
</button>
|
|
792
|
-
`;
|
|
793
|
-
}
|
|
794
|
-
/**
|
|
795
|
-
* 따로 움직이는 부품 예산.
|
|
796
|
-
*
|
|
797
|
-
* **넘어도 막지 않는다.** 제 변환을 지는 부품은 저마다 단독 묶음이라 draw call 이 늘고,
|
|
798
|
-
* 그만큼 한 화면에 함께 세울 수 있는 대수가 준다 — 그 대가가 수로 보이면 저작자가 아낀다.
|
|
799
|
-
* 랜드마크 설비 하나는 정교해도 되고, 그 값이 도면 예산에 드러나면 된다.
|
|
800
|
-
*/
|
|
801
|
-
renderBudget() {
|
|
802
|
-
const used = this.movingParts;
|
|
803
|
-
const over = used > LIMITS.independentParts;
|
|
804
|
-
return html `
|
|
805
|
-
<section>
|
|
806
|
-
<h3>${i18next.t('figure.label.budget-independent')}</h3>
|
|
807
|
-
<div budget ?over=${over}>
|
|
808
|
-
<strong>${used}</strong>
|
|
809
|
-
<span quiet>/ ${LIMITS.independentParts}</span>
|
|
810
|
-
</div>
|
|
811
|
-
<p quiet>${i18next.t('figure.text.independent-parts-cost')}</p>
|
|
812
|
-
${over ? html `<p warn>${i18next.t('figure.text.independent-parts-over')}</p>` : nothing}
|
|
813
|
-
</section>
|
|
814
|
-
`;
|
|
815
|
-
}
|
|
816
|
-
// ─── 고치기 ───────────────────────────────────────────────────────────────
|
|
817
|
-
/**
|
|
818
|
-
* 초안에 실어 올려보낸다.
|
|
819
|
-
*
|
|
820
|
-
* `animations` 는 도형 전체의 것이므로 부품 편집(`update-part`)이 아니라 `update-draft` 로
|
|
821
|
-
* 간다. 빈 배열은 칸을 지운다 — clip 을 다 지운 도형은 `animations: []` 가 아니라 없는 것이다.
|
|
822
|
-
*/
|
|
823
|
-
tell(over) {
|
|
824
|
-
if (!this.draft)
|
|
825
|
-
return;
|
|
826
|
-
const draft = { ...this.draft, ...over };
|
|
827
|
-
if (!draft.animations?.length)
|
|
828
|
-
delete draft.animations;
|
|
829
|
-
if (!draft.parameters?.length)
|
|
830
|
-
delete draft.parameters;
|
|
831
|
-
this.dispatchEvent(new CustomEvent('update-draft', { detail: { draft }, bubbles: true, composed: true }));
|
|
832
|
-
}
|
|
833
|
-
patchClip(at, patch) {
|
|
834
|
-
this.tell({ animations: this.clips.map((clip, i) => (i === at ? { ...clip, ...patch } : clip)) });
|
|
835
|
-
}
|
|
836
|
-
patchParameter(at, patch) {
|
|
837
|
-
this.tell({ parameters: this.parameters.map((one, i) => (i === at ? { ...one, ...patch } : one)) });
|
|
838
|
-
}
|
|
839
|
-
/** 채널 묶음을 통째로 얹는다. **어느 목록이든 같은 자리에 닿는다.** */
|
|
840
|
-
setChannels(kind, at, channels) {
|
|
841
|
-
if (kind === 'clip')
|
|
842
|
-
this.patchClip(at, { channels });
|
|
843
|
-
else {
|
|
844
|
-
const parameter = this.parameters[at];
|
|
845
|
-
if (!parameter)
|
|
846
|
-
return;
|
|
847
|
-
this.patchParameter(at, { clip: { ...parameter.clip, channels } });
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
patchChannel(kind, at, j, next) {
|
|
851
|
-
this.setChannels(kind, at, this.channelsOf(kind, at).map((channel, i) => (i === j ? next : channel)));
|
|
852
|
-
}
|
|
853
|
-
addClip() {
|
|
854
|
-
const target = this.partNames[0];
|
|
855
|
-
if (!target)
|
|
856
|
-
return;
|
|
857
|
-
const clip = {
|
|
858
|
-
name: freeName(this.allNames, 'clip-1'),
|
|
859
|
-
channels: [{ target, path: 'translation', keys: keysFor('translation') }]
|
|
860
|
-
};
|
|
861
|
-
this.open = `clip:${this.clips.length}`;
|
|
862
|
-
this.tell({ animations: [...this.clips, clip] });
|
|
863
|
-
}
|
|
864
|
-
addParameter() {
|
|
865
|
-
const target = this.partNames[0];
|
|
866
|
-
if (!target)
|
|
867
|
-
return;
|
|
868
|
-
const parameter = {
|
|
869
|
-
name: freeName(this.allNames, 'parameter-1'),
|
|
870
|
-
range: { ...NEW_RANGE },
|
|
871
|
-
clip: { channels: [{ target, path: 'translation', keys: keysFor('translation') }] }
|
|
872
|
-
};
|
|
873
|
-
this.open = `parameter:${this.parameters.length}`;
|
|
874
|
-
this.tell({ parameters: [...this.parameters, parameter] });
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Recipe creates a valid, editable two-pose motion; it never saves or publishes the Figure.
|
|
878
|
-
*
|
|
879
|
-
* 어느 목록으로 가는지는 레시피가 안다 — 도는 롤러는 애니메이션이고 열리는 문은
|
|
880
|
-
* 파라미터다. 저작자에게 그 갈래를 먼저 고르게 하지 않는 이유가 이것이다.
|
|
881
|
-
*/
|
|
882
|
-
addRecipe(recipe) {
|
|
883
|
-
const target = this.recipeTarget || this.partNames[0];
|
|
884
|
-
if (!target)
|
|
885
|
-
return;
|
|
886
|
-
const channel = {
|
|
887
|
-
target,
|
|
888
|
-
path: recipe.path,
|
|
889
|
-
keys: [{ at: 0, value: startValue(recipe.path) }, { at: 1, value: { ...recipe.end } }]
|
|
890
|
-
};
|
|
891
|
-
if (recipe.pivot)
|
|
892
|
-
channel.pivot = { ...recipe.pivot };
|
|
893
|
-
const name = freeName(this.allNames, recipe.name);
|
|
894
|
-
if (recipe.kind === 'clip') {
|
|
895
|
-
this.open = `clip:${this.clips.length}`;
|
|
896
|
-
this.tell({ animations: [...this.clips, { name, channels: [channel] }] });
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
this.open = `parameter:${this.parameters.length}`;
|
|
900
|
-
this.tell({
|
|
901
|
-
parameters: [...this.parameters, { name, range: { ...(recipe.range ?? NEW_RANGE) }, clip: { channels: [channel] } }]
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
renameClip(at, name) {
|
|
905
|
-
this.patchClip(at, { name: name.trim() });
|
|
906
|
-
}
|
|
907
|
-
renameParameter(at, name) {
|
|
908
|
-
this.patchParameter(at, { name: name.trim() });
|
|
909
|
-
}
|
|
910
|
-
/**
|
|
911
|
-
* 단위 칸 — **목록이 보이는 채로, 적는 길도 열어 둔다.**
|
|
912
|
-
*
|
|
913
|
-
* 처음에는 `datalist` 를 붙였다. 그것은 브라우저가 쥐고 있는 기능이라 **눌러 보기 전에는 목록이
|
|
914
|
-
* 있는지조차 안 보인다** — 화살표도 없고, 어디에 어떻게 뜨는지도 브라우저가 정한다. 고를 수
|
|
915
|
-
* 있다는 것을 알리는 것이 이 칸의 목적이므로 `select` 로 바꿨다.
|
|
916
|
-
*
|
|
917
|
-
* 형식은 단위를 규칙으로 쓰지 않으므로(표시용 말이다) 목록에 가두지 않는다. 마지막 줄이
|
|
918
|
-
* 「직접 적기」이고, 목록에 없는 말이 이미 적혀 있으면 처음부터 적는 칸으로 연다.
|
|
919
|
-
*/
|
|
920
|
-
renderUnit(at, unit) {
|
|
921
|
-
const listed = PARAMETER_UNITS.includes(unit);
|
|
922
|
-
const typing = this.typingUnit.has(at) || (unit !== '' && !listed);
|
|
923
|
-
if (typing) {
|
|
924
|
-
return html `<input
|
|
925
|
-
unit
|
|
926
|
-
.value=${live(unit)}
|
|
927
|
-
placeholder="mm"
|
|
928
|
-
aria-label="unit"
|
|
929
|
-
title=${i18next.t('figure.text.unit-is-a-word-not-a-rule')}
|
|
930
|
-
@change=${(e) => this.setRange(at, { unit: e.target.value.trim() })}
|
|
931
|
-
/>`;
|
|
932
|
-
}
|
|
933
|
-
return html `<select
|
|
934
|
-
unit
|
|
935
|
-
aria-label="unit"
|
|
936
|
-
title=${i18next.t('figure.text.unit-is-a-word-not-a-rule')}
|
|
937
|
-
@change=${(e) => this.chooseUnit(at, e.target.value)}
|
|
938
|
-
>
|
|
939
|
-
<option value="" ?selected=${unit === ''}>${i18next.t('figure.label.unit-none')}</option>
|
|
940
|
-
${PARAMETER_UNITS.map(one => html `<option value=${one} ?selected=${one === unit}>${one}</option>`)}
|
|
941
|
-
<option value=${UNIT_TYPE_IT}>${i18next.t('figure.label.unit-type-it')}</option>
|
|
942
|
-
</select>`;
|
|
943
|
-
}
|
|
944
|
-
/** 고른 것을 넣는다. 「직접 적기」는 값이 아니라 **칸을 바꾸는 것**이다. */
|
|
945
|
-
chooseUnit(at, value) {
|
|
946
|
-
if (value === UNIT_TYPE_IT) {
|
|
947
|
-
this.typingUnit = new Set([...this.typingUnit, at]);
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
const next = new Set(this.typingUnit);
|
|
951
|
-
next.delete(at);
|
|
952
|
-
this.typingUnit = next;
|
|
953
|
-
this.setRange(at, { unit: value });
|
|
954
|
-
}
|
|
955
|
-
setRange(at, patch) {
|
|
956
|
-
const parameter = this.parameters[at];
|
|
957
|
-
if (!parameter)
|
|
958
|
-
return;
|
|
959
|
-
this.patchParameter(at, { range: { ...(parameter.range ?? NEW_RANGE), ...patch } });
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Transition time. Clearing the input removes the field, which means instant. 0 is instant too, but
|
|
963
|
-
* a stored 0 and a missing field are different sources, so an empty input is never saved as 0.
|
|
964
|
-
*/
|
|
965
|
-
setDuration(at, raw) {
|
|
966
|
-
const parameter = this.parameters[at];
|
|
967
|
-
if (!parameter)
|
|
968
|
-
return;
|
|
969
|
-
const { duration: _dropped, ...rest } = parameter.clip;
|
|
970
|
-
const text = raw.trim();
|
|
971
|
-
const clip = text === '' || !Number.isFinite(Number(text)) ? rest : { ...rest, duration: Number(text) };
|
|
972
|
-
this.patchParameter(at, { clip });
|
|
973
|
-
}
|
|
974
|
-
setDefault(at, value) {
|
|
975
|
-
this.patchParameter(at, { default: value });
|
|
976
|
-
}
|
|
977
|
-
removeClip(at) {
|
|
978
|
-
this.open = '';
|
|
979
|
-
this.tell({ animations: this.clips.filter((_, i) => i !== at) });
|
|
980
|
-
}
|
|
981
|
-
removeParameter(at) {
|
|
982
|
-
this.open = '';
|
|
983
|
-
this.tell({ parameters: this.parameters.filter((_, i) => i !== at) });
|
|
984
|
-
}
|
|
985
|
-
addChannel(kind, at) {
|
|
986
|
-
const target = this.partNames[0];
|
|
987
|
-
if (!target)
|
|
988
|
-
return;
|
|
989
|
-
this.setChannels(kind, at, [
|
|
990
|
-
...this.channelsOf(kind, at),
|
|
991
|
-
{ target, path: 'translation', keys: keysFor('translation') }
|
|
992
|
-
]);
|
|
993
|
-
}
|
|
994
|
-
/**
|
|
995
|
-
* Points a channel at another part or joint. Crossing between the two changes the channel's shape: a
|
|
996
|
-
* joint channel has no path and one number per key, a part channel a path and a vector. The key positions
|
|
997
|
-
* are kept, since they are the author's timing; values restart at the rest pose.
|
|
998
|
-
*/
|
|
999
|
-
retarget(kind, at, j, target) {
|
|
1000
|
-
const channel = this.channelsOf(kind, at)[j];
|
|
1001
|
-
if (!channel)
|
|
1002
|
-
return;
|
|
1003
|
-
const toJoint = this.joints.some(one => one.name === target);
|
|
1004
|
-
if (toJoint && !isJointChannel(channel)) {
|
|
1005
|
-
const next = { target, keys: channel.keys.map(key => ({ at: key.at, value: 0 })) };
|
|
1006
|
-
if (channel.interpolation !== undefined)
|
|
1007
|
-
next.interpolation = channel.interpolation;
|
|
1008
|
-
this.patchChannel(kind, at, j, next);
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
if (!toJoint && isJointChannel(channel)) {
|
|
1012
|
-
const next = { target, path: 'translation', keys: channel.keys.map(key => ({ at: key.at, value: startValue('translation') })) };
|
|
1013
|
-
if (channel.interpolation !== undefined)
|
|
1014
|
-
next.interpolation = channel.interpolation;
|
|
1015
|
-
this.patchChannel(kind, at, j, next);
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
this.patchChannel(kind, at, j, { ...channel, target });
|
|
1019
|
-
}
|
|
1020
|
-
setChannel(kind, at, j, patch) {
|
|
1021
|
-
const channel = this.channelsOf(kind, at)[j];
|
|
1022
|
-
if (!channel || isJointChannel(channel))
|
|
1023
|
-
return;
|
|
1024
|
-
const next = { ...channel, ...patch };
|
|
1025
|
-
/* `undefined` 를 얹는 것은 지우는 뜻이다. 남겨 두면 저장 형식에 빈 칸이 실린다. */
|
|
1026
|
-
if ('pivot' in patch && patch.pivot === undefined)
|
|
1027
|
-
delete next.pivot;
|
|
1028
|
-
this.patchChannel(kind, at, j, next);
|
|
1029
|
-
}
|
|
1030
|
-
/**
|
|
1031
|
-
* 경로를 바꾼다 — **값까지 함께 옮긴다.**
|
|
1032
|
-
*
|
|
1033
|
-
* 옮김·회전의 0 은 「제자리」인데 배율의 0 은 **부품을 지운다.** 경로만 바꾸고 값을 그대로
|
|
1034
|
-
* 두면 저작자는 부품이 사라진 것을 보고 도구가 고장 난 줄 안다. 그래서 옮김·회전 ↔ 배율을
|
|
1035
|
-
* 건널 때 시작값을 그 경로의 제자리로 되돌린다. 시각은 지킨다 — 저작자가 짠 박자다.
|
|
1036
|
-
*/
|
|
1037
|
-
setPath(kind, at, j, path) {
|
|
1038
|
-
const channel = this.channelsOf(kind, at)[j];
|
|
1039
|
-
if (!channel || isJointChannel(channel))
|
|
1040
|
-
return;
|
|
1041
|
-
const crossing = (channel.path === 'scale') !== (path === 'scale');
|
|
1042
|
-
const next = {
|
|
1043
|
-
...channel,
|
|
1044
|
-
path,
|
|
1045
|
-
keys: crossing ? channel.keys.map(key => ({ at: key.at, value: startValue(path) })) : channel.keys
|
|
1046
|
-
};
|
|
1047
|
-
/* 회전이 아니면 회전 중심은 쓰이지 않는다. 남겨 두면 검증이 「쓰이지 않는다」고 알린다. */
|
|
1048
|
-
if (path !== 'rotation')
|
|
1049
|
-
delete next.pivot;
|
|
1050
|
-
this.patchChannel(kind, at, j, next);
|
|
1051
|
-
}
|
|
1052
|
-
removeChannel(kind, at, j) {
|
|
1053
|
-
this.setChannels(kind, at, this.channelsOf(kind, at).filter((_, i) => i !== j));
|
|
1054
|
-
}
|
|
1055
|
-
/** 마지막 키에서 1 초 뒤. 값은 그 키를 그대로 받는다 — 붙여 놓고 고치는 것이 짜기 쉽다. */
|
|
1056
|
-
addKey(kind, at, j) {
|
|
1057
|
-
const channel = this.channelsOf(kind, at)[j];
|
|
1058
|
-
if (!channel)
|
|
1059
|
-
return;
|
|
1060
|
-
if (isJointChannel(channel)) {
|
|
1061
|
-
const keys = channel.keys;
|
|
1062
|
-
const end = keys[keys.length - 1];
|
|
1063
|
-
if (kind === 'parameter' && keys.length >= 2) {
|
|
1064
|
-
const before = keys[keys.length - 2];
|
|
1065
|
-
const middle = { at: (before.at + end.at) / 2, value: before.value };
|
|
1066
|
-
this.patchChannel(kind, at, j, { ...channel, keys: [...keys.slice(0, -1), middle, end] });
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
this.patchChannel(kind, at, j, { ...channel, keys: [...keys, { at: (end?.at ?? 0) + 1, value: end?.value ?? 0 }] });
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
const last = channel.keys[channel.keys.length - 1];
|
|
1073
|
-
/*
|
|
1074
|
-
A parameter curve ends at 1, so appending a key would make it unreachable. Insert it between the last two keys.
|
|
1075
|
-
*/
|
|
1076
|
-
if (kind === 'parameter' && channel.keys.length >= 2) {
|
|
1077
|
-
const before = channel.keys[channel.keys.length - 2];
|
|
1078
|
-
const middle = { at: (before.at + last.at) / 2, value: { ...before.value } };
|
|
1079
|
-
this.patchChannel(kind, at, j, { ...channel, keys: [...channel.keys.slice(0, -1), middle, last] });
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
const key = { at: (last?.at ?? 0) + 1, value: { ...(last?.value ?? startValue(channel.path)) } };
|
|
1083
|
-
this.patchChannel(kind, at, j, { ...channel, keys: [...channel.keys, key] });
|
|
1084
|
-
}
|
|
1085
|
-
setJointKey(kind, at, j, k, patch) {
|
|
1086
|
-
const channel = this.channelsOf(kind, at)[j];
|
|
1087
|
-
if (!channel || !isJointChannel(channel))
|
|
1088
|
-
return;
|
|
1089
|
-
this.patchChannel(kind, at, j, { ...channel, keys: channel.keys.map((key, i) => (i === k ? { ...key, ...patch } : key)) });
|
|
1090
|
-
}
|
|
1091
|
-
setKey(kind, at, j, k, patch) {
|
|
1092
|
-
const channel = this.channelsOf(kind, at)[j];
|
|
1093
|
-
if (!channel || isJointChannel(channel))
|
|
1094
|
-
return;
|
|
1095
|
-
this.patchChannel(kind, at, j, {
|
|
1096
|
-
...channel,
|
|
1097
|
-
keys: channel.keys.map((key, i) => (i === k ? { ...key, ...patch } : key))
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
/** 둘 미만으로 내려가지 않는다 — 단추가 이미 죽어 있지만 밖에서 불릴 수 있다. */
|
|
1101
|
-
removeKey(kind, at, j, k) {
|
|
1102
|
-
const channel = this.channelsOf(kind, at)[j];
|
|
1103
|
-
if (!channel || channel.keys.length <= 2)
|
|
1104
|
-
return;
|
|
1105
|
-
this.patchChannel(kind, at, j, { ...channel, keys: channel.keys.filter((_, i) => i !== k) });
|
|
1106
|
-
}
|
|
1107
|
-
};
|
|
1108
|
-
__decorate([
|
|
1109
|
-
property({ type: Object }),
|
|
1110
|
-
__metadata("design:type", Object)
|
|
1111
|
-
], FigureAnimations.prototype, "draft", void 0);
|
|
1112
|
-
__decorate([
|
|
1113
|
-
property({ type: Array }),
|
|
1114
|
-
__metadata("design:type", Array)
|
|
1115
|
-
], FigureAnimations.prototype, "parts", void 0);
|
|
1116
|
-
__decorate([
|
|
1117
|
-
state(),
|
|
1118
|
-
__metadata("design:type", Object)
|
|
1119
|
-
], FigureAnimations.prototype, "open", void 0);
|
|
1120
|
-
__decorate([
|
|
1121
|
-
state(),
|
|
1122
|
-
__metadata("design:type", Object)
|
|
1123
|
-
], FigureAnimations.prototype, "recipeTarget", void 0);
|
|
1124
|
-
__decorate([
|
|
1125
|
-
state(),
|
|
1126
|
-
__metadata("design:type", Object)
|
|
1127
|
-
], FigureAnimations.prototype, "typingUnit", void 0);
|
|
1128
|
-
FigureAnimations = __decorate([
|
|
1129
|
-
customElement('figure-animations')
|
|
1130
|
-
], FigureAnimations);
|
|
1131
|
-
export { FigureAnimations };
|
|
1132
|
-
//# sourceMappingURL=figure-animations.js.map
|