@things-factory/figure-ui 10.1.45 → 10.1.47
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/modeller/angle-dial.ts +70 -0
- package/client/modeller/figure-settings.ts +328 -0
- package/client/modeller/figure-v3-capability.ts +137 -0
- package/client/modeller/figure-v3-clips.ts +253 -0
- package/client/modeller/figure-v3-controls.ts +212 -0
- package/client/modeller/figure-v3-links.ts +150 -79
- package/client/modeller/figure-v3-material.ts +108 -35
- package/client/modeller/figure-v3-parts.ts +96 -3
- package/client/modeller/figure-v3-preview.ts +326 -0
- package/client/modeller/figure-v3-properties.ts +29 -57
- package/client/modeller/figure-v3-verdict.ts +598 -60
- package/client/modeller/figure-view.ts +152 -0
- package/client/modeller/side-panel-styles.ts +326 -0
- package/client/modeller/v3-canvas.ts +221 -263
- package/client/modeller/v3-clips.ts +89 -0
- package/client/modeller/v3-controls.ts +54 -0
- package/client/modeller/v3-editor.ts +532 -223
- package/client/modeller/v3-fields.ts +62 -0
- package/client/modeller/v3-help.ts +41 -0
- package/client/modeller/v3-keys.ts +34 -0
- package/client/modeller/v3-links.ts +126 -10
- package/client/modeller/v3-live.ts +79 -0
- package/client/modeller/v3-maturity.ts +415 -0
- package/client/modeller/v3-palette.ts +45 -0
- package/client/modeller/v3-preview-model.ts +54 -0
- package/client/modeller/v3-stage-model.ts +159 -0
- package/client/pages/figure-list-page.ts +4 -4
- package/client/pages/figure-v3-modeller-page.ts +66 -134
- 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/figure-settings.d.ts +39 -0
- package/dist-client/modeller/figure-settings.js +317 -0
- package/dist-client/modeller/figure-settings.js.map +1 -0
- package/dist-client/modeller/figure-v3-capability.d.ts +22 -0
- package/dist-client/modeller/figure-v3-capability.js +141 -0
- package/dist-client/modeller/figure-v3-capability.js.map +1 -0
- package/dist-client/modeller/figure-v3-clips.d.ts +31 -0
- package/dist-client/modeller/figure-v3-clips.js +268 -0
- package/dist-client/modeller/figure-v3-clips.js.map +1 -0
- package/dist-client/modeller/figure-v3-controls.d.ts +36 -0
- package/dist-client/modeller/figure-v3-controls.js +226 -0
- package/dist-client/modeller/figure-v3-controls.js.map +1 -0
- package/dist-client/modeller/figure-v3-links.d.ts +10 -2
- package/dist-client/modeller/figure-v3-links.js +134 -79
- package/dist-client/modeller/figure-v3-links.js.map +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +5 -2
- package/dist-client/modeller/figure-v3-material.js +109 -35
- package/dist-client/modeller/figure-v3-material.js.map +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +10 -0
- package/dist-client/modeller/figure-v3-parts.js +88 -3
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-preview.d.ts +34 -0
- package/dist-client/modeller/figure-v3-preview.js +322 -0
- package/dist-client/modeller/figure-v3-preview.js.map +1 -0
- package/dist-client/modeller/figure-v3-properties.d.ts +2 -2
- package/dist-client/modeller/figure-v3-properties.js +29 -57
- package/dist-client/modeller/figure-v3-properties.js.map +1 -1
- package/dist-client/modeller/figure-v3-verdict.d.ts +31 -17
- package/dist-client/modeller/figure-v3-verdict.js +579 -59
- package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +118 -0
- package/dist-client/modeller/figure-view.js +78 -0
- package/dist-client/modeller/figure-view.js.map +1 -0
- package/dist-client/modeller/side-panel-styles.d.ts +23 -0
- package/dist-client/modeller/side-panel-styles.js +326 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -0
- package/dist-client/modeller/v3-canvas.d.ts +29 -37
- package/dist-client/modeller/v3-canvas.js +188 -260
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-clips.d.ts +39 -0
- package/dist-client/modeller/v3-clips.js +58 -0
- package/dist-client/modeller/v3-clips.js.map +1 -0
- package/dist-client/modeller/v3-controls.d.ts +28 -0
- package/dist-client/modeller/v3-controls.js +28 -0
- package/dist-client/modeller/v3-controls.js.map +1 -0
- package/dist-client/modeller/v3-editor.d.ts +58 -1
- package/dist-client/modeller/v3-editor.js +556 -218
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +30 -0
- package/dist-client/modeller/v3-fields.js +46 -0
- package/dist-client/modeller/v3-fields.js.map +1 -0
- package/dist-client/modeller/v3-help.d.ts +18 -0
- package/dist-client/modeller/v3-help.js +37 -0
- package/dist-client/modeller/v3-help.js.map +1 -0
- package/dist-client/modeller/v3-keys.d.ts +14 -0
- package/dist-client/modeller/v3-keys.js +23 -0
- package/dist-client/modeller/v3-keys.js.map +1 -0
- package/dist-client/modeller/v3-links.d.ts +44 -2
- package/dist-client/modeller/v3-links.js +125 -8
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-live.d.ts +31 -0
- package/dist-client/modeller/v3-live.js +58 -0
- package/dist-client/modeller/v3-live.js.map +1 -0
- package/dist-client/modeller/v3-maturity.d.ts +120 -0
- package/dist-client/modeller/v3-maturity.js +317 -0
- package/dist-client/modeller/v3-maturity.js.map +1 -0
- package/dist-client/modeller/v3-palette.d.ts +7 -0
- package/dist-client/modeller/v3-palette.js +42 -0
- package/dist-client/modeller/v3-palette.js.map +1 -0
- package/dist-client/modeller/v3-preview-model.d.ts +32 -0
- package/dist-client/modeller/v3-preview-model.js +48 -0
- package/dist-client/modeller/v3-preview-model.js.map +1 -0
- package/dist-client/modeller/v3-stage-model.d.ts +31 -0
- package/dist-client/modeller/v3-stage-model.js +121 -0
- package/dist-client/modeller/v3-stage-model.js.map +1 -0
- package/dist-client/pages/figure-list-page.js +4 -4
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -6
- package/dist-client/pages/figure-v3-modeller-page.js +64 -136
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.chk.tsbuildinfo +1 -0
- package/dist-client/tsconfig.scratch-fm.tsbuildinfo +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/v3-clips.test.ts +70 -0
- package/test/v3-controls.test.ts +68 -0
- package/test/v3-fields.test.ts +62 -0
- package/test/v3-gizmo.test.ts +3 -5
- package/test/v3-keys.test.ts +37 -0
- package/test/v3-layout.test.ts +78 -0
- package/test/v3-links.test.ts +118 -7
- package/test/v3-live.test.ts +99 -0
- package/test/v3-material.test.ts +6 -0
- package/test/v3-maturity.test.ts +268 -0
- package/test/v3-parts-wiring.test.ts +3 -1
- package/test/v3-preview.test.ts +42 -0
- package/test/v3-properties.test.ts +4 -2
- package/test/v3-scene-canvas.test.ts +85 -0
- package/test/v3-stage-model.test.ts +25 -0
- package/test/v3-theme.test.ts +16 -25
- package/test/v3-verdict.test.ts +12 -6
- package/test/v3-view.test.ts +20 -0
- package/translations/en.json +98 -5
- package/translations/ja.json +100 -7
- package/translations/ko.json +98 -5
- package/translations/ms.json +100 -7
- package/translations/zh.json +100 -7
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 막대 옆에 서는 **각도 눈**. 지금 각이 범위의 어디인지를 한 눈에 보인다.
|
|
7
|
+
*
|
|
8
|
+
* ## 왜 3D 로는 모자랐나
|
|
9
|
+
*
|
|
10
|
+
* 관절이 돌아도 화면이 그대로인 경우가 있다 — 제 축에서 대칭인 부품이 그렇다. 그래서 관절이
|
|
11
|
+
* 모는 부품에 바늘을 달아 미리보기에서 함께 돌게 했는데(`joint-gizmo.ts` 의 `jointNeedles`),
|
|
12
|
+
* **미리보기에서는 그 바늘이 4 × 2 픽셀이다**(잰 값: 부품 100mm · 바늘 60 × 12mm · 여덟 대를
|
|
13
|
+
* 한 화면에 세운 상태). 미리보기는 크기 반응을 보려고 여덟 대를 나란히 놓는 자리라 한 대가
|
|
14
|
+
* 20 픽셀쯤이고, 그 위의 어떤 표시도 몇 픽셀을 넘지 못한다.
|
|
15
|
+
*
|
|
16
|
+
* 각을 읽는 일은 3D 의 배율에서 떼어 놓는다. 눈은 막대 옆 24 픽셀 자리에 서고, 도형이 아무리
|
|
17
|
+
* 작게 서 있어도 같은 크기로 읽힌다.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface DialShape {
|
|
21
|
+
/** 가동 범위를 채운 부채꼴. SVG path 의 `d`. */
|
|
22
|
+
sector: string
|
|
23
|
+
/** 지금 값이 가리키는 끝점. */
|
|
24
|
+
needle: { x: number; y: number }
|
|
25
|
+
centre: { x: number; y: number }
|
|
26
|
+
radius: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 0 도가 위를 가리키고, 값이 커지면 시계 방향으로 돈다 — 화면의 각과 같은 방향이다. */
|
|
30
|
+
function at(centre: { x: number; y: number }, radius: number, degrees: number) {
|
|
31
|
+
const rad = (degrees * Math.PI) / 180
|
|
32
|
+
return {
|
|
33
|
+
x: +(centre.x + radius * Math.sin(rad)).toFixed(2),
|
|
34
|
+
y: +(centre.y - radius * Math.cos(rad)).toFixed(2)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 한 바퀴에 가까우면 부채꼴 대신 온전한 원이다. */
|
|
39
|
+
const FULL = 359.9
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 눈의 모양. 값이 범위를 벗어나면 범위 끝에 세운다 — 밖을 가리키는 바늘은 없는 값을 말한다.
|
|
43
|
+
*/
|
|
44
|
+
export function dialShape(min: number, max: number, value: number, radius = 9): DialShape {
|
|
45
|
+
const centre = { x: radius + 1, y: radius + 1 }
|
|
46
|
+
const span = Math.abs(max - min)
|
|
47
|
+
const held = Math.min(Math.max(value, Math.min(min, max)), Math.max(min, max))
|
|
48
|
+
|
|
49
|
+
if (span >= FULL) {
|
|
50
|
+
const top = at(centre, radius, 0)
|
|
51
|
+
const bottom = at(centre, radius, 180)
|
|
52
|
+
return {
|
|
53
|
+
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`,
|
|
54
|
+
needle: at(centre, radius, held),
|
|
55
|
+
centre,
|
|
56
|
+
radius
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const from = at(centre, radius, min)
|
|
61
|
+
const to = at(centre, radius, max)
|
|
62
|
+
const large = span > 180 ? 1 : 0
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
sector: `M ${centre.x} ${centre.y} L ${from.x} ${from.y} A ${radius} ${radius} 0 ${large} 1 ${to.x} ${to.y} Z`,
|
|
66
|
+
needle: at(centre, radius, held),
|
|
67
|
+
centre,
|
|
68
|
+
radius
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { css, html, LitElement } from 'lit'
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
3
|
+
|
|
4
|
+
import { i18next, localize } from '@operato/i18n'
|
|
5
|
+
import { ScrollbarStyles } from '@operato/styles'
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_VIEW, ENVIRONMENTS, SCENE_REFERENCES } from './figure-view.js'
|
|
8
|
+
import type { EnvironmentName, ViewSettings } from './figure-view.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 저작 화면의 **보기 설정**.
|
|
12
|
+
*
|
|
13
|
+
* ## 자산이 아니라 작업대다
|
|
14
|
+
*
|
|
15
|
+
* 여기서 고치는 것은 저장되지 않는다. 조명을 밝게 하고 바닥을 켠 것은 지금 보기 편한
|
|
16
|
+
* 상태일 뿐이다. 그래서 「저장 안 됨」을 화면에 적어 둔다 — 안 적으면 저작자가 조명을
|
|
17
|
+
* 맞춰 두고 그것이 자산에 딸려 간다고 여긴다.
|
|
18
|
+
*
|
|
19
|
+
* ## 값을 들고 있지 않는다
|
|
20
|
+
*
|
|
21
|
+
* 이 패널은 받은 것을 그리고 바뀐 것을 알린다. 정본은 모델러 페이지가 갖는다 — 속성
|
|
22
|
+
* 패널과 같은 방식이다. 여기서 들고 있으면 페이지가 다시 그릴 때 두 벌이 갈린다.
|
|
23
|
+
*/
|
|
24
|
+
@customElement('figure-settings')
|
|
25
|
+
export class FigureSettings extends localize(i18next)(LitElement) {
|
|
26
|
+
static styles = [
|
|
27
|
+
ScrollbarStyles,
|
|
28
|
+
css`
|
|
29
|
+
:host {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
min-height: 0;
|
|
33
|
+
overflow-y: auto;
|
|
34
|
+
background-color: var(--md-sys-color-surface-container-lowest);
|
|
35
|
+
color: var(--md-sys-color-on-surface);
|
|
36
|
+
font: var(--label-font, inherit);
|
|
37
|
+
font-size: 0.76rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
section {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
gap: var(--spacing-medium, 8px);
|
|
44
|
+
padding: var(--spacing-large, 12px);
|
|
45
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* 속성 패널의 묶음 제목과 같은 규격이다 — 두 탭이 다른 눈금을 쓰면 따로 논다 */
|
|
49
|
+
h3 {
|
|
50
|
+
margin: 0 0 2px 0;
|
|
51
|
+
font-size: var(--ui-t-micro, 11px);
|
|
52
|
+
font-weight: 700;
|
|
53
|
+
letter-spacing: 0.04em;
|
|
54
|
+
text-transform: uppercase;
|
|
55
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* 저장되지 않는다는 것을 한 번 말해 둔다 */
|
|
59
|
+
p[quiet] {
|
|
60
|
+
margin: 0;
|
|
61
|
+
font-size: 0.72rem;
|
|
62
|
+
line-height: 1.5;
|
|
63
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
label[row] {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: space-between;
|
|
70
|
+
gap: var(--spacing-medium, 8px);
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
}
|
|
73
|
+
label[row] span {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
}
|
|
77
|
+
label[row] em {
|
|
78
|
+
font-style: normal;
|
|
79
|
+
font-size: 0.7rem;
|
|
80
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
input[type='checkbox'] {
|
|
84
|
+
flex: none;
|
|
85
|
+
width: 16px;
|
|
86
|
+
height: 16px;
|
|
87
|
+
margin: 0;
|
|
88
|
+
accent-color: var(--md-sys-color-primary);
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
label[field] {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 4px;
|
|
96
|
+
}
|
|
97
|
+
label[field] > span {
|
|
98
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
select,
|
|
102
|
+
input[type='number'],
|
|
103
|
+
input[type='color'] {
|
|
104
|
+
width: 100%;
|
|
105
|
+
padding: 5px 6px;
|
|
106
|
+
font: var(--input-field-font, inherit);
|
|
107
|
+
font-size: 0.76rem;
|
|
108
|
+
color: var(--md-sys-color-on-surface);
|
|
109
|
+
background-color: var(--md-sys-color-surface);
|
|
110
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
}
|
|
113
|
+
input[type='color'] {
|
|
114
|
+
height: 30px;
|
|
115
|
+
padding: 2px;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* 값이 옆에 붙어야 끌면서 얼마인지 안다 */
|
|
120
|
+
div[slider] {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: var(--spacing-medium, 8px);
|
|
124
|
+
}
|
|
125
|
+
input[type='range'] {
|
|
126
|
+
flex: 1;
|
|
127
|
+
accent-color: var(--md-sys-color-primary);
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
}
|
|
130
|
+
div[slider] output {
|
|
131
|
+
flex: none;
|
|
132
|
+
width: 2.4em;
|
|
133
|
+
text-align: right;
|
|
134
|
+
font: var(--mono-font, ui-monospace, monospace);
|
|
135
|
+
font-size: 0.72rem;
|
|
136
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
button[reset] {
|
|
140
|
+
align-self: flex-start;
|
|
141
|
+
padding: 5px 12px;
|
|
142
|
+
font: var(--label-font, inherit);
|
|
143
|
+
font-size: 0.74rem;
|
|
144
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
145
|
+
background: none;
|
|
146
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
147
|
+
border-radius: 4px;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
}
|
|
150
|
+
button[reset]:hover {
|
|
151
|
+
background-color: var(--md-sys-color-surface-container);
|
|
152
|
+
}
|
|
153
|
+
`
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
@property({ type: Object }) view: ViewSettings = DEFAULT_VIEW
|
|
157
|
+
|
|
158
|
+
render() {
|
|
159
|
+
const view = this.view ?? DEFAULT_VIEW
|
|
160
|
+
|
|
161
|
+
return html`
|
|
162
|
+
<!--
|
|
163
|
+
놓이는 도면.
|
|
164
|
+
|
|
165
|
+
이 묶음이 맨 위인 것은, 나머지가 「어떻게 보이나」인데 이것은 「무엇으로 보고
|
|
166
|
+
있나」이기 때문이다. 미리보기가 도면에 놓인 모습을 말하는 자리이므로, 어떤 도면을
|
|
167
|
+
가정하는지가 먼저 정해져야 그 다음이 뜻을 갖는다.
|
|
168
|
+
|
|
169
|
+
도형 탭의 「배치 기준」과 짝이지 같은 것이 아니다. 저쪽은 이 도형이 어느 면에
|
|
170
|
+
붙는가(저장된다), 이쪽은 그 면을 가진 도면이 어느 쪽을 기준으로 재는가(저장 안 된다).
|
|
171
|
+
-->
|
|
172
|
+
<section>
|
|
173
|
+
<h3>${i18next.t('figure.label.assumed-drawing')}</h3>
|
|
174
|
+
<p quiet>${i18next.t('figure.text.view-settings-are-not-saved')}</p>
|
|
175
|
+
|
|
176
|
+
<label field>
|
|
177
|
+
<span>${i18next.t('figure.label.scene-reference')}</span>
|
|
178
|
+
<select
|
|
179
|
+
.value=${view.placement}
|
|
180
|
+
@change=${(e: Event) =>
|
|
181
|
+
this.put({ placement: (e.target as HTMLSelectElement).value as ViewSettings['placement'] })}
|
|
182
|
+
>
|
|
183
|
+
${SCENE_REFERENCES.map(
|
|
184
|
+
name => html`<option value=${name} ?selected=${view.placement === name}>
|
|
185
|
+
${i18next.t(`figure.label.scene-reference-${name}`)}
|
|
186
|
+
</option>`
|
|
187
|
+
)}
|
|
188
|
+
</select>
|
|
189
|
+
</label>
|
|
190
|
+
<p quiet>${i18next.t('figure.text.scene-reference-explained')}</p>
|
|
191
|
+
|
|
192
|
+
<label field>
|
|
193
|
+
<span>${i18next.t('figure.label.ceiling-height')}</span>
|
|
194
|
+
<input
|
|
195
|
+
type="number"
|
|
196
|
+
min="0"
|
|
197
|
+
step="100"
|
|
198
|
+
.value=${String(view.ceilingHeight)}
|
|
199
|
+
@change=${(e: Event) => this.put({ ceilingHeight: Number((e.target as HTMLInputElement).value) || 0 })}
|
|
200
|
+
/>
|
|
201
|
+
</label>
|
|
202
|
+
<p quiet>${i18next.t('figure.text.ceiling-height-explained')}</p>
|
|
203
|
+
</section>
|
|
204
|
+
|
|
205
|
+
<section>
|
|
206
|
+
<h3>${i18next.t('figure.label.guides')}</h3>
|
|
207
|
+
|
|
208
|
+
${this.toggle('grid', view.grid, i18next.t('figure.label.coordinate-ground'), i18next.t('figure.text.coordinate-ground-explained'))}
|
|
209
|
+
${this.toggle('gridLabels', view.gridLabels, i18next.t('figure.label.axis-labels'))}
|
|
210
|
+
${this.toggle('floor', view.floor, i18next.t('figure.label.scene-floor'), i18next.t('figure.text.floor-explained'))}
|
|
211
|
+
</section>
|
|
212
|
+
|
|
213
|
+
<section>
|
|
214
|
+
<h3>${i18next.t('figure.label.environment')}</h3>
|
|
215
|
+
|
|
216
|
+
<label field>
|
|
217
|
+
<span>${i18next.t('figure.label.preset')}</span>
|
|
218
|
+
<select
|
|
219
|
+
.value=${view.environment}
|
|
220
|
+
@change=${(e: Event) => this.put({ environment: (e.target as HTMLSelectElement).value as EnvironmentName })}
|
|
221
|
+
>
|
|
222
|
+
${ENVIRONMENTS.map(
|
|
223
|
+
name => html`<option value=${name} ?selected=${view.environment === name}>
|
|
224
|
+
${i18next.t(`figure.label.environment-${name}`)}
|
|
225
|
+
</option>`
|
|
226
|
+
)}
|
|
227
|
+
</select>
|
|
228
|
+
</label>
|
|
229
|
+
<p quiet>${i18next.t('figure.text.environment-explained')}</p>
|
|
230
|
+
|
|
231
|
+
<label field>
|
|
232
|
+
<span>${i18next.t('figure.label.sky-color')}</span>
|
|
233
|
+
<input
|
|
234
|
+
type="color"
|
|
235
|
+
.value=${view.skyColor}
|
|
236
|
+
@change=${(e: Event) => this.put({ skyColor: (e.target as HTMLInputElement).value })}
|
|
237
|
+
/>
|
|
238
|
+
</label>
|
|
239
|
+
</section>
|
|
240
|
+
|
|
241
|
+
<section>
|
|
242
|
+
<h3>${i18next.t('figure.label.key-light')}</h3>
|
|
243
|
+
|
|
244
|
+
${this.slider('dirLightIntensity', view.dirLightIntensity, i18next.t('figure.label.intensity'), 0, 5, 0.1)}
|
|
245
|
+
${this.toggle(
|
|
246
|
+
'dirLightFollowCamera',
|
|
247
|
+
view.dirLightFollowCamera,
|
|
248
|
+
i18next.t('figure.label.follow-camera'),
|
|
249
|
+
i18next.t('figure.text.follow-camera-explained')
|
|
250
|
+
)}
|
|
251
|
+
${this.toggle('dirShadowEnabled', view.dirShadowEnabled, i18next.t('figure.label.cast-shadow'))}
|
|
252
|
+
${view.dirLightIntensity === 0 && view.hemiIntensity === 0
|
|
253
|
+
? html`<p quiet>${i18next.t('figure.text.all-lights-off')}</p>`
|
|
254
|
+
: ''}
|
|
255
|
+
</section>
|
|
256
|
+
|
|
257
|
+
<section>
|
|
258
|
+
<h3>${i18next.t('figure.label.hemisphere-light')}</h3>
|
|
259
|
+
|
|
260
|
+
${this.slider('hemiIntensity', view.hemiIntensity, i18next.t('figure.label.intensity'), 0, 8, 0.1)}
|
|
261
|
+
<p quiet>${i18next.t('figure.text.hemisphere-light-explained')}</p>
|
|
262
|
+
</section>
|
|
263
|
+
|
|
264
|
+
<section>
|
|
265
|
+
<button reset @click=${() => this.reset()}>${i18next.t('figure.button.reset-view')}</button>
|
|
266
|
+
</section>
|
|
267
|
+
`
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private toggle(key: keyof ViewSettings, on: boolean, label: string, hint?: string) {
|
|
271
|
+
return html`
|
|
272
|
+
<label row>
|
|
273
|
+
<span>
|
|
274
|
+
${label}${hint ? html`<em>${hint}</em>` : ''}
|
|
275
|
+
</span>
|
|
276
|
+
<input
|
|
277
|
+
type="checkbox"
|
|
278
|
+
.checked=${on}
|
|
279
|
+
@change=${(e: Event) => this.put({ [key]: (e.target as HTMLInputElement).checked } as Partial<ViewSettings>)}
|
|
280
|
+
/>
|
|
281
|
+
</label>
|
|
282
|
+
`
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private slider(key: keyof ViewSettings, value: number, label: string, min: number, max: number, step: number) {
|
|
286
|
+
return html`
|
|
287
|
+
<label field>
|
|
288
|
+
<span>${label}</span>
|
|
289
|
+
<div slider>
|
|
290
|
+
<input
|
|
291
|
+
type="range"
|
|
292
|
+
min=${min}
|
|
293
|
+
max=${max}
|
|
294
|
+
step=${step}
|
|
295
|
+
.value=${String(value)}
|
|
296
|
+
@input=${(e: Event) =>
|
|
297
|
+
this.put({ [key]: Number((e.target as HTMLInputElement).value) } as Partial<ViewSettings>)}
|
|
298
|
+
/>
|
|
299
|
+
<output>${value.toFixed(1)}</output>
|
|
300
|
+
</div>
|
|
301
|
+
</label>
|
|
302
|
+
`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** 처음 값으로 되돌린다. 만지다 길을 잃었을 때 돌아올 자리가 있어야 한다. */
|
|
306
|
+
private reset() {
|
|
307
|
+
this.tell({ ...DEFAULT_VIEW })
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private put(patch: Partial<ViewSettings>) {
|
|
311
|
+
this.tell({ ...(this.view ?? DEFAULT_VIEW), ...patch })
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 바뀐 것을 알린다.
|
|
316
|
+
*
|
|
317
|
+
* **알리기 전에 내 것도 바꿔 둔다.** Lit 은 속성을 다음 렌더에 넣으므로, 같은 프레임에
|
|
318
|
+
* 두 번 고치면 두 번째가 `this.view` 의 옛 값을 읽어 첫 번째를 덮는다. 실제로 그랬다 —
|
|
319
|
+
* 축 가이드를 끄고 바닥판을 켰더니 축 가이드가 도로 켜져 있었고, 화면의 체크 상자와
|
|
320
|
+
* 씬의 상태가 서로 다른 말을 했다.
|
|
321
|
+
*
|
|
322
|
+
* 정본은 여전히 모델러 페이지가 갖는다. 여기 값은 다음 렌더에 같은 것으로 덮인다.
|
|
323
|
+
*/
|
|
324
|
+
private tell(view: ViewSettings) {
|
|
325
|
+
this.view = view
|
|
326
|
+
this.dispatchEvent(new CustomEvent('update-view', { detail: { view }, bubbles: true, composed: true }))
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { css, html, LitElement, nothing } from 'lit'
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
3
|
+
import { live } from 'lit/directives/live.js'
|
|
4
|
+
|
|
5
|
+
import { i18next, localize } from '@operato/i18n'
|
|
6
|
+
import { v3BindingsOf } from '@hatiolab/figure-model'
|
|
7
|
+
import type { FigureCapability, V3Asset } from '@hatiolab/figure-model'
|
|
8
|
+
import { SidePanelStyles } from './side-panel-styles.js'
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
One rung per set of the format's capability names. Typed as the format's names, so a misspelt rung does not pass
|
|
12
|
+
(ADR-0051 ④, the V2 ladder's lesson).
|
|
13
|
+
*/
|
|
14
|
+
type Rung = 'none' | 'hold' | 'flow'
|
|
15
|
+
const RUNGS: Readonly<Record<Rung, readonly FigureCapability[]>> = Object.freeze({
|
|
16
|
+
none: [],
|
|
17
|
+
hold: ['Capacity', 'Holdable'],
|
|
18
|
+
flow: ['FlowNode']
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/** Which rung a set of names is, or null when it is none of them -- not invented. */
|
|
22
|
+
export function rungOf(names: readonly string[] | undefined): Rung | null {
|
|
23
|
+
const has = new Set(names ?? [])
|
|
24
|
+
if (has.size === 0) return 'none'
|
|
25
|
+
for (const [rung, wanted] of Object.entries(RUNGS) as [Rung, readonly FigureCapability[]][])
|
|
26
|
+
if (wanted.length === has.size && wanted.every(name => has.has(name))) return rung
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 자리·능력 — what the figure does on a board (V2's 「능력」 tab, parity row ⑤): the ladder from drawing only, to
|
|
32
|
+
* holding things, to passing them on, and every place things are held, whatever part it is on. A place is not drawn,
|
|
33
|
+
* so the list is the only way to find one; choosing a row selects its part, where the place is edited.
|
|
34
|
+
*/
|
|
35
|
+
@customElement('figure-v3-capability')
|
|
36
|
+
export class FigureV3Capability extends localize(i18next)(LitElement) {
|
|
37
|
+
static styles = [SidePanelStyles, css`
|
|
38
|
+
:host {
|
|
39
|
+
display: block;
|
|
40
|
+
}
|
|
41
|
+
label[rung] {
|
|
42
|
+
display: flex;
|
|
43
|
+
gap: 8px;
|
|
44
|
+
align-items: flex-start;
|
|
45
|
+
padding: 8px;
|
|
46
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
47
|
+
border-radius: 8px;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
}
|
|
50
|
+
label[rung][on] {
|
|
51
|
+
border-color: var(--md-sys-color-primary);
|
|
52
|
+
background: var(--md-sys-color-secondary-container);
|
|
53
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
54
|
+
}
|
|
55
|
+
label[rung] span {
|
|
56
|
+
display: grid;
|
|
57
|
+
gap: 2px;
|
|
58
|
+
}
|
|
59
|
+
label[rung] em,
|
|
60
|
+
p[hint],
|
|
61
|
+
small {
|
|
62
|
+
font-style: normal;
|
|
63
|
+
margin: 0;
|
|
64
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
65
|
+
}
|
|
66
|
+
button[place] {
|
|
67
|
+
display: grid;
|
|
68
|
+
gap: 2px;
|
|
69
|
+
text-align: left;
|
|
70
|
+
padding: 6px 8px;
|
|
71
|
+
font: inherit;
|
|
72
|
+
color: inherit;
|
|
73
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
74
|
+
border-radius: 8px;
|
|
75
|
+
background: none;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
}
|
|
78
|
+
button[place][on] {
|
|
79
|
+
border-color: var(--md-sys-color-primary);
|
|
80
|
+
}
|
|
81
|
+
code {
|
|
82
|
+
font-size: 0.9em;
|
|
83
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
84
|
+
}
|
|
85
|
+
`]
|
|
86
|
+
|
|
87
|
+
@property({ attribute: false }) asset?: V3Asset
|
|
88
|
+
/** The selected part, whose places are marked. */
|
|
89
|
+
@property() selected = ''
|
|
90
|
+
@property({ type: Boolean }) locked = false
|
|
91
|
+
|
|
92
|
+
render() {
|
|
93
|
+
const asset = this.asset
|
|
94
|
+
if (!asset) return nothing
|
|
95
|
+
const names = asset.document.capabilities.names ?? []
|
|
96
|
+
const rung = rungOf(names)
|
|
97
|
+
const places = v3BindingsOf(asset)
|
|
98
|
+
return html`<section>
|
|
99
|
+
<h3>${i18next.t('figure.label.what-this-does')}</h3>
|
|
100
|
+
${(['none', 'hold', 'flow'] as const).map(
|
|
101
|
+
which => html`<label rung ?on=${rung === which}>
|
|
102
|
+
<input type="radio" name="rung" .checked=${live(rung === which)} ?disabled=${this.locked} @change=${() => this.setRung(which)} />
|
|
103
|
+
<span>
|
|
104
|
+
<strong>${i18next.t(`figure.label.rung-${which}`)}</strong>
|
|
105
|
+
<em>${i18next.t(`figure.text.rung-${which}-note`)}</em>
|
|
106
|
+
</span>
|
|
107
|
+
</label>`
|
|
108
|
+
)}
|
|
109
|
+
${rung === null ? html`<p hint>${i18next.t('figure.text.capability-hand-written')}</p>` : nothing}
|
|
110
|
+
${names.length ? html`<code>${names.join(' · ')}</code>` : nothing}
|
|
111
|
+
</section>
|
|
112
|
+
<section>
|
|
113
|
+
<h3>${i18next.t('figure.label.places')}</h3>
|
|
114
|
+
${places.length
|
|
115
|
+
? places.map(
|
|
116
|
+
one => html`<button place ?on=${one.part === this.selected} @click=${() => one.part && this.pick(one.part)}>
|
|
117
|
+
<strong>${one.label}</strong>
|
|
118
|
+
<small>
|
|
119
|
+
${i18next.t('figure.label.place-' + one.role)} · ${one.part ?? '?'}${one.face ? ' · ' + i18next.t('figure.label.face-' + one.face) : ''} ·
|
|
120
|
+
${one.accepts.join(', ')} · ${i18next.t('figure.label.place-holds', { count: one.capacity })}
|
|
121
|
+
</small>
|
|
122
|
+
</button>`
|
|
123
|
+
)
|
|
124
|
+
: html`<p hint>${i18next.t('figure.text.no-places')}</p>`}
|
|
125
|
+
${places.length && rung === 'none' ? html`<p hint>${i18next.t('figure.text.place-needs-holding')}</p>` : nothing}
|
|
126
|
+
</section>
|
|
127
|
+
`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private setRung(which: Rung) {
|
|
131
|
+
this.dispatchEvent(new CustomEvent('v3-part-edit', { detail: { edit: { kind: 'set-capabilities', names: [...RUNGS[which]] } }, bubbles: true, composed: true }))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private pick(part: string) {
|
|
135
|
+
this.dispatchEvent(new CustomEvent('select-part', { detail: { id: part }, bubbles: true, composed: true }))
|
|
136
|
+
}
|
|
137
|
+
}
|