@things-factory/figure-ui 10.1.149 → 10.1.151
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/editor.ts +14 -6
- package/dist-client/modeller/editor.d.ts +6 -0
- package/dist-client/modeller/editor.js +14 -6
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/browser/build.check.mjs +77 -0
- package/test/browser/build.mjs +25 -5
- package/test/browser/part-ids.check.mjs +86 -0
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.151",
|
|
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/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",
|
|
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",
|
|
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.6.0",
|
|
31
|
+
"@hatiolab/things-scene": "^10.1.83",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.0.0",
|
|
34
34
|
"@operato/graphql": "^10.0.0",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"@operato/slot-layout-editor": "^10.29.25",
|
|
42
42
|
"@operato/styles": "^10.0.0",
|
|
43
43
|
"@operato/utils": "^10.0.0",
|
|
44
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
45
|
-
"@things-factory/figure-service": "^10.1.
|
|
44
|
+
"@things-factory/ai-assistant": "^10.1.150",
|
|
45
|
+
"@things-factory/figure-service": "^10.1.151",
|
|
46
46
|
"three": "^0.185.1"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "495d06ef9ecf4d19fc9bf45ebc3cff4fdfefca53"
|
|
49
49
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Two runs building the same page at the same time, as two sessions on one machine do: each builds in a folder of its
|
|
3
|
+
* own, neither takes away the other's page, and each folder goes when its process ends -- by itself or stopped.
|
|
4
|
+
*
|
|
5
|
+
* Run: node --test test/browser/build.check.mjs
|
|
6
|
+
*/
|
|
7
|
+
import { test } from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
import { spawn } from 'node:child_process'
|
|
10
|
+
import { existsSync } from 'node:fs'
|
|
11
|
+
import { dirname } from 'node:path'
|
|
12
|
+
|
|
13
|
+
const BUILD = new URL('./build.mjs', import.meta.url).href
|
|
14
|
+
|
|
15
|
+
/* A run: builds the page, says where and what it built, and when asked says whether its page is still as it built it. */
|
|
16
|
+
const RUN = `
|
|
17
|
+
import { buildPage } from ${JSON.stringify(BUILD)}
|
|
18
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
19
|
+
import { createHash } from 'node:crypto'
|
|
20
|
+
const hash = file => (existsSync(file) ? createHash('sha1').update(readFileSync(file)).digest('hex') : null)
|
|
21
|
+
const file = await buildPage('modeller-page.js')
|
|
22
|
+
const built = hash(file)
|
|
23
|
+
console.log(JSON.stringify({ file }))
|
|
24
|
+
process.stdin.on('data', () => console.log(JSON.stringify({ intact: hash(file) === built })))
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
/** Starts a run; `next()` gives the next line it says, as JSON. */
|
|
28
|
+
function start() {
|
|
29
|
+
const child = spawn(process.execPath, ['--input-type=module', '-e', RUN], { stdio: ['pipe', 'pipe', 'inherit'] })
|
|
30
|
+
const lines = []
|
|
31
|
+
const waiting = []
|
|
32
|
+
let rest = ''
|
|
33
|
+
child.stdout.on('data', chunk => {
|
|
34
|
+
rest += chunk
|
|
35
|
+
for (let at = rest.indexOf('\n'); at >= 0; at = rest.indexOf('\n')) {
|
|
36
|
+
const line = rest.slice(0, at)
|
|
37
|
+
rest = rest.slice(at + 1)
|
|
38
|
+
if (!line.startsWith('{')) continue
|
|
39
|
+
const said = JSON.parse(line)
|
|
40
|
+
waiting.length ? waiting.shift()(said) : lines.push(said)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
const ended = new Promise(done => child.on('exit', done))
|
|
44
|
+
/* A run that ends before it says what was asked has failed -- its build did not finish: said at once, not waited out. */
|
|
45
|
+
const quit = ended.then(code => {
|
|
46
|
+
throw Error(`the run ended (${code}) before it said what was asked`)
|
|
47
|
+
})
|
|
48
|
+
quit.catch(() => {})
|
|
49
|
+
return {
|
|
50
|
+
child,
|
|
51
|
+
ended,
|
|
52
|
+
next: () => (lines.length ? Promise.resolve(lines.shift()) : Promise.race([new Promise(done => waiting.push(done)), quit]))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
test('two runs at once build in folders of their own, keep each other\'s page, and take their folder with them', { timeout: 300000 }, async t => {
|
|
57
|
+
const a = start()
|
|
58
|
+
const b = start()
|
|
59
|
+
/* Whatever fails below, no run is left waiting: a run left behind keeps this file from ending. */
|
|
60
|
+
t.after(() => {
|
|
61
|
+
for (const run of [a, b]) if (run.child.exitCode === null && run.child.signalCode === null) run.child.kill('SIGKILL')
|
|
62
|
+
})
|
|
63
|
+
const [builtA, builtB] = await Promise.all([a.next(), b.next()])
|
|
64
|
+
assert.notEqual(dirname(builtA.file), dirname(builtB.file), 'each run builds in a folder of its own')
|
|
65
|
+
|
|
66
|
+
/* Both have built: each page is still the one its run built, whichever finished last. */
|
|
67
|
+
a.child.stdin.write('\n')
|
|
68
|
+
b.child.stdin.write('\n')
|
|
69
|
+
assert.deepEqual(await Promise.all([a.next(), b.next()]), [{ intact: true }, { intact: true }])
|
|
70
|
+
|
|
71
|
+
/* One ends by itself, the other is stopped: neither leaves its folder behind. */
|
|
72
|
+
a.child.stdin.end()
|
|
73
|
+
b.child.kill('SIGTERM')
|
|
74
|
+
await Promise.all([a.ended, b.ended])
|
|
75
|
+
assert.equal(existsSync(dirname(builtA.file)), false, 'the folder of the run that ended')
|
|
76
|
+
assert.equal(existsSync(dirname(builtB.file)), false, 'the folder of the run that was stopped')
|
|
77
|
+
})
|
package/test/browser/build.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (its browser bundle, `things-scene.mjs`).
|
|
8
8
|
*/
|
|
9
9
|
import { execFileSync } from 'node:child_process'
|
|
10
|
-
import {
|
|
10
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs'
|
|
11
11
|
import { tmpdir } from 'node:os'
|
|
12
12
|
import { dirname, join, resolve } from 'node:path'
|
|
13
13
|
import { fileURLToPath } from 'node:url'
|
|
@@ -20,15 +20,30 @@ const HERE = dirname(fileURLToPath(import.meta.url))
|
|
|
20
20
|
const PACKAGE = resolve(HERE, '../..')
|
|
21
21
|
const ROOT = resolve(PACKAGE, '../..')
|
|
22
22
|
|
|
23
|
+
/*
|
|
24
|
+
* Each build is made in a folder of its own and taken away when the process ends. A folder named by the page alone was
|
|
25
|
+
* removed and written again by every build of that page, so runs at the same time on one machine (sessions side by
|
|
26
|
+
* side, a server beside a check) could take away each other's page while it was being built or served.
|
|
27
|
+
*/
|
|
28
|
+
const made = []
|
|
29
|
+
const takeAway = () => {
|
|
30
|
+
for (const dir of made.splice(0)) rmSync(dir, { recursive: true, force: true })
|
|
31
|
+
}
|
|
32
|
+
process.on('exit', takeAway)
|
|
33
|
+
for (const signal of ['SIGINT', 'SIGTERM'])
|
|
34
|
+
process.once(signal, () => {
|
|
35
|
+
takeAway()
|
|
36
|
+
process.kill(process.pid, signal)
|
|
37
|
+
})
|
|
38
|
+
|
|
23
39
|
/*
|
|
24
40
|
* `standIns` maps a module the page would import to a file beside this one: for a module that cannot run here (the
|
|
25
41
|
* assistant dock needs the whole app's tool bar), a stand-in that records what the product asked of it.
|
|
26
42
|
*/
|
|
27
43
|
export async function buildPage(page = 'modeller-page.js', { standIns = {} } = {}) {
|
|
28
|
-
const out = join(tmpdir(), 'figure-ui-browser-' + page.replace(/\W+/g, '-'))
|
|
44
|
+
const out = mkdtempSync(join(tmpdir(), 'figure-ui-browser-' + page.replace(/\W+/g, '-') + '-'))
|
|
45
|
+
made.push(out)
|
|
29
46
|
const emitted = join(out, 'client')
|
|
30
|
-
rmSync(out, { recursive: true, force: true })
|
|
31
|
-
mkdirSync(out, { recursive: true })
|
|
32
47
|
|
|
33
48
|
/* Types are the unit suite's and tsc's business; here only what runs. */
|
|
34
49
|
execFileSync(join(ROOT, 'node_modules/.bin/tsc'), ['-p', join(PACKAGE, 'client/tsconfig.json'), '--outDir', emitted, '--noCheck', '--declaration', 'false', '--incremental', 'false'], {
|
|
@@ -80,4 +95,9 @@ export async function buildPage(page = 'modeller-page.js', { standIns = {} } = {
|
|
|
80
95
|
return file
|
|
81
96
|
}
|
|
82
97
|
|
|
83
|
-
|
|
98
|
+
/* Run on its own it prints where the page is, and leaves it there for whoever asked. */
|
|
99
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
100
|
+
const file = await buildPage()
|
|
101
|
+
made.length = 0
|
|
102
|
+
console.log(file)
|
|
103
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A new part's id, in a real browser, on a figure with a row: the palette's box, a copy from the parts list. A row's
|
|
3
|
+
* copies are drawn under ids of their own; the part made into the row still holds its id, and the next part is not
|
|
4
|
+
* offered it (on :3300, 2026-10-11, a box added after a row was refused with DUPLICATE_PART).
|
|
5
|
+
*
|
|
6
|
+
* Run: node --test test/browser/part-ids.check.mjs
|
|
7
|
+
*/
|
|
8
|
+
import { test, describe, before, after } from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
|
|
11
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
12
|
+
|
|
13
|
+
import { buildPage } from './build.mjs'
|
|
14
|
+
import { openPage } from './serve.mjs'
|
|
15
|
+
|
|
16
|
+
const sleep = ms => new Promise(done => setTimeout(done, ms))
|
|
17
|
+
|
|
18
|
+
let opened
|
|
19
|
+
before(async () => {
|
|
20
|
+
opened = await openPage(await buildPage('places-mesh-page.js'))
|
|
21
|
+
})
|
|
22
|
+
after(async () => {
|
|
23
|
+
await opened?.close()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const page = () => opened.page
|
|
27
|
+
const read = (fn, ...args) => page().evaluate(fn, ...args)
|
|
28
|
+
const state = () => read(() => probe.state())
|
|
29
|
+
/** The parts as authored, one per part as the parts list shows them. */
|
|
30
|
+
const parts = () => read(() => probe.source().appearance.map(look => look.target))
|
|
31
|
+
|
|
32
|
+
async function until(fn, args, what, ms = 8000) {
|
|
33
|
+
const deadline = Date.now() + ms
|
|
34
|
+
for (;;) {
|
|
35
|
+
if (await read(fn, ...args)) return
|
|
36
|
+
if (Date.now() > deadline) assert.fail(`waited for ${what}`)
|
|
37
|
+
await sleep(50)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const settled = () => read(() => document.querySelector('figure-editor').updateComplete)
|
|
41
|
+
async function click(selector, options = {}, what = selector) {
|
|
42
|
+
const element = await page().evaluateHandle((s, o) => findDeep(s, o), selector, options)
|
|
43
|
+
assert.ok(await element.evaluate(e => !!e), `${what} is on the page`)
|
|
44
|
+
await element.evaluate(e => e.scrollIntoView({ block: 'center' }))
|
|
45
|
+
await element.click()
|
|
46
|
+
await settled()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* box-1 on the floor, box-2 on it made into a row 300 apart: box-2's id is drawn nowhere as it is. */
|
|
50
|
+
function rowed() {
|
|
51
|
+
let a = addFigureBox(createFigureAsset('Rowed'), { id: 'box-1', size: { x: 1000, y: 100, z: 400 }, position: { x: 0, y: 50, z: 0 } })
|
|
52
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'box-1', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
53
|
+
a = addFigureBox(a, { id: 'box-2', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 150, z: 0 } })
|
|
54
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'box-2', face: 'bottom', to: { part: 'box-1', face: 'top' } })
|
|
55
|
+
return applyFigureAuthoring(a, { kind: 'make-repeat', part: 'box-2', axis: 'x', pitch: 300 })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('a new part on a figure with a row takes an id no part holds', () => {
|
|
59
|
+
before(async () => {
|
|
60
|
+
await read(text => harness.mount('given', text), serializeFigureAsset(rowed()))
|
|
61
|
+
await until(() => look.drawn().length > 0, [], 'the board is drawn')
|
|
62
|
+
assert.equal((await read(() => probe.ids())).includes('box-2'), false, 'the row is drawn under its copies\' ids')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('a box from the palette: added as box-3, one step, selected', async () => {
|
|
66
|
+
const start = await state()
|
|
67
|
+
await click('button[shape]', { has: 'figure.label.primitive-cube' }, 'the palette\'s box')
|
|
68
|
+
await until(() => probe.state().selected === 'box-3', [], 'box-3 selected')
|
|
69
|
+
const after = await state()
|
|
70
|
+
assert.equal(after.error, '')
|
|
71
|
+
assert.equal(after.revision, start.revision + 1)
|
|
72
|
+
assert.deepEqual(await parts(), ['box-1', 'box-2', 'box-3'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('the row\'s part copied from the list: the copy is a part, and it is what is selected', async () => {
|
|
76
|
+
const at = await read(() => probe.row('box-2'))
|
|
77
|
+
await page().mouse.click(at.x, at.y)
|
|
78
|
+
await until(() => probe.state().selected === 'box-2', [], 'box-2 selected')
|
|
79
|
+
const before = await parts()
|
|
80
|
+
await click('button[op="duplicate-part"]')
|
|
81
|
+
await until(n => probe.source().appearance.length === n, [before.length + 1], 'the copy')
|
|
82
|
+
const copy = (await parts()).find(id => !before.includes(id))
|
|
83
|
+
assert.equal((await state()).selected, copy)
|
|
84
|
+
assert.equal((await state()).error, '')
|
|
85
|
+
})
|
|
86
|
+
})
|