@things-factory/figure-ui 10.1.26 → 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/modeller/figure-animations.ts +172 -24
- package/client/modeller/figure-inspector.ts +143 -3
- package/client/modeller/figure-preview.ts +2 -2
- package/client/modeller/figure-source.ts +16 -4
- package/client/modeller/figure-view.ts +20 -0
- package/client/modeller/joint-edits.ts +201 -0
- package/dist-client/modeller/figure-animations.d.ts +14 -0
- package/dist-client/modeller/figure-animations.js +151 -17
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +11 -0
- package/dist-client/modeller/figure-inspector.js +116 -2
- package/dist-client/modeller/figure-inspector.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-source.d.ts +8 -0
- package/dist-client/modeller/figure-source.js +8 -4
- 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/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/test/figure-source.test.ts +32 -0
- package/test/joint-edits.test.ts +130 -0
- package/test/preview-board-depth.test.ts +27 -0
- package/translations/en.json +24 -0
- package/translations/ja.json +24 -0
- package/translations/ko.json +24 -0
- package/translations/ms.json +24 -0
- package/translations/zh.json +24 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FigureJoint, FigurePart, JointType, Vec3 } from '@hatiolab/figure-model'
|
|
6
|
+
|
|
7
|
+
import type { FigureDraft, PartModel } from './figure-source.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Editing joints and parents (ADR-0066). Pure functions over the draft and the part list, so the inspector
|
|
11
|
+
* only renders and dispatches.
|
|
12
|
+
*
|
|
13
|
+
* A joint is always created together with the parameter that drives it. Values reach a joint only through
|
|
14
|
+
* `parameters`, and the preview draws a slider per parameter, so a joint without one could be defined but
|
|
15
|
+
* never seen moving.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type Parameter = NonNullable<FigureDraft['parameters']>[number]
|
|
19
|
+
|
|
20
|
+
/** The limits a new joint starts with. Revolute in degrees, prismatic in millimetres. */
|
|
21
|
+
const NEW_LIMITS: Record<Exclude<JointType, 'continuous'>, { min: number; max: number }> = {
|
|
22
|
+
revolute: { min: -90, max: 90 },
|
|
23
|
+
prismatic: { min: 0, max: 500 }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The span a parameter driving a continuous joint offers: one turn either way. */
|
|
27
|
+
const CONTINUOUS_SPAN = { min: -360, max: 360 }
|
|
28
|
+
|
|
29
|
+
/** The joint whose child is this part, if any. */
|
|
30
|
+
export function jointOf(draft: FigureDraft | undefined, part: string): FigureJoint | undefined {
|
|
31
|
+
return draft?.joints?.find(joint => joint.child === part)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parts a part may be attached to: every other part except itself and anything attached below it, since
|
|
36
|
+
* either would close a loop the format refuses (`parent-cycle`).
|
|
37
|
+
*/
|
|
38
|
+
export function parentChoices(parts: readonly PartModel[], part: string): string[] {
|
|
39
|
+
const below = new Set<string>([part])
|
|
40
|
+
let grew = true
|
|
41
|
+
while (grew) {
|
|
42
|
+
grew = false
|
|
43
|
+
for (const one of parts) {
|
|
44
|
+
if (one.parent !== undefined && below.has(one.parent) && !below.has(one.name)) {
|
|
45
|
+
below.add(one.name)
|
|
46
|
+
grew = true
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return parts.map(one => one.name).filter(name => !!name && !below.has(name))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function freeName(taken: Iterable<string>, stem: string): string {
|
|
54
|
+
const used = new Set(taken)
|
|
55
|
+
if (!used.has(stem)) return stem
|
|
56
|
+
for (let n = 2; ; n++) if (!used.has(`${stem}-${n}`)) return `${stem}-${n}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The span the driving parameter offers for a joint: its limits, or one turn either way when it has none. */
|
|
60
|
+
function spanOf(joint: FigureJoint): { min: number; max: number } {
|
|
61
|
+
return joint.limits ?? CONTINUOUS_SPAN
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function drivingParameter(joint: FigureJoint): Parameter {
|
|
65
|
+
const span = spanOf(joint)
|
|
66
|
+
return {
|
|
67
|
+
name: joint.name,
|
|
68
|
+
range: { unit: joint.type === 'prismatic' ? 'mm' : 'deg', min: span.min, max: span.max },
|
|
69
|
+
default: 0,
|
|
70
|
+
clip: {
|
|
71
|
+
channels: [
|
|
72
|
+
{
|
|
73
|
+
target: joint.name,
|
|
74
|
+
keys: [
|
|
75
|
+
{ at: 0, value: span.min },
|
|
76
|
+
{ at: 1, value: span.max }
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether a parameter is the plain one `setJointType` made for a joint: one channel on that joint, keys at
|
|
86
|
+
* 0 and 1 spanning the given limits. Only such a parameter follows the joint when its limits or type change;
|
|
87
|
+
* one the author reshaped is left as written.
|
|
88
|
+
*/
|
|
89
|
+
function isPlainDriver(parameter: Parameter, joint: FigureJoint, span: { min: number; max: number }): boolean {
|
|
90
|
+
const channels = parameter.clip?.channels ?? []
|
|
91
|
+
if (channels.length !== 1) return false
|
|
92
|
+
const [channel] = channels
|
|
93
|
+
if (channel!.target !== joint.name || channel!.path !== undefined) return false
|
|
94
|
+
const keys = channel!.keys as { at: number; value: number }[]
|
|
95
|
+
return (
|
|
96
|
+
keys.length === 2 &&
|
|
97
|
+
keys[0]!.at === 0 &&
|
|
98
|
+
keys[1]!.at === 1 &&
|
|
99
|
+
keys[0]!.value === span.min &&
|
|
100
|
+
keys[1]!.value === span.max &&
|
|
101
|
+
parameter.range?.min === span.min &&
|
|
102
|
+
parameter.range?.max === span.max
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Sets how a part moves relative to its parent, or clears the joint with `undefined`.
|
|
108
|
+
*
|
|
109
|
+
* - From none: adds a joint whose origin is the centre of the part's bottom face, with a vertical axis and
|
|
110
|
+
* starting limits, and a parameter that drives it over those limits.
|
|
111
|
+
* - To none: removes the joint and every channel that drove it; a parameter or clip left with no channel
|
|
112
|
+
* is removed too, because the format refuses an empty one.
|
|
113
|
+
* - Between types: keeps the origin and axis, sets limits for the new type (none for continuous), and
|
|
114
|
+
* moves a plain driving parameter along with them.
|
|
115
|
+
*/
|
|
116
|
+
export function setJointType(draft: FigureDraft, part: FigurePart, type: JointType | undefined): FigureDraft {
|
|
117
|
+
const joints = draft.joints ?? []
|
|
118
|
+
const existing = joints.find(joint => joint.child === part.name)
|
|
119
|
+
|
|
120
|
+
if (type === undefined) {
|
|
121
|
+
if (!existing) return draft
|
|
122
|
+
return withoutJoint(draft, existing.name)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!existing) {
|
|
126
|
+
const { position, size } = part.transform
|
|
127
|
+
const joint: FigureJoint = {
|
|
128
|
+
name: freeName([...joints.map(one => one.name)], `${part.name}-joint`),
|
|
129
|
+
child: part.name,
|
|
130
|
+
type,
|
|
131
|
+
origin: { x: position.x, y: position.y - size.y / 2, z: position.z },
|
|
132
|
+
axis: { x: 0, y: 1, z: 0 }
|
|
133
|
+
}
|
|
134
|
+
if (type !== 'continuous') joint.limits = { ...NEW_LIMITS[type] }
|
|
135
|
+
|
|
136
|
+
const parameters = draft.parameters ?? []
|
|
137
|
+
const taken = [...parameters.map(one => one.name), ...(draft.animations ?? []).map(one => one.name)]
|
|
138
|
+
const driver = drivingParameter(joint)
|
|
139
|
+
driver.name = freeName(taken, joint.name)
|
|
140
|
+
|
|
141
|
+
return { ...draft, joints: [...joints, joint], parameters: [...parameters, driver] }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (existing.type === type) return draft
|
|
145
|
+
|
|
146
|
+
const next: FigureJoint = { ...existing, type }
|
|
147
|
+
if (type === 'continuous') delete next.limits
|
|
148
|
+
else if (existing.type === 'continuous' || (existing.type === 'prismatic') !== (type === 'prismatic')) {
|
|
149
|
+
next.limits = { ...NEW_LIMITS[type] }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return replaceJoint(draft, existing, next)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Changes a joint's origin, axis or limits. A plain driving parameter follows new limits. */
|
|
156
|
+
export function patchJoint(
|
|
157
|
+
draft: FigureDraft,
|
|
158
|
+
name: string,
|
|
159
|
+
patch: { origin?: Vec3; axis?: Vec3; limits?: { min: number; max: number } }
|
|
160
|
+
): FigureDraft {
|
|
161
|
+
const existing = draft.joints?.find(joint => joint.name === name)
|
|
162
|
+
if (!existing) return draft
|
|
163
|
+
const next: FigureJoint = { ...existing, ...patch }
|
|
164
|
+
if (existing.type === 'continuous') delete next.limits
|
|
165
|
+
return replaceJoint(draft, existing, next)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function replaceJoint(draft: FigureDraft, before: FigureJoint, after: FigureJoint): FigureDraft {
|
|
169
|
+
const joints = (draft.joints ?? []).map(joint => (joint === before ? after : joint))
|
|
170
|
+
const was = spanOf(before)
|
|
171
|
+
const unitChanged = (before.type === 'prismatic') !== (after.type === 'prismatic')
|
|
172
|
+
const spanChanged = was.min !== spanOf(after).min || was.max !== spanOf(after).max
|
|
173
|
+
|
|
174
|
+
if (!unitChanged && !spanChanged) return { ...draft, joints }
|
|
175
|
+
|
|
176
|
+
const parameters = (draft.parameters ?? []).map(parameter => {
|
|
177
|
+
if (!isPlainDriver(parameter, before, was)) return parameter
|
|
178
|
+
const driver = drivingParameter(after)
|
|
179
|
+
return { ...parameter, range: driver.range, clip: { ...parameter.clip, channels: driver.clip.channels } }
|
|
180
|
+
})
|
|
181
|
+
return { ...draft, joints, parameters }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function withoutJoint(draft: FigureDraft, name: string): FigureDraft {
|
|
185
|
+
const keep = <T extends { channels?: { target: string }[] }>(one: T) => (one.channels ?? []).filter(channel => channel.target !== name)
|
|
186
|
+
|
|
187
|
+
const parameters = (draft.parameters ?? [])
|
|
188
|
+
.map(parameter => ({ ...parameter, clip: { ...parameter.clip, channels: keep(parameter.clip) } }) as Parameter)
|
|
189
|
+
.filter(parameter => parameter.clip.channels.length > 0)
|
|
190
|
+
const animations = (draft.animations ?? [])
|
|
191
|
+
.map(clip => ({ ...clip, channels: keep(clip) }) as NonNullable<FigureDraft['animations']>[number])
|
|
192
|
+
.filter(clip => clip.channels.length > 0)
|
|
193
|
+
const joints = (draft.joints ?? []).filter(joint => joint.name !== name)
|
|
194
|
+
|
|
195
|
+
const next: FigureDraft = { ...draft }
|
|
196
|
+
if (joints.length > 0) next.joints = joints
|
|
197
|
+
else delete next.joints
|
|
198
|
+
if (draft.parameters !== undefined) next.parameters = parameters
|
|
199
|
+
if (draft.animations !== undefined) next.animations = animations
|
|
200
|
+
return next
|
|
201
|
+
}
|
|
@@ -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
|
* 움직이는 부품 예산.
|
|
@@ -111,6 +118,12 @@ export declare class FigureAnimations extends FigureAnimations_base {
|
|
|
111
118
|
private removeClip;
|
|
112
119
|
private removeParameter;
|
|
113
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;
|
|
114
127
|
private setChannel;
|
|
115
128
|
/**
|
|
116
129
|
* 경로를 바꾼다 — **값까지 함께 옮긴다.**
|
|
@@ -123,6 +136,7 @@ export declare class FigureAnimations extends FigureAnimations_base {
|
|
|
123
136
|
private removeChannel;
|
|
124
137
|
/** 마지막 키에서 1 초 뒤. 값은 그 키를 그대로 받는다 — 붙여 놓고 고치는 것이 짜기 쉽다. */
|
|
125
138
|
private addKey;
|
|
139
|
+
private setJointKey;
|
|
126
140
|
private setKey;
|
|
127
141
|
/** 둘 미만으로 내려가지 않는다 — 단추가 이미 죽어 있지만 밖에서 불릴 수 있다. */
|
|
128
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
|
*
|
|
@@ -520,23 +528,45 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
520
528
|
* 검증은 저장할 때 말하고 여기서는 **그 자리에서** 보여 준다.
|
|
521
529
|
*/
|
|
522
530
|
renderChannel(kind, channel, at, j) {
|
|
523
|
-
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
|
+
}
|
|
524
566
|
const turning = channel.path === 'rotation';
|
|
525
567
|
return html `
|
|
526
568
|
<div channel ?orphan=${orphan}>
|
|
527
|
-
|
|
528
|
-
<label>${i18next.t('figure.label.channel-target')}</label>
|
|
529
|
-
<select
|
|
530
|
-
.value=${live(channel.target ?? '')}
|
|
531
|
-
@change=${(e) => this.setChannel(kind, at, j, { target: e.target.value })}
|
|
532
|
-
>
|
|
533
|
-
${orphan ? html `<option value=${channel.target}>${channel.target}</option>` : nothing}
|
|
534
|
-
${this.partNames.map(name => html `<option value=${name}>${name}</option>`)}
|
|
535
|
-
</select>
|
|
536
|
-
<button plain @click=${() => this.removeChannel(kind, at, j)} title=${i18next.t('figure.button.remove')}>
|
|
537
|
-
<md-icon>close</md-icon>
|
|
538
|
-
</button>
|
|
539
|
-
</div>
|
|
569
|
+
${target}
|
|
540
570
|
|
|
541
571
|
${orphan ? html `<p warn>${i18next.t('figure.text.channel-target-is-gone')}</p>` : nothing}
|
|
542
572
|
|
|
@@ -607,6 +637,66 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
607
637
|
* 시각이 오름차순이어야 하는 것도 형식의 규칙이다. 여기서는 어긋난 줄을 붉게 내고 값을
|
|
608
638
|
* 고치지는 않는다 — 저작자가 3 을 1 로 고치던 중일 수 있다.
|
|
609
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
|
+
}
|
|
610
700
|
renderKeys(kind, channel, at, j) {
|
|
611
701
|
const keys = channel.keys ?? [];
|
|
612
702
|
/*
|
|
@@ -836,10 +926,36 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
836
926
|
{ target, path: 'translation', keys: keysFor('translation') }
|
|
837
927
|
]);
|
|
838
928
|
}
|
|
839
|
-
|
|
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) {
|
|
840
935
|
const channel = this.channelsOf(kind, at)[j];
|
|
841
936
|
if (!channel)
|
|
842
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;
|
|
843
959
|
const next = { ...channel, ...patch };
|
|
844
960
|
/* `undefined` 를 얹는 것은 지우는 뜻이다. 남겨 두면 저장 형식에 빈 칸이 실린다. */
|
|
845
961
|
if ('pivot' in patch && patch.pivot === undefined)
|
|
@@ -855,7 +971,7 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
855
971
|
*/
|
|
856
972
|
setPath(kind, at, j, path) {
|
|
857
973
|
const channel = this.channelsOf(kind, at)[j];
|
|
858
|
-
if (!channel)
|
|
974
|
+
if (!channel || isJointChannel(channel))
|
|
859
975
|
return;
|
|
860
976
|
const crossing = (channel.path === 'scale') !== (path === 'scale');
|
|
861
977
|
const next = {
|
|
@@ -876,6 +992,18 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
876
992
|
const channel = this.channelsOf(kind, at)[j];
|
|
877
993
|
if (!channel)
|
|
878
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
|
+
}
|
|
879
1007
|
const last = channel.keys[channel.keys.length - 1];
|
|
880
1008
|
/*
|
|
881
1009
|
A parameter curve ends at 1, so appending a key would make it unreachable. Insert it between the last two keys.
|
|
@@ -889,9 +1017,15 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
889
1017
|
const key = { at: (last?.at ?? 0) + 1, value: { ...(last?.value ?? startValue(channel.path)) } };
|
|
890
1018
|
this.patchChannel(kind, at, j, { ...channel, keys: [...channel.keys, key] });
|
|
891
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
|
+
}
|
|
892
1026
|
setKey(kind, at, j, k, patch) {
|
|
893
1027
|
const channel = this.channelsOf(kind, at)[j];
|
|
894
|
-
if (!channel)
|
|
1028
|
+
if (!channel || isJointChannel(channel))
|
|
895
1029
|
return;
|
|
896
1030
|
this.patchChannel(kind, at, j, {
|
|
897
1031
|
...channel,
|