@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
|
@@ -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>
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { css, html, LitElement, nothing, unsafeCSS } from 'lit'
|
|
2
|
+
import { customElement, property, query, state } from 'lit/decorators.js'
|
|
3
|
+
|
|
4
|
+
import { i18next, localize } from '@operato/i18n'
|
|
5
|
+
import { compileV3Asset } from '@hatiolab/figure-model'
|
|
6
|
+
import type { V3Asset } from '@hatiolab/figure-model'
|
|
7
|
+
import { create, registerFigureV3, unregisterFigureV3 } from '@hatiolab/things-scene'
|
|
8
|
+
|
|
9
|
+
import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH, type ViewSettings } from './figure-view.js'
|
|
10
|
+
import { dialShape } from './angle-dial.js'
|
|
11
|
+
import { authoredSizeOf, PREVIEW_FACTOR, PREVIEW_WAYS, previewInstances } from './v3-preview-model.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The figure placed on a board -- V2's preview, for a V3 figure.
|
|
15
|
+
*
|
|
16
|
+
* The editing canvas shows the figure at the size it was made. Half of what a figure is lies in how it answers a
|
|
17
|
+
* different size, so here the figure being authored is registered as a board registers it and placed as
|
|
18
|
+
* `FigureInstance`s: the size it was made, and each of the seven ways of growing one, two or three axes twice as
|
|
19
|
+
* large, side by side at one scale, four to a row (V2's eight). The board draws them as it will on a real board --
|
|
20
|
+
* placement, lights, environment, the same V3 renderer.
|
|
21
|
+
*
|
|
22
|
+
* The controls are V2's too: each value that poses the figure with a slider (and a dial for an angle), each clip's
|
|
23
|
+
* speed, and 「이 자세를 기본으로」 to keep the pose as the one it stands in. Nothing here is edited; the canvas edits.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface SceneHandle {
|
|
27
|
+
dispose?: () => void
|
|
28
|
+
resize?: () => void
|
|
29
|
+
fit?: (type?: string) => void
|
|
30
|
+
root?: {
|
|
31
|
+
set?: (patch: Record<string, unknown>) => void
|
|
32
|
+
fitCameraToModel?: () => void
|
|
33
|
+
components?: { set?: (property: string, value: unknown) => void }[]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@customElement('figure-v3-preview')
|
|
38
|
+
export class FigureV3Preview extends localize(i18next)(LitElement) {
|
|
39
|
+
static styles = css`
|
|
40
|
+
:host {
|
|
41
|
+
position: relative;
|
|
42
|
+
display: flex;
|
|
43
|
+
min-width: 0;
|
|
44
|
+
min-height: 0;
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
background-color: ${unsafeCSS(WORKBENCH)};
|
|
47
|
+
font-size: 0.76rem;
|
|
48
|
+
}
|
|
49
|
+
div[stage] {
|
|
50
|
+
position: absolute;
|
|
51
|
+
inset: 0;
|
|
52
|
+
width: 100%;
|
|
53
|
+
height: 100%;
|
|
54
|
+
}
|
|
55
|
+
/* Which is which: side by side means nothing without a legend. */
|
|
56
|
+
div[legend] {
|
|
57
|
+
position: absolute;
|
|
58
|
+
top: 8px;
|
|
59
|
+
left: 8px;
|
|
60
|
+
display: flex;
|
|
61
|
+
gap: 6px;
|
|
62
|
+
padding: 4px 8px;
|
|
63
|
+
color: rgba(255, 255, 255, 0.9);
|
|
64
|
+
background-color: rgba(28, 28, 30, 0.6);
|
|
65
|
+
border-radius: 6px;
|
|
66
|
+
font-size: 0.72rem;
|
|
67
|
+
}
|
|
68
|
+
div[clips] {
|
|
69
|
+
position: absolute;
|
|
70
|
+
right: 8px;
|
|
71
|
+
bottom: 8px;
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
gap: 6px;
|
|
75
|
+
padding: 8px 10px;
|
|
76
|
+
color: rgba(255, 255, 255, 0.92);
|
|
77
|
+
background-color: rgba(28, 28, 30, 0.72);
|
|
78
|
+
border-radius: 8px;
|
|
79
|
+
font-size: 0.72rem;
|
|
80
|
+
}
|
|
81
|
+
div[clips] label {
|
|
82
|
+
display: grid;
|
|
83
|
+
grid-template-columns: auto 20px 96px 34px;
|
|
84
|
+
align-items: center;
|
|
85
|
+
gap: 8px;
|
|
86
|
+
}
|
|
87
|
+
div[clips] svg[dial] {
|
|
88
|
+
width: 20px;
|
|
89
|
+
height: 20px;
|
|
90
|
+
}
|
|
91
|
+
div[clips] svg[dial] path {
|
|
92
|
+
fill: rgba(255, 255, 255, 0.22);
|
|
93
|
+
}
|
|
94
|
+
div[clips] svg[dial] line {
|
|
95
|
+
stroke: var(--md-sys-color-primary, #7cc6ff);
|
|
96
|
+
stroke-width: 2;
|
|
97
|
+
stroke-linecap: round;
|
|
98
|
+
}
|
|
99
|
+
div[clips] svg[dial] circle {
|
|
100
|
+
fill: none;
|
|
101
|
+
stroke: rgba(255, 255, 255, 0.35);
|
|
102
|
+
}
|
|
103
|
+
div[clips] button[pose] {
|
|
104
|
+
align-self: stretch;
|
|
105
|
+
padding: 4px 8px;
|
|
106
|
+
font: inherit;
|
|
107
|
+
font-size: 0.7rem;
|
|
108
|
+
color: rgba(255, 255, 255, 0.92);
|
|
109
|
+
background-color: rgba(255, 255, 255, 0.14);
|
|
110
|
+
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
111
|
+
border-radius: 5px;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
}
|
|
114
|
+
div[clips] button[pose]:hover {
|
|
115
|
+
background-color: rgba(255, 255, 255, 0.24);
|
|
116
|
+
}
|
|
117
|
+
div[clips] span[drive] {
|
|
118
|
+
opacity: 0.6;
|
|
119
|
+
font-size: 0.66rem;
|
|
120
|
+
}
|
|
121
|
+
div[clips] input {
|
|
122
|
+
width: 96px;
|
|
123
|
+
accent-color: var(--md-sys-color-primary);
|
|
124
|
+
}
|
|
125
|
+
div[clips] output {
|
|
126
|
+
font-variant-numeric: tabular-nums;
|
|
127
|
+
text-align: right;
|
|
128
|
+
opacity: 0.85;
|
|
129
|
+
}
|
|
130
|
+
div[cannot] {
|
|
131
|
+
position: absolute;
|
|
132
|
+
inset: 0;
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: center;
|
|
136
|
+
padding: 12px;
|
|
137
|
+
text-align: center;
|
|
138
|
+
line-height: 1.6;
|
|
139
|
+
color: #f4f4f4;
|
|
140
|
+
background-color: ${unsafeCSS(WORKBENCH)};
|
|
141
|
+
}
|
|
142
|
+
`
|
|
143
|
+
|
|
144
|
+
/** The figure being authored, as the editor holds it now. */
|
|
145
|
+
@property({ attribute: false }) asset?: V3Asset
|
|
146
|
+
/** Which figure: the type it is registered under here is this one's own, never a real type a board uses. */
|
|
147
|
+
@property() context = ''
|
|
148
|
+
@property({ attribute: false }) view: ViewSettings = DEFAULT_VIEW
|
|
149
|
+
|
|
150
|
+
@query('div[stage]') private stage!: HTMLDivElement
|
|
151
|
+
@state() private cannot = ''
|
|
152
|
+
/** Clip speeds and control values given here -- the preview's, not the figure's. */
|
|
153
|
+
@state() private driving: Record<string, number> = {}
|
|
154
|
+
@state() private giving: Record<string, number> = {}
|
|
155
|
+
|
|
156
|
+
private scene?: SceneHandle
|
|
157
|
+
private standing?: string
|
|
158
|
+
private registered?: string
|
|
159
|
+
private watcher?: ResizeObserver
|
|
160
|
+
|
|
161
|
+
render() {
|
|
162
|
+
const asset = this.asset
|
|
163
|
+
const controls = asset ? asset.document.model.inputs.filter(i => i.role === 'state') : []
|
|
164
|
+
const clips = asset ? [...new Set((asset.drivers ?? []).map(d => d.clip))] : []
|
|
165
|
+
return html`<div stage></div>
|
|
166
|
+
${this.cannot
|
|
167
|
+
? html`<div cannot>${this.cannot}</div>`
|
|
168
|
+
: html`<div legend>${PREVIEW_WAYS.map(way => html`<span>${way.axes.length ? way.name : i18next.t('figure.label.preview-base')}</span>`)}</div>
|
|
169
|
+
${controls.length + clips.length ? this.renderDrivers(asset!, controls, clips) : nothing}`}`
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private renderDrivers(asset: V3Asset, controls: V3Asset['document']['model']['inputs'], clips: string[]) {
|
|
173
|
+
const changed = controls.filter(c => this.giving[c.id] !== undefined && this.giving[c.id] !== asset.stateDefaults[c.id])
|
|
174
|
+
return html`<div clips>
|
|
175
|
+
${changed.length
|
|
176
|
+
? html`<button pose title=${i18next.t('figure.text.pose-as-default-explained')} @click=${() => this.poseAsDefault()}>
|
|
177
|
+
${i18next.t('figure.button.pose-as-default')}
|
|
178
|
+
</button>`
|
|
179
|
+
: nothing}
|
|
180
|
+
${controls.map(control => {
|
|
181
|
+
const value = this.giving[control.id] ?? asset.stateDefaults[control.id] ?? control.min
|
|
182
|
+
const step = Math.max((control.max - control.min) / 100, 0.01)
|
|
183
|
+
const label = asset.stateInputs?.[control.id]?.label ?? control.id
|
|
184
|
+
return html`<label>
|
|
185
|
+
<span>${label} <span drive>${control.unit}</span></span>
|
|
186
|
+
${this.renderDial(control, value)}
|
|
187
|
+
<input
|
|
188
|
+
type="range"
|
|
189
|
+
min=${control.min}
|
|
190
|
+
max=${control.max}
|
|
191
|
+
step=${step}
|
|
192
|
+
.value=${String(value)}
|
|
193
|
+
@input=${(e: Event) => this.give(control.id, Number((e.target as HTMLInputElement).value))}
|
|
194
|
+
/>
|
|
195
|
+
<output>${Math.round(value)}</output>
|
|
196
|
+
</label>`
|
|
197
|
+
})}
|
|
198
|
+
${clips.map(clip => {
|
|
199
|
+
const value = this.driving[clip] ?? 1
|
|
200
|
+
return html`<label>
|
|
201
|
+
<span>${clip} <span drive>${i18next.t('figure.label.playback-speed')}</span></span>
|
|
202
|
+
<span></span>
|
|
203
|
+
<input type="range" min="0" max="3" step="0.05" .value=${String(value)} @input=${(e: Event) => this.drive(clip, Number((e.target as HTMLInputElement).value))} />
|
|
204
|
+
<output>${value.toFixed(2)}</output>
|
|
205
|
+
</label>`
|
|
206
|
+
})}
|
|
207
|
+
</div>`
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private renderDial(control: { unit: string; min: number; max: number }, value: number) {
|
|
211
|
+
if (control.unit !== 'deg') return html`<span></span>`
|
|
212
|
+
const dial = dialShape(control.min, control.max, value)
|
|
213
|
+
const span = dial.radius * 2 + 2
|
|
214
|
+
return html`<svg dial viewBox="0 0 ${span} ${span}" aria-hidden="true">
|
|
215
|
+
<path d=${dial.sector}></path>
|
|
216
|
+
<circle cx=${dial.centre.x} cy=${dial.centre.y} r=${dial.radius}></circle>
|
|
217
|
+
<line x1=${dial.centre.x} y1=${dial.centre.y} x2=${dial.needle.x} y2=${dial.needle.y}></line>
|
|
218
|
+
</svg>`
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private drive(clip: string, value: number) {
|
|
222
|
+
this.driving = { ...this.driving, [clip]: value }
|
|
223
|
+
this.tellInstances('clips', this.driving)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private give(id: string, value: number) {
|
|
227
|
+
this.giving = { ...this.giving, [id]: value }
|
|
228
|
+
this.tellInstances('parameters', this.giving)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private poseAsDefault() {
|
|
232
|
+
this.dispatchEvent(new CustomEvent('v3-part-edit', { detail: { edit: { kind: 'set-state-defaults', values: { ...this.giving } } }, bubbles: true, composed: true }))
|
|
233
|
+
this.giving = {}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private tellInstances(key: 'clips' | 'parameters', values: Record<string, number>) {
|
|
237
|
+
for (const component of this.scene?.root?.components ?? []) component.set?.(key, { ...values })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
willUpdate(changed: Map<string, unknown>) {
|
|
241
|
+
if (!this.hasUpdated) return
|
|
242
|
+
if (changed.has('asset') || changed.has('context')) {
|
|
243
|
+
this.standUp()
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
if (!changed.has('view')) return
|
|
247
|
+
const before = changed.get('view') as ViewSettings | undefined
|
|
248
|
+
const now = this.view ?? DEFAULT_VIEW
|
|
249
|
+
if (before?.placement !== now.placement || before?.ceilingHeight !== now.ceilingHeight) this.standUp()
|
|
250
|
+
else this.scene?.root?.set?.(sceneLook(now))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
firstUpdated() {
|
|
254
|
+
this.standUp()
|
|
255
|
+
this.watcher = new ResizeObserver(() => this.scene?.resize?.())
|
|
256
|
+
this.watcher.observe(this)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
disconnectedCallback() {
|
|
260
|
+
super.disconnectedCallback()
|
|
261
|
+
this.watcher?.disconnect()
|
|
262
|
+
this.watcher = undefined
|
|
263
|
+
this.teardown()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private standUp() {
|
|
267
|
+
const asset = this.asset
|
|
268
|
+
const view = this.view ?? DEFAULT_VIEW
|
|
269
|
+
if (!asset || !asset.appearance.length) {
|
|
270
|
+
this.cannot = i18next.t('figure.text.preview-needs-a-part')
|
|
271
|
+
this.teardown()
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
const signature = JSON.stringify([asset, view.placement, view.ceilingHeight])
|
|
275
|
+
if (this.scene && this.standing === signature) return
|
|
276
|
+
try {
|
|
277
|
+
compileV3Asset(asset)
|
|
278
|
+
} catch {
|
|
279
|
+
this.cannot = i18next.t('figure.text.preview-blocked-by-errors')
|
|
280
|
+
this.teardown()
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
this.teardown()
|
|
284
|
+
this.driving = {}
|
|
285
|
+
try {
|
|
286
|
+
/* Registered as a board registers it, under a type of this preview's own: a real type's drawings stay as they are. */
|
|
287
|
+
const type = `figure-v3-preview-${this.context || 'new'}`
|
|
288
|
+
registerFigureV3(type, asset)
|
|
289
|
+
this.registered = type
|
|
290
|
+
const size = authoredSizeOf(asset)
|
|
291
|
+
const { components, board } = previewInstances(type, size)
|
|
292
|
+
this.scene = create({
|
|
293
|
+
target: this.stage,
|
|
294
|
+
model: {
|
|
295
|
+
type: 'model-layer',
|
|
296
|
+
width: board.width,
|
|
297
|
+
height: board.height,
|
|
298
|
+
depth: previewBoardDepth({ placement: asset.occupancy?.placement, base: { y: size.y } }, view.ceilingHeight, PREVIEW_FACTOR),
|
|
299
|
+
threed: true,
|
|
300
|
+
sky: 'color',
|
|
301
|
+
...sceneLook(view),
|
|
302
|
+
placement: view.placement,
|
|
303
|
+
heights: { floor: 0, ceiling: view.ceilingHeight },
|
|
304
|
+
pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),
|
|
305
|
+
components
|
|
306
|
+
},
|
|
307
|
+
mode: 0
|
|
308
|
+
}) as SceneHandle
|
|
309
|
+
this.standing = signature
|
|
310
|
+
this.scene.resize?.()
|
|
311
|
+
this.scene.fit?.('ratio')
|
|
312
|
+
this.scene.root?.fitCameraToModel?.()
|
|
313
|
+
this.cannot = ''
|
|
314
|
+
} catch (error) {
|
|
315
|
+
this.cannot = (error as Error).message
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private teardown() {
|
|
320
|
+
this.scene?.dispose?.()
|
|
321
|
+
this.scene = undefined
|
|
322
|
+
this.standing = undefined
|
|
323
|
+
if (this.registered) unregisterFigureV3(this.registered)
|
|
324
|
+
this.registered = undefined
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { V3AuthoringAction, V3Face } from '@hatiolab/figure-model'
|
|
|
9
9
|
|
|
10
10
|
import { occupancyFromParts } from './v3-occupancy.js'
|
|
11
11
|
import { tagsOf, v3FigureFacts, type Placement, type V3FigureFacts } from './v3-figure-facts.js'
|
|
12
|
+
import { SidePanelStyles } from './side-panel-styles.js'
|
|
12
13
|
|
|
13
14
|
const PLACEMENTS: Placement[] = ['floor', 'ceiling', 'center']
|
|
14
15
|
const FACES = Object.keys(V3_FACES) as V3Face[]
|
|
@@ -31,71 +32,30 @@ const FACES = Object.keys(V3_FACES) as V3Face[]
|
|
|
31
32
|
*/
|
|
32
33
|
@customElement('figure-v3-properties')
|
|
33
34
|
export class FigureV3Properties extends localize(i18next)(LitElement) {
|
|
34
|
-
static styles = css`
|
|
35
|
+
static styles = [SidePanelStyles, css`
|
|
35
36
|
:host {
|
|
36
|
-
display:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
padding: 12px;
|
|
37
|
+
display: block;
|
|
38
|
+
/* It stands inside the properties tab's panel, which gives the margin; a second one indented it past the type. */
|
|
39
|
+
padding: 0;
|
|
40
40
|
overflow: auto;
|
|
41
|
-
font-size: 0.76rem;
|
|
42
|
-
color: var(--md-sys-color-on-surface);
|
|
43
|
-
}
|
|
44
|
-
div[field] {
|
|
45
|
-
display: grid;
|
|
46
|
-
gap: 4px;
|
|
47
|
-
}
|
|
48
|
-
div[field] > label {
|
|
49
|
-
font-weight: 600;
|
|
50
41
|
}
|
|
51
42
|
div[row] {
|
|
52
43
|
display: flex;
|
|
53
44
|
gap: 6px;
|
|
54
45
|
align-items: center;
|
|
55
46
|
}
|
|
56
|
-
input,
|
|
57
|
-
select,
|
|
58
|
-
button {
|
|
59
|
-
font: inherit;
|
|
60
|
-
min-width: 0;
|
|
61
|
-
}
|
|
62
|
-
input,
|
|
63
|
-
select {
|
|
64
|
-
padding: 5px 8px;
|
|
65
|
-
border: 1px solid var(--md-sys-color-outline-variant);
|
|
66
|
-
border-radius: 4px;
|
|
67
|
-
background: var(--md-sys-color-surface);
|
|
68
|
-
color: var(--md-sys-color-on-surface);
|
|
69
|
-
}
|
|
70
47
|
div[row] > select {
|
|
71
48
|
flex: 1;
|
|
72
49
|
}
|
|
73
|
-
button {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
padding: 4px 10px;
|
|
78
|
-
border: none;
|
|
79
|
-
border-radius: 999em;
|
|
80
|
-
cursor: pointer;
|
|
81
|
-
color: var(--md-sys-color-on-secondary-container);
|
|
82
|
-
background: var(--md-sys-color-secondary-container);
|
|
83
|
-
}
|
|
84
|
-
button:disabled,
|
|
85
|
-
input:disabled,
|
|
86
|
-
select:disabled {
|
|
87
|
-
opacity: 0.5;
|
|
88
|
-
cursor: default;
|
|
89
|
-
}
|
|
90
|
-
button md-icon {
|
|
91
|
-
font-size: 16px;
|
|
92
|
-
width: 16px;
|
|
93
|
-
height: 16px;
|
|
50
|
+
div[row] > button[act] {
|
|
51
|
+
flex: 1;
|
|
52
|
+
width: auto;
|
|
53
|
+
margin-top: 0;
|
|
94
54
|
}
|
|
95
|
-
|
|
96
|
-
|
|
55
|
+
div[row] > span {
|
|
56
|
+
flex: 1;
|
|
97
57
|
}
|
|
98
|
-
`
|
|
58
|
+
`]
|
|
99
59
|
|
|
100
60
|
/** The source being edited. */
|
|
101
61
|
@property({ type: String }) source?: string
|
|
@@ -119,8 +79,20 @@ export class FigureV3Properties extends localize(i18next)(LitElement) {
|
|
|
119
79
|
render() {
|
|
120
80
|
const facts = this.facts()
|
|
121
81
|
if (!facts) return nothing
|
|
122
|
-
|
|
123
|
-
|
|
82
|
+
/* The figure's type is the page's (it lives on the row); it is slotted into the first section, as V2 had it. */
|
|
83
|
+
return html`<section>
|
|
84
|
+
<h3>${i18next.t('figure.label.group-figure')}</h3>
|
|
85
|
+
<slot name="type"></slot>
|
|
86
|
+
${this.renderName(facts)} ${this.renderPlacement(facts)}
|
|
87
|
+
</section>
|
|
88
|
+
<section>
|
|
89
|
+
<h3>${i18next.t('figure.label.group-standing')}</h3>
|
|
90
|
+
${this.renderSpace(facts)} ${this.renderMounting(facts)}
|
|
91
|
+
</section>
|
|
92
|
+
<section>
|
|
93
|
+
<h3>${i18next.t('figure.label.group-catalog')}</h3>
|
|
94
|
+
${this.renderCatalog()}
|
|
95
|
+
</section>`
|
|
124
96
|
}
|
|
125
97
|
|
|
126
98
|
private renderName(facts: V3FigureFacts) {
|
|
@@ -172,7 +144,7 @@ export class FigureV3Properties extends localize(i18next)(LitElement) {
|
|
|
172
144
|
<label>${i18next.t('figure.label.declared-space')}</label>
|
|
173
145
|
<div row>
|
|
174
146
|
<span>${i18next.t(facts.declared ? 'figure.text.declared-space-declared' : 'figure.text.declared-space-none')}</span>
|
|
175
|
-
<button
|
|
147
|
+
<button act ?disabled=${!facts.parts.length} @click=${() => this.declare(facts.placement ?? 'floor')}>
|
|
176
148
|
<md-icon>fit_screen</md-icon>${i18next.t('figure.button.fit-base-to-parts')}
|
|
177
149
|
</button>
|
|
178
150
|
</div>
|
|
@@ -196,10 +168,10 @@ export class FigureV3Properties extends localize(i18next)(LitElement) {
|
|
|
196
168
|
</select>
|
|
197
169
|
</div>
|
|
198
170
|
<div row>
|
|
199
|
-
<button ?disabled=${!facts.declared || !part} @click=${() => this.command({ kind: 'set-mounting-face', part: part!, face })}>
|
|
171
|
+
<button act ?disabled=${!facts.declared || !part} @click=${() => this.command({ kind: 'set-mounting-face', part: part!, face })}>
|
|
200
172
|
${i18next.t('figure.button.set-mounting-face')}
|
|
201
173
|
</button>
|
|
202
|
-
<button ?disabled=${!mounting} @click=${() => this.command({ kind: 'clear-mounting-face' })}>
|
|
174
|
+
<button act ?disabled=${!mounting} @click=${() => this.command({ kind: 'clear-mounting-face' })}>
|
|
203
175
|
${i18next.t('figure.button.clear-mounting-face')}
|
|
204
176
|
</button>
|
|
205
177
|
</div>
|