@things-factory/figure-ui 10.1.16 → 10.1.17

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.
Files changed (77) hide show
  1. package/client/modeller/figure-animations.ts +12 -12
  2. package/client/modeller/figure-canvas.ts +32 -48
  3. package/client/modeller/figure-capabilities.ts +10 -10
  4. package/client/modeller/figure-inspector.ts +39 -28
  5. package/client/modeller/figure-parts.ts +1 -1
  6. package/client/modeller/figure-preview.ts +122 -25
  7. package/client/modeller/figure-report.ts +24 -6
  8. package/client/modeller/figure-settings.ts +45 -2
  9. package/client/modeller/figure-side.ts +6 -6
  10. package/client/modeller/figure-source.ts +52 -32
  11. package/client/modeller/figure-thumbnail.ts +5 -5
  12. package/client/modeller/figure-view.ts +53 -0
  13. package/client/modeller/part-edits.ts +36 -36
  14. package/client/modeller/proposal.ts +25 -4
  15. package/client/pages/figure-modeller-page.ts +64 -58
  16. package/dist-client/modeller/figure-animations.d.ts +4 -4
  17. package/dist-client/modeller/figure-animations.js +10 -10
  18. package/dist-client/modeller/figure-animations.js.map +1 -1
  19. package/dist-client/modeller/figure-canvas.d.ts +7 -7
  20. package/dist-client/modeller/figure-canvas.js +27 -40
  21. package/dist-client/modeller/figure-canvas.js.map +1 -1
  22. package/dist-client/modeller/figure-capabilities.d.ts +2 -2
  23. package/dist-client/modeller/figure-capabilities.js +9 -9
  24. package/dist-client/modeller/figure-capabilities.js.map +1 -1
  25. package/dist-client/modeller/figure-inspector.d.ts +16 -5
  26. package/dist-client/modeller/figure-inspector.js +38 -27
  27. package/dist-client/modeller/figure-inspector.js.map +1 -1
  28. package/dist-client/modeller/figure-parts.d.ts +1 -1
  29. package/dist-client/modeller/figure-parts.js +1 -1
  30. package/dist-client/modeller/figure-parts.js.map +1 -1
  31. package/dist-client/modeller/figure-preview.d.ts +39 -0
  32. package/dist-client/modeller/figure-preview.js +112 -23
  33. package/dist-client/modeller/figure-preview.js.map +1 -1
  34. package/dist-client/modeller/figure-report.d.ts +9 -0
  35. package/dist-client/modeller/figure-report.js +23 -5
  36. package/dist-client/modeller/figure-report.js.map +1 -1
  37. package/dist-client/modeller/figure-settings.js +42 -2
  38. package/dist-client/modeller/figure-settings.js.map +1 -1
  39. package/dist-client/modeller/figure-side.d.ts +2 -2
  40. package/dist-client/modeller/figure-side.js +5 -5
  41. package/dist-client/modeller/figure-side.js.map +1 -1
  42. package/dist-client/modeller/figure-source.d.ts +17 -15
  43. package/dist-client/modeller/figure-source.js +40 -21
  44. package/dist-client/modeller/figure-source.js.map +1 -1
  45. package/dist-client/modeller/figure-thumbnail.d.ts +2 -2
  46. package/dist-client/modeller/figure-thumbnail.js +3 -3
  47. package/dist-client/modeller/figure-thumbnail.js.map +1 -1
  48. package/dist-client/modeller/figure-view.d.ts +45 -0
  49. package/dist-client/modeller/figure-view.js +27 -0
  50. package/dist-client/modeller/figure-view.js.map +1 -1
  51. package/dist-client/modeller/part-edits.d.ts +16 -16
  52. package/dist-client/modeller/part-edits.js +35 -35
  53. package/dist-client/modeller/part-edits.js.map +1 -1
  54. package/dist-client/modeller/proposal.d.ts +1 -1
  55. package/dist-client/modeller/proposal.js +15 -3
  56. package/dist-client/modeller/proposal.js.map +1 -1
  57. package/dist-client/pages/figure-modeller-page.d.ts +9 -10
  58. package/dist-client/pages/figure-modeller-page.js +60 -54
  59. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  60. package/dist-client/tsconfig.tsbuildinfo +1 -1
  61. package/dist-server/tsconfig.tsbuildinfo +1 -1
  62. package/package.json +4 -4
  63. package/test/ai-proposal-contract.test.ts +393 -12
  64. package/test/expect.ts +55 -0
  65. package/test/figure-source.test.ts +562 -0
  66. package/test/i18n-prefix-guard.test.ts +1 -1
  67. package/{client/modeller → test}/part-edits.test.ts +50 -45
  68. package/translations/en.json +31 -28
  69. package/translations/ja.json +16 -21
  70. package/translations/ko.json +41 -38
  71. package/translations/ms.json +16 -21
  72. package/translations/zh.json +16 -21
  73. package/client/modeller/figure-ask.ts +0 -265
  74. package/client/modeller/figure-source.test.ts +0 -352
  75. package/dist-client/modeller/figure-ask.d.ts +0 -40
  76. package/dist-client/modeller/figure-ask.js +0 -282
  77. package/dist-client/modeller/figure-ask.js.map +0 -1
