@things-factory/figure-ui 10.1.210 → 10.1.212

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/graphql/index.ts +1 -1
  2. package/client/modeller/angle-dial.ts +1 -1
  3. package/client/modeller/canvas.ts +1 -1
  4. package/client/modeller/editor.ts +24 -32
  5. package/client/modeller/figure-history-panel.ts +1 -1
  6. package/client/modeller/figure-links.ts +61 -37
  7. package/client/modeller/figure-parts.ts +3 -3
  8. package/client/modeller/figure-settings.ts +1 -1
  9. package/client/modeller/figure-size-editor.ts +41 -4
  10. package/client/modeller/links.ts +63 -24
  11. package/client/modeller/live.ts +33 -0
  12. package/client/modeller/scene-budget.ts +3 -3
  13. package/client/modeller/thumbnail.ts +1 -1
  14. package/client/pages/figure-list-page.ts +2 -2
  15. package/client/pages/figure-modeller-page.ts +4 -4
  16. package/client/types.ts +2 -2
  17. package/client/viewparts/figure-thumb.ts +1 -1
  18. package/dist-client/graphql/index.d.ts +1 -1
  19. package/dist-client/graphql/index.js +1 -1
  20. package/dist-client/graphql/index.js.map +1 -1
  21. package/dist-client/modeller/angle-dial.d.ts +1 -1
  22. package/dist-client/modeller/angle-dial.js.map +1 -1
  23. package/dist-client/modeller/canvas.js +1 -1
  24. package/dist-client/modeller/canvas.js.map +1 -1
  25. package/dist-client/modeller/editor.d.ts +5 -0
  26. package/dist-client/modeller/editor.js +24 -32
  27. package/dist-client/modeller/editor.js.map +1 -1
  28. package/dist-client/modeller/figure-history-panel.d.ts +1 -1
  29. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  30. package/dist-client/modeller/figure-links.d.ts +2 -0
  31. package/dist-client/modeller/figure-links.js +66 -39
  32. package/dist-client/modeller/figure-links.js.map +1 -1
  33. package/dist-client/modeller/figure-parts.d.ts +3 -3
  34. package/dist-client/modeller/figure-parts.js.map +1 -1
  35. package/dist-client/modeller/figure-settings.js +1 -1
  36. package/dist-client/modeller/figure-settings.js.map +1 -1
  37. package/dist-client/modeller/figure-size-editor.d.ts +3 -1
  38. package/dist-client/modeller/figure-size-editor.js +45 -4
  39. package/dist-client/modeller/figure-size-editor.js.map +1 -1
  40. package/dist-client/modeller/links.d.ts +15 -6
  41. package/dist-client/modeller/links.js +63 -25
  42. package/dist-client/modeller/links.js.map +1 -1
  43. package/dist-client/modeller/live.d.ts +14 -0
  44. package/dist-client/modeller/live.js +31 -0
  45. package/dist-client/modeller/live.js.map +1 -1
  46. package/dist-client/modeller/scene-budget.d.ts +3 -3
  47. package/dist-client/modeller/scene-budget.js +3 -3
  48. package/dist-client/modeller/scene-budget.js.map +1 -1
  49. package/dist-client/modeller/thumbnail.js +1 -1
  50. package/dist-client/modeller/thumbnail.js.map +1 -1
  51. package/dist-client/pages/figure-list-page.d.ts +1 -1
  52. package/dist-client/pages/figure-list-page.js +2 -2
  53. package/dist-client/pages/figure-list-page.js.map +1 -1
  54. package/dist-client/pages/figure-modeller-page.d.ts +3 -3
  55. package/dist-client/pages/figure-modeller-page.js +4 -4
  56. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  57. package/dist-client/route.d.ts +1 -1
  58. package/dist-client/tsconfig.tsbuildinfo +1 -1
  59. package/dist-client/types.d.ts +2 -2
  60. package/dist-client/types.js +1 -1
  61. package/dist-client/types.js.map +1 -1
  62. package/dist-client/viewparts/figure-thumb.js +1 -1
  63. package/dist-client/viewparts/figure-thumb.js.map +1 -1
  64. package/docs/multipart-response-review-2026-10-24.md +1 -1
  65. package/docs/response-authoring-2026-10-24.md +4 -4
  66. package/package.json +6 -6
  67. package/test/ai-proposal.test.ts +1 -1
  68. package/test/align.test.ts +1 -1
  69. package/test/browser/editing.check.mjs +38 -0
  70. package/test/browser/modeller.check.mjs +55 -0
  71. package/test/command-preview.test.ts +3 -3
  72. package/test/fields.test.ts +2 -2
  73. package/test/first-minute.test.ts +5 -5
  74. package/test/first-save.test.ts +3 -3
  75. package/test/gizmo.test.ts +3 -3
  76. package/test/keys.test.ts +2 -2
  77. package/test/layout.test.ts +1 -1
  78. package/test/links.test.ts +53 -14
  79. package/test/live.test.ts +4 -4
  80. package/test/material.test.ts +1 -1
  81. package/test/properties.test.ts +1 -1
  82. package/test/proposal-record.test.ts +1 -1
  83. package/test/refusals.test.ts +1 -1
  84. package/test/resize-with-placement.test.ts +1 -1
  85. package/test/scene-budget.test.ts +4 -4
  86. package/test/verdict.test.ts +1 -1
  87. package/translations/en.json +3 -0
  88. package/translations/es.json +3 -0
  89. package/translations/ja.json +3 -0
  90. package/translations/ko.json +109 -106
  91. package/translations/ms.json +3 -0
  92. package/translations/zh.json +3 -0
