@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-ui",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.225",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "npm run build:server && npm run build:client",
|
|
20
20
|
"test": "FIGURE_FREEZE_COMPILED=1 node --import ./test/resolve-ts-siblings.mjs --test test/*.test.ts",
|
|
21
|
-
"test:browser": "node --test test/browser/build.check.mjs test/browser/modeller.check.mjs test/browser/modeller-revision.check.mjs test/browser/editing.check.mjs test/browser/links.check.mjs test/browser/section.check.mjs test/browser/screen.check.mjs test/browser/product-page.check.mjs test/browser/parts.check.mjs test/browser/places-mesh.check.mjs test/browser/part-ids.check.mjs test/browser/fix-alignment.check.mjs test/browser/mesh-select.check.mjs test/browser/row-preview.check.mjs test/browser/detail-tabs.check.mjs test/browser/props.check.mjs test/browser/retire.check.mjs test/browser/namespace.check.mjs test/browser/public.check.mjs test/browser/category.check.mjs test/browser/property-section.check.mjs test/browser/list.check.mjs test/browser/item.check.mjs",
|
|
21
|
+
"test:browser": "node --test test/browser/build.check.mjs test/browser/modeller.check.mjs test/browser/modeller-revision.check.mjs test/browser/editing.check.mjs test/browser/links.check.mjs test/browser/section.check.mjs test/browser/screen.check.mjs test/browser/product-page.check.mjs test/browser/parts.check.mjs test/browser/places-mesh.check.mjs test/browser/part-ids.check.mjs test/browser/fix-alignment.check.mjs test/browser/mesh-select.check.mjs test/browser/row-preview.check.mjs test/browser/detail-tabs.check.mjs test/browser/props.check.mjs test/browser/retire.check.mjs test/browser/namespace.check.mjs test/browser/public.check.mjs test/browser/category.check.mjs test/browser/property-section.check.mjs test/browser/list.check.mjs test/browser/item.check.mjs test/browser/side-panel-sweep.check.mjs",
|
|
22
22
|
"copy:files": "copyfiles -e \"./client/**/*.{ts,js,json}\" -u 1 \"./client/**/*\" dist-client",
|
|
23
23
|
"build:client": "npm run copy:files && npx tsc --p ./client/tsconfig.json",
|
|
24
24
|
"build:server": "npx tsc --p ./server/tsconfig.json",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"clean": "npm run clean:server && npm run clean:client"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hatiolab/figure-model": "^0.
|
|
31
|
-
"@hatiolab/things-scene": "^10.1.
|
|
30
|
+
"@hatiolab/figure-model": "^0.12.0",
|
|
31
|
+
"@hatiolab/things-scene": "^10.1.102",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.29.43",
|
|
34
34
|
"@operato/graphql": "^10.29.43",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"@operato/p13n": "^10.29.43",
|
|
40
40
|
"@operato/popup": "^10.29.43",
|
|
41
41
|
"@operato/shell": "^10.29.43",
|
|
42
|
-
"@operato/slot-layout-editor": "^10.29.
|
|
42
|
+
"@operato/slot-layout-editor": "^10.29.57",
|
|
43
43
|
"@operato/styles": "^10.0.0",
|
|
44
44
|
"@operato/utils": "^10.29.43",
|
|
45
45
|
"@things-factory/ai-assistant": "^10.1.223",
|
|
46
|
-
"@things-factory/figure-service": "^10.1.
|
|
47
|
-
"@things-factory/figure-viewer": "^10.1.
|
|
46
|
+
"@things-factory/figure-service": "^10.1.224",
|
|
47
|
+
"@things-factory/figure-viewer": "^10.1.224",
|
|
48
48
|
"@things-factory/setting-base": "^10.1.223",
|
|
49
49
|
"three": "^0.185.1"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "9929ca76f614ff84bea1f61dabe0e1b7f00cd18a"
|
|
52
52
|
}
|
package/test/ai-proposal.test.ts
CHANGED
|
@@ -157,7 +157,7 @@ test('the same new figure\'s card in another tab before the first save makes a s
|
|
|
157
157
|
})
|
|
158
158
|
const made = [{ kind: 'add-part', part: { id: 'lid', kind: 'box', dimensions: { width: 100, height: 10, depth: 100 } } }] as any[]
|
|
159
159
|
const first = tab()
|
|
160
|
-
first.startNew()
|
|
160
|
+
first.startNew({ name: 'New' })
|
|
161
161
|
const detail: FigureApplyDetail = { key: K, steps: made, create: true }
|
|
162
162
|
const here = { ...taker({}), offer: (d: FigureApplyDetail) => (takeFigureOffer(first, d, { onSavedFigure: false, say }), d) }
|
|
163
163
|
assert.equal((await runFigureProposal(detail, here, say)).verdict, 'accepted')
|
|
@@ -166,7 +166,7 @@ test('the same new figure\'s card in another tab before the first save makes a s
|
|
|
166
166
|
const there = {
|
|
167
167
|
...taker({}),
|
|
168
168
|
async openNew(s: FigureProposalStep[], key?: string) {
|
|
169
|
-
second.startNew()
|
|
169
|
+
second.startNew({ name: 'New' })
|
|
170
170
|
second.createUnder(key!)
|
|
171
171
|
second.applyProposal(s, second.read().revision!, second.read().context!, key)
|
|
172
172
|
return { applied: true }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* Manual check of the data tab's tables (ADR-0166): the real editor on the robot dog, in Korean, over a store in memory. */
|
|
2
|
+
import './modeller-page.js'
|
|
3
|
+
import { i18next } from '@operato/i18n'
|
|
4
|
+
import { robotDogFigure, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
5
|
+
|
|
6
|
+
const ko = await fetch('/ko.json').then(r => r.json())
|
|
7
|
+
i18next.addResourceBundle('ko', 'translations', ko, true, true)
|
|
8
|
+
await i18next.changeLanguage('ko')
|
|
9
|
+
await window.harness.mount('dog', serializeFigureAsset(robotDogFigure().asset))
|
|
@@ -350,13 +350,14 @@ describe('a row\'s pitch', () => {
|
|
|
350
350
|
})
|
|
351
351
|
|
|
352
352
|
describe('holding places on a part', () => {
|
|
353
|
-
test('a place
|
|
353
|
+
test('a place takes any item until kinds are named; a figure that holds nothing says so, and declaring it holds things is one step', async () => {
|
|
354
354
|
await mount('box')
|
|
355
355
|
await pick('box')
|
|
356
356
|
/* What the figure does is a tab again (user 2026-10-17). */
|
|
357
357
|
await tab('capability')
|
|
358
|
-
|
|
359
|
-
|
|
358
|
+
/* No kinds named: it takes any item (ADR-0051 §4.1 decision 5, 2026-10-27). */
|
|
359
|
+
assert.equal((await at('button[act]', 'figure.button.add-place'))?.disabled, false, 'no kinds: a place for any item')
|
|
360
|
+
await type(await handle('input[placeholder="figure.text.place-accepts-any"]'), 'BOX')
|
|
360
361
|
assert.equal((await at('button[act]', 'figure.button.add-place'))?.disabled, false, 'a kind: a place may be made')
|
|
361
362
|
assert.equal((await all('p[hint]', 'figure.text.place-needs-holding')).length, 1, 'said: nothing is passed without holding')
|
|
362
363
|
const start = await state()
|
|
@@ -365,6 +366,27 @@ describe('holding places on a part', () => {
|
|
|
365
366
|
assert.ok((await source()).document.capabilities.names.includes('Capacity'))
|
|
366
367
|
assert.deepEqual(await all('p[hint]', 'figure.text.place-needs-holding'), [], 'not said again')
|
|
367
368
|
})
|
|
369
|
+
|
|
370
|
+
test('a place is changed under its own id: its types emptied, it takes any item, in one step', async () => {
|
|
371
|
+
let a = box(createFigureAsset('Gripper'), 'box', [200, 100, 200], [0, 50, 0])
|
|
372
|
+
a = applyFigureAuthoring(a, { kind: 'add-binding', id: 'box-grip-old', label: 'Grip', role: 'grip', part: 'box', face: 'top', accepts: ['BOX', 'WORKPIECE'], capacity: 1 })
|
|
373
|
+
await mount('box', a)
|
|
374
|
+
await pick('box')
|
|
375
|
+
await tab('capability')
|
|
376
|
+
await click(await at('button[edit-place]'), 'change the place')
|
|
377
|
+
const field = await handle('input[placeholder="figure.text.place-accepts-any"]')
|
|
378
|
+
assert.equal(await field.evaluate(i => i.value), 'BOX, WORKPIECE', 'its types are in the form')
|
|
379
|
+
await field.click({ clickCount: 3 })
|
|
380
|
+
await page().keyboard.press('Backspace')
|
|
381
|
+
await page().keyboard.press('Tab')
|
|
382
|
+
await settled()
|
|
383
|
+
const start = await state()
|
|
384
|
+
await click(await at('button[act]', 'figure.button.save-place'), 'save the place')
|
|
385
|
+
await oneStep(start)
|
|
386
|
+
const places = (await source()).document.capabilities.bindings
|
|
387
|
+
assert.deepEqual(places.map(b => [b.id, b.label, b.role, Object.hasOwn(b, 'accepts')]), [['box-grip-old', 'Grip', 'grip', false]])
|
|
388
|
+
assert.ok(await at('button[act]', 'figure.button.add-place'), 'the form adds again')
|
|
389
|
+
})
|
|
368
390
|
})
|
|
369
391
|
|
|
370
392
|
describe('a position worked out from the figure size', () => {
|
|
@@ -173,6 +173,11 @@ describe('the first save of a new figure', () => {
|
|
|
173
173
|
return { first: popup.shadowRoot.querySelector('input')?.id, type: popup.shadowRoot.querySelector('#type').value, focused: active?.id }
|
|
174
174
|
})
|
|
175
175
|
|
|
176
|
+
test('a new figure is named in the screen\'s words, the tab too (sweep 2026-10-27 U8)', async () => {
|
|
177
|
+
assert.equal(await read(() => product.page().session.read().name), 'figure.text.new-figure-name')
|
|
178
|
+
assert.equal(await read(() => product.page().context.title), 'figure.text.new-figure-name')
|
|
179
|
+
})
|
|
180
|
+
|
|
176
181
|
test('before it is saved: the type can be written, held by the page, and the release waits', async () => {
|
|
177
182
|
const field = await read(() => { const input = product.page().shadowRoot.querySelector('label[type-field] input'); return input && { disabled: input.disabled } })
|
|
178
183
|
assert.deepEqual(field, { disabled: false })
|
|
@@ -242,6 +247,22 @@ describe('opening another figure', () => {
|
|
|
242
247
|
})
|
|
243
248
|
})
|
|
244
249
|
|
|
250
|
+
test('nothing open on the page: its properties say nothing of an empty figure (sweep 2026-10-27 U3)', async () => {
|
|
251
|
+
await open('a')
|
|
252
|
+
const empty = await read(async () => {
|
|
253
|
+
const page = product.page()
|
|
254
|
+
/* The state a page is in before it is told which figure, and after it is left: the session holds none. */
|
|
255
|
+
page.session.close()
|
|
256
|
+
const editor = product.deep('figure-editor')[0]
|
|
257
|
+
editor.requestUpdate()
|
|
258
|
+
await editor.updateComplete
|
|
259
|
+
return editor.shadowRoot.querySelectorAll('section[empty-figure]').length
|
|
260
|
+
})
|
|
261
|
+
/* Told another figure, the page opens it again: the checks after this one start from an open page. */
|
|
262
|
+
await open('b')
|
|
263
|
+
assert.equal(empty, 0)
|
|
264
|
+
})
|
|
265
|
+
|
|
245
266
|
test('the row is emptied first: the type of the one before does not stand while the next one loads', async () => {
|
|
246
267
|
const typeField = () => read(() => product.page().shadowRoot.querySelector('label[type-field] input')?.value)
|
|
247
268
|
await open('a')
|
|
@@ -258,6 +279,10 @@ describe('opening another figure', () => {
|
|
|
258
279
|
await until(() => !!window.letB, 'b asked for')
|
|
259
280
|
await read(() => product.page().updateComplete)
|
|
260
281
|
assert.equal(await typeField(), '')
|
|
282
|
+
/* Still loading, the figure is not an empty one: no 「add a part」 (sweep 2026-10-27 U3). */
|
|
283
|
+
await read(() => product.deep('figure-editor')[0].updateComplete)
|
|
284
|
+
assert.equal(await read(() => product.deep('figure-editor >>> section[empty-figure]').length), 0)
|
|
285
|
+
assert.equal(await read(() => product.page().context.title), 'figure.label.figure')
|
|
261
286
|
await read(() => window.letB())
|
|
262
287
|
await until(() => product.page().shadowRoot.querySelector('label[type-field] input')?.value === 'type-b', 'b\'s type')
|
|
263
288
|
})
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* What the modeller sweep of 2026-10-27 found on the side panel and the preview, in a real browser
|
|
3
|
+
* (operato-figure docs/audits/modeller-ux-sweep-2026-10-27.md, U1 U2 U4 U5 U6 U7): each as a person would meet it -- a
|
|
4
|
+
* box that reads as a box, a heading nothing is drawn over, a delete that looks like one, the empty animation said in
|
|
5
|
+
* its own words, a field added by Enter, the preview's values starting in one column and its help in the bar's look.
|
|
6
|
+
* The page loads no translations, so the words read are i18n keys.
|
|
7
|
+
*
|
|
8
|
+
* Run: node --test test/browser/side-panel-sweep.check.mjs
|
|
9
|
+
*/
|
|
10
|
+
import { test, describe, before, after } from 'node:test'
|
|
11
|
+
import assert from 'node:assert/strict'
|
|
12
|
+
|
|
13
|
+
import { addFigureBox, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
14
|
+
|
|
15
|
+
import { buildPage } from './build.mjs'
|
|
16
|
+
import { openPage } from './serve.mjs'
|
|
17
|
+
|
|
18
|
+
const sleep = ms => new Promise(done => setTimeout(done, ms))
|
|
19
|
+
|
|
20
|
+
let opened
|
|
21
|
+
before(async () => {
|
|
22
|
+
opened = await openPage(await buildPage('links-page.js'))
|
|
23
|
+
})
|
|
24
|
+
after(async () => {
|
|
25
|
+
await opened?.close()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const page = () => opened.page
|
|
29
|
+
const read = (fn, ...args) => page().evaluate(fn, ...args)
|
|
30
|
+
const source = () => read(() => probe.source())
|
|
31
|
+
const settled = () => read(() => document.querySelector('figure-editor').updateComplete)
|
|
32
|
+
|
|
33
|
+
async function until(fn, args, what, ms = 8000) {
|
|
34
|
+
const deadline = Date.now() + ms
|
|
35
|
+
for (;;) {
|
|
36
|
+
const got = await read(fn, ...args)
|
|
37
|
+
if (got) return got
|
|
38
|
+
if (Date.now() > deadline) assert.fail(`waited for ${what}`)
|
|
39
|
+
await sleep(50)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function mount(kind, text = null) {
|
|
43
|
+
await read((kind, text) => harness.mount(kind, text), kind, text)
|
|
44
|
+
await until(
|
|
45
|
+
() => {
|
|
46
|
+
try {
|
|
47
|
+
return probe.ids().some(id => !!probe.partOnPage(id))
|
|
48
|
+
} catch {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
[],
|
|
53
|
+
'the figure drawn in 3D',
|
|
54
|
+
15000
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
async function click(point, what) {
|
|
58
|
+
assert.ok(point, `${what} is on the page to click`)
|
|
59
|
+
await page().mouse.click(point.x, point.y)
|
|
60
|
+
await settled()
|
|
61
|
+
}
|
|
62
|
+
const tab = name => read(l => probe.tab(l), `figure.label.tab-short-${name}`)
|
|
63
|
+
|
|
64
|
+
/** Every element matching the selector through every shadow root, shown on the page. */
|
|
65
|
+
const DEEP = `(selector => {
|
|
66
|
+
const deep = (root = document) => [...root.querySelectorAll(selector)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
67
|
+
return deep().filter(e => e.getBoundingClientRect().height > 0)
|
|
68
|
+
})`
|
|
69
|
+
|
|
70
|
+
describe('the side panel', () => {
|
|
71
|
+
before(() => mount('box'))
|
|
72
|
+
|
|
73
|
+
test('U1 the capability tab: the item box keeps its size beside its long note', async () => {
|
|
74
|
+
await click(await tab('capability'), 'the capability tab')
|
|
75
|
+
/* The note as Korean reads it: words that wrap (the key alone is one word that never does). */
|
|
76
|
+
const width = await read(DEEP => {
|
|
77
|
+
eval(DEEP)('label[item] em')[0].textContent = '홀더가 자리에 앉혀 나르는 물건의 모양입니다. 릴 · 트레이 · 매거진. 아이템 타입 표에서 이 피규어를 고를 수 있습니다.'
|
|
78
|
+
return eval(DEEP)('label[item] > input')[0]?.getBoundingClientRect().width
|
|
79
|
+
}, DEEP)
|
|
80
|
+
assert.ok(width >= 12, `the item box is ${width}px wide`)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('U2 scrolled under the heading, the places chips are drawn below it, not over it', async () => {
|
|
84
|
+
/* A short window, so the panel scrolls. */
|
|
85
|
+
await page().setViewport({ width: 1400, height: 520 })
|
|
86
|
+
await click(await read(id => probe.row(id), 'box'), 'the box in the parts list')
|
|
87
|
+
await click(await tab('capability'), 'the capability tab')
|
|
88
|
+
const hit = await read(DEEP => {
|
|
89
|
+
const chips = eval(DEEP)('ox-input-select-buttons[places-by]')[0]
|
|
90
|
+
const subject = eval(DEEP)('div[subject]')[0]
|
|
91
|
+
if (!chips || !subject) return 'no chips or no heading'
|
|
92
|
+
let scroller = chips.parentNode
|
|
93
|
+
for (;;) {
|
|
94
|
+
if (!scroller) return 'nothing scrolls'
|
|
95
|
+
if (scroller instanceof ShadowRoot) scroller = scroller.host
|
|
96
|
+
else if (scroller instanceof Element && /auto|scroll/.test(getComputedStyle(scroller).overflowY) && scroller.scrollHeight > scroller.clientHeight) break
|
|
97
|
+
else scroller = scroller.parentNode
|
|
98
|
+
}
|
|
99
|
+
const head = subject.getBoundingClientRect()
|
|
100
|
+
const before = chips.getBoundingClientRect()
|
|
101
|
+
scroller.scrollTop += before.top - (head.top + head.height / 2) + 4
|
|
102
|
+
const at = chips.getBoundingClientRect()
|
|
103
|
+
const x = at.left + Math.min(40, at.width / 2),
|
|
104
|
+
y = at.top + 6
|
|
105
|
+
if (y > head.bottom || y < head.top) return `the chips stand at ${Math.round(y)}, the heading ${Math.round(head.top)}..${Math.round(head.bottom)}`
|
|
106
|
+
let el = document.elementFromPoint(x, y)
|
|
107
|
+
while (el?.shadowRoot) {
|
|
108
|
+
const inner = el.shadowRoot.elementFromPoint(x, y)
|
|
109
|
+
if (!inner || inner === el) break
|
|
110
|
+
el = inner
|
|
111
|
+
}
|
|
112
|
+
for (let n = el; n; n = n.parentNode instanceof ShadowRoot ? n.parentNode.host : n.parentNode) if (n === subject) return 'heading'
|
|
113
|
+
return el?.tagName.toLowerCase()
|
|
114
|
+
}, DEEP)
|
|
115
|
+
await page().setViewport({ width: 1400, height: 900 })
|
|
116
|
+
assert.equal(hit, 'heading')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('U5 the motion tab: no animation yet is said in its own words, the controls\' words said once', async () => {
|
|
120
|
+
await click(await tab('motion'), 'the motion tab')
|
|
121
|
+
const hints = await read(DEEP => eval(DEEP)('p[hint]').map(p => p.textContent.trim()), DEEP)
|
|
122
|
+
assert.ok(hints.includes('figure.text.no-clips'), hints.join(' | '))
|
|
123
|
+
assert.equal(hints.filter(h => h === 'figure.text.no-controls').length, 1)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('U6 the data tab: a field name and Enter adds the field', async () => {
|
|
127
|
+
await click(await tab('data'), 'the data tab')
|
|
128
|
+
await click(await read(() => look.at('input[aria-label="figure.label.data-name"]')), 'the field name')
|
|
129
|
+
await page().keyboard.type('speed')
|
|
130
|
+
await page().keyboard.press('Enter')
|
|
131
|
+
await settled()
|
|
132
|
+
await until(() => probe.source().dataInterface?.schema?.some(f => f.name === 'speed'), [], 'the field added')
|
|
133
|
+
assert.equal((await read(DEEP => eval(DEEP)('input[aria-label="figure.label.data-name"]')[0].value, DEEP)), '')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('U4 a field opened: its delete is the bin, not a cross that reads as close', async () => {
|
|
137
|
+
await click(await read(() => look.at('button', 'speed')), 'the speed field')
|
|
138
|
+
const icon = await until(DEEP => eval(DEEP)('button[title="figure.button.remove-data-field"] md-icon')[0]?.textContent.trim(), [DEEP], 'the field opened')
|
|
139
|
+
assert.equal(icon, 'delete')
|
|
140
|
+
assert.ok((await source()).dataInterface.schema.some(f => f.name === 'speed'))
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('the preview', () => {
|
|
145
|
+
/* A number and a text, of names far apart in length, each read by a table: the preview gives each a field. */
|
|
146
|
+
const asset = addFigureBox(createFigureAsset('Probe'), { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 200, z: 0 } })
|
|
147
|
+
asset.dataInterface = {
|
|
148
|
+
version: 'figure-data-interface-2',
|
|
149
|
+
schema: [
|
|
150
|
+
{ name: 'A', kind: 'number' },
|
|
151
|
+
{ name: 'CARRIERSTATUS', kind: 'text' }
|
|
152
|
+
],
|
|
153
|
+
bindings: [
|
|
154
|
+
{ id: 'a', accessor: 'A', rule: 'table', lookup: 'ranges', targets: { parts: ['box'] }, table: { '~10': { color: '#ff0000' }, default: { color: '#0000ff' } } },
|
|
155
|
+
{ id: 's', accessor: 'CARRIERSTATUS', rule: 'table', lookup: 'code', targets: { parts: ['box'] }, table: { X: { color: '#00ff00' } } }
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
before(async () => {
|
|
159
|
+
await mount('box', serializeFigureAsset(asset))
|
|
160
|
+
await click(await read(() => probe.modeButton('preview')), 'the preview')
|
|
161
|
+
await until(DEEP => eval(DEEP)('figure-preview')[0]?.shadowRoot?.querySelectorAll('div[clips] input').length === 2, [DEEP], 'the two values in the preview')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('U7 a number\'s field and a text\'s start in one column, whatever the names\' lengths', async () => {
|
|
165
|
+
const lefts = await read(DEEP => [...eval(DEEP)('figure-preview')[0].shadowRoot.querySelectorAll('div[clips] input')].map(i => Math.round(i.getBoundingClientRect().left)), DEEP)
|
|
166
|
+
assert.equal(lefts[0], lefts[1], `the fields start at ${lefts.join(' and ')}`)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('U7 the size help\'s choice and button are in the bar\'s look, not the browser\'s grey', async () => {
|
|
170
|
+
const looks = await read(DEEP => {
|
|
171
|
+
const root = eval(DEEP)('figure-preview')[0].shadowRoot
|
|
172
|
+
return ['div[setup] select', 'div[setup] button', 'div[clips] input'].map(s => {
|
|
173
|
+
const c = getComputedStyle(root.querySelector(s))
|
|
174
|
+
return { radius: c.borderTopLeftRadius, background: c.backgroundColor }
|
|
175
|
+
})
|
|
176
|
+
}, DEEP)
|
|
177
|
+
assert.deepEqual(looks[0], looks[1])
|
|
178
|
+
assert.deepEqual(looks[0], looks[2])
|
|
179
|
+
assert.equal(looks[0].radius, '5px')
|
|
180
|
+
assert.notEqual(looks[0].background, 'rgb(239, 239, 239)')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A product's capabilities in the modeller (ADR-0167 3, 9, 12): the registered descriptors, what each still needs, its
|
|
3
|
+
* settings as the descriptor draws them, and the kernel taking what the panel writes.
|
|
4
|
+
*/
|
|
5
|
+
import { test } from 'node:test'
|
|
6
|
+
import assert from 'node:assert/strict'
|
|
7
|
+
import { readFileSync } from 'node:fs'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
import { addFigureBox, addFigurePrimitive, createFigureAsset, editFigurePart } from '@hatiolab/figure-model'
|
|
11
|
+
import { figureBindingsTakenOver, registerFigureCapability } from '@hatiolab/things-scene'
|
|
12
|
+
import {
|
|
13
|
+
consumerDescriptors,
|
|
14
|
+
inputTableAt,
|
|
15
|
+
namesWith,
|
|
16
|
+
needsMissing,
|
|
17
|
+
partsAt,
|
|
18
|
+
settingsIssues,
|
|
19
|
+
settingsOf,
|
|
20
|
+
unknownConsumerNames,
|
|
21
|
+
withInputTable,
|
|
22
|
+
withParts
|
|
23
|
+
} from '../client/modeller/consumer-capabilities.ts'
|
|
24
|
+
|
|
25
|
+
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
26
|
+
const FILL = { key: 'fill', kind: 'input-table', label: '적재량', keys: ['FULL', 'EMPTY', 'EMPTYEMPTY'], start: { FULL: 100, EMPTY: 0, EMPTYEMPTY: 0 } } as const
|
|
27
|
+
const CARRIER = {
|
|
28
|
+
name: 'acme.Carrier',
|
|
29
|
+
label: 'Carrier',
|
|
30
|
+
note: '캐리어',
|
|
31
|
+
version: '1',
|
|
32
|
+
needs: ['item'],
|
|
33
|
+
settings: [{ key: 'loadParts', kind: 'parts', label: '적재물', required: true }, FILL],
|
|
34
|
+
data: [{ name: 'EMPTYTYPE' }, { name: 'CARRIERSTATUS' }],
|
|
35
|
+
check: (settings: Record<string, unknown> | undefined) => (Array.isArray(settings?.loadParts) && settings.loadParts.length ? [] : ['적재물 부품을 고릅니다.'])
|
|
36
|
+
} as never
|
|
37
|
+
|
|
38
|
+
function reel() {
|
|
39
|
+
let asset = addFigurePrimitive(createFigureAsset('Reel'), { id: 'wound', kind: 'cylinder', dimensions: { radius: 400, length: 300 }, position: { x: 0, y: 400, z: 0 } })
|
|
40
|
+
asset.document.model.inputs.push({ id: 'fill', unit: 'percent', min: 0, max: 100, role: 'state' })
|
|
41
|
+
asset.stateDefaults['fill'] = 100
|
|
42
|
+
return asset
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('the registered products\' descriptors; one the figure wears that this modeller does not know is said', () => {
|
|
46
|
+
const off = registerFigureCapability(CARRIER)
|
|
47
|
+
try {
|
|
48
|
+
assert.deepEqual(consumerDescriptors().map(d => d.name), ['acme.Carrier'], 'the shared four are the ladder, not here')
|
|
49
|
+
const asset = editFigurePart(reel(), { kind: 'set-capabilities', names: ['acme.Carrier', 'other.Gauge'] })
|
|
50
|
+
assert.deepEqual(unknownConsumerNames(asset), ['other.Gauge'])
|
|
51
|
+
} finally {
|
|
52
|
+
off()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('what a capability still needs: the item mark, a name; put on and off keeps the other names', () => {
|
|
57
|
+
const asset = reel()
|
|
58
|
+
assert.deepEqual(needsMissing(CARRIER, asset), ['item'])
|
|
59
|
+
assert.deepEqual(needsMissing(CARRIER, { ...asset, item: true }), [])
|
|
60
|
+
assert.deepEqual(needsMissing({ ...(CARRIER as object), needs: ['Holdable', ['Capacity', 'FlowNode']] } as never, asset), ['Holdable', 'Capacity | FlowNode'])
|
|
61
|
+
const held = editFigurePart(asset, { kind: 'set-capabilities', names: ['Capacity', 'Holdable'] })
|
|
62
|
+
assert.deepEqual(namesWith(held, 'acme.Carrier', true), ['Capacity', 'Holdable', 'acme.Carrier'])
|
|
63
|
+
assert.deepEqual(namesWith(editFigurePart(held, { kind: 'set-capabilities', names: namesWith(held, 'acme.Carrier', true) }), 'acme.Carrier', false), ['Capacity', 'Holdable'])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('settings as the descriptor draws them: parts picked, an input with a value per fixed key; the kernel takes them', () => {
|
|
67
|
+
let asset = editFigurePart({ ...reel(), item: true }, { kind: 'set-capabilities', names: ['acme.Carrier'] })
|
|
68
|
+
assert.deepEqual(settingsIssues(CARRIER, asset), ['적재물 부품을 고릅니다.'])
|
|
69
|
+
let settings = withParts(settingsOf(asset, 'acme.Carrier'), 'loadParts', ['wound'])
|
|
70
|
+
assert.deepEqual(partsAt(settings, 'loadParts'), ['wound'])
|
|
71
|
+
/* No input yet: the fixed keys start at the descriptor's values. */
|
|
72
|
+
assert.deepEqual(inputTableAt(settings, FILL), { values: { FULL: 100, EMPTY: 0, EMPTYEMPTY: 0 } })
|
|
73
|
+
settings = withInputTable(settings, FILL, 'fill', { ...inputTableAt(settings, FILL).values, EMPTY: 5 })
|
|
74
|
+
assert.deepEqual(settings, { loadParts: ['wound'], fill: { input: 'fill', values: { FULL: 100, EMPTY: 5, EMPTYEMPTY: 0 } } })
|
|
75
|
+
asset = editFigurePart(asset, { kind: 'set-capability-settings', name: 'acme.Carrier', settings })
|
|
76
|
+
assert.deepEqual(settingsIssues(CARRIER, asset), [])
|
|
77
|
+
assert.deepEqual(inputTableAt(settingsOf(asset, 'acme.Carrier'), FILL), { input: 'fill', values: { FULL: 100, EMPTY: 5, EMPTYEMPTY: 0 } })
|
|
78
|
+
/* No input: the table goes; no parts: the key goes. */
|
|
79
|
+
assert.deepEqual(withInputTable(settings, FILL, undefined, {}), { loadParts: ['wound'] })
|
|
80
|
+
assert.deepEqual(withParts(settings, 'loadParts', []), { fill: settings.fill })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('the panel writes through the kernel\'s commands, the ladder keeps a product\'s names, and every word is there', () => {
|
|
84
|
+
const panel = read('../client/modeller/figure-capability-panel.ts')
|
|
85
|
+
assert.match(panel, /kind: 'set-capability-settings', name: d\.name/)
|
|
86
|
+
assert.match(panel, /\[\.\.\.RUNGS\[which\], \.\.\.consumers\]/)
|
|
87
|
+
const keys = [read('../client/modeller/figure-capability-panel.ts'), read('../client/modeller/figure-data-panel.ts')].flatMap(src => [...src.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1]!)).filter(k => /product-capabilit|data-capability|data-add-selected-part/.test(k))
|
|
88
|
+
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
|
|
89
|
+
const table = JSON.parse(read(`../translations/${lang}.json`))
|
|
90
|
+
for (const key of keys) assert.ok(table[key], `${lang}: ${key}`)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('a binding a product capability takes over is marked where it rests, whole or for the targets named (ADR-0167 17)', () => {
|
|
95
|
+
const off = registerFigureCapability({ name: 'acme.Lamp', label: 'Lamp', note: '', version: '1', needs: [], takes: settings => ({ parts: (settings?.parts as string[]) ?? [] }) })
|
|
96
|
+
try {
|
|
97
|
+
let asset = addFigureBox(addFigureBox(createFigureAsset('Lamp post'), { id: 'head', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 450, z: 0 } }), { id: 'pole', size: { x: 20, y: 400, z: 20 }, position: { x: 0, y: 200, z: 0 } })
|
|
98
|
+
asset = editFigurePart(asset, { kind: 'set-capabilities', names: ['acme.Lamp'] })
|
|
99
|
+
asset = editFigurePart(asset, { kind: 'set-capability-settings', name: 'acme.Lamp', settings: { parts: ['head'] } })
|
|
100
|
+
asset = editFigurePart(asset, {
|
|
101
|
+
kind: 'set-data-binding',
|
|
102
|
+
schema: [{ name: 'mode', kind: 'text' }],
|
|
103
|
+
binding: { id: 'look', accessor: 'mode', rule: 'table', lookup: 'code', targets: { parts: ['head', 'pole'] }, table: { default: { color: '#111111' } } }
|
|
104
|
+
})
|
|
105
|
+
assert.deepEqual(figureBindingsTakenOver(asset), [{ id: 'look', capability: 'acme.Lamp', whole: false, parts: ['head'], inputs: [] }])
|
|
106
|
+
} finally {
|
|
107
|
+
off()
|
|
108
|
+
}
|
|
109
|
+
const panel = read('../client/modeller/figure-data-panel.ts')
|
|
110
|
+
assert.match(panel, /const resting = figureBindingsTakenOver\(asset\)/)
|
|
111
|
+
assert.match(panel, /resting\.filter\(r => r\.id === b\.id\)\.map\(r => this\.renderResting\(r\)\)/)
|
|
112
|
+
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
|
|
113
|
+
const table = JSON.parse(read(`../translations/${lang}.json`))
|
|
114
|
+
for (const key of ['figure.text.data-binding-rests', 'figure.text.data-binding-rests-targets']) assert.match(table[key], /\{where\}.*\{name\}/, `${lang}: ${key}`)
|
|
115
|
+
}
|
|
116
|
+
})
|