@things-factory/figure-ui 10.1.162 → 10.1.164

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 (35) hide show
  1. package/client/modeller/data-interface.ts +334 -79
  2. package/client/modeller/editor.ts +4 -3
  3. package/client/modeller/figure-data-panel.ts +683 -193
  4. package/client/modeller/figure-preview.ts +46 -30
  5. package/client/modeller/refusals.ts +18 -14
  6. package/client/viewparts/figure-detail-dock.ts +9 -3
  7. package/dist-client/modeller/data-interface.d.ts +91 -30
  8. package/dist-client/modeller/data-interface.js +314 -71
  9. package/dist-client/modeller/data-interface.js.map +1 -1
  10. package/dist-client/modeller/editor.d.ts +1 -1
  11. package/dist-client/modeller/editor.js +3 -2
  12. package/dist-client/modeller/editor.js.map +1 -1
  13. package/dist-client/modeller/figure-data-panel.d.ts +42 -13
  14. package/dist-client/modeller/figure-data-panel.js +679 -186
  15. package/dist-client/modeller/figure-data-panel.js.map +1 -1
  16. package/dist-client/modeller/figure-preview.d.ts +7 -5
  17. package/dist-client/modeller/figure-preview.js +47 -30
  18. package/dist-client/modeller/figure-preview.js.map +1 -1
  19. package/dist-client/modeller/refusals.d.ts +2 -2
  20. package/dist-client/modeller/refusals.js +24 -18
  21. package/dist-client/modeller/refusals.js.map +1 -1
  22. package/dist-client/tsconfig.tsbuildinfo +1 -1
  23. package/dist-client/viewparts/figure-detail-dock.js +9 -3
  24. package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
  25. package/package.json +7 -7
  26. package/test/browser/build.mjs +9 -0
  27. package/test/browser/detail-tabs-page.js +36 -0
  28. package/test/browser/detail-tabs.check.mjs +123 -0
  29. package/test/data-motion.test.ts +173 -53
  30. package/translations/en.json +122 -43
  31. package/translations/es.json +122 -43
  32. package/translations/ja.json +122 -43
  33. package/translations/ko.json +122 -43
  34. package/translations/ms.json +122 -43
  35. package/translations/zh.json +122 -43
@@ -3,12 +3,12 @@ import { customElement, property, query, state } from 'lit/decorators.js'
3
3
 
4
4
  import { i18next, localize } from '@operato/i18n'
5
5
  import { compileFigureAsset } from '@hatiolab/figure-model'
6
- import type { FigureAsset, FigureDataProperty, FigureDataValue } from '@hatiolab/figure-model'
6
+ import type { FigureAsset, FigureDataRow, FigureDataValue } from '@hatiolab/figure-model'
7
7
  import { create, registerFigure, unregisterFigure } from '@hatiolab/things-scene'
8
8
 
9
9
  import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH, type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js'
10
10
  import { dialShape } from './angle-dial.js'
11
- import { domainOf, motionWrittenByData, sampleValueOf } from './data-interface.js'
11
+ import { domainOf, motionWrittenByData, typedValueOf, valueRowsOf } from './data-interface.js'
12
12
  import { derivedOf, type FigureRevision } from './revision.js'
13
13
  import { authoredSizeOf, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewTypeOf, setupPartOf } from '@things-factory/figure-viewer/dist-client/preview-model.js'
14
14
 
