@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.
Files changed (138) hide show
  1. package/client/modeller/angle-dial.ts +70 -0
  2. package/client/modeller/figure-settings.ts +328 -0
  3. package/client/modeller/figure-v3-capability.ts +137 -0
  4. package/client/modeller/figure-v3-clips.ts +253 -0
  5. package/client/modeller/figure-v3-controls.ts +212 -0
  6. package/client/modeller/figure-v3-links.ts +150 -79
  7. package/client/modeller/figure-v3-material.ts +108 -35
  8. package/client/modeller/figure-v3-parts.ts +96 -3
  9. package/client/modeller/figure-v3-preview.ts +326 -0
  10. package/client/modeller/figure-v3-properties.ts +29 -57
  11. package/client/modeller/figure-v3-verdict.ts +598 -60
  12. package/client/modeller/figure-view.ts +152 -0
  13. package/client/modeller/side-panel-styles.ts +326 -0
  14. package/client/modeller/v3-canvas.ts +221 -263
  15. package/client/modeller/v3-clips.ts +89 -0
  16. package/client/modeller/v3-controls.ts +54 -0
  17. package/client/modeller/v3-editor.ts +532 -223
  18. package/client/modeller/v3-fields.ts +62 -0
  19. package/client/modeller/v3-help.ts +41 -0
  20. package/client/modeller/v3-keys.ts +34 -0
  21. package/client/modeller/v3-links.ts +126 -10
  22. package/client/modeller/v3-live.ts +79 -0
  23. package/client/modeller/v3-maturity.ts +415 -0
  24. package/client/modeller/v3-palette.ts +45 -0
  25. package/client/modeller/v3-preview-model.ts +54 -0
  26. package/client/modeller/v3-stage-model.ts +159 -0
  27. package/client/pages/figure-list-page.ts +4 -4
  28. package/client/pages/figure-v3-modeller-page.ts +66 -134
  29. package/dist-client/modeller/angle-dial.d.ts +32 -0
  30. package/dist-client/modeller/angle-dial.js +41 -0
  31. package/dist-client/modeller/angle-dial.js.map +1 -0
  32. package/dist-client/modeller/figure-settings.d.ts +39 -0
  33. package/dist-client/modeller/figure-settings.js +317 -0
  34. package/dist-client/modeller/figure-settings.js.map +1 -0
  35. package/dist-client/modeller/figure-v3-capability.d.ts +22 -0
  36. package/dist-client/modeller/figure-v3-capability.js +141 -0
  37. package/dist-client/modeller/figure-v3-capability.js.map +1 -0
  38. package/dist-client/modeller/figure-v3-clips.d.ts +31 -0
  39. package/dist-client/modeller/figure-v3-clips.js +268 -0
  40. package/dist-client/modeller/figure-v3-clips.js.map +1 -0
  41. package/dist-client/modeller/figure-v3-controls.d.ts +36 -0
  42. package/dist-client/modeller/figure-v3-controls.js +226 -0
  43. package/dist-client/modeller/figure-v3-controls.js.map +1 -0
  44. package/dist-client/modeller/figure-v3-links.d.ts +10 -2
  45. package/dist-client/modeller/figure-v3-links.js +134 -79
  46. package/dist-client/modeller/figure-v3-links.js.map +1 -1
  47. package/dist-client/modeller/figure-v3-material.d.ts +5 -2
  48. package/dist-client/modeller/figure-v3-material.js +109 -35
  49. package/dist-client/modeller/figure-v3-material.js.map +1 -1
  50. package/dist-client/modeller/figure-v3-parts.d.ts +10 -0
  51. package/dist-client/modeller/figure-v3-parts.js +88 -3
  52. package/dist-client/modeller/figure-v3-parts.js.map +1 -1
  53. package/dist-client/modeller/figure-v3-preview.d.ts +34 -0
  54. package/dist-client/modeller/figure-v3-preview.js +322 -0
  55. package/dist-client/modeller/figure-v3-preview.js.map +1 -0
  56. package/dist-client/modeller/figure-v3-properties.d.ts +2 -2
  57. package/dist-client/modeller/figure-v3-properties.js +29 -57
  58. package/dist-client/modeller/figure-v3-properties.js.map +1 -1
  59. package/dist-client/modeller/figure-v3-verdict.d.ts +31 -17
  60. package/dist-client/modeller/figure-v3-verdict.js +579 -59
  61. package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
  62. package/dist-client/modeller/figure-view.d.ts +118 -0
  63. package/dist-client/modeller/figure-view.js +78 -0
  64. package/dist-client/modeller/figure-view.js.map +1 -0
  65. package/dist-client/modeller/side-panel-styles.d.ts +23 -0
  66. package/dist-client/modeller/side-panel-styles.js +326 -0
  67. package/dist-client/modeller/side-panel-styles.js.map +1 -0
  68. package/dist-client/modeller/v3-canvas.d.ts +29 -37
  69. package/dist-client/modeller/v3-canvas.js +188 -260
  70. package/dist-client/modeller/v3-canvas.js.map +1 -1
  71. package/dist-client/modeller/v3-clips.d.ts +39 -0
  72. package/dist-client/modeller/v3-clips.js +58 -0
  73. package/dist-client/modeller/v3-clips.js.map +1 -0
  74. package/dist-client/modeller/v3-controls.d.ts +28 -0
  75. package/dist-client/modeller/v3-controls.js +28 -0
  76. package/dist-client/modeller/v3-controls.js.map +1 -0
  77. package/dist-client/modeller/v3-editor.d.ts +58 -1
  78. package/dist-client/modeller/v3-editor.js +556 -218
  79. package/dist-client/modeller/v3-editor.js.map +1 -1
  80. package/dist-client/modeller/v3-fields.d.ts +30 -0
  81. package/dist-client/modeller/v3-fields.js +46 -0
  82. package/dist-client/modeller/v3-fields.js.map +1 -0
  83. package/dist-client/modeller/v3-help.d.ts +18 -0
  84. package/dist-client/modeller/v3-help.js +37 -0
  85. package/dist-client/modeller/v3-help.js.map +1 -0
  86. package/dist-client/modeller/v3-keys.d.ts +14 -0
  87. package/dist-client/modeller/v3-keys.js +23 -0
  88. package/dist-client/modeller/v3-keys.js.map +1 -0
  89. package/dist-client/modeller/v3-links.d.ts +44 -2
  90. package/dist-client/modeller/v3-links.js +125 -8
  91. package/dist-client/modeller/v3-links.js.map +1 -1
  92. package/dist-client/modeller/v3-live.d.ts +31 -0
  93. package/dist-client/modeller/v3-live.js +58 -0
  94. package/dist-client/modeller/v3-live.js.map +1 -0
  95. package/dist-client/modeller/v3-maturity.d.ts +120 -0
  96. package/dist-client/modeller/v3-maturity.js +317 -0
  97. package/dist-client/modeller/v3-maturity.js.map +1 -0
  98. package/dist-client/modeller/v3-palette.d.ts +7 -0
  99. package/dist-client/modeller/v3-palette.js +42 -0
  100. package/dist-client/modeller/v3-palette.js.map +1 -0
  101. package/dist-client/modeller/v3-preview-model.d.ts +32 -0
  102. package/dist-client/modeller/v3-preview-model.js +48 -0
  103. package/dist-client/modeller/v3-preview-model.js.map +1 -0
  104. package/dist-client/modeller/v3-stage-model.d.ts +31 -0
  105. package/dist-client/modeller/v3-stage-model.js +121 -0
  106. package/dist-client/modeller/v3-stage-model.js.map +1 -0
  107. package/dist-client/pages/figure-list-page.js +4 -4
  108. package/dist-client/pages/figure-list-page.js.map +1 -1
  109. package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -6
  110. package/dist-client/pages/figure-v3-modeller-page.js +64 -136
  111. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  112. package/dist-client/tsconfig.chk.tsbuildinfo +1 -0
  113. package/dist-client/tsconfig.scratch-fm.tsbuildinfo +1 -0
  114. package/dist-client/tsconfig.tsbuildinfo +1 -1
  115. package/package.json +6 -6
  116. package/test/v3-clips.test.ts +70 -0
  117. package/test/v3-controls.test.ts +68 -0
  118. package/test/v3-fields.test.ts +62 -0
  119. package/test/v3-gizmo.test.ts +3 -5
  120. package/test/v3-keys.test.ts +37 -0
  121. package/test/v3-layout.test.ts +78 -0
  122. package/test/v3-links.test.ts +118 -7
  123. package/test/v3-live.test.ts +99 -0
  124. package/test/v3-material.test.ts +6 -0
  125. package/test/v3-maturity.test.ts +268 -0
  126. package/test/v3-parts-wiring.test.ts +3 -1
  127. package/test/v3-preview.test.ts +42 -0
  128. package/test/v3-properties.test.ts +4 -2
  129. package/test/v3-scene-canvas.test.ts +85 -0
  130. package/test/v3-stage-model.test.ts +25 -0
  131. package/test/v3-theme.test.ts +16 -25
  132. package/test/v3-verdict.test.ts +12 -6
  133. package/test/v3-view.test.ts +20 -0
  134. package/translations/en.json +98 -5
  135. package/translations/ja.json +100 -7
  136. package/translations/ko.json +98 -5
  137. package/translations/ms.json +100 -7
  138. package/translations/zh.json +100 -7
