@things-factory/figure-ui 10.1.25 → 10.1.27
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 +4 -1
- package/client/modeller/figure-animations.ts +229 -29
- package/client/modeller/figure-history-panel.ts +2 -2
- package/client/modeller/figure-inspector.ts +151 -5
- package/client/modeller/figure-preview.ts +5 -5
- package/client/modeller/figure-source.ts +33 -45
- package/client/modeller/figure-view.ts +20 -0
- package/client/modeller/joint-edits.ts +201 -0
- package/client/modeller/proposal-session.ts +8 -2
- package/client/pages/figure-modeller-page.ts +50 -15
- package/dist-client/graphql/index.js +4 -1
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-animations.d.ts +19 -0
- package/dist-client/modeller/figure-animations.js +206 -22
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.js +2 -2
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +11 -0
- package/dist-client/modeller/figure-inspector.js +124 -4
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +5 -5
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +12 -1
- package/dist-client/modeller/figure-source.js +13 -40
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +16 -0
- package/dist-client/modeller/figure-view.js +15 -0
- package/dist-client/modeller/figure-view.js.map +1 -1
- package/dist-client/modeller/joint-edits.d.ts +29 -0
- package/dist-client/modeller/joint-edits.js +175 -0
- package/dist-client/modeller/joint-edits.js.map +1 -0
- package/dist-client/modeller/proposal-session.d.ts +5 -0
- package/dist-client/modeller/proposal-session.js +3 -2
- package/dist-client/modeller/proposal-session.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +2 -22
- package/dist-client/pages/figure-modeller-page.js +46 -15
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +3 -3
- package/test/ai-proposal-contract.test.ts +3 -2
- package/test/figure-source.test.ts +43 -57
- package/test/i18n-prefix-guard.test.ts +2 -3
- package/test/joint-edits.test.ts +130 -0
- package/test/preview-board-depth.test.ts +27 -0
- package/translations/en.json +119 -80
- package/translations/ja.json +208 -58
- package/translations/ko.json +139 -100
- package/translations/ms.json +206 -56
- package/translations/zh.json +207 -57
|
@@ -19,6 +19,8 @@ export declare class FigureAnimations extends FigureAnimations_base {
|
|
|
19
19
|
private channelsOf;
|
|
20
20
|
/** 부품 이름 — 채널이 이것을 가리킨다. */
|
|
21
21
|
private get partNames();
|
|
22
|
+
/** Joints a channel can drive instead of a part (ADR-0066). */
|
|
23
|
+
private get joints();
|
|
22
24
|
/**
|
|
23
25
|
* 몇 부품이 움직이나 — 채널이 가리키는 **서로 다른 부품**의 수.
|
|
24
26
|
*
|
|
@@ -69,6 +71,11 @@ export declare class FigureAnimations extends FigureAnimations_base {
|
|
|
69
71
|
* 시각이 오름차순이어야 하는 것도 형식의 규칙이다. 여기서는 어긋난 줄을 붉게 내고 값을
|
|
70
72
|
* 고치지는 않는다 — 저작자가 3 을 1 로 고치던 중일 수 있다.
|
|
71
73
|
*/
|
|
74
|
+
/**
|
|
75
|
+
* Keys of a joint channel: a position and one joint coordinate each. A value outside the joint's limits
|
|
76
|
+
* is marked and left as typed; the format refuses it on save.
|
|
77
|
+
*/
|
|
78
|
+
private renderJointKeys;
|
|
72
79
|
private renderKeys;
|
|
73
80
|
/**
|
|
74
81
|
* 움직이는 부품 예산.
|
|
@@ -102,10 +109,21 @@ export declare class FigureAnimations extends FigureAnimations_base {
|
|
|
102
109
|
private renameClip;
|
|
103
110
|
private renameParameter;
|
|
104
111
|
private setRange;
|
|
112
|
+
/**
|
|
113
|
+
* Transition time. Clearing the input removes the field, which means instant. 0 is instant too, but
|
|
114
|
+
* a stored 0 and a missing field are different sources, so an empty input is never saved as 0.
|
|
115
|
+
*/
|
|
116
|
+
private setDuration;
|
|
105
117
|
private setDefault;
|
|
106
118
|
private removeClip;
|
|
107
119
|
private removeParameter;
|
|
108
120
|
private addChannel;
|
|
121
|
+
/**
|
|
122
|
+
* Points a channel at another part or joint. Crossing between the two changes the channel's shape: a
|
|
123
|
+
* joint channel has no path and one number per key, a part channel a path and a vector. The key positions
|
|
124
|
+
* are kept, since they are the author's timing; values restart at the rest pose.
|
|
125
|
+
*/
|
|
126
|
+
private retarget;
|
|
109
127
|
private setChannel;
|
|
110
128
|
/**
|
|
111
129
|
* 경로를 바꾼다 — **값까지 함께 옮긴다.**
|
|
@@ -118,6 +136,7 @@ export declare class FigureAnimations extends FigureAnimations_base {
|
|
|
118
136
|
private removeChannel;
|
|
119
137
|
/** 마지막 키에서 1 초 뒤. 값은 그 키를 그대로 받는다 — 붙여 놓고 고치는 것이 짜기 쉽다. */
|
|
120
138
|
private addKey;
|
|
139
|
+
private setJointKey;
|
|
121
140
|
private setKey;
|
|
122
141
|
/** 둘 미만으로 내려가지 않는다 — 단추가 이미 죽어 있지만 밖에서 불릴 수 있다. */
|
|
123
142
|
private removeKey;
|
|
@@ -5,6 +5,10 @@ import { live } from 'lit/directives/live.js';
|
|
|
5
5
|
import { i18next, localize } from '@operato/i18n';
|
|
6
6
|
import { ScrollbarStyles } from '@operato/styles';
|
|
7
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
|
+
}
|
|
8
12
|
/*
|
|
9
13
|
레시피의 `end` 가 곧 그 범위의 끝 자세다 — 리프트가 100mm 올라가는 곡선이면 범위도
|
|
10
14
|
0~100mm 다. 둘이 어긋나면 저작자가 1200 을 주고도 100 만 올라가는 것을 보게 된다.
|
|
@@ -291,6 +295,10 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
291
295
|
get partNames() {
|
|
292
296
|
return this.parts.map(part => part.name).filter(name => !!name);
|
|
293
297
|
}
|
|
298
|
+
/** Joints a channel can drive instead of a part (ADR-0066). */
|
|
299
|
+
get joints() {
|
|
300
|
+
return this.draft?.joints ?? [];
|
|
301
|
+
}
|
|
294
302
|
/**
|
|
295
303
|
* 몇 부품이 움직이나 — 채널이 가리키는 **서로 다른 부품**의 수.
|
|
296
304
|
*
|
|
@@ -315,7 +323,6 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
315
323
|
return html `
|
|
316
324
|
<section>
|
|
317
325
|
<h3>${i18next.t('figure.label.animation')}</h3>
|
|
318
|
-
<p quiet>${i18next.t('figure.text.animation-is-driven-by-a-fact')}</p>
|
|
319
326
|
</section>
|
|
320
327
|
|
|
321
328
|
${this.renderRecipes()}
|
|
@@ -418,6 +425,7 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
418
425
|
const nameless = !parameter.name?.trim();
|
|
419
426
|
const range = parameter.range ?? NEW_RANGE;
|
|
420
427
|
const backwards = !(range.max > range.min);
|
|
428
|
+
const slowness = parameter.clip?.duration ?? 0;
|
|
421
429
|
return html `
|
|
422
430
|
<div clip ?on=${on}>
|
|
423
431
|
<div head>
|
|
@@ -481,6 +489,21 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
481
489
|
/>
|
|
482
490
|
</div>
|
|
483
491
|
|
|
492
|
+
<div row>
|
|
493
|
+
<label>${i18next.t('figure.label.parameter-duration')}</label>
|
|
494
|
+
<input
|
|
495
|
+
type="number"
|
|
496
|
+
step="0.1"
|
|
497
|
+
min="0"
|
|
498
|
+
?bad=${slowness < 0}
|
|
499
|
+
.value=${live(parameter.clip?.duration === undefined ? '' : String(parameter.clip.duration))}
|
|
500
|
+
placeholder="0"
|
|
501
|
+
aria-label="duration"
|
|
502
|
+
@change=${(e) => this.setDuration(at, e.target.value)}
|
|
503
|
+
/>
|
|
504
|
+
</div>
|
|
505
|
+
<p quiet>${i18next.t('figure.text.parameter-duration-empty-is-instant')}</p>
|
|
506
|
+
|
|
484
507
|
${on ? this.renderChannels('parameter', at) : nothing}
|
|
485
508
|
</div>
|
|
486
509
|
`;
|
|
@@ -505,23 +528,45 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
505
528
|
* 검증은 저장할 때 말하고 여기서는 **그 자리에서** 보여 준다.
|
|
506
529
|
*/
|
|
507
530
|
renderChannel(kind, channel, at, j) {
|
|
508
|
-
const
|
|
531
|
+
const joint = this.joints.find(one => one.name === channel.target);
|
|
532
|
+
const orphan = !this.partNames.includes(channel.target) && !joint;
|
|
533
|
+
const target = html `
|
|
534
|
+
<div row>
|
|
535
|
+
<label>${i18next.t('figure.label.channel-target')}</label>
|
|
536
|
+
<select
|
|
537
|
+
.value=${live(channel.target ?? '')}
|
|
538
|
+
@change=${(e) => this.retarget(kind, at, j, e.target.value)}
|
|
539
|
+
>
|
|
540
|
+
${orphan ? html `<option value=${channel.target}>${channel.target}</option>` : nothing}
|
|
541
|
+
<optgroup label=${i18next.t('figure.label.channel-target-parts')}>
|
|
542
|
+
${this.partNames.map(name => html `<option value=${name}>${name}</option>`)}
|
|
543
|
+
</optgroup>
|
|
544
|
+
${this.joints.length > 0
|
|
545
|
+
? html `<optgroup label=${i18next.t('figure.label.channel-target-joints')}>
|
|
546
|
+
${this.joints.map(one => html `<option value=${one.name}>${one.name}</option>`)}
|
|
547
|
+
</optgroup>`
|
|
548
|
+
: nothing}
|
|
549
|
+
</select>
|
|
550
|
+
<button plain @click=${() => this.removeChannel(kind, at, j)} title=${i18next.t('figure.button.remove')}>
|
|
551
|
+
<md-icon>close</md-icon>
|
|
552
|
+
</button>
|
|
553
|
+
</div>
|
|
554
|
+
`;
|
|
555
|
+
/* A joint channel has no path or pivot: the joint says what moves. Each key is the joint coordinate. */
|
|
556
|
+
if (isJointChannel(channel)) {
|
|
557
|
+
const unit = joint?.type === 'prismatic' ? 'mm' : 'deg';
|
|
558
|
+
return html `
|
|
559
|
+
<div channel ?orphan=${orphan}>
|
|
560
|
+
${target} ${orphan ? html `<p warn>${i18next.t('figure.text.channel-target-is-gone')}</p>` : nothing}
|
|
561
|
+
<p quiet>${i18next.t('figure.text.joint-channel-value-' + unit)}</p>
|
|
562
|
+
${this.renderJointKeys(kind, channel, at, j, joint?.limits)}
|
|
563
|
+
</div>
|
|
564
|
+
`;
|
|
565
|
+
}
|
|
509
566
|
const turning = channel.path === 'rotation';
|
|
510
567
|
return html `
|
|
511
568
|
<div channel ?orphan=${orphan}>
|
|
512
|
-
|
|
513
|
-
<label>${i18next.t('figure.label.channel-target')}</label>
|
|
514
|
-
<select
|
|
515
|
-
.value=${live(channel.target ?? '')}
|
|
516
|
-
@change=${(e) => this.setChannel(kind, at, j, { target: e.target.value })}
|
|
517
|
-
>
|
|
518
|
-
${orphan ? html `<option value=${channel.target}>${channel.target}</option>` : nothing}
|
|
519
|
-
${this.partNames.map(name => html `<option value=${name}>${name}</option>`)}
|
|
520
|
-
</select>
|
|
521
|
-
<button plain @click=${() => this.removeChannel(kind, at, j)} title=${i18next.t('figure.button.remove')}>
|
|
522
|
-
<md-icon>close</md-icon>
|
|
523
|
-
</button>
|
|
524
|
-
</div>
|
|
569
|
+
${target}
|
|
525
570
|
|
|
526
571
|
${orphan ? html `<p warn>${i18next.t('figure.text.channel-target-is-gone')}</p>` : nothing}
|
|
527
572
|
|
|
@@ -592,12 +637,79 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
592
637
|
* 시각이 오름차순이어야 하는 것도 형식의 규칙이다. 여기서는 어긋난 줄을 붉게 내고 값을
|
|
593
638
|
* 고치지는 않는다 — 저작자가 3 을 1 로 고치던 중일 수 있다.
|
|
594
639
|
*/
|
|
640
|
+
/**
|
|
641
|
+
* Keys of a joint channel: a position and one joint coordinate each. A value outside the joint's limits
|
|
642
|
+
* is marked and left as typed; the format refuses it on save.
|
|
643
|
+
*/
|
|
644
|
+
renderJointKeys(kind, channel, at, j, limits) {
|
|
645
|
+
const keys = channel.keys ?? [];
|
|
646
|
+
const onValues = kind === 'parameter';
|
|
647
|
+
const beyond = (key) => !!limits && (key.value < limits.min || key.value > limits.max);
|
|
648
|
+
return html `
|
|
649
|
+
<table keys>
|
|
650
|
+
<tr>
|
|
651
|
+
<th>${i18next.t(onValues ? 'figure.label.key-at-value' : 'figure.label.key-at')}</th>
|
|
652
|
+
<th>${i18next.t('figure.label.joint-value')}</th>
|
|
653
|
+
<th></th>
|
|
654
|
+
</tr>
|
|
655
|
+
${keys.map((key, k) => {
|
|
656
|
+
const backwards = k > 0 && key.at <= (keys[k - 1].at ?? 0);
|
|
657
|
+
return html `
|
|
658
|
+
<tr>
|
|
659
|
+
<td>
|
|
660
|
+
<input
|
|
661
|
+
type="number"
|
|
662
|
+
step=${onValues ? '0.05' : '0.1'}
|
|
663
|
+
min="0"
|
|
664
|
+
max=${onValues ? '1' : nothing}
|
|
665
|
+
?bad=${backwards || key.at < 0 || (onValues && key.at > 1)}
|
|
666
|
+
.value=${live(String(key.at))}
|
|
667
|
+
@change=${(e) => this.setJointKey(kind, at, j, k, { at: Math.max(0, Number(e.target.value) || 0) })}
|
|
668
|
+
/>
|
|
669
|
+
</td>
|
|
670
|
+
<td>
|
|
671
|
+
<input
|
|
672
|
+
type="number"
|
|
673
|
+
step="1"
|
|
674
|
+
?bad=${beyond(key)}
|
|
675
|
+
.value=${live(String(key.value))}
|
|
676
|
+
title=${beyond(key) ? i18next.t('figure.text.joint-value-beyond-limits') : ''}
|
|
677
|
+
@change=${(e) => this.setJointKey(kind, at, j, k, { value: Number(e.target.value) || 0 })}
|
|
678
|
+
/>
|
|
679
|
+
</td>
|
|
680
|
+
<td drop>
|
|
681
|
+
<button
|
|
682
|
+
plain
|
|
683
|
+
?disabled=${keys.length <= 2}
|
|
684
|
+
title=${keys.length <= 2 ? i18next.t('figure.text.two-keys-at-least') : i18next.t('figure.button.remove')}
|
|
685
|
+
@click=${() => this.removeKey(kind, at, j, k)}
|
|
686
|
+
>
|
|
687
|
+
<md-icon>close</md-icon>
|
|
688
|
+
</button>
|
|
689
|
+
</td>
|
|
690
|
+
</tr>
|
|
691
|
+
`;
|
|
692
|
+
})}
|
|
693
|
+
</table>
|
|
694
|
+
${keys.some(beyond) ? html `<p warn>${i18next.t('figure.text.joint-value-beyond-limits')}</p>` : nothing}
|
|
695
|
+
<button @click=${() => this.addKey(kind, at, j)}>
|
|
696
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add-key')}
|
|
697
|
+
</button>
|
|
698
|
+
`;
|
|
699
|
+
}
|
|
595
700
|
renderKeys(kind, channel, at, j) {
|
|
596
701
|
const keys = channel.keys ?? [];
|
|
702
|
+
/*
|
|
703
|
+
Keys on a parameter curve sit on the 0..1 value span (ADR-0051). A key above 1 is unreachable and
|
|
704
|
+
the format refuses it. The screen marks it and leaves the number alone: rewriting what the author
|
|
705
|
+
typed would hide what they wrote.
|
|
706
|
+
*/
|
|
707
|
+
const onValues = kind === 'parameter';
|
|
708
|
+
const outside = (key) => onValues && key.at > 1;
|
|
597
709
|
return html `
|
|
598
710
|
<table keys>
|
|
599
711
|
<tr>
|
|
600
|
-
<th>${i18next.t('figure.label.key-at')}</th>
|
|
712
|
+
<th>${i18next.t(onValues ? 'figure.label.key-at-value' : 'figure.label.key-at')}</th>
|
|
601
713
|
${AXES.map(axis => html `<th>${axis}</th>`)}
|
|
602
714
|
<th></th>
|
|
603
715
|
</tr>
|
|
@@ -608,11 +720,16 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
608
720
|
<td>
|
|
609
721
|
<input
|
|
610
722
|
type="number"
|
|
611
|
-
step
|
|
723
|
+
step=${onValues ? '0.05' : '0.1'}
|
|
612
724
|
min="0"
|
|
613
|
-
|
|
725
|
+
max=${onValues ? '1' : nothing}
|
|
726
|
+
?bad=${backwards || key.at < 0 || outside(key)}
|
|
614
727
|
.value=${live(String(key.at))}
|
|
615
|
-
title=${backwards
|
|
728
|
+
title=${backwards
|
|
729
|
+
? i18next.t('figure.text.keys-must-rise')
|
|
730
|
+
: outside(key)
|
|
731
|
+
? i18next.t('figure.text.parameter-key-within-span')
|
|
732
|
+
: ''}
|
|
616
733
|
@change=${(e) => this.setKey(kind, at, j, k, { at: Math.max(0, Number(e.target.value) || 0) })}
|
|
617
734
|
/>
|
|
618
735
|
</td>
|
|
@@ -647,6 +764,7 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
647
764
|
${keys.some((key, k) => k > 0 && key.at <= (keys[k - 1].at ?? 0))
|
|
648
765
|
? html `<p warn>${i18next.t('figure.text.keys-must-rise')}</p>`
|
|
649
766
|
: nothing}
|
|
767
|
+
${keys.some(outside) ? html `<p warn>${i18next.t('figure.text.parameter-key-within-span')}</p>` : nothing}
|
|
650
768
|
|
|
651
769
|
<button @click=${() => this.addKey(kind, at, j)}>
|
|
652
770
|
<md-icon>add</md-icon>${i18next.t('figure.button.add-key')}
|
|
@@ -775,6 +893,19 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
775
893
|
return;
|
|
776
894
|
this.patchParameter(at, { range: { ...(parameter.range ?? NEW_RANGE), ...patch } });
|
|
777
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* Transition time. Clearing the input removes the field, which means instant. 0 is instant too, but
|
|
898
|
+
* a stored 0 and a missing field are different sources, so an empty input is never saved as 0.
|
|
899
|
+
*/
|
|
900
|
+
setDuration(at, raw) {
|
|
901
|
+
const parameter = this.parameters[at];
|
|
902
|
+
if (!parameter)
|
|
903
|
+
return;
|
|
904
|
+
const { duration: _dropped, ...rest } = parameter.clip;
|
|
905
|
+
const text = raw.trim();
|
|
906
|
+
const clip = text === '' || !Number.isFinite(Number(text)) ? rest : { ...rest, duration: Number(text) };
|
|
907
|
+
this.patchParameter(at, { clip });
|
|
908
|
+
}
|
|
778
909
|
setDefault(at, value) {
|
|
779
910
|
this.patchParameter(at, { default: value });
|
|
780
911
|
}
|
|
@@ -795,10 +926,36 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
795
926
|
{ target, path: 'translation', keys: keysFor('translation') }
|
|
796
927
|
]);
|
|
797
928
|
}
|
|
798
|
-
|
|
929
|
+
/**
|
|
930
|
+
* Points a channel at another part or joint. Crossing between the two changes the channel's shape: a
|
|
931
|
+
* joint channel has no path and one number per key, a part channel a path and a vector. The key positions
|
|
932
|
+
* are kept, since they are the author's timing; values restart at the rest pose.
|
|
933
|
+
*/
|
|
934
|
+
retarget(kind, at, j, target) {
|
|
799
935
|
const channel = this.channelsOf(kind, at)[j];
|
|
800
936
|
if (!channel)
|
|
801
937
|
return;
|
|
938
|
+
const toJoint = this.joints.some(one => one.name === target);
|
|
939
|
+
if (toJoint && !isJointChannel(channel)) {
|
|
940
|
+
const next = { target, keys: channel.keys.map(key => ({ at: key.at, value: 0 })) };
|
|
941
|
+
if (channel.interpolation !== undefined)
|
|
942
|
+
next.interpolation = channel.interpolation;
|
|
943
|
+
this.patchChannel(kind, at, j, next);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (!toJoint && isJointChannel(channel)) {
|
|
947
|
+
const next = { target, path: 'translation', keys: channel.keys.map(key => ({ at: key.at, value: startValue('translation') })) };
|
|
948
|
+
if (channel.interpolation !== undefined)
|
|
949
|
+
next.interpolation = channel.interpolation;
|
|
950
|
+
this.patchChannel(kind, at, j, next);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
this.patchChannel(kind, at, j, { ...channel, target });
|
|
954
|
+
}
|
|
955
|
+
setChannel(kind, at, j, patch) {
|
|
956
|
+
const channel = this.channelsOf(kind, at)[j];
|
|
957
|
+
if (!channel || isJointChannel(channel))
|
|
958
|
+
return;
|
|
802
959
|
const next = { ...channel, ...patch };
|
|
803
960
|
/* `undefined` 를 얹는 것은 지우는 뜻이다. 남겨 두면 저장 형식에 빈 칸이 실린다. */
|
|
804
961
|
if ('pivot' in patch && patch.pivot === undefined)
|
|
@@ -814,7 +971,7 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
814
971
|
*/
|
|
815
972
|
setPath(kind, at, j, path) {
|
|
816
973
|
const channel = this.channelsOf(kind, at)[j];
|
|
817
|
-
if (!channel)
|
|
974
|
+
if (!channel || isJointChannel(channel))
|
|
818
975
|
return;
|
|
819
976
|
const crossing = (channel.path === 'scale') !== (path === 'scale');
|
|
820
977
|
const next = {
|
|
@@ -835,13 +992,40 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
835
992
|
const channel = this.channelsOf(kind, at)[j];
|
|
836
993
|
if (!channel)
|
|
837
994
|
return;
|
|
995
|
+
if (isJointChannel(channel)) {
|
|
996
|
+
const keys = channel.keys;
|
|
997
|
+
const end = keys[keys.length - 1];
|
|
998
|
+
if (kind === 'parameter' && keys.length >= 2) {
|
|
999
|
+
const before = keys[keys.length - 2];
|
|
1000
|
+
const middle = { at: (before.at + end.at) / 2, value: before.value };
|
|
1001
|
+
this.patchChannel(kind, at, j, { ...channel, keys: [...keys.slice(0, -1), middle, end] });
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
this.patchChannel(kind, at, j, { ...channel, keys: [...keys, { at: (end?.at ?? 0) + 1, value: end?.value ?? 0 }] });
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
838
1007
|
const last = channel.keys[channel.keys.length - 1];
|
|
1008
|
+
/*
|
|
1009
|
+
A parameter curve ends at 1, so appending a key would make it unreachable. Insert it between the last two keys.
|
|
1010
|
+
*/
|
|
1011
|
+
if (kind === 'parameter' && channel.keys.length >= 2) {
|
|
1012
|
+
const before = channel.keys[channel.keys.length - 2];
|
|
1013
|
+
const middle = { at: (before.at + last.at) / 2, value: { ...before.value } };
|
|
1014
|
+
this.patchChannel(kind, at, j, { ...channel, keys: [...channel.keys.slice(0, -1), middle, last] });
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
839
1017
|
const key = { at: (last?.at ?? 0) + 1, value: { ...(last?.value ?? startValue(channel.path)) } };
|
|
840
1018
|
this.patchChannel(kind, at, j, { ...channel, keys: [...channel.keys, key] });
|
|
841
1019
|
}
|
|
1020
|
+
setJointKey(kind, at, j, k, patch) {
|
|
1021
|
+
const channel = this.channelsOf(kind, at)[j];
|
|
1022
|
+
if (!channel || !isJointChannel(channel))
|
|
1023
|
+
return;
|
|
1024
|
+
this.patchChannel(kind, at, j, { ...channel, keys: channel.keys.map((key, i) => (i === k ? { ...key, ...patch } : key)) });
|
|
1025
|
+
}
|
|
842
1026
|
setKey(kind, at, j, k, patch) {
|
|
843
1027
|
const channel = this.channelsOf(kind, at)[j];
|
|
844
|
-
if (!channel)
|
|
1028
|
+
if (!channel || isJointChannel(channel))
|
|
845
1029
|
return;
|
|
846
1030
|
this.patchChannel(kind, at, j, {
|
|
847
1031
|
...channel,
|