@things-factory/figure-ui 10.1.162 → 10.1.163
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/data-interface.ts +227 -76
- package/client/modeller/editor.ts +3 -3
- package/client/modeller/figure-data-panel.ts +602 -193
- package/client/modeller/figure-preview.ts +46 -30
- package/client/modeller/refusals.ts +18 -14
- package/dist-client/modeller/data-interface.d.ts +53 -30
- package/dist-client/modeller/data-interface.js +228 -69
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +1 -1
- package/dist-client/modeller/editor.js +2 -2
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-data-panel.d.ts +35 -13
- package/dist-client/modeller/figure-data-panel.js +598 -186
- package/dist-client/modeller/figure-data-panel.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +7 -5
- package/dist-client/modeller/figure-preview.js +47 -30
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +2 -2
- package/dist-client/modeller/refusals.js +24 -18
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/data-motion.test.ts +130 -52
- package/translations/en.json +109 -43
- package/translations/es.json +109 -43
- package/translations/ja.json +109 -43
- package/translations/ko.json +109 -43
- package/translations/ms.json +109 -43
- package/translations/zh.json +109 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-ui",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.163",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"clean": "npm run clean:server && npm run clean:client"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hatiolab/figure-model": "^0.6.
|
|
31
|
-
"@hatiolab/things-scene": "^10.1.
|
|
30
|
+
"@hatiolab/figure-model": "^0.6.6",
|
|
31
|
+
"@hatiolab/things-scene": "^10.1.86",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.0.0",
|
|
34
34
|
"@operato/graphql": "^10.0.0",
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"@operato/styles": "^10.0.0",
|
|
43
43
|
"@operato/utils": "^10.0.0",
|
|
44
44
|
"@things-factory/ai-assistant": "^10.1.161",
|
|
45
|
-
"@things-factory/figure-service": "^10.1.
|
|
46
|
-
"@things-factory/figure-viewer": "^10.1.
|
|
45
|
+
"@things-factory/figure-service": "^10.1.163",
|
|
46
|
+
"@things-factory/figure-viewer": "^10.1.163",
|
|
47
47
|
"@things-factory/setting-base": "^10.1.159",
|
|
48
48
|
"three": "^0.185.1"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "fe2a9c39294d64cd50e52b283105390bb0fa6cee"
|
|
51
51
|
}
|
package/test/data-motion.test.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* **데이터
|
|
2
|
+
* **데이터 형식 · 연결 · 동작 그룹** (ADR-0051 §8 · §3.2, 데이터 바인딩 설계 결정 4, 2026-10-15).
|
|
3
3
|
*
|
|
4
4
|
* 데이터 탭과 클립 패널이 고를 수 있게 내놓는 것과, 그 초안을 figure-model 이 받는지를 로봇 개(robotDogFigure) 위에서 본다.
|
|
5
|
-
* 규칙은 커널의 것이다 -- 여기는 목록과 초안과 문구만 시험한다.
|
|
5
|
+
* 규칙은 커널의 것이고 데이터 읽기는 things-scene 의 것이다 -- 여기는 목록과 초안과 문구만 시험한다.
|
|
6
6
|
*/
|
|
7
7
|
import { test } from 'node:test'
|
|
8
8
|
import assert from 'node:assert/strict'
|
|
@@ -10,10 +10,25 @@ import { readFileSync } from 'node:fs'
|
|
|
10
10
|
import { fileURLToPath } from 'node:url'
|
|
11
11
|
|
|
12
12
|
import { editFigurePart, quadrupedRobotFigure, robotDogFigure } from '@hatiolab/figure-model'
|
|
13
|
-
import type { FigureAsset,
|
|
13
|
+
import type { FigureAsset, FigureDataField } from '@hatiolab/figure-model'
|
|
14
14
|
|
|
15
15
|
import { clipGroupsOf, freeGroupIdOf, groupEdit, undrivenOf } from '../client/modeller/clips.ts'
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
accessorOfRow,
|
|
18
|
+
behaviorsOf,
|
|
19
|
+
bindingDraftOf,
|
|
20
|
+
fieldAt,
|
|
21
|
+
fieldOfKind,
|
|
22
|
+
fitsField,
|
|
23
|
+
freeIdOf,
|
|
24
|
+
motionWrittenByData,
|
|
25
|
+
portDraftOf,
|
|
26
|
+
sampleOf,
|
|
27
|
+
schemaWithField,
|
|
28
|
+
schemaWithoutField,
|
|
29
|
+
typedValueOf,
|
|
30
|
+
valueRowsOf
|
|
31
|
+
} from '../client/modeller/data-interface.ts'
|
|
17
32
|
import { editRefusalOf } from '../client/modeller/refusals.ts'
|
|
18
33
|
|
|
19
34
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
@@ -56,51 +71,104 @@ test('그룹 목록과 빈 클립 · 그룹 id 는 사람이 적은 이름에서
|
|
|
56
71
|
assert.deepEqual([two.motionGroups![0]!.transitionSeconds, two.motionGroups![0]!.defaultClip], [0.5, 'walk'])
|
|
57
72
|
})
|
|
58
73
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
assert.deepEqual(
|
|
70
|
-
|
|
71
|
-
assert.deepEqual(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
assert.deepEqual(
|
|
78
|
-
editFigurePart(asset, { kind: 'set-data-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
/** The dog with one more top field, the way the data tab adds one. */
|
|
75
|
+
const withField = (asset: FigureAsset, field: FigureDataField) => editFigurePart(asset, { kind: 'set-data-schema', schema: schemaWithField(asset.dataInterface!.schema, '', field) })
|
|
76
|
+
const choice = (asset: FigureAsset, key: string) => behaviorsOf(asset).find(c => c.key === key)!
|
|
77
|
+
const draft = (asset: FigureAsset, path: string, key: string) => bindingDraftOf(asset, accessorOfRow(path), fieldAt(asset.dataInterface!.schema, path)!, choice(asset, key))
|
|
78
|
+
|
|
79
|
+
test('작동 목록: 이 Figure 가 할 수 있는 것 전부, 값의 종류마다 맞는 것만', () => {
|
|
80
|
+
const kinds = [...new Set(behaviorsOf(DOG).map(c => c.kind))]
|
|
81
|
+
assert.deepEqual(kinds, ['state', 'palette', 'motion-clip', 'motion-speed', 'motion-paused', 'status', 'pose', 'items'])
|
|
82
|
+
const fits = (field: FigureDataField) => [...new Set(behaviorsOf(DOG).filter(c => fitsField(field, c)).map(c => c.kind))]
|
|
83
|
+
assert.deepEqual(fits({ name: 'n', kind: 'number' }), ['state', 'palette', 'motion-clip', 'motion-speed', 'status', 'pose', 'items'])
|
|
84
|
+
assert.deepEqual(fits({ name: 'b', kind: 'boolean' }), ['state', 'palette', 'motion-clip', 'motion-speed', 'motion-paused'])
|
|
85
|
+
assert.deepEqual(fits({ name: 'o', kind: 'object', fields: [] }), [])
|
|
86
|
+
assert.deepEqual(fits({ name: 'l', kind: 'list', item: { kind: 'object', fields: [] } }), ['items'])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('첫 연결 초안: 맞으면 값 그대로, 범위·단위만 다르면 배율, 닫힌 값은 값마다 -- 커널이 받는다', () => {
|
|
90
|
+
let asset = withField(DOG, { name: 'lean', kind: 'number', unit: 'deg', min: -30, max: 30 })
|
|
91
|
+
const lean = draft(asset, 'lean', 'state:fl-hip')
|
|
92
|
+
assert.deepEqual([lean.id, lean.rule, lean.param], ['lean-fl-hip', 'value', undefined], '같은 단위·범위 안은 그대로')
|
|
93
|
+
asset = editFigurePart(asset, { kind: 'set-data-binding', binding: lean })
|
|
94
|
+
/* 0~100 % 를 배속 0~3 에 -- 범위를 겹쳐 놓는 배율. */
|
|
95
|
+
asset = withField(asset, { name: 'pct', kind: 'number', min: 0, max: 100 })
|
|
96
|
+
const pct = draft({ ...asset, dataInterface: { ...asset.dataInterface!, bindings: asset.dataInterface!.bindings.filter(b => b.id !== 'cadence-locomotion') } }, 'pct', 'motion-speed:locomotion')
|
|
97
|
+
assert.deepEqual([pct.rule, pct.param], ['scale', { scale: 0.03, offset: 0 }])
|
|
98
|
+
/* m 로 오는 위치는 mm 로. */
|
|
99
|
+
asset = withField(asset, { name: 'east', kind: 'number', unit: 'm' })
|
|
100
|
+
const east = draft({ ...asset, dataInterface: { ...asset.dataInterface!, bindings: asset.dataInterface!.bindings.filter(b => b.id !== 'at-x') } }, 'east', 'pose:x')
|
|
101
|
+
assert.deepEqual([east.rule, east.param], ['scale', { scale: 1000, offset: 0 }])
|
|
102
|
+
/* 닫힌 글자를 색에: 값마다 지금 색으로 시작한다. */
|
|
103
|
+
const body = draft(asset, 'mode', 'palette:palette.body')
|
|
104
|
+
assert.deepEqual([body.rule, body.param], ['map', Object.fromEntries(['idle', 'walk', 'run'].map(v => [v, DOG.palette['palette.body']]))])
|
|
105
|
+
editFigurePart(asset, { kind: 'set-data-binding', binding: body })
|
|
106
|
+
/* 열린 숫자를 색에: 구간마다, 그 밖은 지금 색. */
|
|
107
|
+
assert.deepEqual(draft(asset, 'lean', 'palette:palette.body').param, { default: DOG.palette['palette.body'] })
|
|
108
|
+
assert.equal(freeIdOf(['mode', 'mode-2'], 'mode'), 'mode-3')
|
|
109
|
+
assert.equal(freeIdOf([], '배터리'), 'binding', '영문 id 가 남지 않으면 binding')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('아이템 연결: 목록 통째로, 요소의 id·자리 필드를 이름으로 짚는다', () => {
|
|
113
|
+
const asset = withField({ ...DOG, dataInterface: { ...DOG.dataInterface!, bindings: DOG.dataInterface!.bindings.filter(b => b.id !== 'back-load') } }, {
|
|
114
|
+
name: 'carriers',
|
|
115
|
+
kind: 'list',
|
|
116
|
+
item: { kind: 'object', fields: [{ name: 'carrierId', kind: 'text' }, { name: 'carrierType', kind: 'text' }, { name: 'slotNo', kind: 'text' }] }
|
|
117
|
+
})
|
|
118
|
+
const items = bindingDraftOf(asset, 'carriers', fieldAt(asset.dataInterface!.schema, 'carriers')!, choice(asset, 'items:'))
|
|
119
|
+
assert.deepEqual(items.behavior, { kind: 'items', id: 'carrierId', type: 'carrierType', place: 'slotNo' })
|
|
120
|
+
editFigurePart(asset, { kind: 'set-data-binding', binding: items })
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('데이터 형식 트리: 필드를 아래에 더하고, 지우고, 종류를 바꾼다 -- 원본은 그대로', () => {
|
|
124
|
+
const schema = DOG.dataInterface!.schema
|
|
125
|
+
const more = schemaWithField(schema, 'load[]', { name: 'weight', kind: 'number', unit: 'kg' } as FigureDataField)
|
|
126
|
+
assert.equal(fieldAt(more, 'load[].weight')?.kind, 'number')
|
|
127
|
+
assert.equal(fieldAt(schema, 'load[].weight'), undefined, '원본 형식은 바뀌지 않는다')
|
|
128
|
+
assert.equal(fieldAt(schemaWithoutField(more, 'load[].weight'), 'load[].weight'), undefined)
|
|
129
|
+
assert.deepEqual(fieldOfKind({ name: 'heading', label: '방향', kind: 'number', unit: 'deg' }, 'text'), { name: 'heading', label: '방향', kind: 'text' })
|
|
130
|
+
/* 읽는 연결이 있는 필드를 지우면 커널이 거절한다. */
|
|
131
|
+
assert.match(refusalOf(() => editFigurePart(DOG, { kind: 'set-data-schema', schema: schemaWithoutField(schema, 'mode') })), /^figure\.text\.data-accessor-invalid-absent .*"accessor":"mode"/)
|
|
132
|
+
/* 목록 밖 필드는 읽는 연결과 함께 이름이 바뀐다. */
|
|
133
|
+
const renamed = editFigurePart(DOG, { kind: 'rename-data-field', accessor: 'mode', to: 'MODE' })
|
|
134
|
+
assert.equal(renamed.dataInterface!.bindings.find(b => b.id === 'mode-locomotion')!.accessor, 'MODE')
|
|
81
135
|
})
|
|
82
136
|
|
|
83
|
-
test('
|
|
84
|
-
const ok =
|
|
85
|
-
assert.ok
|
|
137
|
+
test('샘플: 보드의 Figure 와 같은 읽기로 -- 무엇을 움직이는지, 틀린 값이 있으면 통째로 거절', () => {
|
|
138
|
+
const ok = sampleOf(DOG, { mode: 'run', cadence: 2, headAngle: 10, batteryLow: true, status: 'RUN', position: { x: 100, z: 50 }, heading: 90, load: [{ id: 'B1', slot: 't1_s1' }] })
|
|
139
|
+
assert.deepEqual(ok.issues, [])
|
|
86
140
|
assert.deepEqual(JSON.parse(JSON.stringify(ok.result.motion)), { locomotion: { clip: 'run', speed: 2 } })
|
|
87
|
-
|
|
88
|
-
assert.ok('
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
assert.
|
|
141
|
+
assert.deepEqual(ok.result.status, { state: 'RUN' })
|
|
142
|
+
assert.deepEqual(ok.result.items.map(i => i.records), [[{ id: 'B1', place: 't1_s1' }]])
|
|
143
|
+
const bad = sampleOf(DOG, { mode: 'fly', headAngle: 90 })
|
|
144
|
+
assert.deepEqual(bad.issues.map(i => `${i.path} ${i.reason}`), ['mode value-unknown', 'headAngle out-of-range'])
|
|
145
|
+
/* 손으로 주는 값: 연결이 읽는 값 하나, 목록 밖. */
|
|
146
|
+
assert.deepEqual(valueRowsOf(DOG).map(r => r.path), ['mode', 'cadence', 'headAngle', 'batteryLow', 'status', 'position.x', 'position.z', 'heading'])
|
|
147
|
+
const row = (path: string) => valueRowsOf(DOG).find(r => r.path === path)!.node
|
|
148
|
+
assert.deepEqual([typedValueOf(row('mode'), 'walk'), typedValueOf(row('batteryLow'), 'false'), typedValueOf(row('cadence'), '0')], ['walk', false, 0])
|
|
149
|
+
assert.ok(Number.isNaN(typedValueOf(row('cadence'), '') as number), '빈 칸은 0 이 아니다')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('상태 표시 초안: 첫 색 토큰에 -- 커널이 받는다', () => {
|
|
153
|
+
const port = portDraftOf(DOG, 'lamp')
|
|
154
|
+
assert.deepEqual(port, { id: 'lamp', label: 'lamp', tokens: ['palette.body'], parts: [] })
|
|
155
|
+
editFigurePart(DOG, { kind: 'set-status-port', port })
|
|
156
|
+
assert.equal(portDraftOf(DOG, 'state').id, 'state-2')
|
|
95
157
|
})
|
|
96
158
|
|
|
97
159
|
test('데이터 · 동작 거절은 커널 문장이 아니라 reason 과 값으로 말한다', () => {
|
|
98
|
-
assert.match(refusalOf(() => editFigurePart(DOG, { kind: 'remove-data-property', id: 'mode' })), /^figure\.text\.data-property-connected .*mode-locomotion/)
|
|
99
|
-
assert.match(refusalOf(() => editFigurePart(DOG, { kind: 'remove-driver', state: 'fl-knee' })), /^figure\.text\.driver-clip-required .*walk/)
|
|
100
160
|
assert.match(
|
|
101
|
-
refusalOf(() =>
|
|
102
|
-
|
|
161
|
+
refusalOf(() =>
|
|
162
|
+
editFigurePart(DOG, { kind: 'set-data-binding', binding: { id: 'head-2', accessor: 'headAngle', behavior: { kind: 'state', input: 'head', sourceMode: 'observed' }, rule: 'scale', param: { scale: 2, offset: 0 } } })
|
|
163
|
+
),
|
|
164
|
+
/^figure\.text\.data-rule-range .*"min":"-90".*"low":"-45"/
|
|
103
165
|
)
|
|
166
|
+
assert.match(
|
|
167
|
+
refusalOf(() => editFigurePart(DOG, { kind: 'set-data-binding', binding: { id: 'lamp-2', accessor: 'mode', behavior: { kind: 'palette', token: 'palette.battery' }, rule: 'map', param: { default: '#000000' } } })),
|
|
168
|
+
/^figure\.text\.data-behavior-duplicate .*battery-colour/
|
|
169
|
+
)
|
|
170
|
+
assert.match(refusalOf(() => editFigurePart(DOG, { kind: 'remove-status-port', id: 'state' })), /^figure\.text\.data-port-bound .*body-status/)
|
|
171
|
+
assert.match(refusalOf(() => editFigurePart(DOG, { kind: 'remove-driver', state: 'fl-knee' })), /^figure\.text\.driver-clip-required .*walk/)
|
|
104
172
|
const lone = editFigurePart(quadrupedRobotFigure().asset, { kind: 'set-driver', state: 'fl-knee', clip: 'walk', duration: 1, keys: [{ seconds: 0, value: 0 }, { seconds: 1, value: 10 }] })
|
|
105
173
|
assert.match(refusalOf(() => editFigurePart(lone, { kind: 'set-driver', state: 'fl-knee', clip: 'run', duration: 1, keys: [{ seconds: 0, value: 0 }, { seconds: 1, value: 10 }] })), /^figure\.text\.driver-writers /)
|
|
106
174
|
})
|
|
@@ -108,8 +176,8 @@ test('데이터 · 동작 거절은 커널 문장이 아니라 reason 과 값으
|
|
|
108
176
|
test('미리보기: 데이터가 정하는 그룹 필드는 그룹 칸이 아니라 데이터 값으로 움직인다 (2026-10-14, 로봇 개)', () => {
|
|
109
177
|
/* The dog's clip and speed are both written by data (mode, cadence): a clip chosen on the group moved nothing. */
|
|
110
178
|
assert.deepEqual(motionWrittenByData(DOG), { locomotion: { clip: true, speed: true } })
|
|
111
|
-
/* Only the speed
|
|
112
|
-
const speedOnly = { ...DOG, dataInterface: { ...DOG.dataInterface!,
|
|
179
|
+
/* Only the speed bound: the clip stays the group's to choose. */
|
|
180
|
+
const speedOnly = { ...DOG, dataInterface: { ...DOG.dataInterface!, bindings: DOG.dataInterface!.bindings.filter(b => b.behavior.kind !== 'motion-clip') } }
|
|
113
181
|
assert.deepEqual(motionWrittenByData(speedOnly), { locomotion: { clip: false, speed: true } })
|
|
114
182
|
/* No data interface: every group field is the group's own. */
|
|
115
183
|
const { dataInterface: _gone, ...bare } = DOG
|
|
@@ -125,27 +193,37 @@ test('데이터 탭 · 클립 패널 · 미리보기의 배선과 문구', () =>
|
|
|
125
193
|
assert.match(editor, /renderTab\('data'/)
|
|
126
194
|
assert.match(editor, /<figure-data-panel/)
|
|
127
195
|
assert.match(editor, /\.sample=\$\{this\.dataSample\}/)
|
|
196
|
+
assert.match(editor, /this\.dataSample = e\.detail\.data/)
|
|
128
197
|
const preview = read('../client/modeller/figure-preview.ts')
|
|
129
|
-
/*
|
|
130
|
-
assert.match(preview,
|
|
198
|
+
/* 샘플은 보드의 데이터 소스처럼 Figure 의 data 로 간다 -- 저장 모델에는 쓰지 않는다. */
|
|
199
|
+
assert.match(preview, /component\.set\?\.\('data', /)
|
|
200
|
+
assert.doesNotMatch(preview, /figureData/)
|
|
131
201
|
assert.match(preview, /tellInstances\('motion'/)
|
|
132
202
|
const clips = read('../client/modeller/figure-clips.ts')
|
|
133
203
|
assert.match(clips, /kind: 'remove-driver', state: track\.state, clip: clip\.name/)
|
|
134
204
|
/* A key's value is shown to three decimals: a slider's 14.399999999999999 is not what a person typed. */
|
|
135
205
|
assert.match(clips, /value: current === undefined \? '' : shown\(current\)/)
|
|
136
206
|
assert.match(clips, /\$\{key\.seconds\}s → \$\{shown\(key\.value\)\}/)
|
|
137
|
-
const keys = [read('../client/modeller/figure-data-panel.ts'), clips, read('../client/modeller/refusals.ts')].flatMap(src =>
|
|
138
|
-
|
|
207
|
+
const keys = [read('../client/modeller/figure-data-panel.ts'), clips, read('../client/modeller/refusals.ts')].flatMap(src =>
|
|
208
|
+
[...src.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1]!)
|
|
209
|
+
)
|
|
210
|
+
/* Keys made from a value: each value the kernel or the reader can give has its words. */
|
|
139
211
|
const made = [
|
|
140
212
|
/* The tab's short name, the one it shows under its icon (renderTab reads figure.label.tab-short-<tab>). */
|
|
141
213
|
'figure.label.tab-short-data',
|
|
142
|
-
...['number', 'boolean', '
|
|
143
|
-
...['state', 'palette', 'motion-clip', 'motion-speed'].
|
|
214
|
+
...['number', 'boolean', 'text', 'object', 'list'].map(k => `figure.label.data-kind-${k}`),
|
|
215
|
+
...['state', 'palette', 'motion-clip', 'motion-speed', 'motion-paused', 'status', 'pose', 'items'].map(k => `figure.label.data-behavior-${k}`),
|
|
216
|
+
...['state', 'palette', 'motion-clip', 'motion-speed', 'motion-paused', 'status', 'items'].map(k => `figure.text.data-behavior-absent-${k}`),
|
|
217
|
+
...['x', 'y', 'z', 'heading'].map(k => `figure.label.data-axis-${k}`),
|
|
218
|
+
...['value', 'map', 'range', 'scale'].map(k => `figure.label.data-rule-${k}`),
|
|
219
|
+
...['id', 'type', 'place', 'stack', 'status'].map(k => `figure.label.data-item-${k}`),
|
|
144
220
|
...['observed', 'commanded'].map(k => `figure.label.data-${k}`),
|
|
145
|
-
...['label', 'unit', 'min', 'default', 'values'].map(k => `figure.text.data-
|
|
146
|
-
...['
|
|
221
|
+
...['name', 'label', 'json', 'unit', 'min', 'max', 'default', 'values'].map(k => `figure.text.data-field-invalid-${k}`),
|
|
222
|
+
...['syntax', 'absent', 'not-a-list', 'not-a-value', 'list-needs-index', 'item-id-required'].map(k => `figure.text.data-accessor-invalid-${k}`),
|
|
223
|
+
...['kind', 'unit', 'range', 'cases', 'output', 'default'].map(k => `figure.text.data-rule-${k}`),
|
|
224
|
+
...['label', 'token', 'part', 'group'].map(k => `figure.text.data-port-invalid-${k}`),
|
|
147
225
|
...['group-unknown', 'clip-not-in-group', 'speed-out-of-range', 'group-selection-required'].map(k => `figure.text.motion-setting-${k}`),
|
|
148
|
-
...['not-a-record', '
|
|
226
|
+
...['not-a-record', 'not-a-list', 'type-mismatch', 'out-of-range', 'value-unknown', 'json-unreadable', 'item-id-missing'].map(k => `figure.text.data-value-${k}`)
|
|
149
227
|
]
|
|
150
228
|
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
|
|
151
229
|
const table = JSON.parse(read(`../translations/${lang}.json`))
|
package/translations/en.json
CHANGED
|
@@ -1039,16 +1039,9 @@
|
|
|
1039
1039
|
"figure.button.add-clip-to-group": "add a clip",
|
|
1040
1040
|
"figure.text.motion-groups-explained": "Clips of one group play one at a time. For several clips to move the same control, put them in one group. A change of clip blends from the pose left to the new clip over the transition time.",
|
|
1041
1041
|
"figure.label.tab-data": "data",
|
|
1042
|
-
"figure.label.data-properties": "data properties",
|
|
1043
|
-
"figure.text.data-properties-explained": "Values the site data gives this figure. A board maps its data onto these ids, never onto a part or an inner name.",
|
|
1044
|
-
"figure.label.data-connections": "connections",
|
|
1045
|
-
"figure.text.data-connections-explained": "What a data property moves: a control, a colour, or a motion group's clip and speed. Every value the property can take must land inside what the target takes.",
|
|
1046
|
-
"figure.text.data-no-properties": "Add a data property first.",
|
|
1047
1042
|
"figure.label.data-kind": "kind",
|
|
1048
1043
|
"figure.label.data-kind-number": "number",
|
|
1049
1044
|
"figure.label.data-kind-boolean": "true/false",
|
|
1050
|
-
"figure.label.data-kind-enum": "choice",
|
|
1051
|
-
"figure.label.data-id": "id",
|
|
1052
1045
|
"figure.label.data-unit": "unit",
|
|
1053
1046
|
"figure.label.data-min": "min",
|
|
1054
1047
|
"figure.label.data-max": "max",
|
|
@@ -1056,51 +1049,124 @@
|
|
|
1056
1049
|
"figure.label.data-values": "values (comma-separated)",
|
|
1057
1050
|
"figure.label.data-true": "true",
|
|
1058
1051
|
"figure.label.data-false": "false",
|
|
1059
|
-
"figure.button.add-data-property": "add a data property",
|
|
1060
|
-
"figure.button.remove-data-property": "remove the data property",
|
|
1061
|
-
"figure.label.data-property": "data property",
|
|
1062
|
-
"figure.label.data-target": "target",
|
|
1063
|
-
"figure.label.data-target-state": "control {name}",
|
|
1064
|
-
"figure.label.data-target-palette": "colour {name}",
|
|
1065
|
-
"figure.label.data-target-motion-clip": "{name} clip",
|
|
1066
|
-
"figure.label.data-target-motion-speed": "{name} speed",
|
|
1067
|
-
"figure.button.add-data-connection": "add a connection",
|
|
1068
|
-
"figure.button.remove-data-connection": "remove the connection",
|
|
1069
1052
|
"figure.label.data-source-mode": "applies",
|
|
1070
1053
|
"figure.label.data-observed": "measured (at once)",
|
|
1071
1054
|
"figure.label.data-commanded": "commanded (travels at the sweep)",
|
|
1072
|
-
"figure.label.data-transform": "transform",
|
|
1073
|
-
"figure.label.data-identity": "as it is",
|
|
1074
|
-
"figure.label.data-affine": "scale and offset",
|
|
1075
1055
|
"figure.label.data-scale": "scale",
|
|
1076
1056
|
"figure.label.data-offset": "offset",
|
|
1077
1057
|
"figure.label.data-sample": "sample data",
|
|
1078
|
-
"figure.text.data-sample-explained": "
|
|
1058
|
+
"figure.text.data-sample-explained": "Enter one record as JSON to see what it moves. It is read the way a figure on a board reads its data, and not saved.",
|
|
1059
|
+
"figure.text.data-sample-moves-all": "A position in the data moves every figure in the preview to it; on a board, figures given the same value stand in the same place too.",
|
|
1079
1060
|
"figure.label.data-sample-in-preview": "apply to the preview",
|
|
1080
|
-
"figure.
|
|
1081
|
-
"figure.text.data-
|
|
1082
|
-
"figure.
|
|
1083
|
-
"figure.text.data-
|
|
1084
|
-
"figure.
|
|
1061
|
+
"figure.label.data-schema": "data shape",
|
|
1062
|
+
"figure.text.data-schema-explained": "The shape of the data a site sends this figure. Name each field as the site's data names it; group fields in objects and lists.",
|
|
1063
|
+
"figure.label.data-bound": "{count} bound",
|
|
1064
|
+
"figure.text.data-no-fields": "Add a field to the data shape first.",
|
|
1065
|
+
"figure.label.data-name": "field name",
|
|
1066
|
+
"figure.label.data-label": "display name",
|
|
1067
|
+
"figure.label.data-json": "also read as JSON text",
|
|
1068
|
+
"figure.label.data-item-kind": "element kind",
|
|
1069
|
+
"figure.label.data-kind-text": "text",
|
|
1070
|
+
"figure.label.data-kind-object": "object",
|
|
1071
|
+
"figure.label.data-kind-list": "list",
|
|
1072
|
+
"figure.button.add-data-field": "add a field",
|
|
1073
|
+
"figure.button.add-data-child": "add a field under {parent}",
|
|
1074
|
+
"figure.button.remove-data-field": "remove the field",
|
|
1075
|
+
"figure.label.data-from-sample": "make from a sample",
|
|
1076
|
+
"figure.text.data-from-sample-explained": "Paste one record of the site's data to guess its fields. Units, ranges and value lists are not guessed; set them after.",
|
|
1077
|
+
"figure.text.data-not-json": "This is not JSON.",
|
|
1078
|
+
"figure.text.data-guessed": "{count} fields found.",
|
|
1079
|
+
"figure.text.data-guessed-replaces": "{count} fields found. Using them replaces the current data shape.",
|
|
1080
|
+
"figure.button.data-use-guessed": "use this shape",
|
|
1081
|
+
"figure.label.data-bindings": "bindings",
|
|
1082
|
+
"figure.text.data-bindings-explained": "One data value moves one thing the figure does: as it is, one result per value, per interval, or scaled. Click a line to edit how.",
|
|
1083
|
+
"figure.label.data-field": "data value",
|
|
1084
|
+
"figure.label.data-behavior": "behaviour",
|
|
1085
|
+
"figure.label.data-behavior-state": "control {name}",
|
|
1086
|
+
"figure.label.data-behavior-palette": "colour {name}",
|
|
1087
|
+
"figure.label.data-behavior-motion-clip": "{name} clip",
|
|
1088
|
+
"figure.label.data-behavior-motion-speed": "{name} speed",
|
|
1089
|
+
"figure.label.data-behavior-motion-paused": "{name} paused",
|
|
1090
|
+
"figure.label.data-behavior-status": "status {name}",
|
|
1091
|
+
"figure.label.data-behavior-pose": "position {name}",
|
|
1092
|
+
"figure.label.data-behavior-items": "items {name}",
|
|
1093
|
+
"figure.label.data-axis-x": "X",
|
|
1094
|
+
"figure.label.data-axis-y": "height (Y)",
|
|
1095
|
+
"figure.label.data-axis-z": "Z",
|
|
1096
|
+
"figure.label.data-axis-heading": "heading",
|
|
1097
|
+
"figure.label.data-rule": "rule",
|
|
1098
|
+
"figure.label.data-rule-value": "as it is",
|
|
1099
|
+
"figure.label.data-rule-map": "per value",
|
|
1100
|
+
"figure.label.data-rule-range": "per interval",
|
|
1101
|
+
"figure.label.data-rule-scale": "scale and offset",
|
|
1102
|
+
"figure.label.data-seconds": "travel time (s)",
|
|
1103
|
+
"figure.label.data-paused": "paused",
|
|
1104
|
+
"figure.label.data-playing": "playing",
|
|
1105
|
+
"figure.label.data-otherwise": "any other value",
|
|
1106
|
+
"figure.label.data-range-from": "from",
|
|
1107
|
+
"figure.label.data-range-to": "below",
|
|
1108
|
+
"figure.button.add-data-range": "add an interval",
|
|
1109
|
+
"figure.button.data-set-result": "set",
|
|
1110
|
+
"figure.button.remove-data-range": "remove the interval",
|
|
1111
|
+
"figure.label.data-item-id": "item id",
|
|
1112
|
+
"figure.label.data-item-type": "item type",
|
|
1113
|
+
"figure.label.data-item-place": "place",
|
|
1114
|
+
"figure.label.data-item-stack": "stack order",
|
|
1115
|
+
"figure.label.data-item-status": "status",
|
|
1116
|
+
"figure.label.data-item-slot": "default place",
|
|
1117
|
+
"figure.text.data-no-behavior": "Nothing of this figure can be moved by this value.",
|
|
1118
|
+
"figure.button.add-data-binding": "add a binding",
|
|
1119
|
+
"figure.button.remove-data-binding": "remove the binding",
|
|
1120
|
+
"figure.label.data-ports": "status display",
|
|
1121
|
+
"figure.text.data-ports-explained": "Where a status code shows. The board's status template picks the colour; here you choose the colour tokens that take it and the parts that blink or glow.",
|
|
1122
|
+
"figure.label.data-port-tokens": "colour tokens",
|
|
1123
|
+
"figure.label.data-port-parts": "parts",
|
|
1124
|
+
"figure.button.add-data-port": "add a status display",
|
|
1125
|
+
"figure.button.remove-data-port": "remove the status display",
|
|
1126
|
+
"figure.label.data-items-count": "{count} items",
|
|
1127
|
+
"figure.text.data-value-not-a-list": "{path}: not a list.",
|
|
1128
|
+
"figure.text.data-value-value-unknown": "{path}: not one of its values.",
|
|
1129
|
+
"figure.text.data-value-json-unreadable": "{path}: the JSON text cannot be read.",
|
|
1130
|
+
"figure.text.data-value-item-id-missing": "{path}: the item has no id.",
|
|
1131
|
+
"figure.text.data-field-invalid-name": "A field name starts with a letter or _, then letters, digits, _ and -.",
|
|
1132
|
+
"figure.text.data-field-invalid-label": "Write a display name, or leave it empty.",
|
|
1133
|
+
"figure.text.data-field-invalid-json": "Only an object or a list may come as JSON text.",
|
|
1134
|
+
"figure.text.data-field-invalid-unit": "The unit is not one the figure knows.",
|
|
1135
|
+
"figure.text.data-field-invalid-min": "Write the range from the smaller number to the larger.",
|
|
1136
|
+
"figure.text.data-field-invalid-max": "Write the max as a number.",
|
|
1137
|
+
"figure.text.data-field-invalid-default": "The default must lie in the range or the value list.",
|
|
1138
|
+
"figure.text.data-field-invalid-values": "The value list needs one or more values, each once.",
|
|
1139
|
+
"figure.text.data-field-duplicate": "There is already a field {name}.",
|
|
1140
|
+
"figure.text.data-accessor-invalid-syntax": "{accessor} is not a data path.",
|
|
1141
|
+
"figure.text.data-accessor-invalid-absent": "The data shape has no field {accessor}.",
|
|
1142
|
+
"figure.text.data-accessor-invalid-not-a-list": "{accessor} is not a list.",
|
|
1143
|
+
"figure.text.data-accessor-invalid-not-a-value": "{accessor} is not one value; choose a field under it.",
|
|
1144
|
+
"figure.text.data-accessor-invalid-list-needs-index": "{accessor} is a list; choose a value of one element.",
|
|
1145
|
+
"figure.text.data-accessor-invalid-item-id-required": "A list of items needs an id field in each element.",
|
|
1146
|
+
"figure.text.data-behavior-absent-state": "This figure has no control {id}.",
|
|
1147
|
+
"figure.text.data-behavior-absent-palette": "This figure has no colour {id}.",
|
|
1148
|
+
"figure.text.data-behavior-absent-motion-clip": "There is no motion group {id}.",
|
|
1149
|
+
"figure.text.data-behavior-absent-motion-speed": "There is no motion group {id}.",
|
|
1150
|
+
"figure.text.data-behavior-absent-motion-paused": "There is no motion group {id}.",
|
|
1151
|
+
"figure.text.data-behavior-absent-status": "There is no status display {id}.",
|
|
1152
|
+
"figure.text.data-behavior-absent-items": "There is no place {id}.",
|
|
1153
|
+
"figure.text.data-behavior-duplicate": "The bindings {a} and {b} both move {id}; one binding per behaviour.",
|
|
1154
|
+
"figure.text.data-rule-kind": "This rule does not fit this value and behaviour.",
|
|
1155
|
+
"figure.text.data-rule-unit": "The units differ ({from} → {to}); convert with scale and offset.",
|
|
1156
|
+
"figure.text.data-rule-range": "It gives {min} to {max}; the behaviour takes {low} to {high}.",
|
|
1157
|
+
"figure.text.data-rule-cases": "{value} is not one of this value's values.",
|
|
1158
|
+
"figure.text.data-rule-output": "{value} is not something the behaviour takes.",
|
|
1159
|
+
"figure.text.data-rule-default": "Set a result for any other value, so every value has one.",
|
|
1160
|
+
"figure.text.data-port-invalid-label": "Write a name for the status display.",
|
|
1161
|
+
"figure.text.data-port-invalid-token": "Choose one or more of this figure's colour tokens or parts.",
|
|
1162
|
+
"figure.text.data-port-invalid-part": "{id} is not a part of this figure.",
|
|
1163
|
+
"figure.text.data-port-invalid-group": "There is no motion group {id}.",
|
|
1164
|
+
"figure.text.data-port-bound": "First remove the bindings that read the status display {port}: {bindings}.",
|
|
1165
|
+
"figure.text.data-value-not-a-record": "{path}: not an object.",
|
|
1166
|
+
"figure.text.data-value-type-mismatch": "{path}: the value is of the wrong kind.",
|
|
1167
|
+
"figure.text.data-value-out-of-range": "{path}: outside its range.",
|
|
1085
1168
|
"figure.text.data-id-invalid": "The id {id} cannot be used: a letter first, then letters, digits, - and _.",
|
|
1086
1169
|
"figure.text.data-id-duplicate": "The id {id} is already used.",
|
|
1087
|
-
"figure.text.data-property-invalid-label": "Give {property} a name.",
|
|
1088
|
-
"figure.text.data-property-invalid-unit": "The unit of {property} is not a unit.",
|
|
1089
|
-
"figure.text.data-property-invalid-min": "The range of {property} runs from a smaller number to a larger one.",
|
|
1090
|
-
"figure.text.data-property-invalid-default": "The default of {property} must be within its range or its values.",
|
|
1091
|
-
"figure.text.data-property-invalid-values": "The values of {property} are one or more, each once.",
|
|
1092
|
-
"figure.text.data-property-absent": "{property} is not a data property of this figure.",
|
|
1093
|
-
"figure.text.data-target-absent-state": "There is no control {id}. Remove the connection that writes it first.",
|
|
1094
|
-
"figure.text.data-target-absent-palette": "There is no colour {id}.",
|
|
1095
|
-
"figure.text.data-target-absent-motion-clip": "There is no motion group {id}.",
|
|
1096
|
-
"figure.text.data-target-absent-motion-speed": "There is no motion group {id}.",
|
|
1097
|
-
"figure.text.data-target-duplicate": "Connections {a} and {b} both write {id}; one connection per target.",
|
|
1098
|
-
"figure.text.data-transform-kind": "This transform does not fit this property and target: a number goes as it is or scaled, a choice or true/false case by case.",
|
|
1099
|
-
"figure.text.data-transform-unit": "The units differ ({from} → {to}). Say how one becomes the other with a scale and offset.",
|
|
1100
|
-
"figure.text.data-transform-range": "It gives {min} to {max}; the target takes {low} to {high}.",
|
|
1101
|
-
"figure.text.data-transform-cases": "Give each value once: {value} is missing, given twice, or not a value of the property.",
|
|
1102
|
-
"figure.text.data-transform-output": "{value} is not a value the target takes.",
|
|
1103
|
-
"figure.text.data-property-connected": "Remove the connections that read {property} first: {connections}.",
|
|
1104
1170
|
"figure.text.motion-clip-absent": "{clip} is not a clip {group} can hold.",
|
|
1105
1171
|
"figure.text.motion-clip-shared": "{clip} is already in {group}; a clip is in one group.",
|
|
1106
1172
|
"figure.text.driver-writers": "Clips {clips} all move {state}. Clips that move one control go in one motion group.",
|