@@ -7,7 +7,29 @@ import { i18next, localize } from '@operato/i18n'
7
7
  import { V3_FACES } from '@hatiolab/figure-model'
8
8
  import type { V3Asset, V3AuthoringAction, V3DimensionSource, V3Face } from '@hatiolab/figure-model'
9
9
 
10
- import { AXES, canFasten, canFollow, choiceOf, fastenedTo, followOf, hasInstanceSize, motionOf, positionsWorkedOut, type Axis, type Follow } from './v3-links.js'
10
+ import {
11
+ acceptsOf,
12
+ AXES,
13
+ canFasten,
14
+ canFollow,
15
+ dimensionNow,
16
+ followableAxes,
17
+ positionIsShare,
18
+ choiceOf,
19
+ fastenedTo,
20
+ followOf,
21
+ hasInstanceSize,
22
+ holdsThings,
23
+ motionOf,
24
+ placeFacesOf,
25
+ placeIdFor,
26
+ placesOn,
27
+ positionsWorkedOut,
28
+ type Axis,
29
+ type Follow
30
+ } from './v3-links.js'
31
+ import { SidePanelStyles } from './side-panel-styles.js'
32
+ import { HelpToggles } from './v3-help.js'
11
33
 
12
34
  const FACES = Object.keys(V3_FACES) as V3Face[]
