@things-factory/figure-ui 10.1.222 → 10.1.224

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 (59) hide show
  1. package/client/modeller/canvas.ts +2 -2
  2. package/client/modeller/consumer-capabilities.ts +66 -0
  3. package/client/modeller/data-interface.ts +230 -80
  4. package/client/modeller/editor.ts +14 -7
  5. package/client/modeller/figure-capability-panel.ts +145 -1
  6. package/client/modeller/figure-data-panel.ts +495 -188
  7. package/client/modeller/figure-links.ts +130 -12
  8. package/client/modeller/figure-preview.ts +19 -2
  9. package/client/modeller/figure-slot-layout-panel.ts +7 -3
  10. package/client/modeller/links.ts +58 -0
  11. package/client/modeller/refusals.ts +12 -4
  12. package/client/modeller/slot-layout-host.ts +76 -5
  13. package/dist-client/modeller/canvas.js +2 -2
  14. package/dist-client/modeller/canvas.js.map +1 -1
  15. package/dist-client/modeller/consumer-capabilities.d.ts +29 -0
  16. package/dist-client/modeller/consumer-capabilities.js +60 -0
  17. package/dist-client/modeller/consumer-capabilities.js.map +1 -0
  18. package/dist-client/modeller/data-interface.d.ts +83 -17
  19. package/dist-client/modeller/data-interface.js +194 -82
  20. package/dist-client/modeller/data-interface.js.map +1 -1
  21. package/dist-client/modeller/editor.d.ts +5 -2
  22. package/dist-client/modeller/editor.js +16 -5
  23. package/dist-client/modeller/editor.js.map +1 -1
  24. package/dist-client/modeller/figure-capability-panel.d.ts +12 -0
  25. package/dist-client/modeller/figure-capability-panel.js +123 -1
  26. package/dist-client/modeller/figure-capability-panel.js.map +1 -1
  27. package/dist-client/modeller/figure-data-panel.d.ts +57 -13
  28. package/dist-client/modeller/figure-data-panel.js +461 -180
  29. package/dist-client/modeller/figure-data-panel.js.map +1 -1
  30. package/dist-client/modeller/figure-links.d.ts +11 -0
  31. package/dist-client/modeller/figure-links.js +137 -16
  32. package/dist-client/modeller/figure-links.js.map +1 -1
  33. package/dist-client/modeller/figure-preview.d.ts +4 -0
  34. package/dist-client/modeller/figure-preview.js +19 -1
  35. package/dist-client/modeller/figure-preview.js.map +1 -1
  36. package/dist-client/modeller/figure-slot-layout-panel.js +6 -4
  37. package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
  38. package/dist-client/modeller/links.d.ts +22 -0
  39. package/dist-client/modeller/links.js +48 -0
  40. package/dist-client/modeller/links.js.map +1 -1
  41. package/dist-client/modeller/refusals.js +14 -6
  42. package/dist-client/modeller/refusals.js.map +1 -1
  43. package/dist-client/modeller/slot-layout-host.d.ts +28 -3
  44. package/dist-client/modeller/slot-layout-host.js +68 -6
  45. package/dist-client/modeller/slot-layout-host.js.map +1 -1
  46. package/dist-client/tsconfig.tsbuildinfo +1 -1
  47. package/package.json +9 -9
  48. package/test/browser/data-table-page.js +9 -0
  49. package/test/browser/links.check.mjs +25 -3
  50. package/test/consumer-capabilities.test.ts +116 -0
  51. package/test/data-motion.test.ts +94 -27
  52. package/test/links.test.ts +15 -0
  53. package/test/slot-layout-host.test.ts +63 -5
  54. package/translations/en.json +81 -23
  55. package/translations/es.json +81 -23
  56. package/translations/ja.json +81 -23
  57. package/translations/ko.json +81 -23
  58. package/translations/ms.json +81 -23
  59. package/translations/zh.json +81 -23
@@ -8,6 +8,20 @@ import type { FigureAsset } from '@hatiolab/figure-model'
8
8
  import { RUNGS, rungOf, type Rung } from '@things-factory/figure-viewer/dist-client/capability-rung.js'
9
9
  import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
10
10
  import { placeTerms } from '@things-factory/figure-viewer/dist-client/detail-facts.js'
11
+ import { onFigureCapabilitiesChanged, type FigureCapabilityDescriptor, type FigureCapabilitySetting } from '@hatiolab/things-scene'
12
+
13
+ import {
14
+ consumerDescriptors,
15
+ inputTableAt,
16
+ namesWith,
17
+ needsMissing,
18
+ partsAt,
19
+ settingsIssues,
20
+ settingsOf,
21
+ unknownConsumerNames,
22
+ withInputTable,
23
+ withParts
24
+ } from './consumer-capabilities.js'
11
25
 
