@things-factory/figure-ui 10.1.128 → 10.1.141
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/align.ts +24 -18
- package/client/modeller/editor.ts +165 -253
- package/client/modeller/fasten-geometry.ts +12 -9
- package/client/modeller/figure-facts.ts +12 -14
- package/client/modeller/figure-links.ts +16 -12
- package/client/modeller/figure-preview.ts +13 -4
- package/client/modeller/figure-properties.ts +6 -5
- package/client/modeller/figure-verdict.ts +14 -8
- package/client/modeller/joint-fan.ts +15 -17
- package/client/modeller/links.ts +17 -23
- package/client/modeller/live.ts +6 -3
- package/client/modeller/motion-ends.ts +6 -7
- package/client/modeller/occupancy.ts +2 -3
- package/client/modeller/product-session.ts +15 -0
- package/client/modeller/revision.ts +66 -0
- package/client/pages/figure-modeller-page.ts +5 -18
- package/dist-client/modeller/align.d.ts +12 -7
- package/dist-client/modeller/align.js +21 -17
- package/dist-client/modeller/align.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +8 -34
- package/dist-client/modeller/editor.js +175 -250
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +3 -2
- package/dist-client/modeller/fasten-geometry.js +11 -9
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-facts.d.ts +9 -1
- package/dist-client/modeller/figure-facts.js +11 -15
- package/dist-client/modeller/figure-facts.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +4 -5
- package/dist-client/modeller/figure-links.js +14 -13
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +5 -2
- package/dist-client/modeller/figure-preview.js +13 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +3 -2
- package/dist-client/modeller/figure-properties.js +6 -6
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-verdict.d.ts +4 -2
- package/dist-client/modeller/figure-verdict.js +12 -9
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/joint-fan.d.ts +8 -4
- package/dist-client/modeller/joint-fan.js +15 -16
- package/dist-client/modeller/joint-fan.js.map +1 -1
- package/dist-client/modeller/links.d.ts +6 -5
- package/dist-client/modeller/links.js +17 -24
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/live.d.ts +3 -2
- package/dist-client/modeller/live.js +7 -3
- package/dist-client/modeller/live.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +2 -2
- package/dist-client/modeller/motion-ends.js +5 -6
- package/dist-client/modeller/motion-ends.js.map +1 -1
- package/dist-client/modeller/occupancy.d.ts +2 -2
- package/dist-client/modeller/occupancy.js +2 -3
- package/dist-client/modeller/occupancy.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +44 -39
- package/dist-client/modeller/product-session.js +17 -0
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/revision.d.ts +37 -0
- package/dist-client/modeller/revision.js +39 -0
- package/dist-client/modeller/revision.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.d.ts +0 -6
- package/dist-client/pages/figure-modeller-page.js +6 -14
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/align.test.ts +14 -12
- package/test/browser/modeller-page.js +52 -4
- package/test/browser/modeller-revision.check.mjs +144 -0
- package/test/command-preview.test.ts +9 -8
- package/test/fasten-drawing.test.ts +1 -1
- package/test/fasten-geometry.test.ts +10 -9
- package/test/fields.test.ts +1 -1
- package/test/first-minute.test.ts +1 -1
- package/test/gizmo.test.ts +1 -1
- package/test/joint-fan.test.ts +30 -14
- package/test/joint-frames.test.ts +9 -8
- package/test/links.test.ts +13 -12
- package/test/live.test.ts +3 -3
- package/test/parts-wiring.test.ts +1 -1
- package/test/properties.test.ts +6 -5
- package/test/release-panel.test.ts +3 -3
- package/test/resize-with-placement.test.ts +8 -6
- package/test/revision-of.ts +22 -0
- package/test/revision.test.ts +172 -0
- package/test/section-editor.test.ts +4 -3
- package/test/section-panel.test.ts +5 -4
- package/test/verdict.test.ts +1 -1
- package/client/modeller/evaluated.ts +0 -47
- package/dist-client/modeller/evaluated.d.ts +0 -12
- package/dist-client/modeller/evaluated.js +0 -42
- package/dist-client/modeller/evaluated.js.map +0 -1
- package/dist-client/tsconfig.scratch.tsbuildinfo +0 -1
- package/test/evaluated.test.ts +0 -40
- package/test/mode-switch.test.ts +0 -43
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.141",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "npm run build:server && npm run build:client",
|
|
20
|
-
"test": "node --import ./test/resolve-ts-siblings.mjs --test --test-force-exit test/*.test.ts",
|
|
21
|
-
"test:browser": "node --test --test-force-exit test/browser/modeller.check.mjs",
|
|
20
|
+
"test": "FIGURE_FREEZE_COMPILED=1 node --import ./test/resolve-ts-siblings.mjs --test --test-force-exit test/*.test.ts",
|
|
21
|
+
"test:browser": "node --test --test-force-exit test/browser/modeller.check.mjs test/browser/modeller-revision.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,7 +27,7 @@
|
|
|
27
27
|
"clean": "npm run clean:server && npm run clean:client"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hatiolab/figure-model": "^0.
|
|
30
|
+
"@hatiolab/figure-model": "^0.5.0",
|
|
31
31
|
"@hatiolab/things-scene": "^10.1.74",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.0.0",
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"@operato/shell": "^10.0.0",
|
|
41
41
|
"@operato/styles": "^10.0.0",
|
|
42
42
|
"@operato/utils": "^10.0.0",
|
|
43
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
44
|
-
"@things-factory/figure-service": "^10.1.
|
|
43
|
+
"@things-factory/ai-assistant": "^10.1.141",
|
|
44
|
+
"@things-factory/figure-service": "^10.1.141",
|
|
45
45
|
"three": "^0.185.1"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "931866a13b9149be42a294f207e245aba11e57f8"
|
|
48
48
|
}
|
package/test/align.test.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { addFigureBox, createFigureAsset, editFigurePart } from '@hatiolab/figur
|
|
|
13
13
|
|
|
14
14
|
import { alignChanges, boxOf, canMirror, gridStepOf, mirrorChanges, niceStep } from '../client/modeller/align.ts'
|
|
15
15
|
import { gizmoReachOf } from '../client/modeller/gizmo.ts'
|
|
16
|
+
import { revisionOf } from './revision-of.ts'
|
|
16
17
|
|
|
17
18
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
18
19
|
const probe = (at = { x: 130, y: 90, z: -70 }, rotation?: { x: number; y: number; z: number }) => {
|
|
@@ -26,11 +27,12 @@ const applied = (asset: ReturnType<typeof probe>, changes: ReturnType<typeof ali
|
|
|
26
27
|
return next
|
|
27
28
|
}
|
|
28
29
|
const run = (asset: ReturnType<typeof probe>, tool: Parameters<typeof alignChanges>[3]) =>
|
|
29
|
-
applied(asset, alignChanges(asset, 'body', gizmoReachOf(asset as never, 'body'), tool))
|
|
30
|
+
applied(asset, alignChanges(revisionOf(asset), 'body', gizmoReachOf(asset as never, 'body'), tool))
|
|
31
|
+
const box = (asset: ReturnType<typeof probe>, part: string) => boxOf(revisionOf(asset), part)
|
|
30
32
|
|
|
31
33
|
test('가운데 X · Z 는 부품 상자의 가운데를 0 에 둔다', () => {
|
|
32
|
-
assert.equal(Math.round(
|
|
33
|
-
assert.equal(Math.round(
|
|
34
|
+
assert.equal(Math.round(box(run(probe(), 'centre-x'), 'body')!.centre.x), 0)
|
|
35
|
+
assert.equal(Math.round(box(run(probe(), 'centre-z'), 'body')!.centre.z), 0)
|
|
34
36
|
})
|
|
35
37
|
|
|
36
38
|
/*
|
|
@@ -40,13 +42,13 @@ test('가운데 X · Z 는 부품 상자의 가운데를 0 에 둔다', () => {
|
|
|
40
42
|
test('기준면 맞추기 — 배치를 따르고, 한 번 옮길 뿐 붙이지 않는다', async () => {
|
|
41
43
|
const { figurePlaneAlignChanges, applyFigureAuthoring } = await import('@hatiolab/figure-model')
|
|
42
44
|
const floor = probe()
|
|
43
|
-
const flat =
|
|
45
|
+
const flat = box(applied(floor, figurePlaneAlignChanges(floor, 'body').changes), 'body')!
|
|
44
46
|
assert.equal(Math.round(flat.centre.y - flat.extent.y / 2), 0, '바닥: 아랫면이 y 0')
|
|
45
47
|
const turned = probe({ x: 0, y: 300, z: 0 }, { x: 0, y: 0, z: 45 })
|
|
46
|
-
const landed =
|
|
48
|
+
const landed = box(applied(turned, figurePlaneAlignChanges(turned, 'body').changes), 'body')!
|
|
47
49
|
assert.ok(Math.abs(landed.centre.y - landed.extent.y / 2) <= 1, `45° 돈 부품의 모서리가 바닥에 닿는다 (${landed.centre.y - landed.extent.y / 2})`)
|
|
48
50
|
const hung = applyFigureAuthoring(probe({ x: 0, y: -300, z: 0 }), { kind: 'set-placement', placement: 'ceiling' })
|
|
49
|
-
const top =
|
|
51
|
+
const top = box(applied(hung, figurePlaneAlignChanges(hung, 'body').changes), 'body')!
|
|
50
52
|
assert.equal(Math.round(top.centre.y + top.extent.y / 2), 0, '천장: 윗면이 y 0')
|
|
51
53
|
const editor = read('../client/modeller/editor.ts')
|
|
52
54
|
assert.ok(editor.includes('const { changes, contact, refusal } = figurePlaneAlignChanges(asset, this.selected)'))
|
|
@@ -55,7 +57,7 @@ test('기준면 맞추기 — 배치를 따르고, 한 번 옮길 뿐 붙이지
|
|
|
55
57
|
|
|
56
58
|
test('눈금은 1 · 2 · 5 간격으로 자리와 크기를 맞추고, 크기를 0 으로 만들지 않는다', () => {
|
|
57
59
|
const asset = probe({ x: 133, y: 47, z: -71 })
|
|
58
|
-
const step = gridStepOf(asset)
|
|
60
|
+
const step = gridStepOf(revisionOf(asset))
|
|
59
61
|
assert.equal(step, 50, '가장 긴 폭 200 을 다섯 칸으로 — 40 이 아니라 50')
|
|
60
62
|
const snapped = run(asset, 'snap')
|
|
61
63
|
assert.deepEqual(['x', 'y', 'z'].map(a => snapped.designInputs[`body.position.${a}`]), [150, 50, -50])
|
|
@@ -65,14 +67,14 @@ test('눈금은 1 · 2 · 5 간격으로 자리와 크기를 맞추고, 크기
|
|
|
65
67
|
test('대칭은 복제본을 가운데 면 반대편 같은 거리에 둔다 · 가운데에 있으면 내지 않는다', () => {
|
|
66
68
|
const asset = probe()
|
|
67
69
|
const copied = editFigurePart(asset, { kind: 'duplicate-part', target: 'body' })
|
|
68
|
-
const mirrored = applied(copied, mirrorChanges(copied, 'body', 'body-copy', 'x'))
|
|
69
|
-
assert.equal(Math.round(
|
|
70
|
-
assert.equal(canMirror(probe({ x: 0, y: 50, z: 0 })
|
|
70
|
+
const mirrored = applied(copied, mirrorChanges(revisionOf(copied), 'body', 'body-copy', 'x'))
|
|
71
|
+
assert.equal(Math.round(box(mirrored, 'body-copy')!.centre.x), -Math.round(box(mirrored, 'body')!.centre.x))
|
|
72
|
+
assert.equal(canMirror(revisionOf(probe({ x: 0, y: 50, z: 0 })), 'body', 'x', gizmoReachOf(probe() as never, 'body')), false)
|
|
71
73
|
})
|
|
72
74
|
|
|
73
75
|
test('붙인 부품처럼 옮길 수 없는 것은 맞추기도 없다', () => {
|
|
74
|
-
assert.deepEqual(alignChanges(probe(), 'body', { translate: false, rotate: false, scale: false }, 'centre-x'), [])
|
|
75
|
-
assert.match(read('../client/modeller/editor.ts'), /if \(!this\.selected \|\| !reach\.translate\) return null/)
|
|
76
|
+
assert.deepEqual(alignChanges(revisionOf(probe()), 'body', { translate: false, rotate: false, scale: false }, 'centre-x'), [])
|
|
77
|
+
assert.match(read('../client/modeller/editor.ts'), /if \(!this\.selected \|\| !reach\.translate \|\| !shown\) return null/)
|
|
76
78
|
})
|
|
77
79
|
|
|
78
80
|
test('the grid step reads well -- 27 or 33 would make coordinates unreadable -- and is never 0', () => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import 'figure-ui-client/modeller/editor.js'
|
|
7
7
|
import { budgetOf, figureReportOf } from 'figure-ui-client/modeller/maturity.js'
|
|
8
8
|
import { createFigureProductSession } from 'figure-ui-client/modeller/product-session.js'
|
|
9
|
-
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset,
|
|
9
|
+
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset, figureWork, robotArmFigure, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
10
10
|
|
|
11
11
|
const PALETTE = {
|
|
12
12
|
'palette.dark': '#2b2b2b',
|
|
@@ -234,6 +234,49 @@ window.probe = {
|
|
|
234
234
|
fan() {
|
|
235
235
|
return canvas().scene.root.components.some(c => c.state.name === '__joint-fan')
|
|
236
236
|
},
|
|
237
|
+
trace() {
|
|
238
|
+
const stacks = (window.__stacks = [])
|
|
239
|
+
for (const key of ['compiles', 'evaluations']) {
|
|
240
|
+
let v = figureWork[key]
|
|
241
|
+
Object.defineProperty(figureWork, key, { configurable: true, get: () => v, set: n => { v = n; stacks.push(key + ' ' + new Error().stack.split('\n').slice(2, 9).map(l => l.trim().replace(/\(http.*?page\.js/, '(').replace(/^at /, '')).join(' < ')) } })
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
traced() {
|
|
245
|
+
return window.__stacks
|
|
246
|
+
},
|
|
247
|
+
/** Whole-figure work done so far (figure-model's count): compiles and evaluations. */
|
|
248
|
+
work() {
|
|
249
|
+
return { ...figureWork }
|
|
250
|
+
},
|
|
251
|
+
/**
|
|
252
|
+
* The two middles -- the edit canvas and the preview -- marked, so a later look says whether each is the same element
|
|
253
|
+
* with the same scene, or was made again.
|
|
254
|
+
*/
|
|
255
|
+
markMiddles() {
|
|
256
|
+
const preview = deep('figure-preview')[0]
|
|
257
|
+
for (const one of [canvas(), canvas()?.scene, preview, preview?.scene]) if (one) one.__mark = true
|
|
258
|
+
},
|
|
259
|
+
middles() {
|
|
260
|
+
const preview = deep('figure-preview')[0]
|
|
261
|
+
const area = deep('div[middle]')[0]
|
|
262
|
+
const size = canvas()?.scene?.root?._threeCapability?.rendererManager?.domElement
|
|
263
|
+
return {
|
|
264
|
+
canvas: !!canvas()?.__mark,
|
|
265
|
+
canvasScene: !!canvas()?.scene?.__mark,
|
|
266
|
+
editHidden: !!area?.hidden && getComputedStyle(area).display === 'none',
|
|
267
|
+
editSize: size ? [size.width, size.height] : null,
|
|
268
|
+
preview: preview ? { marked: !!preview.__mark, scene: !!preview.scene, sceneMarked: !!preview.scene?.__mark, hidden: preview.hidden, stale: preview.stale } : null
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
/** Whether the edit canvas gives the card a picture now. */
|
|
272
|
+
async editPicture() {
|
|
273
|
+
return !!(await canvas().snapshot())
|
|
274
|
+
},
|
|
275
|
+
/** The page point of the edit / preview switch. */
|
|
276
|
+
modeButton(mode) {
|
|
277
|
+
const b = deep('button[mode]').find(b => b.title === `figure.text.mode-${mode}-explained`)
|
|
278
|
+
return b ? centreOf(b) : null
|
|
279
|
+
},
|
|
237
280
|
/** The drawn position of a part's component, as the canvas shows it now. */
|
|
238
281
|
drawn(id) {
|
|
239
282
|
const c = canvas().scene.root.components.find(c => c.state.name === id)
|
|
@@ -251,16 +294,21 @@ window.harness = {
|
|
|
251
294
|
await session.open('probe')
|
|
252
295
|
const editor = document.createElement('figure-editor')
|
|
253
296
|
editor.session = session
|
|
254
|
-
/* The cost line as the modeller page gives it: the maturity report of
|
|
297
|
+
/* The cost line as the modeller page gives it: the maturity report of the version on screen (figure-verdict). */
|
|
255
298
|
const report = () => {
|
|
256
299
|
try {
|
|
257
|
-
return budgetOf(figureReportOf(
|
|
300
|
+
return budgetOf(figureReportOf(session.revisionNow().asset))
|
|
258
301
|
} catch {
|
|
259
302
|
return undefined
|
|
260
303
|
}
|
|
261
304
|
}
|
|
262
305
|
editor.budget = report()
|
|
263
|
-
|
|
306
|
+
/* Measured once the figure has settled, as the page's verdict measures it. */
|
|
307
|
+
let settle
|
|
308
|
+
editor.addEventListener('changed', () => {
|
|
309
|
+
clearTimeout(settle)
|
|
310
|
+
settle = setTimeout(() => (editor.budget = report()), 200)
|
|
311
|
+
})
|
|
264
312
|
editor.templates = templates
|
|
265
313
|
editor.guideOf = async name => guides[name] ?? ''
|
|
266
314
|
editor.revision = 0
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The modeller in a real browser, counted: what a selection, a value typed and going to the preview and back cost in
|
|
3
|
+
* whole-figure work (figure-model's compiles and evaluations), and that the two middles -- the edit canvas and the
|
|
4
|
+
* preview -- stay standing rather than being made again. A count is the same on every machine; the milliseconds are
|
|
5
|
+
* measured on the app (figure-model docs/modeller-interaction-performance-2026-10-07.md).
|
|
6
|
+
*
|
|
7
|
+
* Run: npm run test:browser
|
|
8
|
+
* FIGURE_MODEL=<a figure-model build> npm run test:browser (a figure-model not published yet)
|
|
9
|
+
*/
|
|
10
|
+
import { test, describe, before, after } from 'node:test'
|
|
11
|
+
import assert from 'node:assert/strict'
|
|
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
|
+
/* The event handled and the render it asked for done -- not the work told to wait until the screen rests (150 ms). */
|
|
18
|
+
const drawn = () => page().evaluate(() => document.querySelector('figure-editor').updateComplete.then(() => probe.work()))
|
|
19
|
+
|
|
20
|
+
let opened
|
|
21
|
+
before(async () => {
|
|
22
|
+
opened = await openPage(await buildPage())
|
|
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 work = () => read(() => probe.work())
|
|
31
|
+
const since = (a, b) => ({ compiles: b.compiles - a.compiles, evaluations: b.evaluations - a.evaluations })
|
|
32
|
+
|
|
33
|
+
/* Nothing still being worked out from before: the count stands still for half a second. */
|
|
34
|
+
async function settled() {
|
|
35
|
+
for (let last = await work(), i = 0; i < 20; i++) {
|
|
36
|
+
await sleep(500)
|
|
37
|
+
const now = await work()
|
|
38
|
+
if (now.compiles === last.compiles && now.evaluations === last.evaluations) return
|
|
39
|
+
last = now
|
|
40
|
+
}
|
|
41
|
+
assert.fail('the modeller kept working')
|
|
42
|
+
}
|
|
43
|
+
async function mount(kind) {
|
|
44
|
+
await read(kind => harness.mount(kind, null), kind)
|
|
45
|
+
await sleep(2500)
|
|
46
|
+
await settled()
|
|
47
|
+
}
|
|
48
|
+
async function click(point) {
|
|
49
|
+
assert.ok(point, 'nothing on the page to click')
|
|
50
|
+
await page().mouse.click(point.x, point.y)
|
|
51
|
+
}
|
|
52
|
+
async function mode(which) {
|
|
53
|
+
await click(await read(m => probe.modeButton(m), which))
|
|
54
|
+
await sleep(1500)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('work counted on the robot arm', () => {
|
|
58
|
+
before(() => mount('arm'))
|
|
59
|
+
|
|
60
|
+
/* The motion tab is drawn out of sight with the rest: the selected part's motion is evaluated at its two ends. */
|
|
61
|
+
test('picking a part on the canvas compiles nothing; it evaluates only the two ends of the part\'s motion', async () => {
|
|
62
|
+
const part = 'upper-arm'
|
|
63
|
+
const before = await work()
|
|
64
|
+
await click(await read(id => probe.partOnPage(id), part))
|
|
65
|
+
const spent = since(before, await drawn())
|
|
66
|
+
assert.equal((await read(() => probe.state())).selected, part)
|
|
67
|
+
assert.deepEqual(spent, { compiles: 0, evaluations: 2 })
|
|
68
|
+
/*
|
|
69
|
+
Once the screen rests, two commands are seen on copies (ADR-0097 §5.5): taking the part off (its step without the
|
|
70
|
+
joint, and its result) and the fastening the form opens on (its result). Each compiles only what it makes; the
|
|
71
|
+
figure as it stands is not compiled again.
|
|
72
|
+
*/
|
|
73
|
+
await settled()
|
|
74
|
+
const rested = since(before, await work())
|
|
75
|
+
assert.ok(rested.compiles <= 3, JSON.stringify(rested))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
/* The edit and its read are the kernel's two compiles (its version's asset is the edit's own); then the part's motion ends. */
|
|
79
|
+
test('a value typed into its field: the edit and its read, and nothing else while it is drawn', async () => {
|
|
80
|
+
const field = await read(() => {
|
|
81
|
+
const deep = (root = document) => [...root.querySelectorAll('input[type=number]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
82
|
+
const input = deep().find(i => !i.disabled && /\.size\.|\.dimension\./.test(i.getAttribute('aria-label') ?? ''))
|
|
83
|
+
if (!input) return null
|
|
84
|
+
input.scrollIntoView({ block: 'center' })
|
|
85
|
+
const r = input.getBoundingClientRect()
|
|
86
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2, label: input.getAttribute('aria-label'), value: Number(input.value) }
|
|
87
|
+
})
|
|
88
|
+
assert.ok(field, 'a size field of the part')
|
|
89
|
+
const revision = (await read(() => probe.state())).revision
|
|
90
|
+
await page().mouse.click(field.x, field.y, { clickCount: 3 })
|
|
91
|
+
await page().keyboard.type(String(field.value + 10))
|
|
92
|
+
const before = await work()
|
|
93
|
+
await page().keyboard.press('Enter')
|
|
94
|
+
const spent = since(before, await drawn())
|
|
95
|
+
assert.equal((await read(() => probe.state())).revision, revision + 1, `${field.label} is one step`)
|
|
96
|
+
assert.ok(spent.compiles <= 2 && spent.evaluations <= 5, JSON.stringify(spent))
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('the edit canvas and the preview stay standing', () => {
|
|
101
|
+
before(() => mount('box'))
|
|
102
|
+
|
|
103
|
+
test('the preview is made when first asked for; going back, the edit canvas is the one that was there', async () => {
|
|
104
|
+
assert.equal((await read(() => probe.middles())).preview, null, 'no preview before it is asked for')
|
|
105
|
+
await read(() => probe.markMiddles())
|
|
106
|
+
await mode('preview')
|
|
107
|
+
let m = await read(() => probe.middles())
|
|
108
|
+
assert.ok(m.preview?.scene && !m.preview.hidden, 'the preview stands in front')
|
|
109
|
+
assert.ok(m.editHidden, 'the edit area is out of sight, not taken down')
|
|
110
|
+
assert.ok(m.editSize && m.editSize[0] > 0 && m.editSize[1] > 0, 'and not resized to nothing')
|
|
111
|
+
assert.equal(await read(() => probe.editPicture()), false, 'out of sight it takes no picture, as when there was no canvas')
|
|
112
|
+
await read(() => probe.markMiddles())
|
|
113
|
+
await mode('edit')
|
|
114
|
+
m = await read(() => probe.middles())
|
|
115
|
+
assert.ok(m.canvas && m.canvasScene, 'the same edit canvas, the same scene')
|
|
116
|
+
assert.ok(m.preview?.marked && m.preview.hidden, 'the preview stays, out of sight')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('shown again unchanged, the preview is the one that stood, and nothing is worked out', async () => {
|
|
120
|
+
const before = await work()
|
|
121
|
+
await mode('preview')
|
|
122
|
+
const m = await read(() => probe.middles())
|
|
123
|
+
assert.ok(m.preview?.marked && m.preview.sceneMarked, 'the same preview and scene')
|
|
124
|
+
assert.deepEqual(since(before, await work()), { compiles: 0, evaluations: 0 })
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('a value changed beside the preview is drawn inside the scene that stands', async () => {
|
|
128
|
+
await click(await read(() => probe.row('box')))
|
|
129
|
+
await sleep(300)
|
|
130
|
+
const field = await read(() => probe.labelled('box.size.y'))
|
|
131
|
+
assert.ok(field, 'the height field')
|
|
132
|
+
await page().mouse.click(field.x, field.y, { clickCount: 3 })
|
|
133
|
+
await page().keyboard.type('600')
|
|
134
|
+
await page().keyboard.press('Enter')
|
|
135
|
+
await sleep(1500)
|
|
136
|
+
const m = await read(() => probe.middles())
|
|
137
|
+
assert.ok(m.preview?.sceneMarked, 'the same 3D scene')
|
|
138
|
+
assert.equal(await read(() => probe.value('box.size.y')), 600)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('the page raised no error', () => {
|
|
143
|
+
assert.deepEqual(opened.errors, [])
|
|
144
|
+
})
|
|
@@ -12,6 +12,7 @@ import { addFigureBox, applyFigureAuthoring, createFigureAsset, figurePartFaceOf
|
|
|
12
12
|
import { createFigureProductSession } from '../client/modeller/product-session.js'
|
|
13
13
|
import { faceHighlightOf, ghostPartsOf, previewWordsOf, type HeldPreview } from '../client/modeller/command-preview.js'
|
|
14
14
|
import { boxShape } from '../client/modeller/drawn-shapes.ts'
|
|
15
|
+
import { revisionOf } from './revision-of.ts'
|
|
15
16
|
|
|
16
17
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
17
18
|
const ko = JSON.parse(read('../translations/ko.json'))
|
|
@@ -118,7 +119,7 @@ test('detaching says what it would do: stays, what stays fastened to it, the cli
|
|
|
118
119
|
assert.equal(seen.moves.length, 0, 'the lamp stays where it stands')
|
|
119
120
|
assert.equal(s.read().revision, 0)
|
|
120
121
|
const editor = read('../client/modeller/editor.ts')
|
|
121
|
-
assert.ok(editor.includes("const own = seen.moves.some(m => m.body ===
|
|
122
|
+
assert.ok(editor.includes("const own = seen.moves.some(m => m.body === part)"))
|
|
122
123
|
/* Staying where it is is as expected: nothing is said then (user 2026-10-01); a move, a child kept, a clip lost are. */
|
|
123
124
|
assert.ok(editor.includes(': /* It stays where it is: as expected, so nothing is said (user 2026-10-01). */ []'), 'nothing said when it stays')
|
|
124
125
|
const links = read('../client/modeller/figure-links.ts')
|
|
@@ -161,8 +162,8 @@ test('a row is seen before it is made, read as figure-model draws it -- and said
|
|
|
161
162
|
assert.ok(links.includes("<section row><details row-fold>") && links.includes("<details row-fold ?open=${this.working === 'row'}>"))
|
|
162
163
|
assert.ok(links.includes("this.candidate = this.working === 'row' && this.rowCandidate ? { action: this.rowCandidate } : this.fastenCandidate"), 'the form being worked on is the one seen')
|
|
163
164
|
const editor = read('../client/modeller/editor.ts')
|
|
164
|
-
assert.ok(editor.includes("const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, this.selected, this.
|
|
165
|
-
assert.ok(editor.includes('<ul row-reading>${rowWordsOf(this.rowNow(
|
|
165
|
+
assert.ok(editor.includes("const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, this.selected, this.shown()!.inputs) : undefined"))
|
|
166
|
+
assert.ok(editor.includes('<ul row-reading>${rowWordsOf(this.rowNow(shown)'), 'a row made says the same, as it stands')
|
|
166
167
|
for (const lang of ['en', 'ja', 'ko', 'ms', 'zh']) {
|
|
167
168
|
const t = JSON.parse(read(`../translations/${lang}.json`))
|
|
168
169
|
for (const key of ['row-count-pitch', 'row-count-uneven', 'row-ends-centred', 'row-ends', 'row-ends-unknown', 'row-companions', 'row-count-spreads', 'row-blocked-unfastened', 'row-blocked-on-plane', 'row-blocked-holds-parts'])
|
|
@@ -210,15 +211,15 @@ test('a motion is named by its parts, and drawn at both ends of its range -- onl
|
|
|
210
211
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', face: 'bottom', to: { part: 'frame', face: 'top' } })
|
|
211
212
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'gate', face: 'bottom', to: { part: 'frame', face: 'top' } })
|
|
212
213
|
const j = (part: string) => a.joints!.find(k => k.body1.part === part)!
|
|
213
|
-
assert.equal(motionEndsOf(a, 'leaf'), null, 'not moving: nothing drawn')
|
|
214
|
+
assert.equal(motionEndsOf(revisionOf(a), 'leaf'), null, 'not moving: nothing drawn')
|
|
214
215
|
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j('leaf'), type: 'revolute', axis: 'Y', lowerLimit: 0, upperLimit: 90, state: { id: 'open' } } })
|
|
215
216
|
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j('gate'), type: 'prismatic', axis: 'X', lowerLimit: 0, upperLimit: 300, state: { id: 'slide' } } })
|
|
216
|
-
/*
|
|
217
|
-
const held =
|
|
218
|
-
const turn = motionEndsOf(
|
|
217
|
+
/* At the version the screen holds, a design value typed since it was opened. */
|
|
218
|
+
const held = revisionOf(a, { 'frame.size.x': 2400 })
|
|
219
|
+
const turn = motionEndsOf(held, 'leaf')!
|
|
219
220
|
assert.deepEqual([turn.control, turn.min, turn.max], ['open', 0, 90])
|
|
220
221
|
assert.deepEqual(turn.ghost.map(g => g.id).sort(), ['__preview:max:leaf', '__preview:min:leaf'], 'the leaf at 0 and at 90, nothing else')
|
|
221
|
-
const slide = motionEndsOf(
|
|
222
|
+
const slide = motionEndsOf(held, 'gate')!
|
|
222
223
|
const line = slide.ghost.find(g => g.id === '__target-face:slide')!
|
|
223
224
|
assert.equal(Math.round((line.shape as unknown as { extrusion: { section: { size: { x: number } } } }).extrusion.section.size.x), 300, 'the line it travels, its whole stroke')
|
|
224
225
|
const editor = read('../client/modeller/editor.ts')
|
|
@@ -21,7 +21,7 @@ test('부품의 면·맞닿기 고르기 칸이 없고, 그 자리에 그림이
|
|
|
21
21
|
assert.ok(!form.includes("i18next.t('figure.label.facing')"), 'no facing list')
|
|
22
22
|
has(form, /<figure-fasten-drawing[\s\S]*?\.modes=\$\{f\.toPlane \? \(\['meet'\] as FastenMode\[\]\)/, 'the plane is set on only')
|
|
23
23
|
has(form, /turnsInPlace\(asset, this\.partId\) \? \(\['centre'\] as FastenMode\[\]\) : \[\]/, 'centre on it only for a part that turns')
|
|
24
|
-
has(form, /const derived = f\.toPlane \|\| f\.target \? myFaceFor\(
|
|
24
|
+
has(form, /const derived = f\.toPlane \|\| f\.target \? myFaceFor\(this\.revision!, this\.partId, aim, mode\) : undefined/, 'the part\'s face follows the target and how')
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
test('상대 목록에 배치 기준의 면이 이름으로 있다: 바닥 · 천장 · 벽 · 중심', () => {
|
|
@@ -12,6 +12,7 @@ import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAss
|
|
|
12
12
|
import type { FigureAsset, FigureAuthoringAction } from '@hatiolab/figure-model'
|
|
13
13
|
|
|
14
14
|
import { alignmentAt, faceViewOf, myFaceFor, offsetOf, overhangOf, screenOf } from '../client/modeller/fasten-geometry.ts'
|
|
15
|
+
import { revisionOf } from './revision-of.ts'
|
|
15
16
|
|
|
16
17
|
const scene = (lidRotation?: { x: number; y: number; z: number }): FigureAsset => {
|
|
17
18
|
let a = addFigureBox(createFigureAsset('Box'), { id: 'bin', size: { x: 600, y: 300, z: 400 }, position: { x: 0, y: 150, z: 0 } })
|
|
@@ -22,24 +23,24 @@ const at = (a: FigureAsset, id: string) => (compileFigureAsset(a).evaluate().geo
|
|
|
22
23
|
|
|
23
24
|
test('부품의 면은 상대 면과 방식으로 정해진다: 올려놓기는 마주 보는 면, 같은 면은 같은 면, 가운데는 가운데', () => {
|
|
24
25
|
const a = scene()
|
|
25
|
-
assert.equal(myFaceFor(a, 'lid', { part: 'bin', face: 'top' }, 'meet'), 'bottom')
|
|
26
|
-
assert.equal(myFaceFor(a, 'lid', { part: 'bin', face: 'top' }, 'flush'), 'top')
|
|
27
|
-
assert.equal(myFaceFor(a, 'lid', { part: 'bin', face: 'front' }, 'meet'), 'back')
|
|
28
|
-
assert.equal(myFaceFor(a, 'lid', { part: 'bin', face: 'top' }, 'centre'), 'centre')
|
|
29
|
-
assert.equal(myFaceFor(a, 'lid', 'plane', 'meet'), 'bottom', 'on a floor: the face looking down')
|
|
30
|
-
assert.equal(myFaceFor(a, 'lid', 'plane', 'centre'), undefined, 'the plane takes a face, never a centre')
|
|
26
|
+
assert.equal(myFaceFor(revisionOf(a), 'lid', { part: 'bin', face: 'top' }, 'meet'), 'bottom')
|
|
27
|
+
assert.equal(myFaceFor(revisionOf(a), 'lid', { part: 'bin', face: 'top' }, 'flush'), 'top')
|
|
28
|
+
assert.equal(myFaceFor(revisionOf(a), 'lid', { part: 'bin', face: 'front' }, 'meet'), 'back')
|
|
29
|
+
assert.equal(myFaceFor(revisionOf(a), 'lid', { part: 'bin', face: 'top' }, 'centre'), 'centre')
|
|
30
|
+
assert.equal(myFaceFor(revisionOf(a), 'lid', 'plane', 'meet'), 'bottom', 'on a floor: the face looking down')
|
|
31
|
+
assert.equal(myFaceFor(revisionOf(a), 'lid', 'plane', 'centre'), undefined, 'the plane takes a face, never a centre')
|
|
31
32
|
})
|
|
32
33
|
|
|
33
34
|
test('돌려 놓인 부품이면 지금 아래를 보는 면이다 — 이름이 아니라 방향으로', () => {
|
|
34
35
|
const a = scene({ x: 90, y: 0, z: 0 })
|
|
35
|
-
const face = myFaceFor(a, 'lid', { part: 'bin', face: 'top' }, 'meet')!
|
|
36
|
+
const face = myFaceFor(revisionOf(a), 'lid', { part: 'bin', face: 'top' }, 'meet')!
|
|
36
37
|
assert.notEqual(face, 'bottom', 'turned a quarter about X, its bottom no longer looks down')
|
|
37
38
|
const n = figurePartFaceOf(a, 'lid', face)!.normal
|
|
38
39
|
assert.ok(Math.abs(n[1]! + 1) < 1e-6, `${face} looks down: ${n}`)
|
|
39
40
|
})
|
|
40
41
|
|
|
41
42
|
test('면 그림: 상대 면의 크기와 부품 발자국, 가장자리 맞춤이 되는 축', () => {
|
|
42
|
-
const v = faceViewOf(scene(), 'lid', { part: 'bin', face: 'top' })!
|
|
43
|
+
const v = faceViewOf(revisionOf(scene()), 'lid', { part: 'bin', face: 'top' })!
|
|
43
44
|
assert.deepEqual(v.axes, ['x', 'z'])
|
|
44
45
|
assert.deepEqual([v.face.x, v.face.z], [600, 400])
|
|
45
46
|
assert.deepEqual([v.part.x, v.part.z], [300, 200])
|
|
@@ -57,7 +58,7 @@ test('놓은 자리: 가운데, 안쪽 끝(가장자리와 나란히), 바깥
|
|
|
57
58
|
})
|
|
58
59
|
|
|
59
60
|
test('면 밖으로 나온 곳과 닿지 않는 것을 말한다', () => {
|
|
60
|
-
const v = faceViewOf(scene(), 'lid', { part: 'bin', face: 'top' })!
|
|
61
|
+
const v = faceViewOf(revisionOf(scene()), 'lid', { part: 'bin', face: 'top' })!
|
|
61
62
|
assert.deepEqual(overhangOf(v, { x: 0, y: 0, z: 0 }), { touching: true, sides: [] })
|
|
62
63
|
assert.deepEqual(overhangOf(v, { x: 200, y: 0, z: 0 }), { touching: true, sides: [{ axis: 'x', side: 'high', mm: 50 }] })
|
|
63
64
|
assert.equal(overhangOf(v, { x: 460, y: 0, z: 0 }).touching, false)
|
package/test/fields.test.ts
CHANGED
|
@@ -83,7 +83,7 @@ test('편집기는 저작한 부품 단위로 고르고 보인다 — 되풀이
|
|
|
83
83
|
assert.ok(canvas.includes('.filter(([name]) => ownerOf(name) === this.selected)'), '사용자 2026-09-24: 「why repeatable parts are not selected?」')
|
|
84
84
|
const editor = read('../client/modeller/editor.ts')
|
|
85
85
|
assert.ok(editor.includes('this.selected = id ? ownerOf(id) : id'), '캔버스에서 복사본을 누르면 그 되풀이를 고른다')
|
|
86
|
-
assert.ok(editor.includes('.parts=${this.partRows(view.geometry, asset
|
|
86
|
+
assert.ok(editor.includes('.parts=${this.partRows(view.geometry, asset)}'), '목록은 저작한 부품 한 줄씩')
|
|
87
87
|
assert.ok(editor.includes("this.session!.command({ kind: 'set-repeat', part: this.selected, pitch })"), 'pitch 는 명령으로 정한다')
|
|
88
88
|
const parts = read('../client/modeller/figure-parts.ts')
|
|
89
89
|
assert.ok(parts.includes('?disabled=${this.locked || full}'), '등급 한도에 이르면 팔레트가 잠긴다')
|
|
@@ -67,7 +67,7 @@ test('narrow, the parts list folds for the canvas, said once, and stays open onc
|
|
|
67
67
|
|
|
68
68
|
test('a palette part\'s own sizes are its shape\'s fields, not a rule\'s: no "set by a rule" size row for them', () => {
|
|
69
69
|
const editor = read('../client/modeller/editor.ts')
|
|
70
|
-
assert.ok(editor.includes("sizeRuled: (['x', 'y', 'z'] as const).every(axis => followOf(
|
|
70
|
+
assert.ok(editor.includes("sizeRuled: (['x', 'y', 'z'] as const).every(axis => followOf(shown.asset, ownerOf(this.selected), axis).of !== 'own')"))
|
|
71
71
|
assert.ok(editor.includes('if (!held?.sizeRuled ||'))
|
|
72
72
|
})
|
|
73
73
|
|
package/test/gizmo.test.ts
CHANGED
|
@@ -168,7 +168,7 @@ test('크기 칸과 손잡이 — 배치가 지키는 면을 지킨다, 커널
|
|
|
168
168
|
hung = applyFigureAuthoring(hung, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(hung), placement: 'ceiling' })
|
|
169
169
|
assert.equal(centreY(grow(hung, 'lamp', gizmoReachOf(hung, 'lamp').sizes![1], 300), 'lamp') + 300 / 2, 0, '천장 도형: 윗면이 천장에')
|
|
170
170
|
const editor = read('../client/modeller/editor.ts')
|
|
171
|
-
assert.ok(editor.includes('figureSizeEditChanges(this.
|
|
171
|
+
assert.ok(editor.includes('figureSizeEditChanges(this.shown()!.asset, part, [change])'), '숫자 칸이 부른다')
|
|
172
172
|
assert.ok(editor.includes('figureSizeEditChanges(asset, ownerOf(id), written)'), '손잡이도 부른다')
|
|
173
173
|
assert.ok(!read('../client/modeller/gizmo.ts').includes('keepBottomChanges'), '화면의 제 산수는 없다')
|
|
174
174
|
})
|
package/test/joint-fan.test.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAss
|
|
|
12
12
|
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
13
13
|
|
|
14
14
|
import { JOINT_FAN, jointFanOf } from '../client/modeller/joint-fan.ts'
|
|
15
|
+
import { revisionOf } from './revision-of.ts'
|
|
16
|
+
import { liveOverSession } from '../client/modeller/live.ts'
|
|
15
17
|
import { moving } from './joints.ts'
|
|
16
18
|
|
|
17
19
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
@@ -43,7 +45,7 @@ const direction = (v: number[]) => {
|
|
|
43
45
|
|
|
44
46
|
test('a part that turns gets a fan from the lowest value to the highest, where the part really goes', () => {
|
|
45
47
|
const asset = arm()
|
|
46
|
-
const fan = jointFanOf(asset, 'arm') as unknown as { id: string; shape: { provider: string; profile: { x: number; y: number }[] }; pose: { t: number[]; r: number[][] } }
|
|
48
|
+
const fan = jointFanOf(revisionOf(asset), 'arm') as unknown as { id: string; shape: { provider: string; profile: { x: number; y: number }[] }; pose: { t: number[]; r: number[][] } }
|
|
47
49
|
assert.ok(fan)
|
|
48
50
|
assert.equal(fan.id, JOINT_FAN)
|
|
49
51
|
assert.equal(fan.shape.provider, 'extrude', 'a section pushed thin, drawn as a part is')
|
|
@@ -65,32 +67,46 @@ test('a part that turns gets a fan from the lowest value to the highest, where t
|
|
|
65
67
|
})
|
|
66
68
|
|
|
67
69
|
test('a fan is as large as what turns, and never smaller than 60 mm', () => {
|
|
68
|
-
const big = jointFanOf(arm(), 'arm') as unknown as { shape: { profile: { x: number; y: number }[] } }
|
|
70
|
+
const big = jointFanOf(revisionOf(arm()), 'arm') as unknown as { shape: { profile: { x: number; y: number }[] } }
|
|
69
71
|
const radius = Math.max(...profileOf(big).map(p => Math.hypot(p.x, p.y)))
|
|
70
72
|
assert.ok(Math.abs(radius - 240) < 1e-3, `0.6 × 400 — ${radius}`)
|
|
71
73
|
})
|
|
72
74
|
|
|
73
75
|
test('no fan for a part that does not turn, nor for a slide -- a fan in a guessed plane would point the wrong way', () => {
|
|
74
76
|
/* The base does not turn; it shows the joint that sits on it, the arm's -- one joint, one fan (the elbow rule). */
|
|
75
|
-
const onBase = jointFanOf(arm(), 'base') as unknown as { shape: { profile: unknown[] } }
|
|
76
|
-
const ofArm = jointFanOf(arm(), 'arm') as unknown as { shape: { profile: unknown[] } }
|
|
77
|
+
const onBase = jointFanOf(revisionOf(arm()), 'base') as unknown as { shape: { profile: unknown[] } }
|
|
78
|
+
const ofArm = jointFanOf(revisionOf(arm()), 'arm') as unknown as { shape: { profile: unknown[] } }
|
|
77
79
|
assert.deepEqual(profileOf(onBase), profileOf(ofArm))
|
|
78
|
-
assert.equal(jointFanOf(arm(), ''), null)
|
|
80
|
+
assert.equal(jointFanOf(revisionOf(arm()), ''), null)
|
|
79
81
|
let slider = createFigureAsset('Probe')
|
|
80
82
|
slider = addFigureBox(slider, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
81
83
|
slider = moving(slider, 'block', { kind: 'slide', axis: 'x', id: 'shift', min: 0, max: 60 })
|
|
82
|
-
assert.equal(jointFanOf(slider, 'block'), null)
|
|
84
|
+
assert.equal(jointFanOf(revisionOf(slider), 'block'), null)
|
|
83
85
|
})
|
|
84
86
|
|
|
85
|
-
test('the editor draws it for the selected part
|
|
87
|
+
test('the editor draws it for the selected part; the canvas never lets it be picked', () => {
|
|
86
88
|
const editor = read('../client/modeller/editor.ts')
|
|
87
|
-
assert.ok(editor.includes('.fan=${this.fanOf(
|
|
88
|
-
assert.ok(editor.includes('return jointFanOf(asset, this.selected, { ...this.currentInputs(), ...this.live.values })'), 'the fan reads the pose the session holds')
|
|
89
|
+
assert.ok(editor.includes('.fan=${this.fanOf(shown, live)}'))
|
|
89
90
|
const canvas = read('../client/modeller/canvas.ts')
|
|
90
91
|
/* The fan, and what a command would do, are shown and never picked (ADR-0097 §5.5). */
|
|
91
92
|
assert.ok(canvas.includes('.map(c => ({ ...c, capturable: false, locked: true }))'))
|
|
92
93
|
})
|
|
93
94
|
|
|
95
|
+
/*
|
|
96
|
+
* The fan stands where the joint is drawn: the pose set with the controls (the version's inputs), and while a control is
|
|
97
|
+
* dragged, the pose in hand -- the same as a version where that pose was set (user 2026-09-24: the arm jumped on release).
|
|
98
|
+
*/
|
|
99
|
+
test('the fan follows the pose as drawn: set with the controls, or in hand while a control is dragged', () => {
|
|
100
|
+
const at = (fan: unknown) => (fan as { pose: { t: number[] } }).pose.t.map(v => Math.round(v * 1000) / 1000)
|
|
101
|
+
const rest = jointFanOf(revisionOf(column()), 'tip')
|
|
102
|
+
const set = jointFanOf(revisionOf(column(), { hinge: 40 }), 'tip')
|
|
103
|
+
assert.notDeepEqual(at(set), at(rest), 'the wrist moves with the hinge')
|
|
104
|
+
const shown = revisionOf(column())
|
|
105
|
+
const live = { values: { hinge: 40 } }
|
|
106
|
+
const inHand = jointFanOf(shown, 'tip', { ...live, geometry: liveOverSession(shown, live)! })
|
|
107
|
+
assert.deepEqual(at(inHand), at(set))
|
|
108
|
+
})
|
|
109
|
+
|
|
94
110
|
/* ── Joints on a chain (user 2026-09-24: 「로봇암 axis와 관절의 각도 표현을 다시 검증해보라」) ───────── */
|
|
95
111
|
|
|
96
112
|
/**
|
|
@@ -122,7 +138,7 @@ for (const [part, joint] of [
|
|
|
122
138
|
] as const) {
|
|
123
139
|
test(`${part} on ${joint}: the axis holds the part's plane, the fan's ends are where it goes, the fan opens as far as it turns`, () => {
|
|
124
140
|
const asset = column()
|
|
125
|
-
const fan = jointFanOf(asset, part) as unknown as { pose: { t: number[]; r: number[][] }; shape: { profile: { x: number; y: number }[] } }
|
|
141
|
+
const fan = jointFanOf(revisionOf(asset), part) as unknown as { pose: { t: number[]; r: number[][] }; shape: { profile: { x: number; y: number }[] } }
|
|
126
142
|
assert.ok(fan, 'a fan -- the first joint of a chain included')
|
|
127
143
|
const input = asset.document.model.inputs.find(i => i.id === joint)!
|
|
128
144
|
const pivot = fan.pose.t
|
|
@@ -152,7 +168,7 @@ for (const [part, joint] of [
|
|
|
152
168
|
|
|
153
169
|
test('a housing shows the joint it houses, not the one that carries it; a part that holds no pivot shows none', () => {
|
|
154
170
|
const asset = column()
|
|
155
|
-
const turns = (part: string) => (jointFanOf(asset, part) as unknown as { turns?: string } | null)?.turns ?? null
|
|
171
|
+
const turns = (part: string) => (jointFanOf(revisionOf(asset), part) as unknown as { turns?: string } | null)?.turns ?? null
|
|
156
172
|
assert.equal(turns('housing'), 'wrist', 'user 2026-09-24: 「elbow-housing itself is a axis」 -- it rides the hinge, it houses the wrist')
|
|
157
173
|
assert.equal(turns('link'), 'hinge', 'a link with pivots only at its ends shows the joint that turns it')
|
|
158
174
|
assert.equal(turns('tip'), 'wrist')
|
|
@@ -160,7 +176,7 @@ test('a housing shows the joint it houses, not the one that carries it; a part t
|
|
|
160
176
|
assert.equal(turns('base'), null)
|
|
161
177
|
|
|
162
178
|
/* The housing's fan is the wrist's: its ends are where the tip goes. */
|
|
163
|
-
const fan = jointFanOf(asset, 'housing') as unknown as { pose: { t: number[]; r: number[][] }; shape: { profile: { x: number; y: number }[] } }
|
|
179
|
+
const fan = jointFanOf(revisionOf(asset), 'housing') as unknown as { pose: { t: number[]; r: number[][] }; shape: { profile: { x: number; y: number }[] } }
|
|
164
180
|
const axis = [0, 1, 2].map(i => fan.pose.r[i]![1]!)
|
|
165
181
|
const flat = (v: number[]) => sub(v, axis.map(a => a * dot(v, axis)))
|
|
166
182
|
const onFan = (p: { x: number; y: number }) => [0, 1, 2].map(i => fan.pose.t[i]! + fan.pose.r[i]![0]! * p.x + fan.pose.r[i]![2]! * p.y)
|
|
@@ -173,7 +189,7 @@ test('a housing shows the joint it houses, not the one that carries it; a part t
|
|
|
173
189
|
test('one joint, one fan: the housing and the part it turns show the same fan, sized from what turns', () => {
|
|
174
190
|
const asset = column()
|
|
175
191
|
const size = (part: string) => {
|
|
176
|
-
const fan = jointFanOf(asset, part) as unknown as { shape: { profile: { x: number; y: number }[] }; pose: { t: number[] } }
|
|
192
|
+
const fan = jointFanOf(revisionOf(asset), part) as unknown as { shape: { profile: { x: number; y: number }[] }; pose: { t: number[] } }
|
|
177
193
|
return { radius: Math.max(...profileOf(fan).map(p => Math.hypot(p.x, p.y))), pivot: fan.pose.t }
|
|
178
194
|
}
|
|
179
195
|
const housing = size('housing'),
|
|
@@ -186,7 +202,7 @@ test('an axis running through a part does not make it that joint’s: a roll alo
|
|
|
186
202
|
/* ROBOT_ARM's forearm showed the wrist roll, whose axis runs its length (2026-09-24 audit). */
|
|
187
203
|
/* A link on a hinge and a roller on a twist whose axis runs along the link. */
|
|
188
204
|
const asset = fixture('joint-roller')
|
|
189
|
-
const turns = (part: string) => (jointFanOf(asset, part) as unknown as { turns?: string } | null)?.turns ?? null
|
|
205
|
+
const turns = (part: string) => (jointFanOf(revisionOf(asset), part) as unknown as { turns?: string } | null)?.turns ?? null
|
|
190
206
|
assert.equal(turns('link'), 'hinge', 'the twist axis runs through the link, and the link does not twist')
|
|
191
207
|
assert.equal(turns('roller'), 'twist')
|
|
192
208
|
assert.equal(turns('base'), null)
|