13
35
 
@@ -26,76 +48,25 @@ const FACES = Object.keys(V3_FACES) as V3Face[]
26
48
  */
27
49
  @customElement('figure-v3-links')
28
50
  export class FigureV3Links extends localize(i18next)(LitElement) {
29
- static styles = css`
51
+ private readonly help = new HelpToggles(this)
52
+ static styles = [SidePanelStyles, css`
30
53
  :host {
31
- display: grid;
32
- gap: 10px;
54
+ display: block;
33
55
  }
34
- h3 {
35
- font-size: var(--ui-t-body, 13px);
36
- margin: 12px 0 0;
56
+ /* The measure's name, then its rule: X, or a cylinder's 반지름 · 길이, on one line. */
57
+ div[row] > b {
58
+ white-space: nowrap;
37
59
  }
38
60
  div[row] {
39
61
  display: grid;
40
- grid-template-columns: 24px minmax(0, 1fr);
41
- gap: 6px;
42
- align-items: center;
43
- }
44
- div[form] {
45
- display: grid;
46
- grid-template-columns: 96px minmax(0, 1fr);
62
+ grid-template-columns: minmax(24px, max-content) minmax(0, 1fr);
47
63
  gap: 6px;
48
64
  align-items: center;
49
65
  }
50
- div[pair] {
51
- display: flex;
52
- gap: 6px;
53
- }
54
- div[pair] > * {
55
- flex: 1;
56
- min-width: 0;
57
- }
58
- select,
59
- input,
60
- button {
61
- font: inherit;
62
- min-width: 0;
63
- }
64
- button[act] {
65
- justify-self: start;
66
- display: inline-flex;
67
- align-items: center;
68
- gap: 4px;
69
- padding: 4px 10px;
70
- border: none;
71
- border-radius: 999em;
72
- cursor: pointer;
73
- color: var(--md-sys-color-on-secondary-container);
74
- background: var(--md-sys-color-secondary-container);
75
- }
76
- button[act]:disabled {
77
- opacity: 0.5;
78
- cursor: default;
79
- }
80
- button[act] md-icon {
81
- font-size: 16px;
82
- width: 16px;
83
- height: 16px;
84
- }
85
- small,
86
- p[hint] {
87
- margin: 0;
88
- color: var(--md-sys-color-on-surface-variant);
89
- }
90
66
  b[bound] {
91
67
  font-weight: 600;
92
68
  }
93
- label[toggle] {
94
- display: flex;
95
- align-items: center;
96
- gap: 6px;
97
- }
98
- `
69
+ `]
99
70
 
100
71
  /** The asset being edited. */
101
72
  @property({ attribute: false }) asset?: V3Asset
@@ -104,37 +75,124 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
104
75
  /** The part's own extent along its axes now, in mm — what a dimension is fixed at when it stops following. */
105
76
  @property({ attribute: false }) dims?: [number, number, number]
106
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'
107
80
 
108
81
  @state() private fasten = { face: 'bottom' as V3Face, toPlane: false, target: '', targetFace: 'top' as V3Face, facing: 'meet' as 'meet' | 'flush', gap: 0 }
109
82
  @state() private motion = { kind: 'slide' as 'slide' | 'turn', axis: 'x' as Axis, id: '', min: 0, max: 500 }
83
+ @state() private place = { role: 'grip' as 'seat' | 'grip', face: 'top' as V3Face, label: '', accepts: '', capacity: 1 }
110
84
 
111
85
  render() {
112
86
  const asset = this.asset
113
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>`
94
+ return html`
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>
104
+ `
105
+ }
106
+
107
+ /** Where things are held on this part: a seat things rest on, or a grip things are held in (parity row ⑤). */
108
+ private renderPlaces(asset: V3Asset) {
109
+ const faces = placeFacesOf(asset, this.partId)
110
+ const p = this.place
111
+ const face = faces.includes(p.face) ? p.face : faces[0]!
112
+ const accepts = acceptsOf(p.accepts)
113
+ const capacityOk = Number.isSafeInteger(p.capacity) && p.capacity >= 1
114
+ const roleName = (role: string) => i18next.t('figure.label.place-' + role)
114
115
  return html`
115
- <h3>${i18next.t('figure.label.dimensions')}</h3>
116
- ${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
117
- <h3>${i18next.t('figure.label.placement')}</h3>
118
- ${this.renderPositions(asset)}
119
- ${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
120
- <h3>${i18next.t('figure.label.motion')}</h3>
121
- ${this.renderMotion(asset)}
116
+ ${placesOn(asset, this.partId).map(
117
+ one => html`<div pair>
118
+ <span>
119
+ <b bound>${one.label}</b>
120
+ <small>${roleName(one.role)} · ${one.face ? i18next.t('figure.label.face-' + one.face) : ''} · ${one.accepts.join(', ')} · ${i18next.t('figure.label.place-holds', { count: one.capacity })}</small>
121
+ </span>
122
+ <button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'remove-binding', id: one.id })}>
123
+ <md-icon>close</md-icon>${i18next.t('figure.button.remove-place')}
124
+ </button>
125
+ </div>`
126
+ )}
127
+ ${holdsThings(asset)
128
+ ? nothing
129
+ : html`<p hint>${i18next.t('figure.text.place-needs-holding')}</p>
130
+ <button act ?disabled=${this.locked} @click=${() => this.declareHolding(asset)}>
131
+ <md-icon>inventory_2</md-icon>${i18next.t('figure.button.declare-holding')}
132
+ </button>`}
133
+ <div pair>
134
+ <select @change=${(e: Event) => (this.place = { ...p, role: (e.target as HTMLSelectElement).value as 'seat' | 'grip' })}>
135
+ <option value="grip" ?selected=${p.role === 'grip'}>${roleName('grip')}</option>
136
+ <option value="seat" ?selected=${p.role === 'seat'}>${roleName('seat')}</option>
137
+ </select>
138
+ <select @change=${(e: Event) => (this.place = { ...p, face: (e.target as HTMLSelectElement).value as V3Face })}>
139
+ ${faces.map(one => html`<option value=${one} ?selected=${one === face}>${i18next.t('figure.label.face-' + one)}</option>`)}
140
+ </select>
141
+ </div>
142
+ <div form>
143
+ <span>${i18next.t('figure.label.place-name')}</span>
144
+ <input .value=${p.label} placeholder=${roleName(p.role)} @change=${(e: Event) => (this.place = { ...p, label: (e.target as HTMLInputElement).value })} />
145
+ <span>${i18next.t('figure.label.place-accepts')}</span>
146
+ <input .value=${p.accepts} placeholder="BOX, TOTE" @input=${(e: Event) => (this.place = { ...p, accepts: (e.target as HTMLInputElement).value })} />
147
+ <span>${i18next.t('figure.label.place-capacity')}</span>
148
+ <input type="number" min="1" step="1" .value=${String(p.capacity)} @change=${(e: Event) => (this.place = { ...p, capacity: (e.target as HTMLInputElement).valueAsNumber })} />
149
+ </div>
150
+ ${accepts.length ? nothing : html`<small>${i18next.t('figure.text.place-accepts-needed')}</small>`}
151
+ <button
152
+ act
153
+ ?disabled=${this.locked || !accepts.length || !capacityOk}
154
+ @click=${() =>
155
+ this.command({
156
+ kind: 'add-binding',
157
+ id: placeIdFor(asset, this.partId, p.role),
158
+ label: p.label.trim() || roleName(p.role),
159
+ role: p.role,
160
+ part: this.partId,
161
+ face,
162
+ accepts,
163
+ capacity: p.capacity
164
+ })}
165
+ >
166
+ <md-icon>back_hand</md-icon>${i18next.t('figure.button.add-place')}
167
+ </button>
122
168
  `
123
169
  }
124
170
 
171
+ /** The figure holds things: `Capacity` and `Holdable`, the ladder's middle rung. One part edit, one undo step. */
172
+ private declareHolding(asset: V3Asset) {
173
+ const names = [...new Set([...(asset.document.capabilities.names ?? []), 'Capacity', 'Holdable'])]
174
+ this.dispatchEvent(new CustomEvent('v3-part-edit', { detail: { edit: { kind: 'set-capabilities', names } }, bubbles: true, composed: true }))
175
+ }
176
+
125
177
  private renderDimensions(asset: V3Asset) {
126
178
  const others = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFollow(asset, one))
127
- return AXES.map(axis => {
179
+ const round = followableAxes(asset, this.partId).length === 2
180
+ return followableAxes(asset, this.partId).map(axis => {
128
181
  const now = followOf(asset, this.partId, axis)
129
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()
130
185
  return html`<div row>
131
- <b>${axis.toUpperCase()}</b>
132
- <select ?disabled=${this.locked || now.of === 'expression'} @change=${(e: Event) => this.follow(axis, (e.target as HTMLSelectElement).value, now)}>
186
+ <b>${name}</b>
187
+ <select ?disabled=${this.locked} @change=${(e: Event) => this.follow(asset, axis, (e.target as HTMLSelectElement).value, now)}>
133
188
  <option value="own" ?selected=${value === 'own'}>${i18next.t('figure.label.follows-nothing')}</option>
134
189
  ${AXES.filter(one => hasInstanceSize(asset, one)).map(
135
- one => html`<option value=${`instance:${one}`} ?selected=${value === `instance:${one}`}>
136
- ${i18next.t('figure.label.follows-instance')} · ${one.toUpperCase()}
137
- </option>`
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>`
138
196
  )}
139
197
  ${others.flatMap(other =>
140
198
  AXES.map(
@@ -149,15 +207,24 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
149
207
  })
150
208
  }
151
209
 
152
- private follow(axis: Axis, choice: string, now: Follow) {
210
+ private follow(asset: V3Asset, axis: Axis, choice: string, now: Follow) {
153
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
154
214
  if (choice === 'own') {
155
215
  if (!following) return
156
- const mm = Math.max(1, Math.round(this.dims?.[AXES.indexOf(axis)] ?? 100))
157
- 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)) })
158
217
  return
159
218
  }
160
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
+ }
161
228
  const source: V3DimensionSource = of === 'instance' ? { of: 'instance', axis: a as Axis } : { of: 'part', part: a!, axis: b as Axis }
162
229
  this.command({ kind: 'link-dimension', part: this.partId, axis, source, replace: following })
163
230
  }
@@ -169,10 +236,15 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
169
236
  private renderPositions(asset: V3Asset) {
170
237
  return positionsWorkedOut(asset, this.partId).map(
171
238
  axis => html`<div pair>
172
- <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>
173
240
  <button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'fix-position', part: this.partId, axis })}>
174
241
  <md-icon>push_pin</md-icon>${i18next.t('figure.button.fix-position')}
175
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}
176
248
  </div>`
177
249
  )
178
250
  }
@@ -189,7 +261,7 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
189
261
  <md-icon>link_off</md-icon>${i18next.t('figure.button.detach')}
190
262
  </button>
191
263
  </div>`
192
- : html`<p hint>${i18next.t('figure.text.fastening-explained')}</p>`}
264
+ : nothing}
193
265
  <div form>
