@things-factory/figure-ui 10.1.208 → 10.1.211

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/client/modeller/data-interface.ts +5 -5
  2. package/client/modeller/editor.ts +97 -88
  3. package/client/modeller/fasten-geometry.ts +2 -1
  4. package/client/modeller/figure-links.ts +87 -157
  5. package/client/modeller/figure-size-editor.ts +233 -0
  6. package/client/modeller/joint-drag.ts +25 -21
  7. package/client/modeller/joint-fan.ts +4 -3
  8. package/client/modeller/links.ts +87 -36
  9. package/client/modeller/live.ts +33 -0
  10. package/client/modeller/motion-ends.ts +10 -6
  11. package/client/modeller/size-response.ts +95 -0
  12. package/dist-client/modeller/data-interface.d.ts +1 -1
  13. package/dist-client/modeller/data-interface.js +5 -5
  14. package/dist-client/modeller/data-interface.js.map +1 -1
  15. package/dist-client/modeller/editor.d.ts +14 -1
  16. package/dist-client/modeller/editor.js +103 -89
  17. package/dist-client/modeller/editor.js.map +1 -1
  18. package/dist-client/modeller/fasten-geometry.js +1 -1
  19. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  20. package/dist-client/modeller/figure-links.d.ts +3 -4
  21. package/dist-client/modeller/figure-links.js +91 -145
  22. package/dist-client/modeller/figure-links.js.map +1 -1
  23. package/dist-client/modeller/figure-size-editor.d.ts +29 -0
  24. package/dist-client/modeller/figure-size-editor.js +262 -0
  25. package/dist-client/modeller/figure-size-editor.js.map +1 -0
  26. package/dist-client/modeller/joint-drag.d.ts +1 -1
  27. package/dist-client/modeller/joint-drag.js +27 -22
  28. package/dist-client/modeller/joint-drag.js.map +1 -1
  29. package/dist-client/modeller/joint-fan.js +4 -3
  30. package/dist-client/modeller/joint-fan.js.map +1 -1
  31. package/dist-client/modeller/links.d.ts +21 -9
  32. package/dist-client/modeller/links.js +85 -35
  33. package/dist-client/modeller/links.js.map +1 -1
  34. package/dist-client/modeller/live.d.ts +14 -0
  35. package/dist-client/modeller/live.js +31 -0
  36. package/dist-client/modeller/live.js.map +1 -1
  37. package/dist-client/modeller/motion-ends.d.ts +4 -1
  38. package/dist-client/modeller/motion-ends.js +10 -6
  39. package/dist-client/modeller/motion-ends.js.map +1 -1
  40. package/dist-client/modeller/size-response.d.ts +29 -0
  41. package/dist-client/modeller/size-response.js +99 -0
  42. package/dist-client/modeller/size-response.js.map +1 -0
  43. package/dist-client/tsconfig.tsbuildinfo +1 -1
  44. package/docs/evidence/2026-10-24/current-size-panel.png +0 -0
  45. package/docs/evidence/2026-10-24/integrated-size.png +0 -0
  46. package/docs/evidence/2026-10-24/multipart-response.png +0 -0
  47. package/docs/evidence/2026-10-24/repeat-layout.png +0 -0
  48. package/docs/evidence/2026-10-24/repeat-shared-style.png +0 -0
  49. package/docs/evidence/2026-10-24/size-in-layout.png +0 -0
  50. package/docs/evidence/2026-10-24/size-shared-style.png +0 -0
  51. package/docs/evidence/2026-10-24/width-preview.png +0 -0
  52. package/docs/multipart-response-review-2026-10-24.md +77 -0
  53. package/docs/response-authoring-2026-10-24.md +46 -0
  54. package/docs/size-and-repeat-ux-2026-10-24.md +67 -0
  55. package/package.json +7 -7
  56. package/test/attachment-size-contract.test.ts +47 -0
  57. package/test/browser/acceptance-page.js +1 -1
  58. package/test/browser/detail-page.js +1 -1
  59. package/test/browser/detail-tabs-page.js +1 -1
  60. package/test/browser/editing-page.js +1 -1
  61. package/test/browser/editing.check.mjs +66 -14
  62. package/test/browser/links.check.mjs +41 -22
  63. package/test/browser/modeller-page.js +1 -1
  64. package/test/browser/modeller-revision.check.mjs +21 -7
  65. package/test/browser/modeller.check.mjs +106 -39
  66. package/test/browser/multipart-response-page.js +8 -0
  67. package/test/browser/parts.check.mjs +2 -1
  68. package/test/browser/product-page.js +1 -1
  69. package/test/browser/props.check.mjs +17 -14
  70. package/test/browser/response-page.js +12 -0
  71. package/test/browser/row-preview.check.mjs +3 -3
  72. package/test/browser/screen.check.mjs +57 -13
  73. package/test/browser/serve-response.mjs +24 -0
  74. package/test/clip-ai-proposal.test.ts +1 -1
  75. package/test/clip-consent.test.ts +2 -2
  76. package/test/command-preview.test.ts +7 -7
  77. package/test/joint-drag.test.ts +6 -6
  78. package/test/joint-frames.test.ts +33 -32
  79. package/test/joints.ts +1 -6
  80. package/test/links.test.ts +60 -20
  81. package/test/multipart-response-fixture.js +17 -0
  82. package/test/multipart-response.test.ts +37 -0
  83. package/test/refusals.test.ts +2 -2
  84. package/test/resize-with-placement.test.ts +23 -1
  85. package/test/size-response.test.ts +49 -0
  86. package/test/size-source-pairing.test.ts +66 -0
  87. package/translations/en.json +47 -10
  88. package/translations/es.json +47 -10
  89. package/translations/ja.json +47 -10
  90. package/translations/ko.json +52 -15
  91. package/translations/ms.json +47 -10
  92. package/translations/zh.json +47 -10