@@ -1,265 +0,0 @@
1
- import '@material/web/icon/icon.js'
2
-
3
- import { css, html, LitElement, nothing } from 'lit'
4
- import { customElement, property, state } from 'lit/decorators.js'
5
-
6
- import { i18next, localize } from '@operato/i18n'
7
- import type { FigureSource } from '@hatiolab/figure-model'
8
- import { activePalette } from '@hatiolab/things-scene'
9
-
10
- import { proposeFigure } from '../graphql/index.js'
11
- import type { FigureProposal, ProposalFeedback } from '../types.js'
12
-
13
- /**
14
- * 말로 시키는 줄.
15
- *
16
- * ## 무엇을 시키는지가 화면에 보여야 한다
17
- *
18
- * 정본이 있으면 **고치는 것**이고, 없으면 **만드는 것**이다. 같은 칸으로 두 가지를
19
- * 하면 저작자가 「이게 지금 것을 덮어쓰나」를 매번 걱정한다. 그래서 무엇을 할지
20
- * 칸 옆에 적어 둔다.
21
- *
22
- * ## 후보를 여기서 받지 않는다
23
- *
24
- * 이 컴포넌트는 시키고 결과를 **알릴 뿐**이다. 받을지 버릴지는 저작면이 정한다 —
25
- * 정본은 한 벌이고 그 한 벌을 가진 자리가 정해야 한다.
26
- *
27
- * ## 팔레트를 함께 보낸다
28
- *
29
- * 서버는 팔레트를 모른다. 그리는 쪽이 어떤 토큰을 풀 수 있는지 아는 유일한 자리다.
30
- */
31
- @customElement('figure-ask')
32
- export class FigureAsk extends localize(i18next)(LitElement) {
33
- static styles = css`
34
- /*
35
- 이 부품은 **자기 줄을 갖지 않는다.**
36
-
37
- 전에는 테두리와 여백으로 스스로 한 줄이 되었다. 저작면의 머리줄 바로 아래에 놓이니
38
- 화면 위쪽이 두 줄이 되었고, 위 줄은 이름을 페이지 제목과 겹쳐 적고 있었다. 캔버스가
39
- 주인공인 화면에서 그 두 줄은 비싸다.
40
-
41
- 이제 머리줄 안에 들어가 그 줄의 남는 폭을 쓴다. 테두리·여백은 담는 줄이 갖는다.
42
- */
43
- :host {
44
- display: flex;
45
- flex: 1;
46
- min-width: 0;
47
- font: var(--label-font, inherit);
48
- /*
49
- 이 줄의 글자 크기를 여기서 정한다.
50
-
51
- 안 적어 두면 브라우저 기본 크기(16px)를 물려받는다. 아래 패널들은 전부 12px
52
- 언저리라 위 두 줄만 커 보이고, 단추가 화면 전체와 따로 논다.
53
-
54
- form 요소(input · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.
55
- */
56
- font-size: 0.76rem;
57
- }
58
-
59
- form {
60
- display: flex;
61
- flex-wrap: wrap;
62
- flex: 1;
63
- min-width: 0;
64
- align-items: center;
65
- gap: var(--spacing-medium, 8px);
66
- }
67
-
68
- md-icon[lead] {
69
- --md-icon-size: 18px;
70
- flex: none;
71
- color: var(--md-sys-color-primary);
72
- }
73
-
74
- input[ask] {
75
- flex: 1;
76
- min-width: 0;
77
- padding: 6px 10px;
78
- font: var(--input-field-font, inherit);
79
- font-size: 0.78rem;
80
- color: var(--md-sys-color-on-surface);
81
- background-color: var(--md-sys-color-surface-container-lowest);
82
- border: 1px solid var(--md-sys-color-outline-variant);
83
- border-radius: 6px;
84
- }
85
- input[ask]:focus {
86
- outline: 2px solid var(--md-sys-color-primary);
87
- outline-offset: -1px;
88
- }
89
- input[ask][disabled] {
90
- opacity: 0.5;
91
- }
92
-
93
- /* 무엇을 시키는 것인지 — 만드나 고치나 */
94
- span[mode] {
95
- flex: none;
96
- font-size: 0.72rem;
97
- color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
98
- }
99
-
100
- /* 그림 붙이기. 붙인 것이 있으면 이름을 보여 준다 */
101
- label[picture] {
102
- display: flex;
103
- align-items: center;
104
- gap: 4px;
105
- flex: none;
106
- padding: 5px 9px;
107
- font-size: 0.74rem;
108
- color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
109
- border: 1px solid var(--md-sys-color-outline-variant);
110
- border-radius: 6px;
111
- cursor: pointer;
112
- }
113
- label[picture]:hover {
114
- background-color: var(--md-sys-color-surface-container);
115
- }
116
- label[picture][has] {
117
- color: var(--md-sys-color-on-primary-container);
118
- background-color: var(--md-sys-color-primary-container);
119
- border-color: transparent;
120
- }
121
- label[picture] md-icon {
122
- --md-icon-size: 16px;
123
- }
124
- label[picture] input {
125
- display: none;
126
- }
127
- span[filename] {
128
- max-width: 8em;
129
- overflow: hidden;
130
- text-overflow: ellipsis;
131
- white-space: nowrap;
132
- }
133
-
134
- /*
135
- 채운 파랑 단추를 저장과 나눠 갖지 않는다.
136
-
137
- 머리줄의 저장이 이 화면의 주 동작이다. 바로 아래 줄에 같은 파랑 단추가 하나 더
138
- 있으면 어느 쪽이 주인지 눈이 못 고른다. 이쪽은 톤 단추로 한 단계 내린다.
139
- */
140
- button[go] {
141
- flex: none;
142
- padding: 6px 14px;
143
- font: var(--label-font, inherit);
144
- font-size: 0.76rem;
145
- font-weight: 600;
146
- color: var(--md-sys-color-on-primary-container);
147
- background-color: var(--md-sys-color-primary-container);
148
- border: none;
149
- border-radius: 6px;
150
- cursor: pointer;
151
- }
152
- button[go]:hover:not([disabled]) {
153
- background-color: color-mix(in srgb, var(--md-sys-color-primary) 24%, var(--md-sys-color-primary-container));
154
- }
155
- button[go][disabled] {
156
- opacity: 0.4;
157
- cursor: default;
158
- }
159
-
160
- /* 못 만들었을 때. 사유를 삼키지 않는다 — 말을 고쳐 다시 시킬 단서다 */
161
- div[failed] {
162
- display: flex;
163
- gap: 6px;
164
- padding: 0 var(--spacing-large, 12px) var(--spacing-medium, 8px) var(--spacing-large, 12px);
165
- font-size: 0.74rem;
166
- line-height: 1.5;
167
- color: var(--md-sys-color-error);
168
- }
169
- div[failed] md-icon {
170
- --md-icon-size: 16px;
171
- flex: none;
172
- }
173
- `
174
-
175
- /** 지금 정본. 있으면 고치는 것이고 없으면 만드는 것이다. */
176
- @property({ type: Object }) source?: FigureSource
177
- /** 새로 만들 때 쓸 타입 이름. */
178
- @property({ type: String }) type = ''
179
- /** 이 모델러 세션에서 사람이 명시적으로 남긴 이전 후보 반응. */
180
- @property({ type: Array }) feedback: ProposalFeedback[] = []
181
- @property({ type: Boolean }) imageOnly = false
182
-
183
- @state() private asking = false
184
- @state() private failure = ''
185
- @state() private picture?: File
186
-
187
- render() {
188
- const revising = !!this.source
189
-
190
- return html`
191
- <form title="이미지의 형태와 비율을 참고한 후보를 만듭니다. 저장은 하지 않습니다." @submit=${(e: Event) => this.submit(e)}>
192
- <md-icon lead>auto_awesome</md-icon>
193
- <span mode>${i18next.t(revising ? 'figure.text.ask-to-revise' : 'figure.text.ask-to-create')}</span>
194
- <input
195
- ask
196
- ?disabled=${this.asking}
197
- placeholder=${i18next.t(revising ? 'figure.text.ask-revise-example' : 'figure.text.ask-create-example')}
198
- />
199
- <label picture ?has=${!!this.picture} title=${i18next.t('figure.text.picture-is-for-proportions')}>
200
- <md-icon>image</md-icon>
201
- ${this.picture
202
- ? html`<span filename>${this.picture.name}</span>`
203
- : i18next.t('figure.button.attach-a-picture')}
204
- <input type="file" accept="image/*" @change=${(e: Event) => this.pick(e)} />
205
- </label>
206
- <button go ?disabled=${this.asking || (this.imageOnly && !this.picture)}>
207
- ${this.asking ? i18next.t('figure.text.asking') : i18next.t('figure.button.ask')}
208
- </button>
209
- </form>
210
-
211
- ${this.failure
212
- ? html`
213
- <div failed>
214
- <md-icon>error</md-icon>
215
- <span>${this.failure}</span>
216
- </div>
217
- `
218
- : nothing}
219
- `
220
- }
221
-
222
- private pick(e: Event) {
223
- const input = e.target as HTMLInputElement
224
- this.picture = input.files?.[0]
225
- }
226
-
227
- private async submit(e: Event) {
228
- e.preventDefault()
229
-
230
- const input = this.renderRoot.querySelector('input[ask]') as HTMLInputElement
231
- const prompt = input?.value?.trim()
232
- if (!prompt || this.asking || (this.imageOnly && !this.picture)) {
233
- return
234
- }
235
-
236
- this.asking = true
237
- this.failure = ''
238
- const baseSource = this.source ? JSON.stringify(this.source) : undefined
239
-
240
- try {
241
- const proposal = await proposeFigure({
242
- prompt,
243
- base: baseSource,
244
- type: this.source ? undefined : this.type,
245
- // 그리는 쪽이 풀 수 있는 토큰만 보낸다. 서버는 이 목록 밖의 색을 거절한다.
246
- palette: Object.keys(activePalette() as Record<string, string>),
247
- // 구조적으로는 맞지만 품질 근거가 남으면 한 번 더 개선 후보를 시킨다.
248
- refine: true,
249
- feedback: this.feedback,
250
- image: this.picture
251
- })
252
-
253
- this.dispatchEvent(
254
- new CustomEvent('proposed', { detail: { proposal, baseSource }, bubbles: true, composed: true })
255
- )
256
- input.value = ''
257
- this.picture = undefined
258
- } catch (err) {
259
- // 사유를 그대로 보여 준다. 「실패했습니다」로 뭉개면 말을 어떻게 고칠지 모른다.
260
- this.failure = (err as Error).message
261
- } finally {
262
- this.asking = false
263
- }
264
- }
265
- }
@@ -1,352 +0,0 @@
1
- import { existsSync, readFileSync } from 'fs'
2
- import { dirname, join } from 'path'
3
-
4
- import { toFigureSource, fromFigureSource } from './figure-source'
5
- import type { BoardModel, PartModel } from './figure-source'
6
- import type { FigureSource } from '@hatiolab/figure-model'
7
-
8
- /**
9
- * 씬 모델 ↔ `FigureSource`.
10
- *
11
- * 저작 중의 정본은 씬 모델이고, 형식은 저장하거나 판정할 때 접어서 만든다. 두 어휘가
12
- * 다르므로 **옮기는 일이 한 곳에만** 있어야 하고, 그 한 곳이 맞는지를 여기서 못 박는다.
13
- *
14
- * 가장 중요한 것은 왕복이다. 접었다 펴면 같아야 하고 펴서 접어도 같아야 한다 —
15
- * 어느 한쪽에서만 맞으면 AI 가 제안해 온 정본을 편집하는 순간 값이 샌다.
16
- */
17
-
18
- /** 판 3000 x 2000 x 800 — 세 축이 다 달라야 축을 바꿔 써도 시험이 잡는다. */
19
- const BOARD: BoardModel = {
20
- width: 3000,
21
- height: 2000,
22
- depth: 800,
23
- figureType: 'AGV',
24
- detailLevel: 'L'
25
- }
26
-
27
- const PART: PartModel = {
28
- type: 'figure-part',
29
- name: 'deck',
30
- primitive: 'cube',
31
- left: 1100,
32
- top: 700,
33
- width: 800,
34
- height: 600,
35
- zPos: 300,
36
- depth: 200,
37
- token: 'palette.primary'
38
- }
39
-
40
- describe('접기 — 씬 모델을 형식으로', () => {
41
- it('판이 기준 상자가 된다 — 높이는 판의 depth 다', () => {
42
- const { base } = toFigureSource(BOARD, [])
43
- expect(base).toEqual({ x: 3000, y: 800, z: 2000 })
44
- })
45
-
46
- it('부품 자리가 판 중심 기준이 된다', () => {
47
- const [part] = toFigureSource(BOARD, [PART]).parts
48
-
49
- // 평면 중심 (1100+400, 700+300) = (1500, 1000), 판 중심 (1500, 1000) → 원점
50
- expect(part.transform.position.x).toBe(0)
51
- expect(part.transform.position.z).toBe(0)
52
- })
53
-
54
- it('zPos 는 밑면이다 — 중심으로 올려 적는다', () => {
55
- const [part] = toFigureSource(BOARD, [PART]).parts
56
-
57
- // 밑면 300 + 두께 200/2 = 중심 400. 판 높이 800 의 절반을 빼면 0
58
- expect(part.transform.position.y).toBe(0)
59
- })
60
-
61
- it('가운데를 벗어난 부품이 그만큼 어긋나 적힌다', () => {
62
- const moved = { ...PART, left: 1200, top: 900, zPos: 500 }
63
- const [part] = toFigureSource(BOARD, [moved]).parts
64
-
65
- expect(part.transform.position).toEqual({ x: 100, y: 200, z: 200 })
66
- })
67
-
68
- it('크기가 축 이름으로 바뀐다 — 두께가 y 다', () => {
69
- const [part] = toFigureSource(BOARD, [PART]).parts
70
- expect(part.transform.size).toEqual({ x: 800, y: 200, z: 600 })
71
- })
72
-
73
- /*
74
- 씬은 라디안을 쓰고 이름이 뒤섞여 있다 — rotationY 가 three 의 z 로 간다.
75
- 형식은 도를 쓰고 축 이름이 그대로다.
76
- */
77
- it('회전이 도로 바뀌고 축이 제자리를 찾는다', () => {
78
- const turned = { ...PART, rotationX: Math.PI / 6, rotation: Math.PI / 2, rotationY: Math.PI }
79
- const [part] = toFigureSource(BOARD, [turned]).parts
80
-
81
- expect(part.transform.rotation).toEqual({ x: 30, y: -90, z: 180 })
82
- })
83
-
84
- it('회전이 없으면 필드를 두지 않는다', () => {
85
- const [part] = toFigureSource(BOARD, [PART]).parts
86
- expect(part.transform.rotation).toBeUndefined()
87
- })
88
-
89
- it('재질이 한 덩어리로 모인다', () => {
90
- const [part] = toFigureSource(BOARD, [{ ...PART, preset: 'metal', flatShading: false }]).parts
91
- expect(part.material).toEqual({ token: 'palette.primary', preset: 'metal', flatShading: false })
92
- })
93
-
94
- it('단면 값이 shape 으로 모인다', () => {
95
- const [part] = toFigureSource(BOARD, [{ ...PART, primitive: 'rect', round: 40 }]).parts
96
- expect(part.shape).toEqual({ round: 40 })
97
- })
98
-
99
- it('단면 값이 없으면 shape 을 두지 않는다', () => {
100
- expect(toFigureSource(BOARD, [PART]).parts[0].shape).toBeUndefined()
101
- })
102
- })
103
-
104
- describe('펴기 — 형식을 씬 모델로', () => {
105
- const SOURCE: FigureSource = {
106
- type: 'AGV',
107
- base: { x: 3000, y: 800, z: 2000 },
108
- detailLevel: 'L',
109
- parts: [
110
- {
111
- name: 'deck',
112
- primitive: 'cube',
113
- transform: { position: { x: 100, y: 200, z: 200 }, size: { x: 800, y: 200, z: 600 } },
114
- material: { token: 'palette.primary' }
115
- }
116
- ]
117
- }
118
-
119
- it('기준 상자가 판이 된다', () => {
120
- const { board } = fromFigureSource(SOURCE)
121
- expect(board).toMatchObject({ width: 3000, height: 2000, depth: 800 })
122
- })
123
-
124
- it('중심 기준 자리가 씬의 모서리·밑면으로 바뀐다', () => {
125
- const [part] = fromFigureSource(SOURCE).parts
126
- expect(part).toMatchObject({ left: 1200, top: 900, zPos: 500 })
127
- })
128
-
129
- it('부품이 씬 타입을 갖는다 — 판에 그대로 놓을 수 있어야 한다', () => {
130
- expect(fromFigureSource(SOURCE).parts[0].type).toBe('figure-part')
131
- })
132
- })
133
-
134
- /*
135
- 왕복.
136
-
137
- 이것이 이 모듈의 존재 이유다. 한쪽에서만 맞으면 AI 가 제안해 온 정본을 펴서
138
- 편집하고 다시 접는 순간 값이 샌다 — 그것도 조용히.
139
- */
140
- /**
141
- * 형식이 가진 것을 되도록 다 담은 정본. 안 쓰이는 필드도 넣는다.
142
- *
143
- * describe 밖에 둔다 — 아래 「픽스처가 형식을 다 담는다」가 이것을 형식의 필드 목록과
144
- * 맞대어 본다.
145
- */
146
- /** 형식이 가진 것을 되도록 다 담은 정본. 안 쓰이는 필드도 넣는다. */
147
- const FULL: FigureSource = {
148
- version: 1,
149
- type: 'AGV',
150
- base: { x: 3000, y: 800, z: 2000 },
151
- placement: 'floor',
152
- detailLevel: 'L',
153
- styleKit: 'factory',
154
- parts: [
155
- {
156
- name: 'deck',
157
- primitive: 'cube',
158
- transform: { position: { x: 100, y: 200, z: 200 }, size: { x: 800, y: 200, z: 600 } },
159
- material: { token: 'palette.primary', preset: 'metal', flatShading: true }
160
- },
161
- {
162
- name: 'wheel',
163
- primitive: 'cylinder',
164
- segments: 24,
165
- transform: {
166
- position: { x: -700, y: -260, z: 420 },
167
- size: { x: 180, y: 70, z: 180 },
168
- rotation: { z: 90 }
169
- },
170
- material: { token: 'palette.dark', preset: 'rubber' }
171
- },
172
- {
173
- name: 'panel',
174
- primitive: 'rect',
175
- // 속을 판 단면. 한 겹 안이라 처음 판 시험이 못 봤던 자리다.
176
- shape: { round: 40, hollow: { wall: 8, floor: 4 } },
177
- transform: {
178
- position: { x: 0, y: 260, z: -500 },
179
- size: { x: 400, y: 30, z: 200 },
180
- rotation: { x: 15, y: -30, z: 45 }
181
- },
182
- material: { token: 'palette.accent', transparent: true },
183
- // 아직 아무 일도 안 하는 필드들. 잃어버리면 안 된다.
184
- materialSlot: 'cap',
185
- keepRound: false,
186
- sizing: 'fixed',
187
- anchor: { x: 'span', y: 'max', z: 'center' },
188
- label: { name: 'rpm', source: 'speed', when: 'zoomed' }
189
- },
190
- {
191
- name: 'foot',
192
- primitive: 'polygon',
193
- shape: {
194
- path: [
195
- { x: -60, y: -40 },
196
- { x: 60, y: -40 },
197
- { x: 0, y: 40 }
198
- ]
199
- },
200
- transform: { position: { x: 900, y: -340, z: -600 }, size: { x: 120, y: 80, z: 80 } },
201
- material: { token: 'palette.neutral' },
202
- sizing: 'repeat',
203
- repeat: { axis: 'x', pitch: 200 }
204
- },
205
- {
206
- /*
207
- 물건을 놓는 자리이면서 드나드는 문이다 — AGV 의 상판이 그렇다. **그려지지 않으므로**
208
- 재질이 없다.
209
-
210
- 그리지 않는 것이라 화면으로는 왕복에서 누락됐는지 알 수 없다. 자리를 잃어버린 도형은
211
- 멀쩡해 보이면서 아무것도 받지 못한다.
212
- */
213
- name: 'seat',
214
- primitive: 'cube',
215
- transform: { position: { x: 0, y: 420, z: 0 }, size: { x: 700, y: 500, z: 500 } },
216
- capability: { roles: ['slot', 'port-in', 'port-out'], accepts: ['PALLET'], capacity: 2 }
217
- }
218
- ],
219
- /* 이 도형이 지는 능력. 부품이 아니라 도형 전체의 것이다. */
220
- capabilities: ['FlowNode'],
221
- /*
222
- 움직임. 편집면이 아직 안 만지는 자리이며, **바로 그래서 여기 있다.**
223
-
224
- 이 픽스처가 「형식이 가진 것을 되도록 다 담은 정본」인데 clip 이 빠져 있었고, 그
225
- 사이 `toFigureSource` 가 clip 을 안 실었다. 시험은 초록이었다 — 픽스처에 없는 것은
226
- 시험이 못 본다. 표본 롤러에 clip 을 적고 미리보기에서 아무것도 안 돌고 나서야 알았다.
227
- */
228
- animations: [
229
- {
230
- name: 'spin',
231
- drive: 'loop',
232
- channels: [
233
- {
234
- target: 'wheel',
235
- path: 'rotation',
236
- pivot: { x: 0, y: 0, z: 0 },
237
- keys: [
238
- { at: 0, value: { x: 0, y: 0, z: 0 } },
239
- { at: 1, value: { x: 0, y: 360, z: 0 } }
240
- ]
241
- }
242
- ]
243
- }
244
- ]
245
- }
246
-
247
- describe('왕복해도 같다', () => {
248
-
249
- it('펴서 접으면 그대로다', () => {
250
- const { board, parts } = fromFigureSource(FULL)
251
- expect(toFigureSource(board, parts)).toEqual(FULL)
252
- })
253
-
254
- it('접어서 펴면 그대로다', () => {
255
- const { board, parts } = fromFigureSource(FULL)
256
- const again = fromFigureSource(toFigureSource(board, parts))
257
-
258
- expect(again.board).toEqual(board)
259
- expect(again.parts).toEqual(parts)
260
- })
261
-
262
- it('두 번 왕복해도 값이 밀리지 않는다 — 라디안·도를 오가도', () => {
263
- let source = FULL
264
- for (let i = 0; i < 5; i++) {
265
- const { board, parts } = fromFigureSource(source)
266
- source = toFigureSource(board, parts)
267
- }
268
- expect(source).toEqual(FULL)
269
- })
270
-
271
- it('움직임도 살아남는다 — 열었다 저장하는 것만으로 지워지던 자리다', () => {
272
- const { board, parts } = fromFigureSource(FULL)
273
-
274
- expect(toFigureSource(board, parts).animations).toEqual(FULL.animations)
275
- })
276
-
277
- it('안 쓰이는 필드도 살아남는다 — 조용히 사라지면 저작자가 알 길이 없다', () => {
278
- const { board, parts } = fromFigureSource(FULL)
279
- const panel = toFigureSource(board, parts).parts.find(part => part.name === 'panel')!
280
-
281
- expect(panel.materialSlot).toBe('cap')
282
- expect(panel.sizing).toBe('fixed')
283
- expect(panel.label).toEqual({ name: 'rpm', source: 'speed', when: 'zoomed' })
284
- })
285
- })
286
-
287
- /*
288
- 픽스처가 형식을 따라가는지.
289
-
290
- **이 파일에서 필드가 네 번 샜다** — `ports` · `animations` · `anchor` · `shape.hollow`.
291
- 네 번 다 왕복 시험은 초록이었다. 위의 `FULL` 이 「형식이 가진 것을 되도록 다 담은
292
- 정본」이라고 적혀 있었지만 실제로는 그때그때 손으로 채운 것이라, 형식에 필드가 늘면
293
- 픽스처는 그대로 있고 시험은 늘어난 자리를 보지 못했다.
294
-
295
- 네 번째 것은 **한 겹 안**이라 최상위만 세던 첫 판이 놓쳤다. 그래서 픽스처의 키를
296
- 깊이 상관없이 다 모아 놓고 맞댄다.
297
-
298
- 그래서 픽스처를 믿지 않고 **형식이 스스로 밝힌 필드 목록**을 읽는다. 형식이 붙은
299
- 타입 선언(`dist/types.d.ts`)이 정본이고, 거기 적힌 이름이 픽스처에 없으면 여기서
300
- 깨진다. 다음 번 필드는 화면이 아니라 이 시험이 먼저 잡는다.
301
- */
302
- describe('픽스처가 형식을 다 담는다', () => {
303
- /**
304
- * 형식이 붙여 보낸 타입 선언 파일.
305
- *
306
- * `require.resolve` 로는 못 잡는다 — jest 의 resolver 가 `.d.ts` 를 모듈로 보지
307
- * 않는다. 올라가면서 찾고, 못 찾으면 조용히 건너뛰지 않고 여기서 깨진다.
308
- */
309
- function declarationPath(): string {
310
- for (let at = __dirname; at !== dirname(at); at = dirname(at)) {
311
- const candidate = join(at, 'node_modules/@hatiolab/figure-model/dist/types.d.ts')
312
- if (existsSync(candidate)) return candidate
313
- }
314
- throw new Error('figure-model 의 타입 선언을 못 찾았다 — 이 시험은 그것이 정본이다')
315
- }
316
-
317
- /** 붙은 타입 선언에서 한 인터페이스의 필드 이름을 뽑는다. */
318
- function fieldsOf(name: string): string[] {
319
- const declared = readFileSync(declarationPath(), 'utf-8')
320
- const start = declared.indexOf(`export interface ${name} {`)
321
- expect(start).toBeGreaterThan(-1)
322
-
323
- // 중첩이 없는 평평한 인터페이스다. 다음 닫는 중괄호가 끝이다.
324
- const body = declared.slice(start, declared.indexOf('\n}', start))
325
- return [...body.matchAll(/^\s{4}(\w+)\??:/gm)].map(m => m[1])
326
- }
327
-
328
- /** 픽스처가 쓴 키를 깊이 상관없이 모은다 — 흘린 필드가 한 겹 안에 있었다. */
329
- function keysDeep(value: unknown, into = new Set<string>()): Set<string> {
330
- if (Array.isArray(value)) value.forEach(one => keysDeep(one, into))
331
- else if (value && typeof value === 'object') {
332
- for (const [key, one] of Object.entries(value)) {
333
- into.add(key)
334
- keysDeep(one, into)
335
- }
336
- }
337
- return into
338
- }
339
-
340
- const WRITTEN = keysDeep(FULL)
341
-
342
- /*
343
- `fold` 가 실제로 낱개로 푸는 것들이다. 통째로 나르는 구조(`transform` 처럼)는 필드가
344
- 늘어도 안 새므로 여기 없다.
345
- */
346
- it.each(['FigureSource', 'FigurePart', 'PartShape', 'HollowSpec'])(
347
- '%s 가 가진 모든 필드가 왕복 픽스처에 있다',
348
- name => {
349
- expect(fieldsOf(name).filter(field => !WRITTEN.has(field))).toEqual([])
350
- }
351
- )
352
- })
@@ -1,40 +0,0 @@
1
- import '@material/web/icon/icon.js';
2
- import { LitElement } from 'lit';
3
- import type { FigureSource } from '@hatiolab/figure-model';
4
- import type { ProposalFeedback } from '../types.js';
5
- declare const FigureAsk_base: (new (...args: any[]) => LitElement) & typeof LitElement;
6
- /**
7
- * 말로 시키는 줄.
8
- *
9
- * ## 무엇을 시키는지가 화면에 보여야 한다
10
- *
11
- * 정본이 있으면 **고치는 것**이고, 없으면 **만드는 것**이다. 같은 칸으로 두 가지를
12
- * 하면 저작자가 「이게 지금 것을 덮어쓰나」를 매번 걱정한다. 그래서 무엇을 할지
13
- * 칸 옆에 적어 둔다.
14
- *
15
- * ## 후보를 여기서 받지 않는다
16
- *
17
- * 이 컴포넌트는 시키고 결과를 **알릴 뿐**이다. 받을지 버릴지는 저작면이 정한다 —
18
- * 정본은 한 벌이고 그 한 벌을 가진 자리가 정해야 한다.
19
- *
20
- * ## 팔레트를 함께 보낸다
21
- *
22
- * 서버는 팔레트를 모른다. 그리는 쪽이 어떤 토큰을 풀 수 있는지 아는 유일한 자리다.
23
- */
24
- export declare class FigureAsk extends FigureAsk_base {
25
- static styles: import("lit").CSSResult;
26
- /** 지금 정본. 있으면 고치는 것이고 없으면 만드는 것이다. */
27
- source?: FigureSource;
28
- /** 새로 만들 때 쓸 타입 이름. */
29
- type: string;
30
- /** 이 모델러 세션에서 사람이 명시적으로 남긴 이전 후보 반응. */
31
- feedback: ProposalFeedback[];
32
- imageOnly: boolean;
33
- private asking;
34
- private failure;
35
- private picture?;
36
- render(): import("lit-html").TemplateResult<1>;
37
- private pick;
38
- private submit;
39
- }
40
- export {};