@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
|
@@ -20,6 +20,7 @@ import { ANCHOR_RULES, JOINT_TYPES, PART_ROLES, REPEAT_LIMIT, anchorOfPart, repe
|
|
|
20
20
|
import type {
|
|
21
21
|
AnchorRule,
|
|
22
22
|
Axis,
|
|
23
|
+
ContractChange,
|
|
23
24
|
FigurePart,
|
|
24
25
|
FigurePlacement,
|
|
25
26
|
JointType,
|
|
@@ -34,6 +35,8 @@ import { partToFigure, partFromFigure } from './figure-source.js'
|
|
|
34
35
|
import type { FigureDraft, PartModel } from './figure-source.js'
|
|
35
36
|
import { canMirror } from './part-edits.js'
|
|
36
37
|
import { jointOf, parentChoices, patchJoint, setJointType } from './joint-edits.js'
|
|
38
|
+
import { baseFittingParts, baseOf, resizeBase } from './base-box.js'
|
|
39
|
+
import type { BaseSize } from './base-box.js'
|
|
37
40
|
|
|
38
41
|
/** Axis choices for a joint. Written as a direction the author reads, stored as a vector. */
|
|
39
42
|
const JOINT_AXES: readonly { key: string; axis: Vec3 }[] = [
|
|
@@ -316,9 +319,60 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
316
319
|
grid-template-columns: repeat(3, 1fr);
|
|
317
320
|
gap: 4px;
|
|
318
321
|
}
|
|
322
|
+
/*
|
|
323
|
+
값이 둘인 줄은 셋으로 나누지 않는다. 가동 범위를 세 칸짜리 줄에 넣었더니 칸이 폭의 1/3 이라
|
|
324
|
+
「최소」라는 말과 -120 이 한 칸에 못 들어가 숫자가 잘렸다.
|
|
325
|
+
*/
|
|
326
|
+
div[double] {
|
|
327
|
+
display: grid;
|
|
328
|
+
grid-template-columns: repeat(2, 1fr);
|
|
329
|
+
gap: 4px;
|
|
330
|
+
}
|
|
331
|
+
/* 칸 아래에 붙는 한 줄짜리 행동. 칸과 같은 폭으로 둬야 한 묶음으로 읽힌다. */
|
|
332
|
+
button[fit] {
|
|
333
|
+
display: inline-flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
justify-content: center;
|
|
336
|
+
gap: 4px;
|
|
337
|
+
width: 100%;
|
|
338
|
+
margin-top: 4px;
|
|
339
|
+
padding: 3px 8px;
|
|
340
|
+
font: inherit;
|
|
341
|
+
font-size: 0.72rem;
|
|
342
|
+
color: var(--md-sys-color-on-surface);
|
|
343
|
+
background-color: var(--md-sys-color-surface-container);
|
|
344
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
345
|
+
border-radius: 6px;
|
|
346
|
+
cursor: pointer;
|
|
347
|
+
}
|
|
348
|
+
/* 고친 칸 바로 아래에 붙는 말. 막는 것은 오류 색, 알리는 것은 조용한 색. */
|
|
349
|
+
p[contract] {
|
|
350
|
+
display: flex;
|
|
351
|
+
gap: 4px;
|
|
352
|
+
margin: 4px 0 0;
|
|
353
|
+
font-size: 0.68rem;
|
|
354
|
+
line-height: 1.4;
|
|
355
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
356
|
+
}
|
|
357
|
+
p[contract][blocking] {
|
|
358
|
+
color: var(--md-sys-color-error);
|
|
359
|
+
}
|
|
360
|
+
p[contract] md-icon {
|
|
361
|
+
--md-icon-size: 14px;
|
|
362
|
+
flex: none;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
button[fit]:hover {
|
|
366
|
+
background-color: var(--md-sys-color-surface-container-high);
|
|
367
|
+
}
|
|
368
|
+
button[fit] md-icon {
|
|
369
|
+
--md-icon-size: 15px;
|
|
370
|
+
}
|
|
319
371
|
div[field] + div[field],
|
|
320
372
|
div[triple] + div[field],
|
|
321
373
|
div[field] + div[triple],
|
|
374
|
+
div[double] + div[field],
|
|
375
|
+
div[field] + div[double],
|
|
322
376
|
div[inline-field] + div[inline-field],
|
|
323
377
|
div[inline-field] + div[field],
|
|
324
378
|
div[field] + div[inline-field],
|
|
@@ -802,6 +856,21 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
802
856
|
|
|
803
857
|
/** 도형의 DB 식별자. 비어 있으면 미저장 신규 모델이다. */
|
|
804
858
|
@property({ type: String }) figureId = ''
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* 발행 판 번호. 0 이면 한 번도 발행되지 않았고, 그때만 `type` 을 고칠 수 있다(ADR-0063 결정 4).
|
|
862
|
+
*/
|
|
863
|
+
@property({ type: Number }) figureVersion = 0
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* 타입이 굳었나 — **한 번이라도 발행됐으면 굳는다**(ADR-0063 결정 4).
|
|
867
|
+
*
|
|
868
|
+
* 저장되지 않은 새 도형과 아직 발행 전인 초안에서는 고칠 수 있다. 전에는 「저장됐나」로
|
|
869
|
+
* 갈랐고, 그래서 저작자가 기계가 지은 이름(`FIGURE_MU6RJH47`)을 한 번도 못 고쳤다.
|
|
870
|
+
*/
|
|
871
|
+
private get typeIsFixed(): boolean {
|
|
872
|
+
return !!this.figureId && (this.figureVersion ?? 0) > 0
|
|
873
|
+
}
|
|
805
874
|
/** 도형의 표시 이름. 정본은 저작면이 들고, 여기서는 보여 주고 바꾼다(`figure-rename` 으로 알린다). */
|
|
806
875
|
@property({ type: String }) figureName = ''
|
|
807
876
|
/** 분류 하나 — 팔레트에서 묶는 기준. */
|
|
@@ -815,6 +884,16 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
815
884
|
@property({ type: Object }) draft?: FigureDraft
|
|
816
885
|
/** 초안 위의 부품들. 씬 모델 그대로다. */
|
|
817
886
|
@property({ type: Array }) parts: PartModel[] = []
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* 이 고침이 이미 놓인 인스턴스에 무엇을 하나 — 발행된 도형에만 있다(ADR-0063 결정 3).
|
|
890
|
+
*
|
|
891
|
+
* 발행 문에서만 말하면 저작자는 열 군데를 고친 뒤에 듣는다. 고치는 칸 옆에서 말한다.
|
|
892
|
+
*/
|
|
893
|
+
@property({ type: Array }) contract: ContractChange[] = []
|
|
894
|
+
|
|
895
|
+
/** 견주지 못한 까닭. 빈 목록과 뜻이 다르다 — 「깨지 않는다」가 아니라 「모른다」다. */
|
|
896
|
+
@property({ type: String }) contractFailure?: string
|
|
818
897
|
@property({ type: Number }) selected = -1
|
|
819
898
|
|
|
820
899
|
/** `part` 는 HTMLElement 의 속성(DOMTokenList)이다 — 가리면 형이 어긋난다. */
|
|
@@ -976,7 +1055,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
976
1055
|
? html`
|
|
977
1056
|
<div vector-row>
|
|
978
1057
|
<label>${i18next.t('figure.label.joint-limits-' + unit)}</label>
|
|
979
|
-
<div
|
|
1058
|
+
<div double>
|
|
980
1059
|
${this.number(i18next.t('figure.label.joint-limit-min'), joint.limits.min, value =>
|
|
981
1060
|
this.tellDraft(patchJoint(this.draft!, joint.name, { limits: { ...joint.limits!, min: value } }))
|
|
982
1061
|
)}
|
|
@@ -1009,9 +1088,73 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
1009
1088
|
})
|
|
1010
1089
|
}
|
|
1011
1090
|
|
|
1091
|
+
/**
|
|
1092
|
+
* 기준 상자를 고친다 — 부품은 저장되는 자리에 그대로 둔 채(`base-box.ts`).
|
|
1093
|
+
*
|
|
1094
|
+
* 초안과 부품이 함께 바뀌므로 둘을 한 번에 올린다. 따로 올리면 되돌리기가 둘로 갈리고, 그 사이
|
|
1095
|
+
* 한 번만 되돌린 화면은 부품이 반쯤 밀린 모습이 된다.
|
|
1096
|
+
*/
|
|
1097
|
+
/**
|
|
1098
|
+
* 기준 상자를 고친 것이 **이미 놓인 인스턴스에 무엇을 하나** — 고친 칸 바로 아래에서 말한다.
|
|
1099
|
+
*
|
|
1100
|
+
* 막는 것과 알리는 것을 가른다. 비율이 달라지면 놓인 것이 찌그러지므로 발행이 막히고, 크기만
|
|
1101
|
+
* 달라지는 것은 실척이 어긋날 뿐이라 알리기만 한다. 까닭은 형식이 적은 것을 그대로 옮긴다 —
|
|
1102
|
+
* 여기서 다시 쓰면 두 벌이 되고 곧 갈린다.
|
|
1103
|
+
*/
|
|
1104
|
+
private renderBaseContract() {
|
|
1105
|
+
if (this.contractFailure) {
|
|
1106
|
+
return html`<p contract>
|
|
1107
|
+
<md-icon>help</md-icon>
|
|
1108
|
+
<span>${i18next.t('figure.text.contract-not-compared', { why: this.contractFailure })}</span>
|
|
1109
|
+
</p>`
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const mine = this.contract.filter(one => one.code === 'base-ratio-changed' || one.code === 'base-size-changed')
|
|
1113
|
+
if (mine.length === 0) return nothing
|
|
1114
|
+
|
|
1115
|
+
return html`${mine.map(
|
|
1116
|
+
one => html`<p contract ?blocking=${one.blocking}>
|
|
1117
|
+
<md-icon>${one.blocking ? 'block' : 'info'}</md-icon>
|
|
1118
|
+
<span><b>${one.message}</b> ${one.why}</span>
|
|
1119
|
+
</p>`
|
|
1120
|
+
)}`
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
private resizeBase(patch: Partial<BaseSize>) {
|
|
1124
|
+
if (!this.draft) return
|
|
1125
|
+
|
|
1126
|
+
const next = resizeBase(this.draft, this.parts, { ...baseOf(this.draft), ...patch })
|
|
1127
|
+
if (next.draft === this.draft) return
|
|
1128
|
+
|
|
1129
|
+
this.dispatchEvent(
|
|
1130
|
+
new CustomEvent('resize-base', {
|
|
1131
|
+
detail: { draft: next.draft, parts: next.parts },
|
|
1132
|
+
bubbles: true,
|
|
1133
|
+
composed: true
|
|
1134
|
+
})
|
|
1135
|
+
)
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/** 부품 전부를 감싸는 가장 작은 상자로. 맞출 것이 없으면 아무 일도 안 한다. */
|
|
1139
|
+
private fitBaseToParts() {
|
|
1140
|
+
if (!this.draft) return
|
|
1141
|
+
const fitted = baseFittingParts(this.draft, this.parts)
|
|
1142
|
+
if (fitted) this.resizeBase(fitted)
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1012
1145
|
private setJointType(part: FigurePart, value: string) {
|
|
1013
1146
|
if (!this.draft) return
|
|
1014
|
-
|
|
1147
|
+
|
|
1148
|
+
/* 붙은 부품을 함께 준다 — 새 관절이 둘이 맞닿는 자리에 서려면 그쪽 치수를 알아야 한다. */
|
|
1149
|
+
const parent = part.parent ? this.parts.find(one => one.name === part.parent) : undefined
|
|
1150
|
+
this.tellDraft(
|
|
1151
|
+
setJointType(
|
|
1152
|
+
this.draft,
|
|
1153
|
+
part,
|
|
1154
|
+
value === '' ? undefined : (value as JointType),
|
|
1155
|
+
parent ? partToFigure(parent, this.draft) : undefined
|
|
1156
|
+
)
|
|
1157
|
+
)
|
|
1015
1158
|
}
|
|
1016
1159
|
|
|
1017
1160
|
private setJointAxis(name: string, key: string) {
|
|
@@ -1104,11 +1247,11 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
1104
1247
|
/>
|
|
1105
1248
|
</div>
|
|
1106
1249
|
<div inline-field>
|
|
1107
|
-
<label>${i18next.t('figure.label.figure-type')}${this.
|
|
1108
|
-
${!this.
|
|
1250
|
+
<label>${i18next.t('figure.label.figure-type')}${this.typeIsFixed ? this.helpButton('figure.text.type-name-cannot-change') : nothing}</label>
|
|
1251
|
+
${!this.typeIsFixed
|
|
1109
1252
|
? html`<input
|
|
1110
1253
|
.value=${this.draft?.figureType ?? ''}
|
|
1111
|
-
placeholder=${i18next.t('figure.text.figure-type-placeholder'
|
|
1254
|
+
placeholder=${i18next.t('figure.text.figure-type-placeholder')}
|
|
1112
1255
|
@change=${(e: Event) =>
|
|
1113
1256
|
this.dispatchEvent(
|
|
1114
1257
|
new CustomEvent('figure-type-change', {
|
|
@@ -1147,6 +1290,27 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
1147
1290
|
${this.helpHint('figure.text.placement-explained')}
|
|
1148
1291
|
</div>
|
|
1149
1292
|
|
|
1293
|
+
<!--
|
|
1294
|
+
기준 상자 — **고치는 길이 화면에 있어야 한다.** 부품이 이 상자를 넘으면 발행이 막히고,
|
|
1295
|
+
고치는 방법은 상자 숫자를 키우는 것이다(ADR-0044 ④). 그 칸이 없어서 저작자는 넘쳤다는
|
|
1296
|
+
말만 듣고 고칠 자리를 못 찾았다.
|
|
1297
|
+
-->
|
|
1298
|
+
<div field>
|
|
1299
|
+
<label>${i18next.t('figure.label.base-box')}${this.helpButton('figure.text.base-box-explained')}</label>
|
|
1300
|
+
<div triple>
|
|
1301
|
+
${this.number('X', this.draft?.width ?? 0, value => this.resizeBase({ width: value }), 1)}
|
|
1302
|
+
${this.number('Y', this.draft?.depth ?? 0, value => this.resizeBase({ depth: value }), 1)}
|
|
1303
|
+
${this.number('Z', this.draft?.height ?? 0, value => this.resizeBase({ height: value }), 1)}
|
|
1304
|
+
</div>
|
|
1305
|
+
${this.parts.length > 0
|
|
1306
|
+
? html`<button fit @click=${() => this.fitBaseToParts()} title=${i18next.t('figure.text.fit-base-explained')}>
|
|
1307
|
+
<md-icon>fit_screen</md-icon>${i18next.t('figure.button.fit-base-to-parts')}
|
|
1308
|
+
</button>`
|
|
1309
|
+
: nothing}
|
|
1310
|
+
${this.renderBaseContract()}
|
|
1311
|
+
${this.helpHint('figure.text.base-box-explained')}
|
|
1312
|
+
</div>
|
|
1313
|
+
|
|
1150
1314
|
<div inline-field>
|
|
1151
1315
|
<label>${i18next.t('figure.label.category')}${this.helpButton('figure.text.category-explained')}</label>
|
|
1152
1316
|
<input
|
|
@@ -8,6 +8,10 @@ import { create, registerFigure } from '@hatiolab/things-scene'
|
|
|
8
8
|
|
|
9
9
|
import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH } from './figure-view.js'
|
|
10
10
|
import type { ViewSettings } from './figure-view.js'
|
|
11
|
+
import { dialShape } from './angle-dial.js'
|
|
12
|
+
import { jointNeedles } from './joint-gizmo.js'
|
|
13
|
+
import { poseChanges } from './pose-defaults.js'
|
|
14
|
+
import type { FigureDraft } from './figure-source.js'
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* 저작 중인 것을 **도면에 놓았을 때**의 모습.
|
|
@@ -140,10 +144,45 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
140
144
|
}
|
|
141
145
|
div[clips] label {
|
|
142
146
|
display: grid;
|
|
143
|
-
grid-template-columns: auto 96px 34px;
|
|
147
|
+
grid-template-columns: auto 20px 96px 34px;
|
|
144
148
|
align-items: center;
|
|
145
149
|
gap: 8px;
|
|
146
150
|
}
|
|
151
|
+
/* 각도 눈. 3D 가 아무리 작게 서 있어도 여기서는 같은 크기로 읽힌다. */
|
|
152
|
+
div[clips] svg[dial] {
|
|
153
|
+
width: 20px;
|
|
154
|
+
height: 20px;
|
|
155
|
+
}
|
|
156
|
+
div[clips] svg[dial] path {
|
|
157
|
+
fill: rgba(255, 255, 255, 0.22);
|
|
158
|
+
}
|
|
159
|
+
div[clips] svg[dial] line {
|
|
160
|
+
stroke: var(--md-sys-color-primary, #7cc6ff);
|
|
161
|
+
stroke-width: 2;
|
|
162
|
+
stroke-linecap: round;
|
|
163
|
+
}
|
|
164
|
+
div[clips] svg[dial] circle {
|
|
165
|
+
fill: none;
|
|
166
|
+
stroke: rgba(255, 255, 255, 0.35);
|
|
167
|
+
}
|
|
168
|
+
/*
|
|
169
|
+
막대 위에 놓는 단추. 자세를 만든 자리에 있어야 「지금 이것을 기본으로」가 읽힌다.
|
|
170
|
+
앉힐 것이 있을 때만 나오므로, 늘 서 있는 칸이 아니다.
|
|
171
|
+
*/
|
|
172
|
+
div[clips] button[pose] {
|
|
173
|
+
align-self: stretch;
|
|
174
|
+
padding: 4px 8px;
|
|
175
|
+
font: inherit;
|
|
176
|
+
font-size: 0.7rem;
|
|
177
|
+
color: rgba(255, 255, 255, 0.92);
|
|
178
|
+
background-color: rgba(255, 255, 255, 0.14);
|
|
179
|
+
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
180
|
+
border-radius: 5px;
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
}
|
|
183
|
+
div[clips] button[pose]:hover {
|
|
184
|
+
background-color: rgba(255, 255, 255, 0.24);
|
|
185
|
+
}
|
|
147
186
|
div[clips] span[drive] {
|
|
148
187
|
opacity: 0.6;
|
|
149
188
|
font-size: 0.66rem;
|
|
@@ -176,6 +215,14 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
176
215
|
/** 저작 중인 것. 저장 형식으로 받는다 — 도면이 소비하는 것이 이 형식이다. */
|
|
177
216
|
@property({ type: Object }) source?: FigureSource
|
|
178
217
|
|
|
218
|
+
/**
|
|
219
|
+
* 저작 중인 초안. **기본값이 지금 무엇인지 알려고만 쥔다** — 고치지 않는다.
|
|
220
|
+
*
|
|
221
|
+
* `source` 로도 파라미터를 읽을 수 있지만, 후보를 미리 볼 때는 `source` 가 후보의 것이고
|
|
222
|
+
* 초안은 열려 있는 도형의 것이다. 「이 자세를 기본으로」는 열려 있는 도형에만 뜻이 있다.
|
|
223
|
+
*/
|
|
224
|
+
@property({ type: Object }) draft?: FigureDraft
|
|
225
|
+
|
|
179
226
|
/**
|
|
180
227
|
* 보기 설정. 저장되지 않는다(`figure-view.ts`).
|
|
181
228
|
*
|
|
@@ -226,8 +273,23 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
226
273
|
clips: NonNullable<FigureSource['animations']>,
|
|
227
274
|
parameters: NonNullable<FigureSource['parameters']>
|
|
228
275
|
) {
|
|
276
|
+
/*
|
|
277
|
+
지금 세운 자세를 기본값으로 앉히는 자리. **막대 옆에 둔다** — 자세를 만든 곳이 여기라,
|
|
278
|
+
값을 읽어 파라미터 칸으로 옮겨 적는 왕복이 없어진다. 앉힐 것이 없으면 아예 안 나온다.
|
|
279
|
+
*/
|
|
280
|
+
const changes = poseChanges(this.draft, this.giving)
|
|
281
|
+
|
|
229
282
|
return html`
|
|
230
283
|
<div clips>
|
|
284
|
+
${changes.length > 0
|
|
285
|
+
? html`<button
|
|
286
|
+
pose
|
|
287
|
+
title=${i18next.t('figure.text.pose-as-default-explained')}
|
|
288
|
+
@click=${() => this.poseAsDefault()}
|
|
289
|
+
>
|
|
290
|
+
${i18next.t('figure.button.pose-as-default')}
|
|
291
|
+
</button>`
|
|
292
|
+
: ''}
|
|
231
293
|
${parameters.map(parameter => {
|
|
232
294
|
const range = parameter.range
|
|
233
295
|
const value = this.giving[parameter.name] ?? parameter.default ?? range?.min ?? 0
|
|
@@ -236,6 +298,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
236
298
|
return html`
|
|
237
299
|
<label>
|
|
238
300
|
<span>${parameter.label ?? parameter.name} <span drive>${range?.unit ?? ''}</span></span>
|
|
301
|
+
${this.renderDial(range, value)}
|
|
239
302
|
<input
|
|
240
303
|
type="range"
|
|
241
304
|
min=${range?.min ?? 0}
|
|
@@ -254,6 +317,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
254
317
|
return html`
|
|
255
318
|
<label>
|
|
256
319
|
<span>${clip.name} <span drive>${i18next.t('figure.label.playback-speed')}</span></span>
|
|
320
|
+
<span></span>
|
|
257
321
|
<input
|
|
258
322
|
type="range"
|
|
259
323
|
min="0"
|
|
@@ -270,6 +334,31 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
270
334
|
`
|
|
271
335
|
}
|
|
272
336
|
|
|
337
|
+
/**
|
|
338
|
+
* 각도 파라미터에만 다는 눈.
|
|
339
|
+
*
|
|
340
|
+
* 미리보기의 3D 는 여덟 대를 한 화면에 세우느라 한 대가 20 픽셀쯤이다. 관절이 돌았다는
|
|
341
|
+
* 표시를 그 안에 그리면 몇 픽셀에 그친다 — 부품에 단 바늘이 4 × 2 픽셀로 나왔다(잰 값).
|
|
342
|
+
* 각을 읽는 일은 배율에서 떼어 여기에 둔다.
|
|
343
|
+
*
|
|
344
|
+
* 각이 아닌 파라미터(밀리미터 · 퍼센트 · 초)에는 안 단다. 눈금이 도는 것이 아니라서
|
|
345
|
+
* 원으로 그리면 없는 뜻이 생긴다.
|
|
346
|
+
*/
|
|
347
|
+
private renderDial(range: { unit?: string; min: number; max: number } | undefined, value: number) {
|
|
348
|
+
if (!range || range.unit !== 'deg') return html`<span></span>`
|
|
349
|
+
|
|
350
|
+
const dial = dialShape(range.min, range.max, value)
|
|
351
|
+
const span = dial.radius * 2 + 2
|
|
352
|
+
|
|
353
|
+
return html`
|
|
354
|
+
<svg dial viewBox="0 0 ${span} ${span}" aria-hidden="true">
|
|
355
|
+
<path d=${dial.sector}></path>
|
|
356
|
+
<circle cx=${dial.centre.x} cy=${dial.centre.y} r=${dial.radius}></circle>
|
|
357
|
+
<line x1=${dial.centre.x} y1=${dial.centre.y} x2=${dial.needle.x} y2=${dial.needle.y}></line>
|
|
358
|
+
</svg>
|
|
359
|
+
`
|
|
360
|
+
}
|
|
361
|
+
|
|
273
362
|
/**
|
|
274
363
|
* 세워 둔 인스턴스 전부에 같은 값을 준다.
|
|
275
364
|
*
|
|
@@ -292,6 +381,18 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
292
381
|
this.tellInstances('parameters', this.giving)
|
|
293
382
|
}
|
|
294
383
|
|
|
384
|
+
/**
|
|
385
|
+
* 세운 자세를 기본값으로 — **셈은 여기서 하지 않는다.**
|
|
386
|
+
*
|
|
387
|
+
* 초안을 쥐고 있는 것은 페이지이고, 되돌리기와 「저장 안 됨」도 거기서 매긴다. 여기서
|
|
388
|
+
* 초안을 만들어 보내면 그 셋이 두 곳으로 갈린다. 민 값만 올린다.
|
|
389
|
+
*/
|
|
390
|
+
private poseAsDefault(): void {
|
|
391
|
+
this.dispatchEvent(
|
|
392
|
+
new CustomEvent('pose-as-default', { detail: { values: { ...this.giving } }, bubbles: true, composed: true })
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
|
|
295
396
|
private tellInstances(key: 'clips' | 'parameters', values: Record<string, number>): void {
|
|
296
397
|
for (const component of this.scene?.root?.components ?? []) {
|
|
297
398
|
component.set?.(key, { ...values })
|
|
@@ -355,8 +456,14 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
355
456
|
}
|
|
356
457
|
|
|
357
458
|
const view = this.view ?? DEFAULT_VIEW
|
|
459
|
+
/*
|
|
460
|
+
관절마다 바늘 하나를 붙여 세운다(`joint-gizmo.ts`). 대칭인 부품은 돌아도 그림이 같아서,
|
|
461
|
+
바늘이 없으면 저작자가 막대를 끝까지 밀고도 관절이 도는 것을 못 본다. 여기서만 붙인다 —
|
|
462
|
+
저장되는 것도 저작면에 서는 것도 원본 그대로다.
|
|
463
|
+
*/
|
|
464
|
+
const shown: FigureSource = { ...source, parts: [...source.parts, ...jointNeedles(source)] }
|
|
358
465
|
/* 씬이 이 둘로 인스턴스 높이를 정한다 — 서명에 안 넣으면 다시 세워도 그대로 선다. */
|
|
359
|
-
const signature = JSON.stringify([
|
|
466
|
+
const signature = JSON.stringify([shown, view.placement, view.ceilingHeight])
|
|
360
467
|
if (this.scene && this.standing === signature) return
|
|
361
468
|
|
|
362
469
|
const { errors } = validate(source)
|
|
@@ -371,7 +478,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
371
478
|
this.driving = {}
|
|
372
479
|
|
|
373
480
|
try {
|
|
374
|
-
registerFigure(compile(
|
|
481
|
+
registerFigure(compile(shown) as never)
|
|
375
482
|
|
|
376
483
|
/*
|
|
377
484
|
도면이 하는 일을 그대로 한다. 컴포넌트에는 타입 이름과 자리·크기만 적는다 —
|
|
@@ -9,7 +9,7 @@ import { ScrollbarStyles } from '@operato/styles'
|
|
|
9
9
|
import { blocksRelease, compile, costCurveSvg, costOf, scoreOf, sizingReport, validate } from '@hatiolab/figure-model'
|
|
10
10
|
import type { FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
|
-
import { capacityOf } from './scene-budget.js'
|
|
12
|
+
import { capacityOf, MEASURED } from './scene-budget.js'
|
|
13
13
|
import { maturityOf } from './maturity.js'
|
|
14
14
|
import type { MaturityAxis } from './maturity.js'
|
|
15
15
|
import type { FigureCapacity } from './scene-budget.js'
|
|
@@ -433,7 +433,6 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
433
433
|
@state() private score?: FigureScore
|
|
434
434
|
@state() private curve = ''
|
|
435
435
|
/** 1만 개를 놓았을 때. 저작자가 실제로 궁금한 숫자다. */
|
|
436
|
-
@state() private many?: FigureCost
|
|
437
436
|
/** 몇 개까지 놓을 수 있나. 이 패널의 머리다. */
|
|
438
437
|
@state() private capacity?: FigureCapacity
|
|
439
438
|
/** 추이 그림을 제 크기로 펼쳤나. */
|
|
@@ -507,8 +506,9 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
507
506
|
* 직접 답하지 않는다 — 형상이 선언된 한도 안인지를 말할 뿐이다. 그래서 개수를
|
|
508
507
|
* 머리에 올리고 등급은 옆의 badge 로 내린다.
|
|
509
508
|
*
|
|
510
|
-
* 근거를 함께 낸다.
|
|
511
|
-
*
|
|
509
|
+
* 근거를 함께 낸다. 묶음으로 갈리는 개수의 분모는 **실측**이고(2026-09-18, Apple M5), 잰
|
|
510
|
+
* 기계와 날짜를 함께 적는다 — 적지 않으면 어디서나 참인 수로 읽힌다. 삼각형 쪽은 아직 설계
|
|
511
|
+
* 문서의 목표치라 그렇다고 적는다.
|
|
512
512
|
*/
|
|
513
513
|
private renderVerdict() {
|
|
514
514
|
const score = this.score
|
|
@@ -544,7 +544,11 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
544
544
|
per: capacity.per.toLocaleString(),
|
|
545
545
|
budget: capacity.budget.toLocaleString()
|
|
546
546
|
})}
|
|
547
|
-
<em
|
|
547
|
+
<em>
|
|
548
|
+
${capacity.bound === 'groups'
|
|
549
|
+
? i18next.t('figure.text.budget-measured-on', { gpu: MEASURED.gpu, on: MEASURED.on })
|
|
550
|
+
: i18next.t('figure.text.budget-not-measured-yet')}
|
|
551
|
+
</em>
|
|
548
552
|
</p>
|
|
549
553
|
</section>
|
|
550
554
|
`
|
|
@@ -763,12 +767,6 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
763
767
|
<dt>${i18next.t('figure.label.triangles')}</dt>
|
|
764
768
|
<dd>${cost.triangles.toLocaleString()}</dd>
|
|
765
769
|
|
|
766
|
-
<dt>${i18next.t('figure.label.triangles-at-100')}</dt>
|
|
767
|
-
<dd>${cost.at.triangles.toLocaleString()}</dd>
|
|
768
|
-
|
|
769
|
-
<dt>${i18next.t('figure.label.draw-calls-at-100')}</dt>
|
|
770
|
-
<dd>${cost.at.drawCalls.toLocaleString()}</dd>
|
|
771
|
-
|
|
772
770
|
<dt>${i18next.t('figure.label.distinct-shapes')}</dt>
|
|
773
771
|
<dd>${cost.distinctShapes}</dd>
|
|
774
772
|
|
|
@@ -780,33 +778,26 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
780
778
|
}
|
|
781
779
|
|
|
782
780
|
/**
|
|
783
|
-
* 재활용 추이
|
|
781
|
+
* 재활용 추이 그림.
|
|
782
|
+
*
|
|
783
|
+
* **「몇 개 놓으면 얼마」를 숫자로 적지 않는다.** 놓는 개수는 값을 치르지 않는다 —
|
|
784
|
+
* 한 화면에 보이는 개수 × 그 도형의 묶음 수가 치른다(실측 2026-09-18, ADR-0051 ③).
|
|
785
|
+
* 전에는 여기에 「100개 기준 draw call」 · 「1만개 기준」을 적었고, 그 수는 실제로 드는
|
|
786
|
+
* 값과 어긋났다. 답은 위의 성숙도 칸이 한다 — 잰 기계와 날짜를 달고 「한 화면에 몇 대」를
|
|
787
|
+
* 말한다.
|
|
784
788
|
*
|
|
785
789
|
* ## 그림을 사이드바에 밀어 넣지 않는다
|
|
786
790
|
*
|
|
787
791
|
* `costCurveSvg` 의 자연 크기는 680x380 이고 글자가 11px 다. 300px 칸에서
|
|
788
792
|
* width:100% 로 줄이면 글자가 4px 이 되어 **아무것도 읽을 수 없다** — 화면을 띄워
|
|
789
|
-
* 보고서야 드러났다.
|
|
790
|
-
* 실제로 궁금한 숫자(1만 개일 때)를 두고 그림은 선택해서 제 크기로 본다.
|
|
793
|
+
* 보고서야 드러났다. 그림은 선택해서 제 크기로 본다.
|
|
791
794
|
*/
|
|
792
795
|
private renderCurve() {
|
|
793
|
-
|
|
794
|
-
if (!many && !this.curve) return nothing
|
|
796
|
+
if (!this.curve) return nothing
|
|
795
797
|
|
|
796
798
|
return html`
|
|
797
799
|
<section>
|
|
798
800
|
<h3>${i18next.t('figure.label.reuse-trend')}</h3>
|
|
799
|
-
${many
|
|
800
|
-
? html`
|
|
801
|
-
<dl metrics>
|
|
802
|
-
<dt>${i18next.t('figure.label.triangles-at-10k')}</dt>
|
|
803
|
-
<dd>${many.at.triangles.toLocaleString()}</dd>
|
|
804
|
-
|
|
805
|
-
<dt>${i18next.t('figure.label.draw-calls-at-10k')}</dt>
|
|
806
|
-
<dd>${many.at.drawCalls.toLocaleString()}</dd>
|
|
807
|
-
</dl>
|
|
808
|
-
`
|
|
809
|
-
: nothing}
|
|
810
801
|
${this.curve
|
|
811
802
|
? html`
|
|
812
803
|
<button expand @click=${() => (this.expanded = true)}>
|
|
@@ -894,7 +885,6 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
894
885
|
if (!source) {
|
|
895
886
|
this.cost = undefined
|
|
896
887
|
this.score = undefined
|
|
897
|
-
this.many = undefined
|
|
898
888
|
this.capacity = undefined
|
|
899
889
|
this.curve = ''
|
|
900
890
|
this.errors = []
|
|
@@ -911,7 +901,6 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
911
901
|
if (this.bare) {
|
|
912
902
|
this.cost = undefined
|
|
913
903
|
this.score = undefined
|
|
914
|
-
this.many = undefined
|
|
915
904
|
this.capacity = undefined
|
|
916
905
|
this.curve = ''
|
|
917
906
|
this.errors = []
|
|
@@ -933,7 +922,6 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
933
922
|
// 형식을 어겼으면 컴파일이 던진다. 셈을 비우고 오류만 보여 준다.
|
|
934
923
|
this.cost = undefined
|
|
935
924
|
this.score = undefined
|
|
936
|
-
this.many = undefined
|
|
937
925
|
this.capacity = undefined
|
|
938
926
|
this.curve = ''
|
|
939
927
|
this.sizing = []
|
|
@@ -958,9 +946,9 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
958
946
|
blocking: finding.code === 'sizing-splits'
|
|
959
947
|
}))
|
|
960
948
|
|
|
961
|
-
|
|
949
|
+
/* 개수 인자는 `cost.at` 만 정한다 — 그 칸을 화면에서 뺐으니 기본값 그대로 둔다. */
|
|
950
|
+
this.cost = costOf(blueprint)
|
|
962
951
|
// 1만 개는 figure-model 에 다시 묻는다 — 화면이 곱셈으로 짐작하지 않는다.
|
|
963
|
-
this.many = costOf(blueprint, 10000)
|
|
964
952
|
this.score = scoreOf(blueprint)
|
|
965
953
|
this.capacity = capacityOf(this.cost)
|
|
966
954
|
|
|
@@ -10,7 +10,7 @@ import { css, html, LitElement } from 'lit'
|
|
|
10
10
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
11
11
|
|
|
12
12
|
import { i18next, localize } from '@operato/i18n'
|
|
13
|
-
import type { FigureSource } from '@hatiolab/figure-model'
|
|
13
|
+
import type { ContractChange, FigureSource } from '@hatiolab/figure-model'
|
|
14
14
|
|
|
15
15
|
import type { FigureDraft, PartModel } from './figure-source.js'
|
|
16
16
|
import { DEFAULT_VIEW } from './figure-view.js'
|
|
@@ -159,6 +159,12 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
159
159
|
@property({ type: Object }) draft?: FigureDraft
|
|
160
160
|
@property({ type: Array }) parts: PartModel[] = []
|
|
161
161
|
@property({ type: Object }) source?: FigureSource
|
|
162
|
+
|
|
163
|
+
/** 이 고침이 이미 놓인 인스턴스에 무엇을 하나(ADR-0063 결정 3). 발행 전 도형에는 비어 있다. */
|
|
164
|
+
@property({ type: Array }) contract: ContractChange[] = []
|
|
165
|
+
|
|
166
|
+
/** 견주지 못한 까닭. 있으면 화면이 「확인하지 못했다」로 말한다. */
|
|
167
|
+
@property({ type: String }) contractFailure?: string
|
|
162
168
|
@property({ type: Number }) selected = -1
|
|
163
169
|
/** 저작 화면의 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다. */
|
|
164
170
|
@property({ type: Object }) view: ViewSettings = DEFAULT_VIEW
|
|
@@ -193,11 +199,14 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
193
199
|
<figure-inspector
|
|
194
200
|
?off=${this.tab !== 'properties'}
|
|
195
201
|
.figureId=${this.figureId}
|
|
202
|
+
.figureVersion=${this.figureVersion}
|
|
196
203
|
.figureName=${this.figureName}
|
|
197
204
|
.figureCategory=${this.figureCategory}
|
|
198
205
|
.figureTags=${this.figureTags}
|
|
199
206
|
.draft=${this.draft}
|
|
200
207
|
.parts=${this.parts}
|
|
208
|
+
.contract=${this.contract}
|
|
209
|
+
.contractFailure=${this.contractFailure}
|
|
201
210
|
.selected=${this.selected}
|
|
202
211
|
></figure-inspector>
|
|
203
212
|
|
|
@@ -174,6 +174,14 @@ export interface PartModel {
|
|
|
174
174
|
*/
|
|
175
175
|
hidden?: boolean
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* 씬이 이것을 **집지 않는다** — 클릭·마퀴·기즈모 어디에도 안 걸린다.
|
|
179
|
+
*
|
|
180
|
+
* 저작면에만 서는 것(관절 부채꼴)이 쓴다. 저장 형식에는 없는 칸이라 `toFigureSource` 가
|
|
181
|
+
* 싣지 않는다.
|
|
182
|
+
*/
|
|
183
|
+
locked?: boolean
|
|
184
|
+
|
|
177
185
|
/** 색 토큰 이름 (`palette.primary` 등). */
|
|
178
186
|
token?: string
|
|
179
187
|
/*
|