@@ -209,10 +209,10 @@ export class FigurePreview extends localize(i18next)(LitElement) {
209
209
  @state() private giving: Record<string, number> = {}
210
210
  /** Per motion group, the clip played here and how fast (ADR-0051 §3.2) -- the preview's, as a board's \`motion\`. */
211
211
  @state() private moving: Record<string, { clip: string; speed: number }> = {}
212
- /** Data values given here, as a board's saved `figureValues`: what moves a group whose clip or speed the data writes. */
212
+ /** Data values given here by accessor, as a board's saved `figureValues`: what moves a group whose clip or speed the data writes. */
213
213
  @state() private valuing: Record<string, FigureDataValue> = {}
214
- /** Data values tried in the data tab: the preview's figures run on them as a live snapshot; null when none are. */
215
- @property({ attribute: false }) sample: Record<string, unknown> | null = null
214
+ /** Data tried in the data tab: the preview's figures take it as their \`data\`, as a board's data source gives it; null when none is. */
215
+ @property({ attribute: false }) sample: unknown = null
216
216
  /** The part chosen in the editor. The size help starts on it, so the help speaks of the part the author was working on. */
217
217
  @property() selected = ''
218
218
  @state() private setupPart = ''
@@ -264,7 +264,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
264
264
  </select>
265
265
  <button @click=${() => this.dispatchEvent(new CustomEvent('configure-size', { detail: { part: setupPart }, bubbles: true, composed: true }))}>${i18next.t('figure.button.configure-size')}</button>
266
266
  </div>` : nothing}
267
- ${controls.length + clips.length + (asset?.motionGroups?.length ?? 0) + (asset?.dataInterface?.properties.length ?? 0) ? this.renderDrivers(asset!, controls, clips) : nothing}</div>`}`
267
+ ${controls.length + clips.length + (asset?.motionGroups?.length ?? 0) + (asset ? valueRowsOf(asset).length : 0) ? this.renderDrivers(asset!, controls, clips) : nothing}</div>`}`
268
268
  }
269
269
 
270
270
  private renderDrivers(asset: FigureAsset, controls: FigureAsset['document']['model']['inputs'], clips: string[]) {
@@ -306,7 +306,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
306
306
  <output>${value.toFixed(2)}</output>
307
307
  </label>`
308
308
  })}
