@things-factory/figure-ui 10.1.7 → 10.1.9
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 +24 -0
- package/client/modeller/figure-animations.ts +61 -4
- package/client/modeller/figure-ask.ts +6 -1
- package/client/modeller/figure-history-panel.ts +52 -1
- package/client/modeller/figure-inspector.ts +477 -124
- package/client/modeller/proposal-session.ts +92 -0
- package/client/pages/figure-modeller-page.ts +179 -53
- package/client/types.ts +33 -0
- package/dist-client/graphql/index.d.ts +3 -1
- package/dist-client/graphql/index.js +21 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-animations.d.ts +4 -1
- package/dist-client/modeller/figure-animations.js +57 -4
- package/dist-client/modeller/figure-animations.js.map +1 -1
- 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 +3 -0
- package/dist-client/modeller/figure-inspector.js +464 -111
- package/dist-client/modeller/figure-inspector.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-modeller-page.d.ts +43 -9
- package/dist-client/pages/figure-modeller-page.js +173 -50
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +38 -0
- package/dist-client/types.js.map +1 -1
- package/package.json +4 -4
- package/test/ai-proposal-contract.test.ts +169 -0
- package/translations/en.json +32 -0
- package/translations/ja.json +32 -0
- package/translations/ko.json +42 -0
- package/translations/ms.json +32 -0
- package/translations/zh.json +32 -0
package/client/graphql/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
FigureListResult,
|
|
9
9
|
FigurePatch,
|
|
10
10
|
FigureProposal,
|
|
11
|
+
ProposalFeedback,
|
|
11
12
|
FigureVersion,
|
|
12
13
|
NewFigure
|
|
13
14
|
} from '../types.js'
|
|
@@ -196,6 +197,8 @@ export async function proposeFigure(request: {
|
|
|
196
197
|
base?: string
|
|
197
198
|
type?: string
|
|
198
199
|
palette: string[]
|
|
200
|
+
refine?: boolean
|
|
201
|
+
feedback?: ProposalFeedback[]
|
|
199
202
|
image?: File
|
|
200
203
|
}): Promise<FigureProposal> {
|
|
201
204
|
const response = await client.mutate({
|
|
@@ -207,6 +210,18 @@ export async function proposeFigure(request: {
|
|
|
207
210
|
grade
|
|
208
211
|
triangles
|
|
209
212
|
groups
|
|
213
|
+
quality {
|
|
214
|
+
status
|
|
215
|
+
summary
|
|
216
|
+
findings {
|
|
217
|
+
dimension
|
|
218
|
+
code
|
|
219
|
+
message
|
|
220
|
+
}
|
|
221
|
+
visualRegions
|
|
222
|
+
visualCoverage
|
|
223
|
+
visualReadability
|
|
224
|
+
}
|
|
210
225
|
attempts
|
|
211
226
|
violations {
|
|
212
227
|
code
|
|
@@ -280,6 +295,15 @@ export async function inspectFigure(id: string): Promise<FigureInspection> {
|
|
|
280
295
|
why
|
|
281
296
|
how
|
|
282
297
|
at
|
|
298
|
+
fixes {
|
|
299
|
+
findingWay
|
|
300
|
+
part
|
|
301
|
+
axis
|
|
302
|
+
from
|
|
303
|
+
to
|
|
304
|
+
resolved
|
|
305
|
+
safe
|
|
306
|
+
}
|
|
283
307
|
blocking
|
|
284
308
|
}
|
|
285
309
|
}
|
|
@@ -39,6 +39,23 @@ type Clip = NonNullable<BoardModel['animations']>[number]
|
|
|
39
39
|
type Channel = Clip['channels'][number]
|
|
40
40
|
type Keyframe = Channel['keys'][number]
|
|
41
41
|
|
|
42
|
+
/** A first-success motion: intent first, raw clip/channel editing second. */
|
|
43
|
+
interface MotionRecipe {
|
|
44
|
+
id: 'conveyor' | 'gate' | 'pusher' | 'lift'
|
|
45
|
+
name: string
|
|
46
|
+
drive: Clip['drive']
|
|
47
|
+
path: ChannelPath
|
|
48
|
+
end: Vec3
|
|
49
|
+
pivot?: Vec3
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const MOTION_RECIPES: readonly MotionRecipe[] = [
|
|
53
|
+
{ id: 'conveyor', name: 'conveyor-running', drive: 'loop', path: 'rotation', end: { x: 360, y: 0, z: 0 } },
|
|
54
|
+
{ id: 'gate', name: 'gate-open', drive: 'hold', path: 'rotation', end: { x: 0, y: 90, z: 0 }, pivot: { x: 0, y: 0, z: 0 } },
|
|
55
|
+
{ id: 'pusher', name: 'pusher-position', drive: 'hold', path: 'translation', end: { x: 100, y: 0, z: 0 } },
|
|
56
|
+
{ id: 'lift', name: 'lift-height', drive: 'hold', path: 'translation', end: { x: 0, y: 100, z: 0 } }
|
|
57
|
+
]
|
|
58
|
+
|
|
42
59
|
/** 새 clip 의 키 둘. **하나로 만들지 않는다** — 키 하나는 움직임이 아니라 자세다. */
|
|
43
60
|
const NEW_KEYS: Keyframe[] = [
|
|
44
61
|
{ at: 0, value: { x: 0, y: 0, z: 0 } },
|
|
@@ -63,6 +80,12 @@ function freeName(taken: readonly Clip[]): string {
|
|
|
63
80
|
}
|
|
64
81
|
}
|
|
65
82
|
|
|
83
|
+
function freeRecipeName(taken: readonly Clip[], base: string): string {
|
|
84
|
+
const used = new Set(taken.map(clip => clip.name))
|
|
85
|
+
if (!used.has(base)) return base
|
|
86
|
+
for (let n = 2; ; n++) if (!used.has(`${base}-${n}`)) return `${base}-${n}`
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
@customElement('figure-animations')
|
|
67
90
|
export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
68
91
|
static styles = [
|
|
@@ -104,6 +127,11 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
104
127
|
color: var(--md-sys-color-on-surface-variant);
|
|
105
128
|
}
|
|
106
129
|
|
|
130
|
+
div[recipes] { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
|
131
|
+
button[recipe] { min-height: 54px; align-items: flex-start; flex-direction: column; text-align: left; }
|
|
132
|
+
button[recipe] strong { font-size: 0.74rem; }
|
|
133
|
+
button[recipe] span { color: var(--md-sys-color-on-surface-variant); font-size: 0.68rem; line-height: 1.35; }
|
|
134
|
+
|
|
107
135
|
/* ── clip ─────────────────────────────────────────────────────── */
|
|
108
136
|
|
|
109
137
|
div[clip] {
|
|
@@ -263,6 +291,7 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
263
291
|
|
|
264
292
|
/** 펼쳐 둔 clip. 이름이 아니라 자리로 잡는다 — 이름은 편집 중에 바뀐다. */
|
|
265
293
|
@state() private open = 0
|
|
294
|
+
@state() private recipeTarget = ''
|
|
266
295
|
|
|
267
296
|
private get clips(): Clip[] {
|
|
268
297
|
return this.board?.animations ?? []
|
|
@@ -294,7 +323,8 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
294
323
|
<p quiet>${i18next.t('figure.text.animation-is-driven-by-a-fact')}</p>
|
|
295
324
|
</section>
|
|
296
325
|
|
|
297
|
-
${this.
|
|
326
|
+
${this.renderRecipes()}
|
|
327
|
+
${this.clips.length === 0 ? nothing : this.renderClips()}
|
|
298
328
|
|
|
299
329
|
<section>
|
|
300
330
|
<button @click=${() => this.addClip()} ?disabled=${this.partNames.length === 0}>
|
|
@@ -315,11 +345,24 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
315
345
|
* 「clip 을 추가하세요」라고만 쓰면 처음 여는 사람은 clip 이 무엇인지 모른 채 단추를 누른다.
|
|
316
346
|
* 그래서 무엇을 만들 수 있는지 예를 들고, 그것이 어떻게 움직이는지(사실이 들어온다)를 적는다.
|
|
317
347
|
*/
|
|
318
|
-
private
|
|
348
|
+
private renderRecipes(): TemplateResult {
|
|
319
349
|
return html`
|
|
320
350
|
<section>
|
|
321
|
-
<
|
|
322
|
-
<p quiet>${i18next.t('figure.text.
|
|
351
|
+
<h3>${i18next.t('figure.label.motion-intent')}</h3>
|
|
352
|
+
<p quiet>${i18next.t('figure.text.motion-intent-help')}</p>
|
|
353
|
+
${this.partNames.length
|
|
354
|
+
? html`<select .value=${this.recipeTarget} @change=${(e: Event) => (this.recipeTarget = (e.target as HTMLSelectElement).value)}>
|
|
355
|
+
${this.partNames.map(name => html`<option value=${name}>${name}</option>`)}
|
|
356
|
+
</select>
|
|
357
|
+
<div recipes>
|
|
358
|
+
${MOTION_RECIPES.map(recipe => html`
|
|
359
|
+
<button recipe @click=${() => this.addRecipe(recipe)}>
|
|
360
|
+
<strong>${i18next.t('figure.label.recipe-' + recipe.id)}</strong>
|
|
361
|
+
<span>${i18next.t('figure.text.recipe-' + recipe.id)}</span>
|
|
362
|
+
</button>
|
|
363
|
+
`)}
|
|
364
|
+
</div>`
|
|
365
|
+
: html`<p quiet>${i18next.t('figure.text.animation-needs-a-named-part')}</p>`}
|
|
323
366
|
</section>
|
|
324
367
|
`
|
|
325
368
|
}
|
|
@@ -622,6 +665,20 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
622
665
|
this.tell([...this.clips, clip])
|
|
623
666
|
}
|
|
624
667
|
|
|
668
|
+
/** Recipe creates a valid, editable two-pose clip; it never saves or publishes the Figure. */
|
|
669
|
+
private addRecipe(recipe: MotionRecipe): void {
|
|
670
|
+
const target = this.recipeTarget || this.partNames[0]
|
|
671
|
+
if (!target) return
|
|
672
|
+
const channel: Channel = {
|
|
673
|
+
target,
|
|
674
|
+
path: recipe.path,
|
|
675
|
+
keys: [{ at: 0, value: startValue(recipe.path) }, { at: 1, value: { ...recipe.end } }]
|
|
676
|
+
}
|
|
677
|
+
if (recipe.pivot) channel.pivot = { ...recipe.pivot }
|
|
678
|
+
this.open = this.clips.length
|
|
679
|
+
this.tell([...this.clips, { name: freeRecipeName(this.clips, recipe.name), drive: recipe.drive, channels: [channel] }])
|
|
680
|
+
}
|
|
681
|
+
|
|
625
682
|
private renameClip(at: number, name: string): void {
|
|
626
683
|
this.patchClip(at, { name: name.trim() })
|
|
627
684
|
}
|
|
@@ -8,7 +8,7 @@ import type { FigureSource } from '@hatiolab/figure-model'
|
|
|
8
8
|
import { activePalette } from '@hatiolab/things-scene'
|
|
9
9
|
|
|
10
10
|
import { proposeFigure } from '../graphql/index.js'
|
|
11
|
-
import type { FigureProposal } from '../types.js'
|
|
11
|
+
import type { FigureProposal, ProposalFeedback } from '../types.js'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* 말로 시키는 줄.
|
|
@@ -175,6 +175,8 @@ export class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
175
175
|
@property({ type: Object }) source?: FigureSource
|
|
176
176
|
/** 새로 만들 때 쓸 타입 이름. */
|
|
177
177
|
@property({ type: String }) type = ''
|
|
178
|
+
/** 이 모델러 세션에서 사람이 명시적으로 남긴 이전 후보 반응. */
|
|
179
|
+
@property({ type: Array }) feedback: ProposalFeedback[] = []
|
|
178
180
|
|
|
179
181
|
@state() private asking = false
|
|
180
182
|
@state() private failure = ''
|
|
@@ -239,6 +241,9 @@ export class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
239
241
|
type: this.source ? undefined : this.type,
|
|
240
242
|
// 그리는 쪽이 풀 수 있는 토큰만 보낸다. 서버는 이 목록 밖의 색을 거절한다.
|
|
241
243
|
palette: Object.keys(activePalette() as Record<string, string>),
|
|
244
|
+
// 구조적으로는 맞지만 품질 근거가 남으면 한 번 더 개선 후보를 시킨다.
|
|
245
|
+
refine: true,
|
|
246
|
+
feedback: this.feedback,
|
|
242
247
|
image: this.picture
|
|
243
248
|
})
|
|
244
249
|
|
|
@@ -7,7 +7,7 @@ import { i18next, localize } from '@operato/i18n'
|
|
|
7
7
|
import { ScrollbarStyles } from '@operato/styles'
|
|
8
8
|
|
|
9
9
|
import { fetchFigureVersions, inspectFigure, releaseFigure, revertFigureVersion } from '../graphql/index.js'
|
|
10
|
-
import type { FigureGateFinding, FigureInspection, FigureVersion } from '../types.js'
|
|
10
|
+
import type { FigureGateFinding, FigureGateFix, FigureInspection, FigureVersion } from '../types.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* 이력 — **발행하는 자리이자, 발행된 판들을 보는 자리.**
|
|
@@ -253,6 +253,33 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
253
253
|
margin-top: 1px;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
/* 설명으로 끝내지 않는다. 이 단추는 원본을 바꾸지 않고 모델러에 후보만 연다. */
|
|
257
|
+
li[finding] button[preview-fix] {
|
|
258
|
+
margin-top: 3px;
|
|
259
|
+
padding: 4px 7px;
|
|
260
|
+
font-weight: 700;
|
|
261
|
+
color: var(--md-sys-color-on-primary-container, var(--md-sys-color-on-surface));
|
|
262
|
+
background-color: var(--md-sys-color-primary-container, transparent);
|
|
263
|
+
border: 0;
|
|
264
|
+
border-radius: 5px;
|
|
265
|
+
cursor: pointer;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
li[finding] button[preview-fix]:hover {
|
|
269
|
+
filter: brightness(1.08);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
li[finding] button[apply-fix] {
|
|
273
|
+
margin-top: 3px;
|
|
274
|
+
padding: 4px 7px;
|
|
275
|
+
font-weight: 700;
|
|
276
|
+
color: var(--ui-on-accent, #fff);
|
|
277
|
+
background-color: var(--md-sys-color-primary);
|
|
278
|
+
border: 0;
|
|
279
|
+
border-radius: 5px;
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
}
|
|
282
|
+
|
|
256
283
|
p[quiet] {
|
|
257
284
|
margin: 0;
|
|
258
285
|
line-height: 1.6;
|
|
@@ -408,10 +435,34 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
408
435
|
${finding.at ? html`<code where>${finding.at}</code>` : nothing}
|
|
409
436
|
${finding.why ? html`<span why-line>${finding.why}</span>` : nothing}
|
|
410
437
|
${finding.how ? html`<span how-line><md-icon>build</md-icon>${finding.how}</span>` : nothing}
|
|
438
|
+
${(finding.fixes ?? []).map(fix => html`
|
|
439
|
+
${fix.safe ? html`
|
|
440
|
+
<button apply-fix @click=${() => this.applyFix(fix)}>
|
|
441
|
+
안전하게 수정 적용 · ${fix.part}.anchor.${fix.axis}: ${fix.from ?? 'auto'} → ${fix.to}
|
|
442
|
+
</button>
|
|
443
|
+
` : nothing}
|
|
444
|
+
<button preview-fix @click=${() => this.previewFix(fix)}>
|
|
445
|
+
수정안 미리보기 · ${fix.part}.anchor.${fix.axis}: ${fix.from ?? 'auto'} → ${fix.to}
|
|
446
|
+
</button>
|
|
447
|
+
`)}
|
|
411
448
|
</li>
|
|
412
449
|
`
|
|
413
450
|
}
|
|
414
451
|
|
|
452
|
+
/**
|
|
453
|
+
* 게이트는 수정을 저장하지 않는다. 부모 모델러가 현재 정본과 이 한 수정안을 나란히 렌더하고,
|
|
454
|
+
* 저작자가 '변경 받기'를 눌렀을 때에만 자기 작업 사본에 적용한다.
|
|
455
|
+
*/
|
|
456
|
+
private previewFix(fix: FigureGateFix) {
|
|
457
|
+
this.dispatchEvent(new CustomEvent('preview-sizing-fix', { detail: fix, bubbles: true, composed: true }))
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** 서버가 전수 재검사해 모든 차단 split이 사라진 후보만 작업 사본에 원클릭 적용한다. 저장은 하지 않는다. */
|
|
461
|
+
private applyFix(fix: FigureGateFix) {
|
|
462
|
+
if (!fix.safe) return
|
|
463
|
+
this.dispatchEvent(new CustomEvent('apply-sizing-fix', { detail: fix, bubbles: true, composed: true }))
|
|
464
|
+
}
|
|
465
|
+
|
|
415
466
|
private renderVersion(version: FigureVersion) {
|
|
416
467
|
return html`
|
|
417
468
|
<li>
|