@@ -96,7 +96,7 @@ test('세 칸의 말은 다섯 말 모두에 있다', () => {
96
96
  }
97
97
  })
98
98
 
99
- test('도형 크기를 따라 움직이는 자리를 가려 읽고, 여기에 고정하면 사라진다 — 드라이룸 램프의 길', () => {
99
+ test('피규어 크기를 따라 움직이는 자리를 가려 읽고, 여기에 고정하면 사라진다 — 드라이룸 램프의 길', () => {
100
100
  let asset = pair()
101
101
  asset = addFigureBox(asset, { id: 'lamp', size: { x: 40, y: 20, z: 40 }, position: { x: 0, y: 10, z: 0 } })
102
102
  const model = asset.document.model
@@ -125,7 +125,7 @@ test('자리 — 부품의 면에 둔 집는 자리·놓는 자리를 그 부품
125
125
  assert.deepEqual(placesOn(asset, 'plate'), [], '다른 부품의 자리는 안 섞인다')
126
126
  })
127
127
 
128
- test('담는 능력이 없는 도형은 자리가 있어도 보드가 아무것도 넘기지 않는다 — 화면이 그것을 말한다', () => {
128
+ test('담는 능력이 없는 피규어는 자리가 있어도 보드가 아무것도 넘기지 않는다 — 화면이 그것을 말한다', () => {
129
129
  const asset = pair()
130
130
  assert.equal(holdsThings(asset), false)
131
131
  asset.document.capabilities.names = ['Capacity', 'Holdable']
@@ -221,7 +221,7 @@ const openSides = () => {
221
221
  }
222
222
 
223
223
  test('도면에서의 크기 — 좁히지 않은 끝은 부품에서 계산하고, 좁힌 끝은 작성자의 값이다', () => {
224
- assert.equal(instanceRangeOf(createFigureAsset('Probe')), undefined, '크기를 선언하지 않은 도형에는 범위가 없다')
224
+ assert.equal(instanceRangeOf(createFigureAsset('Probe')), undefined, '크기를 선언하지 않은 피규어에는 범위가 없다')
225
225
  const asset = openSides()
226
226
  const y = asset.document.model.inputs.find(i => i.id === 'size.y')!
227
227
  y.min = 90
@@ -245,7 +245,7 @@ test('도면에서의 크기 — 숫자를 치면 그 끝을 좁히고, 칸을
245
245
  )
246
246
  })
247
247
 
248
- test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한 번에 따라가게 하며, 무엇이 남는지 이름을 댄다(아키텍트 판정 09-25)', () => {
248
+ test('한 비율로 커지는 피규어 — 자기 숫자인 길이를 세고, 한 번에 따라가게 하며, 무엇이 남는지 이름을 댄다(아키텍트 판정 09-25)', () => {
249
249
  let asset = createFigureAsset('Probe')
250
250
  asset = addFigureBox(asset, { id: 'base', size: { x: 400, y: 100, z: 400 }, position: { x: 0, y: 0, z: 0 } })
251
251
  asset = addFigureBox(asset, { id: 'lamp', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 0, z: 0 } })
@@ -262,7 +262,7 @@ test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한
262
262
  assert.equal(Math.round(valueNow(revisionOf(asset), 'lamp.dimension.x.value')! * 1e6) / 1e6, 60, '따르게 해도 지금 크기는 그대로')
263
263
  })
264
264
 
265
- test('관절이 있는 도형에는 한 비율로 키우기를 권하고, 선언하면 규칙으로 보인다(수석 아키텍트 판정 09-24)', () => {
265
+ test('관절이 있는 피규어에는 한 비율로 키우기를 권하고, 선언하면 규칙으로 보인다(수석 아키텍트 판정 09-24)', () => {
266
266
  let asset = sizedPair()
267
267
  assert.equal(turnsAboutAJoint(asset), false)
268
268
  asset = moving(asset, 'flange', { kind: 'turn', axis: 'y', id: 'flange.turn', min: 0, max: 90 })
@@ -372,7 +372,7 @@ test('따라 움직이는 관절 — 같은 방식으로 움직이고 제 조작
372
372
  assert.deepEqual(motionOf(asset, 'right')?.follows, { sum: [{ joint: 'left', multiplier: -1 }], offset: 0 }, '화면은 무엇을 따르는지 읽는다')
373
373
  })
374
374
 
375
- test('면은 도형이 정한다 — 문틀(portal)은 바깥 여섯에 개구부 셋, 공은 없고 곡면, 원뿔대는 위아래뿐, 원기둥 옆은 면이 아님 (2026-09-25, ADR-0140 3.9)', async () => {
375
+ test('면은 피규어가 정한다 — 문틀(portal)은 바깥 여섯에 개구부 셋, 공은 없고 곡면, 원뿔대는 위아래뿐, 원기둥 옆은 면이 아님 (2026-09-25, ADR-0140 3.9)', async () => {
376
376
  const { facesOf, faceAxisOf, canFasten, alignAxesOf, surfaceOf } = await import('../client/modeller/links.ts')
377
377
  let asset = createFigureAsset('Doorway')
378
378
  asset = addFigurePrimitive(asset, {
@@ -431,20 +431,20 @@ test('돌려 놓인 부품에도 모서리 맞춤을 내놓는다 — 지금 자
431
431
  void compileFigureAsset
432
432
  })
433
433
 
434
- test('모델러에서 만든 도형은 부품에서 재어 도형 크기를 선언한다 — 그 뒤에야 부품이 도형 크기를 따를 수 있다 (사용자 09-25)', async () => {
434
+ test('모델러에서 만든 피규어는 부품에서 재어 피규어 크기를 선언한다 — 그 뒤에야 부품이 피규어 크기를 따를 수 있다 (사용자 09-25)', async () => {
435
435
  const { instanceSizeToDeclare, instanceRangeOf, hasInstanceSize } = await import('../client/modeller/links.ts')
436
436
  const { compileFigureAsset } = await import('@hatiolab/figure-model')
437
437
  let asset = createFigureAsset('Conveyor')
438
438
  assert.equal(instanceSizeToDeclare(asset), undefined, '부품이 없으면 잴 것이 없다')
439
439
  asset = addFigureBox(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
440
440
  asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
441
- assert.equal(hasInstanceSize(asset, 'x'), false, '새 도형에는 도형 크기가 없다')
441
+ assert.equal(hasInstanceSize(asset, 'x'), false, '새 피규어에는 피규어 크기가 없다')
442
442
  const toDeclare = instanceSizeToDeclare(asset)!
443
443
  assert.deepEqual(toDeclare.at, { x: 2000, y: 100, z: 600 }, '지금 부품의 크기가 선언할 크기')
444
444
  asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', at: toDeclare.at })
445
445
  assert.ok(instanceRangeOf(asset), '선언 뒤에는 도면에서의 크기가 보인다')
446
446
  assert.equal(instanceSizeToDeclare(asset), undefined, '이미 있으면 다시 권하지 않는다')
447
- assert.equal(hasInstanceSize(asset, 'x'), true, '부품이 도형 크기를 따르는 선택이 열린다')
447
+ assert.equal(hasInstanceSize(asset, 'x'), true, '부품이 피규어 크기를 따르는 선택이 열린다')
448
448
  asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'frame', axis: 'x', source: { of: 'instance', axis: 'x' } } as never)
449
449
  const at = (size: number) => (compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'frame')!.shape.dimensions as Record<string, number>)['size.x']
450
450
  assert.equal(at(3000), 3000, '보드가 3000 으로 놓으면 틀도 3000')
@@ -490,7 +490,7 @@ test('미끄러짐의 이동 거리가 부품 길이를 따른다 — 레일이
490
490
  assert.equal(end(4500), 0, '레일이 길어져도 끝까지')
491
491
  })
492
492
 
493
- test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자리도 비율로 옮긴다 (사이징, 2026-09-25)', async () => {
493
+ test('면 위 거리를 피규어 크기 비율로 — 피규어가 커지면 그 자리도 비율로 옮긴다 (사이징, 2026-09-25)', async () => {
494
494
  const { alignOf } = await import('../client/modeller/links.ts')
495
495
  const { compileFigureAsset } = await import('@hatiolab/figure-model')
496
496
  let asset = createFigureAsset('Panel')
@@ -498,7 +498,7 @@ test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자
498
498
  asset = addFigureBox(asset, { id: 'lamp', size: { x: 60, y: 60, z: 30 }, position: { x: 0, y: 0, z: 0 } })
499
499
  asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'panel', to: { plane: 'mounting-plane' } })
500
500
  const front = { part: 'panel', face: 'front' }
501
- assert.deepEqual(alignOf(asset, front, { x: 'share' }, { x: 300 }), undefined, '도형 크기가 없으면 비율을 보낼 수 없다')
501
+ assert.deepEqual(alignOf(asset, front, { x: 'share' }, { x: 300 }), undefined, '피규어 크기가 없으면 비율을 보낼 수 없다')
502
502
  asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 200, max: 5000 }, y: { min: 200, max: 5000 }, z: { min: 10, max: 500 } }, at: { x: 1000, y: 800, z: 40 } })
503
503
  asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'panel', axis: 'x', source: { of: 'instance', axis: 'x' } })
504
504
  const align = alignOf(asset, front, { x: 'share' }, { x: 300 })!
@@ -506,7 +506,7 @@ test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자
506
506
  asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp', to: front, align })
507
507
  const x = (size: number) => Math.round(compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'lamp')!.pose.t[0]!)
508
508
  assert.equal(x(1000), 300)
509
- assert.equal(x(2000), 600, '도형이 두 배면 거리도 두 배')
509
+ assert.equal(x(2000), 600, '피규어가 두 배면 거리도 두 배')
510
510
  })
511
511
 
512
512
  test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이 도는 자리까지 재지 않는다 (2026-09-25)', async () => {
@@ -521,7 +521,7 @@ test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이
521
521
  assert.deepEqual(instanceSizeToDeclare(asset)!.at, { x: 995, y: 2100, z: 150 })
522
522
  })
523
523
 
524
- test('바닥에 선 부품도 도형 크기를 선언한 도형이면 도형의 옆면에 맞출 수 있다 (2026-09-25)', async () => {
524
+ test('바닥에 선 부품도 피규어 크기를 선언한 피규어면 피규어의 옆면에 맞출 수 있다 (2026-09-25)', async () => {
525
525
  const { alignAxesOf, alignOf } = await import('../client/modeller/links.ts')
526
526
  let asset = addFigureBox(createFigureAsset('Gate'), { id: 'post', size: { x: 100, y: 2000, z: 150 }, position: { x: 0, y: 0, z: 0 } })
527
527
  assert.deepEqual(alignAxesOf(asset, 'plane'), [{ axis: 'x', edges: false }, { axis: 'z', edges: false }], '크기가 없으면 옆면도 없다')
@@ -616,7 +616,7 @@ test('팔레트의 문틀은 문 크기로 서고, 세 축 모두 크기를 따
616
616
  })
617
617
 
618
618
  /* 크기를 따라감 한 번 — 치수 칸의 단추 하나, 크기를 대신 정했으면 한 줄로 말한다 (ADR 판정 Q1, 09-26). */
619
- test('치수 칸 — 「도형 크기를 따라감」 단추는 follow-size 한 번, 다 따르면 사라진다; 크기를 대신 정하면 한 줄', () => {
619
+ test('치수 칸 — 「피규어 크기를 따라감」 단추는 follow-size 한 번, 다 따르면 사라진다; 크기를 대신 정하면 한 줄', () => {
620
620
  const ko = wordsOf('ko')
621
621
  assert.match(ko['figure.text.size-declared-to-follow'], /\{x\} × \{y\} × \{z\} mm/)
622
622
  })
@@ -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
+ })
package/test/live.test.ts CHANGED
@@ -17,7 +17,7 @@ const slider = () => {
17
17
  return moving(asset, 'block', { kind: 'slide', axis: 'x', id: 'shift', min: 0, max: 60, start: 0 })
18
18
  }
19
19
 
20
- test('끄는 값으로 그 자세를 바로 그린다 — 도형은 그대로', () => {
20
+ test('끄는 값으로 그 자세를 바로 그린다 — 피규어는 그대로', () => {
21
21
  const asset = slider()
22
22
  const before = JSON.stringify(asset)
23
23
  const parts = livePartsOf(asset, { values: { shift: 45 } })!
@@ -26,7 +26,7 @@ test('끄는 값으로 그 자세를 바로 그린다 — 도형은 그대로',
26
26
  assert.equal(livePartsOf(asset, NOTHING_LIVE), null, '끄는 것이 없으면 평소대로')
27
27
  })
28
28
 
29
- test('속성창의 설계 값도 치는 동안 그린다 — 크기가 따라 변하고 도형은 그대로', () => {
29
+ test('속성창의 설계 값도 치는 동안 그린다 — 크기가 따라 변하고 피규어는 그대로', () => {
30
30
  const asset = slider()
31
31
  const before = JSON.stringify(asset)
32
32
  const size = (parts: { id: string; shape?: unknown }[]) => JSON.stringify((parts.find(p => p.id === 'block') as { shape: unknown }).shape)
@@ -40,7 +40,7 @@ test('설계 값과 자세를 가른다 — 모르는 이름은 버린다', () =
40
40
  assert.deepEqual(liveOverridesOf(slider(), { shift: 10, 'block.size.x': 5, nothing: 1 }), { design: { 'block.size.x': 5 }, state: { shift: 10 } })
41
41
  })
42
42
 
43
- test('치는 도중 도형이 못 받는 값이면 지금 모습을 그린다 — 지어낸 도형을 보이지 않는다', () => {
43
+ test('치는 도중 피규어가 못 받는 값이면 지금 모습을 그린다 — 지어낸 피규어를 보이지 않는다', () => {
44
44
  assert.equal(livePartsOf(slider(), { values: { 'block.size.x': -5 } }), null)
45
45
  })
46
46
 
@@ -51,7 +51,7 @@ test('재질의 끄는 값(발광)도 그린다 — 편집 하나를 기록 없
51
51
  edit: { kind: 'set-material', target: 'block', material: { token: 'palette.primary', emissive: { token: 'palette.primary', intensity: 0.5, on: true } } }
52
52
  })!
53
53
  assert.equal(glowing.find(p => p.id === 'block')!.material.emissive?.intensity, 0.5)
54
- assert.equal(asset.appearance[0]!.material.emissive, undefined, '도형은 그대로')
54
+ assert.equal(asset.appearance[0]!.material.emissive, undefined, '피규어는 그대로')
55
55
  })
56
56
 
57
57
  test('이름표를 끌면 값이 크기에 맞는 걸음으로 움직인다 — 각도 1°, 길이는 자릿수에 따라, Shift 는 열 배 곱게', () => {
@@ -57,7 +57,7 @@ test('곡면 분할은 둥근 몸체에만 내고, 고르면 삼각형 수가
57
57
  assert.ok(fine > coarse, `48 분할이 8 분할보다 삼각형이 많다 (${coarse} → ${fine})`)
58
58
  })
59
59
 
60
- test('자산에 없는 색 역할은 그 색을 함께 보낸다 — 새 도형의 팔레트는 기본색 하나다', () => {
60
+ test('자산에 없는 색 역할은 그 색을 함께 보낸다 — 새 피규어의 팔레트는 기본색 하나다', () => {
61
61
  const asset = probe()
62
62
  const offered = offeredPaletteOf({ 'palette.primary': '#000000', 'palette.accent': '#f28c28', 'palette.dark': '#2b2b2b' }, asset.palette)
63
63
  assert.equal(offered['palette.primary'], asset.palette['palette.primary'], '자산의 색이 앱의 색 위에 선다')
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **도형의 속성을 고친다** (2026-09-24).
2
+ * **피규어의 속성을 고친다** (2026-09-24).
3
3
  *
4
4
  * 속성 탭은 이름 · 배치 기준 · 차지하는 공간 · 분류 · 태그를 맨 위에 둔다. 전에는 타입 칸 하나뿐이었다.
5
5
  * 두 축을 본다: 탭이 내는 명령이 figure-model 에서 **정말로** 그 일을 하는가, 그리고 화면이 명령과 행 쓰기를 잇는가.
@@ -115,7 +115,7 @@ test('the dock checks the result from the server: saved is accepted and named, n
115
115
  const { proposalOutcomeFrom } = await import('../client/modeller/ai-proposal.js')
116
116
  const words = JSON.parse(read('../translations/ko.json'))
117
117
  const sayKo = (key: string, params: Record<string, string> = {}) => words[key].replace(/\{(\w+)\}/g, (_: string, k: string) => params[k] ?? '')
118
- assert.deepEqual(proposalOutcomeFrom([{ figureId: 'x', type: 'LIDDED', name: '뚜껑 상자' }], sayKo), { verdict: 'accepted', note: '저장된 도형에 들어 있습니다: 뚜껑 상자' })
118
+ assert.deepEqual(proposalOutcomeFrom([{ figureId: 'x', type: 'LIDDED', name: '뚜껑 상자' }], sayKo), { verdict: 'accepted', note: '저장된 피규어에 들어 있습니다: 뚜껑 상자' })
119
119
  assert.equal(proposalOutcomeFrom([], sayKo), undefined, 'not saved is not known, never refused')
120
120
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) assert.ok(JSON.parse(read(`../translations/${lang}.json`))['figure.text.proposal-saved-into'], lang)
121
121
  })
@@ -271,7 +271,7 @@ test('단면 · 속 공간 — 모서리, 벽, 막힌 끝, 붙은 면을 거절
271
271
  assert.match(editRefusalOf(refusalOf(() => act(wedge, { kind: 'set-section', part: 'wedge', points: crossed })), keyed), /^figure\.text\.edit-refused .*"code":"SECTION_INVALID"/)
272
272
  })
273
273
 
274
- test('도형의 한도 — 노드 수, 저장 크기, 중첩 깊이를 숫자로 말한다 (「노드 한도」로 잘못 말하던 결함)', () => {
274
+ test('피규어의 한도 — 노드 수, 저장 크기, 중첩 깊이를 숫자로 말한다 (「노드 한도」로 잘못 말하던 결함)', () => {
275
275
  const doc = structuredClone(box().document) as { model: { nodes: unknown[] } }
276
276
  for (let i = 0; i <= fm.FIGURE_DOCUMENT_NODE_LIMIT; i++) doc.model.nodes.push({ id: `k${i}`, op: 'const@1', args: [], outputs: {} })
277
277
  const nodes = editRefusalOf(refusalOf(() => fm.compileFigureDocument(doc as never)), say)
@@ -94,7 +94,7 @@ test('fresh, width only, and all set back to fixed are three different answers',
94
94
  assert.deepEqual(resizeAxesOf(following, 'box'), { axes: ['y'], basis: 'chosen' })
95
95
  assert.deepEqual(resizeAxesOf(following, 'box', ['x']), { axes: ['x', 'y'], basis: 'chosen' })
96
96
  const ko = JSON.parse(readFileSync(new URL('../translations/ko.json', import.meta.url), 'utf-8'))
97
- assert.equal(ko['figure.button.follow-size'], '도형 크기에 맞추기')
97
+ assert.equal(ko['figure.button.follow-size'], '피규어 크기에 맞추기')
98
98
  })
99
99
 
100
100
  test('a part off the floor is not moved by choosing its size response', () => {
@@ -45,9 +45,9 @@ describe('한 화면이 감당하는 양', () => {
45
45
  })
46
46
 
47
47
  /*
48
- 실측한 두 도형이 이 분모로 되돌아오는지 본다. 분모를 누가 고치면 이 둘이 먼저 어긋난다.
48
+ 실측한 두 피규어가 이 분모로 되돌아오는지 본다. 분모를 누가 고치면 이 둘이 먼저 어긋난다.
49
49
  */
50
- it('잰 두 도형이 잰 수와 맞다', () => {
50
+ it('잰 두 피규어가 잰 수와 맞다', () => {
51
51
  /* 묶음 9 짜리 6 축 로봇: 600 대에서 59.5fps, 700 대에서 56.7fps 였다. */
52
52
  assert.ok(capacityFrom(9, 200)!.instances <= 600, '잰 값보다 많이 놓으라고 하면 안 된다')
53
53
  /* 묶음 2 짜리 컨베이어: 1,600 대에서 59.3fps, 2,500 대에서 36.8fps 였다. */
@@ -60,8 +60,8 @@ describe('한 화면이 감당하는 양', () => {
60
60
  * 놓는 개수로 값을 말하지 않는다 — ADR-0051 ③.
61
61
  *
62
62
  * 보고서에 「100개 기준 draw call」 · 「1만개 기준」이 오래 남아 있었다. 놓는 개수는 값을 치르지
63
- * 않는다(가만히 선 도형은 프레임 비용 0, 카메라 밖은 컬링). 치르는 것은 한 화면에 보이는 개수 ×
64
- * 그 도형의 묶음 수이고, 그 답은 위의 실측 분모가 한다.
63
+ * 않는다(가만히 선 피규어는 프레임 비용 0, 카메라 밖은 컬링). 치르는 것은 한 화면에 보이는 개수 ×
64
+ * 그 피규어의 묶음 수이고, 그 답은 위의 실측 분모가 한다.
65
65
  */
66
66
  describe('놓는 개수 기준 문구가 화면에 없다', () => {
67
67
  const HERE = fileURLToPath(new URL('.', import.meta.url))
@@ -36,7 +36,7 @@ test('개수는 목록 카드와 같은 셈이다 — 원가를 같은 capacityF
36
36
  assert.deepEqual(measured.report, figureReportOf(asset), '탭은 그 셈을 부르고 보여 줄 뿐이다')
37
37
  assert.equal(measured.told.instances, capacityFrom(cost.groups, cost.triangles)!.instances, '탭 이름표의 개수도 같은 셈에서 온다')
38
38
  assert.deepEqual(measured.told.budget, budgetOf(figureReportOf(asset)), '부품 목록 머리의 줄도 같은 보고서에서 온다')
39
- assert.deepEqual(verdictMeasureOf(undefined), { report: undefined, told: { instances: undefined, budget: undefined } }, '연 도형이 없으면 잴 것도 알릴 개수도 없다')
39
+ assert.deepEqual(verdictMeasureOf(undefined), { report: undefined, told: { instances: undefined, budget: undefined } }, '연 피규어가 없으면 잴 것도 알릴 개수도 없다')
40
40
  })
41
41
 
42
42
  test('판정은 발행 패널과 같은 서버 문에서 온다 — 화면이 따로 판정하지 않는다', () => {
@@ -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.",