12
26
  /**
13
27
  * 자리·능력 — what the figure does on a board: the ladder from drawing only, to
@@ -15,6 +29,10 @@ import { placeTerms } from '@things-factory/figure-viewer/dist-client/detail-fac
15
29
  * so the list is the only way to find one; choosing a row selects its part, where the place is edited. Whether the
16
30
  * figure is the look of an item a holder carries is said here too (ADR-0155 amendment 2): a tag, apart from the ladder,
17
31
  * since an item may hold things as well.
32
+ *
33
+ * Below the ladder, the products' capabilities this modeller has descriptors for (ADR-0167 9, 12): each put on or off,
34
+ * what it still needs, and its settings as its descriptor draws them -- parts picked (the part chosen here by one
35
+ * button), an input and a value per fixed key, never a row added or taken away -- with what its check says.
18
36
  */
19
37
  @customElement('figure-capability-panel')
20
38
  export class FigureCapabilityPanel extends localize(i18next)(LitElement) {
@@ -70,8 +88,42 @@ export class FigureCapabilityPanel extends localize(i18next)(LitElement) {
70
88
  font-size: var(--ui-t-small, 11.5px);
71
89
  color: var(--md-sys-color-on-surface-variant);
72
90
  }
91
+ div[chips] {
92
+ display: flex;
93
+ flex-wrap: wrap;
94
+ gap: 4px 10px;
95
+ align-items: center;
96
+ }
97
+ div[chips] label {
98
+ display: inline-flex;
99
+ gap: 4px;
100
+ align-items: center;
101
+ }
102
+ div[table] {
103
+ display: grid;
104
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
105
+ gap: 4px 6px;
106
+ align-items: center;
107
+ }
108
+ p[warn] {
109
+ margin: 0;
110
+ color: var(--md-sys-color-error);
111
+ }
73
112
  `]
74
113
 
114
+ private unlisten?: () => void
115
+
116
+ connectedCallback() {
117
+ super.connectedCallback()
118
+ /* A product's descriptors may be registered after the panel stands. */
119
+ this.unlisten = onFigureCapabilitiesChanged(() => this.requestUpdate())
120
+ }
121
+
122
+ disconnectedCallback() {
123
+ super.disconnectedCallback()
124
+ this.unlisten?.()
125
+ }
126
+
75
127
  @property({ attribute: false }) asset?: FigureAsset
76
128
  /** The selected part, whose places are marked. */
77
129
  @property() selected = ''
@@ -97,6 +149,7 @@ export class FigureCapabilityPanel extends localize(i18next)(LitElement) {
97
149
  ${rung === null ? html`<p hint>${i18next.t('figure.text.capability-hand-written')}</p>` : nothing}
98
150
  ${names.length ? html`<code>${names.join(' · ')}</code>` : nothing}
99
151
  </section>
152
+ ${this.renderConsumers(asset)}
100
153
  <section>
101
154
  <h3>${i18next.t('figure.label.places')}</h3>
102
155
  ${places.length
@@ -125,8 +178,99 @@ export class FigureCapabilityPanel extends localize(i18next)(LitElement) {
125
178
  `
126
179
  }
127
180
 