@@ -83,7 +83,7 @@ function sourceOf(kind, text) {
83
83
  /* On the roller's side at 0° round its own Y -- its own +X (ADR-0140 3.9). */
84
84
  asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'mark', to: { part: 'roller', surface: { around: 0, along: 'centre' } } })
85
85
  const j = asset.joints.find(o => o.body1.part === 'roller')
86
- asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Z', lowerLimit: -360, upperLimit: 360, state: { id: 'spin' } } })
86
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -360, upperLimit: 360, state: { id: 'spin' } }] } })
87
87
  return serializeFigureAsset(applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 }))
88
88
  }
89
89
  throw Error(`no figure for ${kind}`)
@@ -72,13 +72,17 @@ async function mode(which) {
72
72
  describe('work counted on the robot arm', () => {
73
73
  before(() => mount('arm'))
74
74
 
75
- /* The motion tab is drawn out of sight with the rest: the selected part's motion is evaluated at its two ends. */
76
- test('picking a part on the canvas compiles nothing; it evaluates only the two ends of the part\'s motion', async () => {
75
+ /*
76
+ The tabs are drawn out of sight with the rest: the selected part's motion is evaluated at its two ends, and the size
77
+ editor on 「배치」 asks once which of the parent's axes each of the part's own lies along, the figure at rest
78
+ (figureAxisPairsOf, ADR-0159 3) -- one evaluation for all three rows.
79
+ */
80
+ test('picking a part on the canvas compiles nothing; it evaluates the two ends of the part\'s motion and its axes at rest', async () => {
77
81
  const part = 'upper-arm'
78
82
  const point = await read(id => probe.partOnPage(id), part)
79
83
  const { before, spent } = await counted(() => click(point))
80
84
  assert.equal((await read(() => probe.state())).selected, part)
81
- assert.deepEqual(spent, { compiles: 0, evaluations: 2 })
85
+ assert.deepEqual(spent, { compiles: 0, evaluations: 3 })
82
86
  /*
83
87
  Once the screen rests, two commands are seen on copies (ADR-0097 §5.5): taking the part off (its step without the
84
88
  joint, and its result) and the fastening the form opens on (its result). Each compiles only what it makes; the
@@ -89,11 +93,18 @@ describe('work counted on the robot arm', () => {
89
93
  assert.ok(rested.compiles <= 3, JSON.stringify(rested))
90
94
  })
91
95
 
92
- /* 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. */
96
+ /*
97
+ 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
98
+ ends, and its axes paired at rest for the size editor once more on the new version.
99
+ */
93
100
  test('a value typed into its field: the edit and its read, and nothing else while it is drawn', async () => {
101
+ /* The part's measures are the 「배치」 tab's size editor (2026-10-24). */
102
+ await click(await read(() => probe.tab('figure.label.tab-arrangement')))
103
+ await settled()
94
104
  const field = await read(() => {
95
105
  const deep = (root = document) => [...root.querySelectorAll('input[type=number]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
96
- const input = deep().find(i => !i.disabled && /\.size\.|\.dimension\./.test(i.getAttribute('aria-label') ?? ''))
106
+ /* A measure of the part in the size editor. */
107
+ const input = deep().find(i => !i.disabled && / figure\.label\.size-measure$/.test(i.getAttribute('aria-label') ?? ''))
97
108
  if (!input) return null
98
109
  input.scrollIntoView({ block: 'center' })
99
110
  const r = input.getBoundingClientRect()
@@ -105,7 +116,7 @@ describe('work counted on the robot arm', () => {
105
116
  await page().keyboard.type(String(field.value + 10))
106
117
  const { spent } = await counted(() => page().keyboard.press('Enter'))
107
118
  assert.equal((await read(() => probe.state())).revision, revision + 1, `${field.label} is one step`)
108
- assert.ok(spent.compiles <= 2 && spent.evaluations <= 5, JSON.stringify(spent))
119
+ assert.ok(spent.compiles <= 2 && spent.evaluations <= 6, JSON.stringify(spent))
109
120
  })
110
121
  })
111
122
 
@@ -178,7 +189,10 @@ describe('the edit canvas and the preview stay standing', () => {
178
189
  test('a value changed beside the preview is drawn inside the scene that stands', async () => {
179
190
  await click(await read(() => probe.row('box')))
180
191
  await sleep(300)
181
- const field = await read(() => probe.labelled('box.size.y'))
192
+ /* The box's size is the 「배치」 tab's (2026-10-24). */
193
+ await click(await read(() => probe.tab('figure.label.tab-arrangement')))
194
+ await sleep(300)
195
+ const field = await read(() => probe.labelled('box Y figure.label.size-measure'))
182
196
  assert.ok(field, 'the height field')
183
197
  await page().mouse.click(field.x, field.y, { clickCount: 3 })
184
198
  await page().keyboard.type('600')
@@ -53,8 +53,11 @@ async function click(point) {
53
53
  await page().mouse.click(point.x, point.y)
54
54
  await sleep(400)
55
55
  }
56
- /* A side tab pressed, by its short name; how a part answers a size is the 「반응형」 tab's (user 2026-10-17). */
57
- const tab = async name => click(await read(l => probe.tab(l), `figure.label.tab-short-${name}`))
56
+ /*
57
+ A side tab pressed, by its short name; how a part answers a size is the 「반응형」 tab's (user 2026-10-17), named
58
+ 「배치」 (figure.label.tab-arrangement) while a part is chosen.
59
+ */
60
+ const tab = async name => click(await read(n => probe.tab(`figure.label.tab-short-${n}`) ?? (n === 'response' ? probe.tab('figure.label.tab-arrangement') : null), name))
58
61
 
59
62
  const KEYS = { translate: 'KeyW', rotate: 'KeyE', scale: 'KeyR' }
60
63
 
@@ -495,37 +498,33 @@ describe('a figure made in the modeller is given its size from its parts (user 2
495
498
  assert.deepEqual(await (await field()).evaluate(i => [i.value, i.placeholder]), ['', '∞'], 'empty, and nothing limits it')
496
499
  })
497
500
 
501
+ /*
502
+ The box's size editor on 「반응형」 (ADR-0159 3): its width made to follow -- on nothing, the figure's own width --
503
+ then kept a difference from it, and the width typed.
504
+ */
498
505
  test('a part follows the figure\'s width less a difference: chosen and typed, the width is the size less it', async () => {
499
506
  await click(await read(() => probe.partOnPage('box')))
500
507
  assert.equal((await state()).selected, 'box')
501
- const handle = (what, label) =>
502
- page().evaluateHandle(
503
- (what, label) => {
504
- const deep = (root = document) => [...root.querySelectorAll(what)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
505
- if (what === 'select') return deep().find(s => s.previousElementSibling?.textContent?.trim() === label && [...s.options].some(o => o.value === 'instance:x')) ?? null
506
- return deep().find(i => i.closest('label[inline-plus]')) ?? null
507
- },
508
- what,
509
- label
510
- )
511
- /* The way to make a side follow is folded under the advanced choice (2026-09-28): opened, as a hand does. */
512
- await click(await read(() => {
513
- const deep = (root = document) => [...root.querySelectorAll('details[resize-choice] > summary')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
514
- const summary = deep()[0]
515
- if (!summary) return null
516
- summary.scrollIntoView({ block: 'center' })
517
- const r = summary.getBoundingClientRect()
518
- return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
519
- }))
520
- const width = await handle('select', 'X')
521
- assert.ok(await width.evaluate(s => !!s), 'the width can follow the figure size')
522
- await width.select('instance:x')
523
- await sleep(600)
524
- const plus = await handle('input')
525
- assert.ok(await plus.evaluate(i => !!i), 'a following width offers its difference')
526
- await plus.evaluate(i => i.scrollIntoView({ block: 'center' }))
527
- await plus.click({ clickCount: 3 })
528
- await page().keyboard.type('-50')
508
+ const handle = selector =>
509
+ page().evaluateHandle(selector => {
510
+ const deep = (root = document) => [...root.querySelectorAll(selector)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
511
+ return deep()[0] ?? null
512
+ }, selector)
513
+ const press = async (selector, what) => {
514
+ const element = await handle(selector)
515
+ assert.ok(await element.evaluate(e => !!e), what)
516
+ await click(await element.evaluate(e => {
517
+ e.scrollIntoView({ block: 'center' })
518
+ const r = e.getBoundingClientRect()
519
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
520
+ }))
521
+ }
522
+ await press('input[aria-label="box X figure.label.size-follows"]', 'the width can follow')
523
+ await press('div.dimension[data-axis=x] div.modes label:nth-of-type(2) input[type=radio]', 'a following width offers its difference')
524
+ const width = await handle('input[aria-label="box X figure.label.size-measure"]')
525
+ await width.evaluate(i => i.scrollIntoView({ block: 'center' }))
526
+ await width.click({ clickCount: 3 })
527
+ await page().keyboard.type('350')
529
528
  await page().keyboard.press('Tab')
530
529
  await sleep(600)
531
530
  /*
@@ -534,6 +533,11 @@ describe('a figure made in the modeller is given its size from its parts (user 2
534
533
  */
535
534
  assert.equal((await state()).error, '', 'taken')
536
535
  assert.equal((await body('box')).dimensions['size.x'], 350, 'the figure\'s 400 less 50')
536
+ const said = await read(() => {
537
+ const deep = (root = document) => [...root.querySelectorAll('div.dimension[data-axis=x] p.result')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
538
+ return deep().map(p => p.textContent.trim())
539
+ })
540
+ assert.deepEqual(said, ['figure.text.size-difference-less'], 'said as the size less a difference')
537
541
  const asked = await read(() => {
538
542
  const deep = (root = document) => [...root.querySelectorAll('ox-prompt')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
539
543
  return deep().length
@@ -622,9 +626,65 @@ describe('a motion is changed where it is (2026-09-25)', () => {
622
626
  assert.equal(after.error, '', 'not refused')
623
627
  assert.equal(after.revision, start.revision + 1, 'one step')
624
628
  const now = await jointOf('upper-arm')
625
- assert.equal(now.upperLimit, 45, 'the new limit')
629
+ /* A joint is a list of axes (ADR-0159): the form changes its first. */
630
+ assert.equal(now.motion[0].upperLimit, 45, 'the new limit')
626
631
  assert.equal(now.id, was.id, 'the same joint')
627
632
  })
633
+
634
+ /* A joint is a list of axes (ADR-0159): one more is added where the motion is said, and taken away again. */
635
+ const jointOf = part => read(part => (JSON.parse(document.querySelector('figure-editor').session.source()).joints ?? []).find(j => j.body1.part === part) ?? null, part)
636
+ const button = async (text, attribute) =>
637
+ page().evaluate(
638
+ (text, attribute) => {
639
+ const deep = (root = document) => [...root.querySelectorAll('button')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
640
+ const b = deep().find(x => x.textContent.includes(text) && (!attribute || x.hasAttribute(attribute)))
641
+ if (!b) return null
642
+ b.scrollIntoView({ block: 'center' })
643
+ const r = b.getBoundingClientRect()
644
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
645
+ },
646
+ text,
647
+ attribute
648
+ )
649
+ const rows = () =>
650
+ page().evaluate(() => {
651
+ const deep = (root = document) => [...root.querySelectorAll('div[motion-axis]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
652
+ return deep().length
653
+ })
654
+
655
+ test('an axis added to the shoulder: one step, after the first, on an axis the first does not use, with a control of its own', async () => {
656
+ const was = await jointOf('upper-arm')
657
+ await click(await button('figure.button.add-axis', 'add-axis'), 'add an axis')
658
+ const start = await state()
659
+ await click(await button('figure.button.add-axis'), 'the form\'s add-axis')
660
+ const after = await state()
661
+ assert.equal(after.error, '', 'not refused')
662
+ assert.equal(after.revision, start.revision + 1, 'one step')
663
+ const now = await jointOf('upper-arm')
664
+ assert.equal(now.motion.length, 2)
665
+ assert.deepEqual(now.motion[0], was.motion[0], 'the first axis as it was')
666
+ assert.notEqual(now.motion[1].axis, now.motion[0].axis, 'another axis')
667
+ assert.equal(now.motion[1].state.id, 'upper-arm-turn-2')
668
+ assert.equal(await rows(), 2, 'a row an axis')
669
+ })
670
+
671
+ test('the second axis taken away: one step, the first as it was', async () => {
672
+ const was = await jointOf('upper-arm')
673
+ const start = await state()
674
+ const removes = await page().evaluate(() => {
675
+ const deep = (root = document) => [...root.querySelectorAll('button[remove-axis]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
676
+ const b = deep()[1]
677
+ if (!b) return null
678
+ b.scrollIntoView({ block: 'center' })
679
+ const r = b.getBoundingClientRect()
680
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
681
+ })
682
+ await click(removes, 'remove the second axis')
683
+ assert.equal((await state()).revision, start.revision + 1, 'one step')
684
+ const now = await jointOf('upper-arm')
685
+ assert.deepEqual(now.motion, [was.motion[0]], 'the first axis alone, as it was')
686
+ assert.equal(await rows(), 1)
687
+ })
628
688
  })
629
689
 
630
690
  describe('the modeller made easier to find and to keep (2026-09-26 baseline)', () => {
@@ -660,16 +720,18 @@ describe('the modeller made easier to find and to keep (2026-09-26 baseline)', (
660
720
  assert.equal(await resize.evaluate(b => b.getAttribute('aria-pressed')), 'false')
661
721
  })
662
722
 
723
+ /* The part's size is the 「반응형」 tab's size editor (2026-10-24), and what a size typed keeps is said under it. */
663
724
  test('a height typed into its field keeps the part\'s middle, and the size group says so (ADR-0140 4.1)', async () => {
664
725
  await mount('box')
665
726
  await click(await read(() => probe.partOnPage('box')))
727
+ await tab('response')
666
728
  const was = await body('box')
667
729
  const middleOf = b => b.t[1] + b.dimensions.start + b.dimensions.length / 2
668
730
  assert.ok(await read(() => {
669
731
  const deep = (root = document) => [...root.querySelectorAll('p[size-keeps]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
670
732
  return deep().some(p => p.textContent.trim() === 'figure.text.size-keeps-centre')
671
733
  }), 'said beside the size fields')
672
- const height = await find('input', 'box.size.y')
734
+ const height = await find('input', 'box Y figure.label.size-measure')
673
735
  assert.ok(await height.evaluate(i => !!i), 'the height field')
674
736
  const at = await centre(height)
675
737
  await page().mouse.click(at.x, at.y, { clickCount: 3 })
@@ -744,20 +806,24 @@ describe('the modeller made easier to find and to keep (2026-09-26 baseline)', (
744
806
  })
745
807
  })
746
808
 
809
+ /*
810
+ The one-click follow-size button is gone (2026-10-24): a side is made to follow by its own 'follows' in the size editor.
811
+ On a figure with no size, the first one declares the figure's size from its parts in the same step.
812
+ */
747
813
  describe('a part follows the figure size in one step (follow-size, 2026-09-26)', () => {
748
- test('one click on a figure with no size: its size is declared from the part, said in a line, and the part follows it', async () => {
814
+ test('a side made to follow on a figure with no size: its size is declared from the part, said in a line, and the part follows it', async () => {
749
815
  await mount('box')
750
816
  await click(await read(() => probe.row('box')))
751
817
  await tab('response')
752
818
  const at = await page().evaluate(() => {
753
- const deep = (root = document) => [...root.querySelectorAll('button[follow-size]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
819
+ const deep = (root = document) => [...root.querySelectorAll('input[aria-label="box X figure.label.size-follows"]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
754
820
  const b = deep()[0]
755
- if (!b) return null
821
+ if (!b || b.checked) return null
756
822
  b.scrollIntoView({ block: 'center' })
757
823
  const r = b.getBoundingClientRect()
758
824
  return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
759
825
  })
760
- assert.ok(at, 'the button is offered for a box that follows nothing')
826
+ assert.ok(at, 'the width of a box that follows nothing can be made to follow')
761
827
  await page().evaluate(() => {
762
828
  window.__told = []
763
829
  document.addEventListener('notify', e => window.__told.push(e.detail))
@@ -892,7 +958,7 @@ describe('a row\'s pitch made a board\'s to set (ADR-0096)', () => {
892
958
  assert.deepEqual([input.min, input.max, source.designInputs['roller.pitch']], [100, 400, 200])
893
959
  assert.deepEqual(source.joints.find(j => j.body1.part === 'roller').repeat.pitch, { from: { of: 'input', input: 'roller.pitch' } })
894
960
  assert.equal((await read(() => probe.ids())).filter(id => id.includes('"roller.placed"')).length, before, 'the row as it was')
895
- await typeInto('roller.repeat.pitch', '400')
961
+ await typeInto('figure.label.repeat-gap', '400')
896
962
  source = await read(() => probe.source())
897
963
  assert.equal(source.designInputs['roller.pitch'], 400, 'the pitch field sets the input')
898
964
  assert.ok((await read(() => probe.ids())).filter(id => id.includes('"roller.placed"')).length < before, 'fewer rollers at a wider pitch')
@@ -919,10 +985,11 @@ describe('a row that carries a part (ADR-0093, 2026-09-26)', () => {
919
985
  assert.ok(copy, 'a mark on the fourth roller is drawn')
920
986
  await click(await read(id => probe.partOnPage(id), copy))
921
987
  assert.equal((await state()).selected, 'mark', 'picked by the part it copies')
922
- assert.equal(await read(() => probe.hasInput('mark.repeat.pitch')), false, 'the mark has no row of its own to set')
988
+ /* The row's pitch field is the selected part's own: named by what it is, the centre spacing. */
989
+ assert.equal(await read(() => probe.hasInput('figure.label.repeat-gap')), false, 'the mark has no row of its own to set')
923
990
  await click(await read(() => probe.row('roller')))
924
991
  assert.equal((await state()).selected, 'roller')
925
- assert.ok(await read(() => probe.hasInput('roller.repeat.pitch')), 'the roller offers its row')
992
+ assert.ok(await read(() => probe.hasInput('figure.label.repeat-gap')), 'the roller offers its row')
926
993
  /* Walk 1 (2026-09-26): the fastening form opens on the roller's own fastening -- by its centre, the drawing's way (2026-10-01). */
927
994
  await click(await read(() => probe.button('change-fastening')))
928
995
  assert.equal(await read(() => probe.fastenMode()), 'centre', 'the way it is fastened reads centre')
@@ -0,0 +1,8 @@
1
+ import './modeller-page.js'
2
+ import { i18next } from '@operato/i18n'
3
+ import { serializeFigureAsset } from '@hatiolab/figure-model'
4
+ import { multipartResponseFixture } from '../multipart-response-fixture.js'
5
+ const ko = await fetch('/ko.json').then(r => r.json())
6
+ i18next.addResourceBundle('ko', 'translations', ko, true, true)
7
+ await i18next.changeLanguage('ko')
8
+ await window.harness.mount('box', serializeFigureAsset(multipartResponseFixture()))
@@ -76,8 +76,9 @@ async function press(...keys) {
76
76
  for (const key of keys.slice(0, -1).reverse()) await page().keyboard.up(key)
77
77
  await sleep(400)
78
78
  }
79
+ /* A side tab by its short name; with a part chosen, 「반응형」 is named 「배치」 (figure.label.tab-arrangement). */
79
80
  async function tab(name) {
80
- await click(await read(name => probe.tab(name), `figure.label.tab-short-${name}`), `the ${name} tab`)
81
+ await click(await read(n => probe.tab(`figure.label.tab-short-${n}`) ?? (n === 'response' ? probe.tab('figure.label.tab-arrangement') : null), name), `the ${name} tab`)
81
82
  }
82
83
 
83
84
  describe('the parts list: each button does what it says, one step of the session each', () => {
@@ -46,7 +46,7 @@ export function doorSource() {
46
46
  a = addFigureBox(a, { id: 'leaf', size: { x: 900, y: 1800, z: 40 }, position: { x: 0, y: 1000, z: 0 } })
47
47
  a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', to: { part: 'frame', face: 'top' } })
48
48
  const j = a.joints.find(k => k.body1.part === 'leaf')
49
- a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: 'open' } } })
49
+ a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Y', state: { id: 'open' } }] } })
50
50
  a = editFigurePart(a, { kind: 'set-driver', state: 'open', clip: 'swing', duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
51
51
  return serializeFigureAsset(a)
52
52
  }
@@ -70,8 +70,9 @@ async function press(selector, what) {
70
70
  }
71
71
 
72
72
  /**
73
- * The groups of the panel shown, top to bottom, by what each is: the part's name, the size and the place the links
74
- * draw, the look and 「더 보기」 on the properties tab; on 「반응형」 the size rules, a position that follows, the row.
73
+ * The groups of the panel shown, top to bottom, by what each is: the part's name, the place the links draw -- and its
74
+ * shape's own measures, for a shape that has any -- the look and 「더 보기」 on the properties tab; on 「반응형」 its size,
75
+ * a position that follows, the row.
75
76
  */
76
77
  const groupsInOrder = () =>
77
78
  read(() => {
@@ -80,7 +81,7 @@ const groupsInOrder = () =>
80
81
  const links = panel.querySelector('figure-links')?.shadowRoot
81
82
  const marks = {
82
83
  name: panel.querySelector('input[name-field]'),
83
- size: links?.querySelector('section[size], section[size-response]'),
84
+ size: links?.querySelector('section[size], section[size-response]') ?? panel.querySelector('section[slot=size]'),
84
85
  place: links?.querySelector('section[fastening]'),
85
86
  position: links?.querySelector('section[position-response]'),
86
87
  row: panel.querySelector('section[slot=row]') ?? links?.querySelector('section[row]'),
@@ -93,9 +94,10 @@ const groupsInOrder = () =>
93
94
  .map(([name]) => name)
94
95
  })
95
96
 
96
- /* A side tab pressed, by its name. */
97
+ /* A side tab pressed, by its name; with a part chosen, 「반응형」 is named 「배치」 (figure.label.tab-arrangement). */
97
98
  async function tab(name) {
98
- await press(`aside button[tab][aria-label="figure.label.tab-short-${name}"]`, `the ${name} tab`)
99
+ const named = `aside button[tab][aria-label="figure.label.tab-short-${name}"]`
100
+ await press(name === 'response' ? `${named}, aside button[tab][aria-label="figure.label.tab-arrangement"]` : named, `the ${name} tab`)
99
101
  await read(() => document.querySelector('figure-editor').updateComplete)
100
102
  }
101
103
 
@@ -150,10 +152,11 @@ describe('the side: six tabs, the view and the releases from the bar', () => {
150
152
  describe('a part\'s groups in one order, whatever its state', () => {
151
153
  before(() => mount('rollers'))
152
154
 
153
- test('a part fastened in a row: name, size, place, look, more; on 「반응형」 its size rules and its row', async () => {
155
+ /* A box's size is the 「배치」 tab's (2026-10-24): it has no measures of its own shape left on the properties. */
156
+ test('a part fastened in a row: name, place, look, more; on 「반응형」 its size and its row', async () => {
154
157
  await pick('roller')
155
158
  await tab('properties')
156
- assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', 'look', 'more'])
159
+ assert.deepEqual(await groupsInOrder(), ['name', 'place', 'look', 'more'])
157
160
  await tab('response')
158
161
  assert.deepEqual(await groupsInOrder(), ['size', 'row'])
159
162
  await tab('properties')
@@ -161,10 +164,10 @@ describe('a part\'s groups in one order, whatever its state', () => {
161
164
 
162
165
  test('a part on the plane, not in a row: the same order, the row folded where a row would be', async () => {
163
166
  await pick('bed')
164
- assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', 'look', 'more'])
165
- /* On the plane, its height is worked out from the figure size: the position group stands between. */
167
+ assert.deepEqual(await groupsInOrder(), ['name', 'place', 'look', 'more'])
168
+ /* On the plane, its place is its fastening's: no position worked out from the figure size is offered (2026-10-24). */
166
169
  await tab('response')
167
- assert.deepEqual(await groupsInOrder(), ['size', 'position', 'row'])
170
+ assert.deepEqual(await groupsInOrder(), ['size', 'row'])
168
171
  await tab('properties')
169
172
  })
170
173
 
@@ -221,14 +224,14 @@ describe('a group\'s 「⋯」: its rarer actions, by the mouse or the keyboard'
221
224
  assert.equal(await read(() => JSON.parse(document.querySelector('figure-editor').session.source()).joints.some(j => j.body1.part === 'bed')), false, 'taken off')
222
225
  })
223
226
 
227
+ /* The row's 「⋯」 is gone: taking a row off is a button of its own (2026-10-24). The roller's place group has one. */
224
228
  test('Esc closes the menu and nothing is done', async () => {
225
229
  await pick('roller')
226
- /* The row is the 「반응형」 tab's. */
227
- await tab('response')
230
+ await tab('properties')
228
231
  const start = await revision()
229
- await press('section[row] button[group-menu]', 'the row\'s 「⋯」')
232
+ await press('section[fastening] button[group-menu]', 'the place group\'s 「⋯」')
230
233
  await until(() => document.activeElement?.localName === 'ox-popup-menu', [], 'the menu open, with the focus')
231
- assert.deepEqual(await read(() => [...document.querySelectorAll('ox-popup-menu[active] ox-popup-menuitem')].map(i => i.getAttribute('label'))), ['figure.button.remove-repeat'])
234
+ assert.deepEqual(await read(() => [...document.querySelectorAll('ox-popup-menu[active] ox-popup-menuitem')].map(i => i.getAttribute('label'))), ['figure.button.detach'])
232
235
  await page().keyboard.press('Escape')
233
236
  await until(() => !document.querySelector('ox-popup-menu[active]'), [], 'the menu closed')
234
237
  await sleep(200)
@@ -0,0 +1,12 @@
1
+ /* Manual CUA check of the real editor, using an in-memory store and a fresh fixed-size part. */
2
+ import './modeller-page.js'
3
+ import { i18next } from '@operato/i18n'
4
+ import { addFigureBox, createFigureAsset, 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
+ const asset = addFigureBox(createFigureAsset('반응형 치수 검증'), {
10
+ id: 'frame', size: { x: 1500, y: 80, z: 600 }, position: { x: 0, y: 50, z: 0 }
11
+ })
12
+ await window.harness.mount('box', serializeFigureAsset(asset))
@@ -60,10 +60,10 @@ describe('a row\'s preview follows its basis', () => {
60
60
  const at = await read(() => probe.row('bin'))
61
61
  await page().mouse.click(at.x, at.y)
62
62
  await until(() => probe.state().selected === 'bin', [], 'bin selected')
63
- /* The row is the 「반응형」 tab's (user 2026-10-17). */
64
- const tab = await read(() => probe.tab('figure.label.tab-short-response'))
63
+ /* The row is the 「반응형」 tab's (user 2026-10-17), named 「배치」 while a part is chosen (figure.label.tab-arrangement). */
64
+ const tab = await read(() => probe.tab('figure.label.tab-arrangement'))
65
65
  await page().mouse.click(tab.x, tab.y)
66
- await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-short-response', [], 'the 반응형 tab on')
66
+ await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-arrangement', [], 'the 배치 tab on')
67
67
  await click('details[row-fold] > summary', {}, 'the row\'s fold')
68
68
  })
69
69