@things-factory/figure-ui 10.1.27 → 10.1.29
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 +23 -0
- package/client/modeller/angle-dial.ts +70 -0
- package/client/modeller/base-box.ts +104 -0
- package/client/modeller/figure-animations.ts +90 -17
- package/client/modeller/figure-canvas.ts +24 -6
- package/client/modeller/figure-inspector.ts +169 -5
- package/client/modeller/figure-preview.ts +110 -3
- package/client/modeller/figure-report.ts +20 -32
- package/client/modeller/figure-side.ts +10 -1
- package/client/modeller/figure-source.ts +8 -0
- package/client/modeller/joint-edits.ts +66 -5
- package/client/modeller/joint-gizmo.ts +180 -0
- package/client/modeller/part-edits.ts +75 -9
- package/client/modeller/pose-defaults.ts +67 -0
- package/client/modeller/proposal.ts +9 -3
- package/client/modeller/scene-budget.ts +31 -40
- package/client/pages/figure-list-page.ts +4 -9
- package/client/pages/figure-modeller-page.ts +144 -43
- package/client/types.ts +2 -0
- package/dist-client/graphql/index.d.ts +7 -0
- package/dist-client/graphql/index.js +21 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/angle-dial.d.ts +32 -0
- package/dist-client/modeller/angle-dial.js +41 -0
- package/dist-client/modeller/angle-dial.js.map +1 -0
- package/dist-client/modeller/base-box.d.ts +47 -0
- package/dist-client/modeller/base-box.js +52 -0
- package/dist-client/modeller/base-box.js.map +1 -0
- package/dist-client/modeller/figure-animations.d.ts +19 -4
- package/dist-client/modeller/figure-animations.js +86 -17
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-canvas.d.ts +9 -0
- package/dist-client/modeller/figure-canvas.js +20 -6
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +37 -0
- package/dist-client/modeller/figure-inspector.js +159 -5
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +26 -0
- package/dist-client/modeller/figure-preview.js +99 -3
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +11 -6
- package/dist-client/modeller/figure-report.js +20 -35
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +5 -1
- package/dist-client/modeller/figure-side.js +13 -0
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +7 -0
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/joint-edits.d.ts +23 -3
- package/dist-client/modeller/joint-edits.js +53 -5
- package/dist-client/modeller/joint-edits.js.map +1 -1
- package/dist-client/modeller/joint-gizmo.d.ts +34 -0
- package/dist-client/modeller/joint-gizmo.js +158 -0
- package/dist-client/modeller/joint-gizmo.js.map +1 -0
- package/dist-client/modeller/part-edits.d.ts +29 -4
- package/dist-client/modeller/part-edits.js +61 -9
- package/dist-client/modeller/part-edits.js.map +1 -1
- package/dist-client/modeller/pose-defaults.d.ts +9 -0
- package/dist-client/modeller/pose-defaults.js +35 -0
- package/dist-client/modeller/pose-defaults.js.map +1 -0
- package/dist-client/modeller/proposal.d.ts +1 -1
- package/dist-client/modeller/proposal.js +8 -3
- package/dist-client/modeller/proposal.js.map +1 -1
- package/dist-client/modeller/scene-budget.d.ts +31 -34
- package/dist-client/modeller/scene-budget.js +28 -37
- package/dist-client/modeller/scene-budget.js.map +1 -1
- package/dist-client/pages/figure-list-page.js +4 -9
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +50 -0
- package/dist-client/pages/figure-modeller-page.js +141 -42
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +2 -0
- package/dist-client/types.js.map +1 -1
- package/package.json +5 -5
- package/test/ai-proposal-contract.test.ts +16 -11
- package/test/angle-dial.test.ts +78 -0
- package/test/base-box.test.ts +165 -0
- package/test/expect.ts +2 -0
- package/test/i18n-prefix-guard.test.ts +17 -0
- package/test/joint-edits.test.ts +37 -2
- package/test/joint-gizmo.test.ts +174 -0
- package/test/joint-needle.test.ts +162 -0
- package/test/part-edits.test.ts +100 -0
- package/test/pose-defaults.test.ts +71 -0
- package/test/scene-budget.test.ts +82 -0
- package/translations/en.json +16 -9
- package/translations/ja.json +16 -9
- package/translations/ko.json +16 -9
- package/translations/ms.json +16 -9
- package/translations/zh.json +16 -9
|
@@ -12,10 +12,9 @@ import { customElement, state } from 'lit/decorators.js'
|
|
|
12
12
|
|
|
13
13
|
import { i18next, localize } from '@operato/i18n'
|
|
14
14
|
import { navigate, PageView } from '@operato/shell'
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import type { DetailLevel, FigureSource, PrimitiveKind } from '@hatiolab/figure-model'
|
|
15
|
+
import { consumePendingFigureProposal } from '../modeller/figure-ai-target.js'
|
|
16
|
+
import { contractChanges, isPlaceholderType, placeholderType, TYPE_LENGTH, TYPE_PATTERN, validate } from '@hatiolab/figure-model'
|
|
17
|
+
import type { ContractChange, DetailLevel, FigureSource, PrimitiveKind } from '@hatiolab/figure-model'
|
|
19
18
|
|
|
20
19
|
import * as edits from '../modeller/part-edits.js'
|
|
21
20
|
import * as proposals from '../modeller/proposal.js'
|
|
@@ -23,9 +22,12 @@ import * as proposalSessions from '../modeller/proposal-session.js'
|
|
|
23
22
|
import type { ProposalSession } from '../modeller/proposal-session.js'
|
|
24
23
|
import { toFigureSource, fromFigureSource } from '../modeller/figure-source.js'
|
|
25
24
|
import type { FigureDraft, PartModel, PlaneAxis } from '../modeller/figure-source.js'
|
|
25
|
+
import { jointOf } from '../modeller/joint-edits.js'
|
|
26
|
+
import { poseAsDefaults } from '../modeller/pose-defaults.js'
|
|
27
|
+
import { gizmoPart } from '../modeller/joint-gizmo.js'
|
|
26
28
|
import { DEFAULT_VIEW } from '../modeller/figure-view.js'
|
|
27
29
|
import type { ViewSettings } from '../modeller/figure-view.js'
|
|
28
|
-
import { createFigure, fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'
|
|
30
|
+
import { createFigure, fetchFigure, fetchFigureTypeNames, fetchReleasedSource, updateFigure } from '../graphql/index.js'
|
|
29
31
|
import type { Figure, FigureFinding, FigureGateFix, FigureProposal, ProposalFeedback } from '../types.js'
|
|
30
32
|
|
|
31
33
|
const FigureModellerPageBase = localize(i18next)(PageView) as typeof PageView
|
|
@@ -344,28 +346,6 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
344
346
|
opacity: 0.35;
|
|
345
347
|
cursor: default;
|
|
346
348
|
}
|
|
347
|
-
div[modes] button[ai-support] {
|
|
348
|
-
display: inline-flex;
|
|
349
|
-
align-items: center;
|
|
350
|
-
gap: 4px;
|
|
351
|
-
padding: 3px 10px;
|
|
352
|
-
margin-right: 6px;
|
|
353
|
-
font: var(--label-font, inherit);
|
|
354
|
-
font-size: 0.74rem;
|
|
355
|
-
font-weight: 600;
|
|
356
|
-
color: var(--md-sys-color-primary);
|
|
357
|
-
background: var(--md-sys-color-primary-container);
|
|
358
|
-
border: 1px solid var(--md-sys-color-outline-variant);
|
|
359
|
-
border-radius: 6px;
|
|
360
|
-
cursor: pointer;
|
|
361
|
-
}
|
|
362
|
-
div[modes] button[ai-support]:hover {
|
|
363
|
-
background-color: var(--md-sys-color-surface-container-high);
|
|
364
|
-
}
|
|
365
|
-
div[modes] button[ai-support] md-icon {
|
|
366
|
-
--md-icon-size: 15px;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
349
|
/*
|
|
370
350
|
후보가 오면 가운데를 둘로 나눈다.
|
|
371
351
|
|
|
@@ -695,9 +675,44 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
695
675
|
*/
|
|
696
676
|
private folded?: FigureSource
|
|
697
677
|
|
|
678
|
+
/**
|
|
679
|
+
* 직전 발행판의 정본. 한 번도 발행 안 한 도형에는 없다.
|
|
680
|
+
*
|
|
681
|
+
* 발행된 도형을 고치는 동안 「이 고침이 이미 놓인 것을 깨나」를 견주려면 이것이 있어야 한다.
|
|
682
|
+
* 저장 전 초안은 서버가 모르므로 판정도 여기서 한다 — 형식의 같은 함수를 쓴다(ADR-0063 결정 3).
|
|
683
|
+
*/
|
|
684
|
+
@state() private releasedSource?: FigureSource
|
|
685
|
+
|
|
686
|
+
/** 지금 고친 것이 놓인 것에 무엇을 하나. 발행 전에는 비어 있다. */
|
|
687
|
+
@state() private contract: ContractChange[] = []
|
|
688
|
+
|
|
689
|
+
/** 견주지 못한 까닭. **비어 있는 목록과 다르다** — 「깨지 않는다」가 아니라 「모른다」다. */
|
|
690
|
+
@state() private contractFailure?: string
|
|
691
|
+
|
|
698
692
|
willUpdate(changed: Map<string, unknown>) {
|
|
699
|
-
if (changed.has('draft') || changed.has('parts')) {
|
|
693
|
+
if (changed.has('draft') || changed.has('parts') || changed.has('releasedSource')) {
|
|
700
694
|
this.folded = this.draft ? toFigureSource(this.draft, this.parts) : undefined
|
|
695
|
+
this.contract = this.compareWithReleased()
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* 고치는 중의 정본은 **잠깐 형식을 못 지날 수 있다** — 부품 이름을 지웠다가 다시 적는 사이가
|
|
701
|
+
* 그렇다. 그때 견주기가 던지면 화면이 통째로 멈추므로 목록은 비운다.
|
|
702
|
+
*
|
|
703
|
+
* **빈 목록을 「깨지 않는다」로 보이게 두지 않는다.** 까닭을 함께 들고 있다가 화면이 「확인하지
|
|
704
|
+
* 못했다」로 말한다. 조용히 비우면 저작자는 안전하다고 읽는다 — 실제로 그런 일이 났다: 앱에
|
|
705
|
+
* 든 형식이 옛 판이라 견주는 함수 자체가 없었는데, 화면은 아무 말도 안 하고 조용했다.
|
|
706
|
+
*/
|
|
707
|
+
private compareWithReleased(): ContractChange[] {
|
|
708
|
+
this.contractFailure = undefined
|
|
709
|
+
if (!this.releasedSource || !this.folded) return []
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
return contractChanges(this.releasedSource, this.folded)
|
|
713
|
+
} catch (e) {
|
|
714
|
+
this.contractFailure = (e as Error).message
|
|
715
|
+
return []
|
|
701
716
|
}
|
|
702
717
|
}
|
|
703
718
|
|
|
@@ -913,6 +928,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
913
928
|
@set-all-parts-hidden=${(e: CustomEvent) => this.setAllHidden(e.detail.hidden)}
|
|
914
929
|
@change-detail-level=${(e: CustomEvent) => this.changeDetailLevel(e.detail.level)}
|
|
915
930
|
@update-draft=${(e: CustomEvent) => this.updateDraft(e.detail.draft)}
|
|
931
|
+
@resize-base=${(e: CustomEvent) => this.resizeBase(e.detail.draft, e.detail.parts)}
|
|
916
932
|
@part-tool=${(e: CustomEvent) => this.runTool(e.detail.index, e.detail.tool, e.detail.axis)}
|
|
917
933
|
@pick-part=${(e: CustomEvent) => this.pickPart(e.detail.name)}
|
|
918
934
|
>
|
|
@@ -955,12 +971,18 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
955
971
|
</div>
|
|
956
972
|
`
|
|
957
973
|
: this.mode === 'preview'
|
|
958
|
-
? html`<figure-preview
|
|
974
|
+
? html`<figure-preview
|
|
975
|
+
.source=${source}
|
|
976
|
+
.draft=${this.draft}
|
|
977
|
+
.view=${this.view}
|
|
978
|
+
@pose-as-default=${(e: CustomEvent) => this.poseAsDefault(e.detail.values)}
|
|
979
|
+
></figure-preview>`
|
|
959
980
|
: html`<figure-canvas
|
|
960
981
|
.figureId=${this.figure?.id || this.figure?.type}
|
|
961
982
|
.draft=${this.draft}
|
|
962
983
|
.parts=${this.parts}
|
|
963
984
|
.picked=${this.pickedName}
|
|
985
|
+
.gizmo=${this.jointGizmo}
|
|
964
986
|
.view=${this.view}
|
|
965
987
|
.revision=${this.sceneRevision}
|
|
966
988
|
></figure-canvas>`}
|
|
@@ -977,6 +999,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
977
999
|
.draft=${this.draft}
|
|
978
1000
|
.parts=${this.parts}
|
|
979
1001
|
.source=${source}
|
|
1002
|
+
.contract=${this.contract}
|
|
1003
|
+
.contractFailure=${this.contractFailure}
|
|
980
1004
|
.selected=${this.selected}
|
|
981
1005
|
.view=${this.view}
|
|
982
1006
|
@update-view=${(e: CustomEvent) => (this.view = e.detail.view)}
|
|
@@ -1039,17 +1063,12 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1039
1063
|
`
|
|
1040
1064
|
: ''}
|
|
1041
1065
|
<div spacer></div>
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
}}
|
|
1049
|
-
>
|
|
1050
|
-
<md-icon>auto_awesome</md-icon>
|
|
1051
|
-
<span>AI</span>
|
|
1052
|
-
</button>
|
|
1066
|
+
<!--
|
|
1067
|
+
AI 를 여는 단추는 여기 두지 않는다. 앱 머리줄에 이미 있고 같은 dock 을 연다 — 한 화면에
|
|
1068
|
+
같은 곳으로 가는 문이 둘이면 사람이 둘의 차이를 찾는다. 열린 도형을 dock 에 넘기는 일은
|
|
1069
|
+
단추가 하던 것이 아니다: dock 이 열릴 때 figure-ai-context-request 로 묻고 이 화면이
|
|
1070
|
+
저장 전 정본 그대로 답한다.
|
|
1071
|
+
-->
|
|
1053
1072
|
${this.saveFailure ? html`<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
|
|
1054
1073
|
${this.dirty ? html`<span dirty>${i18next.t('figure.text.unsaved-changes')}</span>` : ''}
|
|
1055
1074
|
<button save ?disabled=${!this.canSave} @click=${() => this.save()}>
|
|
@@ -1199,7 +1218,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1199
1218
|
styleKit: 'palette',
|
|
1200
1219
|
animations: 'animation',
|
|
1201
1220
|
parameters: 'tune',
|
|
1202
|
-
capabilities: 'hub'
|
|
1221
|
+
capabilities: 'hub',
|
|
1222
|
+
joints: 'settings_input_component'
|
|
1203
1223
|
} satisfies Record<proposals.ChangeKind, string>)[change.kind]
|
|
1204
1224
|
|
|
1205
1225
|
const on = this.proposalSession?.picked.has(change.key)
|
|
@@ -1422,6 +1442,15 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1422
1442
|
this.draft = model?.draft
|
|
1423
1443
|
this.parts = model?.parts ?? []
|
|
1424
1444
|
|
|
1445
|
+
/*
|
|
1446
|
+
발행된 적이 있으면 그 판을 가져다 둔다 — 고치는 동안 견줄 상대다. 기다리지 않는다:
|
|
1447
|
+
도형은 이미 화면에 있고, 판정은 도착하는 대로 붙는다.
|
|
1448
|
+
*/
|
|
1449
|
+
this.releasedSource = undefined
|
|
1450
|
+
if (figure.id && (figure.version ?? 0) > 0) {
|
|
1451
|
+
void this.loadReleased(figure.id, generation)
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1425
1454
|
/*
|
|
1426
1455
|
**아무것도 고르지 않은 채로 연다.**
|
|
1427
1456
|
|
|
@@ -1441,6 +1470,17 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1441
1470
|
this.lastCoalesceKey = undefined
|
|
1442
1471
|
}
|
|
1443
1472
|
|
|
1473
|
+
/** 직전 발행판을 가져다 둔다. 못 읽으면 견주지 않는다 — 없는 판정을 지어내지 않는다. */
|
|
1474
|
+
private async loadReleased(id: string, generation: number) {
|
|
1475
|
+
try {
|
|
1476
|
+
const source = await fetchReleasedSource(id)
|
|
1477
|
+
if (generation !== this.loadGeneration || !this.active) return
|
|
1478
|
+
this.releasedSource = source ? (JSON.parse(source) as FigureSource) : undefined
|
|
1479
|
+
} catch (e) {
|
|
1480
|
+
console.warn(`[figure] 직전 발행판을 읽지 못했습니다 — ${(e as Error).message}`)
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1444
1484
|
private startNew() {
|
|
1445
1485
|
++this.loadGeneration
|
|
1446
1486
|
this.proposalSession = undefined
|
|
@@ -1470,8 +1510,15 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1470
1510
|
this.dirty = true
|
|
1471
1511
|
}
|
|
1472
1512
|
|
|
1513
|
+
/**
|
|
1514
|
+
* 타입을 고친다 — **발행 전까지만**(ADR-0063 결정 4).
|
|
1515
|
+
*
|
|
1516
|
+
* 전에는 저장된 도형이면 무조건 돌아섰다. 그래서 기계가 지은 이름이 그대로 굳었다. 발행된
|
|
1517
|
+
* 뒤에는 문이 거절하므로 여기서도 막는다 — 눌러서 되는 것처럼 보이고 저장에서 실패하면
|
|
1518
|
+
* 저작자가 무엇을 잘못했는지 못 읽는다.
|
|
1519
|
+
*/
|
|
1473
1520
|
private typeChanged(type: string) {
|
|
1474
|
-
if (!type || this.figure?.
|
|
1521
|
+
if (!type || (this.figure?.version ?? 0) > 0) return
|
|
1475
1522
|
const sanitized = type.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()
|
|
1476
1523
|
if (this.draft) this.draft.figureType = sanitized
|
|
1477
1524
|
this.figure = { ...this.figure, type: sanitized } as Figure
|
|
@@ -1555,8 +1602,13 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1555
1602
|
|
|
1556
1603
|
private addPart(primitive: PrimitiveKind) {
|
|
1557
1604
|
if (!this.draft) return
|
|
1605
|
+
/*
|
|
1606
|
+
고른 부품 위에 얹는다 — 그 자리에서 보이고, 관절을 걸면 맞닿는 면이 회전 중심이 된다.
|
|
1607
|
+
고른 것이 없으면 `newPart` 가 바닥 한가운데로 놓는다.
|
|
1608
|
+
*/
|
|
1609
|
+
const onTopOf = this.parts[this.selected]
|
|
1558
1610
|
// 추가한 것을 바로 수정할 수 있게 선택 상태로 둔다.
|
|
1559
|
-
this.put(edits.addPart(this.draft, this.parts, primitive), this.parts.length)
|
|
1611
|
+
this.put(edits.addPart(this.draft, this.parts, primitive, onTopOf), this.parts.length)
|
|
1560
1612
|
}
|
|
1561
1613
|
|
|
1562
1614
|
private removePart(index: number) {
|
|
@@ -1581,6 +1633,33 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1581
1633
|
* 부품 목록은 건드리지 않는다. `put` 은 부품을 함께 받으므로 여기서 쓰면 능력 하나를
|
|
1582
1634
|
* 고칠 때마다 부품 목록이 새 배열이 되고, 그리는 쪽이 전부 다시 그린다.
|
|
1583
1635
|
*/
|
|
1636
|
+
/**
|
|
1637
|
+
* 미리보기에서 세운 자세를 파라미터의 기본값으로 앉힌다.
|
|
1638
|
+
*
|
|
1639
|
+
* 관절이 있는 도형은 값이 0 인 자세(휴지 자세)가 곧게 선 기둥이라, 기본값이 없으면 목록과
|
|
1640
|
+
* 도면에서 그 모양으로 선다. 저작자가 미리보기에서 맞춘 자세가 곧 그 답이다.
|
|
1641
|
+
*/
|
|
1642
|
+
private poseAsDefault(values: Record<string, number>) {
|
|
1643
|
+
if (!this.draft) return
|
|
1644
|
+
|
|
1645
|
+
const next = poseAsDefaults(this.draft, values)
|
|
1646
|
+
/* 앉힐 것이 없으면 초안이 그대로 온다. 그때 되돌리기 칸과 「저장 안 됨」을 만들지 않는다. */
|
|
1647
|
+
if (next !== this.draft) this.updateDraft(next)
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* 기준 상자를 고친다 — 초안과 부품이 **한 걸음으로** 바뀐다.
|
|
1652
|
+
*
|
|
1653
|
+
* 상자가 커지면 한가운데가 옮겨 가므로 부품을 그만큼 되밀어야 저장되는 자리가 유지된다
|
|
1654
|
+
* (`base-box.ts`). 그 둘을 따로 기록하면 한 번 되돌렸을 때 부품이 반쯤 밀린 화면이 남는다.
|
|
1655
|
+
*/
|
|
1656
|
+
private resizeBase(draft: FigureDraft, parts: PartModel[]) {
|
|
1657
|
+
this.recordHistory('base')
|
|
1658
|
+
this.draft = draft
|
|
1659
|
+
this.parts = parts
|
|
1660
|
+
this.dirty = true
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1584
1663
|
private updateDraft(draft: FigureDraft) {
|
|
1585
1664
|
this.recordHistory('draft')
|
|
1586
1665
|
this.draft = draft
|
|
@@ -1670,6 +1749,22 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1670
1749
|
return this.parts[this.selected]?.name
|
|
1671
1750
|
}
|
|
1672
1751
|
|
|
1752
|
+
/**
|
|
1753
|
+
* 고른 부품에 관절이 있으면 **그 관절이 도는 자리**를 저작면에 함께 그린다(`joint-gizmo.ts`).
|
|
1754
|
+
*
|
|
1755
|
+
* 축을 고르는 칸은 이 화면에 있는데 도는 모습은 미리보기에만 있었다. 고르는 자리와 보이는
|
|
1756
|
+
* 자리가 갈리면 사람은 왕복하며 맞춘다 — 여섯 축짜리 로봇이면 여섯 번이다.
|
|
1757
|
+
*/
|
|
1758
|
+
private get jointGizmo(): PartModel | undefined {
|
|
1759
|
+
const name = this.pickedName
|
|
1760
|
+
if (!name || !this.draft) return undefined
|
|
1761
|
+
|
|
1762
|
+
const joint = jointOf(this.draft, name)
|
|
1763
|
+
if (!joint) return undefined
|
|
1764
|
+
|
|
1765
|
+
return gizmoPart(joint, this.parts.find(part => part.name === name), this.draft)
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1673
1768
|
private pickPart(name: string) {
|
|
1674
1769
|
const at = this.parts.findIndex(part => part.name === name)
|
|
1675
1770
|
if (at >= 0) this.selected = at
|
|
@@ -1803,6 +1898,12 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1803
1898
|
const thumbnail = await captureThumbnail(this.draft, this.parts)
|
|
1804
1899
|
|
|
1805
1900
|
const result = await updateFigure(this.figure.id, {
|
|
1901
|
+
/*
|
|
1902
|
+
타입을 함께 보낸다. 정본 안의 `type` 만 고치고 행을 안 고치면 저장된 JSON 과 행이
|
|
1903
|
+
다른 이름을 말하고, 발행할 때 어느 쪽이 도면에 적히는지가 갈린다. 발행된 뒤라면
|
|
1904
|
+
문이 거절하고 그 사유가 화면에 그대로 뜬다.
|
|
1905
|
+
*/
|
|
1906
|
+
type: this.figure.type,
|
|
1806
1907
|
name: this.figure.name,
|
|
1807
1908
|
category: this.figure.category,
|
|
1808
1909
|
tags: this.figure.tags,
|
package/client/types.ts
CHANGED
|
@@ -67,4 +67,11 @@ export declare function revertFigureVersion(id: string, version: number): Promis
|
|
|
67
67
|
*/
|
|
68
68
|
export declare function inspectFigure(id: string): Promise<FigureInspection>;
|
|
69
69
|
/** 발행된 판들 — 최근 것부터 열 개. */
|
|
70
|
+
/**
|
|
71
|
+
* 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.
|
|
72
|
+
*
|
|
73
|
+
* 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장
|
|
74
|
+
* 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.
|
|
75
|
+
*/
|
|
76
|
+
export declare function fetchReleasedSource(id: string): Promise<string | undefined>;
|
|
70
77
|
export declare function fetchFigureVersions(id: string): Promise<FigureVersion[]>;
|
|
@@ -267,6 +267,27 @@ export async function inspectFigure(id) {
|
|
|
267
267
|
return unwrap(response, 'inspectFigure');
|
|
268
268
|
}
|
|
269
269
|
/** 발행된 판들 — 최근 것부터 열 개. */
|
|
270
|
+
/**
|
|
271
|
+
* 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.
|
|
272
|
+
*
|
|
273
|
+
* 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장
|
|
274
|
+
* 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.
|
|
275
|
+
*/
|
|
276
|
+
export async function fetchReleasedSource(id) {
|
|
277
|
+
const response = await client.query({
|
|
278
|
+
query: gql `
|
|
279
|
+
query FigureReleased($id: String!) {
|
|
280
|
+
figureReleased(id: $id) {
|
|
281
|
+
version
|
|
282
|
+
source
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
`,
|
|
286
|
+
variables: { id },
|
|
287
|
+
fetchPolicy: 'network-only'
|
|
288
|
+
});
|
|
289
|
+
return unwrap(response, 'figureReleased')?.source ?? undefined;
|
|
290
|
+
}
|
|
270
291
|
export async function fetchFigureVersions(id) {
|
|
271
292
|
const response = await client.query({
|
|
272
293
|
query: gql `
|
|
@@ -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/** 발행된 판들 — 최근 것부터 열 개. */\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 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"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 막대 옆에 서는 **각도 눈**. 지금 각이 범위의 어디인지를 한 눈에 보인다.
|
|
3
|
+
*
|
|
4
|
+
* ## 왜 3D 로는 모자랐나
|
|
5
|
+
*
|
|
6
|
+
* 관절이 돌아도 화면이 그대로인 경우가 있다 — 제 축에서 대칭인 부품이 그렇다. 그래서 관절이
|
|
7
|
+
* 모는 부품에 바늘을 달아 미리보기에서 함께 돌게 했는데(`joint-gizmo.ts` 의 `jointNeedles`),
|
|
8
|
+
* **미리보기에서는 그 바늘이 4 × 2 픽셀이다**(잰 값: 부품 100mm · 바늘 60 × 12mm · 여덟 대를
|
|
9
|
+
* 한 화면에 세운 상태). 미리보기는 크기 반응을 보려고 여덟 대를 나란히 놓는 자리라 한 대가
|
|
10
|
+
* 20 픽셀쯤이고, 그 위의 어떤 표시도 몇 픽셀을 넘지 못한다.
|
|
11
|
+
*
|
|
12
|
+
* 각을 읽는 일은 3D 의 배율에서 떼어 놓는다. 눈은 막대 옆 24 픽셀 자리에 서고, 도형이 아무리
|
|
13
|
+
* 작게 서 있어도 같은 크기로 읽힌다.
|
|
14
|
+
*/
|
|
15
|
+
export interface DialShape {
|
|
16
|
+
/** 가동 범위를 채운 부채꼴. SVG path 의 `d`. */
|
|
17
|
+
sector: string;
|
|
18
|
+
/** 지금 값이 가리키는 끝점. */
|
|
19
|
+
needle: {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
};
|
|
23
|
+
centre: {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
};
|
|
27
|
+
radius: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 눈의 모양. 값이 범위를 벗어나면 범위 끝에 세운다 — 밖을 가리키는 바늘은 없는 값을 말한다.
|
|
31
|
+
*/
|
|
32
|
+
export declare function dialShape(min: number, max: number, value: number, radius?: number): DialShape;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/** 0 도가 위를 가리키고, 값이 커지면 시계 방향으로 돈다 — 화면의 각과 같은 방향이다. */
|
|
5
|
+
function at(centre, radius, degrees) {
|
|
6
|
+
const rad = (degrees * Math.PI) / 180;
|
|
7
|
+
return {
|
|
8
|
+
x: +(centre.x + radius * Math.sin(rad)).toFixed(2),
|
|
9
|
+
y: +(centre.y - radius * Math.cos(rad)).toFixed(2)
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** 한 바퀴에 가까우면 부채꼴 대신 온전한 원이다. */
|
|
13
|
+
const FULL = 359.9;
|
|
14
|
+
/**
|
|
15
|
+
* 눈의 모양. 값이 범위를 벗어나면 범위 끝에 세운다 — 밖을 가리키는 바늘은 없는 값을 말한다.
|
|
16
|
+
*/
|
|
17
|
+
export function dialShape(min, max, value, radius = 9) {
|
|
18
|
+
const centre = { x: radius + 1, y: radius + 1 };
|
|
19
|
+
const span = Math.abs(max - min);
|
|
20
|
+
const held = Math.min(Math.max(value, Math.min(min, max)), Math.max(min, max));
|
|
21
|
+
if (span >= FULL) {
|
|
22
|
+
const top = at(centre, radius, 0);
|
|
23
|
+
const bottom = at(centre, radius, 180);
|
|
24
|
+
return {
|
|
25
|
+
sector: `M ${top.x} ${top.y} A ${radius} ${radius} 0 1 1 ${bottom.x} ${bottom.y} A ${radius} ${radius} 0 1 1 ${top.x} ${top.y} Z`,
|
|
26
|
+
needle: at(centre, radius, held),
|
|
27
|
+
centre,
|
|
28
|
+
radius
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const from = at(centre, radius, min);
|
|
32
|
+
const to = at(centre, radius, max);
|
|
33
|
+
const large = span > 180 ? 1 : 0;
|
|
34
|
+
return {
|
|
35
|
+
sector: `M ${centre.x} ${centre.y} L ${from.x} ${from.y} A ${radius} ${radius} 0 ${large} 1 ${to.x} ${to.y} Z`,
|
|
36
|
+
needle: at(centre, radius, held),
|
|
37
|
+
centre,
|
|
38
|
+
radius
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=angle-dial.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"angle-dial.js","sourceRoot":"","sources":["../../client/modeller/angle-dial.ts"],"names":[],"mappings":"AAAA;;GAEG;AA0BH,wDAAwD;AACxD,SAAS,EAAE,CAAC,MAAgC,EAAE,MAAc,EAAE,OAAe;IAC3E,MAAM,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAA;IACrC,OAAO;QACL,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QAClD,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;KACnD,CAAA;AACH,CAAC;AAED,iCAAiC;AACjC,MAAM,IAAI,GAAG,KAAK,CAAA;AAElB;;GAEG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW,EAAE,GAAW,EAAE,KAAa,EAAE,MAAM,GAAG,CAAC;IAC3E,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,CAAA;IAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,CAAA;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;IAE9E,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QACjB,MAAM,GAAG,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;QACjC,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;QACtC,OAAO;YACL,MAAM,EAAE,KAAK,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,MAAM,MAAM,IAAI,MAAM,UAAU,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,MAAM,IAAI,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI;YACjI,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC;YAChC,MAAM;YACN,MAAM;SACP,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;IAClC,MAAM,KAAK,GAAG,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAEhC,OAAO;QACL,MAAM,EAAE,KAAK,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,MAAM,IAAI,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI;QAC9G,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC;QAChC,MAAM;QACN,MAAM;KACP,CAAA;AACH,CAAC","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\n\n/**\n * 막대 옆에 서는 **각도 눈**. 지금 각이 범위의 어디인지를 한 눈에 보인다.\n *\n * ## 왜 3D 로는 모자랐나\n *\n * 관절이 돌아도 화면이 그대로인 경우가 있다 — 제 축에서 대칭인 부품이 그렇다. 그래서 관절이\n * 모는 부품에 바늘을 달아 미리보기에서 함께 돌게 했는데(`joint-gizmo.ts` 의 `jointNeedles`),\n * **미리보기에서는 그 바늘이 4 × 2 픽셀이다**(잰 값: 부품 100mm · 바늘 60 × 12mm · 여덟 대를\n * 한 화면에 세운 상태). 미리보기는 크기 반응을 보려고 여덟 대를 나란히 놓는 자리라 한 대가\n * 20 픽셀쯤이고, 그 위의 어떤 표시도 몇 픽셀을 넘지 못한다.\n *\n * 각을 읽는 일은 3D 의 배율에서 떼어 놓는다. 눈은 막대 옆 24 픽셀 자리에 서고, 도형이 아무리\n * 작게 서 있어도 같은 크기로 읽힌다.\n */\n\nexport interface DialShape {\n /** 가동 범위를 채운 부채꼴. SVG path 의 `d`. */\n sector: string\n /** 지금 값이 가리키는 끝점. */\n needle: { x: number; y: number }\n centre: { x: number; y: number }\n radius: number\n}\n\n/** 0 도가 위를 가리키고, 값이 커지면 시계 방향으로 돈다 — 화면의 각과 같은 방향이다. */\nfunction at(centre: { x: number; y: number }, radius: number, degrees: number) {\n const rad = (degrees * Math.PI) / 180\n return {\n x: +(centre.x + radius * Math.sin(rad)).toFixed(2),\n y: +(centre.y - radius * Math.cos(rad)).toFixed(2)\n }\n}\n\n/** 한 바퀴에 가까우면 부채꼴 대신 온전한 원이다. */\nconst FULL = 359.9\n\n/**\n * 눈의 모양. 값이 범위를 벗어나면 범위 끝에 세운다 — 밖을 가리키는 바늘은 없는 값을 말한다.\n */\nexport function dialShape(min: number, max: number, value: number, radius = 9): DialShape {\n const centre = { x: radius + 1, y: radius + 1 }\n const span = Math.abs(max - min)\n const held = Math.min(Math.max(value, Math.min(min, max)), Math.max(min, max))\n\n if (span >= FULL) {\n const top = at(centre, radius, 0)\n const bottom = at(centre, radius, 180)\n return {\n sector: `M ${top.x} ${top.y} A ${radius} ${radius} 0 1 1 ${bottom.x} ${bottom.y} A ${radius} ${radius} 0 1 1 ${top.x} ${top.y} Z`,\n needle: at(centre, radius, held),\n centre,\n radius\n }\n }\n\n const from = at(centre, radius, min)\n const to = at(centre, radius, max)\n const large = span > 180 ? 1 : 0\n\n return {\n sector: `M ${centre.x} ${centre.y} L ${from.x} ${from.y} A ${radius} ${radius} 0 ${large} 1 ${to.x} ${to.y} Z`,\n needle: at(centre, radius, held),\n centre,\n radius\n }\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { FigureDraft, PartModel } from './figure-source.js';
|
|
2
|
+
/**
|
|
3
|
+
* 기준 상자를 고친다 — **부품은 제자리에 둔 채.**
|
|
4
|
+
*
|
|
5
|
+
* ## 왜 화면에 있어야 하나
|
|
6
|
+
*
|
|
7
|
+
* 기준 상자는 도면에 놓일 때의 배율을 정하는 수다(ADR-0044 ①). 부품이 그 상자를 넘으면 발행이
|
|
8
|
+
* 막히고, 고치는 길은 「상자 숫자 하나를 키우는 것」인데 **화면에 그 칸이 없었다.** 저작자는
|
|
9
|
+
* 넘쳤다는 말만 듣고 고칠 자리를 못 찾았다.
|
|
10
|
+
*
|
|
11
|
+
* ## 상자를 키우면 부품이 움직인다 — 그것을 되돌린다
|
|
12
|
+
*
|
|
13
|
+
* 저작면의 부품 자리는 판의 왼쪽 위에서 잰 값이고, 저장 형식의 자리는 **상자의 한가운데**에서 잰
|
|
14
|
+
* 값이다(`toFigureSource` 가 절반을 뺀다). 그래서 상자만 키우면 한가운데가 옮겨 가고 부품이 전부
|
|
15
|
+
* 반대로 밀린다. 여기서 늘어난 절반만큼 부품을 함께 옮겨, 저장되는 자리가 한 값도 안 바뀌게 한다.
|
|
16
|
+
*
|
|
17
|
+
* ## 「부품에 맞추기」는 한가운데를 지킨다
|
|
18
|
+
*
|
|
19
|
+
* 부품을 감싸는 가장 작은 상자를 만들 때, 부품 쪽을 **옮기지 않는다.** 한가운데에서 가장 먼
|
|
20
|
+
* 부품까지의 거리를 양쪽으로 잡는다. 부품이 한쪽으로 치우쳐 있으면 반대쪽에 여유가 남는데,
|
|
21
|
+
* 그것이 사실이다 — 치우친 것을 가운데로 끌어오면 저작자가 놓은 자리가 소리 없이 달라지고,
|
|
22
|
+
* 발행된 도형에서는 문(port)의 자리가 함께 움직인다.
|
|
23
|
+
*
|
|
24
|
+
* 높이는 밑면이 바닥이라 한쪽만 늘린다. 바닥 아래로 내려간 부품이 있으면 그 부품은 상자 밖에
|
|
25
|
+
* 남는다 — 그것을 담으려면 부품을 올려야 하고, 그것은 저작자가 정할 일이다. 발행 관문이 이미
|
|
26
|
+
* `part-outside-base` 로 말한다.
|
|
27
|
+
*/
|
|
28
|
+
export interface BaseSize {
|
|
29
|
+
/** 저작면의 가로 — 형식의 `base.x`. */
|
|
30
|
+
width: number;
|
|
31
|
+
/** 저작면의 세로 — 형식의 `base.z`. */
|
|
32
|
+
height: number;
|
|
33
|
+
/** 높이 — 형식의 `base.y`. */
|
|
34
|
+
depth: number;
|
|
35
|
+
}
|
|
36
|
+
export declare const baseOf: (draft: FigureDraft) => BaseSize;
|
|
37
|
+
/** 부품 전부를 담는 가장 작은 상자. 부품이 없으면 잴 것이 없다. */
|
|
38
|
+
export declare function baseFittingParts(draft: FigureDraft, parts: readonly PartModel[]): BaseSize | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* 상자를 이 크기로. 부품은 저장되는 자리가 그대로이도록 함께 옮긴다.
|
|
41
|
+
*
|
|
42
|
+
* 달라질 것이 없으면 **받은 것을 그대로 돌려준다** — 고친 것이 없는데 「저장 안 됨」이 붙지 않게.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resizeBase(draft: FigureDraft, parts: readonly PartModel[], next: BaseSize): {
|
|
45
|
+
draft: FigureDraft;
|
|
46
|
+
parts: readonly PartModel[];
|
|
47
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/** 상자는 밀리미터 하나보다 작아질 수 없다. 0 이 되면 배율이 0 으로 나뉜다. */
|
|
5
|
+
const LEAST = 1;
|
|
6
|
+
const round = (value) => Math.round(value);
|
|
7
|
+
export const baseOf = (draft) => ({
|
|
8
|
+
width: draft.width,
|
|
9
|
+
height: draft.height,
|
|
10
|
+
depth: draft.depth
|
|
11
|
+
});
|
|
12
|
+
/** 부품 전부를 담는 가장 작은 상자. 부품이 없으면 잴 것이 없다. */
|
|
13
|
+
export function baseFittingParts(draft, parts) {
|
|
14
|
+
if (parts.length === 0)
|
|
15
|
+
return undefined;
|
|
16
|
+
const middle = { x: draft.width / 2, y: draft.height / 2 };
|
|
17
|
+
let half = { x: 0, y: 0 };
|
|
18
|
+
let tallest = 0;
|
|
19
|
+
for (const part of parts) {
|
|
20
|
+
half = {
|
|
21
|
+
x: Math.max(half.x, Math.abs(part.left - middle.x), Math.abs(part.left + part.width - middle.x)),
|
|
22
|
+
y: Math.max(half.y, Math.abs(part.top - middle.y), Math.abs(part.top + part.height - middle.y))
|
|
23
|
+
};
|
|
24
|
+
tallest = Math.max(tallest, part.zPos + part.depth);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
width: Math.max(LEAST, round(half.x * 2)),
|
|
28
|
+
height: Math.max(LEAST, round(half.y * 2)),
|
|
29
|
+
depth: Math.max(LEAST, round(tallest))
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 상자를 이 크기로. 부품은 저장되는 자리가 그대로이도록 함께 옮긴다.
|
|
34
|
+
*
|
|
35
|
+
* 달라질 것이 없으면 **받은 것을 그대로 돌려준다** — 고친 것이 없는데 「저장 안 됨」이 붙지 않게.
|
|
36
|
+
*/
|
|
37
|
+
export function resizeBase(draft, parts, next) {
|
|
38
|
+
const size = {
|
|
39
|
+
width: Math.max(LEAST, round(next.width)),
|
|
40
|
+
height: Math.max(LEAST, round(next.height)),
|
|
41
|
+
depth: Math.max(LEAST, round(next.depth))
|
|
42
|
+
};
|
|
43
|
+
if (size.width === draft.width && size.height === draft.height && size.depth === draft.depth) {
|
|
44
|
+
return { draft, parts };
|
|
45
|
+
}
|
|
46
|
+
const shift = { x: (size.width - draft.width) / 2, y: (size.height - draft.height) / 2 };
|
|
47
|
+
const moved = shift.x === 0 && shift.y === 0
|
|
48
|
+
? parts
|
|
49
|
+
: parts.map(part => ({ ...part, left: part.left + shift.x, top: part.top + shift.y }));
|
|
50
|
+
return { draft: { ...draft, ...size }, parts: moved };
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=base-box.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-box.js","sourceRoot":"","sources":["../../client/modeller/base-box.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport type { FigureDraft, PartModel } from './figure-source.js'\n\n/**\n * 기준 상자를 고친다 — **부품은 제자리에 둔 채.**\n *\n * ## 왜 화면에 있어야 하나\n *\n * 기준 상자는 도면에 놓일 때의 배율을 정하는 수다(ADR-0044 ①). 부품이 그 상자를 넘으면 발행이\n * 막히고, 고치는 길은 「상자 숫자 하나를 키우는 것」인데 **화면에 그 칸이 없었다.** 저작자는\n * 넘쳤다는 말만 듣고 고칠 자리를 못 찾았다.\n *\n * ## 상자를 키우면 부품이 움직인다 — 그것을 되돌린다\n *\n * 저작면의 부품 자리는 판의 왼쪽 위에서 잰 값이고, 저장 형식의 자리는 **상자의 한가운데**에서 잰\n * 값이다(`toFigureSource` 가 절반을 뺀다). 그래서 상자만 키우면 한가운데가 옮겨 가고 부품이 전부\n * 반대로 밀린다. 여기서 늘어난 절반만큼 부품을 함께 옮겨, 저장되는 자리가 한 값도 안 바뀌게 한다.\n *\n * ## 「부품에 맞추기」는 한가운데를 지킨다\n *\n * 부품을 감싸는 가장 작은 상자를 만들 때, 부품 쪽을 **옮기지 않는다.** 한가운데에서 가장 먼\n * 부품까지의 거리를 양쪽으로 잡는다. 부품이 한쪽으로 치우쳐 있으면 반대쪽에 여유가 남는데,\n * 그것이 사실이다 — 치우친 것을 가운데로 끌어오면 저작자가 놓은 자리가 소리 없이 달라지고,\n * 발행된 도형에서는 문(port)의 자리가 함께 움직인다.\n *\n * 높이는 밑면이 바닥이라 한쪽만 늘린다. 바닥 아래로 내려간 부품이 있으면 그 부품은 상자 밖에\n * 남는다 — 그것을 담으려면 부품을 올려야 하고, 그것은 저작자가 정할 일이다. 발행 관문이 이미\n * `part-outside-base` 로 말한다.\n */\n\nexport interface BaseSize {\n /** 저작면의 가로 — 형식의 `base.x`. */\n width: number\n /** 저작면의 세로 — 형식의 `base.z`. */\n height: number\n /** 높이 — 형식의 `base.y`. */\n depth: number\n}\n\n/** 상자는 밀리미터 하나보다 작아질 수 없다. 0 이 되면 배율이 0 으로 나뉜다. */\nconst LEAST = 1\n\nconst round = (value: number) => Math.round(value)\n\nexport const baseOf = (draft: FigureDraft): BaseSize => ({\n width: draft.width,\n height: draft.height,\n depth: draft.depth\n})\n\n/** 부품 전부를 담는 가장 작은 상자. 부품이 없으면 잴 것이 없다. */\nexport function baseFittingParts(draft: FigureDraft, parts: readonly PartModel[]): BaseSize | undefined {\n if (parts.length === 0) return undefined\n\n const middle = { x: draft.width / 2, y: draft.height / 2 }\n let half = { x: 0, y: 0 }\n let tallest = 0\n\n for (const part of parts) {\n half = {\n x: Math.max(half.x, Math.abs(part.left - middle.x), Math.abs(part.left + part.width - middle.x)),\n y: Math.max(half.y, Math.abs(part.top - middle.y), Math.abs(part.top + part.height - middle.y))\n }\n tallest = Math.max(tallest, part.zPos + part.depth)\n }\n\n return {\n width: Math.max(LEAST, round(half.x * 2)),\n height: Math.max(LEAST, round(half.y * 2)),\n depth: Math.max(LEAST, round(tallest))\n }\n}\n\n/**\n * 상자를 이 크기로. 부품은 저장되는 자리가 그대로이도록 함께 옮긴다.\n *\n * 달라질 것이 없으면 **받은 것을 그대로 돌려준다** — 고친 것이 없는데 「저장 안 됨」이 붙지 않게.\n */\nexport function resizeBase(\n draft: FigureDraft,\n parts: readonly PartModel[],\n next: BaseSize\n): { draft: FigureDraft; parts: readonly PartModel[] } {\n const size = {\n width: Math.max(LEAST, round(next.width)),\n height: Math.max(LEAST, round(next.height)),\n depth: Math.max(LEAST, round(next.depth))\n }\n\n if (size.width === draft.width && size.height === draft.height && size.depth === draft.depth) {\n return { draft, parts }\n }\n\n const shift = { x: (size.width - draft.width) / 2, y: (size.height - draft.height) / 2 }\n const moved =\n shift.x === 0 && shift.y === 0\n ? parts\n : parts.map(part => ({ ...part, left: part.left + shift.x, top: part.top + shift.y }))\n\n return { draft: { ...draft, ...size }, parts: moved }\n}\n"]}
|