@things-factory/figure-ui 10.1.46 → 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 +61 -81
- 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 +531 -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 +93 -9
- 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 +5 -2
- package/dist-client/modeller/figure-v3-links.js +64 -82
- 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 +555 -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 +30 -1
- package/dist-client/modeller/v3-links.js +94 -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 -1
- 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 +77 -6
- 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 +84 -5
- package/translations/ja.json +86 -7
- package/translations/ko.json +84 -5
- package/translations/ms.json +86 -7
- package/translations/zh.json +86 -7
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
AXES,
|
|
13
13
|
canFasten,
|
|
14
14
|
canFollow,
|
|
15
|
+
dimensionNow,
|
|
16
|
+
followableAxes,
|
|
17
|
+
positionIsShare,
|
|
15
18
|
choiceOf,
|
|
16
19
|
fastenedTo,
|
|
17
20
|
followOf,
|
|
@@ -25,6 +28,8 @@ import {
|
|
|
25
28
|
type Axis,
|
|
26
29
|
type Follow
|
|
27
30
|
} from './v3-links.js'
|
|
31
|
+
import { SidePanelStyles } from './side-panel-styles.js'
|
|
32
|
+
import { HelpToggles } from './v3-help.js'
|
|
28
33
|
|
|
29
34
|
const FACES = Object.keys(V3_FACES) as V3Face[]
|
|
30
35
|
|
|
@@ -43,76 +48,25 @@ const FACES = Object.keys(V3_FACES) as V3Face[]
|
|
|
43
48
|
*/
|
|
44
49
|
@customElement('figure-v3-links')
|
|
45
50
|
export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
46
|
-
|
|
51
|
+
private readonly help = new HelpToggles(this)
|
|
52
|
+
static styles = [SidePanelStyles, css`
|
|
47
53
|
:host {
|
|
48
|
-
display:
|
|
49
|
-
gap: 10px;
|
|
54
|
+
display: block;
|
|
50
55
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
/* The measure's name, then its rule: X, or a cylinder's 반지름 · 길이, on one line. */
|
|
57
|
+
div[row] > b {
|
|
58
|
+
white-space: nowrap;
|
|
54
59
|
}
|
|
55
60
|
div[row] {
|
|
56
61
|
display: grid;
|
|
57
|
-
grid-template-columns: 24px minmax(0, 1fr);
|
|
58
|
-
gap: 6px;
|
|
59
|
-
align-items: center;
|
|
60
|
-
}
|
|
61
|
-
div[form] {
|
|
62
|
-
display: grid;
|
|
63
|
-
grid-template-columns: 96px minmax(0, 1fr);
|
|
62
|
+
grid-template-columns: minmax(24px, max-content) minmax(0, 1fr);
|
|
64
63
|
gap: 6px;
|
|
65
64
|
align-items: center;
|
|
66
65
|
}
|
|
67
|
-
div[pair] {
|
|
68
|
-
display: flex;
|
|
69
|
-
gap: 6px;
|
|
70
|
-
}
|
|
71
|
-
div[pair] > * {
|
|
72
|
-
flex: 1;
|
|
73
|
-
min-width: 0;
|
|
74
|
-
}
|
|
75
|
-
select,
|
|
76
|
-
input,
|
|
77
|
-
button {
|
|
78
|
-
font: inherit;
|
|
79
|
-
min-width: 0;
|
|
80
|
-
}
|
|
81
|
-
button[act] {
|
|
82
|
-
justify-self: start;
|
|
83
|
-
display: inline-flex;
|
|
84
|
-
align-items: center;
|
|
85
|
-
gap: 4px;
|
|
86
|
-
padding: 4px 10px;
|
|
87
|
-
border: none;
|
|
88
|
-
border-radius: 999em;
|
|
89
|
-
cursor: pointer;
|
|
90
|
-
color: var(--md-sys-color-on-secondary-container);
|
|
91
|
-
background: var(--md-sys-color-secondary-container);
|
|
92
|
-
}
|
|
93
|
-
button[act]:disabled {
|
|
94
|
-
opacity: 0.5;
|
|
95
|
-
cursor: default;
|
|
96
|
-
}
|
|
97
|
-
button[act] md-icon {
|
|
98
|
-
font-size: 16px;
|
|
99
|
-
width: 16px;
|
|
100
|
-
height: 16px;
|
|
101
|
-
}
|
|
102
|
-
small,
|
|
103
|
-
p[hint] {
|
|
104
|
-
margin: 0;
|
|
105
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
106
|
-
}
|
|
107
66
|
b[bound] {
|
|
108
67
|
font-weight: 600;
|
|
109
68
|
}
|
|
110
|
-
|
|
111
|
-
display: flex;
|
|
112
|
-
align-items: center;
|
|
113
|
-
gap: 6px;
|
|
114
|
-
}
|
|
115
|
-
`
|
|
69
|
+
`]
|
|
116
70
|
|
|
117
71
|
/** The asset being edited. */
|
|
118
72
|
@property({ attribute: false }) asset?: V3Asset
|
|
@@ -121,6 +75,8 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
121
75
|
/** The part's own extent along its axes now, in mm — what a dimension is fixed at when it stops following. */
|
|
122
76
|
@property({ attribute: false }) dims?: [number, number, number]
|
|
123
77
|
@property({ type: Boolean }) locked = false
|
|
78
|
+
/** Which ties this shows: the part's shape (size and placement, in 속성), its places (자리·능력) or its motion (움직임). */
|
|
79
|
+
@property() section: 'shape' | 'places' | 'motion' = 'shape'
|
|
124
80
|
|
|
125
81
|
@state() private fasten = { face: 'bottom' as V3Face, toPlane: false, target: '', targetFace: 'top' as V3Face, facing: 'meet' as 'meet' | 'flush', gap: 0 }
|
|
126
82
|
@state() private motion = { kind: 'slide' as 'slide' | 'turn', axis: 'x' as Axis, id: '', min: 0, max: 500 }
|
|
@@ -129,16 +85,22 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
129
85
|
render() {
|
|
130
86
|
const asset = this.asset
|
|
131
87
|
if (!asset || !this.partId) return nothing
|
|
88
|
+
if (this.section === 'motion')
|
|
89
|
+
return html`<section><h3>${i18next.t('figure.label.motion')}${this.help.button('figure.text.motion-explained')}</h3>${this.help.hint('figure.text.motion-explained')}
|
|
90
|
+
${this.renderMotion(asset)}</section>`
|
|
91
|
+
if (this.section === 'places')
|
|
92
|
+
return html`<section><h3>${i18next.t('figure.label.places-on-part', { part: this.partId })}${this.help.button('figure.text.place-explained')}</h3>${this.help.hint('figure.text.place-explained')}
|
|
93
|
+
${placeFacesOf(asset, this.partId).length ? this.renderPlaces(asset) : html`<p hint>${i18next.t('figure.text.places-need-a-box')}</p>`}</section>`
|
|
132
94
|
return html`
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
95
|
+
<section>
|
|
96
|
+
<h3>${i18next.t('figure.label.dimensions')}</h3>
|
|
97
|
+
${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
98
|
+
</section>
|
|
99
|
+
<section>
|
|
100
|
+
<h3>${i18next.t('figure.label.placement')}${this.help.button('figure.text.fastening-explained')}</h3>${this.help.hint('figure.text.fastening-explained')}
|
|
101
|
+
${this.renderPositions(asset)}
|
|
102
|
+
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
103
|
+
</section>
|
|
142
104
|
`
|
|
143
105
|
}
|
|
144
106
|
|
|
@@ -162,7 +124,6 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
162
124
|
</button>
|
|
163
125
|
</div>`
|
|
164
126
|
)}
|
|
165
|
-
<p hint>${i18next.t('figure.text.place-explained')}</p>
|
|
166
127
|
${holdsThings(asset)
|
|
167
128
|
? nothing
|
|
168
129
|
: html`<p hint>${i18next.t('figure.text.place-needs-holding')}</p>
|
|
@@ -215,17 +176,23 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
215
176
|
|
|
216
177
|
private renderDimensions(asset: V3Asset) {
|
|
217
178
|
const others = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFollow(asset, one))
|
|
218
|
-
|
|
179
|
+
const round = followableAxes(asset, this.partId).length === 2
|
|
180
|
+
return followableAxes(asset, this.partId).map(axis => {
|
|
219
181
|
const now = followOf(asset, this.partId, axis)
|
|
220
182
|
const value = choiceOf(now)
|
|
183
|
+
/* A cylinder's two measures are its radius and its length; its X and Y name them. */
|
|
184
|
+
const name = round ? i18next.t(axis === 'x' ? 'figure.label.field-radius' : 'figure.label.field-length') : axis.toUpperCase()
|
|
221
185
|
return html`<div row>
|
|
222
|
-
<b>${
|
|
223
|
-
<select ?disabled=${this.locked
|
|
186
|
+
<b>${name}</b>
|
|
187
|
+
<select ?disabled=${this.locked} @change=${(e: Event) => this.follow(asset, axis, (e.target as HTMLSelectElement).value, now)}>
|
|
224
188
|
<option value="own" ?selected=${value === 'own'}>${i18next.t('figure.label.follows-nothing')}</option>
|
|
225
189
|
${AXES.filter(one => hasInstanceSize(asset, one)).map(
|
|
226
|
-
one => html`<option value=${`
|
|
227
|
-
|
|
228
|
-
|
|
190
|
+
one => html`<option value=${`share:${one}`} ?selected=${value === `share:${one}`}>
|
|
191
|
+
${i18next.t('figure.label.follows-instance-share')} · ${one.toUpperCase()}
|
|
192
|
+
</option>
|
|
193
|
+
<option value=${`instance:${one}`} ?selected=${value === `instance:${one}`}>
|
|
194
|
+
${i18next.t('figure.label.follows-instance')} · ${one.toUpperCase()}
|
|
195
|
+
</option>`
|
|
229
196
|
)}
|
|
230
197
|
${others.flatMap(other =>
|
|
231
198
|
AXES.map(
|
|
@@ -240,15 +207,24 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
240
207
|
})
|
|
241
208
|
}
|
|
242
209
|
|
|
243
|
-
private follow(axis: Axis, choice: string, now: Follow) {
|
|
210
|
+
private follow(asset: V3Asset, axis: Axis, choice: string, now: Follow) {
|
|
244
211
|
const following = now.of !== 'own'
|
|
212
|
+
/* The measure as it stands: a dimension stops following at it, and a share is taken of it. */
|
|
213
|
+
const current = dimensionNow(asset, this.partId, axis) ?? this.dims?.[AXES.indexOf(axis)] ?? 100
|
|
245
214
|
if (choice === 'own') {
|
|
246
215
|
if (!following) return
|
|
247
|
-
|
|
248
|
-
this.command({ kind: 'unlink-dimension', part: this.partId, axis, mm })
|
|
216
|
+
this.command({ kind: 'unlink-dimension', part: this.partId, axis, mm: Math.max(1, Math.round(current)) })
|
|
249
217
|
return
|
|
250
218
|
}
|
|
251
219
|
const [of, a, b] = choice.split(':')
|
|
220
|
+
if (of === 'share') {
|
|
221
|
+
/* V2's proportional rule: this measure is the share of the instance size it is now, so it grows with the figure. */
|
|
222
|
+
const size = asset.designInputs[`size.${a}`]
|
|
223
|
+
if (!size) return
|
|
224
|
+
const times = Math.round((current / size) * 1e6) / 1e6
|
|
225
|
+
this.command({ kind: 'link-dimension', part: this.partId, axis, source: { of: 'instance', axis: a as Axis }, times, replace: following })
|
|
226
|
+
return
|
|
227
|
+
}
|
|
252
228
|
const source: V3DimensionSource = of === 'instance' ? { of: 'instance', axis: a as Axis } : { of: 'part', part: a!, axis: b as Axis }
|
|
253
229
|
this.command({ kind: 'link-dimension', part: this.partId, axis, source, replace: following })
|
|
254
230
|
}
|
|
@@ -260,10 +236,15 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
260
236
|
private renderPositions(asset: V3Asset) {
|
|
261
237
|
return positionsWorkedOut(asset, this.partId).map(
|
|
262
238
|
axis => html`<div pair>
|
|
263
|
-
<span>${axis.toUpperCase()} · ${i18next.t('figure.label.position-worked-out')}</span>
|
|
239
|
+
<span>${axis.toUpperCase()} · ${i18next.t(positionIsShare(asset, this.partId, axis) ? 'figure.label.position-in-proportion' : 'figure.label.position-worked-out')}</span>
|
|
264
240
|
<button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'fix-position', part: this.partId, axis })}>
|
|
265
241
|
<md-icon>push_pin</md-icon>${i18next.t('figure.button.fix-position')}
|
|
266
242
|
</button>
|
|
243
|
+
${hasInstanceSize(asset, axis) && !positionIsShare(asset, this.partId, axis)
|
|
244
|
+
? html`<button act ?disabled=${this.locked} title=${i18next.t('figure.text.scale-position-explained')} @click=${() => this.command({ kind: 'scale-position', part: this.partId, axis })}>
|
|
245
|
+
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.scale-position')}
|
|
246
|
+
</button>`
|
|
247
|
+
: nothing}
|
|
267
248
|
</div>`
|
|
268
249
|
)
|
|
269
250
|
}
|
|
@@ -280,7 +261,7 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
280
261
|
<md-icon>link_off</md-icon>${i18next.t('figure.button.detach')}
|
|
281
262
|
</button>
|
|
282
263
|
</div>`
|
|
283
|
-
:
|
|
264
|
+
: nothing}
|
|
284
265
|
<div form>
|
|
285
266
|
<span>${i18next.t('figure.label.my-face')}</span>
|
|
286
267
|
<select @change=${(e: Event) => (this.fasten = { ...f, face: (e.target as HTMLSelectElement).value as V3Face })}>
|
|
@@ -348,7 +329,6 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
348
329
|
const m = this.motion
|
|
349
330
|
const unit = m.kind === 'turn' ? 'deg' : 'mm'
|
|
350
331
|
return html`
|
|
351
|
-
<p hint>${i18next.t('figure.text.motion-explained')}</p>
|
|
352
332
|
<div pair>
|
|
353
333
|
<select @change=${(e: Event) => this.setKind((e.target as HTMLSelectElement).value as 'slide' | 'turn')}>
|
|
354
334
|
<option value="slide" ?selected=${m.kind === 'slide'}>${i18next.t('figure.label.motion-slide')}</option>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { css, html, LitElement, nothing } from 'lit'
|
|
2
|
-
import { customElement, property } from 'lit/decorators.js'
|
|
1
|
+
import { css, html, LitElement, nothing, svg } from 'lit'
|
|
2
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
3
3
|
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
5
|
import { MATERIAL_PRESETS, SEGMENT_PRESETS, v3TrianglesOf } from '@hatiolab/figure-model'
|
|
6
6
|
import { activePalette } from '@hatiolab/things-scene'
|
|
7
7
|
import type { V3AssetGeometry, V3Material, V3PartEdit } from '@hatiolab/figure-model'
|
|
8
|
+
import { SidePanelStyles } from './side-panel-styles.js'
|
|
8
9
|
|
|
9
10
|
/** The purpose each palette role is for, by its token — the V2 inspector's words for the same roles. */
|
|
10
11
|
const PURPOSE: Record<string, string> = {
|
|
@@ -40,14 +41,9 @@ const DEFAULT_SEGMENTS = 12
|
|
|
40
41
|
*/
|
|
41
42
|
@customElement('figure-v3-material')
|
|
42
43
|
export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
43
|
-
static styles = css`
|
|
44
|
+
static styles = [SidePanelStyles, css`
|
|
44
45
|
:host {
|
|
45
|
-
display:
|
|
46
|
-
gap: 10px;
|
|
47
|
-
}
|
|
48
|
-
h3 {
|
|
49
|
-
font-size: var(--ui-t-body, 13px);
|
|
50
|
-
margin: 12px 0 0;
|
|
46
|
+
display: block;
|
|
51
47
|
}
|
|
52
48
|
div[field] {
|
|
53
49
|
display: grid;
|
|
@@ -74,46 +70,96 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
74
70
|
outline: 2px solid var(--md-sys-color-primary);
|
|
75
71
|
outline-offset: 1px;
|
|
76
72
|
}
|
|
77
|
-
small {
|
|
78
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
79
|
-
}
|
|
80
73
|
select,
|
|
81
74
|
input[type='range'] {
|
|
82
75
|
font: inherit;
|
|
83
76
|
min-width: 0;
|
|
84
77
|
}
|
|
85
|
-
|
|
86
|
-
display: flex;
|
|
87
|
-
align-items: center;
|
|
88
|
-
gap: 6px;
|
|
89
|
-
}
|
|
78
|
+
/* V2's segment picker: the four common counts as their silhouettes with their triangle cost, the rest folded. */
|
|
90
79
|
div[segments] {
|
|
91
80
|
display: grid;
|
|
92
|
-
grid-template-columns: repeat(
|
|
81
|
+
grid-template-columns: repeat(4, 1fr);
|
|
93
82
|
gap: 4px;
|
|
94
83
|
}
|
|
95
|
-
|
|
96
|
-
display:
|
|
97
|
-
|
|
84
|
+
button[seg] {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: 1px;
|
|
89
|
+
padding: 6px 2px 4px;
|
|
98
90
|
font: inherit;
|
|
91
|
+
color: var(--md-sys-color-on-surface);
|
|
92
|
+
background: none;
|
|
99
93
|
border: 1px solid var(--md-sys-color-outline-variant);
|
|
100
94
|
border-radius: 6px;
|
|
101
|
-
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
}
|
|
97
|
+
button[seg]:hover:not([on]) {
|
|
98
|
+
background-color: var(--md-sys-color-surface-container);
|
|
99
|
+
}
|
|
100
|
+
button[seg][on] {
|
|
101
|
+
color: var(--md-sys-color-on-primary-container);
|
|
102
|
+
background-color: var(--md-sys-color-primary-container);
|
|
103
|
+
border-color: transparent;
|
|
104
|
+
}
|
|
105
|
+
/* Small, and 12 and 16 both read as circles: large enough for the corners to show (V2). */
|
|
106
|
+
button[seg] svg {
|
|
107
|
+
display: block;
|
|
108
|
+
width: 32px;
|
|
109
|
+
height: 32px;
|
|
110
|
+
}
|
|
111
|
+
button[seg] svg polygon {
|
|
112
|
+
fill: var(--md-sys-color-surface-container-highest);
|
|
113
|
+
stroke: currentColor;
|
|
114
|
+
stroke-width: 1.1;
|
|
115
|
+
stroke-linejoin: miter;
|
|
116
|
+
}
|
|
117
|
+
button[seg][on] svg polygon {
|
|
118
|
+
fill: var(--md-sys-color-surface-container-lowest);
|
|
119
|
+
}
|
|
120
|
+
button[seg] b {
|
|
121
|
+
font-size: 0.8rem;
|
|
122
|
+
}
|
|
123
|
+
button[seg] small {
|
|
124
|
+
margin: 0;
|
|
125
|
+
font-size: 0.6rem;
|
|
126
|
+
}
|
|
127
|
+
details[free] summary {
|
|
128
|
+
margin-top: 4px;
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
font-size: 0.68rem;
|
|
131
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
132
|
+
}
|
|
133
|
+
div[presets] {
|
|
134
|
+
display: flex;
|
|
135
|
+
gap: 4px;
|
|
136
|
+
margin-top: 4px;
|
|
137
|
+
}
|
|
138
|
+
div[presets] button {
|
|
139
|
+
flex: 1;
|
|
140
|
+
padding: 3px 0;
|
|
141
|
+
font: inherit;
|
|
142
|
+
font-size: 0.72rem;
|
|
102
143
|
color: var(--md-sys-color-on-surface);
|
|
144
|
+
background: none;
|
|
145
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
146
|
+
border-radius: 6px;
|
|
103
147
|
cursor: pointer;
|
|
104
148
|
}
|
|
105
|
-
div[
|
|
106
|
-
|
|
107
|
-
color: var(--md-sys-color-
|
|
108
|
-
border-color:
|
|
149
|
+
div[presets] button[on] {
|
|
150
|
+
color: var(--md-sys-color-on-primary-container);
|
|
151
|
+
background-color: var(--md-sys-color-primary-container);
|
|
152
|
+
border-color: transparent;
|
|
109
153
|
}
|
|
110
|
-
`
|
|
154
|
+
`]
|
|
111
155
|
|
|
112
156
|
/** The selected body as evaluated — its material, its segments and its shape. */
|
|
113
157
|
@property({ attribute: false }) body?: V3AssetGeometry & { id: string }
|
|
114
158
|
/** The asset's palette, token → colour. */
|
|
115
159
|
@property({ attribute: false }) palette: Record<string, string> = {}
|
|
116
160
|
@property({ type: Boolean }) locked = false
|
|
161
|
+
/** The glow while its slider is dragged: shown beside it and drawn on the canvas, recorded on release. */
|
|
162
|
+
@state() private glowing?: number
|
|
117
163
|
|
|
118
164
|
render() {
|
|
119
165
|
const part = this.body
|
|
@@ -127,7 +173,7 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
127
173
|
const painted = entries.filter(([token]) => !token.startsWith('palette.status-'))
|
|
128
174
|
const lit = entries.filter(([token]) => token.startsWith('palette.status-'))
|
|
129
175
|
const glow = material.emissive
|
|
130
|
-
return html
|
|
176
|
+
return html`<section>
|
|
131
177
|
<h3>${i18next.t('figure.label.part-material')}</h3>
|
|
132
178
|
<div field>
|
|
133
179
|
<label>${i18next.t('figure.label.palette-role')}</label>
|
|
@@ -168,9 +214,10 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
168
214
|
step="0.05"
|
|
169
215
|
?disabled=${this.locked}
|
|
170
216
|
.value=${String(glow?.intensity ?? 0)}
|
|
217
|
+
@input=${(e: Event) => this.glow(Number((e.target as HTMLInputElement).value), true)}
|
|
171
218
|
@change=${(e: Event) => this.glow(Number((e.target as HTMLInputElement).value))}
|
|
172
219
|
/>
|
|
173
|
-
<small>${(glow?.intensity ?? 0).toFixed(2)}</small>
|
|
220
|
+
<small>${(this.glowing ?? glow?.intensity ?? 0).toFixed(2)}</small>
|
|
174
221
|
</div>
|
|
175
222
|
${glow
|
|
176
223
|
? html`<div field>
|
|
@@ -179,7 +226,7 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
179
226
|
</div>`
|
|
180
227
|
: nothing}
|
|
181
228
|
${this.renderSegments(part)}
|
|
182
|
-
|
|
229
|
+
</section>`
|
|
183
230
|
}
|
|
184
231
|
|
|
185
232
|
private renderSegments(part: V3AssetGeometry & { id: string }) {
|
|
@@ -190,17 +237,29 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
190
237
|
<div field>
|
|
191
238
|
<label>${i18next.t('figure.label.segments')}</label>
|
|
192
239
|
<div segments>
|
|
193
|
-
${SEGMENT_PRESETS.map(
|
|
240
|
+
${SEGMENT_PRESETS.slice(0, 4).map(
|
|
194
241
|
preset => html`<button
|
|
242
|
+
seg
|
|
195
243
|
?on=${preset === current}
|
|
196
244
|
?disabled=${this.locked}
|
|
197
245
|
@click=${() => this.edit({ kind: 'set-segments', target: part.id, segments: preset })}
|
|
198
246
|
>
|
|
247
|
+
${silhouette(preset)}
|
|
199
248
|
<b>${preset}</b>
|
|
200
249
|
<small>${i18next.t('figure.text.n-triangles', { count: v3TrianglesOf({ ...part, segments: preset }) })}</small>
|
|
201
250
|
</button>`
|
|
202
251
|
)}
|
|
203
252
|
</div>
|
|
253
|
+
<details free ?open=${SEGMENT_PRESETS.slice(4).includes(current as never)}>
|
|
254
|
+
<summary>${i18next.t('figure.text.more-segment-options')}</summary>
|
|
255
|
+
<div presets>
|
|
256
|
+
${SEGMENT_PRESETS.slice(4).map(
|
|
257
|
+
preset => html`<button ?on=${preset === current} ?disabled=${this.locked} @click=${() => this.edit({ kind: 'set-segments', target: part.id, segments: preset })}>
|
|
258
|
+
${preset}
|
|
259
|
+
</button>`
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
</details>
|
|
204
263
|
</div>
|
|
205
264
|
`
|
|
206
265
|
}
|
|
@@ -224,12 +283,14 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
224
283
|
}
|
|
225
284
|
|
|
226
285
|
/** Intensity 0 is no glow at all, as in V2; a new glow takes the part's own colour until another is picked. */
|
|
227
|
-
private glow(intensity: number) {
|
|
286
|
+
private glow(intensity: number, live = false) {
|
|
228
287
|
const material = this.body!.material
|
|
229
|
-
this.
|
|
288
|
+
this.glowing = live ? intensity : undefined
|
|
289
|
+
this.put({ emissive: intensity > 0 ? { token: material.emissive?.token ?? material.token, intensity, on: true } : undefined }, live)
|
|
230
290
|
}
|
|
231
291
|
|
|
232
|
-
|
|
292
|
+
/** A material change: recorded, or -- while a slider is dragged -- only drawn (`v3-part-live`). */
|
|
293
|
+
private put(patch: Partial<V3Material>, live = false) {
|
|
233
294
|
const material: V3Material = { ...this.body!.material, ...patch }
|
|
234
295
|
for (const key of Object.keys(material) as (keyof V3Material)[]) if (material[key] === undefined) delete material[key]
|
|
235
296
|
/* The colours this material names that the asset does not carry yet. */
|
|
@@ -239,10 +300,22 @@ export class FigureV3Material extends localize(i18next)(LitElement) {
|
|
|
239
300
|
.filter((token): token is string => !!token && this.palette[token] === undefined && offered[token] !== undefined)
|
|
240
301
|
.map(token => [token, offered[token]!])
|
|
241
302
|
)
|
|
242
|
-
|
|
303
|
+
const edit: V3PartEdit = { kind: 'set-material', target: this.body!.id, material, ...(Object.keys(palette).length ? { palette } : {}) }
|
|
304
|
+
if (live) this.dispatchEvent(new CustomEvent('v3-part-live', { detail: { edit }, bubbles: true, composed: true }))
|
|
305
|
+
else this.edit(edit)
|
|
243
306
|
}
|
|
244
307
|
|
|
245
308
|
private edit(edit: V3PartEdit) {
|
|
246
309
|
this.dispatchEvent(new CustomEvent('v3-part-edit', { detail: { edit }, bubbles: true, composed: true }))
|
|
247
310
|
}
|
|
248
311
|
}
|
|
312
|
+
|
|
313
|
+
/** A regular polygon of this many sides -- how round a body with this rim count looks from its end (V2). */
|
|
314
|
+
function silhouette(segments: number) {
|
|
315
|
+
const points: string[] = []
|
|
316
|
+
for (let i = 0; i < segments; i++) {
|
|
317
|
+
const angle = -Math.PI / 2 + (i * 2 * Math.PI) / segments
|
|
318
|
+
points.push(`${(16 + 13 * Math.cos(angle)).toFixed(2)},${(16 + 13 * Math.sin(angle)).toFixed(2)}`)
|
|
319
|
+
}
|
|
320
|
+
return svg`<svg viewBox="0 0 32 32" aria-hidden="true"><polygon points=${points.join(' ')}></polygon></svg>`
|
|
321
|
+
}
|
|
@@ -4,6 +4,8 @@ import { customElement, property } from 'lit/decorators.js'
|
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
5
|
import { ScrollbarStyles } from '@operato/styles'
|
|
6
6
|
import type { V3PrimitiveCreation } from '@hatiolab/figure-model'
|
|
7
|
+
import type { V3BudgetLine } from './v3-maturity.js'
|
|
8
|
+
import { partLabelOf } from './v3-fields.js'
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* 도형 팔레트와 부품 목록 — V3 자산의 것.
|
|
@@ -44,10 +46,20 @@ const PROVIDER_ICON: Record<string, string> = {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/** 팔레트 — 저작 API 가 만들 수 있는 것만 보인다. */
|
|
47
|
-
|
|
49
|
+
/*
|
|
50
|
+
The palette: V2's shapes. A plate and a wall are boxes of a shape a person reaches for (V2 drew both as boxes), so
|
|
51
|
+
they are starting sizes, not kinds of their own.
|
|
52
|
+
*/
|
|
53
|
+
export type V3PaletteKind = V3PrimitiveCreation['kind'] | 'plate' | 'wall'
|
|
54
|
+
const RECIPES: Array<{ kind: V3PaletteKind; icon: string; label: string }> = [
|
|
48
55
|
{ kind: 'box', icon: 'deployed_code', label: 'figure.label.primitive-cube' },
|
|
49
56
|
{ kind: 'cylinder', icon: 'database', label: 'figure.label.primitive-cylinder' },
|
|
50
57
|
{ kind: 'rounded-box', icon: 'crop_square', label: 'figure.label.primitive-rounded-box' },
|
|
58
|
+
{ kind: 'sphere', icon: 'circle', label: 'figure.label.primitive-sphere' },
|
|
59
|
+
{ kind: 'frustum', icon: 'change_history', label: 'figure.label.primitive-frustum' },
|
|
60
|
+
{ kind: 'polygon', icon: 'hexagon', label: 'figure.label.primitive-polygon' },
|
|
61
|
+
{ kind: 'plate', icon: 'check_box_outline_blank', label: 'figure.label.primitive-plate' },
|
|
62
|
+
{ kind: 'wall', icon: 'wall', label: 'figure.label.primitive-wall' },
|
|
51
63
|
{ kind: 'portal', icon: 'door_front', label: 'figure.label.primitive-portal' }
|
|
52
64
|
]
|
|
53
65
|
|
|
@@ -62,7 +74,7 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
62
74
|
min-height: 0;
|
|
63
75
|
overflow: hidden;
|
|
64
76
|
background-color: var(--md-sys-color-surface-container-lowest);
|
|
65
|
-
|
|
77
|
+
/* The divider beside it is the editor's frame's (ox-three-pane). */
|
|
66
78
|
color: var(--md-sys-color-on-surface);
|
|
67
79
|
font: var(--label-font, inherit);
|
|
68
80
|
font-size: 0.76rem;
|
|
@@ -90,6 +102,51 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
90
102
|
text-transform: uppercase;
|
|
91
103
|
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
92
104
|
}
|
|
105
|
+
/* The cost line: the grade's letter in its colour, then what the figure costs. */
|
|
106
|
+
button[budget] {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: flex-start;
|
|
109
|
+
gap: 6px;
|
|
110
|
+
width: 100%;
|
|
111
|
+
margin: 0 0 6px;
|
|
112
|
+
padding: 4px 6px;
|
|
113
|
+
font: inherit;
|
|
114
|
+
font-size: 0.68rem;
|
|
115
|
+
line-height: 1.5;
|
|
116
|
+
text-align: left;
|
|
117
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
118
|
+
background: none;
|
|
119
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
120
|
+
border-radius: 6px;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
}
|
|
123
|
+
button[budget]:hover {
|
|
124
|
+
background: var(--md-sys-color-surface-container);
|
|
125
|
+
}
|
|
126
|
+
button[budget] b {
|
|
127
|
+
flex: none;
|
|
128
|
+
min-width: 16px;
|
|
129
|
+
text-align: center;
|
|
130
|
+
border-radius: 4px;
|
|
131
|
+
color: var(--md-sys-color-on-primary-container);
|
|
132
|
+
background: var(--md-sys-color-primary-container);
|
|
133
|
+
}
|
|
134
|
+
button[budget][grade='C'] b {
|
|
135
|
+
color: var(--md-sys-color-on-tertiary-container);
|
|
136
|
+
background: var(--md-sys-color-tertiary-container);
|
|
137
|
+
}
|
|
138
|
+
button[budget][grade='D'] {
|
|
139
|
+
border-color: var(--md-sys-color-error);
|
|
140
|
+
}
|
|
141
|
+
button[budget][grade='D'] b {
|
|
142
|
+
color: var(--md-sys-color-on-error);
|
|
143
|
+
background: var(--md-sys-color-error);
|
|
144
|
+
}
|
|
145
|
+
button[budget] em {
|
|
146
|
+
display: block;
|
|
147
|
+
font-style: normal;
|
|
148
|
+
color: var(--md-sys-color-error);
|
|
149
|
+
}
|
|
93
150
|
span[room] {
|
|
94
151
|
font-size: 0.7rem;
|
|
95
152
|
font-weight: 400;
|
|
@@ -261,6 +318,11 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
261
318
|
@property({ type: String }) selected = ''
|
|
262
319
|
/** 편집할 수 없을 때(불러오는 중 · 저장 중) 팔레트와 조작을 잠근다. */
|
|
263
320
|
@property({ type: Boolean }) locked = false
|
|
321
|
+
/**
|
|
322
|
+
* 이 도형이 치르는 값 — 삼각형 · 묶음 · 한 화면에 몇 대 · 등급. 부품을 더하는 바로 그 자리에서 보여야 로우폴리의
|
|
323
|
+
* 기준이 저작을 이끈다(V2 모델러의 정체성). 등급이 D 면 무엇이 넘쳤는지를 적는다. 누르면 성숙도 탭이 열린다.
|
|
324
|
+
*/
|
|
325
|
+
@property({ attribute: false }) budget?: V3BudgetLine
|
|
264
326
|
|
|
265
327
|
render() {
|
|
266
328
|
return html`
|
|
@@ -289,6 +351,7 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
289
351
|
<span room>${this.parts.length}</span>
|
|
290
352
|
${this.renderShowAll()}
|
|
291
353
|
</h3>
|
|
354
|
+
${this.renderBudget()}
|
|
292
355
|
${this.parts.length === 0
|
|
293
356
|
? html`<p quiet>${i18next.t('figure.text.no-parts-yet')}</p>`
|
|
294
357
|
: html`<ol>
|
|
@@ -298,6 +361,36 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
298
361
|
`
|
|
299
362
|
}
|
|
300
363
|
|
|
364
|
+
private renderBudget() {
|
|
365
|
+
const b = this.budget
|
|
366
|
+
if (!b || !this.parts.length) return nothing
|
|
367
|
+
const over = b.grade === 'D'
|
|
368
|
+
return html`<button
|
|
369
|
+
budget
|
|
370
|
+
grade=${b.grade}
|
|
371
|
+
title=${i18next.t('figure.text.parts-budget-open')}
|
|
372
|
+
@click=${() => this.dispatchEvent(new CustomEvent('show-maturity', { bubbles: true, composed: true }))}
|
|
373
|
+
>
|
|
374
|
+
<b>${b.grade}</b>
|
|
375
|
+
<span>
|
|
376
|
+
${i18next.t('figure.text.parts-budget', {
|
|
377
|
+
triangles: b.triangles.toLocaleString(),
|
|
378
|
+
groups: b.groups.toLocaleString(),
|
|
379
|
+
instances: b.instances.toLocaleString()
|
|
380
|
+
})}
|
|
381
|
+
${over
|
|
382
|
+
? html`<em
|
|
383
|
+
>${i18next.t('figure.text.parts-budget-over', {
|
|
384
|
+
budget: i18next.t('figure.label.budget-' + b.tightest.key),
|
|
385
|
+
used: b.tightest.used.toLocaleString(),
|
|
386
|
+
limit: b.tightest.limit.toLocaleString()
|
|
387
|
+
})}</em
|
|
388
|
+
>`
|
|
389
|
+
: nothing}
|
|
390
|
+
</span>
|
|
391
|
+
</button>`
|
|
392
|
+
}
|
|
393
|
+
|
|
301
394
|
/** 한꺼번에 감추고 한꺼번에 되돌린다 — 하나라도 감춰져 있으면 다 되돌리고, 다 보이면 다 감춘다. */
|
|
302
395
|
private renderShowAll() {
|
|
303
396
|
if (this.parts.length === 0) return nothing
|
|
@@ -341,7 +434,7 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
341
434
|
</button>
|
|
342
435
|
<md-icon>${PROVIDER_ICON[part.provider] ?? 'deployed_code'}</md-icon>
|
|
343
436
|
<span label title=${part.id}
|
|
344
|
-
>${part.id}${part.invisible ? html`<small>${i18next.t('figure.label.state-hidden')}</small>` : nothing}</span
|
|
437
|
+
>${partLabelOf(part.id)}${part.invisible ? html`<small>${i18next.t('figure.label.state-hidden')}</small>` : nothing}</span
|
|
345
438
|
>
|
|
346
439
|
${selected ? this.renderOps(part.id, index) : nothing}
|
|
347
440
|
</li>
|