@things-factory/figure-ui 10.1.3
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/bootstrap.ts +16 -0
- package/client/graphql/index.ts +209 -0
- package/client/index.ts +2 -0
- package/client/modeller/figure-ask.ts +247 -0
- package/client/modeller/figure-canvas.ts +482 -0
- package/client/modeller/figure-inspector.ts +1054 -0
- package/client/modeller/figure-parts.ts +479 -0
- package/client/modeller/figure-preview.ts +381 -0
- package/client/modeller/figure-report.ts +895 -0
- package/client/modeller/figure-settings.ts +285 -0
- package/client/modeller/figure-side.ts +162 -0
- package/client/modeller/figure-source.test.ts +336 -0
- package/client/modeller/figure-source.ts +338 -0
- package/client/modeller/figure-view.ts +79 -0
- package/client/modeller/maturity.ts +288 -0
- package/client/modeller/part-edits.test.ts +252 -0
- package/client/modeller/part-edits.ts +246 -0
- package/client/modeller/proposal.ts +142 -0
- package/client/modeller/scene-budget.ts +102 -0
- package/client/pages/figure-list-page.ts +274 -0
- package/client/pages/figure-modeller-page.ts +1067 -0
- package/client/route.ts +11 -0
- package/client/themes/brand.css +51 -0
- package/client/themes/dark.css +64 -0
- package/client/themes/light.css +95 -0
- package/client/tsconfig.json +19 -0
- package/client/types.ts +96 -0
- package/client/viewparts/figure-card.ts +226 -0
- package/dist-client/bootstrap.d.ts +14 -0
- package/dist-client/bootstrap.js +17 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/graphql/index.d.ts +41 -0
- package/dist-client/graphql/index.js +173 -0
- package/dist-client/graphql/index.js.map +1 -0
- package/dist-client/index.d.ts +2 -0
- package/dist-client/index.js +3 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/modeller/figure-ask.d.ts +36 -0
- package/dist-client/modeller/figure-ask.js +256 -0
- package/dist-client/modeller/figure-ask.js.map +1 -0
- package/dist-client/modeller/figure-canvas.d.ts +128 -0
- package/dist-client/modeller/figure-canvas.js +431 -0
- package/dist-client/modeller/figure-canvas.js.map +1 -0
- package/dist-client/modeller/figure-inspector.d.ts +149 -0
- package/dist-client/modeller/figure-inspector.js +991 -0
- package/dist-client/modeller/figure-inspector.js.map +1 -0
- package/dist-client/modeller/figure-parts.d.ts +64 -0
- package/dist-client/modeller/figure-parts.js +478 -0
- package/dist-client/modeller/figure-parts.js.map +1 -0
- package/dist-client/modeller/figure-preview.d.ts +62 -0
- package/dist-client/modeller/figure-preview.js +360 -0
- package/dist-client/modeller/figure-preview.js.map +1 -0
- package/dist-client/modeller/figure-report.d.ts +134 -0
- package/dist-client/modeller/figure-report.js +881 -0
- package/dist-client/modeller/figure-report.js.map +1 -0
- package/dist-client/modeller/figure-settings.d.ts +39 -0
- package/dist-client/modeller/figure-settings.js +277 -0
- package/dist-client/modeller/figure-settings.js.map +1 -0
- package/dist-client/modeller/figure-side.d.ts +41 -0
- package/dist-client/modeller/figure-side.js +175 -0
- package/dist-client/modeller/figure-side.js.map +1 -0
- package/dist-client/modeller/figure-source.d.ts +196 -0
- package/dist-client/modeller/figure-source.js +153 -0
- package/dist-client/modeller/figure-source.js.map +1 -0
- package/dist-client/modeller/figure-view.d.ts +57 -0
- package/dist-client/modeller/figure-view.js +36 -0
- package/dist-client/modeller/figure-view.js.map +1 -0
- package/dist-client/modeller/maturity.d.ts +55 -0
- package/dist-client/modeller/maturity.js +212 -0
- package/dist-client/modeller/maturity.js.map +1 -0
- package/dist-client/modeller/part-edits.d.ts +64 -0
- package/dist-client/modeller/part-edits.js +218 -0
- package/dist-client/modeller/part-edits.js.map +1 -0
- package/dist-client/modeller/proposal.d.ts +45 -0
- package/dist-client/modeller/proposal.js +96 -0
- package/dist-client/modeller/proposal.js.map +1 -0
- package/dist-client/modeller/scene-budget.d.ts +78 -0
- package/dist-client/modeller/scene-budget.js +80 -0
- package/dist-client/modeller/scene-budget.js.map +1 -0
- package/dist-client/pages/figure-list-page.d.ts +40 -0
- package/dist-client/pages/figure-list-page.js +280 -0
- package/dist-client/pages/figure-list-page.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.d.ts +219 -0
- package/dist-client/pages/figure-modeller-page.js +1045 -0
- package/dist-client/pages/figure-modeller-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +11 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/themes/brand.css +51 -0
- package/dist-client/themes/dark.css +64 -0
- package/dist-client/themes/light.css +95 -0
- package/dist-client/types.d.ts +93 -0
- package/dist-client/types.js +2 -0
- package/dist-client/types.js.map +1 -0
- package/dist-client/viewparts/figure-card.d.ts +24 -0
- package/dist-client/viewparts/figure-card.js +221 -0
- package/dist-client/viewparts/figure-card.js.map +1 -0
- package/dist-server/index.d.ts +7 -0
- package/dist-server/index.js +3 -0
- package/dist-server/index.js.map +1 -0
- package/package.json +41 -0
- package/server/index.ts +7 -0
- package/server/tsconfig.json +18 -0
- package/test/brand-palette.test.ts +188 -0
- package/test/heading-scale-guard.test.ts +144 -0
- package/test/resolve-ts-siblings.mjs +67 -0
- package/things-factory.config.js +11 -0
- package/translations/en.json +247 -0
- package/translations/ja.json +247 -0
- package/translations/ko.json +247 -0
- package/translations/ms.json +247 -0
- package/translations/zh.json +247 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import gql from 'graphql-tag';
|
|
2
|
+
import { client } from '@operato/graphql';
|
|
3
|
+
/**
|
|
4
|
+
* 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**
|
|
5
|
+
*
|
|
6
|
+
* 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가
|
|
7
|
+
* 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는
|
|
8
|
+
* 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI
|
|
9
|
+
* 요청에서 그렇게 났다.
|
|
10
|
+
*
|
|
11
|
+
* 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.
|
|
12
|
+
* 여기서 막는 것은 **`data` 자체가 없는 것**이다.
|
|
13
|
+
*/
|
|
14
|
+
function unwrap(response, field) {
|
|
15
|
+
const said = response.errors?.[0]?.message;
|
|
16
|
+
if (said)
|
|
17
|
+
throw new Error(said);
|
|
18
|
+
if (!response.data) {
|
|
19
|
+
throw new Error(`서버가 ${field} 를 돌려주지 않았습니다.`);
|
|
20
|
+
}
|
|
21
|
+
return response.data[field];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.
|
|
25
|
+
*
|
|
26
|
+
* `score`·`triangles` 를 함께 받는 이유는 카드에 그것을 보여 주기 때문이다. 저작자가
|
|
27
|
+
* 목록에서 「이 Figure 가 무거운가」를 바로 알아야 한다.
|
|
28
|
+
*/
|
|
29
|
+
const FIGURE_LIST_FIELDS = `
|
|
30
|
+
id
|
|
31
|
+
type
|
|
32
|
+
name
|
|
33
|
+
description
|
|
34
|
+
category
|
|
35
|
+
state
|
|
36
|
+
score
|
|
37
|
+
triangles
|
|
38
|
+
groups
|
|
39
|
+
thumbnail
|
|
40
|
+
thumbnailUpdatedAt
|
|
41
|
+
updatedAt
|
|
42
|
+
updater { id name }
|
|
43
|
+
`;
|
|
44
|
+
export async function fetchFigureList(params) {
|
|
45
|
+
const filters = [];
|
|
46
|
+
if (params.category)
|
|
47
|
+
filters.push({ name: 'category', operator: 'eq', value: params.category });
|
|
48
|
+
if (params.state)
|
|
49
|
+
filters.push({ name: 'state', operator: 'eq', value: params.state });
|
|
50
|
+
const response = await client.query({
|
|
51
|
+
query: gql `
|
|
52
|
+
query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
|
|
53
|
+
figures(filters: $filters, pagination: $pagination, sortings: $sortings) {
|
|
54
|
+
items { ${FIGURE_LIST_FIELDS} }
|
|
55
|
+
total
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`,
|
|
59
|
+
variables: {
|
|
60
|
+
filters,
|
|
61
|
+
pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },
|
|
62
|
+
sortings: [{ name: 'updatedAt', desc: true }]
|
|
63
|
+
},
|
|
64
|
+
fetchPolicy: 'network-only'
|
|
65
|
+
});
|
|
66
|
+
return unwrap(response, 'figures');
|
|
67
|
+
}
|
|
68
|
+
/** 저작면에 필요한 전부 — `source` 를 포함한다. */
|
|
69
|
+
export async function fetchFigure(id) {
|
|
70
|
+
const response = await client.query({
|
|
71
|
+
query: gql `
|
|
72
|
+
query ($id: String!) {
|
|
73
|
+
figure(id: $id) {
|
|
74
|
+
${FIGURE_LIST_FIELDS}
|
|
75
|
+
source
|
|
76
|
+
properties
|
|
77
|
+
version
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
`,
|
|
81
|
+
variables: { id },
|
|
82
|
+
fetchPolicy: 'network-only'
|
|
83
|
+
});
|
|
84
|
+
return unwrap(response, 'figure');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 이미 쓰이고 있는 타입 이름.
|
|
88
|
+
*
|
|
89
|
+
* `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러
|
|
90
|
+
* 거절당하고 나서 아는 것은 늦다.
|
|
91
|
+
*/
|
|
92
|
+
export async function fetchFigureTypeNames() {
|
|
93
|
+
const response = await client.query({
|
|
94
|
+
query: gql `
|
|
95
|
+
query {
|
|
96
|
+
figureTypeNames
|
|
97
|
+
}
|
|
98
|
+
`,
|
|
99
|
+
fetchPolicy: 'network-only'
|
|
100
|
+
});
|
|
101
|
+
return unwrap(response, 'figureTypeNames') ?? [];
|
|
102
|
+
}
|
|
103
|
+
export async function createFigure(figure) {
|
|
104
|
+
const response = await client.mutate({
|
|
105
|
+
mutation: gql `
|
|
106
|
+
mutation ($figure: NewFigure!) {
|
|
107
|
+
createFigure(figure: $figure) {
|
|
108
|
+
figure { ${FIGURE_LIST_FIELDS} source }
|
|
109
|
+
violations { code message at }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
`,
|
|
113
|
+
variables: { figure }
|
|
114
|
+
});
|
|
115
|
+
return unwrap(response, 'createFigure');
|
|
116
|
+
}
|
|
117
|
+
export async function updateFigure(id, patch) {
|
|
118
|
+
const response = await client.mutate({
|
|
119
|
+
mutation: gql `
|
|
120
|
+
mutation ($id: String!, $patch: FigurePatch!) {
|
|
121
|
+
updateFigure(id: $id, patch: $patch) {
|
|
122
|
+
figure { ${FIGURE_LIST_FIELDS} source version }
|
|
123
|
+
violations { code message at }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
`,
|
|
127
|
+
variables: { id, patch }
|
|
128
|
+
});
|
|
129
|
+
return unwrap(response, 'updateFigure');
|
|
130
|
+
}
|
|
131
|
+
export async function deleteFigure(id) {
|
|
132
|
+
const response = await client.mutate({
|
|
133
|
+
mutation: gql `
|
|
134
|
+
mutation ($id: String!) {
|
|
135
|
+
deleteFigure(id: $id)
|
|
136
|
+
}
|
|
137
|
+
`,
|
|
138
|
+
variables: { id }
|
|
139
|
+
});
|
|
140
|
+
return unwrap(response, 'deleteFigure');
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 저작 보조에게 후보를 시킨다.
|
|
144
|
+
*
|
|
145
|
+
* 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고
|
|
146
|
+
* 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을
|
|
147
|
+
* 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.
|
|
148
|
+
*
|
|
149
|
+
* 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.
|
|
150
|
+
*/
|
|
151
|
+
export async function proposeFigure(request) {
|
|
152
|
+
const response = await client.mutate({
|
|
153
|
+
mutation: gql `
|
|
154
|
+
mutation ProposeFigure($request: ProposeRequest!) {
|
|
155
|
+
proposeFigure(request: $request) {
|
|
156
|
+
source
|
|
157
|
+
score
|
|
158
|
+
grade
|
|
159
|
+
triangles
|
|
160
|
+
groups
|
|
161
|
+
attempts
|
|
162
|
+
violations {
|
|
163
|
+
code
|
|
164
|
+
message
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
`,
|
|
169
|
+
variables: { request }
|
|
170
|
+
});
|
|
171
|
+
return unwrap(response, 'proposeFigure');
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { client } from '@operato/graphql'\n\nimport type { Figure, FigureFinding, FigureListResult, FigurePatch, FigureProposal, NewFigure } from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n throw new Error(`서버가 ${field} 를 돌려주지 않았습니다.`)\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * `score`·`triangles` 를 함께 받는 이유는 카드에 그것을 보여 주기 때문이다. 저작자가\n * 목록에서 「이 Figure 가 무거운가」를 바로 알아야 한다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n state\n score\n triangles\n groups\n thumbnail\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n search?: string\n category?: string\n state?: string\n}): Promise<FigureListResult> {\n const filters: { name: string; operator: string; value: unknown }[] = []\n if (params.category) filters.push({ name: 'category', operator: 'eq', value: params.category })\n if (params.state) filters.push({ name: 'state', operator: 'eq', value: params.state })\n\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters,\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n version\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source version }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 저작 보조에게 후보를 시킨다.\n *\n * 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고\n * 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을\n * 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.\n *\n * 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.\n */\nexport async function proposeFigure(request: {\n prompt: string\n base?: string\n type?: string\n palette: string[]\n image?: File\n}): Promise<FigureProposal> {\n const response = await client.mutate({\n mutation: gql`\n mutation ProposeFigure($request: ProposeRequest!) {\n proposeFigure(request: $request) {\n source\n score\n grade\n triangles\n groups\n attempts\n violations {\n code\n message\n }\n }\n }\n `,\n variables: { request }\n })\n\n return unwrap<FigureProposal>(response, 'proposeFigure')\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA","sourcesContent":["export * from './types.js'\nexport * from './graphql/index.js'\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js';
|
|
2
|
+
import { LitElement } from 'lit';
|
|
3
|
+
import type { FigureSource } from '@hatiolab/figure-model';
|
|
4
|
+
declare const FigureAsk_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
5
|
+
/**
|
|
6
|
+
* 말로 시키는 줄.
|
|
7
|
+
*
|
|
8
|
+
* ## 무엇을 시키는지가 화면에 보여야 한다
|
|
9
|
+
*
|
|
10
|
+
* 정본이 있으면 **고치는 것**이고, 없으면 **만드는 것**이다. 같은 칸으로 두 가지를
|
|
11
|
+
* 하면 저작자가 「이게 지금 것을 덮어쓰나」를 매번 걱정한다. 그래서 무엇을 할지
|
|
12
|
+
* 칸 옆에 적어 둔다.
|
|
13
|
+
*
|
|
14
|
+
* ## 후보를 여기서 받지 않는다
|
|
15
|
+
*
|
|
16
|
+
* 이 컴포넌트는 시키고 결과를 **알릴 뿐**이다. 받을지 버릴지는 저작면이 정한다 —
|
|
17
|
+
* 정본은 한 벌이고 그 한 벌을 가진 자리가 정해야 한다.
|
|
18
|
+
*
|
|
19
|
+
* ## 팔레트를 함께 보낸다
|
|
20
|
+
*
|
|
21
|
+
* 서버는 팔레트를 모른다. 그리는 쪽이 어떤 토큰을 풀 수 있는지 아는 유일한 자리다.
|
|
22
|
+
*/
|
|
23
|
+
export declare class FigureAsk extends FigureAsk_base {
|
|
24
|
+
static styles: import("lit").CSSResult;
|
|
25
|
+
/** 지금 정본. 있으면 고치는 것이고 없으면 만드는 것이다. */
|
|
26
|
+
source?: FigureSource;
|
|
27
|
+
/** 새로 만들 때 쓸 타입 이름. */
|
|
28
|
+
type: string;
|
|
29
|
+
private asking;
|
|
30
|
+
private failure;
|
|
31
|
+
private picture?;
|
|
32
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
33
|
+
private pick;
|
|
34
|
+
private submit;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@material/web/icon/icon.js';
|
|
3
|
+
import { css, html, LitElement, nothing } from 'lit';
|
|
4
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
5
|
+
import { i18next, localize } from '@operato/i18n';
|
|
6
|
+
import { activePalette } from '@hatiolab/things-scene';
|
|
7
|
+
import { proposeFigure } from '../graphql/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* 말로 시키는 줄.
|
|
10
|
+
*
|
|
11
|
+
* ## 무엇을 시키는지가 화면에 보여야 한다
|
|
12
|
+
*
|
|
13
|
+
* 정본이 있으면 **고치는 것**이고, 없으면 **만드는 것**이다. 같은 칸으로 두 가지를
|
|
14
|
+
* 하면 저작자가 「이게 지금 것을 덮어쓰나」를 매번 걱정한다. 그래서 무엇을 할지
|
|
15
|
+
* 칸 옆에 적어 둔다.
|
|
16
|
+
*
|
|
17
|
+
* ## 후보를 여기서 받지 않는다
|
|
18
|
+
*
|
|
19
|
+
* 이 컴포넌트는 시키고 결과를 **알릴 뿐**이다. 받을지 버릴지는 저작면이 정한다 —
|
|
20
|
+
* 정본은 한 벌이고 그 한 벌을 가진 자리가 정해야 한다.
|
|
21
|
+
*
|
|
22
|
+
* ## 팔레트를 함께 보낸다
|
|
23
|
+
*
|
|
24
|
+
* 서버는 팔레트를 모른다. 그리는 쪽이 어떤 토큰을 풀 수 있는지 아는 유일한 자리다.
|
|
25
|
+
*/
|
|
26
|
+
let FigureAsk = class FigureAsk extends localize(i18next)(LitElement) {
|
|
27
|
+
constructor() {
|
|
28
|
+
super(...arguments);
|
|
29
|
+
/** 새로 만들 때 쓸 타입 이름. */
|
|
30
|
+
this.type = '';
|
|
31
|
+
this.asking = false;
|
|
32
|
+
this.failure = '';
|
|
33
|
+
}
|
|
34
|
+
static { this.styles = css `
|
|
35
|
+
:host {
|
|
36
|
+
display: block;
|
|
37
|
+
background-color: var(--md-sys-color-surface-container-lowest);
|
|
38
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
39
|
+
font: var(--label-font, inherit);
|
|
40
|
+
/*
|
|
41
|
+
이 줄의 글자 크기를 여기서 정한다.
|
|
42
|
+
|
|
43
|
+
안 적어 두면 브라우저 기본 크기(16px)를 물려받는다. 아래 패널들은 전부 12px
|
|
44
|
+
언저리라 위 두 줄만 커 보이고, 단추가 화면 전체와 따로 논다.
|
|
45
|
+
|
|
46
|
+
form 요소(input · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.
|
|
47
|
+
*/
|
|
48
|
+
font-size: 0.76rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
form {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: var(--spacing-medium, 8px);
|
|
55
|
+
padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
md-icon[lead] {
|
|
59
|
+
--md-icon-size: 18px;
|
|
60
|
+
flex: none;
|
|
61
|
+
color: var(--md-sys-color-primary);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
input[ask] {
|
|
65
|
+
flex: 1;
|
|
66
|
+
min-width: 0;
|
|
67
|
+
padding: 6px 10px;
|
|
68
|
+
font: var(--input-field-font, inherit);
|
|
69
|
+
font-size: 0.78rem;
|
|
70
|
+
color: var(--md-sys-color-on-surface);
|
|
71
|
+
background-color: var(--md-sys-color-surface-container-lowest);
|
|
72
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
73
|
+
border-radius: 6px;
|
|
74
|
+
}
|
|
75
|
+
input[ask]:focus {
|
|
76
|
+
outline: 2px solid var(--md-sys-color-primary);
|
|
77
|
+
outline-offset: -1px;
|
|
78
|
+
}
|
|
79
|
+
input[ask][disabled] {
|
|
80
|
+
opacity: 0.5;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* 무엇을 시키는 것인지 — 만드나 고치나 */
|
|
84
|
+
span[mode] {
|
|
85
|
+
flex: none;
|
|
86
|
+
font-size: 0.72rem;
|
|
87
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* 그림 붙이기. 붙인 것이 있으면 이름을 보여 준다 */
|
|
91
|
+
label[picture] {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: 4px;
|
|
95
|
+
flex: none;
|
|
96
|
+
padding: 5px 9px;
|
|
97
|
+
font-size: 0.74rem;
|
|
98
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
99
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
100
|
+
border-radius: 6px;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
}
|
|
103
|
+
label[picture]:hover {
|
|
104
|
+
background-color: var(--md-sys-color-surface-container);
|
|
105
|
+
}
|
|
106
|
+
label[picture][has] {
|
|
107
|
+
color: var(--md-sys-color-on-primary-container);
|
|
108
|
+
background-color: var(--md-sys-color-primary-container);
|
|
109
|
+
border-color: transparent;
|
|
110
|
+
}
|
|
111
|
+
label[picture] md-icon {
|
|
112
|
+
--md-icon-size: 16px;
|
|
113
|
+
}
|
|
114
|
+
label[picture] input {
|
|
115
|
+
display: none;
|
|
116
|
+
}
|
|
117
|
+
span[filename] {
|
|
118
|
+
max-width: 8em;
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
text-overflow: ellipsis;
|
|
121
|
+
white-space: nowrap;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
채운 파랑 단추를 저장과 나눠 갖지 않는다.
|
|
126
|
+
|
|
127
|
+
머리줄의 저장이 이 화면의 주 동작이다. 바로 아래 줄에 같은 파랑 단추가 하나 더
|
|
128
|
+
있으면 어느 쪽이 주인지 눈이 못 고른다. 이쪽은 톤 단추로 한 단계 내린다.
|
|
129
|
+
*/
|
|
130
|
+
button[go] {
|
|
131
|
+
flex: none;
|
|
132
|
+
padding: 6px 14px;
|
|
133
|
+
font: var(--label-font, inherit);
|
|
134
|
+
font-size: 0.76rem;
|
|
135
|
+
font-weight: 600;
|
|
136
|
+
color: var(--md-sys-color-on-primary-container);
|
|
137
|
+
background-color: var(--md-sys-color-primary-container);
|
|
138
|
+
border: none;
|
|
139
|
+
border-radius: 6px;
|
|
140
|
+
cursor: pointer;
|
|
141
|
+
}
|
|
142
|
+
button[go]:hover:not([disabled]) {
|
|
143
|
+
background-color: color-mix(in srgb, var(--md-sys-color-primary) 24%, var(--md-sys-color-primary-container));
|
|
144
|
+
}
|
|
145
|
+
button[go][disabled] {
|
|
146
|
+
opacity: 0.4;
|
|
147
|
+
cursor: default;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* 못 만들었을 때. 사유를 삼키지 않는다 — 말을 고쳐 다시 시킬 단서다 */
|
|
151
|
+
div[failed] {
|
|
152
|
+
display: flex;
|
|
153
|
+
gap: 6px;
|
|
154
|
+
padding: 0 var(--spacing-large, 12px) var(--spacing-medium, 8px) var(--spacing-large, 12px);
|
|
155
|
+
font-size: 0.74rem;
|
|
156
|
+
line-height: 1.5;
|
|
157
|
+
color: var(--md-sys-color-error);
|
|
158
|
+
}
|
|
159
|
+
div[failed] md-icon {
|
|
160
|
+
--md-icon-size: 16px;
|
|
161
|
+
flex: none;
|
|
162
|
+
}
|
|
163
|
+
`; }
|
|
164
|
+
render() {
|
|
165
|
+
const revising = !!this.source;
|
|
166
|
+
return html `
|
|
167
|
+
<form @submit=${(e) => this.submit(e)}>
|
|
168
|
+
<md-icon lead>auto_awesome</md-icon>
|
|
169
|
+
<span mode>${i18next.t(revising ? 'text.ask-to-revise' : 'text.ask-to-create')}</span>
|
|
170
|
+
<input
|
|
171
|
+
ask
|
|
172
|
+
?disabled=${this.asking}
|
|
173
|
+
placeholder=${i18next.t(revising ? 'text.ask-revise-example' : 'text.ask-create-example')}
|
|
174
|
+
/>
|
|
175
|
+
<label picture ?has=${!!this.picture} title=${i18next.t('text.picture-is-for-proportions')}>
|
|
176
|
+
<md-icon>image</md-icon>
|
|
177
|
+
${this.picture
|
|
178
|
+
? html `<span filename>${this.picture.name}</span>`
|
|
179
|
+
: i18next.t('button.attach-a-picture')}
|
|
180
|
+
<input type="file" accept="image/*" @change=${(e) => this.pick(e)} />
|
|
181
|
+
</label>
|
|
182
|
+
<button go ?disabled=${this.asking}>
|
|
183
|
+
${this.asking ? i18next.t('text.asking') : i18next.t('button.ask')}
|
|
184
|
+
</button>
|
|
185
|
+
</form>
|
|
186
|
+
|
|
187
|
+
${this.failure
|
|
188
|
+
? html `
|
|
189
|
+
<div failed>
|
|
190
|
+
<md-icon>error</md-icon>
|
|
191
|
+
<span>${this.failure}</span>
|
|
192
|
+
</div>
|
|
193
|
+
`
|
|
194
|
+
: nothing}
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
pick(e) {
|
|
198
|
+
const input = e.target;
|
|
199
|
+
this.picture = input.files?.[0];
|
|
200
|
+
}
|
|
201
|
+
async submit(e) {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
const input = this.renderRoot.querySelector('input[ask]');
|
|
204
|
+
const prompt = input?.value?.trim();
|
|
205
|
+
if (!prompt || this.asking) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
this.asking = true;
|
|
209
|
+
this.failure = '';
|
|
210
|
+
try {
|
|
211
|
+
const proposal = await proposeFigure({
|
|
212
|
+
prompt,
|
|
213
|
+
base: this.source ? JSON.stringify(this.source) : undefined,
|
|
214
|
+
type: this.source ? undefined : this.type,
|
|
215
|
+
// 그리는 쪽이 풀 수 있는 토큰만 보낸다. 서버는 이 목록 밖의 색을 거절한다.
|
|
216
|
+
palette: Object.keys(activePalette()),
|
|
217
|
+
image: this.picture
|
|
218
|
+
});
|
|
219
|
+
this.dispatchEvent(new CustomEvent('proposed', { detail: { proposal }, bubbles: true, composed: true }));
|
|
220
|
+
input.value = '';
|
|
221
|
+
this.picture = undefined;
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
// 사유를 그대로 보여 준다. 「실패했습니다」로 뭉개면 말을 어떻게 고칠지 모른다.
|
|
225
|
+
this.failure = err.message;
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
this.asking = false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
__decorate([
|
|
233
|
+
property({ type: Object }),
|
|
234
|
+
__metadata("design:type", Object)
|
|
235
|
+
], FigureAsk.prototype, "source", void 0);
|
|
236
|
+
__decorate([
|
|
237
|
+
property({ type: String }),
|
|
238
|
+
__metadata("design:type", Object)
|
|
239
|
+
], FigureAsk.prototype, "type", void 0);
|
|
240
|
+
__decorate([
|
|
241
|
+
state(),
|
|
242
|
+
__metadata("design:type", Object)
|
|
243
|
+
], FigureAsk.prototype, "asking", void 0);
|
|
244
|
+
__decorate([
|
|
245
|
+
state(),
|
|
246
|
+
__metadata("design:type", Object)
|
|
247
|
+
], FigureAsk.prototype, "failure", void 0);
|
|
248
|
+
__decorate([
|
|
249
|
+
state(),
|
|
250
|
+
__metadata("design:type", File)
|
|
251
|
+
], FigureAsk.prototype, "picture", void 0);
|
|
252
|
+
FigureAsk = __decorate([
|
|
253
|
+
customElement('figure-ask')
|
|
254
|
+
], FigureAsk);
|
|
255
|
+
export { FigureAsk };
|
|
256
|
+
//# sourceMappingURL=figure-ask.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figure-ask.js","sourceRoot":"","sources":["../../client/modeller/figure-ask.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport type { FigureSource } from '@hatiolab/figure-model'\nimport { activePalette } from '@hatiolab/things-scene'\n\nimport { proposeFigure } from '../graphql/index.js'\nimport type { FigureProposal } from '../types.js'\n\n/**\n * 말로 시키는 줄.\n *\n * ## 무엇을 시키는지가 화면에 보여야 한다\n *\n * 정본이 있으면 **고치는 것**이고, 없으면 **만드는 것**이다. 같은 칸으로 두 가지를\n * 하면 저작자가 「이게 지금 것을 덮어쓰나」를 매번 걱정한다. 그래서 무엇을 할지\n * 칸 옆에 적어 둔다.\n *\n * ## 후보를 여기서 받지 않는다\n *\n * 이 컴포넌트는 시키고 결과를 **알릴 뿐**이다. 받을지 버릴지는 저작면이 정한다 —\n * 정본은 한 벌이고 그 한 벌을 가진 자리가 정해야 한다.\n *\n * ## 팔레트를 함께 보낸다\n *\n * 서버는 팔레트를 모른다. 그리는 쪽이 어떤 토큰을 풀 수 있는지 아는 유일한 자리다.\n */\n@customElement('figure-ask')\nexport class FigureAsk extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: block;\n background-color: var(--md-sys-color-surface-container-lowest);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n font: var(--label-font, inherit);\n /*\n 이 줄의 글자 크기를 여기서 정한다.\n\n 안 적어 두면 브라우저 기본 크기(16px)를 물려받는다. 아래 패널들은 전부 12px\n 언저리라 위 두 줄만 커 보이고, 단추가 화면 전체와 따로 논다.\n\n form 요소(input · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.\n */\n font-size: 0.76rem;\n }\n\n form {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n }\n\n md-icon[lead] {\n --md-icon-size: 18px;\n flex: none;\n color: var(--md-sys-color-primary);\n }\n\n input[ask] {\n flex: 1;\n min-width: 0;\n padding: 6px 10px;\n font: var(--input-field-font, inherit);\n font-size: 0.78rem;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n }\n input[ask]:focus {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: -1px;\n }\n input[ask][disabled] {\n opacity: 0.5;\n }\n\n /* 무엇을 시키는 것인지 — 만드나 고치나 */\n span[mode] {\n flex: none;\n font-size: 0.72rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 그림 붙이기. 붙인 것이 있으면 이름을 보여 준다 */\n label[picture] {\n display: flex;\n align-items: center;\n gap: 4px;\n flex: none;\n padding: 5px 9px;\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n label[picture]:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n label[picture][has] {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-color: transparent;\n }\n label[picture] md-icon {\n --md-icon-size: 16px;\n }\n label[picture] input {\n display: none;\n }\n span[filename] {\n max-width: 8em;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /*\n 채운 파랑 단추를 저장과 나눠 갖지 않는다.\n\n 머리줄의 저장이 이 화면의 주 동작이다. 바로 아래 줄에 같은 파랑 단추가 하나 더\n 있으면 어느 쪽이 주인지 눈이 못 고른다. 이쪽은 톤 단추로 한 단계 내린다.\n */\n button[go] {\n flex: none;\n padding: 6px 14px;\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n font-weight: 600;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n button[go]:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--md-sys-color-primary) 24%, var(--md-sys-color-primary-container));\n }\n button[go][disabled] {\n opacity: 0.4;\n cursor: default;\n }\n\n /* 못 만들었을 때. 사유를 삼키지 않는다 — 말을 고쳐 다시 시킬 단서다 */\n div[failed] {\n display: flex;\n gap: 6px;\n padding: 0 var(--spacing-large, 12px) var(--spacing-medium, 8px) var(--spacing-large, 12px);\n font-size: 0.74rem;\n line-height: 1.5;\n color: var(--md-sys-color-error);\n }\n div[failed] md-icon {\n --md-icon-size: 16px;\n flex: none;\n }\n `\n\n /** 지금 정본. 있으면 고치는 것이고 없으면 만드는 것이다. */\n @property({ type: Object }) source?: FigureSource\n /** 새로 만들 때 쓸 타입 이름. */\n @property({ type: String }) type = ''\n\n @state() private asking = false\n @state() private failure = ''\n @state() private picture?: File\n\n render() {\n const revising = !!this.source\n\n return html`\n <form @submit=${(e: Event) => this.submit(e)}>\n <md-icon lead>auto_awesome</md-icon>\n <span mode>${i18next.t(revising ? 'text.ask-to-revise' : 'text.ask-to-create')}</span>\n <input\n ask\n ?disabled=${this.asking}\n placeholder=${i18next.t(revising ? 'text.ask-revise-example' : 'text.ask-create-example')}\n />\n <label picture ?has=${!!this.picture} title=${i18next.t('text.picture-is-for-proportions')}>\n <md-icon>image</md-icon>\n ${this.picture\n ? html`<span filename>${this.picture.name}</span>`\n : i18next.t('button.attach-a-picture')}\n <input type=\"file\" accept=\"image/*\" @change=${(e: Event) => this.pick(e)} />\n </label>\n <button go ?disabled=${this.asking}>\n ${this.asking ? i18next.t('text.asking') : i18next.t('button.ask')}\n </button>\n </form>\n\n ${this.failure\n ? html`\n <div failed>\n <md-icon>error</md-icon>\n <span>${this.failure}</span>\n </div>\n `\n : nothing}\n `\n }\n\n private pick(e: Event) {\n const input = e.target as HTMLInputElement\n this.picture = input.files?.[0]\n }\n\n private async submit(e: Event) {\n e.preventDefault()\n\n const input = this.renderRoot.querySelector('input[ask]') as HTMLInputElement\n const prompt = input?.value?.trim()\n if (!prompt || this.asking) {\n return\n }\n\n this.asking = true\n this.failure = ''\n\n try {\n const proposal = await proposeFigure({\n prompt,\n base: this.source ? JSON.stringify(this.source) : undefined,\n type: this.source ? undefined : this.type,\n // 그리는 쪽이 풀 수 있는 토큰만 보낸다. 서버는 이 목록 밖의 색을 거절한다.\n palette: Object.keys(activePalette() as Record<string, string>),\n image: this.picture\n })\n\n this.dispatchEvent(\n new CustomEvent('proposed', { detail: { proposal }, bubbles: true, composed: true })\n )\n input.value = ''\n this.picture = undefined\n } catch (err) {\n // 사유를 그대로 보여 준다. 「실패했습니다」로 뭉개면 말을 어떻게 고칠지 모른다.\n this.failure = (err as Error).message\n } finally {\n this.asking = false\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import type { ViewSettings } from './figure-view.js';
|
|
3
|
+
import type { BoardModel, PartModel } from './figure-source.js';
|
|
4
|
+
declare const FigureCanvas_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
5
|
+
/**
|
|
6
|
+
* 편집 캔버스 — 부품들을 보드 위에 세우고, 여기서 고치게 한다.
|
|
7
|
+
*
|
|
8
|
+
* ## 미리보기가 아니다
|
|
9
|
+
*
|
|
10
|
+
* 한동안 이름이 figure-preview 였다. 원본을 컴파일해 컴포넌트 하나로 세우던 시절에는
|
|
11
|
+
* 정말 보기만 하는 화면이었다. 부품을 컴포넌트로 바꾸면서 편집이 여기서 일어나게 되었다.
|
|
12
|
+
* 부품을 고르고, 2D 로 바꿔 편집하고, 기즈모로 끄는 일이 전부 이 안에서 일어난다.
|
|
13
|
+
* 이름만 남아 있었다.
|
|
14
|
+
*
|
|
15
|
+
* ## 부품이 컴포넌트다
|
|
16
|
+
*
|
|
17
|
+
* 한동안 원본 데이터를 컴파일해 **컴포넌트 하나**로 놓고, 그 안에서 부품을 찾아 다뤘다.
|
|
18
|
+
* 배포용 형태를 편집 화면에 그대로 쓴 것이다. 그래서 어느 부품이 선택됐나 · 무엇을 감쌀까 ·
|
|
19
|
+
* 무엇을 끌까를 전부 따로 뚫어야 했다.
|
|
20
|
+
*
|
|
21
|
+
* 이제 부품 하나가 컴포넌트 하나다. 선택 · 기즈모 · 윤곽선 · 다중 선택 · 되돌리기가
|
|
22
|
+
* 씬의 기본 동작으로 그냥 된다.
|
|
23
|
+
*
|
|
24
|
+
* ## 보드가 곧 기준 상자다
|
|
25
|
+
*
|
|
26
|
+
* 보드의 `width` · `height` · `depth` 가 `FigureSource.base` 다. 부품 좌표를 변환할 때
|
|
27
|
+
* 보드 중심만 빼면 된다 — `figure-source.ts` 가 그 일을 한다.
|
|
28
|
+
*
|
|
29
|
+
* ## 다시 세우지 않는다
|
|
30
|
+
*
|
|
31
|
+
* 부품이 바뀌면 씬을 통째로 다시 세우지 않고 **바뀐 컴포넌트만 고친다.** 다시 세우면
|
|
32
|
+
* 고른 것과 카메라가 날아가서, 숫자 하나 고칠 때마다 화면이 처음으로 돌아간다.
|
|
33
|
+
* 판 크기가 바뀔 때만 다시 세운다 — 판은 씬을 만들 때 정해진다.
|
|
34
|
+
*/
|
|
35
|
+
export declare class FigureCanvas extends FigureCanvas_base {
|
|
36
|
+
static styles: import("lit").CSSResult;
|
|
37
|
+
/**
|
|
38
|
+
* 편집 중인 내용. **씬 모델 그대로 받는다.**
|
|
39
|
+
*
|
|
40
|
+
* 한동안 `FigureSource` 로 받아 여기서 변환했다. 그런데 모델러 페이지가 갖고 있는 것이
|
|
41
|
+
* 이미 씬 모델이므로, 그러면 값을 하나 고칠 때마다 왕복 변환이 한 번씩 일어난다.
|
|
42
|
+
* 무손실이긴 하지만 하는 일이 없다 — 보드가 곧 보드이고 부품이 곧 컴포넌트다.
|
|
43
|
+
*/
|
|
44
|
+
board?: BoardModel;
|
|
45
|
+
/** 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다(`figure-view.ts`). */
|
|
46
|
+
view: ViewSettings;
|
|
47
|
+
parts: PartModel[];
|
|
48
|
+
/** 지금 고른 부품 이름. 목록에서 고른 것을 캔버스에 알린다. */
|
|
49
|
+
picked?: string;
|
|
50
|
+
/**
|
|
51
|
+
* 보기 전용 — 씬에서 일어난 일을 밖에 알리지 않는다.
|
|
52
|
+
*
|
|
53
|
+
* AI 후보를 나란히 세울 때 쓴다. 후보는 아직 편집 대상이 아닌데, 씬은 컴포넌트를
|
|
54
|
+
* 만들면서 상태를 정리하고 그때 변경 이벤트를 낸다. 그 이벤트가 그대로 올라가면
|
|
55
|
+
* **후보의 값이 편집 중인 데이터를 덮어쓴다** — 실제로 그랬다. 받지 않기로 한 부품
|
|
56
|
+
* 위치가 오류보고없이 반영되어 있었고, 후보를 버려도 되돌아오지 않았다.
|
|
57
|
+
*/
|
|
58
|
+
viewOnly: boolean;
|
|
59
|
+
private stage;
|
|
60
|
+
/** 세울 수 없는 사유. 없으면 세운 것이다. */
|
|
61
|
+
private cannot;
|
|
62
|
+
private scene?;
|
|
63
|
+
/** 지금 씬이 서 있는 판. 이것이 달라지면 다시 세운다. */
|
|
64
|
+
private standing?;
|
|
65
|
+
/** 칸 크기를 따라가게 지켜본다. 안 지켜보면 0x0 으로 고정된다. */
|
|
66
|
+
private watcher?;
|
|
67
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
68
|
+
willUpdate(changed: Map<string, unknown>): void;
|
|
69
|
+
firstUpdated(): void;
|
|
70
|
+
disconnectedCallback(): void;
|
|
71
|
+
/**
|
|
72
|
+
* 받은 것을 씬에 반영한다.
|
|
73
|
+
*
|
|
74
|
+
* **판이 그대로면 다시 세우지 않는다.** 다시 세우면 고른 것과 카메라가 날아가서,
|
|
75
|
+
* 숫자 하나 고칠 때마다 화면이 처음으로 돌아간다.
|
|
76
|
+
*/
|
|
77
|
+
private standUp;
|
|
78
|
+
/**
|
|
79
|
+
* 지금 판 위에 있는 부품 컴포넌트를 이름으로 찾는다.
|
|
80
|
+
*
|
|
81
|
+
* **참조를 들고 있지 않는다.** 전에는 세울 때 한 번 색인해 두고 계속 그 Map 을 썼다.
|
|
82
|
+
* 그런데 씬은 우리 모르게 판을 다시 세울 때가 있다 — 2D 와 3D 를 오갈 때가 그렇다.
|
|
83
|
+
* 그러면 Map 안의 것들은 부모가 없는 죽은 객체가 되고, 그 뒤로는
|
|
84
|
+
*
|
|
85
|
+
* - 목록에서 골라도 선택이 안 된다. 루트의 `selected` 게터가 부모 없는 것을
|
|
86
|
+
* 걸러 내므로 빈 배열이 되고, 테두리도 기즈모도 붙지 않는다
|
|
87
|
+
* - 속성을 고쳐도 3D 가 안 따라온다. 죽은 객체에 값을 넣기 때문이다
|
|
88
|
+
*
|
|
89
|
+
* 둘 다 오류보고없이 일어난다. 실제로 화면에서 「기즈모가 안 잡힌다」로 나타났다.
|
|
90
|
+
*
|
|
91
|
+
* 부품은 많아야 20개다. 매번 순회하는 값이 참조를 갖고 있다가 틀리는 값보다 훨씬 싸다.
|
|
92
|
+
*/
|
|
93
|
+
private partsOnBoard;
|
|
94
|
+
/**
|
|
95
|
+
* 보기 설정을 씬이 아는 이름으로 낸다.
|
|
96
|
+
*
|
|
97
|
+
* 옮겨 적는 표를 두지 않는다 — `ViewSettings` 의 이름이 곧 `model-layer` 의 상태
|
|
98
|
+
* 이름이다. 표를 두면 씬이 새 설정을 받아도 이쪽이 모른다.
|
|
99
|
+
*/
|
|
100
|
+
private sceneView;
|
|
101
|
+
/**
|
|
102
|
+
* 설정이 바뀌면 **씬에 그대로 넣는다.** 다시 세우지 않는다.
|
|
103
|
+
*
|
|
104
|
+
* 씬이 이 값들에 스스로 반응한다(`three-capability` 의 `onchange`) — 조명 · 반사 ·
|
|
105
|
+
* 바닥 · 좌표 바탕이 그 자리에서 바뀐다. 다시 세우면 고른 것과 카메라가 날아가서,
|
|
106
|
+
* 밝기 하나 끄는 동안 화면이 계속 처음으로 돌아간다.
|
|
107
|
+
*/
|
|
108
|
+
private applyView;
|
|
109
|
+
/**
|
|
110
|
+
* 부품 목록을 씬에 맞춘다. **다시 세우지 않고 바뀐 것만 고친다.**
|
|
111
|
+
*
|
|
112
|
+
* 다시 세우면 고른 것과 카메라가 날아가서, 숫자 하나 고칠 때마다 화면이 처음으로
|
|
113
|
+
* 돌아간다.
|
|
114
|
+
*/
|
|
115
|
+
private syncParts;
|
|
116
|
+
/**
|
|
117
|
+
* 씬에서 일어난 일을 밖에 알린다.
|
|
118
|
+
*
|
|
119
|
+
* **자리가 아니라 이름으로 알린다.** 목록의 몇 번째인지로 알리면, 알림이 오는 사이에
|
|
120
|
+
* 목록이 바뀌면 엉뚱한 부품을 가리킨다.
|
|
121
|
+
*/
|
|
122
|
+
private watchScene;
|
|
123
|
+
/** 목록에서 고른 것을 캔버스의 선택에 맞춘다. */
|
|
124
|
+
private showPicked;
|
|
125
|
+
private tell;
|
|
126
|
+
private teardown;
|
|
127
|
+
}
|
|
128
|
+
export {};
|