@things-factory/figure-ui 10.1.210 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.210",
3
+ "version": "10.1.211",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -48,5 +48,5 @@
48
48
  "@things-factory/setting-base": "^10.1.208",
49
49
  "three": "^0.185.1"
50
50
  },
51
- "gitHead": "89fbda12d43d8532ba4e95ba6d6d7e05e2da2502"
51
+ "gitHead": "1b781d2bbb963380eda73ab7020091987393de37"
52
52
  }
@@ -374,6 +374,44 @@ describe('values in hand on the properties tab: drawn at once, written on change
374
374
  })
375
375
  })
376
376
 
377
+ /* A size of the part's own on the 「배치」 tab's size editor is a value in hand as any field is (user 2026-09-24). */
378
+ describe('a size in hand on the arrangement tab: drawn while typed or dragged, written once', () => {
379
+ const WIDTH = 'box X figure.label.size-measure'
380
+ const wide = async () => Math.round((await look('box')).size[0])
381
+ let was
382
+ before(async () => {
383
+ await mount('box')
384
+ await pick('box')
385
+ await click(await read(() => probe.tab('figure.label.tab-arrangement')))
386
+ })
387
+
388
+ test('a width typed is drawn while it is typed, and written once on Enter', async () => {
389
+ was = await wide()
390
+ const start = await revision()
391
+ await type(WIDTH, '700')
392
+ assert.equal(await wide(), 700, 'drawn 700 wide while typed')
393
+ assert.equal(await revision(), start, 'nothing written yet')
394
+ await page().keyboard.press('Enter')
395
+ await sleep(400)
396
+ assert.equal(await revision(), start + 1)
397
+ assert.equal(await read(() => probe.value('box.size.x')), 700)
398
+ await press(...UNDO)
399
+ assert.equal(await wide(), was, 'undone, drawn as it stands')
400
+ })
401
+
402
+ test('its label dragged sideways scrubs the width: drawn while held, one step on release', async () => {
403
+ const start = await revision()
404
+ const label = await read(id => editing.fieldLabel(id), WIDTH)
405
+ assert.ok(label, 'the X of the size editor')
406
+ const held = await drag(label, 60, async () => ({ drawn: await wide(), shown: await read(id => editing.fieldValue(id), WIDTH), revision: await revision() }))
407
+ assert.notEqual(held.drawn, was, 'drawn while held')
408
+ assert.equal(String(held.drawn), held.shown, 'the field follows what is drawn')
409
+ assert.equal(held.revision, start, 'nothing written while held')
410
+ assert.equal(await revision(), start + 1, 'the release is one step')
411
+ assert.equal(await read(() => probe.value('box.size.x')), held.drawn)
412
+ })
413
+ })
414
+
377
415
  describe('the pose sliders: drawn while dragged, one step on release; the pose kept as the default (user 2026-09-24)', () => {
378
416
  before(async () => {
379
417
  await mount('six-axis')
@@ -630,6 +630,61 @@ describe('a motion is changed where it is (2026-09-25)', () => {
630
630
  assert.equal(now.motion[0].upperLimit, 45, 'the new limit')
631
631
  assert.equal(now.id, was.id, 'the same joint')
632
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
+ })
633
688
  })
634
689
 
635
690
  describe('the modeller made easier to find and to keep (2026-09-26 baseline)', () => {
@@ -703,3 +703,42 @@ test('a follower of several leaders and a stroke that is a sum of lengths are wr
703
703
  assert.equal(slide.travel, '')
704
704
  assert.deepEqual(motionJoint(reach, slide, 'wrist').motion![0]!.travel, reach.motion[0]!.travel, 'saving keeps the summed stroke')
705
705
  })
706
+
707
+ test('어깨는 축 둘 — 축 추가는 쓰지 않은 축으로 시작하고, 고쳐도 다른 축은 그대로, 같은 관절의 다른 축을 따를 수 있고, 축을 빼도 남은 축의 조작값 이름은 그대로 (ADR-0159)', async () => {
708
+ const { leadersFor, motionJoint, motionFormOf, motionOf, withoutAxis, controlIdFor } = await import('../client/modeller/links.ts')
709
+ const { compileFigureAsset, motionIdOf } = await import('@hatiolab/figure-model')
710
+ let asset = createFigureAsset('Shoulder')
711
+ asset = addFigureBox(asset, { id: 'body', size: { x: 300, y: 400, z: 200 }, position: { x: 0, y: 200, z: 0 } })
712
+ asset = addFigureBox(asset, { id: 'arm', size: { x: 60, y: 300, z: 60 }, position: { x: 0, y: 550, z: 0 } })
713
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } })
714
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'arm', to: { part: 'body', face: 'top' } })
715
+ const jointOf = () => asset.joints!.find(j => j.body1.part === 'arm')!
716
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: motionJoint(jointOf(), { ...motionFormOf(jointOf()), kind: 'turn', axis: 'x', id: 'pitch', min: -60, max: 180 }, 'arm') })
717
+
718
+ const fresh = motionFormOf(jointOf(), 1)
719
+ assert.deepEqual([fresh.kind, fresh.axis, fresh.id, fresh.min, fresh.max], ['turn', 'y', '', -90, 90], '새 축은 쓰지 않은 축의 돎')
720
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: motionJoint(jointOf(), fresh, 'arm', 1) })
721
+ assert.equal(jointOf().motion!.length, 2)
722
+ assert.equal(controlIdFor('arm', 'turn', 1), 'arm-turn-2')
723
+ assert.deepEqual(motionOf(asset, 'arm', 1), { kind: 'turn', axis: 'y', control: { id: 'arm-turn-2', unit: 'deg', min: -90, max: 90 } }, '둘째 축은 기록대로 읽는다')
724
+ assert.equal(motionOf(asset, 'arm')?.control?.id, 'pitch', '첫 축은 그대로')
725
+
726
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: motionJoint(jointOf(), { ...motionFormOf(jointOf(), 1), min: -30, max: 30 }, 'arm', 1) })
727
+ assert.deepEqual([jointOf().motion![0]!.lowerLimit, jointOf().motion![0]!.upperLimit, jointOf().motion![1]!.lowerLimit], [-60, 180, -30], '둘째 축만 바뀐다')
728
+
729
+ /* The second axis turns the arm about Y, on top of the first. */
730
+ const turned = (state: Record<string, number>) => compileFigureAsset(asset).evaluate(state).geometry.find(g => g.id === 'arm')!.pose.r
731
+ assert.notDeepEqual(turned({ 'arm-turn-2': 30 }), turned({}))
732
+
733
+ assert.deepEqual(leadersFor(asset, 'arm', 'turn', motionIdOf(jointOf(), 1)).map(a => a.id), ['arm'], '같은 관절의 첫 축은 따를 수 있다')
734
+ assert.deepEqual(leadersFor(asset, 'arm', 'turn', motionIdOf(jointOf(), 0)).map(a => a.id), ['arm.2'], '제 축은 고르지 않는다')
735
+
736
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: motionJoint(jointOf(), { ...motionFormOf(jointOf(), 2), id: 'roll' }, 'arm', 2) })
737
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: withoutAxis(jointOf(), 0) })
738
+ assert.deepEqual(
739
+ jointOf().motion!.map(a => [a.id, a.state?.id]),
740
+ [['arm.2', 'arm-turn-2'], ['arm.3', 'roll']],
741
+ '앞 축을 빼도 뒤 축은 제 이름과 조작값을 지킨다'
742
+ )
743
+ assert.notDeepEqual(turned({ 'arm-turn-2': 30 }), turned({}), '남은 축은 같은 조작값으로 움직인다')
744
+ })
@@ -594,6 +594,9 @@
594
594
  "figure.label.control-start": "start",