309
- ${(asset.dataInterface?.properties ?? []).map(property => this.renderDataValue(property))}
309
+ ${valueRowsOf(asset).map(row => this.renderDataValue(row))}
310
310
  ${(asset.motionGroups ?? []).map(group => {
311
311
  const playing = this.moving[group.id] ?? { clip: group.defaultClip, speed: 0 }
312
312
  /* A field the data writes wins over the group's own setting, here as on a board: it is moved by the data values above. */
@@ -331,28 +331,44 @@ export class FigurePreview extends localize(i18next)(LitElement) {
331
331
  </div>`
332
332
  }
333
333
 
334
- /** A data property's value, as the board's data values give it: a number on a slider, a boolean or an enum chosen. */
335
- private renderDataValue(property: FigureDataProperty) {
336
- const value = this.valuing[property.id] ?? property.default
337
- if (property.kind === 'number') {
338
- const step = Math.max((property.max - property.min) / 100, 0.01)
334
+ /** A value the data's bindings read, as the board's saved values give it: a number on a slider when it has a range, a choice when it is closed. */
335
+ private renderDataValue(row: FigureDataRow) {
336
+ const node = row.node
337
+ const label = row.label ?? row.path
338
+ const fallback = 'default' in node ? node.default : undefined
339
+ const value = this.valuing[row.path] ?? fallback
340
+ if (node.kind === 'number') {
341
+ const ranged = node.min !== undefined && node.max !== undefined && node.max > node.min
339
342
  return html`<label>
340
- <span>${property.label} <span drive>${property.unit}</span></span>
343
+ <span>${label} <span drive>${node.unit ?? ''}</span></span>
341
344
  <span></span>
342
- <input type="range" min=${property.min} max=${property.max} step=${step} .value=${String(value)} @input=${(e: Event) => this.value(property, (e.target as HTMLInputElement).value)} />
343
- <output>${Number(value).toFixed(2)}</output>
345
+ ${ranged
346
+ ? html`<input type="range" min=${node.min!} max=${node.max!} step=${Math.max((node.max! - node.min!) / 100, 0.01)} .value=${String(value ?? node.min)} @input=${(e: Event) => this.value(row, (e.target as HTMLInputElement).value)} />
347
+ <output>${value === undefined ? '' : Number(value).toFixed(2)}</output>`
348
+ : html`<input type="number" step="any" .value=${value === undefined ? '' : String(value)} @change=${(e: Event) => this.value(row, (e.target as HTMLInputElement).value)} />
349
+ <span></span>`}
344
350
  </label>`
345
351
  }
352
+ const domain = domainOf(node)
353
+ if (!domain.length)
354
+ return html`<label>
355
+ <span>${label}</span>
356
+ <input aria-label=${label} .value=${value === undefined ? '' : String(value)} @change=${(e: Event) => this.value(row, (e.target as HTMLInputElement).value)} />
357
+ </label>`
346
358
  return html`<label>
347
- <span>${property.label}</span>
348
- <select aria-label=${property.label} @change=${(e: Event) => this.value(property, (e.target as HTMLSelectElement).value)}>
349
- ${domainOf(property).map(v => html`<option value=${String(v)} ?selected=${v === value}>${typeof v === 'boolean' ? i18next.t(v ? 'figure.label.data-true' : 'figure.label.data-false') : v}</option>`)}
359
+ <span>${label}</span>
360
+ <select aria-label=${label} @change=${(e: Event) => this.value(row, (e.target as HTMLSelectElement).value)}>
361
+ ${domain.map(v => html`<option value=${String(v)} ?selected=${v === value}>${typeof v === 'boolean' ? i18next.t(v ? 'figure.label.data-true' : 'figure.label.data-false') : v}</option>`)}
350
362
  </select>
351
363
  </label>`
352
364
  }
353
365
 
354
- private value(property: FigureDataProperty, typed: string) {
355
- this.valuing = { ...this.valuing, [property.id]: sampleValueOf(property, typed) }
366
+ private value(row: FigureDataRow, typed: string) {
367
+ const value = typedValueOf(row.node, typed)
368
+ const next = { ...this.valuing }
369
+ if (typeof value === 'number' && Number.isNaN(value)) delete next[row.path]
370
+ else next[row.path] = value
371
+ this.valuing = next
356
372
  this.tellInstances('figureValues', this.valuing)
357
373
  }
358
374
 
@@ -387,23 +403,23 @@ export class FigurePreview extends localize(i18next)(LitElement) {
387
403
  this.giving = {}
388
404
  }
389
405
 
390
- private tellInstances(key: 'clips' | 'parameters' | 'motion' | 'figureData' | 'figureValues', values: Record<string, unknown> | null) {
391
- for (const component of this.scene?.root?.components ?? []) {
392
- const value = values === null ? null : { ...values }
393
- /* A live snapshot is transient on the figure (its \`figureData\` accessor), never written into the saved model. */
394
- if (key === 'figureData') (component as { figureData?: unknown }).figureData = value
395
- else component.set?.(key, value)
396
- }
406
+ private tellInstances(key: 'clips' | 'parameters' | 'motion' | 'figureValues', values: Record<string, unknown> | null) {
407
+ for (const component of this.scene?.root?.components ?? []) component.set?.(key, values === null ? null : { ...values })
408
+ }
409
+
410
+ /** The tried data to the figures standing now, as their \`data\`: the preview's board is not saved. */
411
+ private tellData(data: unknown) {
412
+ for (const component of this.scene?.root?.components ?? []) component.set?.('data', data === null || data === undefined ? null : structuredClone(data))
397
413
  }
398
414
 
399
415
 
400
- /** The data sample to the figures standing now: a live snapshot, transient on each, never saved with the board. */
416
+ /** The data sample to the figures standing now. */
401
417
  private tellSample() {
402
- if (this.sample) this.tellInstances('figureData', { ...this.sample })
418
+ if (this.sample !== null && this.sample !== undefined) this.tellData(this.sample)
403
419
  }
404
420
 
405
421
  willUpdate(changed: Map<string, unknown>) {
406
- if (changed.has('sample') && this.scene) this.tellInstances('figureData', this.sample ? { ...this.sample } : null)
422
+ if (changed.has('sample') && this.scene) this.tellData(this.sample)
407
423
  if (!this.hasUpdated) return
408
424
  if (!this.active) {
409
425
  this.stale = true
@@ -243,25 +243,27 @@ export function editRefusalOf(error: unknown, say: Say): string {
243
243
  }
244
244
 
245
245
  /**
246
- * A data property, a connection, a motion group or a clip's tracks refused (ADR-0051 §2.2, §3.2), said with the ids and
247
- * numbers the kernel gave. Nothing for any other refusal.
246
+ * A data field, a binding, a status port, a motion group or a clip's tracks refused (ADR-0051 §8, §3.2), said with the ids
247
+ * and numbers the kernel gave. Nothing for any other refusal.
248
248
  */
249
249
  export function dataMotionRefusalOf(error: unknown, say: Say): string | null {
250
250
  const invalidId = detailsOf(error, 'data-id-invalid')
251
251
  if (invalidId) return say('figure.text.data-id-invalid', { id: invalidId.id })
252
252
  const twice = detailsOf(error, 'data-id-duplicate')
253
253
  if (twice) return say('figure.text.data-id-duplicate', { id: twice.id })
254
- const property = detailsOf(error, 'data-property-invalid')
255
- if (property) return say('figure.text.data-property-invalid-' + property.field, { property: property.property })
256
- const absent = detailsOf(error, 'data-property-absent')
257
- if (absent) return say('figure.text.data-property-absent', { property: absent.property })
258
- const target = detailsOf(error, 'data-target-absent')
259
- if (target) return say('figure.text.data-target-absent-' + target.target, { id: target.id })
260
- const writers = detailsOf(error, 'data-target-duplicate')
261
- if (writers) return say('figure.text.data-target-duplicate', { a: writers.connections[0], b: writers.connections[1], id: writers.id })
262
- const domain = detailsOf(error, 'data-transform-domain')
254
+ const field = detailsOf(error, 'data-field-invalid')
255
+ if (field) return say('figure.text.data-field-invalid-' + field.what, {})
256
+ const fieldTwice = detailsOf(error, 'data-field-duplicate')
257
+ if (fieldTwice) return say('figure.text.data-field-duplicate', { name: fieldTwice.name })
258
+ const accessor = detailsOf(error, 'data-accessor-invalid')
259
+ if (accessor) return say('figure.text.data-accessor-invalid-' + accessor.why, { accessor: accessor.accessor })
260
+ const behavior = detailsOf(error, 'data-behavior-absent')
261
+ if (behavior) return say('figure.text.data-behavior-absent-' + behavior.behavior, { id: behavior.id })
262
+ const writers = detailsOf(error, 'data-behavior-duplicate')
263
+ if (writers) return say('figure.text.data-behavior-duplicate', { a: writers.bindings[0], b: writers.bindings[1], id: writers.id })
264
+ const domain = detailsOf(error, 'data-rule-domain')
263
265
  if (domain)
264
- return say('figure.text.data-transform-' + domain.why, {
266
+ return say('figure.text.data-rule-' + domain.why, {
265
267
  from: domain.from ?? '',
266
268
  to: domain.to ?? '',
267
269
  min: domain.min === undefined ? '' : numberOf(domain.min),
@@ -270,8 +272,10 @@ export function dataMotionRefusalOf(error: unknown, say: Say): string | null {
270
272
  high: domain.within ? numberOf(domain.within[1]) : '',
271
273
  value: domain.value === undefined ? '' : String(domain.value)
272
274
  })
273
- const connected = detailsOf(error, 'data-property-connected')
274
- if (connected) return say('figure.text.data-property-connected', { property: connected.property, connections: connected.connections.join(', ') })
275
+ const port = detailsOf(error, 'data-port-invalid')
276
+ if (port) return say('figure.text.data-port-invalid-' + port.what, { port: port.port, id: port.id ?? '' })
277
+ const bound = detailsOf(error, 'data-port-bound')
278
+ if (bound) return say('figure.text.data-port-bound', { port: bound.port, bindings: bound.bindings.join(', ') })
275
279
  const clipAbsent = detailsOf(error, 'motion-clip-absent')
276
280
  if (clipAbsent) return say('figure.text.motion-clip-absent', { clip: clipAbsent.clip, group: clipAbsent.group })
277
281
  const shared = detailsOf(error, 'motion-clip-shared')
@@ -46,7 +46,13 @@ export class FigureDetailDock extends localize(i18next)(LitElement) {
46
46
  button[icon]:hover:not(:disabled) { background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); }
47
47
  button[icon]:disabled { opacity: 0.38; cursor: default; }
48
48
  button[icon] md-icon { --md-icon-size: 18px; }
49
- [body] { flex: 1; overflow: auto; min-height: 0; }
49
+ /*
50
+ The figure stays in view under what it is (which model, how many fit) and its tabs; only the tab's body scrolls
51
+ (user 2026-10-15: a clip played at the bottom of a scrolled dock moved a figure no longer seen).
52
+ */
53
+ [body] { flex: 1; display: flex; flex-direction: column; min-height: 0; }
54
+ [body] > * { flex: none; }
55
+ [body] > figure-asset-viewer { flex: 1; }
50
56
  [meta] { display: grid; gap: 8px; padding: 0 16px 12px; }
51
57
  /* Which model is shown: words that change colour, not boxes. */
52
58
  [source] { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; color: var(--md-sys-color-on-surface-variant); }
@@ -60,7 +66,7 @@ export class FigureDetailDock extends localize(i18next)(LitElement) {
60
66
  dl[stats] > div { display: flex; align-items: baseline; gap: 4px; }
61
67
  dl[stats] dd { font-weight: 600; font-variant-numeric: tabular-nums; }
62
68
  p { margin: 0; line-height: 1.5; }
63
- [description] { color: var(--md-sys-color-on-surface-variant); }
69
+ [description] { padding: 10px 16px 0; color: var(--md-sys-color-on-surface-variant); }
64
70
  [status] { display: grid; gap: 8px; justify-items: start; padding: 12px 16px; }
65
71
  @media (max-width: 1439.98px) {
66
72
  :host { position: fixed; inset: var(--figure-detail-top, 44px) 0 0 auto; height: calc(100dvh - var(--figure-detail-top, 44px)); z-index: 11; box-shadow: -4px 0 16px rgb(0 0 0 / 20%); }
@@ -199,10 +205,10 @@ export class FigureDetailDock extends localize(i18next)(LitElement) {
199
205
  <div><dt>${i18next.t('figure.label.budget-groups')}</dt><dd>${cost?.groups ?? '—'}</dd></div>
200
206
  <div><dt>${i18next.t('figure.label.budget-parts')}</dt><dd>${this.asset.appearance.length}</dd></div>
201
207
  </dl>` : nothing}
202
- ${saved.description ? html`<p description>${saved.description}</p>` : nothing}
203
208
  </section>` : nothing}
204
209
  ${this.asset ? html`<figure-asset-viewer .asset=${this.asset}
205
210
  .keepShot=${saved?.id ? (dataUrl: string) => this.keepThumbnail(saved.id, dataUrl) : undefined}>
211
+ ${saved?.description ? html`<p slot="facts" description>${saved.description}</p>` : nothing}
206
212
  ${saved ? html`<figure-facts-list slot="facts" .asset=${this.asset} .row=${saved}></figure-facts-list>` : nothing}
207
213
  </figure-asset-viewer>` : nothing}
208
214
  </div>`
@@ -1,51 +1,112 @@
1
- import type { FigureAsset, FigureDataConnection, FigureDataProperty, FigureDataResult, FigureDataTarget, FigureDataValue, FigureDataValueIssue } from '@hatiolab/figure-model';
2
- /** Something a connection can write, with what the screen shows of it. */
3
- export interface DataTargetChoice {
4
- /** One string per target, for a select's value. */
1
+ import type { FigureAsset, FigureDataBehavior, FigureDataBinding, FigureDataField, FigureDataNode, FigureDataRow, FigureDataValue, FigureDataValueIssue, FigureProposalStep, FigureStatusPort } from '@hatiolab/figure-model';
2
+ import { type FigureDataResult } from '@hatiolab/things-scene';
3
+ /** Something a binding can move, with what the screen shows of it. */
4
+ export interface BehaviorChoice {
5
+ /** One string per behavior, for a select's value. */
5
6
  key: string;
6
- target: FigureDataTarget;
7
- /** The control's label, the colour token, or the group's label. */
7
+ behavior: FigureDataBehavior;
8
+ /** The screen's word for the kind: a control, a colour, a group's clip, … */
9
+ kind: FigureDataBehavior['kind'];
10
+ /** The control's label, the colour token, the group's label, the port's label, the axis. */
8
11
  name: string;
9
12
  unit?: string;
10
13
  min?: number;
11
14
  max?: number;
12
15
  }
13
- export declare const targetKeyOf: (t: FigureDataTarget) => string;
16
+ export declare const behaviorKeyOf: (b: FigureDataBehavior) => string;
14
17
  /**
15
- * What a property of this kind can be connected to: a number moves a control or a group's speed; an enum or a boolean
16
- * moves a control, a colour or which clip of a group plays (through a case per value). A control is offered observed: a
17
- * value read from the machine applies as it comes.
18
+ * Everything the figure can do that data may drive: each control, each colour, each motion group's clip, speed and
19
+ * pause, each status port, its position and heading, and -- when it has places -- the items in them.
18
20
  */
19
- export declare function dataTargetsOf(asset: FigureAsset, kind: FigureDataProperty['kind']): DataTargetChoice[];
20
- /** The values a lookup has a case for: an enum's values, or both booleans. Nothing for a number. */
21
- export declare const domainOf: (property: FigureDataProperty) => FigureDataValue[];
21
+ export declare function behaviorsOf(asset: FigureAsset): BehaviorChoice[];
22
+ /** Whether a field of this kind can drive the behavior, by some rule -- the choices offered for it. */
23
+ export declare function fitsField(node: FigureDataNode, choice: BehaviorChoice): boolean;
22
24
  /** An id not taken among `taken`: `base`, then `base-2`, `base-3`, … made into the kernel's id shape. */
23
- export declare function freeDataIdOf(taken: Iterable<string>, base: string): string;
25
+ export declare function freeIdOf(taken: Iterable<string>, base: string): string;
26
+ /** The values a closed field takes: a closed text's values, both booleans. Empty when open. */
27
+ export declare const domainOf: (node: FigureDataNode) => FigureDataValue[];
24
28
  /**
25
- * A first connection of a property to a target, for the author to adjust: a number as it is in the control's own unit,
26
- * or scaled from another; an enum or a boolean case by case, each case at what the target shows now -- the control's
27
- * rest value, the colour, the group's first clip. The kernel says whether it holds.
29
+ * A first binding of a field to a behavior, for the author to adjust: the value as it is when it already fits, a scale
30
+ * when only its unit or range differs, one result per value (a map) for a closed field, a default for an open one.
31
+ * Each result starts at what the behavior shows now -- the control's rest value, the colour, the group's default clip --
32
+ * or, for a clip, the clip of the value's own name. The kernel says whether it holds.
28
33
  */
29
- export declare function connectionDraftOf(asset: FigureAsset, property: FigureDataProperty, choice: DataTargetChoice): FigureDataConnection;
30
- /** What a property looks like when first made: a number 0 to 1, a boolean off, an enum of two values. */
31
- export declare function propertyDraftOf(asset: FigureAsset, label: string, kind: FigureDataProperty['kind']): FigureDataProperty;
32
- /** A value typed in a sample field, as the property takes it: a number, true or false, or the enum value. */
33
- export declare function sampleValueOf(property: FigureDataProperty, typed: string): FigureDataValue;
34
+ export declare function bindingDraftOf(asset: FigureAsset, accessor: string, node: FigureDataNode, choice: BehaviorChoice): FigureDataBinding;
35
+ /** A row's path (`L[].F`) as the accessor a value binding reads (`L[0].F`): the first element of each list. */
36
+ export declare const accessorOfRow: (path: string) => string;
37
+ /** The field at a row's path, or undefined. */
38
+ export declare function fieldAt(schema: readonly FigureDataField[], path: string): FigureDataField | undefined;
39
+ /** The schema with a field added under a path ('' for the top): a copy, the given one left as it is. */
40
+ export declare function schemaWithField(schema: readonly FigureDataField[], under: string, field: FigureDataField): FigureDataField[];
41
+ /** The schema with the field at a path replaced (its name kept unless the new one says another). */
42
+ export declare function schemaWithFieldReplaced(schema: readonly FigureDataField[], path: string, field: FigureDataField): FigureDataField[];
43
+ /** The schema without the field at a path. */
44
+ export declare function schemaWithoutField(schema: readonly FigureDataField[], path: string): FigureDataField[];
45
+ /** A field of another kind, its name, label and json flag kept, the rest started fresh. */
46
+ export declare function fieldOfKind(field: FigureDataField, kind: FigureDataNode['kind']): FigureDataField;
47
+ /** The rows of the schema, each with the bindings that read it (figure-model's view). */
48
+ export declare const schemaRowsOf: (asset: FigureAsset) => FigureDataRow[];
34
49
  /**
35
- * What a sample of values would move, as the figure would run it: the kernel's check and evaluation of the interface.
36
- * Refused whole when one value cannot be taken, with each one's reason, as a live snapshot is.
50
+ * What a sample of data would do on this figure, read the one way a placed figure reads it (things-scene's figure
51
+ * inputs): what it moves, or why it is refused, each value's reason. Nothing here reads data itself.
37
52
  */
38
- export declare function dataSampleOf(asset: FigureAsset, values: Record<string, FigureDataValue>): {
39
- result: FigureDataResult;
40
- } | {
53
+ export declare function sampleOf(asset: FigureAsset, sample: unknown): {
41
54
  issues: FigureDataValueIssue[];
55
+ result: FigureDataResult;
42
56
  };
57
+ /** The rows a person gives a value by hand, as a board's saved values: one value, read by a binding, not inside a list. */
58
+ export declare const valueRowsOf: (asset: FigureAsset) => FigureDataRow[];
59
+ /** A value typed or chosen for a row, as its kind reads it: a number (NaN when empty), true or false, or the text. */
60
+ export declare function typedValueOf(node: FigureDataNode, typed: string): FigureDataValue;
61
+ /** The parts a status port may show on: the parts the figure's appearance names. */
62
+ export declare const partsOf: (asset: FigureAsset) => string[];
63
+ /** A first status port: the label as given, on the first colour token, no part yet. The kernel says whether it holds. */
64
+ export declare function portDraftOf(asset: FigureAsset, label: string): FigureStatusPort;
65
+ /** Whether a text is an accessor the kernel takes. */
66
+ export declare const accessorTaken: (accessor: string) => boolean;
43
67
  /**
44
- * Which fields of each motion group the data interface writes: its clip (a lookup to `motion-clip`), its speed
45
- * (`motion-speed`). The data always writes them -- from a live value, a saved one, or the property's default -- and
46
- * wins over a group's own setting for that field (detail design §5.1), so a control for such a field moves nothing.
68
+ * Which fields of each motion group the data binds: its clip, its speed. A field bound wins over the group's own setting
69
+ * whenever the data, a saved value or the schema's default gives it a value, so the preview drives it through the data.
47
70
  */
48
71
  export declare function motionWrittenByData(asset: FigureAsset): Record<string, {
49
72
  clip: boolean;
50
73
  speed: boolean;
51
74
  }>;
75
+ /** A joint that turns without end (a roller, a fan, a turntable): its control, by the id the clips write. */
76
+ export interface EndlessJoint {
77
+ input: string;
78
+ label: string;
79
+ unit: 'deg' | 'rad';
80
+ }
81
+ /** The joints that turn without end: revolute, with no limits, leading no one's mimic. */
82
+ export declare function endlessJointsOf(asset: FigureAsset): EndlessJoint[];
83
+ /** What the rotation card is told: the joint, and the data fields that turn it on, set its speed and its direction. */
84
+ export interface RotationChoice {
85
+ input: string;
86
+ /** The motion group's name, shown to a person. */
87
+ label: string;
88
+ /** A boolean field: true turns, false stops where it is. */
89
+ on?: string;
90
+ /** A number field in turns per minute, and the most it gives; the field's range is set to 0..max when it has none. */
91
+ speed?: {
92
+ path: string;
93
+ max: number;
94
+ };
95
+ /** A closed text field, and its value that turns forward and the one that turns back. */
96
+ direction?: {
97
+ path: string;
98
+ forward: string;
99
+ reverse: string;
100
+ };
101
+ }
102
+ /**
103
+ * The rotation card's commands (data binding design decision 1, 「회전」): a clip that turns the joint once around and
104
+ * keeps adding the turn (wrap, accumulating), a reverse one when a direction is given, the motion group holding them,
105
+ * and the bindings -- on to the group's pause, speed to its speed, direction to its clip. Nothing new runs them: they
106
+ * are the drivers, the group and the bindings any author could write, and stay editable one by one.
107
+ *
108
+ * The clip lasts `T = 3 × 60 / max rpm` seconds, so the most the field gives plays at the group's top speed, 3 -- the
109
+ * speed range is not widened. One turn per `T` at speed 1, and speed = rpm × T / 60. A stop is a pause: the joint stays
110
+ * where it is, where a stopped clip would jump back to its first key.
111
+ */
112
+ export declare function rotationStepsOf(asset: FigureAsset, c: RotationChoice): FigureProposalStep[];