@things-factory/figure-ui 10.1.27 → 10.1.28
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/base-box.ts +104 -0
- package/client/modeller/figure-animations.ts +81 -8
- package/client/modeller/figure-canvas.ts +18 -3
- package/client/modeller/figure-inspector.ts +151 -2
- package/client/modeller/figure-preview.ts +55 -0
- package/client/modeller/figure-report.ts +9 -4
- package/client/modeller/figure-side.ts +9 -1
- package/client/modeller/figure-source.ts +8 -0
- package/client/modeller/joint-edits.ts +66 -5
- package/client/modeller/joint-gizmo.ts +118 -0
- 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 +124 -41
- 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/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 +15 -0
- package/dist-client/modeller/figure-animations.js +77 -8
- 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 +14 -3
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +26 -0
- package/dist-client/modeller/figure-inspector.js +139 -2
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +15 -0
- package/dist-client/modeller/figure-preview.js +46 -0
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +3 -2
- package/dist-client/modeller/figure-report.js +9 -4
- 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 +12 -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 +31 -0
- package/dist-client/modeller/joint-gizmo.js +104 -0
- package/dist-client/modeller/joint-gizmo.js.map +1 -0
- 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 +43 -0
- package/dist-client/pages/figure-modeller-page.js +121 -40
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/test/ai-proposal-contract.test.ts +16 -11
- package/test/base-box.test.ts +154 -0
- package/test/joint-edits.test.ts +37 -2
- package/test/joint-gizmo.test.ts +174 -0
- package/test/pose-defaults.test.ts +71 -0
- package/test/scene-budget.test.ts +53 -0
- package/translations/en.json +13 -2
- package/translations/ja.json +13 -2
- package/translations/ko.json +13 -2
- package/translations/ms.json +13 -2
- package/translations/zh.json +13 -2
|
@@ -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
|
|
@@ -1581,6 +1621,33 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1581
1621
|
* 부품 목록은 건드리지 않는다. `put` 은 부품을 함께 받으므로 여기서 쓰면 능력 하나를
|
|
1582
1622
|
* 고칠 때마다 부품 목록이 새 배열이 되고, 그리는 쪽이 전부 다시 그린다.
|
|
1583
1623
|
*/
|
|
1624
|
+
/**
|
|
1625
|
+
* 미리보기에서 세운 자세를 파라미터의 기본값으로 앉힌다.
|
|
1626
|
+
*
|
|
1627
|
+
* 관절이 있는 도형은 값이 0 인 자세(휴지 자세)가 곧게 선 기둥이라, 기본값이 없으면 목록과
|
|
1628
|
+
* 도면에서 그 모양으로 선다. 저작자가 미리보기에서 맞춘 자세가 곧 그 답이다.
|
|
1629
|
+
*/
|
|
1630
|
+
private poseAsDefault(values: Record<string, number>) {
|
|
1631
|
+
if (!this.draft) return
|
|
1632
|
+
|
|
1633
|
+
const next = poseAsDefaults(this.draft, values)
|
|
1634
|
+
/* 앉힐 것이 없으면 초안이 그대로 온다. 그때 되돌리기 칸과 「저장 안 됨」을 만들지 않는다. */
|
|
1635
|
+
if (next !== this.draft) this.updateDraft(next)
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* 기준 상자를 고친다 — 초안과 부품이 **한 걸음으로** 바뀐다.
|
|
1640
|
+
*
|
|
1641
|
+
* 상자가 커지면 한가운데가 옮겨 가므로 부품을 그만큼 되밀어야 저장되는 자리가 유지된다
|
|
1642
|
+
* (`base-box.ts`). 그 둘을 따로 기록하면 한 번 되돌렸을 때 부품이 반쯤 밀린 화면이 남는다.
|
|
1643
|
+
*/
|
|
1644
|
+
private resizeBase(draft: FigureDraft, parts: PartModel[]) {
|
|
1645
|
+
this.recordHistory('base')
|
|
1646
|
+
this.draft = draft
|
|
1647
|
+
this.parts = parts
|
|
1648
|
+
this.dirty = true
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1584
1651
|
private updateDraft(draft: FigureDraft) {
|
|
1585
1652
|
this.recordHistory('draft')
|
|
1586
1653
|
this.draft = draft
|
|
@@ -1670,6 +1737,22 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1670
1737
|
return this.parts[this.selected]?.name
|
|
1671
1738
|
}
|
|
1672
1739
|
|
|
1740
|
+
/**
|
|
1741
|
+
* 고른 부품에 관절이 있으면 **그 관절이 도는 자리**를 저작면에 함께 그린다(`joint-gizmo.ts`).
|
|
1742
|
+
*
|
|
1743
|
+
* 축을 고르는 칸은 이 화면에 있는데 도는 모습은 미리보기에만 있었다. 고르는 자리와 보이는
|
|
1744
|
+
* 자리가 갈리면 사람은 왕복하며 맞춘다 — 여섯 축짜리 로봇이면 여섯 번이다.
|
|
1745
|
+
*/
|
|
1746
|
+
private get jointGizmo(): PartModel | undefined {
|
|
1747
|
+
const name = this.pickedName
|
|
1748
|
+
if (!name || !this.draft) return undefined
|
|
1749
|
+
|
|
1750
|
+
const joint = jointOf(this.draft, name)
|
|
1751
|
+
if (!joint) return undefined
|
|
1752
|
+
|
|
1753
|
+
return gizmoPart(joint, this.parts.find(part => part.name === name), this.draft)
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1673
1756
|
private pickPart(name: string) {
|
|
1674
1757
|
const at = this.parts.findIndex(part => part.name === name)
|
|
1675
1758
|
if (at >= 0) this.selected = at
|
|
@@ -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,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":"AAAA;;GAEG;AAwCH,mDAAmD;AACnD,MAAM,KAAK,GAAG,CAAC,CAAA;AAEf,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AAElD,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,KAAkB,EAAY,EAAE,CAAC,CAAC;IACvD,KAAK,EAAE,KAAK,CAAC,KAAK;IAClB,MAAM,EAAE,KAAK,CAAC,MAAM;IACpB,KAAK,EAAE,KAAK,CAAC,KAAK;CACnB,CAAC,CAAA;AAEF,2CAA2C;AAC3C,MAAM,UAAU,gBAAgB,CAAC,KAAkB,EAAE,KAA2B;IAC9E,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAExC,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAA;IAC1D,IAAI,IAAI,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAA;IACzB,IAAI,OAAO,GAAG,CAAC,CAAA;IAEf,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,GAAG;YACL,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YAChG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;SAChG,CAAA;QACD,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;IACrD,CAAC;IAED,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACzC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1C,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;KACvC,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CACxB,KAAkB,EAClB,KAA2B,EAC3B,IAAc;IAEd,MAAM,IAAI,GAAG;QACX,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3C,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;KAC1C,CAAA;IAED,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC;QAC7F,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;IACzB,CAAC;IAED,MAAM,KAAK,GAAG,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAA;IACxF,MAAM,KAAK,GACT,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,KAAK,CAAC;QAC5B,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAE1F,OAAO,EAAE,KAAK,EAAE,EAAE,GAAG,KAAK,EAAE,GAAG,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;AACvD,CAAC","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"]}
|
|
@@ -12,6 +12,8 @@ export declare class FigureAnimations extends FigureAnimations_base {
|
|
|
12
12
|
*/
|
|
13
13
|
private open;
|
|
14
14
|
private recipeTarget;
|
|
15
|
+
/** 단위를 손으로 적는 중인 파라미터. 목록에 없는 말을 적으려고 연 칸이다. */
|
|
16
|
+
private typingUnit;
|
|
15
17
|
private get clips();
|
|
16
18
|
private get parameters();
|
|
17
19
|
/** 두 목록의 이름을 한자리에서 본다 — 인스턴스가 이름 하나로 값을 준다. */
|
|
@@ -108,6 +110,19 @@ export declare class FigureAnimations extends FigureAnimations_base {
|
|
|
108
110
|
private addRecipe;
|
|
109
111
|
private renameClip;
|
|
110
112
|
private renameParameter;
|
|
113
|
+
/**
|
|
114
|
+
* 단위 칸 — **목록이 보이는 채로, 적는 길도 열어 둔다.**
|
|
115
|
+
*
|
|
116
|
+
* 처음에는 `datalist` 를 붙였다. 그것은 브라우저가 쥐고 있는 기능이라 **눌러 보기 전에는 목록이
|
|
117
|
+
* 있는지조차 안 보인다** — 화살표도 없고, 어디에 어떻게 뜨는지도 브라우저가 정한다. 고를 수
|
|
118
|
+
* 있다는 것을 알리는 것이 이 칸의 목적이므로 `select` 로 바꿨다.
|
|
119
|
+
*
|
|
120
|
+
* 형식은 단위를 규칙으로 쓰지 않으므로(표시용 말이다) 목록에 가두지 않는다. 마지막 줄이
|
|
121
|
+
* 「직접 적기」이고, 목록에 없는 말이 이미 적혀 있으면 처음부터 적는 칸으로 연다.
|
|
122
|
+
*/
|
|
123
|
+
private renderUnit;
|
|
124
|
+
/** 고른 것을 넣는다. 「직접 적기」는 값이 아니라 **칸을 바꾸는 것**이다. */
|
|
125
|
+
private chooseUnit;
|
|
111
126
|
private setRange;
|
|
112
127
|
/**
|
|
113
128
|
* Transition time. Clearing the input removes the field, which means instant. 0 is instant too, but
|
|
@@ -69,6 +69,16 @@ function freeName(taken, stem) {
|
|
|
69
69
|
if (!used.has(`${stem}-${n}`))
|
|
70
70
|
return `${stem}-${n}`;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* 파라미터 범위에 적히는 단위 — **권하는 것이지 가두는 것이 아니다.**
|
|
74
|
+
*
|
|
75
|
+
* 관절이 만드는 파라미터가 `deg` 와 `mm` 을 쓰고(회전과 직선), 그리퍼·문 같은 값은 `%` 를 쓴다.
|
|
76
|
+
* 속도는 `mm/s` 와 `rpm`, 시간은 `s` 다. 이 여섯이 지금 쓰이는 전부이고, 목록에 없는 말을 적는
|
|
77
|
+
* 길은 그대로 열려 있다.
|
|
78
|
+
*/
|
|
79
|
+
const PARAMETER_UNITS = ['deg', 'mm', '%', 'mm/s', 'rpm', 's'];
|
|
80
|
+
/** 목록 대신 손으로 적겠다는 표. `select` 의 값으로 쓴다. */
|
|
81
|
+
const UNIT_TYPE_IT = '__type-it';
|
|
72
82
|
let FigureAnimations = class FigureAnimations extends localize(i18next)(LitElement) {
|
|
73
83
|
constructor() {
|
|
74
84
|
super(...arguments);
|
|
@@ -80,6 +90,8 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
80
90
|
*/
|
|
81
91
|
this.open = '';
|
|
82
92
|
this.recipeTarget = '';
|
|
93
|
+
/** 단위를 손으로 적는 중인 파라미터. 목록에 없는 말을 적으려고 연 칸이다. */
|
|
94
|
+
this.typingUnit = new Set();
|
|
83
95
|
}
|
|
84
96
|
static { this.styles = [
|
|
85
97
|
ScrollbarStyles,
|
|
@@ -174,6 +186,21 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
174
186
|
min-width: 3.2em;
|
|
175
187
|
color: var(--md-sys-color-on-surface-variant);
|
|
176
188
|
}
|
|
189
|
+
/*
|
|
190
|
+
칸이 줄어들 줄 알아야 한다. 숫자 입력칸은 제 기본 너비(20자 남짓)를 갖고 있고 flex 는
|
|
191
|
+
기본 크기 아래로는 안 줄이므로, 범위처럼 한 줄에 셋이 서는 줄은 패널 밖으로 밀려
|
|
192
|
+
오른쪽 칸이 잘렸다.
|
|
193
|
+
*/
|
|
194
|
+
div[row] > input {
|
|
195
|
+
flex: 1 1 0;
|
|
196
|
+
min-width: 0;
|
|
197
|
+
}
|
|
198
|
+
/* 단위는 「mm」 정도의 짧은 말이라 숫자 칸보다 좁게 둔다. 고르는 칸도 같은 폭이다. */
|
|
199
|
+
div[row] > input[unit],
|
|
200
|
+
div[row] > select[unit] {
|
|
201
|
+
flex: 0 1 5.5em;
|
|
202
|
+
min-width: 0;
|
|
203
|
+
}
|
|
177
204
|
|
|
178
205
|
/* ── keyframe ─────────────────────────────────────────────────── */
|
|
179
206
|
|
|
@@ -468,14 +495,7 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
468
495
|
aria-label="max"
|
|
469
496
|
@change=${(e) => this.setRange(at, { max: Number(e.target.value) || 0 })}
|
|
470
497
|
/>
|
|
471
|
-
|
|
472
|
-
unit
|
|
473
|
-
.value=${live(range.unit ?? '')}
|
|
474
|
-
placeholder="mm"
|
|
475
|
-
aria-label="unit"
|
|
476
|
-
title=${i18next.t('figure.text.unit-is-a-word-not-a-rule')}
|
|
477
|
-
@change=${(e) => this.setRange(at, { unit: e.target.value.trim() })}
|
|
478
|
-
/>
|
|
498
|
+
${this.renderUnit(at, range.unit ?? '')}
|
|
479
499
|
</div>
|
|
480
500
|
${backwards ? html `<p warn>${i18next.t('figure.text.range-needs-room')}</p>` : nothing}
|
|
481
501
|
|
|
@@ -887,6 +907,51 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
|
|
|
887
907
|
renameParameter(at, name) {
|
|
888
908
|
this.patchParameter(at, { name: name.trim() });
|
|
889
909
|
}
|
|
910
|
+
/**
|
|
911
|
+
* 단위 칸 — **목록이 보이는 채로, 적는 길도 열어 둔다.**
|
|
912
|
+
*
|
|
913
|
+
* 처음에는 `datalist` 를 붙였다. 그것은 브라우저가 쥐고 있는 기능이라 **눌러 보기 전에는 목록이
|
|
914
|
+
* 있는지조차 안 보인다** — 화살표도 없고, 어디에 어떻게 뜨는지도 브라우저가 정한다. 고를 수
|
|
915
|
+
* 있다는 것을 알리는 것이 이 칸의 목적이므로 `select` 로 바꿨다.
|
|
916
|
+
*
|
|
917
|
+
* 형식은 단위를 규칙으로 쓰지 않으므로(표시용 말이다) 목록에 가두지 않는다. 마지막 줄이
|
|
918
|
+
* 「직접 적기」이고, 목록에 없는 말이 이미 적혀 있으면 처음부터 적는 칸으로 연다.
|
|
919
|
+
*/
|
|
920
|
+
renderUnit(at, unit) {
|
|
921
|
+
const listed = PARAMETER_UNITS.includes(unit);
|
|
922
|
+
const typing = this.typingUnit.has(at) || (unit !== '' && !listed);
|
|
923
|
+
if (typing) {
|
|
924
|
+
return html `<input
|
|
925
|
+
unit
|
|
926
|
+
.value=${live(unit)}
|
|
927
|
+
placeholder="mm"
|
|
928
|
+
aria-label="unit"
|
|
929
|
+
title=${i18next.t('figure.text.unit-is-a-word-not-a-rule')}
|
|
930
|
+
@change=${(e) => this.setRange(at, { unit: e.target.value.trim() })}
|
|
931
|
+
/>`;
|
|
932
|
+
}
|
|
933
|
+
return html `<select
|
|
934
|
+
unit
|
|
935
|
+
aria-label="unit"
|
|
936
|
+
title=${i18next.t('figure.text.unit-is-a-word-not-a-rule')}
|
|
937
|
+
@change=${(e) => this.chooseUnit(at, e.target.value)}
|
|
938
|
+
>
|
|
939
|
+
<option value="" ?selected=${unit === ''}>${i18next.t('figure.label.unit-none')}</option>
|
|
940
|
+
${PARAMETER_UNITS.map(one => html `<option value=${one} ?selected=${one === unit}>${one}</option>`)}
|
|
941
|
+
<option value=${UNIT_TYPE_IT}>${i18next.t('figure.label.unit-type-it')}</option>
|
|
942
|
+
</select>`;
|
|
943
|
+
}
|
|
944
|
+
/** 고른 것을 넣는다. 「직접 적기」는 값이 아니라 **칸을 바꾸는 것**이다. */
|
|
945
|
+
chooseUnit(at, value) {
|
|
946
|
+
if (value === UNIT_TYPE_IT) {
|
|
947
|
+
this.typingUnit = new Set([...this.typingUnit, at]);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const next = new Set(this.typingUnit);
|
|
951
|
+
next.delete(at);
|
|
952
|
+
this.typingUnit = next;
|
|
953
|
+
this.setRange(at, { unit: value });
|
|
954
|
+
}
|
|
890
955
|
setRange(at, patch) {
|
|
891
956
|
const parameter = this.parameters[at];
|
|
892
957
|
if (!parameter)
|
|
@@ -1056,6 +1121,10 @@ __decorate([
|
|
|
1056
1121
|
state(),
|
|
1057
1122
|
__metadata("design:type", Object)
|
|
1058
1123
|
], FigureAnimations.prototype, "recipeTarget", void 0);
|
|
1124
|
+
__decorate([
|
|
1125
|
+
state(),
|
|
1126
|
+
__metadata("design:type", Object)
|
|
1127
|
+
], FigureAnimations.prototype, "typingUnit", void 0);
|
|
1059
1128
|
FigureAnimations = __decorate([
|
|
1060
1129
|
customElement('figure-animations')
|
|
1061
1130
|
], FigureAnimations);
|