@things-factory/figure-ui 10.1.223 → 10.1.225
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/canvas.ts +2 -2
- package/client/modeller/consumer-capabilities.ts +66 -0
- package/client/modeller/data-interface.ts +230 -80
- package/client/modeller/editor-styles.ts +3 -2
- package/client/modeller/editor.ts +17 -8
- package/client/modeller/figure-capability-panel.ts +151 -1
- package/client/modeller/figure-clips.ts +1 -1
- package/client/modeller/figure-data-panel.ts +509 -199
- package/client/modeller/figure-links.ts +130 -12
- package/client/modeller/figure-preview.ts +65 -8
- package/client/modeller/figure-slot-layout-panel.ts +7 -3
- package/client/modeller/links.ts +58 -0
- package/client/modeller/product-session.ts +3 -2
- package/client/modeller/refusals.ts +12 -4
- package/client/modeller/slot-layout-host.ts +76 -5
- package/client/pages/figure-modeller-page.ts +2 -2
- package/dist-client/modeller/canvas.js +2 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/consumer-capabilities.d.ts +29 -0
- package/dist-client/modeller/consumer-capabilities.js +60 -0
- package/dist-client/modeller/consumer-capabilities.js.map +1 -0
- package/dist-client/modeller/data-interface.d.ts +83 -17
- package/dist-client/modeller/data-interface.js +194 -82
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +3 -2
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +5 -2
- package/dist-client/modeller/editor.js +21 -8
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-capability-panel.d.ts +12 -0
- package/dist-client/modeller/figure-capability-panel.js +129 -1
- package/dist-client/modeller/figure-capability-panel.js.map +1 -1
- package/dist-client/modeller/figure-clips.js +1 -1
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-data-panel.d.ts +57 -13
- package/dist-client/modeller/figure-data-panel.js +476 -191
- package/dist-client/modeller/figure-data-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +11 -0
- package/dist-client/modeller/figure-links.js +137 -16
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +4 -0
- package/dist-client/modeller/figure-preview.js +65 -7
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js +6 -4
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
- package/dist-client/modeller/links.d.ts +22 -0
- package/dist-client/modeller/links.js +48 -0
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +4 -2
- package/dist-client/modeller/product-session.js +3 -2
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusals.js +14 -6
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/slot-layout-host.d.ts +28 -3
- package/dist-client/modeller/slot-layout-host.js +68 -6
- package/dist-client/modeller/slot-layout-host.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.js +2 -2
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/ai-proposal.test.ts +2 -2
- package/test/browser/data-table-page.js +9 -0
- package/test/browser/links.check.mjs +25 -3
- package/test/browser/product-page.check.mjs +25 -0
- package/test/browser/side-panel-sweep.check.mjs +182 -0
- package/test/consumer-capabilities.test.ts +116 -0
- package/test/data-motion.test.ts +94 -27
- package/test/figure-assistant-surface.test.ts +1 -1
- package/test/first-save.test.ts +2 -2
- package/test/links.test.ts +15 -0
- package/test/product-session.test.ts +5 -5
- package/test/proposal-record.test.ts +3 -3
- package/test/revert-keeping.test.ts +1 -1
- package/test/slot-layout-host.test.ts +63 -5
- package/translations/en.json +82 -23
- package/translations/es.json +82 -23
- package/translations/ja.json +82 -23
- package/translations/ko.json +82 -23
- package/translations/ms.json +82 -23
- package/translations/zh.json +82 -23
|
@@ -7,7 +7,7 @@ import type { FigureRenderPart } from './renderer.js'
|
|
|
7
7
|
import { DEFAULT_VIEW, sceneLook, WORKBENCH, type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js'
|
|
8
8
|
import { boardFor, changedKeys, componentOf, gizmoResultOf, needsRefit, type Board } from './stage-model.js'
|
|
9
9
|
import { JOINT_FAN } from './joint-fan.js'
|
|
10
|
-
import { SLOT_MARK } from './slot-layout-host.js'
|
|
10
|
+
import { SLOT_MARK, SLOT_MARK_CHOSEN } from './slot-layout-host.js'
|
|
11
11
|
import { PREVIEW, TARGET_FACE } from './command-preview.js'
|
|
12
12
|
import { ownerOf } from './fields.js'
|
|
13
13
|
|
|
@@ -256,7 +256,7 @@ export class FigureCanvas extends LitElement {
|
|
|
256
256
|
/* The fan in the accent colour, translucent; `capturable: false` keeps clicks, the marquee and the gizmo off it. */
|
|
257
257
|
const fan = this.fan ? componentOf(this.fan, { ...this.palette, [JOINT_FAN]: this.palette['palette.accent'] ?? FAN_COLOUR }, this.board!) : null
|
|
258
258
|
const accent = this.palette['palette.accent'] ?? FAN_COLOUR
|
|
259
|
-
const slots = this.marks.map(part => componentOf(part, { ...this.palette, [SLOT_MARK]: this.palette['palette.secondary'] ?? accent }, this.board!))
|
|
259
|
+
const slots = this.marks.map(part => componentOf(part, { ...this.palette, [SLOT_MARK]: this.palette['palette.secondary'] ?? accent, [SLOT_MARK_CHOSEN]: this.palette['palette.primary'] ?? accent }, this.board!))
|
|
260
260
|
const shown = [...(fan ? [fan] : []), ...slots, ...this.ghost.map(part => componentOf(part, { ...this.palette, [PREVIEW]: accent, [TARGET_FACE]: accent }, this.board!)), ...(this.highlight ? [componentOf(this.highlight, { ...this.palette, [TARGET_FACE]: accent }, this.board!)] : [])]
|
|
261
261
|
return [...parts, ...shown.filter((c): c is Record<string, unknown> => !!c).map(c => ({ ...c, capturable: false, locked: true }))]
|
|
262
262
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A product's capabilities in the modeller (ADR-0167 3, 9, 12): the descriptors registered with the scene, which ones
|
|
3
|
+
* the figure wears, what each still needs, and its settings on the asset as its descriptor draws them -- parts picked,
|
|
4
|
+
* an input and a value per fixed key. The descriptor's own check says what the settings lack. Kept apart from the
|
|
5
|
+
* element so it is tested without a browser.
|
|
6
|
+
*/
|
|
7
|
+
import { isFigureConsumerCapability } from '@hatiolab/figure-model'
|
|
8
|
+
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
9
|
+
import { figureCapabilityDescriptors, type FigureCapabilityDescriptor, type FigureCapabilitySetting } from '@hatiolab/things-scene'
|
|
10
|
+
|
|
11
|
+
/** The products' capabilities this modeller knows: registered descriptors with a namespaced name. */
|
|
12
|
+
export const consumerDescriptors = (): FigureCapabilityDescriptor[] => figureCapabilityDescriptors().filter(d => isFigureConsumerCapability(d.name))
|
|
13
|
+
|
|
14
|
+
/** The products' capabilities the figure declares that this modeller has no descriptor for: kept, said, removable. */
|
|
15
|
+
export function unknownConsumerNames(asset: FigureAsset): string[] {
|
|
16
|
+
const known = new Set(consumerDescriptors().map(d => d.name))
|
|
17
|
+
return (asset.document.capabilities.names ?? []).filter(n => isFigureConsumerCapability(n) && !known.has(n))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What a capability still needs on this figure: a shared or a product's name not declared, or the item mark. */
|
|
21
|
+
export function needsMissing(descriptor: FigureCapabilityDescriptor, asset: FigureAsset): string[] {
|
|
22
|
+
const names = new Set(asset.document.capabilities.names ?? [])
|
|
23
|
+
const has = (need: string) => (need === 'item' ? !!asset.item : names.has(need))
|
|
24
|
+
return descriptor.needs.filter(need => (Array.isArray(need) ? !need.some(has) : !has(need as string))).map(need => (Array.isArray(need) ? need.join(' | ') : (need as string)))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The names with a product's capability put on or taken off; the shared ones as they are. */
|
|
28
|
+
export function namesWith(asset: FigureAsset, name: string, on: boolean): string[] {
|
|
29
|
+
const names = asset.document.capabilities.names ?? []
|
|
30
|
+
return on ? [...new Set([...names, name])] : names.filter(n => n !== name)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A capability's settings on the asset, or an empty object. */
|
|
34
|
+
export const settingsOf = (asset: FigureAsset, name: string): Record<string, unknown> => ({ ...(asset.document.capabilities.extensions?.[name] ?? {}) })
|
|
35
|
+
|
|
36
|
+
/** The parts a parts setting holds. */
|
|
37
|
+
export function partsAt(settings: Record<string, unknown>, key: string): string[] {
|
|
38
|
+
const v = settings[key]
|
|
39
|
+
return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The settings with a parts setting changed; none is left out. */
|
|
43
|
+
export function withParts(settings: Record<string, unknown>, key: string, parts: string[]): Record<string, unknown> {
|
|
44
|
+
const next = { ...settings }
|
|
45
|
+
if (parts.length) next[key] = [...parts]
|
|
46
|
+
else delete next[key]
|
|
47
|
+
return next
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** An input table as held: the input, and a value per fixed key (the descriptor's start where none is given yet). */
|
|
51
|
+
export function inputTableAt(settings: Record<string, unknown>, setting: Extract<FigureCapabilitySetting, { kind: 'input-table' }>): { input?: string; values: Record<string, number> } {
|
|
52
|
+
const v = settings[setting.key] as { input?: unknown; values?: Record<string, unknown> } | undefined
|
|
53
|
+
const values = Object.fromEntries(setting.keys.map(k => [k, typeof v?.values?.[k] === 'number' ? (v.values[k] as number) : setting.start[k]!]))
|
|
54
|
+
return { ...(typeof v?.input === 'string' ? { input: v.input } : {}), values }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The settings with an input table changed: an input chosen writes every fixed key's value; none takes the table away. */
|
|
58
|
+
export function withInputTable(settings: Record<string, unknown>, setting: Extract<FigureCapabilitySetting, { kind: 'input-table' }>, input: string | undefined, values: Record<string, number>): Record<string, unknown> {
|
|
59
|
+
const next = { ...settings }
|
|
60
|
+
if (input) next[setting.key] = { input, values: Object.fromEntries(setting.keys.map(k => [k, values[k] ?? setting.start[k]!])) }
|
|
61
|
+
else delete next[setting.key]
|
|
62
|
+
return next
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** What the capability's own check says of the settings now; nothing when it has no check. */
|
|
66
|
+
export const settingsIssues = (descriptor: FigureCapabilityDescriptor, asset: FigureAsset): string[] => descriptor.check?.(asset.document.capabilities.extensions?.[descriptor.name], asset) ?? []
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The figure's data and what it moves (ADR-0051 §8,
|
|
3
|
-
*
|
|
2
|
+
* The figure's data and what it moves (ADR-0051 §8, ADR-0166): what the data tab offers and shows -- one binding, its
|
|
3
|
+
* method as the field's kind allows (as it is, scaled, a code table, a range table). Every rule is figure-model's -- which accessor holds, which rule's results a behavior takes -- and the reading
|
|
4
4
|
* of data is things-scene's; this only lists what there is to choose, drafts a binding the kernel then checks, and edits
|
|
5
5
|
* the schema as a tree. Kept apart from the element so it is tested without a browser.
|
|
6
6
|
*/
|
|
7
7
|
import { FIGURE_PART_ID, FIGURE_MOTION_SPEED_MAX, FigureContractError, figureAxesOf, figureDataRows, motionStateIdOf, parseFigureAccessor } from '@hatiolab/figure-model'
|
|
8
|
-
import type {
|
|
9
|
-
|
|
8
|
+
import type {
|
|
9
|
+
FigureAsset,
|
|
10
|
+
FigureDataBehavior,
|
|
11
|
+
FigureDataBinding,
|
|
12
|
+
FigureDataField,
|
|
13
|
+
FigureDataNode,
|
|
14
|
+
FigureDataRow,
|
|
15
|
+
FigureDataTableBinding,
|
|
16
|
+
FigureDataTableEntry,
|
|
17
|
+
FigureDataTargets,
|
|
18
|
+
FigureDataValue,
|
|
19
|
+
FigureDataValueBinding,
|
|
20
|
+
FigureDataValueIssue,
|
|
21
|
+
FigureProposalStep
|
|
22
|
+
} from '@hatiolab/figure-model'
|
|
23
|
+
import { FigureInputs, type FigureDataResult, type FigureRuntimeIssue, type FigureTableUse } from '@hatiolab/things-scene'
|
|
10
24
|
|
|
11
25
|
/** Something a binding can move, with what the screen shows of it. */
|
|
12
26
|
export interface BehaviorChoice {
|
|
@@ -15,11 +29,13 @@ export interface BehaviorChoice {
|
|
|
15
29
|
behavior: FigureDataBehavior
|
|
16
30
|
/** The screen's word for the kind: a control, a colour, a group's clip, … */
|
|
17
31
|
kind: FigureDataBehavior['kind']
|
|
18
|
-
/** The control's label, the colour token, the group's label, the
|
|
32
|
+
/** The control's label, the colour token, the group's label, the axis. */
|
|
19
33
|
name: string
|
|
20
34
|
unit?: string
|
|
21
35
|
min?: number
|
|
22
36
|
max?: number
|
|
37
|
+
/** A motion group's clips. */
|
|
38
|
+
clips?: string[]
|
|
23
39
|
}
|
|
24
40
|
|
|
25
41
|
export const behaviorKeyOf = (b: FigureDataBehavior): string =>
|
|
@@ -27,17 +43,15 @@ export const behaviorKeyOf = (b: FigureDataBehavior): string =>
|
|
|
27
43
|
? `state:${b.input}`
|
|
28
44
|
: b.kind === 'palette'
|
|
29
45
|
? `palette:${b.token}`
|
|
30
|
-
: b.kind === '
|
|
31
|
-
? `
|
|
32
|
-
: b.kind === '
|
|
33
|
-
? `
|
|
34
|
-
: b.kind
|
|
35
|
-
? `items:${b.slot ?? ''}`
|
|
36
|
-
: `${b.kind}:${b.group}`
|
|
46
|
+
: b.kind === 'pose'
|
|
47
|
+
? `pose:${b.axis}`
|
|
48
|
+
: b.kind === 'items'
|
|
49
|
+
? `items:${b.slot ?? ''}`
|
|
50
|
+
: `${b.kind}:${b.group}`
|
|
37
51
|
|
|
38
52
|
/**
|
|
39
|
-
* Everything
|
|
40
|
-
*
|
|
53
|
+
* Everything a value binding may drive: each control, each colour, each motion group's clip, speed and pause, its
|
|
54
|
+
* position and heading, and -- when it has places -- the items in them.
|
|
41
55
|
*/
|
|
42
56
|
export function behaviorsOf(asset: FigureAsset): BehaviorChoice[] {
|
|
43
57
|
const out: BehaviorChoice[] = []
|
|
@@ -48,11 +62,10 @@ export function behaviorsOf(asset: FigureAsset): BehaviorChoice[] {
|
|
|
48
62
|
}
|
|
49
63
|
for (const token of Object.keys(asset.palette)) add({ kind: 'palette', token }, token)
|
|
50
64
|
for (const group of asset.motionGroups ?? []) {
|
|
51
|
-
add({ kind: 'motion-clip', group: group.id }, group.label)
|
|
65
|
+
add({ kind: 'motion-clip', group: group.id }, group.label, { clips: [...group.clips] })
|
|
52
66
|
add({ kind: 'motion-speed', group: group.id }, group.label, { min: 0, max: FIGURE_MOTION_SPEED_MAX })
|
|
53
67
|
add({ kind: 'motion-paused', group: group.id }, group.label)
|
|
54
68
|
}
|
|
55
|
-
for (const port of asset.dataInterface?.ports ?? []) add({ kind: 'status', port: port.id }, port.label)
|
|
56
69
|
for (const axis of ['x', 'z', 'y', 'heading'] as const) add({ kind: 'pose', axis, seconds: 1 }, axis, { unit: axis === 'heading' ? 'deg' : 'mm' })
|
|
57
70
|
if (asset.document.capabilities.slotLayout || asset.document.capabilities.names?.includes('Holdable')) add({ kind: 'items' }, asset.document.capabilities.slotLayout?.label ?? 'items')
|
|
58
71
|
return out
|
|
@@ -60,17 +73,40 @@ export function behaviorsOf(asset: FigureAsset): BehaviorChoice[] {
|
|
|
60
73
|
|
|
61
74
|
const isValue = (n: FigureDataNode): n is Extract<FigureDataNode, { kind: 'number' | 'boolean' | 'text' }> => n.kind === 'number' || n.kind === 'boolean' || n.kind === 'text'
|
|
62
75
|
|
|
63
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Whether a field of this kind drives the behavior as it is or scaled -- the choices a value binding offers for it. A
|
|
78
|
+
* value that picks a look per code is a table (ADR-0166 2).
|
|
79
|
+
*/
|
|
64
80
|
export function fitsField(node: FigureDataNode, choice: BehaviorChoice): boolean {
|
|
65
81
|
if (choice.kind === 'items') return node.kind === 'list' || node.kind === 'text' || node.kind === 'number'
|
|
66
82
|
if (!isValue(node)) return false
|
|
67
|
-
if (choice.kind === '
|
|
68
|
-
if (choice.kind === '
|
|
69
|
-
if (choice.kind === 'motion-
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
if (choice.kind === 'motion-paused') return node.kind === 'boolean'
|
|
84
|
+
if (choice.kind === 'palette') return node.kind === 'text' && !!node.values?.every(v => /^#[0-9a-f]{6}$/i.test(v))
|
|
85
|
+
if (choice.kind === 'motion-clip') {
|
|
86
|
+
const clips = choice.behavior.kind === 'motion-clip' ? choice.clips : undefined
|
|
87
|
+
return node.kind === 'text' && !!node.values && (!clips || node.values.every(v => clips.includes(v)))
|
|
88
|
+
}
|
|
89
|
+
return node.kind === 'number'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** How a binding turns a value into what the figure shows (ADR-0166 1): the screen's four methods. */
|
|
93
|
+
export type DataMethod = 'value' | 'scale' | 'code' | 'ranges'
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The methods a field allows (ADR-0166 2): a number as it is, scaled or by a range table; a text or true/false by a code
|
|
97
|
+
* table, or as it is where a behavior takes it so; a list as it is (its items). A new field is made by a table.
|
|
98
|
+
*/
|
|
99
|
+
export function methodsOf(node: FigureDataNode | null, choices: BehaviorChoice[]): DataMethod[] {
|
|
100
|
+
if (!node) return ['code', 'ranges']
|
|
101
|
+
const fitting = choices.some(c => fitsField(node, c))
|
|
102
|
+
if (node.kind === 'number') return [...(fitting ? (['value', 'scale'] as const) : []), 'ranges']
|
|
103
|
+
if (node.kind === 'text' || node.kind === 'boolean') return [...(fitting ? (['value'] as const) : []), 'code']
|
|
104
|
+
return fitting ? ['value'] : []
|
|
72
105
|
}
|
|
73
106
|
|
|
107
|
+
/** The method a binding is shown by. */
|
|
108
|
+
export const methodOf = (b: FigureDataBinding): DataMethod => (b.rule === 'table' ? (b.lookup === 'code' ? 'code' : 'ranges') : b.rule)
|
|
109
|
+
|
|
74
110
|
/** An id not taken among `taken`: `base`, then `base-2`, `base-3`, … made into the kernel's id shape. */
|
|
75
111
|
export function freeIdOf(taken: Iterable<string>, base: string): string {
|
|
76
112
|
const used = new Set(taken)
|
|
@@ -90,19 +126,15 @@ const TO_DEG: Record<string, number> = { deg: 1, rad: 180 / Math.PI }
|
|
|
90
126
|
export const domainOf = (node: FigureDataNode): FigureDataValue[] => (node.kind === 'boolean' ? [false, true] : node.kind === 'text' && node.values ? [...node.values] : [])
|
|
91
127
|
|
|
92
128
|
/**
|
|
93
|
-
* A first binding of a field to a behavior, for the author to adjust: the value as it is when it already fits, a
|
|
94
|
-
* when only its unit or range differs
|
|
95
|
-
* Each result starts at what the behavior shows now -- the control's rest value, the colour, the group's default clip --
|
|
96
|
-
* or, for a clip, the clip of the value's own name. The kernel says whether it holds.
|
|
129
|
+
* A first value binding of a field to a behavior, for the author to adjust: the value as it is when it already fits, a
|
|
130
|
+
* scale when only its unit or range differs -- or the method the author chose. The kernel says whether it holds.
|
|
97
131
|
*/
|
|
98
|
-
export function bindingDraftOf(asset: FigureAsset, accessor: string, node: FigureDataNode, choice: BehaviorChoice):
|
|
132
|
+
export function bindingDraftOf(asset: FigureAsset, accessor: string, node: FigureDataNode, choice: BehaviorChoice, method?: 'value' | 'scale'): FigureDataValueBinding {
|
|
99
133
|
const taken = (asset.dataInterface?.bindings ?? []).map(b => b.id)
|
|
100
134
|
const name = accessor.replace(/\[\d+\]/g, '').split('.').pop() ?? accessor
|
|
101
135
|
const id = freeIdOf(taken, `${name}-${choice.kind === 'state' ? (choice.behavior as { input: string }).input : choice.kind}`)
|
|
102
136
|
const behavior = choice.behavior
|
|
103
|
-
const make = (rule:
|
|
104
|
-
const domain = domainOf(node)
|
|
105
|
-
const caseMap = (each: (v: FigureDataValue) => FigureDataValue, fallback: FigureDataValue) => Object.fromEntries([...domain.map(v => [String(v), each(v)] as const), ...(domain.length ? [] : [['default', fallback] as const])])
|
|
137
|
+
const make = (rule: 'value' | 'scale', param?: FigureDataValueBinding['param']): FigureDataValueBinding => ({ id, accessor, behavior, rule, ...(param ? { param } : {}) })
|
|
106
138
|
|
|
107
139
|
if (behavior.kind === 'items') {
|
|
108
140
|
if (node.kind !== 'list') return make('value')
|
|
@@ -114,47 +146,152 @@ export function bindingDraftOf(asset: FigureAsset, accessor: string, node: Figur
|
|
|
114
146
|
const stack = pick(/stack/i, /level/i)
|
|
115
147
|
return { id, accessor, behavior: { kind: 'items', ...(itemId ? { id: itemId } : {}), ...(type ? { type } : {}), ...(place ? { place } : {}), ...(stack ? { stack } : {}) }, rule: 'value' }
|
|
116
148
|
}
|
|
117
|
-
if (
|
|
118
|
-
if (behavior.kind === 'palette') {
|
|
119
|
-
const now = asset.palette[behavior.token] ?? '#000000'
|
|
120
|
-
if (node.kind === 'number') return make('range', { default: now })
|
|
121
|
-
return make('map', caseMap(() => now, now))
|
|
122
|
-
}
|
|
123
|
-
if (behavior.kind === 'motion-clip') {
|
|
124
|
-
const group = (asset.motionGroups ?? []).find(g => g.id === behavior.group)
|
|
125
|
-
const clips = group?.clips ?? []
|
|
126
|
-
if (node.kind === 'text' && node.values && node.values.every(v => clips.includes(v))) return make('value')
|
|
127
|
-
return make('map', caseMap(v => (clips.includes(String(v)) ? String(v) : (group?.defaultClip ?? '')), group?.defaultClip ?? ''))
|
|
128
|
-
}
|
|
129
|
-
if (behavior.kind === 'motion-paused') {
|
|
130
|
-
if (node.kind === 'boolean') return make('value')
|
|
131
|
-
return make('map', caseMap(() => false, false))
|
|
132
|
-
}
|
|
149
|
+
if (node.kind !== 'number') return make('value')
|
|
133
150
|
if (behavior.kind === 'pose') {
|
|
134
|
-
if (node.kind !== 'number') return make('value')
|
|
135
151
|
const want = behavior.axis === 'heading' ? TO_DEG : TO_MM
|
|
136
152
|
const unit = node.unit ?? ''
|
|
137
|
-
if (unit === (behavior.axis === 'heading' ? 'deg' : 'mm')) return make('value')
|
|
138
|
-
return
|
|
153
|
+
if (method === 'value' || unit === (behavior.axis === 'heading' ? 'deg' : 'mm') || (method === undefined && !(unit in want))) return make('value')
|
|
154
|
+
return make('scale', { scale: want[unit] ?? 1, offset: 0 })
|
|
139
155
|
}
|
|
140
156
|
/* state and motion-speed: a number into a range. */
|
|
141
157
|
const [lo, hi, unit] =
|
|
142
158
|
behavior.kind === 'motion-speed' ? [0, FIGURE_MOTION_SPEED_MAX, 'ratio'] : [choice.min ?? 0, choice.max ?? 1, choice.unit ?? '']
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return make('scale', { scale: round(scale), offset: round(lo - scale * node.min) })
|
|
151
|
-
}
|
|
152
|
-
return make('value')
|
|
159
|
+
const sameUnit = behavior.kind === 'motion-speed' ? node.unit === undefined || node.unit === 'ratio' : node.unit === unit
|
|
160
|
+
const inside = node.min !== undefined && node.max !== undefined && node.min >= lo && node.max <= hi
|
|
161
|
+
if (method === 'value' || (method === undefined && sameUnit && inside)) return make('value')
|
|
162
|
+
if (node.min !== undefined && node.max !== undefined && node.max > node.min) {
|
|
163
|
+
/* The field's range laid onto the behavior's. */
|
|
164
|
+
const scale = (hi - lo) / (node.max - node.min)
|
|
165
|
+
return make('scale', { scale: round(scale), offset: round(lo - scale * node.min) })
|
|
153
166
|
}
|
|
154
|
-
|
|
155
|
-
if (node.kind === 'boolean') return make('map', { false: behavior.kind === 'motion-speed' ? 0 : lo, true: behavior.kind === 'motion-speed' ? 1 : hi })
|
|
156
|
-
return make('map', caseMap(() => rest, rest))
|
|
167
|
+
return method === 'scale' ? make('scale', { scale: 1, offset: 0 }) : make('value')
|
|
157
168
|
}
|
|
169
|
+
|
|
170
|
+
/* ---------------------------------------------------------------- tables (ADR-0166 3) */
|
|
171
|
+
|
|
172
|
+
/** The first code a new code table names, and the next ones: `CODE`, `CODE2`, … */
|
|
173
|
+
export function freeCodeOf(taken: string[]): string {
|
|
174
|
+
if (!taken.includes('CODE')) return 'CODE'
|
|
175
|
+
let n = 2
|
|
176
|
+
while (taken.includes(`CODE${n}`)) n++
|
|
177
|
+
return `CODE${n}`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A legend role from a binding's id: lower case letters, digits and `-`. */
|
|
181
|
+
export const legendRoleOf = (id: string): string =>
|
|
182
|
+
id
|
|
183
|
+
.toLowerCase()
|
|
184
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
185
|
+
.replace(/^-+|-+$/g, '') || 'legend'
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A first table on a field (ADR-0166 10): a row per value of a closed field, an open field's rows typed later, one
|
|
189
|
+
* interval over the whole of a number. A new field is made by the table (P2): a code table's a text whose values are the
|
|
190
|
+
* table's keys, starting with one code; a range table's a number. Returns the binding and, for a new field, the schema
|
|
191
|
+
* with it.
|
|
192
|
+
*/
|
|
193
|
+
export function tableDraftOf(
|
|
194
|
+
asset: FigureAsset,
|
|
195
|
+
field: { path: string; node: FigureDataNode } | { name: string },
|
|
196
|
+
method: 'code' | 'ranges',
|
|
197
|
+
targets: FigureDataTargets
|
|
198
|
+
): { binding: FigureDataTableBinding; schema?: FigureDataField[] } {
|
|
199
|
+
const taken = (asset.dataInterface?.bindings ?? []).map(b => b.id)
|
|
200
|
+
const made = 'name' in field
|
|
201
|
+
const name = made ? field.name : (field.path.replace(/\[\]/g, '').split('.').pop() ?? field.path)
|
|
202
|
+
const id = freeIdOf(taken, name.toLowerCase())
|
|
203
|
+
const node: FigureDataNode = made ? (method === 'code' ? { kind: 'text', values: ['CODE'] } : { kind: 'number' }) : field.node
|
|
204
|
+
const accessor = made ? field.name : accessorOfRow(field.path)
|
|
205
|
+
const table: Record<string, FigureDataTableEntry> =
|
|
206
|
+
method === 'code' ? Object.fromEntries(domainOf(node).map(v => [String(v), {}])) : { [`${node.kind === 'number' ? (node.min ?? '') : ''}~`]: {} }
|
|
207
|
+
const binding: FigureDataTableBinding = { id, accessor, rule: 'table', lookup: method, targets: tidyTargets(targets), table }
|
|
208
|
+
if (!made) return { binding }
|
|
209
|
+
const schema = [...(asset.dataInterface?.schema ?? []), { name: field.name, ...node } as FigureDataField]
|
|
210
|
+
return { binding, schema }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Targets with empty lists left out, as the kernel keeps them. */
|
|
214
|
+
const tidyTargets = (t: FigureDataTargets): FigureDataTargets => ({
|
|
215
|
+
...(t.parts?.length ? { parts: [...t.parts] } : {}),
|
|
216
|
+
...(t.inputs?.length ? { inputs: [...t.inputs] } : {}),
|
|
217
|
+
...(t.group ? { group: t.group } : {})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
/** A look with only the columns its targets take: a target taken away takes its columns with it. */
|
|
221
|
+
function lookFor(look: FigureDataTableEntry, t: FigureDataTargets): FigureDataTableEntry {
|
|
222
|
+
const out: FigureDataTableEntry = {}
|
|
223
|
+
if (t.parts?.length) {
|
|
224
|
+
if (look.color !== undefined) out.color = look.color
|
|
225
|
+
if (look.effect !== undefined) out.effect = look.effect
|
|
226
|
+
}
|
|
227
|
+
if (t.inputs?.length && look.values) {
|
|
228
|
+
const values = Object.fromEntries(Object.entries(look.values).filter(([id]) => t.inputs!.includes(id)))
|
|
229
|
+
if (Object.keys(values).length) out.values = values
|
|
230
|
+
}
|
|
231
|
+
if (t.group) {
|
|
232
|
+
if (look.clip !== undefined) out.clip = look.clip
|
|
233
|
+
if (look.speed !== undefined) out.speed = look.speed
|
|
234
|
+
if (look.paused !== undefined) out.paused = look.paused
|
|
235
|
+
}
|
|
236
|
+
return out
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The table with these targets: each look keeps only the columns they take. */
|
|
240
|
+
export function tableWithTargets(b: FigureDataTableBinding, targets: FigureDataTargets): FigureDataTableBinding {
|
|
241
|
+
const t = tidyTargets(targets)
|
|
242
|
+
return { ...b, targets: t, table: Object.fromEntries(Object.entries(b.table).map(([k, look]) => [k, lookFor(look, t)])) }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The table with one look set: the whole look given, a column left out cleared. */
|
|
246
|
+
export const tableWithLook = (b: FigureDataTableBinding, key: string, look: FigureDataTableEntry): FigureDataTableBinding => ({ ...b, table: { ...b.table, [key]: lookFor(look, b.targets) } })
|
|
247
|
+
|
|
248
|
+
/** The table's rows renamed, added or taken away, in order; `default` kept last. */
|
|
249
|
+
export function tableWithKeys(b: FigureDataTableBinding, rows: Array<[string, FigureDataTableEntry]>): FigureDataTableBinding {
|
|
250
|
+
const d = b.table.default
|
|
251
|
+
return { ...b, table: { ...Object.fromEntries(rows), ...(d !== undefined ? { default: d } : {}) } }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The columns a table's targets give its looks, as the row editor shows them: colour and effect for parts, a value per
|
|
256
|
+
* state input (its unit and range), the clip, speed and pause of the group.
|
|
257
|
+
*/
|
|
258
|
+
export function tableColumnsOf(asset: FigureAsset, b: FigureDataTableBinding): TableColumn[] {
|
|
259
|
+
const out: TableColumn[] = []
|
|
260
|
+
if (b.targets.parts?.length) out.push({ kind: 'color' }, { kind: 'effect' })
|
|
261
|
+
for (const id of b.targets.inputs ?? []) {
|
|
262
|
+
const input = asset.document.model.inputs.find(i => i.id === id)
|
|
263
|
+
out.push({ kind: 'value', id, label: asset.stateInputs?.[id]?.label ?? id, unit: input?.unit, min: input?.min, max: input?.max })
|
|
264
|
+
}
|
|
265
|
+
if (b.targets.group) {
|
|
266
|
+
const group = (asset.motionGroups ?? []).find(g => g.id === b.targets.group)
|
|
267
|
+
out.push({ kind: 'clip', clips: group?.clips ?? [] }, { kind: 'speed' }, { kind: 'paused' })
|
|
268
|
+
}
|
|
269
|
+
return out
|
|
270
|
+
}
|
|
271
|
+
export type TableColumn =
|
|
272
|
+
| { kind: 'color' | 'effect' | 'speed' | 'paused' }
|
|
273
|
+
| { kind: 'value'; id: string; label: string; unit?: string; min?: number; max?: number }
|
|
274
|
+
| { kind: 'clip'; clips: string[] }
|
|
275
|
+
|
|
276
|
+
/** The state inputs a table may give values to: each control, with its label. */
|
|
277
|
+
export const stateInputsOf = (asset: FigureAsset): Array<{ id: string; label: string }> =>
|
|
278
|
+
asset.document.model.inputs.filter(i => i.role === 'state').map(i => ({ id: i.id, label: asset.stateInputs?.[i.id]?.label ?? i.id }))
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* A code table's field follows its keys (ADR-0166 risk 3): when the field is a closed text, its values are the table's
|
|
282
|
+
* codes. The schema with the field so, or undefined when the field is open or already so.
|
|
283
|
+
*/
|
|
284
|
+
export function schemaFollowingCodes(asset: FigureAsset, b: FigureDataTableBinding, codes: string[]): FigureDataField[] | undefined {
|
|
285
|
+
const schema = asset.dataInterface?.schema ?? []
|
|
286
|
+
const path = b.accessor.replace(/\[\d+\]/g, '[]')
|
|
287
|
+
const field = fieldAt(schema, path)
|
|
288
|
+
if (field?.kind !== 'text' || !field.values || !codes.length) return undefined
|
|
289
|
+
if (field.values.length === codes.length && field.values.every((v, i) => v === codes[i])) return undefined
|
|
290
|
+
const next = { ...field, values: [...codes] } as FigureDataField & { default?: string }
|
|
291
|
+
if (next.default !== undefined && !codes.includes(next.default)) delete next.default
|
|
292
|
+
return schemaWithFieldReplaced(schema, path, next)
|
|
293
|
+
}
|
|
294
|
+
|
|
158
295
|
const round = (v: number) => Math.round(v * 1e6) / 1e6
|
|
159
296
|
|
|
160
297
|
/* ---------------------------------------------------------------- the schema as a tree */
|
|
@@ -231,12 +368,23 @@ export const schemaRowsOf = (asset: FigureAsset): FigureDataRow[] => (asset.data
|
|
|
231
368
|
|
|
232
369
|
/**
|
|
233
370
|
* What a sample of data would do on this figure, read the one way a placed figure reads it (things-scene's figure
|
|
234
|
-
* inputs): what it moves, or why it is refused, each value's reason
|
|
371
|
+
* inputs): what it moves, or why it is refused, each value's reason; each table's look and where it came from, and what
|
|
372
|
+
* of a legend was left out. `legends`: sample legends by role, as a scene's legend of the role's name would be.
|
|
235
373
|
*/
|
|
236
|
-
export function sampleOf(
|
|
374
|
+
export function sampleOf(
|
|
375
|
+
asset: FigureAsset,
|
|
376
|
+
sample: unknown,
|
|
377
|
+
legends: Record<string, unknown> = {}
|
|
378
|
+
): { issues: FigureDataValueIssue[]; result: FigureDataResult; uses: Readonly<Record<string, FigureTableUse>>; notes: FigureRuntimeIssue[] } {
|
|
237
379
|
const inputs = new FigureInputs(asset)
|
|
380
|
+
inputs.setThemes({ legends: Object.fromEntries(Object.entries(legends).map(([role, legend]) => [role, { legend, source: 'domain' as const, name: role }])) })
|
|
238
381
|
const taken = inputs.setData(sample)
|
|
239
|
-
return {
|
|
382
|
+
return {
|
|
383
|
+
issues: taken.accepted ? [] : (taken.issues[0]?.issues ?? []),
|
|
384
|
+
result: inputs.data,
|
|
385
|
+
uses: inputs.tableUses,
|
|
386
|
+
notes: inputs.diagnostics().filter(i => i.severity === 'note')
|
|
387
|
+
}
|
|
240
388
|
}
|
|
241
389
|
|
|
242
390
|
/** 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. */
|
|
@@ -249,16 +397,9 @@ export function typedValueOf(node: FigureDataNode, typed: string): FigureDataVal
|
|
|
249
397
|
return typed
|
|
250
398
|
}
|
|
251
399
|
|
|
252
|
-
/** The parts a
|
|
400
|
+
/** The parts a table may show on: the parts the figure's appearance names. */
|
|
253
401
|
export const partsOf = (asset: FigureAsset): string[] => [...new Set(asset.appearance.map(a => a.target))]
|
|
254
402
|
|
|
255
|
-
/** A first status port: the label as given, on the first colour token, no part yet. The kernel says whether it holds. */
|
|
256
|
-
export function portDraftOf(asset: FigureAsset, label: string): FigureStatusPort {
|
|
257
|
-
const taken = (asset.dataInterface?.ports ?? []).map(p => p.id)
|
|
258
|
-
const token = Object.keys(asset.palette)[0]
|
|
259
|
-
return { id: freeIdOf(taken, label.trim() || 'status'), label: label.trim(), tokens: token ? [token] : [], parts: [] }
|
|
260
|
-
}
|
|
261
|
-
|
|
262
403
|
/** Whether a text is an accessor the kernel takes. */
|
|
263
404
|
export const accessorTaken = (accessor: string): boolean => parseFigureAccessor(accessor) !== null
|
|
264
405
|
|
|
@@ -269,6 +410,14 @@ export const accessorTaken = (accessor: string): boolean => parseFigureAccessor(
|
|
|
269
410
|
export function motionWrittenByData(asset: FigureAsset): Record<string, { clip: boolean; speed: boolean }> {
|
|
270
411
|
const out: Record<string, { clip: boolean; speed: boolean }> = {}
|
|
271
412
|
for (const b of asset.dataInterface?.bindings ?? []) {
|
|
413
|
+
if (b.rule === 'table') {
|
|
414
|
+
if (!b.targets.group) continue
|
|
415
|
+
const one = (out[b.targets.group] ??= { clip: false, speed: false })
|
|
416
|
+
const looks = Object.values(b.table)
|
|
417
|
+
if (looks.some(l => l.clip !== undefined)) one.clip = true
|
|
418
|
+
if (looks.some(l => l.speed !== undefined)) one.speed = true
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
272
421
|
if (b.behavior.kind !== 'motion-clip' && b.behavior.kind !== 'motion-speed') continue
|
|
273
422
|
const one = (out[b.behavior.group] ??= { clip: false, speed: false })
|
|
274
423
|
if (b.behavior.kind === 'motion-clip') one.clip = true
|
|
@@ -315,7 +464,7 @@ export interface RotationChoice {
|
|
|
315
464
|
/**
|
|
316
465
|
* The rotation card's commands (data binding design decision 1, 「회전」): a clip that turns the joint once around and
|
|
317
466
|
* keeps adding the turn (wrap, accumulating), a reverse one when a direction is given, the motion group holding them,
|
|
318
|
-
* and the bindings -- on to the group's pause, speed to its speed, direction to its clip. Nothing new runs them: they
|
|
467
|
+
* and the bindings -- on, by a table, to the group's pause, speed to its speed, direction, by a table, to its clip. Nothing new runs them: they
|
|
319
468
|
* are the drivers, the group and the bindings any author could write, and stay editable one by one.
|
|
320
469
|
*
|
|
321
470
|
* The clip lasts `T = 3 × 60 / max rpm` seconds, so the most the field gives plays at the group's top speed, 3 -- the
|
|
@@ -359,12 +508,12 @@ export function rotationStepsOf(asset: FigureAsset, c: RotationChoice): FigurePr
|
|
|
359
508
|
steps.push({ kind: 'set-motion-group', group: { id: group, label: c.label, clips, defaultClip: forward, transitionSeconds: 0 } })
|
|
360
509
|
}
|
|
361
510
|
const taken = (asset.dataInterface?.bindings ?? []).map(b => b.id)
|
|
362
|
-
const bind = (base: string, binding: Omit<
|
|
511
|
+
const bind = (base: string, binding: Omit<FigureDataValueBinding, 'id'> | Omit<FigureDataTableBinding, 'id'>): FigureProposalStep => {
|
|
363
512
|
const id = freeIdOf(taken, base)
|
|
364
513
|
taken.push(id)
|
|
365
|
-
return { kind: 'set-data-binding', binding: { id, ...binding } }
|
|
514
|
+
return { kind: 'set-data-binding', binding: { id, ...binding } as FigureDataBinding }
|
|
366
515
|
}
|
|
367
|
-
if (c.on) steps.push(bind(`${group}-on`, { accessor: c.on,
|
|
516
|
+
if (c.on) steps.push(bind(`${group}-on`, { accessor: c.on, rule: 'table', lookup: 'code', targets: { group }, table: { true: { paused: false }, false: { paused: true } } }))
|
|
368
517
|
if (c.speed) steps.push(bind(`${group}-speed`, { accessor: c.speed.path, behavior: { kind: 'motion-speed', group }, rule: 'scale', param: { scale: round(seconds / 60), offset: 0 } }))
|
|
369
518
|
if (c.direction) {
|
|
370
519
|
const values = domainOf(fieldAt(schema, c.direction.path) ?? { kind: 'text' }).map(String)
|
|
@@ -372,9 +521,10 @@ export function rotationStepsOf(asset: FigureAsset, c: RotationChoice): FigurePr
|
|
|
372
521
|
steps.push(
|
|
373
522
|
bind(`${group}-direction`, {
|
|
374
523
|
accessor: c.direction.path,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
524
|
+
rule: 'table',
|
|
525
|
+
lookup: 'code',
|
|
526
|
+
targets: { group },
|
|
527
|
+
table: { [c.direction.forward]: { clip: forward }, [c.direction.reverse]: { clip: reverse }, ...(covered ? {} : { default: { clip: forward } }) }
|
|
378
528
|
})
|
|
379
529
|
)
|
|
380
530
|
}
|
|
@@ -339,11 +339,12 @@ export const EditorStyles = css`
|
|
|
339
339
|
min-height: 0;
|
|
340
340
|
overflow: auto;
|
|
341
341
|
}
|
|
342
|
-
/* Which part the panel speaks of.
|
|
342
|
+
/* Which part the panel speaks of. Above z-index 1: a chip's label (md-filter-chip) sits at 1 and, coming later, was
|
|
343
|
+
drawn over it when the panel scrolled (sweep 2026-10-27 U2). */
|
|
343
344
|
div[subject] {
|
|
344
345
|
position: sticky;
|
|
345
346
|
top: 0;
|
|
346
|
-
z-index:
|
|
347
|
+
z-index: 2;
|
|
347
348
|
display: grid;
|
|
348
349
|
gap: 4px;
|
|
349
350
|
padding: 10px 12px;
|
|
@@ -47,7 +47,7 @@ import { paletteRecipe } from './palette.js'
|
|
|
47
47
|
import { meshChoicesOf, meshPartOf } from './mesh-import.js'
|
|
48
48
|
import './figure-mesh-choice.js'
|
|
49
49
|
import { uploadFigureMesh } from '../graphql/index.js'
|
|
50
|
-
import { slotMarksOf } from './slot-layout-host.js'
|
|
50
|
+
import { placeMarksOf, slotMarksOf } from './slot-layout-host.js'
|
|
51
51
|
|
|
52
52
|
const NO_MARKS: FigureRenderPart[] = []
|
|
53
53
|
import { apexOver, drawnOf } from './section-drawing.js'
|
|
@@ -950,6 +950,8 @@ export class FigureEditor extends LitElement {
|
|
|
950
950
|
}
|
|
951
951
|
/** Data tried in the data tab, given to the preview's figures as their `data`; null when none is (ADR-0051 §8). */
|
|
952
952
|
@state() private dataSample: unknown = null
|
|
953
|
+
/** Sample legends by role, pasted in the data tab: the preview shows the figure's tables by them. Not saved. */
|
|
954
|
+
@state() private legendSample: Record<string, unknown> = {}
|
|
953
955
|
/** The figure's controls: try a value, name it, set its range and travel time, keep the pose. */
|
|
954
956
|
private renderControls(view: { fields: Array<{ id: string; role?: string; value: number }> }, unavailable: boolean) {
|
|
955
957
|
const asset = this.shown()?.asset
|
|
@@ -1574,18 +1576,20 @@ export class FigureEditor extends LitElement {
|
|
|
1574
1576
|
return { ...(this.selected ? { part: this.selected } : {}), ...(trying ? { trying } : {}) }
|
|
1575
1577
|
}
|
|
1576
1578
|
/**
|
|
1577
|
-
* The
|
|
1578
|
-
*
|
|
1579
|
+
* The places things are held, where they fall now (ADR-0118): the slots the slot layout unfolds to and the places held
|
|
1580
|
+
* one by one, drawn while the places are being edited, the selected part's in the primary colour. Made again only when the
|
|
1581
|
+
* evaluation or the selection is new, so the canvas is not stood up again for nothing.
|
|
1579
1582
|
*/
|
|
1580
|
-
private slotMarks(view: { bindings?: readonly unknown[] }, asset: unknown) {
|
|
1583
|
+
private slotMarks(view: { bindings?: readonly unknown[]; geometry: readonly unknown[] }, asset: unknown) {
|
|
1581
1584
|
const places = view.bindings ?? []
|
|
1582
|
-
if (this.slotMarksDrawn?.of !== places) {
|
|
1585
|
+
if (this.slotMarksDrawn?.of !== places || this.slotMarksDrawn.selected !== this.selected) {
|
|
1583
1586
|
const layout = (asset as { document?: { capabilities?: { slotLayout?: { layout: never } } } })?.document?.capabilities?.slotLayout?.layout
|
|
1584
|
-
|
|
1587
|
+
const marks = [...slotMarksOf(places as never, layout), ...(asset ? placeMarksOf(places as never, asset as never, view.geometry as never, this.selected) : [])]
|
|
1588
|
+
this.slotMarksDrawn = { of: places, selected: this.selected, marks: marks as never }
|
|
1585
1589
|
}
|
|
1586
1590
|
return this.slotMarksDrawn.marks
|
|
1587
1591
|
}
|
|
1588
|
-
private slotMarksDrawn?: { of: readonly unknown[]; marks: FigureRenderPart[] }
|
|
1592
|
+
private slotMarksDrawn?: { of: readonly unknown[]; selected: string; marks: FigureRenderPart[] }
|
|
1589
1593
|
private choose(id: string) {
|
|
1590
1594
|
/* A copy on the canvas selects its repeat: the part the author made. */
|
|
1591
1595
|
this.selected = id ? ownerOf(id) : id
|
|
@@ -1868,6 +1872,7 @@ export class FigureEditor extends LitElement {
|
|
|
1868
1872
|
.view=${this.view}
|
|
1869
1873
|
.selected=${this.selected}
|
|
1870
1874
|
.sample=${this.dataSample}
|
|
1875
|
+
.legends=${this.legendSample}
|
|
1871
1876
|
@configure-size=${async (e: CustomEvent<{ part: string }>) => {
|
|
1872
1877
|
this.middle = 'edit'
|
|
1873
1878
|
this.selected = e.detail.part
|
|
@@ -1938,7 +1943,9 @@ export class FigureEditor extends LitElement {
|
|
|
1938
1943
|
@part-live=${(e: CustomEvent) => this.hold({}, e.detail.edit)}
|
|
1939
1944
|
></figure-material-panel>
|
|
1940
1945
|
${this.renderMore(part, view.palette ?? {}, unavailable)}`
|
|
1941
|
-
: view.
|
|
1946
|
+
: view.loading || !view.context
|
|
1947
|
+
? nothing
|
|
1948
|
+
: view.geometry.length === 0
|
|
1942
1949
|
? html`${this.renderSubject()}<section empty-figure>
|
|
1943
1950
|
<p>${i18next.t('figure.text.empty-figure-next')}</p>
|
|
1944
1951
|
<details>
|
|
@@ -1976,8 +1983,10 @@ export class FigureEditor extends LitElement {
|
|
|
1976
1983
|
${this.renderSubject()}
|
|
1977
1984
|
<figure-data-panel
|
|
1978
1985
|
.asset=${asset}
|
|
1986
|
+
.selected=${this.selected}
|
|
1979
1987
|
?locked=${unavailable}
|
|
1980
1988
|
@data-sample=${(e: CustomEvent) => (this.dataSample = e.detail.data)}
|
|
1989
|
+
@legend-sample=${(e: CustomEvent) => (this.legendSample = e.detail.legends)}
|
|
1981
1990
|
@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
1982
1991
|
@part-edits=${(e: CustomEvent) => this.run(() => this.session!.applyRecipe(e.detail.edits))}
|
|
1983
1992
|
></figure-data-panel>
|