595
595
  "figure.label.travel-follows": "travel follows",
596
596
  "figure.button.add-motion": "Add motion",
597
+ "figure.button.add-axis": "Add axis",
598
+ "figure.button.remove-axis": "Remove axis",
599
+ "figure.label.motion-axis-n": "Axis {n}",
597
600
  "figure.button.remove-motion": "Remove motion",
598
601
  "figure.label.room": "Room it takes up",
599
602
  "figure.text.room-explained": "Bounds computed from the parts are a proposal. Nothing is declared until you confirm, and a declaration never widens on its own.",
@@ -594,6 +594,9 @@
594
594
  "figure.label.control-start": "inicio",
595
595
  "figure.label.travel-follows": "el recorrido sigue a",
596
596
  "figure.button.add-motion": "Añadir movimiento",
597
+ "figure.button.add-axis": "Añadir eje",
598
+ "figure.button.remove-axis": "Quitar eje",
599
+ "figure.label.motion-axis-n": "Eje {n}",
597
600
  "figure.button.remove-motion": "Quitar movimiento",
598
601
  "figure.label.room": "Espacio que ocupa",
599
602
  "figure.text.room-explained": "Los límites calculados a partir de las piezas son una propuesta. No se declara nada hasta que lo confirme, y una declaración nunca se amplía por sí sola.",