181
+ /** The products' capabilities: those this modeller knows, each on or off with its settings, and those it does not know. */
182
+ private renderConsumers(asset: FigureAsset) {
183
+ const known = consumerDescriptors()
184
+ const unknown = unknownConsumerNames(asset)
185
+ if (!known.length && !unknown.length) return nothing
186
+ const names = asset.document.capabilities.names ?? []
187
+ return html`<section>
188
+ <h3>${i18next.t('figure.label.product-capabilities')}</h3>
189
+ ${known.map(d => {
190
+ const on = names.includes(d.name)
191
+ const missing = needsMissing(d, asset)
192
+ return html`<label rung ?on=${on}>
193
+ <input type="checkbox" .checked=${live(on)} ?disabled=${this.locked || (!on && missing.length > 0)} @change=${(e: Event) => this.setConsumer(asset, d.name, (e.target as HTMLInputElement).checked)} />
194
+ <span>
195
+ <strong>${d.label}</strong>
196
+ <em>${d.note}</em>
197
+ <code>${d.name} · ${d.version}</code>
198
+ </span>
199
+ </label>
200
+ ${missing.length ? html`<p hint>${i18next.t('figure.text.product-capability-needs', { needs: missing.map(m => (m === 'item' ? i18next.t('figure.label.is-item') : m)).join(', ') })}</p>` : nothing}
201
+ ${on ? this.renderSettings(asset, d) : nothing}`
202
+ })}
203
+ ${unknown.map(
204
+ name => html`<p hint>
205
+ ${i18next.t('figure.text.product-capability-unknown', { name })}
206
+ <button quiet ?disabled=${this.locked} @click=${() => this.setConsumer(asset, name, false)}>${i18next.t('figure.button.product-capability-remove')}</button>
207
+ </p>`
208
+ )}
209
+ </section>`
210
+ }
211
+
212
+ /** A capability's settings as its descriptor draws them, and what its check says of them. */
213
+ private renderSettings(asset: FigureAsset, d: FigureCapabilityDescriptor) {
214
+ const settings = settingsOf(asset, d.name)
215
+ const write = (next: Record<string, unknown>) =>
216
+ this.dispatchEvent(new CustomEvent('part-edit', { detail: { edit: { kind: 'set-capability-settings', name: d.name, settings: Object.keys(next).length ? next : null } }, bubbles: true, composed: true }))
217
+ const parts = [...new Set(asset.appearance.map(a => a.target))]
218
+ const inputs = asset.document.model.inputs.filter(i => i.role === 'state')
219
+ const issues = settingsIssues(d, asset)
220
+ return html`<div form>
221
+ ${(d.settings ?? []).map((setting: FigureCapabilitySetting) => {
222
+ if (setting.kind === 'parts') {
223
+ const chosen = partsAt(settings, setting.key)
224
+ const toggle = (part: string, on: boolean) => write(withParts(settings, setting.key, on ? [...chosen, part] : chosen.filter(p => p !== part)))
225
+ return html`<span>${setting.label}</span>
226
+ <div chips>
227
+ ${parts.map(p => html`<label><input type="checkbox" .checked=${live(chosen.includes(p))} ?disabled=${this.locked} @change=${(e: Event) => toggle(p, (e.target as HTMLInputElement).checked)} />${p}</label>`)}
228
+ ${this.selected && parts.includes(this.selected) && !chosen.includes(this.selected)
229
+ ? html`<button quiet ?disabled=${this.locked} @click=${() => toggle(this.selected, true)}>${i18next.t('figure.button.data-add-selected-part', { part: this.selected })}</button>`
230
+ : nothing}
231
+ </div>`
232
+ }
233
+ const table = inputTableAt(settings, setting)
234
+ const input = inputs.find(i => i.id === table.input)
235
+ return html`<span>${setting.label}</span>
236
+ <select ?disabled=${this.locked} @change=${(e: Event) => write(withInputTable(settings, setting, (e.target as HTMLSelectElement).value || undefined, table.values))}>
237
+ <option value="" ?selected=${!table.input}>${i18next.t('figure.label.data-none')}</option>
238
+ ${inputs.map(i => html`<option value=${i.id} ?selected=${i.id === table.input}>${asset.stateInputs?.[i.id]?.label ?? i.id}${i.unit ? ` (${i.unit} ${i.min ?? ''}~${i.max ?? ''})` : ''}</option>`)}
239
+ </select>
240
+ ${table.input
241
+ ? html`<span></span>
242
+ <div table>
243
+ ${setting.keys.map(
244
+ key => html`<span>${key}</span>
245
+ <input
246
+ type="number"
247
+ step="any"
248
+ min=${input?.min ?? ''}
249
+ max=${input?.max ?? ''}
250
+ .value=${String(table.values[key])}
251
+ ?disabled=${this.locked}
252
+ @change=${(e: Event) => {
253
+ const n = (e.target as HTMLInputElement).valueAsNumber
254
+ if (!Number.isFinite(n)) return this.requestUpdate()
255
+ write(withInputTable(settings, setting, table.input, { ...table.values, [key]: n }))
256
+ }}
257
+ />`
258
+ )}
259
+ </div>`
260
+ : nothing}`
261
+ })}
262
+ </div>
263
+ ${issues.map(reason => html`<p warn>${reason}</p>`)}`
264
+ }
265
+
266
+ private setConsumer(asset: FigureAsset, name: string, on: boolean) {
267
+ this.dispatchEvent(new CustomEvent('part-edit', { detail: { edit: { kind: 'set-capabilities', names: namesWith(asset, name, on) } }, bubbles: true, composed: true }))
268
+ }
269
+
128
270
  private setRung(which: Rung) {
129
- this.dispatchEvent(new CustomEvent('part-edit', { detail: { edit: { kind: 'set-capabilities', names: [...RUNGS[which]] } }, bubbles: true, composed: true }))
271
+ /* The ladder sets the shared names; a product's capabilities stay as they are. */
272
+ const consumers = (this.asset?.document.capabilities.names ?? []).filter(n => n.includes('.'))
273
+ this.dispatchEvent(new CustomEvent('part-edit', { detail: { edit: { kind: 'set-capabilities', names: [...RUNGS[which], ...consumers] } }, bubbles: true, composed: true }))
130
274
  }
131
275
 
132
276
  private setItem(item: boolean) {