194
266
  <span>${i18next.t('figure.label.my-face')}</span>
195
267
  <select @change=${(e: Event) => (this.fasten = { ...f, face: (e.target as HTMLSelectElement).value as V3Face })}>
@@ -257,7 +329,6 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
257
329
  const m = this.motion
258
330
  const unit = m.kind === 'turn' ? 'deg' : 'mm'
259
331
  return html`
260
- <p hint>${i18next.t('figure.text.motion-explained')}</p>
261
332
  <div pair>
262
333
  <select @change=${(e: Event) => this.setKind((e.target as HTMLSelectElement).value as 'slide' | 'turn')}>
263
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: grid;
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
- label[toggle] {
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(3, 1fr);
81
+ grid-template-columns: repeat(4, 1fr);
93
82
  gap: 4px;
94
83
  }
95
- div[segments] button {
96
- display: grid;
97
- padding: 4px;
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
- background: var(--md-sys-color-surface);
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[segments] button[on] {
106
- background: var(--md-sys-color-secondary-container);
107
- color: var(--md-sys-color-on-secondary-container);
108
- border-color: var(--md-sys-color-primary);
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.put({ emissive: intensity > 0 ? { token: material.emissive?.token ?? material.token, intensity, on: true } : undefined })
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
- private put(patch: Partial<V3Material>) {
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
- this.edit({ kind: 'set-material', target: this.body!.id, material, ...(Object.keys(palette).length ? { palette } : {}) })
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
+ }