@things-factory/figure-ui 10.1.48 → 10.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/graphql/v3-store.ts +2 -1
- package/client/modeller/figure-history-panel.ts +9 -1
- package/client/modeller/figure-v3-first-save.ts +98 -0
- package/client/modeller/figure-v3-links.ts +89 -26
- package/client/modeller/figure-v3-parts.ts +86 -10
- package/client/modeller/figure-v3-properties.ts +2 -1
- package/client/modeller/side-panel-styles.ts +8 -0
- package/client/modeller/v3-canvas.ts +86 -12
- package/client/modeller/v3-editor.ts +244 -37
- package/client/modeller/v3-fields.ts +15 -1
- package/client/modeller/v3-first-save.ts +34 -0
- package/client/modeller/v3-gizmo.ts +52 -3
- package/client/modeller/v3-joint-drag.ts +116 -0
- package/client/modeller/v3-joint-fan.ts +24 -11
- package/client/modeller/v3-keys.ts +12 -1
- package/client/modeller/v3-links.ts +72 -5
- package/client/modeller/v3-live.ts +11 -0
- package/client/modeller/v3-product-session.ts +4 -2
- package/client/modeller/v3-refusals.ts +21 -0
- package/client/modeller/v3-stage-model.ts +24 -0
- package/client/pages/figure-v3-modeller-page.ts +46 -7
- package/dist-client/graphql/v3-store.js +2 -1
- package/dist-client/graphql/v3-store.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.js +9 -1
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-v3-first-save.d.ts +20 -0
- package/dist-client/modeller/figure-v3-first-save.js +118 -0
- package/dist-client/modeller/figure-v3-first-save.js.map +1 -0
- package/dist-client/modeller/figure-v3-links.d.ts +5 -0
- package/dist-client/modeller/figure-v3-links.js +81 -23
- package/dist-client/modeller/figure-v3-links.js.map +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +13 -0
- package/dist-client/modeller/figure-v3-parts.js +81 -10
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-properties.js +2 -1
- package/dist-client/modeller/figure-v3-properties.js.map +1 -1
- package/dist-client/modeller/side-panel-styles.js +8 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -1
- package/dist-client/modeller/v3-canvas.d.ts +13 -0
- package/dist-client/modeller/v3-canvas.js +88 -12
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.d.ts +32 -1
- package/dist-client/modeller/v3-editor.js +245 -35
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +6 -0
- package/dist-client/modeller/v3-fields.js +14 -2
- package/dist-client/modeller/v3-fields.js.map +1 -1
- package/dist-client/modeller/v3-first-save.d.ts +17 -0
- package/dist-client/modeller/v3-first-save.js +27 -0
- package/dist-client/modeller/v3-first-save.js.map +1 -0
- package/dist-client/modeller/v3-gizmo.d.ts +18 -0
- package/dist-client/modeller/v3-gizmo.js +45 -2
- package/dist-client/modeller/v3-gizmo.js.map +1 -1
- package/dist-client/modeller/v3-joint-drag.d.ts +19 -0
- package/dist-client/modeller/v3-joint-drag.js +97 -0
- package/dist-client/modeller/v3-joint-drag.js.map +1 -0
- package/dist-client/modeller/v3-joint-fan.js +24 -11
- package/dist-client/modeller/v3-joint-fan.js.map +1 -1
- package/dist-client/modeller/v3-keys.d.ts +9 -1
- package/dist-client/modeller/v3-keys.js +3 -0
- package/dist-client/modeller/v3-keys.js.map +1 -1
- package/dist-client/modeller/v3-links.d.ts +45 -2
- package/dist-client/modeller/v3-links.js +52 -3
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-live.d.ts +6 -0
- package/dist-client/modeller/v3-live.js +10 -0
- package/dist-client/modeller/v3-live.js.map +1 -1
- package/dist-client/modeller/v3-product-session.d.ts +4 -1
- package/dist-client/modeller/v3-product-session.js +2 -2
- package/dist-client/modeller/v3-product-session.js.map +1 -1
- package/dist-client/modeller/v3-refusals.d.ts +4 -0
- package/dist-client/modeller/v3-refusals.js +21 -0
- package/dist-client/modeller/v3-refusals.js.map +1 -0
- package/dist-client/modeller/v3-stage-model.d.ts +8 -0
- package/dist-client/modeller/v3-stage-model.js +26 -0
- package/dist-client/modeller/v3-stage-model.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +4 -0
- package/dist-client/pages/figure-v3-modeller-page.js +45 -7
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -5
- package/test/browser/build.mjs +62 -0
- package/test/browser/modeller-page.js +198 -0
- package/test/browser/modeller.check.mjs +334 -0
- package/test/browser/serve.mjs +43 -0
- package/test/joints.ts +33 -0
- package/test/v3-clips.test.ts +3 -2
- package/test/v3-controls.test.ts +3 -2
- package/test/v3-fields.test.ts +38 -1
- package/test/v3-first-save.test.ts +75 -0
- package/test/v3-gizmo.test.ts +40 -5
- package/test/v3-joint-drag.test.ts +93 -0
- package/test/v3-joint-fan.test.ts +16 -6
- package/test/v3-keys.test.ts +15 -2
- package/test/v3-links.test.ts +62 -14
- package/test/v3-live.test.ts +23 -4
- package/test/v3-maturity.test.ts +3 -7
- package/test/v3-refusals.test.ts +28 -0
- package/translations/en.json +43 -5
- package/translations/ja.json +42 -4
- package/translations/ko.json +42 -4
- package/translations/ms.json +42 -4
- package/translations/zh.json +42 -4
|
@@ -13,13 +13,14 @@ export const v3ProductStore: V3ProductStore = {
|
|
|
13
13
|
async save(request) {
|
|
14
14
|
const response = await client.mutate({
|
|
15
15
|
mutation: gql`
|
|
16
|
-
mutation SaveFigureV3($source: String!, $id: String, $expectedSource: String, $creationId: String, $thumbnail: String) {
|
|
16
|
+
mutation SaveFigureV3($source: String!, $id: String, $expectedSource: String, $creationId: String, $thumbnail: String, $type: String) {
|
|
17
17
|
saveFigureV3(
|
|
18
18
|
source: $source
|
|
19
19
|
id: $id
|
|
20
20
|
expectedSource: $expectedSource
|
|
21
21
|
creationId: $creationId
|
|
22
22
|
thumbnail: $thumbnail
|
|
23
|
+
type: $type
|
|
23
24
|
) {
|
|
24
25
|
id
|
|
25
26
|
source
|
|
@@ -9,6 +9,7 @@ import { ScrollbarStyles } from '@operato/styles'
|
|
|
9
9
|
|
|
10
10
|
import { fetchFigureVersions, inspectFigure, releaseFigure, revertFigureVersion } from '../graphql/index.js'
|
|
11
11
|
import type { FigureGateFinding, FigureGateFix, FigureGateFixPlan, FigureInspection, FigureVersion } from '../types.js'
|
|
12
|
+
import { OxPrompt } from '@operato/popup'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* 이력 — **발행하는 자리이자, 발행된 판들을 보는 자리.**
|
|
@@ -558,7 +559,14 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
558
559
|
|
|
559
560
|
private async revert(version: number) {
|
|
560
561
|
/* 되돌아갈 수 없는 조작은 아니지만(판본은 남는다), 지금 화면의 것이 덮인다 — 이름으로 확인한다. */
|
|
561
|
-
|
|
562
|
+
const agreed = await OxPrompt.open({
|
|
563
|
+
type: 'warning',
|
|
564
|
+
title: i18next.t('figure.button.revert'),
|
|
565
|
+
text: i18next.t('figure.text.revert-to-version-confirm', { version }),
|
|
566
|
+
confirmButton: { text: i18next.t('figure.button.revert') },
|
|
567
|
+
cancelButton: { text: i18next.t('figure.button.keep-editing') }
|
|
568
|
+
})
|
|
569
|
+
if (!agreed) {
|
|
562
570
|
return
|
|
563
571
|
}
|
|
564
572
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { LitElement, html, css, nothing } from 'lit'
|
|
2
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
3
|
+
import { i18next } from '@operato/i18n'
|
|
4
|
+
import { closePopup } from '@operato/popup'
|
|
5
|
+
import '@operato/popup/ox-popup-actions.js'
|
|
6
|
+
import { CommonPopupStyles, FormFieldStyles } from '@operato/styles'
|
|
7
|
+
import '@material/web/button/filled-button.js'
|
|
8
|
+
import '@material/web/button/text-button.js'
|
|
9
|
+
|
|
10
|
+
import { firstSaveRefusalOf, releasableType, typeOf, type FirstSave } from './v3-first-save.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The first save of a new figure: its type and its name, asked once (V2's save dialog, in the suite's popup look).
|
|
14
|
+
* Closes with the answer, or with nothing when cancelled -- then nothing is saved.
|
|
15
|
+
*/
|
|
16
|
+
@customElement('figure-v3-first-save')
|
|
17
|
+
export class FigureV3FirstSave extends LitElement {
|
|
18
|
+
static styles = [
|
|
19
|
+
FormFieldStyles,
|
|
20
|
+
CommonPopupStyles,
|
|
21
|
+
css`
|
|
22
|
+
/* The popup's height is a share of the window (small is 40%): on a low window the buttons must still be reached. */
|
|
23
|
+
:host {
|
|
24
|
+
overflow-y: auto;
|
|
25
|
+
}
|
|
26
|
+
div[field] {
|
|
27
|
+
display: grid;
|
|
28
|
+
gap: 4px;
|
|
29
|
+
}
|
|
30
|
+
div[field] label {
|
|
31
|
+
font-size: var(--popup-font-size-small);
|
|
32
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
33
|
+
}
|
|
34
|
+
div[field] input {
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
width: 100%;
|
|
37
|
+
}
|
|
38
|
+
input[type-field] {
|
|
39
|
+
font-family: var(--mono-font, monospace);
|
|
40
|
+
}
|
|
41
|
+
`
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
/** The type the figure has so far -- empty for a temporary one, which is not offered. */
|
|
45
|
+
@property() type = ''
|
|
46
|
+
@property() name = ''
|
|
47
|
+
/** Type names already used in this domain. */
|
|
48
|
+
@property({ attribute: false }) taken: string[] = []
|
|
49
|
+
@state() private refusal: { key: string; type?: string } | null = null
|
|
50
|
+
|
|
51
|
+
render() {
|
|
52
|
+
return html`
|
|
53
|
+
<div class="what">${i18next.t('figure.text.save-new-figure-desc')}</div>
|
|
54
|
+
<div field>
|
|
55
|
+
<label for="type">${i18next.t('figure.label.figure-type')}</label>
|
|
56
|
+
<input
|
|
57
|
+
id="type"
|
|
58
|
+
type-field
|
|
59
|
+
autofocus
|
|
60
|
+
.value=${this.type}
|
|
61
|
+
placeholder=${i18next.t('figure.text.figure-type-example')}
|
|
62
|
+
@input=${(e: Event) => {
|
|
63
|
+
this.type = typeOf((e.target as HTMLInputElement).value)
|
|
64
|
+
;(e.target as HTMLInputElement).value = this.type
|
|
65
|
+
this.refusal = null
|
|
66
|
+
}}
|
|
67
|
+
@keydown=${this.onKey}
|
|
68
|
+
/>
|
|
69
|
+
${this.type && !releasableType(this.type) ? html`<div class="note">${i18next.t('figure.text.type-name-will-not-release')}</div>` : nothing}
|
|
70
|
+
</div>
|
|
71
|
+
<div field>
|
|
72
|
+
<label for="name">${i18next.t('figure.label.name')}</label>
|
|
73
|
+
<input
|
|
74
|
+
id="name"
|
|
75
|
+
.value=${this.name}
|
|
76
|
+
placeholder=${i18next.t('figure.text.figure-name-example')}
|
|
77
|
+
@input=${(e: Event) => (this.name = (e.target as HTMLInputElement).value)}
|
|
78
|
+
@keydown=${this.onKey}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
${this.refusal ? html`<div class="failure" role="alert">${i18next.t(this.refusal.key, { type: this.refusal.type })}</div>` : nothing}
|
|
82
|
+
<ox-popup-actions>
|
|
83
|
+
<md-text-button slot="cancel" @click=${() => closePopup(this)}>${i18next.t('figure.button.cancel')}</md-text-button>
|
|
84
|
+
<md-filled-button slot="confirm" ?disabled=${!this.type} @click=${this.confirm}>${i18next.t('figure.button.save')}</md-filled-button>
|
|
85
|
+
</ox-popup-actions>
|
|
86
|
+
`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private onKey = (e: KeyboardEvent) => {
|
|
90
|
+
if (e.key === 'Enter' && !e.isComposing) this.confirm()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private confirm = () => {
|
|
94
|
+
this.refusal = firstSaveRefusalOf(this.type, this.taken)
|
|
95
|
+
if (this.refusal) return
|
|
96
|
+
closePopup(this, { type: this.type, name: this.name.trim() || this.type } satisfies FirstSave)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -9,6 +9,8 @@ import type { V3Asset, V3AuthoringAction, V3DimensionSource, V3Face } from '@hat
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
acceptsOf,
|
|
12
|
+
alignAxesOf,
|
|
13
|
+
alignOf,
|
|
12
14
|
AXES,
|
|
13
15
|
canFasten,
|
|
14
16
|
canFollow,
|
|
@@ -20,13 +22,17 @@ import {
|
|
|
20
22
|
followOf,
|
|
21
23
|
hasInstanceSize,
|
|
22
24
|
holdsThings,
|
|
25
|
+
leadersFor,
|
|
23
26
|
motionOf,
|
|
27
|
+
motionJoint,
|
|
24
28
|
placeFacesOf,
|
|
25
29
|
placeIdFor,
|
|
26
30
|
placesOn,
|
|
27
31
|
positionsWorkedOut,
|
|
32
|
+
type Alignment,
|
|
28
33
|
type Axis,
|
|
29
|
-
type Follow
|
|
34
|
+
type Follow,
|
|
35
|
+
type MotionForm
|
|
30
36
|
} from './v3-links.js'
|
|
31
37
|
import { SidePanelStyles } from './side-panel-styles.js'
|
|
32
38
|
import { HelpToggles } from './v3-help.js'
|
|
@@ -78,8 +84,8 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
78
84
|
/** Which ties this shows: the part's shape (size and placement, in 속성), its places (자리·능력) or its motion (움직임). */
|
|
79
85
|
@property() section: 'shape' | 'places' | 'motion' = 'shape'
|
|
80
86
|
|
|
81
|
-
@state() private fasten = { face: 'bottom' as V3Face, toPlane: false, target: '', targetFace: 'top' as V3Face, facing: 'meet' as 'meet' | 'flush', gap: 0 }
|
|
82
|
-
@state() private motion = { kind: 'slide'
|
|
87
|
+
@state() private fasten = { face: 'bottom' as V3Face, toPlane: false, target: '', targetFace: 'top' as V3Face, facing: 'meet' as 'meet' | 'flush', gap: 0, align: {} as Partial<Record<Axis, Alignment>>, alignMm: {} as Partial<Record<Axis, number>> }
|
|
88
|
+
@state() private motion: MotionForm = { kind: 'slide', axis: 'x', id: '', min: 0, max: 500, follow: '', multiplier: 1, offset: 0 }
|
|
83
89
|
@state() private place = { role: 'grip' as 'seat' | 'grip', face: 'top' as V3Face, label: '', accepts: '', capacity: 1 }
|
|
84
90
|
|
|
85
91
|
render() {
|
|
@@ -290,6 +296,7 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
290
296
|
<option value="flush" ?selected=${f.facing === 'flush'}>${i18next.t('figure.label.facing-flush')}</option>
|
|
291
297
|
</select>
|
|
292
298
|
</div>`}
|
|
299
|
+
${this.renderAlignment()}
|
|
293
300
|
<div form>
|
|
294
301
|
<span>${i18next.t('figure.label.gap')}</span>
|
|
295
302
|
<input type="number" step="any" .value=${String(f.gap)} @change=${(e: Event) => (this.fasten = { ...f, gap: Number((e.target as HTMLInputElement).value) || 0 })} />
|
|
@@ -305,6 +312,7 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
305
312
|
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: f.targetFace },
|
|
306
313
|
...(f.toPlane ? {} : { facing: f.facing }),
|
|
307
314
|
...(f.gap ? { gap: f.gap } : {}),
|
|
315
|
+
...(alignOf(f.toPlane ? 'plane' : f.targetFace, f.align, f.alignMm) ? { align: alignOf(f.toPlane ? 'plane' : f.targetFace, f.align, f.alignMm) } : {}),
|
|
308
316
|
replace: !!to
|
|
309
317
|
} as V3AuthoringAction)}
|
|
310
318
|
>
|
|
@@ -313,58 +321,113 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
313
321
|
`
|
|
314
322
|
}
|
|
315
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Where on the target face the part sits, on the two axes across it: centred, level with the target's edge on
|
|
326
|
+
* either side, or a distance off centre. A door hung level with the frame's front edge turns about that edge.
|
|
327
|
+
*/
|
|
328
|
+
private renderAlignment() {
|
|
329
|
+
const f = this.fasten
|
|
330
|
+
const set = (axis: Axis, how: Alignment) => (this.fasten = { ...f, align: { ...f.align, [axis]: how } })
|
|
331
|
+
const setMm = (axis: Axis, mm: number) => (this.fasten = { ...f, alignMm: { ...f.alignMm, [axis]: mm } })
|
|
332
|
+
return html`<div form>
|
|
333
|
+
${alignAxesOf(f.toPlane ? 'plane' : f.targetFace).map(({ axis, edges }) => {
|
|
334
|
+
const how = f.align[axis] === 'min' || f.align[axis] === 'max' ? (edges ? f.align[axis]! : 'centre') : (f.align[axis] ?? 'centre')
|
|
335
|
+
return html`<span>${i18next.t('figure.label.align-' + axis)}</span>
|
|
336
|
+
<select @change=${(e: Event) => set(axis, (e.target as HTMLSelectElement).value as Alignment)}>
|
|
337
|
+
<option value="centre" ?selected=${how === 'centre'}>${i18next.t('figure.label.align-centre')}</option>
|
|
338
|
+
${edges
|
|
339
|
+
? html`<option value="min" ?selected=${how === 'min'}>${i18next.t(`figure.label.align-${axis}-min`)}</option>
|
|
340
|
+
<option value="max" ?selected=${how === 'max'}>${i18next.t(`figure.label.align-${axis}-max`)}</option>`
|
|
341
|
+
: nothing}
|
|
342
|
+
<option value="mm" ?selected=${how === 'mm'}>${i18next.t('figure.label.align-mm')}</option>
|
|
343
|
+
</select>
|
|
344
|
+
${how === 'mm'
|
|
345
|
+
? html`<span></span><input type="number" step="any" aria-label=${i18next.t('figure.label.align-mm')} .value=${String(f.alignMm[axis] ?? 0)} @change=${(e: Event) => setMm(axis, Number((e.target as HTMLInputElement).value) || 0)} />`
|
|
346
|
+
: nothing}`
|
|
347
|
+
})}
|
|
348
|
+
</div>`
|
|
349
|
+
}
|
|
350
|
+
|
|
316
351
|
private renderMotion(asset: V3Asset) {
|
|
317
352
|
const now = motionOf(asset, this.partId)
|
|
353
|
+
const joint = asset.joints?.find(j => j.body1.part === this.partId)
|
|
318
354
|
if (now) {
|
|
319
355
|
return html`<div pair>
|
|
320
356
|
<span>
|
|
321
357
|
${i18next.t('figure.label.motion-' + now.kind)} · ${String(now.axis).toUpperCase()}
|
|
322
|
-
${now.
|
|
358
|
+
${now.follows
|
|
359
|
+
? html`<small>${i18next.t('figure.label.motion-follows', { joint: now.follows.joint, multiplier: now.follows.multiplier, offset: now.follows.offset ?? 0 })}</small>`
|
|
360
|
+
: now.control
|
|
361
|
+
? html`<small>${now.control.id} (${now.control.unit}) ${now.control.min} … ${now.control.max}</small>`
|
|
362
|
+
: nothing}
|
|
323
363
|
</span>
|
|
324
|
-
<button
|
|
364
|
+
<button
|
|
365
|
+
act
|
|
366
|
+
?disabled=${this.locked || !joint}
|
|
367
|
+
@click=${() => joint && this.command({ kind: 'set-joint', joint: { id: joint.id, type: 'fixed', body0: joint.body0, body1: joint.body1, ...(joint.origin ? { origin: joint.origin } : {}) } })}
|
|
368
|
+
>
|
|
325
369
|
<md-icon>close</md-icon>${i18next.t('figure.button.remove-motion')}
|
|
326
370
|
</button>
|
|
327
371
|
</div>`
|
|
328
372
|
}
|
|
373
|
+
/* A joint needs a parent (URDF): a part fastened to nothing is fastened first. */
|
|
374
|
+
if (!joint) return html`<p hint>${i18next.t('figure.text.fasten-before-moving')}</p>`
|
|
329
375
|
const m = this.motion
|
|
330
|
-
const unit = m.kind === '
|
|
376
|
+
const unit = m.kind === 'slide' ? 'mm' : 'deg'
|
|
377
|
+
/* A follower follows a joint that moves the same way; a choice the kind no longer allows is dropped. */
|
|
378
|
+
const leaders = leadersFor(asset, this.partId, m.kind)
|
|
379
|
+
const follow = leaders.some(j => j.id === m.follow) ? m.follow : ''
|
|
331
380
|
return html`
|
|
332
381
|
<div pair>
|
|
333
|
-
<select @change=${(e: Event) => this.setKind((e.target as HTMLSelectElement).value as 'slide' | 'turn')}>
|
|
382
|
+
<select @change=${(e: Event) => this.setKind((e.target as HTMLSelectElement).value as 'slide' | 'turn' | 'continuous')}>
|
|
334
383
|
<option value="slide" ?selected=${m.kind === 'slide'}>${i18next.t('figure.label.motion-slide')}</option>
|
|
335
384
|
<option value="turn" ?selected=${m.kind === 'turn'}>${i18next.t('figure.label.motion-turn')}</option>
|
|
385
|
+
<option value="continuous" ?selected=${m.kind === 'continuous'}>${i18next.t('figure.label.motion-continuous')}</option>
|
|
336
386
|
</select>
|
|
337
387
|
<select @change=${(e: Event) => (this.motion = { ...m, axis: (e.target as HTMLSelectElement).value as Axis })}>
|
|
338
388
|
${AXES.map(a => html`<option value=${a} ?selected=${a === m.axis}>${i18next.t('figure.label.motion-axis')} ${a.toUpperCase()}</option>`)}
|
|
339
389
|
</select>
|
|
340
390
|
</div>
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
391
|
+
${leaders.length
|
|
392
|
+
? html`<div form>
|
|
393
|
+
<span>${i18next.t('figure.label.motion-follow')}</span>
|
|
394
|
+
<select @change=${(e: Event) => (this.motion = { ...m, follow: (e.target as HTMLSelectElement).value })}>
|
|
395
|
+
<option value="" ?selected=${!follow}>${i18next.t('figure.label.motion-own-control')}</option>
|
|
396
|
+
${leaders.map(j => html`<option value=${j.id} ?selected=${j.id === follow}>${j.body1.part}${j.state ? ` · ${j.state.id}` : ''}</option>`)}
|
|
397
|
+
</select>
|
|
398
|
+
</div>`
|
|
399
|
+
: nothing}
|
|
400
|
+
${follow
|
|
401
|
+
? html`<div form>
|
|
402
|
+
<span>${i18next.t('figure.label.motion-multiplier')}</span>
|
|
403
|
+
<input type="number" step="any" .value=${String(m.multiplier)} @change=${(e: Event) => (this.motion = { ...m, multiplier: Number((e.target as HTMLInputElement).value) })} />
|
|
404
|
+
<span>${i18next.t('figure.label.motion-offset')} (${unit})</span>
|
|
405
|
+
<input type="number" step="any" .value=${String(m.offset)} @change=${(e: Event) => (this.motion = { ...m, offset: Number((e.target as HTMLInputElement).value) || 0 })} />
|
|
406
|
+
</div>`
|
|
407
|
+
: html`<div form>
|
|
408
|
+
<span>${i18next.t('figure.label.control-id')}</span>
|
|
409
|
+
<input .value=${m.id} placeholder=${`${this.partId}-${m.kind}`} @change=${(e: Event) => (this.motion = { ...m, id: (e.target as HTMLInputElement).value.trim() })} />
|
|
410
|
+
${m.kind === 'continuous'
|
|
411
|
+
? nothing
|
|
412
|
+
: html`<span>${i18next.t('figure.label.control-range')} (${unit})</span>
|
|
413
|
+
<div pair>
|
|
414
|
+
<input type="number" step="any" .value=${String(m.min)} @change=${(e: Event) => (this.motion = { ...m, min: Number((e.target as HTMLInputElement).value) })} />
|
|
415
|
+
<input type="number" step="any" .value=${String(m.max)} @change=${(e: Event) => (this.motion = { ...m, max: Number((e.target as HTMLInputElement).value) })} />
|
|
416
|
+
</div>`}
|
|
417
|
+
</div>`}
|
|
350
418
|
<button
|
|
351
419
|
act
|
|
352
|
-
?disabled=${this.locked || !(m.max > m.min)}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
kind: 'add-motion',
|
|
356
|
-
part: this.partId,
|
|
357
|
-
motion: { kind: m.kind, axis: m.axis },
|
|
358
|
-
state: { id: m.id || `${this.partId}-${m.kind}`, unit, min: m.min, max: m.max, start: m.min }
|
|
359
|
-
})}
|
|
420
|
+
?disabled=${this.locked || (follow ? !(Number.isFinite(m.multiplier) && m.multiplier !== 0) : m.kind !== 'continuous' && !(m.max > m.min && m.min <= 0 && 0 <= m.max))}
|
|
421
|
+
title=${!follow && m.kind !== 'continuous' && !(m.min <= 0 && 0 <= m.max) ? i18next.t('figure.text.joint-range-holds-zero') : ''}
|
|
422
|
+
@click=${() => this.command({ kind: 'set-joint', joint: motionJoint(joint, { ...m, follow }, this.partId) })}
|
|
360
423
|
>
|
|
361
424
|
<md-icon>animation</md-icon>${i18next.t('figure.button.add-motion')}
|
|
362
425
|
</button>
|
|
363
426
|
`
|
|
364
427
|
}
|
|
365
428
|
|
|
366
|
-
private setKind(kind: 'slide' | 'turn') {
|
|
367
|
-
this.motion = kind === '
|
|
429
|
+
private setKind(kind: 'slide' | 'turn' | 'continuous') {
|
|
430
|
+
this.motion = kind === 'slide' ? { ...this.motion, kind, min: 0, max: 500 } : { ...this.motion, kind, min: -90, max: 90 }
|
|
368
431
|
}
|
|
369
432
|
|
|
370
433
|
private command(action: V3AuthoringAction) {
|
|
@@ -3,6 +3,7 @@ import { css, html, LitElement, nothing } from 'lit'
|
|
|
3
3
|
import { customElement, property } from 'lit/decorators.js'
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
5
|
import { ScrollbarStyles } from '@operato/styles'
|
|
6
|
+
import { PART_LIMIT } from '@hatiolab/figure-model'
|
|
6
7
|
import type { V3PrimitiveCreation } from '@hatiolab/figure-model'
|
|
7
8
|
import type { V3BudgetLine } from './v3-maturity.js'
|
|
8
9
|
import { partLabelOf } from './v3-fields.js'
|
|
@@ -28,6 +29,8 @@ export interface V3PartRow {
|
|
|
28
29
|
hidden?: boolean
|
|
29
30
|
/** 자산이 지금 상태에서 보이지 않는다고 한 것. 저장되는 것이고, 여기서는 표시만 한다. */
|
|
30
31
|
invisible?: boolean
|
|
32
|
+
/** 되풀이한 부품이면 지금 몇 개로 그려지나. 되풀이는 한 부품이다 — 한 줄로 보인다. */
|
|
33
|
+
copies?: number
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/** 제공자마다의 아이콘. Material Symbols 이름이다. */
|
|
@@ -147,25 +150,61 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
147
150
|
font-style: normal;
|
|
148
151
|
color: var(--md-sys-color-error);
|
|
149
152
|
}
|
|
153
|
+
span[room][full] {
|
|
154
|
+
color: var(--md-sys-color-error);
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
}
|
|
157
|
+
p[full] {
|
|
158
|
+
margin: 4px 0 0;
|
|
159
|
+
font-size: 0.68rem;
|
|
160
|
+
color: var(--md-sys-color-error);
|
|
161
|
+
}
|
|
162
|
+
div[level] {
|
|
163
|
+
display: grid;
|
|
164
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
165
|
+
align-items: center;
|
|
166
|
+
gap: 6px;
|
|
167
|
+
margin: 0 0 6px;
|
|
168
|
+
font-size: 0.68rem;
|
|
169
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
170
|
+
}
|
|
171
|
+
div[level] select {
|
|
172
|
+
min-width: 0;
|
|
173
|
+
padding: 2px 4px;
|
|
174
|
+
font: inherit;
|
|
175
|
+
font-size: 0.7rem;
|
|
176
|
+
color: var(--md-sys-color-on-surface);
|
|
177
|
+
background: var(--md-sys-color-surface-container-lowest);
|
|
178
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
179
|
+
border-radius: 6px;
|
|
180
|
+
}
|
|
181
|
+
small[copies] {
|
|
182
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
183
|
+
}
|
|
150
184
|
span[room] {
|
|
151
185
|
font-size: 0.7rem;
|
|
152
186
|
font-weight: 400;
|
|
153
187
|
letter-spacing: 0;
|
|
154
188
|
text-transform: none;
|
|
155
189
|
}
|
|
190
|
+
/* Three to a row, small (user, 2026-09-24): the palette is reached for, not read -- the list below is what grows. */
|
|
156
191
|
div[palette] {
|
|
157
192
|
display: grid;
|
|
158
|
-
grid-template-columns: repeat(
|
|
159
|
-
gap:
|
|
193
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
194
|
+
gap: 3px;
|
|
160
195
|
}
|
|
161
196
|
button[shape] {
|
|
162
197
|
display: flex;
|
|
163
198
|
flex-direction: column;
|
|
164
199
|
align-items: center;
|
|
165
|
-
gap:
|
|
166
|
-
|
|
200
|
+
gap: 1px;
|
|
201
|
+
min-width: 0;
|
|
202
|
+
padding: 5px 1px 4px;
|
|
167
203
|
font: var(--label-font, inherit);
|
|
168
|
-
font-size: 0.
|
|
204
|
+
font-size: 0.6rem;
|
|
205
|
+
line-height: 1.2;
|
|
206
|
+
text-align: center;
|
|
207
|
+
overflow-wrap: anywhere;
|
|
169
208
|
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
170
209
|
background: none;
|
|
171
210
|
border: 1px solid var(--md-sys-color-outline-variant);
|
|
@@ -182,7 +221,7 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
182
221
|
cursor: default;
|
|
183
222
|
}
|
|
184
223
|
button[shape] md-icon {
|
|
185
|
-
--md-icon-size:
|
|
224
|
+
--md-icon-size: 18px;
|
|
186
225
|
}
|
|
187
226
|
ol {
|
|
188
227
|
margin: 0;
|
|
@@ -323,8 +362,15 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
323
362
|
* 기준이 저작을 이끈다(V2 모델러의 정체성). 등급이 D 면 무엇이 넘쳤는지를 적는다. 누르면 성숙도 탭이 열린다.
|
|
324
363
|
*/
|
|
325
364
|
@property({ attribute: false }) budget?: V3BudgetLine
|
|
365
|
+
/**
|
|
366
|
+
* 디테일 등급의 부품 한도 — V2 의 S/M/L(사용자 2026-09-24). 부품을 넣기 **전에** 몇 개 남았는지 보이고, 다 차면 팔레트가
|
|
367
|
+
* 잠긴다. 되풀이는 한 부품으로 센다. 등급을 안 정했으면 한도는 없다.
|
|
368
|
+
*/
|
|
369
|
+
@property({ attribute: false }) partBudget?: { level: 'S' | 'M' | 'L' | null; used: number; limit: number | null }
|
|
326
370
|
|
|
327
371
|
render() {
|
|
372
|
+
const b = this.partBudget
|
|
373
|
+
const full = !!b && b.limit !== null && b.used >= b.limit
|
|
328
374
|
return html`
|
|
329
375
|
<section>
|
|
330
376
|
<h3>${i18next.t('figure.label.shapes')}</h3>
|
|
@@ -333,8 +379,8 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
333
379
|
recipe => html`
|
|
334
380
|
<button
|
|
335
381
|
shape
|
|
336
|
-
?disabled=${this.locked}
|
|
337
|
-
title=${i18next.t(recipe.label)}
|
|
382
|
+
?disabled=${this.locked || full}
|
|
383
|
+
title=${full ? i18next.t('figure.text.part-limit-reached') : i18next.t(recipe.label)}
|
|
338
384
|
@click=${() => this.ask('add-part', { kind: recipe.kind })}
|
|
339
385
|
>
|
|
340
386
|
<md-icon>${recipe.icon}</md-icon>
|
|
@@ -343,14 +389,18 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
343
389
|
`
|
|
344
390
|
)}
|
|
345
391
|
</div>
|
|
392
|
+
${full ? html`<p full>${i18next.t('figure.text.part-limit-reached-short', { level: b!.level, limit: b!.limit })}</p>` : nothing}
|
|
346
393
|
</section>
|
|
347
394
|
|
|
348
395
|
<section list>
|
|
349
396
|
<h3>
|
|
350
397
|
<span>${i18next.t('figure.label.parts')}</span>
|
|
351
|
-
<span room
|
|
398
|
+
<span room ?full=${full} title=${i18next.t('figure.text.limit-comes-from-level')}
|
|
399
|
+
>${b && b.limit !== null ? i18next.t('figure.text.n-of-limit', { used: b.used, limit: b.limit }) : (b?.used ?? this.parts.length)}</span
|
|
400
|
+
>
|
|
352
401
|
${this.renderShowAll()}
|
|
353
402
|
</h3>
|
|
403
|
+
${this.renderLevel()}
|
|
354
404
|
${this.renderBudget()}
|
|
355
405
|
${this.parts.length === 0
|
|
356
406
|
? html`<p quiet>${i18next.t('figure.text.no-parts-yet')}</p>`
|
|
@@ -361,6 +411,32 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
361
411
|
`
|
|
362
412
|
}
|
|
363
413
|
|
|
414
|
+
/** 디테일 등급 — 고르면 한도가 바뀐다. V2 의 자리 그대로, 목록 머리 아래. */
|
|
415
|
+
private renderLevel() {
|
|
416
|
+
const b = this.partBudget
|
|
417
|
+
if (!b) return nothing
|
|
418
|
+
return html`<div level>
|
|
419
|
+
<label for="detail-level">${i18next.t('figure.label.detail-level')}</label>
|
|
420
|
+
<select
|
|
421
|
+
id="detail-level"
|
|
422
|
+
title=${i18next.t('figure.text.detail-level-explained')}
|
|
423
|
+
.value=${b.level ?? ''}
|
|
424
|
+
?disabled=${this.locked}
|
|
425
|
+
@change=${(e: Event) => {
|
|
426
|
+
const value = (e.target as HTMLSelectElement).value
|
|
427
|
+
this.dispatchEvent(new CustomEvent('set-detail-level', { detail: { level: value || null }, bubbles: true, composed: true }))
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
<option value="" ?selected=${!b.level}>${i18next.t('figure.label.detail-none')}</option>
|
|
431
|
+
${(['S', 'M', 'L'] as const).map(
|
|
432
|
+
one => html`<option value=${one} ?selected=${one === b.level}>
|
|
433
|
+
${one} ${i18next.t('figure.label.detail-' + one)} · ${i18next.t('figure.text.n-parts-at-most', { limit: PART_LIMIT[one] })}
|
|
434
|
+
</option>`
|
|
435
|
+
)}
|
|
436
|
+
</select>
|
|
437
|
+
</div>`
|
|
438
|
+
}
|
|
439
|
+
|
|
364
440
|
private renderBudget() {
|
|
365
441
|
const b = this.budget
|
|
366
442
|
if (!b || !this.parts.length) return nothing
|
|
@@ -434,7 +510,7 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
434
510
|
</button>
|
|
435
511
|
<md-icon>${PROVIDER_ICON[part.provider] ?? 'deployed_code'}</md-icon>
|
|
436
512
|
<span label title=${part.id}
|
|
437
|
-
>${partLabelOf(part.id)}${part.invisible ? html`<small>${i18next.t('figure.label.state-hidden')}</small>` : nothing}</span
|
|
513
|
+
>${partLabelOf(part.id)}${part.copies ? html` <small copies>× ${part.copies}</small>` : nothing}${part.invisible ? html`<small>${i18next.t('figure.label.state-hidden')}</small>` : nothing}</span
|
|
438
514
|
>
|
|
439
515
|
${selected ? this.renderOps(part.id, index) : nothing}
|
|
440
516
|
</li>
|
|
@@ -10,6 +10,7 @@ import type { V3AuthoringAction, V3Face } from '@hatiolab/figure-model'
|
|
|
10
10
|
import { occupancyFromParts } from './v3-occupancy.js'
|
|
11
11
|
import { tagsOf, v3FigureFacts, type Placement, type V3FigureFacts } from './v3-figure-facts.js'
|
|
12
12
|
import { SidePanelStyles } from './side-panel-styles.js'
|
|
13
|
+
import { ScrollbarStyles } from '@operato/styles'
|
|
13
14
|
|
|
14
15
|
const PLACEMENTS: Placement[] = ['floor', 'ceiling', 'center']
|
|
15
16
|
const FACES = Object.keys(V3_FACES) as V3Face[]
|
|
@@ -32,7 +33,7 @@ const FACES = Object.keys(V3_FACES) as V3Face[]
|
|
|
32
33
|
*/
|
|
33
34
|
@customElement('figure-v3-properties')
|
|
34
35
|
export class FigureV3Properties extends localize(i18next)(LitElement) {
|
|
35
|
-
static styles = [SidePanelStyles, css`
|
|
36
|
+
static styles = [ScrollbarStyles, SidePanelStyles, css`
|
|
36
37
|
:host {
|
|
37
38
|
display: block;
|
|
38
39
|
/* It stands inside the properties tab's panel, which gives the margin; a second one indented it past the type. */
|
|
@@ -88,6 +88,14 @@ export const SidePanelStyles = css`
|
|
|
88
88
|
overflow-wrap: anywhere;
|
|
89
89
|
white-space: normal;
|
|
90
90
|
}
|
|
91
|
+
/* A value read, not edited, beside its label: the field's size and colour, not the page's. */
|
|
92
|
+
div[inline-field] > span[value] {
|
|
93
|
+
font-size: 0.76rem;
|
|
94
|
+
color: var(--md-sys-color-on-surface);
|
|
95
|
+
}
|
|
96
|
+
label span[unit] {
|
|
97
|
+
color: var(--md-sys-color-outline);
|
|
98
|
+
}
|
|
91
99
|
div[inline-field] + div[inline-field],
|
|
92
100
|
div[inline-field] + div[vector-row],
|
|
93
101
|
div[vector-row] + div[inline-field] {
|