@@ -594,6 +594,9 @@
594
594
  "figure.label.control-start": "start",
595
595
  "figure.label.travel-follows": "travel follows",
596
596
  "figure.button.add-motion": "Add motion",
597
+ "figure.button.add-axis": "軸を追加",
598
+ "figure.button.remove-axis": "軸を削除",
599
+ "figure.label.motion-axis-n": "軸 {n}",
597
600
  "figure.button.remove-motion": "Remove motion",
598
601
  "figure.label.room": "Room it takes up",
599
602
  "figure.text.room-explained": "Bounds computed from the parts are a proposal. Nothing is declared until you confirm, and a declaration never widens on its own.",
@@ -594,6 +594,9 @@
594
594
  "figure.label.control-start": "시작값",
595
595
  "figure.label.travel-follows": "이동 거리가 따르는 것",
596
596
  "figure.button.add-motion": "움직임 추가",
597
+ "figure.button.add-axis": "축 추가",
598
+ "figure.button.remove-axis": "축 삭제",
599
+ "figure.label.motion-axis-n": "축 {n}",
597
600
  "figure.button.remove-motion": "움직임 삭제",
598
601
  "figure.label.room": "차지하는 공간",
599
602
  "figure.text.room-explained": "부품에서 계산한 경계는 권장 범위입니다. 확인 후 공간 선언에 반영됩니다. 모델을 변경해도 선언된 범위는 자동으로 확장되지 않습니다.",
@@ -594,6 +594,9 @@
594
594
  "figure.label.control-start": "start",
595
595
  "figure.label.travel-follows": "travel follows",
596
596
  "figure.button.add-motion": "Add motion",
597
+ "figure.button.add-axis": "Tambah paksi",
598
+ "figure.button.remove-axis": "Buang paksi",
599
+ "figure.label.motion-axis-n": "Paksi {n}",
597
600
  "figure.button.remove-motion": "Remove motion",
598
601
  "figure.label.room": "Room it takes up",
599
602
  "figure.text.room-explained": "Bounds computed from the parts are a proposal. Nothing is declared until you confirm, and a declaration never widens on its own.",
@@ -594,6 +594,9 @@
594
594
  "figure.label.control-start": "start",
595
595
  "figure.label.travel-follows": "travel follows",
596
596
  "figure.button.add-motion": "Add motion",
597
+ "figure.button.add-axis": "添加轴",
598
+ "figure.button.remove-axis": "删除轴",
599
+ "figure.label.motion-axis-n": "轴 {n}",
597
600
  "figure.button.remove-motion": "Remove motion",
598
601
  "figure.label.room": "Room it takes up",
599
602
  "figure.text.room-explained": "Bounds computed from the parts are a proposal. Nothing is declared until you confirm, and a declaration never widens on its own.",