@things-factory/figure-ui 10.1.6 → 10.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/graphql/index.ts +25 -0
- package/client/modeller/figure-animations.ts +769 -0
- package/client/modeller/figure-ask.ts +6 -1
- package/client/modeller/figure-history-panel.ts +52 -1
- package/client/modeller/figure-inspector.ts +520 -120
- package/client/modeller/figure-side.ts +65 -17
- package/client/modeller/proposal-session.ts +92 -0
- package/client/pages/figure-list-page.ts +56 -4
- package/client/pages/figure-modeller-page.ts +204 -179
- package/client/types.ts +39 -0
- package/client/viewparts/figure-thumb.ts +78 -0
- package/dist-client/graphql/index.d.ts +3 -1
- package/dist-client/graphql/index.js +22 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-animations.d.ts +96 -0
- package/dist-client/modeller/figure-animations.js +708 -0
- package/dist-client/modeller/figure-animations.js.map +1 -0
- package/dist-client/modeller/figure-ask.d.ts +3 -0
- package/dist-client/modeller/figure-ask.js +9 -0
- package/dist-client/modeller/figure-ask.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +7 -0
- package/dist-client/modeller/figure-history-panel.js +50 -0
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +17 -0
- package/dist-client/modeller/figure-inspector.js +506 -107
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +23 -0
- package/dist-client/modeller/figure-side.js +68 -16
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/proposal-session.d.ts +23 -0
- package/dist-client/modeller/proposal-session.js +65 -0
- package/dist-client/modeller/proposal-session.js.map +1 -0
- package/dist-client/pages/figure-list-page.d.ts +10 -0
- package/dist-client/pages/figure-list-page.js +55 -4
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +50 -28
- package/dist-client/pages/figure-modeller-page.js +197 -176
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +44 -0
- package/dist-client/types.js.map +1 -1
- package/dist-client/viewparts/figure-thumb.js +74 -0
- package/dist-client/viewparts/figure-thumb.js.map +1 -1
- package/package.json +4 -5
- package/test/ai-proposal-contract.test.ts +169 -0
- package/test/i18n-prefix-guard.test.ts +42 -0
- package/translations/en.json +82 -6
- package/translations/ja.json +34 -1
- package/translations/ko.json +92 -6
- package/translations/ms.json +34 -1
- package/translations/zh.json +34 -1
package/dist-client/types.d.ts
CHANGED
|
@@ -13,7 +13,10 @@ export interface Figure {
|
|
|
13
13
|
type: string;
|
|
14
14
|
name: string;
|
|
15
15
|
description?: string;
|
|
16
|
+
/** 분류 하나 — 팔레트에서 묶어 보이는 기준. */
|
|
16
17
|
category?: string;
|
|
18
|
+
/** 태그 여럿 — 분류로는 못 하는 찾기. */
|
|
19
|
+
tags?: string[];
|
|
17
20
|
state?: 'draft' | 'released';
|
|
18
21
|
/** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */
|
|
19
22
|
score?: number;
|
|
@@ -65,6 +68,7 @@ export interface NewFigure {
|
|
|
65
68
|
name: string;
|
|
66
69
|
description?: string;
|
|
67
70
|
category?: string;
|
|
71
|
+
tags?: string[];
|
|
68
72
|
source: string;
|
|
69
73
|
properties?: string;
|
|
70
74
|
state?: 'draft' | 'released';
|
|
@@ -80,6 +84,8 @@ export interface FigurePatch {
|
|
|
80
84
|
name?: string;
|
|
81
85
|
description?: string;
|
|
82
86
|
category?: string;
|
|
87
|
+
/** 빈 배열은 「전부 지운다」, 안 주는 것은 「건드리지 않는다」. */
|
|
88
|
+
tags?: string[];
|
|
83
89
|
source?: string;
|
|
84
90
|
properties?: string;
|
|
85
91
|
state?: 'draft' | 'released';
|
|
@@ -105,11 +111,36 @@ export interface FigureProposal {
|
|
|
105
111
|
triangles: number;
|
|
106
112
|
/** 재질 묶음 = 인스턴스 하나가 무는 draw call. */
|
|
107
113
|
groups: number;
|
|
114
|
+
/** Rule-based review of measurable design concerns; it is not an aesthetic verdict. */
|
|
115
|
+
quality: {
|
|
116
|
+
status: 'ready' | 'review';
|
|
117
|
+
summary: string;
|
|
118
|
+
findings: Array<{
|
|
119
|
+
dimension: string;
|
|
120
|
+
code: string;
|
|
121
|
+
message: string;
|
|
122
|
+
}>;
|
|
123
|
+
visualRegions: number;
|
|
124
|
+
visualCoverage: number;
|
|
125
|
+
visualReadability: number;
|
|
126
|
+
};
|
|
108
127
|
/** 형식은 맞으나 정책을 넘은 것. 막지 않는다 — 저작자가 정한다. */
|
|
109
128
|
violations: FigureFinding[];
|
|
110
129
|
/** 몇 번 만에 됐나. 진단용이다. */
|
|
111
130
|
attempts: number;
|
|
112
131
|
}
|
|
132
|
+
/** 사람이 후보를 수락하거나 버릴 때 직접 남긴 현재 저작 세션의 참고 신호. */
|
|
133
|
+
export interface ProposalFeedback {
|
|
134
|
+
outcome: 'accepted' | 'discarded';
|
|
135
|
+
selectedChanges: number;
|
|
136
|
+
totalChanges: number;
|
|
137
|
+
note?: string;
|
|
138
|
+
grade?: string;
|
|
139
|
+
quality?: {
|
|
140
|
+
status?: 'ready' | 'review';
|
|
141
|
+
findingCodes?: string[];
|
|
142
|
+
};
|
|
143
|
+
}
|
|
113
144
|
/**
|
|
114
145
|
* 발행 판정 한 줄.
|
|
115
146
|
*
|
|
@@ -117,6 +148,17 @@ export interface FigureProposal {
|
|
|
117
148
|
* 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고
|
|
118
149
|
* 화면이 다시 분류하면 규칙이 두 벌이 된다.
|
|
119
150
|
*/
|
|
151
|
+
/** One reversible anchor edit the sizing gate has measured against this exact source. */
|
|
152
|
+
export interface FigureGateFix {
|
|
153
|
+
findingWay: string;
|
|
154
|
+
part: string;
|
|
155
|
+
axis: 'x' | 'y' | 'z';
|
|
156
|
+
from?: string;
|
|
157
|
+
to: 'min' | 'center' | 'max' | 'span';
|
|
158
|
+
resolved: number;
|
|
159
|
+
/** All blocking sizing cases clear after this exact edit was remeasured server-side. */
|
|
160
|
+
safe: boolean;
|
|
161
|
+
}
|
|
120
162
|
export interface FigureGateFinding {
|
|
121
163
|
code: string;
|
|
122
164
|
message: string;
|
|
@@ -126,6 +168,8 @@ export interface FigureGateFinding {
|
|
|
126
168
|
how?: string;
|
|
127
169
|
/** 갈라진 부품 이름들, 또는 정본 안의 경로. */
|
|
128
170
|
at?: string;
|
|
171
|
+
/** Candidate edits; the UI must preview and explicitly apply these, never auto-save them. */
|
|
172
|
+
fixes?: FigureGateFix[];
|
|
129
173
|
blocking: boolean;
|
|
130
174
|
}
|
|
131
175
|
/** 발행해도 되나. `blocked` 면 서버가 거절한다. */
|
package/dist-client/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../client/types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * 화면이 다루는 모양.\n *\n * 정본 형식(`FigureSource`)의 타입은 **여기서 다시 정의하지 않는다** —\n * `@hatiolab/figure-model` 에서 가져온다. 두 벌이면 갈리고, 갈리면 화면이 서버가\n * 거절할 것을 만들어 낸다.\n */\nexport type { FigureSource, FigureBlueprint, FigurePart, ErrorCode, ViolationCode } from '@hatiolab/figure-model'\n\n/** 서버가 돌려주는 Figure 한 벌. `source` 는 저작면에서만 받는다. */\nexport interface Figure {\n id: string\n /** 컴포넌트 타입 이름. **만든 뒤에 고칠 수 없다.** */\n type: string\n name: string\n description?: string\n category?: string\n state?: 'draft' | 'released'\n /** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */\n score?: number\n triangles?: number\n groups?: number\n thumbnail?: string\n thumbnailUpdatedAt?: string\n updatedAt?: string\n updater?: { id: string; name: string }\n /** `FigureSource` JSON 문자열. 목록 질의에는 실려 오지 않는다. */\n source?: string\n properties?: string\n version?: number\n}\n\n/**\n * 발행된 판 하나.\n *\n * 저작 중 저장은 여기 오지 않는다 — 판본은 발행에서만 생긴다.\n */\nexport interface FigureVersion {\n version: number\n /** 발행하며 남긴 말. */\n comment?: string\n state?: 'draft' | 'released'\n updatedAt?: string\n updater?: { id: string; name: string }\n score?: number\n triangles?: number\n groups?: number\n}\n\nexport interface FigureListResult {\n items: Figure[]\n total: number\n}\n\n/**\n * 새 Figure.\n *\n * `score`·`triangles`·`groups` 가 없다 — **서버가 정본에서 계산한다.** 화면이 보내면\n * 거짓이 들어오고 목록이 실물과 다른 것을 보여 준다.\n */\nexport interface NewFigure {\n type: string\n name: string\n description?: string\n category?: string\n source: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/**\n * 수정.\n *\n * `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된 보드가 그 이름으로 타입을\n * 찾으므로, 바꾸면 그 보드가 빈 화면이 된다.\n */\nexport interface FigurePatch {\n name?: string\n description?: string\n category?: string\n source?: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/** 검증 결과 한 줄. 코드는 figure-model 의 것을 그대로 쓴다. */\nexport interface FigureFinding {\n code: string\n message: string\n at?: string\n}\n\n/**\n * 저작 보조가 낸 후보.\n *\n * **정본이 아니다.** 저작자가 보고 받아야 정본이 된다 — 서버는 저장하지 않고\n * 돌려주기만 한다.\n */\nexport interface FigureProposal {\n /** 후보 정본. JSON 문자열이다 — 화면이 파싱한다. */\n source: string\n score: number\n grade: string\n triangles: number\n /** 재질 묶음 = 인스턴스 하나가 무는 draw call. */\n groups: number\n /** 형식은 맞으나 정책을 넘은 것. 막지 않는다 — 저작자가 정한다. */\n violations: FigureFinding[]\n /** 몇 번 만에 됐나. 진단용이다. */\n attempts: number\n}\n\n/**\n * 발행 판정 한 줄.\n *\n * `FigureFinding` 과 달리 **`blocking` 이 있다.** 저장 쪽 발견은 정의상 막지 않는 것들이고,\n * 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고\n * 화면이 다시 분류하면 규칙이 두 벌이 된다.\n */\nexport interface FigureGateFinding {\n code: string\n message: string\n /** 왜 그것이 문제인가 — 사람이 무엇을 보게 되나. */\n why?: string\n /** 무엇을 바꾸면 되나 — 부품 이름과 값으로. */\n how?: string\n /** 갈라진 부품 이름들, 또는 정본 안의 경로. */\n at?: string\n blocking: boolean\n}\n\n/** 발행해도 되나. `blocked` 면 서버가 거절한다. */\nexport interface FigureInspection {\n blocked: boolean\n findings: FigureGateFinding[]\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../client/types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * 화면이 다루는 모양.\n *\n * 정본 형식(`FigureSource`)의 타입은 **여기서 다시 정의하지 않는다** —\n * `@hatiolab/figure-model` 에서 가져온다. 두 벌이면 갈리고, 갈리면 화면이 서버가\n * 거절할 것을 만들어 낸다.\n */\nexport type { FigureSource, FigureBlueprint, FigurePart, ErrorCode, ViolationCode } from '@hatiolab/figure-model'\n\n/** 서버가 돌려주는 Figure 한 벌. `source` 는 저작면에서만 받는다. */\nexport interface Figure {\n id: string\n /** 컴포넌트 타입 이름. **만든 뒤에 고칠 수 없다.** */\n type: string\n name: string\n description?: string\n /** 분류 하나 — 팔레트에서 묶어 보이는 기준. */\n category?: string\n /** 태그 여럿 — 분류로는 못 하는 찾기. */\n tags?: string[]\n state?: 'draft' | 'released'\n /** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */\n score?: number\n triangles?: number\n groups?: number\n thumbnail?: string\n thumbnailUpdatedAt?: string\n updatedAt?: string\n updater?: { id: string; name: string }\n /** `FigureSource` JSON 문자열. 목록 질의에는 실려 오지 않는다. */\n source?: string\n properties?: string\n version?: number\n}\n\n/**\n * 발행된 판 하나.\n *\n * 저작 중 저장은 여기 오지 않는다 — 판본은 발행에서만 생긴다.\n */\nexport interface FigureVersion {\n version: number\n /** 발행하며 남긴 말. */\n comment?: string\n state?: 'draft' | 'released'\n updatedAt?: string\n updater?: { id: string; name: string }\n score?: number\n triangles?: number\n groups?: number\n}\n\nexport interface FigureListResult {\n items: Figure[]\n total: number\n}\n\n/**\n * 새 Figure.\n *\n * `score`·`triangles`·`groups` 가 없다 — **서버가 정본에서 계산한다.** 화면이 보내면\n * 거짓이 들어오고 목록이 실물과 다른 것을 보여 준다.\n */\nexport interface NewFigure {\n type: string\n name: string\n description?: string\n category?: string\n tags?: string[]\n source: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/**\n * 수정.\n *\n * `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된 보드가 그 이름으로 타입을\n * 찾으므로, 바꾸면 그 보드가 빈 화면이 된다.\n */\nexport interface FigurePatch {\n name?: string\n description?: string\n category?: string\n /** 빈 배열은 「전부 지운다」, 안 주는 것은 「건드리지 않는다」. */\n tags?: string[]\n source?: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/** 검증 결과 한 줄. 코드는 figure-model 의 것을 그대로 쓴다. */\nexport interface FigureFinding {\n code: string\n message: string\n at?: string\n}\n\n/**\n * 저작 보조가 낸 후보.\n *\n * **정본이 아니다.** 저작자가 보고 받아야 정본이 된다 — 서버는 저장하지 않고\n * 돌려주기만 한다.\n */\nexport interface FigureProposal {\n /** 후보 정본. JSON 문자열이다 — 화면이 파싱한다. */\n source: string\n score: number\n grade: string\n triangles: number\n /** 재질 묶음 = 인스턴스 하나가 무는 draw call. */\n groups: number\n /** Rule-based review of measurable design concerns; it is not an aesthetic verdict. */\n quality: {\n status: 'ready' | 'review'\n summary: string\n findings: Array<{ dimension: string; code: string; message: string }>\n visualRegions: number\n visualCoverage: number\n visualReadability: number\n }\n /** 형식은 맞으나 정책을 넘은 것. 막지 않는다 — 저작자가 정한다. */\n violations: FigureFinding[]\n /** 몇 번 만에 됐나. 진단용이다. */\n attempts: number\n}\n\n/** 사람이 후보를 수락하거나 버릴 때 직접 남긴 현재 저작 세션의 참고 신호. */\nexport interface ProposalFeedback {\n outcome: 'accepted' | 'discarded'\n selectedChanges: number\n totalChanges: number\n note?: string\n grade?: string\n quality?: { status?: 'ready' | 'review'; findingCodes?: string[] }\n}\n\n/**\n * 발행 판정 한 줄.\n *\n * `FigureFinding` 과 달리 **`blocking` 이 있다.** 저장 쪽 발견은 정의상 막지 않는 것들이고,\n * 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고\n * 화면이 다시 분류하면 규칙이 두 벌이 된다.\n */\n/** One reversible anchor edit the sizing gate has measured against this exact source. */\nexport interface FigureGateFix {\n findingWay: string\n part: string\n axis: 'x' | 'y' | 'z'\n from?: string\n to: 'min' | 'center' | 'max' | 'span'\n resolved: number\n /** All blocking sizing cases clear after this exact edit was remeasured server-side. */\n safe: boolean\n}\n\nexport interface FigureGateFinding {\n code: string\n message: string\n /** 왜 그것이 문제인가 — 사람이 무엇을 보게 되나. */\n why?: string\n /** 무엇을 바꾸면 되나 — 부품 이름과 값으로. */\n how?: string\n /** 갈라진 부품 이름들, 또는 정본 안의 경로. */\n at?: string\n /** Candidate edits; the UI must preview and explicitly apply these, never auto-save them. */\n fixes?: FigureGateFix[]\n blocking: boolean\n}\n\n/** 발행해도 되나. `blocked` 면 서버가 거절한다. */\nexport interface FigureInspection {\n blocked: boolean\n findings: FigureGateFinding[]\n}\n"]}
|
|
@@ -97,6 +97,65 @@ let FigureThumb = class FigureThumb extends localize(i18next)(LitElement) {
|
|
|
97
97
|
border-radius: 4px;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/*
|
|
101
|
+
태그 — **그림 위에 얹는다.**
|
|
102
|
+
|
|
103
|
+
카드 본문에 한 줄로 두었더니 태그가 있는 카드만 그만큼 높아져 격자가 들쭉날쭉해졌다.
|
|
104
|
+
태그는 도형마다 개수가 다르므로 본문에 두는 한 이 문제가 남는다. 그림 위는 이미 자리가
|
|
105
|
+
잡혀 있고(초안 표시·삭제), 얹어도 카드 높이가 변하지 않는다.
|
|
106
|
+
|
|
107
|
+
아래쪽에 둔다 — 위는 초안 표시와 삭제 단추가 쓴다.
|
|
108
|
+
|
|
109
|
+
**투명한 알약이다.** 면을 꽉 채우면 그림 위에 흰 띠가 생겨 형상을 가린다 — 카탈로그에서
|
|
110
|
+
먼저 보아야 하는 것은 형상이다. 그래서 바탕은 거의 비우고, 읽히는 것은 뒤를 흐리는 것과
|
|
111
|
+
실낱 테두리가 맡는다. 흐림이 안 되는 환경에서도 테두리가 남아 글자가 형상에 묻히지 않는다.
|
|
112
|
+
*/
|
|
113
|
+
div[tags] {
|
|
114
|
+
position: absolute;
|
|
115
|
+
right: 6px;
|
|
116
|
+
bottom: 6px;
|
|
117
|
+
left: 6px;
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-wrap: wrap-reverse;
|
|
120
|
+
justify-content: flex-end;
|
|
121
|
+
gap: 3px;
|
|
122
|
+
/* 태그가 많아도 그림을 덮지 않는다 — 넘치는 것은 위로 잘린다 */
|
|
123
|
+
max-height: 44px;
|
|
124
|
+
overflow: hidden;
|
|
125
|
+
/* 알약 사이의 빈 자리는 그림이 받는다 — 거기서 누르면 저작면이 열린다 */
|
|
126
|
+
pointer-events: none;
|
|
127
|
+
}
|
|
128
|
+
/*
|
|
129
|
+
**알약은 누를 수 있다.** 누르면 그 태그로 목록을 거른다.
|
|
130
|
+
|
|
131
|
+
태그를 눈에 보이게 한 다음의 물음은 「그래서 이걸로 뭘 하나」다. 같은 태그가 붙은 것을
|
|
132
|
+
보려고 태그를 눈으로 읽어 검색창에 옮겨 적게 두면, 보여 준 뜻이 절반만 산다.
|
|
133
|
+
|
|
134
|
+
카드 자체가 저작면을 여는 자리이므로 알약에서 그 클릭을 멈춘다 — 안 멈추면 태그를 누른
|
|
135
|
+
사람이 편집기로 끌려간다.
|
|
136
|
+
*/
|
|
137
|
+
div[tags] button {
|
|
138
|
+
pointer-events: auto;
|
|
139
|
+
cursor: pointer;
|
|
140
|
+
padding: 0 7px;
|
|
141
|
+
font: var(--label-font, inherit);
|
|
142
|
+
font-size: 0.66rem;
|
|
143
|
+
line-height: 1.7;
|
|
144
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
145
|
+
background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 34%, transparent);
|
|
146
|
+
border: 1px solid color-mix(in srgb, var(--md-sys-color-outline, #888) 42%, transparent);
|
|
147
|
+
backdrop-filter: blur(4px);
|
|
148
|
+
border-radius: 999em;
|
|
149
|
+
white-space: nowrap;
|
|
150
|
+
}
|
|
151
|
+
div[tags] button:hover,
|
|
152
|
+
div[tags] button:focus-visible {
|
|
153
|
+
color: var(--md-sys-color-on-surface);
|
|
154
|
+
background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 72%, transparent);
|
|
155
|
+
border-color: var(--md-sys-color-primary);
|
|
156
|
+
outline: none;
|
|
157
|
+
}
|
|
158
|
+
|
|
100
159
|
button[remove] {
|
|
101
160
|
position: absolute;
|
|
102
161
|
top: 5px;
|
|
@@ -142,6 +201,21 @@ let FigureThumb = class FigureThumb extends localize(i18next)(LitElement) {
|
|
|
142
201
|
</div>
|
|
143
202
|
`}
|
|
144
203
|
${figure.state === 'released' ? '' : html `<span draft>${i18next.t('figure.label.draft')}</span>`}
|
|
204
|
+
${figure.tags?.length
|
|
205
|
+
? html `<div tags>
|
|
206
|
+
${figure.tags.map(tag => html `
|
|
207
|
+
<button
|
|
208
|
+
title=${i18next.t('figure.text.find-by-this-tag')}
|
|
209
|
+
@click=${(e) => {
|
|
210
|
+
e.stopPropagation();
|
|
211
|
+
this.dispatchEvent(new CustomEvent('search-tag', { detail: { tag }, bubbles: true, composed: true }));
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
${tag}
|
|
215
|
+
</button>
|
|
216
|
+
`)}
|
|
217
|
+
</div>`
|
|
218
|
+
: ''}
|
|
145
219
|
|
|
146
220
|
<button
|
|
147
221
|
remove
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"figure-thumb.js","sourceRoot":"","sources":["../../client/viewparts/figure-thumb.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AAEnC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAI5C;;;;;;;;;GASG;AACH,SAAS,YAAY,CAAC,MAAc;IAClC,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAC;QAC/B,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,EAAE,WAAW,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;IACtD,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,OAAO,EAAE,CAAA;IAE3D,OAAO,GAAG,WAAW,qBAAqB,MAAM,CAAC,EAAE,MAAM,KAAK,EAAE,CAAA;AAClE,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AAEI,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;aACrD,WAAM,GAAG,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"figure-thumb.js","sourceRoot":"","sources":["../../client/viewparts/figure-thumb.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { getPathInfo } from '@operato/utils'\n\nimport type { Figure } from '../types.js'\n\n/**\n * 그림을 부르는 주소.\n *\n * **base64 를 목록 응답에 실어 오지 않는다.** data URL 은 브라우저가 캐시할 수 없어, 카탈로그를\n * 열 때마다 그림 전부가 다시 온다([확인함] 표본 14개 227KB). 주소로 부르면 캐시가 되고, 판을\n * 가리는 `v` 가 바뀐 것만 다시 받는다. 내주는 자리는 `figure-thumbnail-router` 다.\n *\n * 도메인 조각(`/domain/{subdomain}`)은 셸이 쓰는 것과 같은 함수로 읽는다 — 화면이 주소를\n * 손으로 짜맞추면 도메인이 바뀌는 배치에서 조용히 틀린다.\n */\nfunction thumbnailSrc(figure: Figure): string | undefined {\n if (!figure.thumbnailUpdatedAt) {\n return undefined\n }\n\n const { contextPath } = getPathInfo(location.pathname)\n const stamp = new Date(figure.thumbnailUpdatedAt).getTime()\n\n return `${contextPath}/figure-thumbnail/${figure.id}?v=${stamp}`\n}\n\n/**\n * 카드의 그림 칸.\n *\n * ## 왜 컴포넌트인가\n *\n * 카탈로그는 `ox-grist` 의 CARD 모드로 그린다. 격자에게 그림 칸은 컬럼 하나이고, 그 칸을 그리는\n * 것은 컬럼의 `record.renderer` 다. 그림 위에 얹히는 것들(초안 표시, 삭제)은 **그림 칸 안에**\n * 있어야 하므로 renderer 가 그려야 하는데, renderer 의 결과는 `ox-card-field` 의 shadow root\n * 안으로 들어간다 — 화면의 `static styles` 가 닿지 않고, 인라인 style 로는 `:hover` 를 쓸 수 없다.\n *\n * 그래서 그림 칸만 컴포넌트로 둔다. 자기 스타일을 갖고 있으니 hover 도 테마도 정상으로 돈다.\n * 격자로 옮기기 전 손으로 만들었던 카드의 위쪽 절반이 그대로 이 자리다.\n *\n * ## 삭제가 왜 여기 있나\n *\n * 격자의 gutter 줄에 두면 카드 아래에 단추 줄이 하나 생긴다. 감춰도 자리가 남고, 자리를 없애면\n * 눌릴 곳이 사라진다. 되돌릴 수 없는 조작은 **고르는 눈길에 걸리지 않아야** 하므로 그림 오른쪽\n * 위, 손이 갔을 때만 나오는 자리에 둔다.\n */\n@customElement('figure-thumb')\nexport class FigureThumb extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n position: relative;\n display: block;\n width: 100%;\n height: 100%;\n overflow: hidden;\n }\n\n img {\n display: block;\n width: 100%;\n height: 100%;\n /* 형상을 자르지 않는다 — 긴 것(컨베이어)과 높은 것(시그널타워)이 한 줄에 선다 */\n object-fit: contain;\n }\n\n /* 아직 그림이 없는 것을 빈 사각형으로 두지 않는다 — 왜 없는지 말한다 */\n div[empty] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-tiny, 4px);\n width: 100%;\n height: 100%;\n font: var(--label-font, inherit);\n font-size: 0.7rem;\n color: var(--md-sys-color-outline);\n }\n div[empty] md-icon {\n --md-icon-size: 26px;\n color: var(--md-sys-color-outline-variant);\n }\n\n /*\n 초안 표시.\n\n 발행된 것에는 아무 말도 하지 않는다 — 카탈로그에 있는 것은 대개 발행된 것이라, 카드마다\n 「발행됨」이 붙으면 아무것도 가르지 않고 자리만 먹는다. 초안만 말하면 이 표시가 있는 카드가\n 곧 「아직 쓰면 안 되는 것」이 된다.\n */\n span[draft] {\n position: absolute;\n top: 6px;\n left: 6px;\n padding: 1px 7px;\n font: var(--label-font, inherit);\n font-size: 0.68rem;\n line-height: 1.5;\n color: var(--md-sys-color-on-secondary-container);\n background-color: var(--md-sys-color-secondary-container);\n border-radius: 4px;\n }\n\n /*\n 태그 — **그림 위에 얹는다.**\n\n 카드 본문에 한 줄로 두었더니 태그가 있는 카드만 그만큼 높아져 격자가 들쭉날쭉해졌다.\n 태그는 도형마다 개수가 다르므로 본문에 두는 한 이 문제가 남는다. 그림 위는 이미 자리가\n 잡혀 있고(초안 표시·삭제), 얹어도 카드 높이가 변하지 않는다.\n\n 아래쪽에 둔다 — 위는 초안 표시와 삭제 단추가 쓴다.\n\n **투명한 알약이다.** 면을 꽉 채우면 그림 위에 흰 띠가 생겨 형상을 가린다 — 카탈로그에서\n 먼저 보아야 하는 것은 형상이다. 그래서 바탕은 거의 비우고, 읽히는 것은 뒤를 흐리는 것과\n 실낱 테두리가 맡는다. 흐림이 안 되는 환경에서도 테두리가 남아 글자가 형상에 묻히지 않는다.\n */\n div[tags] {\n position: absolute;\n right: 6px;\n bottom: 6px;\n left: 6px;\n display: flex;\n flex-wrap: wrap-reverse;\n justify-content: flex-end;\n gap: 3px;\n /* 태그가 많아도 그림을 덮지 않는다 — 넘치는 것은 위로 잘린다 */\n max-height: 44px;\n overflow: hidden;\n /* 알약 사이의 빈 자리는 그림이 받는다 — 거기서 누르면 저작면이 열린다 */\n pointer-events: none;\n }\n /*\n **알약은 누를 수 있다.** 누르면 그 태그로 목록을 거른다.\n\n 태그를 눈에 보이게 한 다음의 물음은 「그래서 이걸로 뭘 하나」다. 같은 태그가 붙은 것을\n 보려고 태그를 눈으로 읽어 검색창에 옮겨 적게 두면, 보여 준 뜻이 절반만 산다.\n\n 카드 자체가 저작면을 여는 자리이므로 알약에서 그 클릭을 멈춘다 — 안 멈추면 태그를 누른\n 사람이 편집기로 끌려간다.\n */\n div[tags] button {\n pointer-events: auto;\n cursor: pointer;\n padding: 0 7px;\n font: var(--label-font, inherit);\n font-size: 0.66rem;\n line-height: 1.7;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 34%, transparent);\n border: 1px solid color-mix(in srgb, var(--md-sys-color-outline, #888) 42%, transparent);\n backdrop-filter: blur(4px);\n border-radius: 999em;\n white-space: nowrap;\n }\n div[tags] button:hover,\n div[tags] button:focus-visible {\n color: var(--md-sys-color-on-surface);\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 72%, transparent);\n border-color: var(--md-sys-color-primary);\n outline: none;\n }\n\n button[remove] {\n position: absolute;\n top: 5px;\n right: 5px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n /* 그림이 밝을 수도 어두울 수도 있다 — 옅은 면을 깔아 아이콘이 어디서나 읽히게 한다 */\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 82%, transparent);\n border: none;\n border-radius: 4px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s ease-out;\n }\n button[remove] md-icon {\n --md-icon-size: 18px;\n }\n :host(:hover) button[remove],\n button[remove]:focus-visible {\n opacity: 1;\n }\n button[remove]:hover {\n color: var(--md-sys-color-on-error, #fff);\n background-color: var(--md-sys-color-error);\n }\n `\n\n @property({ type: Object }) figure!: Figure\n\n render() {\n const figure = this.figure\n if (!figure) {\n return html``\n }\n\n const src = thumbnailSrc(figure)\n\n return html`\n ${src\n ? html`<img src=${src} alt=${figure.name || ''} loading=\"lazy\" />`\n : html`\n <div empty>\n <md-icon>deployed_code</md-icon>\n ${i18next.t('figure.text.thumbnail-not-made-yet')}\n </div>\n `}\n ${figure.state === 'released' ? '' : html`<span draft>${i18next.t('figure.label.draft')}</span>`}\n ${figure.tags?.length\n ? html`<div tags>\n ${figure.tags.map(\n tag => html`\n <button\n title=${i18next.t('figure.text.find-by-this-tag')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.dispatchEvent(\n new CustomEvent('search-tag', { detail: { tag }, bubbles: true, composed: true })\n )\n }}\n >\n ${tag}\n </button>\n `\n )}\n </div>`\n : ''}\n\n <button\n remove\n title=${i18next.t('figure.button.delete-figure')}\n @click=${(e: Event) => {\n /* 카드 클릭(=저작면 열기)까지 올라가지 않게 여기서 멈춘다 */\n e.stopPropagation()\n this.dispatchEvent(new CustomEvent('remove'))\n }}\n >\n <md-icon>delete</md-icon>\n </button>\n `\n }\n}\n"]}
|
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.8",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clean": "npm run clean:server && npm run clean:client"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@hatiolab/figure-model": "^0.1.
|
|
29
|
+
"@hatiolab/figure-model": "^0.1.16",
|
|
30
30
|
"@hatiolab/things-scene": "^10.1.10",
|
|
31
31
|
"@material/web": "^2.0.0",
|
|
32
32
|
"@operato/data-grist": "^10.0.0",
|
|
@@ -37,9 +37,8 @@
|
|
|
37
37
|
"@operato/shell": "^10.0.0",
|
|
38
38
|
"@operato/styles": "^10.0.0",
|
|
39
39
|
"@operato/utils": "^10.0.0",
|
|
40
|
-
"@things-factory/
|
|
41
|
-
"@things-factory/figure-service": "^10.1.6",
|
|
40
|
+
"@things-factory/figure-service": "^10.1.8",
|
|
42
41
|
"three": "^0.185.1"
|
|
43
42
|
},
|
|
44
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "c402982e29aab3b49e56bea4e41948be6a749c7f"
|
|
45
44
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* AI 후보가 저작 화면의 정본에 닿는 마지막 경계다.
|
|
3
|
+
*
|
|
4
|
+
* 모델의 응답은 신뢰하지 않는다. `proposal.ts`는 사람이 고른 변경만 옮기고, 저장 식별자인
|
|
5
|
+
* Figure type과 이름으로 연결된 부품의 의미를 보존해야 한다. 이 순수 로직을 Lit 화면 밖에서
|
|
6
|
+
* 시험해 두면 DOM 배선이 바뀌어도 후보 수락 규칙이 조용히 약해지지 않는다.
|
|
7
|
+
*/
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
import { readFileSync } from 'node:fs'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
import { test } from 'node:test'
|
|
12
|
+
|
|
13
|
+
import { applyProposal, diffProposal, takeProposal } from '../client/modeller/proposal.ts'
|
|
14
|
+
import { feedbackFromSession, openAiProposal, openSizingFix, toggleProposalChange } from '../client/modeller/proposal-session.ts'
|
|
15
|
+
|
|
16
|
+
const current = () => ({
|
|
17
|
+
type: 'MIXER',
|
|
18
|
+
base: { x: 100, y: 100, z: 100 },
|
|
19
|
+
detailLevel: 'M',
|
|
20
|
+
parts: [
|
|
21
|
+
{
|
|
22
|
+
name: 'body',
|
|
23
|
+
primitive: 'cube',
|
|
24
|
+
transform: { position: { x: 0, y: 0, z: 0 }, size: { x: 100, y: 100, z: 100 } },
|
|
25
|
+
material: { token: 'palette.primary' }
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'motor',
|
|
29
|
+
primitive: 'cube',
|
|
30
|
+
transform: { position: { x: 0, y: 60, z: 0 }, size: { x: 20, y: 20, z: 20 } },
|
|
31
|
+
material: { token: 'palette.neutral' }
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const proposal = () => ({
|
|
37
|
+
type: 'FORKLIFT', // 모델이 이 값을 지어내도 이미 저장된 Figure의 type은 바꾸지 않는다.
|
|
38
|
+
base: { x: 200, y: 100, z: 100 },
|
|
39
|
+
detailLevel: 'L',
|
|
40
|
+
parts: [
|
|
41
|
+
{
|
|
42
|
+
name: 'body',
|
|
43
|
+
primitive: 'cube',
|
|
44
|
+
transform: { position: { x: 0, y: 0, z: 0 }, size: { x: 200, y: 100, z: 100 } },
|
|
45
|
+
material: { token: 'palette.accent' }
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'guard',
|
|
49
|
+
primitive: 'cube',
|
|
50
|
+
transform: { position: { x: 0, y: 50, z: 0 }, size: { x: 100, y: 10, z: 10 } },
|
|
51
|
+
material: { token: 'palette.primary' }
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('AI 후보는 모든 변경을 이름 단위로 드러낸다 — 사람에게 숨은 변경이 없다', () => {
|
|
57
|
+
const changes = diffProposal(current() as any, proposal() as any)
|
|
58
|
+
|
|
59
|
+
assert.deepEqual(
|
|
60
|
+
changes.map(change => `${change.kind}:${change.key}`).sort(),
|
|
61
|
+
['added:guard', 'base:base', 'changed:body', 'detail:detailLevel', 'removed:motor']
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('선택하지 않은 후보 변경은 정본에 들어가지 않는다', () => {
|
|
66
|
+
const result = applyProposal(current() as any, proposal() as any, new Set(['body', 'base']))
|
|
67
|
+
|
|
68
|
+
assert.deepEqual(result.base, { x: 200, y: 100, z: 100 })
|
|
69
|
+
assert.equal(result.detailLevel, 'M')
|
|
70
|
+
assert.deepEqual(result.parts.map(part => part.name), ['body', 'motor'])
|
|
71
|
+
assert.equal(result.parts[0].material.token, 'palette.accent')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('후보가 다른 type을 내도 이미 존재하는 Figure의 저장 식별자는 보존한다', () => {
|
|
75
|
+
const result = takeProposal(current() as any, proposal() as any)
|
|
76
|
+
|
|
77
|
+
assert.equal(result.type, 'MIXER')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('새 Figure의 후보는 사람의 수락 전까지 입력 source를 바꾸지 않는다', () => {
|
|
81
|
+
const candidate = proposal()
|
|
82
|
+
const result = takeProposal(undefined, candidate as any)
|
|
83
|
+
|
|
84
|
+
assert.notEqual(result, candidate)
|
|
85
|
+
assert.equal(candidate.type, 'FORKLIFT')
|
|
86
|
+
assert.equal(result.type, 'FORKLIFT')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('AI 후보의 검토 상태는 하나의 세션으로 열리고, 선택은 불변으로 바뀐다', () => {
|
|
90
|
+
const candidate = openAiProposal(current() as any, {
|
|
91
|
+
source: JSON.stringify(proposal()),
|
|
92
|
+
grade: 'L',
|
|
93
|
+
triangles: 42,
|
|
94
|
+
groups: 2,
|
|
95
|
+
attempts: 1,
|
|
96
|
+
quality: { status: 'ready', summary: 'ready', findings: [], visualRegions: 1, visualCoverage: 1, visualReadability: 1 }
|
|
97
|
+
} as any)
|
|
98
|
+
|
|
99
|
+
const toggled = toggleProposalChange(candidate, 'body')
|
|
100
|
+
assert.notEqual(toggled, candidate)
|
|
101
|
+
assert.equal(candidate.picked.has('body'), true)
|
|
102
|
+
assert.equal(toggled.picked.has('body'), false)
|
|
103
|
+
assert.equal(toggled.note, '')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('발행 게이트 수정과 AI 후보는 같은 검토 세션 경계를 쓴다', () => {
|
|
107
|
+
const session = openSizingFix(current() as any, { part: 'body', axis: 'x', from: 'auto', to: 'max' } as any)
|
|
108
|
+
assert.equal(session?.metrics, undefined)
|
|
109
|
+
assert.equal(session?.source.parts[0].anchor?.x, 'max')
|
|
110
|
+
assert.equal(openSizingFix(current() as any, { part: 'missing', axis: 'x', to: 'max' } as any), undefined)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('피드백은 후보 세션의 선택과 측정값만 전송한다', () => {
|
|
114
|
+
const session = openAiProposal(current() as any, {
|
|
115
|
+
source: JSON.stringify(proposal()),
|
|
116
|
+
grade: 'L',
|
|
117
|
+
triangles: 42,
|
|
118
|
+
groups: 2,
|
|
119
|
+
attempts: 1,
|
|
120
|
+
quality: { status: 'review', summary: 'check', findings: [{ dimension: 'shape', code: 'S1', message: 'check' }], visualRegions: 1, visualCoverage: 1, visualReadability: 1 }
|
|
121
|
+
} as any)
|
|
122
|
+
const feedback = feedbackFromSession(current() as any, { ...session, note: ' keep body ' }, 'accepted')
|
|
123
|
+
assert.deepEqual(feedback, {
|
|
124
|
+
outcome: 'accepted', selectedChanges: session.picked.size, totalChanges: session.picked.size,
|
|
125
|
+
note: 'keep body', grade: 'L', quality: { status: 'review', findingCodes: ['S1'] }
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('모델러 AI 입구는 후보만 알리고, 팔레트·그림·현재 정본을 서버에 함께 보낸다', () => {
|
|
130
|
+
/* Lit 컴포넌트는 DOM 없이 실로드하지 않는다. 이 경계에서는 실제 소스 계약을 읽는다. */
|
|
131
|
+
const path = fileURLToPath(new URL('../client/modeller/figure-ask.ts', import.meta.url))
|
|
132
|
+
const source = readFileSync(path, 'utf8')
|
|
133
|
+
|
|
134
|
+
assert.match(source, /proposeFigure\(\{[\s\S]*prompt,[\s\S]*base: this\.source \? JSON\.stringify\(this\.source\) : undefined,[\s\S]*type: this\.source \? undefined : this\.type,[\s\S]*palette: Object\.keys\(activePalette\(\)/)
|
|
135
|
+
assert.match(source, /image: this\.picture/)
|
|
136
|
+
assert.match(source, /feedback: this\.feedback/)
|
|
137
|
+
assert.match(source, /new CustomEvent\('proposed', \{ detail: \{ proposal \}/)
|
|
138
|
+
assert.match(source, /this\.failure = \(err as Error\)\.message/)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('모델러의 수락·거절 피드백은 다음 인라인 후보 요청에도 같은 Figure 세션에서만 전달한다', () => {
|
|
142
|
+
const path = fileURLToPath(new URL('../client/pages/figure-modeller-page.ts', import.meta.url))
|
|
143
|
+
const source = readFileSync(path, 'utf8')
|
|
144
|
+
|
|
145
|
+
assert.match(source, /\.feedback=\$\{this\.proposalFeedback\}/)
|
|
146
|
+
assert.match(source, /this\.proposalFeedback = \[\.\.\.this\.proposalFeedback, feedback\]\.slice\(-5\)/)
|
|
147
|
+
assert.match(source, /changed\.has\('figure'\).*this\.proposalFeedback = \[\]/s)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('발행 게이트 수정안은 원본을 바꾸지 않고 후보로만 연다', () => {
|
|
151
|
+
const historyPath = fileURLToPath(new URL('../client/modeller/figure-history-panel.ts', import.meta.url))
|
|
152
|
+
const modellerPath = fileURLToPath(new URL('../client/pages/figure-modeller-page.ts', import.meta.url))
|
|
153
|
+
const sessionPath = fileURLToPath(new URL('../client/modeller/proposal-session.ts', import.meta.url))
|
|
154
|
+
const history = readFileSync(historyPath, 'utf8')
|
|
155
|
+
const modeller = readFileSync(modellerPath, 'utf8')
|
|
156
|
+
const session = readFileSync(sessionPath, 'utf8')
|
|
157
|
+
|
|
158
|
+
assert.match(history, /수정안 미리보기/)
|
|
159
|
+
assert.match(history, /안전하게 수정 적용/)
|
|
160
|
+
assert.match(history, /new CustomEvent\('apply-sizing-fix'/)
|
|
161
|
+
assert.match(history, /new CustomEvent\('preview-sizing-fix'/)
|
|
162
|
+
assert.match(modeller, /@preview-sizing-fix=\$\{\(e: CustomEvent\) => this\.previewSizingFix\(e\.detail\)\}/)
|
|
163
|
+
assert.match(modeller, /private previewSizingFix\(fix: FigureGateFix\)/)
|
|
164
|
+
assert.match(modeller, /private applySizingFix\(fix: FigureGateFix\)/)
|
|
165
|
+
assert.match(modeller, /if \(!fix\.safe \|\| !this\.folded\) return/)
|
|
166
|
+
assert.match(modeller, /this\.proposalSession = proposalSessions\.openSizingFix\(this\.folded, fix\)/)
|
|
167
|
+
assert.match(modeller, /private take\(\)[\s\S]*applyProposal\(this\.folded, this\.proposalSession\.source, this\.proposalSession\.picked\)[\s\S]*this\.dirty = true/)
|
|
168
|
+
assert.match(session, /export function openSizingFix[\s\S]*anchor: \{ \.\.\.\(part\.anchor \?\? \{\}\), \[fix\.axis\]: fix\.to \}/)
|
|
169
|
+
})
|
|
@@ -28,6 +28,8 @@ import { readFileSync, readdirSync } from 'node:fs'
|
|
|
28
28
|
import { fileURLToPath } from 'node:url'
|
|
29
29
|
import { join } from 'node:path'
|
|
30
30
|
|
|
31
|
+
import { CHANNEL_PATHS, CLIP_DRIVES, INTERPOLATIONS } from '@hatiolab/figure-model'
|
|
32
|
+
|
|
31
33
|
const HERE = fileURLToPath(new URL('.', import.meta.url))
|
|
32
34
|
const TRANSLATIONS = join(HERE, '../translations')
|
|
33
35
|
const CLIENT = join(HERE, '../client')
|
|
@@ -120,6 +122,46 @@ test('★ 통이 갈린 짝 — 사다리 이름과 설명', () => {
|
|
|
120
122
|
assert.ok(source.includes('figure.text.rung-${which}-note'), '사다리 설명을 text 통에서 찾지 않는다')
|
|
121
123
|
})
|
|
122
124
|
|
|
125
|
+
/*
|
|
126
|
+
애니메이션 탭도 열거 값을 키에 이어 붙인다. 사다리와 **같은 모양의 함정**이라 같은 못을 박는다.
|
|
127
|
+
|
|
128
|
+
갈린 통이 둘이다 — 구동은 이름(`label`)과 설명(`text.…-note`), 경로는 이름(`label`)과
|
|
129
|
+
단위 설명(`text.…-unit`). 한쪽만 넣으면 화면에 키가 그대로 뜨고, 넓은 앞머리가 위의
|
|
130
|
+
미사용 검사를 덮어 통과시킨다.
|
|
131
|
+
|
|
132
|
+
열거 값을 여기 다시 적지 않고 형식에서 읽는다. 적어 두면 형식에 값을 하나 더할 때
|
|
133
|
+
검사가 조용히 통과한다.
|
|
134
|
+
*/
|
|
135
|
+
test('★ 열거 값마다 짝이 다 있다 — 구동 · 경로 · 보간', () => {
|
|
136
|
+
const missing: string[] = []
|
|
137
|
+
const want = (key: string) => {
|
|
138
|
+
if (!(key in KO)) missing.push(key)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const drive of CLIP_DRIVES) {
|
|
142
|
+
want(`figure.label.drive-${drive}`)
|
|
143
|
+
want(`figure.text.drive-${drive}-note`)
|
|
144
|
+
}
|
|
145
|
+
for (const path of CHANNEL_PATHS) {
|
|
146
|
+
want(`figure.label.path-${path}`)
|
|
147
|
+
want(`figure.text.path-${path}-unit`)
|
|
148
|
+
}
|
|
149
|
+
for (const one of INTERPOLATIONS) {
|
|
150
|
+
want(`figure.label.interpolation-${one}`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
assert.deepEqual(missing, [], `이어 붙여 부르는 키가 번역에 없다:\n ${missing.join('\n ')}`)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('★ 애니메이션 탭이 그 통에서 찾는다 — 이름은 label, 설명은 text', () => {
|
|
157
|
+
const source = readFileSync(join(CLIENT, 'modeller/figure-animations.ts'), 'utf-8')
|
|
158
|
+
|
|
159
|
+
assert.ok(source.includes("'figure.label.drive-' + drive"), '구동 이름을 label 통에서 찾지 않는다')
|
|
160
|
+
assert.ok(source.includes("'figure.text.drive-'"), '구동 설명을 text 통에서 찾지 않는다')
|
|
161
|
+
assert.ok(source.includes("'figure.label.path-' + path"), '경로 이름을 label 통에서 찾지 않는다')
|
|
162
|
+
assert.ok(source.includes("'figure.text.path-' + channel.path + '-unit'"), '단위 설명을 text 통에서 찾지 않는다')
|
|
163
|
+
})
|
|
164
|
+
|
|
123
165
|
test('★ 이 가드가 실제로 읽고 있다 — 0이면 검사가 죽은 것이다', () => {
|
|
124
166
|
assert.ok(Object.keys(KO).length > 250, `읽은 키가 ${Object.keys(KO).length}개다`)
|
|
125
167
|
assert.ok(FILES.length > 10, `읽은 화면이 ${FILES.length}개다`)
|