@things-factory/figure-ui 10.1.33 → 10.1.35
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 +13 -0
- package/client/modeller/figure-history-panel.ts +82 -2
- package/client/modeller/figure-side.ts +3 -0
- package/client/modeller/v3-renderer.ts +61 -0
- package/client/pages/figure-modeller-page.ts +67 -3
- package/client/types.ts +8 -0
- package/dist-client/graphql/index.js +13 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +13 -0
- package/dist-client/modeller/figure-history-panel.js +89 -4
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +2 -0
- package/dist-client/modeller/figure-side.js +5 -0
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/v3-renderer.d.ts +31 -0
- package/dist-client/modeller/v3-renderer.js +81 -0
- package/dist-client/modeller/v3-renderer.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.d.ts +9 -0
- package/dist-client/pages/figure-modeller-page.js +73 -2
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +7 -0
- package/dist-client/types.js.map +1 -1
- package/package.json +4 -4
- package/test/base-box.test.ts +13 -0
- package/test/v3-renderer.test.ts +24 -0
- package/translations/en.json +7 -0
- package/translations/ja.json +3 -0
- package/translations/ko.json +7 -0
- package/translations/ms.json +3 -0
- package/translations/zh.json +3 -0
package/client/graphql/index.ts
CHANGED
|
@@ -310,6 +310,19 @@ export async function inspectFigure(id: string): Promise<FigureInspection> {
|
|
|
310
310
|
resolved
|
|
311
311
|
safe
|
|
312
312
|
}
|
|
313
|
+
fixPlan {
|
|
314
|
+
resolved
|
|
315
|
+
safe
|
|
316
|
+
fixes {
|
|
317
|
+
findingWay
|
|
318
|
+
part
|
|
319
|
+
axis
|
|
320
|
+
from
|
|
321
|
+
to
|
|
322
|
+
resolved
|
|
323
|
+
safe
|
|
324
|
+
}
|
|
325
|
+
}
|
|
313
326
|
blocking
|
|
314
327
|
}
|
|
315
328
|
}
|
|
@@ -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, FigureGateFix, FigureInspection, FigureVersion } from '../types.js'
|
|
10
|
+
import type { FigureGateFinding, FigureGateFix, FigureGateFixPlan, FigureInspection, FigureVersion } from '../types.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* 이력 — **발행하는 자리이자, 발행된 판들을 보는 자리.**
|
|
@@ -234,6 +234,16 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
234
234
|
opacity: 0.85;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
li[finding] details[affected] {
|
|
238
|
+
width: 100%;
|
|
239
|
+
color: inherit;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
li[finding] details[affected] summary {
|
|
243
|
+
cursor: pointer;
|
|
244
|
+
font-weight: 600;
|
|
245
|
+
}
|
|
246
|
+
|
|
237
247
|
/* 왜 나쁜지는 곁글로 — 읽어야 하지만 잰 것보다 먼저 눈에 들 필요는 없다 */
|
|
238
248
|
li[finding] span[why-line] {
|
|
239
249
|
opacity: 0.8;
|
|
@@ -280,6 +290,28 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
280
290
|
cursor: pointer;
|
|
281
291
|
}
|
|
282
292
|
|
|
293
|
+
div[ratio-choice] {
|
|
294
|
+
display: flex;
|
|
295
|
+
flex-wrap: wrap;
|
|
296
|
+
gap: 6px;
|
|
297
|
+
margin-top: 4px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
div[ratio-choice] button:not([apply-fix]) {
|
|
301
|
+
padding: 4px 7px;
|
|
302
|
+
color: var(--md-sys-color-on-surface);
|
|
303
|
+
background: transparent;
|
|
304
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
305
|
+
border-radius: 5px;
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
div[ratio-choice] button[selected] {
|
|
310
|
+
color: var(--md-sys-color-on-primary-container);
|
|
311
|
+
background: var(--md-sys-color-primary-container);
|
|
312
|
+
border-color: transparent;
|
|
313
|
+
}
|
|
314
|
+
|
|
283
315
|
p[quiet] {
|
|
284
316
|
margin: 0;
|
|
285
317
|
line-height: 1.6;
|
|
@@ -294,10 +326,13 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
294
326
|
@property({ type: Number }) figureVersion = 0
|
|
295
327
|
/** 저장하지 않은 것이 있나 — 있으면 발행을 막는다. */
|
|
296
328
|
@property({ type: Boolean }) dirty = false
|
|
329
|
+
/** 직전 발행판의 기준 상자. 외형 변화 안내에서 X·Z 복원 선택지에만 쓴다. */
|
|
330
|
+
@property({ attribute: false }) releasedBase?: { x: number; y: number; z: number }
|
|
297
331
|
|
|
298
332
|
@state() private versions: FigureVersion[] = []
|
|
299
333
|
@state() private busy = false
|
|
300
334
|
@state() private failure = ''
|
|
335
|
+
@state() private keepChangedAppearance = false
|
|
301
336
|
/**
|
|
302
337
|
* 발행 판정. **저장된 정본에 대한 것**이라 저장할 때마다 다시 묻는다(`figureVersion` 이 아니라
|
|
303
338
|
* `dirty` 가 풀리는 순간이 그때다).
|
|
@@ -319,6 +354,7 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
319
354
|
}
|
|
320
355
|
|
|
321
356
|
private async inspect() {
|
|
357
|
+
this.keepChangedAppearance = false
|
|
322
358
|
if (!this.figureId || this.dirty) {
|
|
323
359
|
this.inspection = undefined
|
|
324
360
|
return
|
|
@@ -374,6 +410,7 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
374
410
|
*/
|
|
375
411
|
const blocking = (this.inspection?.findings ?? []).filter(finding => finding.blocking)
|
|
376
412
|
const advisory = (this.inspection?.findings ?? []).filter(finding => !finding.blocking)
|
|
413
|
+
const appearanceChoiceNeeded = advisory.some(finding => finding.code === 'base-ratio-changed') && !this.keepChangedAppearance
|
|
377
414
|
|
|
378
415
|
/*
|
|
379
416
|
── 「발행되나」에 **한 번만** 답한다 ─────────────────────────────────────
|
|
@@ -388,6 +425,8 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
388
425
|
? i18next.t('figure.text.release-needs-a-save-first')
|
|
389
426
|
: released
|
|
390
427
|
? i18next.t('figure.text.already-released-edit-to-release-again')
|
|
428
|
+
: appearanceChoiceNeeded
|
|
429
|
+
? i18next.t('figure.text.choose-appearance-change')
|
|
391
430
|
: blocking.length
|
|
392
431
|
? i18next.t('figure.text.cannot-release-now')
|
|
393
432
|
: ''
|
|
@@ -432,7 +471,12 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
432
471
|
return html`
|
|
433
472
|
<li finding ?blocking=${blocking}>
|
|
434
473
|
<span msg>${finding.message}</span>
|
|
435
|
-
${finding.at ? html
|
|
474
|
+
${finding.at ? html`
|
|
475
|
+
<details affected>
|
|
476
|
+
<summary>${i18next.t('figure.text.show-affected-parts')}</summary>
|
|
477
|
+
<code where>${finding.at}</code>
|
|
478
|
+
</details>
|
|
479
|
+
` : nothing}
|
|
436
480
|
${finding.why ? html`<span why-line>${finding.why}</span>` : nothing}
|
|
437
481
|
${finding.how ? html`<span how-line><md-icon>build</md-icon>${finding.how}</span>` : nothing}
|
|
438
482
|
${finding.code === 'part-outside-base'
|
|
@@ -445,6 +489,17 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
445
489
|
${i18next.t('figure.button.settle-on-floor')}
|
|
446
490
|
</button>`
|
|
447
491
|
: nothing}
|
|
492
|
+
${finding.code === 'base-ratio-changed' && this.releasedBase
|
|
493
|
+
? html`<div ratio-choice>
|
|
494
|
+
<button apply-fix @click=${() => this.restoreReleasedFootprint()}>
|
|
495
|
+
${i18next.t('figure.button.restore-released-base')}
|
|
496
|
+
</button>
|
|
497
|
+
<button ?selected=${this.keepChangedAppearance} @click=${() => this.keepChangedFootprint()}>
|
|
498
|
+
${i18next.t('figure.button.keep-changed-appearance')}
|
|
499
|
+
</button>
|
|
500
|
+
</div>
|
|
501
|
+
<span how-line><md-icon>info</md-icon>${i18next.t('figure.text.appearance-choice-explained')}</span>`
|
|
502
|
+
: nothing}
|
|
448
503
|
${(finding.fixes ?? []).map(fix => html`
|
|
449
504
|
${fix.safe ? html`
|
|
450
505
|
<button apply-fix @click=${() => this.applyFix(fix)}>
|
|
@@ -455,6 +510,12 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
455
510
|
${i18next.t('figure.button.preview-fix', { part: fix.part, axis: fix.axis.toUpperCase(), from: fix.from ?? 'auto', to: fix.to })}
|
|
456
511
|
</button>
|
|
457
512
|
`)}
|
|
513
|
+
${finding.fixPlan?.safe ? html`
|
|
514
|
+
<span how-line><md-icon>auto_fix_high</md-icon>${i18next.t('figure.text.recommended-fix-explained', { n: finding.fixPlan.fixes.length })}</span>
|
|
515
|
+
<button apply-fix @click=${() => this.applyFixPlan(finding.fixPlan!)}>
|
|
516
|
+
${i18next.t('figure.button.apply-recommended-fix')}
|
|
517
|
+
</button>
|
|
518
|
+
` : nothing}
|
|
458
519
|
</li>
|
|
459
520
|
`
|
|
460
521
|
}
|
|
@@ -473,6 +534,12 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
473
534
|
this.dispatchEvent(new CustomEvent('apply-sizing-fix', { detail: fix, bubbles: true, composed: true }))
|
|
474
535
|
}
|
|
475
536
|
|
|
537
|
+
/** Apply the server-remeasured coordinated repair to the draft only; save remains explicit. */
|
|
538
|
+
private applyFixPlan(plan: FigureGateFixPlan) {
|
|
539
|
+
if (!plan.safe || plan.fixes.length === 0) return
|
|
540
|
+
this.dispatchEvent(new CustomEvent('apply-sizing-fix-plan', { detail: plan, bubbles: true, composed: true }))
|
|
541
|
+
}
|
|
542
|
+
|
|
476
543
|
/**
|
|
477
544
|
* 넘친 것을 담는 길 하나를 여기서도 내민다 — **같은 동작이 속성 패널에도 있다**(「부품에 맞추기」).
|
|
478
545
|
*
|
|
@@ -493,6 +560,19 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
493
560
|
this.dispatchEvent(new CustomEvent('settle-parts-on-floor', { bubbles: true, composed: true }))
|
|
494
561
|
}
|
|
495
562
|
|
|
563
|
+
/** 기준 상자의 X·Z만 직전 발행값으로 되돌린다. Y와 부품 편집은 보존한다. */
|
|
564
|
+
private restoreReleasedFootprint() {
|
|
565
|
+
if (!this.releasedBase) return
|
|
566
|
+
this.dispatchEvent(new CustomEvent('restore-released-footprint', {
|
|
567
|
+
detail: { x: this.releasedBase.x, z: this.releasedBase.z }, bubbles: true, composed: true
|
|
568
|
+
}))
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** 현재 외형으로 발행하겠다는 명시적 선택이다. 저장·발행은 이어서 따로 한다. */
|
|
572
|
+
private keepChangedFootprint() {
|
|
573
|
+
this.keepChangedAppearance = true
|
|
574
|
+
}
|
|
575
|
+
|
|
496
576
|
private renderVersion(version: FigureVersion) {
|
|
497
577
|
return html`
|
|
498
578
|
<li>
|
|
@@ -162,6 +162,8 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
162
162
|
|
|
163
163
|
/** 이 고침이 이미 놓인 인스턴스에 무엇을 하나(ADR-0063 결정 3). 발행 전 도형에는 비어 있다. */
|
|
164
164
|
@property({ type: Array }) contract: ContractChange[] = []
|
|
165
|
+
/** 직전 발행 기준 상자. 외형 변화 안내의 X·Z 복원 선택지에만 쓴다. */
|
|
166
|
+
@property({ type: Object }) releasedBase?: FigureSource['base']
|
|
165
167
|
|
|
166
168
|
/** 견주지 못한 까닭. 있으면 화면이 「확인하지 못했다」로 말한다. */
|
|
167
169
|
@property({ type: String }) contractFailure?: string
|
|
@@ -239,6 +241,7 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
239
241
|
.figureState=${this.figureState}
|
|
240
242
|
.figureVersion=${this.figureVersion}
|
|
241
243
|
.dirty=${this.dirty}
|
|
244
|
+
.releasedBase=${this.releasedBase}
|
|
242
245
|
></figure-history-panel>
|
|
243
246
|
`
|
|
244
247
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
/** Evaluated V3 boundary: graph values are resolved before reaching the renderer. */
|
|
4
|
+
export interface V3RenderPart {
|
|
5
|
+
id: string
|
|
6
|
+
shape: { provider: string; dimensions: Record<string, number>; profile?: Array<{x:number;y:number}> }
|
|
7
|
+
pose: {r:number[][];t:number[]}
|
|
8
|
+
material: {token:string;preset?:string;transparent?:boolean;emissive?:{token:string;intensity:number;on:boolean}}
|
|
9
|
+
segments?:number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function geometry(part:V3RenderPart):THREE.BufferGeometry {
|
|
13
|
+
const d=part.shape.dimensions
|
|
14
|
+
let g:THREE.BufferGeometry
|
|
15
|
+
if(part.shape.provider==='box')g=new THREE.BoxGeometry(d.width,d.height,d.depth)
|
|
16
|
+
else if(part.shape.provider==='cylinder')g=new THREE.CylinderGeometry(d.radius,d.radius,d.length,part.segments??12)
|
|
17
|
+
else if(part.shape.provider==='roundedBox'){
|
|
18
|
+
const w=d.width/2,z=d.depth/2,r=d.round,s=new THREE.Shape()
|
|
19
|
+
s.moveTo(-w+r,-z);s.lineTo(w-r,-z);s.quadraticCurveTo(w,-z,w,-z+r)
|
|
20
|
+
s.lineTo(w,z-r);s.quadraticCurveTo(w,z,w-r,z);s.lineTo(-w+r,z)
|
|
21
|
+
s.quadraticCurveTo(-w,z,-w,z-r);s.lineTo(-w,-z+r);s.quadraticCurveTo(-w,-z,-w+r,-z)
|
|
22
|
+
g=new THREE.ExtrudeGeometry(s,{depth:d.height,bevelEnabled:false,steps:1,curveSegments:6})
|
|
23
|
+
g.rotateX(Math.PI/2);g.translate(0,d.height/2,0)
|
|
24
|
+
}else if(part.shape.provider==='portal'){
|
|
25
|
+
if(!part.shape.profile?.length)throw Error('Portal profile is missing')
|
|
26
|
+
const s=new THREE.Shape(part.shape.profile.map(p=>new THREE.Vector2(p.x,p.y)))
|
|
27
|
+
g=new THREE.ExtrudeGeometry(s,{depth:d.depth,bevelEnabled:false,steps:1});g.translate(0,0,-d.depth/2)
|
|
28
|
+
}else throw Error('Unsupported V3 provider: '+part.shape.provider)
|
|
29
|
+
return g
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function disposeV3Meshes(group:THREE.Group):void {
|
|
33
|
+
for(const child of [...group.children]){
|
|
34
|
+
if(child instanceof THREE.Mesh){child.geometry.dispose();const materials=Array.isArray(child.material)?child.material:[child.material];materials.forEach(m=>m.dispose())}
|
|
35
|
+
group.remove(child)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Build first, then swap; failed rendering must not erase the previous scene. No old meshes survive a successful swap. */
|
|
40
|
+
export function replaceV3Meshes(group:THREE.Group,parts:readonly V3RenderPart[],palette:Record<string,string>):void {
|
|
41
|
+
const staged=new THREE.Group(),ids=new Set<string>()
|
|
42
|
+
try{
|
|
43
|
+
for(const part of parts){
|
|
44
|
+
if(ids.has(part.id))throw Error('Duplicate V3 geometry ID: '+part.id);ids.add(part.id)
|
|
45
|
+
const color=palette[part.material.token]
|
|
46
|
+
if(!color||!/^#[0-9a-f]{6}$/i.test(color))throw Error('Unresolved material token: '+part.material.token)
|
|
47
|
+
const {r,t}=part.pose
|
|
48
|
+
if(r.length!==3||r.some(row=>row.length!==3||row.some(x=>!Number.isFinite(x)))||t.length!==3||t.some(x=>!Number.isFinite(x)))throw Error('Invalid V3 pose: '+part.id)
|
|
49
|
+
const emissive=part.material.emissive
|
|
50
|
+
if(emissive?.on&&!/^#[0-9a-f]{6}$/i.test(palette[emissive.token]??''))throw Error('Unresolved emissive token')
|
|
51
|
+
const g=geometry(part)
|
|
52
|
+
const material=new THREE.MeshStandardMaterial({color,metalness:part.material.preset==='metal'?0.8:0,roughness:part.material.preset==='metal'?0.3:0.65,transparent:!!part.material.transparent,opacity:part.material.transparent?0.35:1,emissive:emissive?.on?palette[emissive.token]:0,emissiveIntensity:emissive?.on?emissive.intensity:0})
|
|
53
|
+
const mesh=new THREE.Mesh(g,material);mesh.name=part.id;mesh.userData.figurePartId=part.id
|
|
54
|
+
mesh.matrixAutoUpdate=false;mesh.matrix.set(r[0][0],r[0][1],r[0][2],t[0],r[1][0],r[1][1],r[1][2],t[1],r[2][0],r[2][1],r[2][2],t[2],0,0,0,1)
|
|
55
|
+
staged.add(mesh)
|
|
56
|
+
}
|
|
57
|
+
}catch(error){disposeV3Meshes(staged);throw error}
|
|
58
|
+
disposeV3Meshes(group)
|
|
59
|
+
for(const mesh of [...staged.children])group.add(mesh)
|
|
60
|
+
group.updateMatrixWorld(true)
|
|
61
|
+
}
|
|
@@ -29,7 +29,7 @@ import { gizmoPart } from '../modeller/joint-gizmo.js'
|
|
|
29
29
|
import { DEFAULT_VIEW } from '../modeller/figure-view.js'
|
|
30
30
|
import type { ViewSettings } from '../modeller/figure-view.js'
|
|
31
31
|
import { createFigure, fetchFigure, fetchFigureTypeNames, fetchReleasedSource, updateFigure } from '../graphql/index.js'
|
|
32
|
-
import type { Figure, FigureFinding, FigureGateFix, FigureProposal, ProposalFeedback } from '../types.js'
|
|
32
|
+
import type { Figure, FigureFinding, FigureGateFix, FigureGateFixPlan, FigureProposal, ProposalFeedback } from '../types.js'
|
|
33
33
|
|
|
34
34
|
const FigureModellerPageBase = localize(i18next)(PageView) as typeof PageView
|
|
35
35
|
|
|
@@ -653,6 +653,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
653
653
|
* 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
|
|
654
654
|
*/
|
|
655
655
|
@state() private saveFailure = ''
|
|
656
|
+
@state() private sourceLoadFailure = ''
|
|
656
657
|
@state() private showSaveModal = false
|
|
657
658
|
@state() private saveModalType = ''
|
|
658
659
|
@state() private saveModalName = ''
|
|
@@ -903,6 +904,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
903
904
|
}
|
|
904
905
|
|
|
905
906
|
render() {
|
|
907
|
+
if (this.sourceLoadFailure) return html`<section role="alert" style="padding:24px">
|
|
908
|
+
<h2>Figure를 열지 못했습니다</h2><p>${this.sourceLoadFailure}</p>
|
|
909
|
+
<button @click=${() => navigate('figure-list')}>Figure 목록으로</button>
|
|
910
|
+
</section>`
|
|
906
911
|
const source = this.folded
|
|
907
912
|
|
|
908
913
|
return html`
|
|
@@ -917,6 +922,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
917
922
|
@figure-reverted=${() => this.figure?.id && this.load(this.figure.id)}
|
|
918
923
|
@preview-sizing-fix=${(e: CustomEvent) => this.previewSizingFix(e.detail)}
|
|
919
924
|
@apply-sizing-fix=${(e: CustomEvent) => this.applySizingFix(e.detail)}
|
|
925
|
+
@apply-sizing-fix-plan=${(e: CustomEvent) => this.applySizingFixPlan(e.detail)}
|
|
926
|
+
@restore-released-footprint=${(e: CustomEvent) => this.restoreReleasedFootprint(e.detail)}
|
|
920
927
|
@fit-base-to-parts=${() => this.fitBaseToParts()}
|
|
921
928
|
@settle-parts-on-floor=${() => this.settlePartsOnFloor()}
|
|
922
929
|
@add-part=${(e: CustomEvent) => this.addPart(e.detail.primitive)}
|
|
@@ -1004,6 +1011,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1004
1011
|
.source=${source}
|
|
1005
1012
|
.contract=${this.contract}
|
|
1006
1013
|
.contractFailure=${this.contractFailure}
|
|
1014
|
+
.releasedBase=${this.releasedSource?.base}
|
|
1007
1015
|
.selected=${this.selected}
|
|
1008
1016
|
.view=${this.view}
|
|
1009
1017
|
@update-view=${(e: CustomEvent) => (this.view = e.detail.view)}
|
|
@@ -1277,6 +1285,36 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1277
1285
|
this.take()
|
|
1278
1286
|
}
|
|
1279
1287
|
|
|
1288
|
+
/**
|
|
1289
|
+
* The release gate may need several anchors to keep a connected assembly together. It has already
|
|
1290
|
+
* measured the complete set. Apply it directly to this unsaved work copy instead of making the
|
|
1291
|
+
* author accept a sequence of opaque mini-proposals.
|
|
1292
|
+
*/
|
|
1293
|
+
private applySizingFixPlan(plan: FigureGateFixPlan) {
|
|
1294
|
+
if (!plan.safe || !this.folded || plan.fixes.length === 0) return
|
|
1295
|
+
const fixes = new Map(plan.fixes.map(fix => [`${fix.part}:${fix.axis}`, fix]))
|
|
1296
|
+
const next: FigureSource = {
|
|
1297
|
+
...this.folded,
|
|
1298
|
+
parts: this.folded.parts.map(part => {
|
|
1299
|
+
const anchor = { ...(part.anchor ?? {}) }
|
|
1300
|
+
let changed = false
|
|
1301
|
+
for (const axis of ['x', 'y', 'z'] as const) {
|
|
1302
|
+
const fix = fixes.get(`${part.name}:${axis}`)
|
|
1303
|
+
if (!fix) continue
|
|
1304
|
+
anchor[axis] = fix.to
|
|
1305
|
+
changed = true
|
|
1306
|
+
}
|
|
1307
|
+
return changed ? { ...part, anchor } : part
|
|
1308
|
+
})
|
|
1309
|
+
}
|
|
1310
|
+
this.recordHistory()
|
|
1311
|
+
const { draft, parts } = fromFigureSource(next)
|
|
1312
|
+
this.draft = draft
|
|
1313
|
+
this.parts = parts
|
|
1314
|
+
this.selected = parts.length ? 0 : -1
|
|
1315
|
+
this.dirty = true
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1280
1318
|
/**
|
|
1281
1319
|
* 고른 변경을 받아들인다.
|
|
1282
1320
|
*
|
|
@@ -1424,6 +1462,17 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1424
1462
|
const figure = await fetchFigure(id)
|
|
1425
1463
|
if (generation !== this.loadGeneration || !this.active) return
|
|
1426
1464
|
|
|
1465
|
+
// Decode before assigning the target ID or generating a thumbnail. On failure the previous
|
|
1466
|
+
// editor snapshot remains internal, but is hidden and cannot be saved under the requested ID.
|
|
1467
|
+
let model: ReturnType<typeof fromFigureSource> | undefined
|
|
1468
|
+
try {
|
|
1469
|
+
model = figure.source ? fromFigureSource(JSON.parse(figure.source) as FigureSource) : undefined
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
this.sourceLoadFailure = error instanceof Error ? error.message : String(error)
|
|
1472
|
+
return
|
|
1473
|
+
}
|
|
1474
|
+
this.sourceLoadFailure = ''
|
|
1475
|
+
|
|
1427
1476
|
this.proposalSession = undefined
|
|
1428
1477
|
this.proposalBaseSource = undefined
|
|
1429
1478
|
this.figure = figure
|
|
@@ -1441,7 +1490,6 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1441
1490
|
|
|
1442
1491
|
// 서버가 준 것을 그대로 쓴다. 읽지 못하면 오류보고없이 빈 상태로 바꾸지 않는다.
|
|
1443
1492
|
// 그러면 사용자가 저장을 선택해 원본을 덮어쓴다.
|
|
1444
|
-
const model = figure.source ? fromFigureSource(JSON.parse(figure.source) as FigureSource) : undefined
|
|
1445
1493
|
this.draft = model?.draft
|
|
1446
1494
|
this.parts = model?.parts ?? []
|
|
1447
1495
|
|
|
@@ -1485,6 +1533,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1485
1533
|
}
|
|
1486
1534
|
|
|
1487
1535
|
private startNew() {
|
|
1536
|
+
this.sourceLoadFailure = ''
|
|
1488
1537
|
++this.loadGeneration
|
|
1489
1538
|
this.proposalSession = undefined
|
|
1490
1539
|
this.proposalBaseSource = undefined
|
|
@@ -1679,6 +1728,21 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1679
1728
|
this.dirty = true
|
|
1680
1729
|
}
|
|
1681
1730
|
|
|
1731
|
+
/** 이전 발행 X·Z만 복원한다. Y와 이번 리비전의 부품 편집은 건드리지 않는다. */
|
|
1732
|
+
private restoreReleasedFootprint(base: { x: number; z: number }) {
|
|
1733
|
+
if (!this.draft || !this.releasedSource) return
|
|
1734
|
+
const released = this.releasedSource.base
|
|
1735
|
+
if (base.x !== released.x || base.z !== released.z) return
|
|
1736
|
+
|
|
1737
|
+
const next = resizedToFit(this.draft, this.parts, {
|
|
1738
|
+
width: released.x,
|
|
1739
|
+
depth: this.draft.depth,
|
|
1740
|
+
height: released.z
|
|
1741
|
+
})
|
|
1742
|
+
if (next.draft === this.draft) return
|
|
1743
|
+
this.resizeBase(next.draft, [...next.parts])
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1682
1746
|
private resizeBase(draft: FigureDraft, parts: PartModel[]) {
|
|
1683
1747
|
this.recordHistory('base')
|
|
1684
1748
|
this.draft = draft
|
|
@@ -1847,7 +1911,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1847
1911
|
}
|
|
1848
1912
|
|
|
1849
1913
|
private save() {
|
|
1850
|
-
if (!this.canSave || !this.folded || !this.figure) {
|
|
1914
|
+
if (this.sourceLoadFailure || !this.canSave || !this.folded || !this.figure) {
|
|
1851
1915
|
return
|
|
1852
1916
|
}
|
|
1853
1917
|
|
package/client/types.ts
CHANGED
|
@@ -158,6 +158,13 @@ export interface FigureGateFix {
|
|
|
158
158
|
safe: boolean
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/** A server-remeasured set of draft-only changes that clears every blocking sizing split. */
|
|
162
|
+
export interface FigureGateFixPlan {
|
|
163
|
+
fixes: FigureGateFix[]
|
|
164
|
+
resolved: number
|
|
165
|
+
safe: boolean
|
|
166
|
+
}
|
|
167
|
+
|
|
161
168
|
export interface FigureGateFinding {
|
|
162
169
|
code: string
|
|
163
170
|
message: string
|
|
@@ -169,6 +176,7 @@ export interface FigureGateFinding {
|
|
|
169
176
|
at?: string
|
|
170
177
|
/** Candidate edits; the UI must preview and explicitly apply these, never auto-save them. */
|
|
171
178
|
fixes?: FigureGateFix[]
|
|
179
|
+
fixPlan?: FigureGateFixPlan
|
|
172
180
|
blocking: boolean
|
|
173
181
|
}
|
|
174
182
|
|
|
@@ -1 +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 { i18next } from '@operato/i18n'\nimport { client } from '@operato/graphql'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureProposal,\n ProposalFeedback,\n FigureVersion,\n NewFigure\n} 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 /* The field name stays off the screen because the user cannot act on it; it goes to the console. */\n console.error(`[figure-ui] the server returned no data for ${field}`)\n throw new Error(i18next.t('figure.text.server-returned-no-data'))\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n tags\n state\n version\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\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: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.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 }\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 }\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 refine?: boolean\n feedback?: ProposalFeedback[]\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 quality {\n status\n summary\n findings {\n dimension\n code\n message\n }\n visualRegions\n visualCoverage\n visualReadability\n }\n attempts\n violations {\n code\n message\n }\n }\n }\n `,\n variables: { request },\n // @operato/graphql 는 이 신호가 있을 때만 Upload가 포함된 변수를 multipart로 인코딩한다.\n // 없으면 브라우저 File은 JSON에서 {}가 되어 서버 Upload scalar가 요청 자체를 거절한다.\n context: { hasUpload: !!request.image }\n })\n\n return unwrap<FigureProposal>(response, 'proposeFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */\nexport async function revertFigureVersion(id: string, version: number): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!) {\n revertFigureVersion(id: $id, version: $version) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, version }\n })\n\n return unwrap<SaveResult>(response, 'revertFigureVersion')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n fixes {\n findingWay\n part\n axis\n from\n to\n resolved\n safe\n }\n blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\n/**\n * 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.\n *\n * 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장\n * 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.\n */\nexport async function fetchReleasedSource(id: string): Promise<string | undefined> {\n const response = await client.query({\n query: gql`\n query FigureReleased($id: String!) {\n figureReleased(id: $id) {\n version\n source\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<{ source?: string }>(response, 'figureReleased')?.source ?? undefined\n}\n\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n"]}
|
|
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 { i18next } from '@operato/i18n'\nimport { client } from '@operato/graphql'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureProposal,\n ProposalFeedback,\n FigureVersion,\n NewFigure\n} 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 /* The field name stays off the screen because the user cannot act on it; it goes to the console. */\n console.error(`[figure-ui] the server returned no data for ${field}`)\n throw new Error(i18next.t('figure.text.server-returned-no-data'))\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n tags\n state\n version\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\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: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.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 }\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 }\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 refine?: boolean\n feedback?: ProposalFeedback[]\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 quality {\n status\n summary\n findings {\n dimension\n code\n message\n }\n visualRegions\n visualCoverage\n visualReadability\n }\n attempts\n violations {\n code\n message\n }\n }\n }\n `,\n variables: { request },\n // @operato/graphql 는 이 신호가 있을 때만 Upload가 포함된 변수를 multipart로 인코딩한다.\n // 없으면 브라우저 File은 JSON에서 {}가 되어 서버 Upload scalar가 요청 자체를 거절한다.\n context: { hasUpload: !!request.image }\n })\n\n return unwrap<FigureProposal>(response, 'proposeFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */\nexport async function revertFigureVersion(id: string, version: number): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!) {\n revertFigureVersion(id: $id, version: $version) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, version }\n })\n\n return unwrap<SaveResult>(response, 'revertFigureVersion')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n fixes {\n findingWay\n part\n axis\n from\n to\n resolved\n safe\n }\n fixPlan {\n resolved\n safe\n fixes {\n findingWay\n part\n axis\n from\n to\n resolved\n safe\n }\n }\n blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\n/**\n * 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.\n *\n * 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장\n * 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.\n */\nexport async function fetchReleasedSource(id: string): Promise<string | undefined> {\n const response = await client.query({\n query: gql`\n query FigureReleased($id: String!) {\n figureReleased(id: $id) {\n version\n source\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<{ source?: string }>(response, 'figureReleased')?.source ?? undefined\n}\n\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n"]}
|
|
@@ -28,9 +28,16 @@ export declare class FigureHistoryPanel extends FigureHistoryPanel_base {
|
|
|
28
28
|
figureVersion: number;
|
|
29
29
|
/** 저장하지 않은 것이 있나 — 있으면 발행을 막는다. */
|
|
30
30
|
dirty: boolean;
|
|
31
|
+
/** 직전 발행판의 기준 상자. 외형 변화 안내에서 X·Z 복원 선택지에만 쓴다. */
|
|
32
|
+
releasedBase?: {
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
z: number;
|
|
36
|
+
};
|
|
31
37
|
private versions;
|
|
32
38
|
private busy;
|
|
33
39
|
private failure;
|
|
40
|
+
private keepChangedAppearance;
|
|
34
41
|
/**
|
|
35
42
|
* 발행 판정. **저장된 정본에 대한 것**이라 저장할 때마다 다시 묻는다(`figureVersion` 이 아니라
|
|
36
43
|
* `dirty` 가 풀리는 순간이 그때다).
|
|
@@ -56,6 +63,8 @@ export declare class FigureHistoryPanel extends FigureHistoryPanel_base {
|
|
|
56
63
|
private previewFix;
|
|
57
64
|
/** 서버가 전수 재검사해 모든 차단 split이 사라진 후보만 작업 사본에 원클릭 적용한다. 저장은 하지 않는다. */
|
|
58
65
|
private applyFix;
|
|
66
|
+
/** Apply the server-remeasured coordinated repair to the draft only; save remains explicit. */
|
|
67
|
+
private applyFixPlan;
|
|
59
68
|
/**
|
|
60
69
|
* 넘친 것을 담는 길 하나를 여기서도 내민다 — **같은 동작이 속성 패널에도 있다**(「부품에 맞추기」).
|
|
61
70
|
*
|
|
@@ -70,6 +79,10 @@ export declare class FigureHistoryPanel extends FigureHistoryPanel_base {
|
|
|
70
79
|
* 고른 것 하나만 옮기고 **여기서는 닿지 않는다.** 막혔다는 말을 읽은 자리에서 조치가 되게 한다.
|
|
71
80
|
*/
|
|
72
81
|
private settleOnFloor;
|
|
82
|
+
/** 기준 상자의 X·Z만 직전 발행값으로 되돌린다. Y와 부품 편집은 보존한다. */
|
|
83
|
+
private restoreReleasedFootprint;
|
|
84
|
+
/** 현재 외형으로 발행하겠다는 명시적 선택이다. 저장·발행은 이어서 따로 한다. */
|
|
85
|
+
private keepChangedFootprint;
|
|
73
86
|
private renderVersion;
|
|
74
87
|
private release;
|
|
75
88
|
private revert;
|