@things-factory/figure-ui 10.1.181 → 10.1.185

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 (67) hide show
  1. package/client/modeller/editor-styles.ts +28 -9
  2. package/client/modeller/editor.ts +105 -12
  3. package/client/modeller/fasten-form.ts +207 -0
  4. package/client/modeller/fasten-geometry.ts +5 -1
  5. package/client/modeller/figure-facts.ts +4 -35
  6. package/client/modeller/figure-fasten-dialog.ts +457 -0
  7. package/client/modeller/figure-fasten-drawing.ts +32 -11
  8. package/client/modeller/figure-links.ts +56 -300
  9. package/client/modeller/figure-properties.ts +23 -70
  10. package/client/modeller/modeller-keys.ts +4 -4
  11. package/dist-client/modeller/clips.d.ts +2 -2
  12. package/dist-client/modeller/editor-styles.js +28 -9
  13. package/dist-client/modeller/editor-styles.js.map +1 -1
  14. package/dist-client/modeller/editor.d.ts +23 -2
  15. package/dist-client/modeller/editor.js +121 -12
  16. package/dist-client/modeller/editor.js.map +1 -1
  17. package/dist-client/modeller/fasten-form.d.ts +69 -0
  18. package/dist-client/modeller/fasten-form.js +131 -0
  19. package/dist-client/modeller/fasten-form.js.map +1 -0
  20. package/dist-client/modeller/fasten-geometry.d.ts +3 -1
  21. package/dist-client/modeller/fasten-geometry.js +6 -1
  22. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  23. package/dist-client/modeller/figure-facts.d.ts +2 -13
  24. package/dist-client/modeller/figure-facts.js +2 -29
  25. package/dist-client/modeller/figure-facts.js.map +1 -1
  26. package/dist-client/modeller/figure-fasten-dialog.d.ts +69 -0
  27. package/dist-client/modeller/figure-fasten-dialog.js +433 -0
  28. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -0
  29. package/dist-client/modeller/figure-fasten-drawing.d.ts +6 -3
  30. package/dist-client/modeller/figure-fasten-drawing.js +36 -10
  31. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  32. package/dist-client/modeller/figure-links.d.ts +14 -38
  33. package/dist-client/modeller/figure-links.js +58 -301
  34. package/dist-client/modeller/figure-links.js.map +1 -1
  35. package/dist-client/modeller/figure-properties.d.ts +12 -12
  36. package/dist-client/modeller/figure-properties.js +23 -72
  37. package/dist-client/modeller/figure-properties.js.map +1 -1
  38. package/dist-client/modeller/modeller-keys.js +4 -4
  39. package/dist-client/modeller/modeller-keys.js.map +1 -1
  40. package/dist-client/route.d.ts +1 -1
  41. package/dist-client/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +8 -8
  43. package/test/browser/links-page.js +4 -4
  44. package/test/browser/links.check.mjs +196 -46
  45. package/test/browser/modeller-page.js +23 -0
  46. package/test/browser/modeller.check.mjs +19 -20
  47. package/test/browser/namespace-page.js +2 -29
  48. package/test/browser/popup-stand-in.js +48 -0
  49. package/test/browser/product-page.check.mjs +60 -9
  50. package/test/browser/product-page.js +30 -33
  51. package/test/browser/property-section.check.mjs +3 -3
  52. package/test/browser/props.check.mjs +28 -7
  53. package/test/browser/screen-page.js +7 -2
  54. package/test/browser/screen.check.mjs +28 -23
  55. package/test/browser/section-page.js +13 -41
  56. package/test/browser/section.check.mjs +39 -30
  57. package/test/fasten-form.test.ts +77 -0
  58. package/test/fasten-geometry.test.ts +2 -0
  59. package/test/first-minute.test.ts +7 -4
  60. package/test/properties.test.ts +11 -19
  61. package/translations/en.json +5 -12
  62. package/translations/es.json +5 -12
  63. package/translations/ja.json +5 -12
  64. package/translations/ko.json +5 -12
  65. package/translations/ms.json +5 -12
  66. package/translations/zh.json +5 -12
  67. package/client/modeller/mounting-part.ts +0 -15
@@ -521,40 +521,32 @@ describe('the inside\'s drawing in the 단면 group', () => {
521
521
  })
522
522
  })
523
523
 
524
- describe('the fastening drawing in the fastening form', () => {
524
+ describe('the fastening drawing in the fastening window', () => {
525
525
  const labels = () => read(() => probe.fastenForm().map(s => s.label))
526
- const target = () => read(() => probe.fastenForm().find(s => s.label === 'figure.label.target-part'))
527
- /* A fastening the part has is said in one line; 「바꾸기」 opens the drawing (chief architect 2026-10-16). */
528
- const change = () => press('button[change-fastening]')
526
+ const targets = () => read(() => probe.fastenTargets())
527
+ /* A fastening the part has is said in one line; 「바꾸기」 opens the window on it (user 2026-10-17). */
528
+ const change = async () => {
529
+ await press('button[change-fastening]')
530
+ await until(() => !!probe.fastenTargets(), 'the fastening window')
531
+ }
529
532
 
530
- test('the part\'s own face and the facing are not asked: the target, its face and how are drawn', async () => {
533
+ test('the part\'s own face and the facing are not asked: what, which face and how, the face and the way drawn', async () => {
531
534
  await mount('arm')
532
535
  await pick('forearm')
533
536
  await change()
534
537
  const asked = await labels()
535
- assert.ok(asked.includes('figure.label.target-part'))
538
+ assert.deepEqual(asked.slice(0, 3), ['figure.label.fasten-what', 'figure.label.fasten-face-on-part', 'figure.label.fasten-how'])
536
539
  assert.ok(!asked.includes('figure.label.my-face') && !asked.includes('figure.label.facing'), `asked: ${asked}`)
537
540
  assert.ok(await read(() => !!probe.fastenDrawing()), 'the drawing in their place')
538
541
  })
539
542
 
540
- test('a fastened part: a face of the target pressed is one step', async () => {
543
+ test('a fastened part: a face of the target pressed, flush pressed, the footprint dragged -- tried, nothing written', async () => {
541
544
  const start = await state()
542
545
  const was = await read(() => probe.fastening('forearm'))
543
546
  const face = was.body0.face === 'front' ? 'right' : 'front'
544
547
  await press(`[face="${face}"]`, { within: 'figure-fasten-drawing' })
545
- await until(r => probe.state().revision === r + 1, 'one step', start.revision)
546
- assert.equal((await read(() => probe.fastening('forearm'))).body0.face, face)
547
- })
548
-
549
- test('a fastened part: flush pressed is one step, and the drawing shows it', async () => {
550
- const start = await state()
551
548
  await press('button[mode="flush"]', { within: 'figure-fasten-drawing' })
552
- await until(r => probe.state().revision === r + 1, 'one step', start.revision)
553
- assert.equal(await read(() => probe.fastenMode()), 'flush')
554
- })
555
-
556
- test('a fastened part: the footprint dragged on the face and let go is one step', async () => {
557
- const start = await state()
549
+ await until(() => probe.fastenMode() === 'flush', 'the drawing shows flush')
558
550
  const foot = await element('g[footprint] rect', { within: 'figure-fasten-drawing' })
559
551
  const at = await foot.evaluate(e => {
560
552
  e.scrollIntoView({ block: 'center' })
@@ -565,25 +557,46 @@ describe('the fastening drawing in the fastening form', () => {
565
557
  await page().mouse.down()
566
558
  for (let i = 1; i <= 5; i++) await page().mouse.move(at.x + 6 * i, at.y)
567
559
  await page().mouse.up()
560
+ await sleep(300)
561
+ assert.equal((await state()).revision, start.revision, 'nothing written while trying')
562
+ assert.deepEqual(await read(() => probe.fastening('forearm')), was)
563
+ })
564
+
565
+ test('a fastened part: 적용 writes all of it as one step', async () => {
566
+ const start = await state()
567
+ const was = await read(() => probe.fastening('forearm'))
568
+ const face = was.body0.face === 'front' ? 'right' : 'front'
569
+ await press('[apply]', { within: 'figure-fasten-dialog' })
568
570
  await until(r => probe.state().revision === r + 1, 'one step', start.revision)
569
571
  await sleep(300)
570
572
  assert.equal((await state()).revision, start.revision + 1, 'and only one')
573
+ const now = await read(() => probe.fastening('forearm'))
574
+ assert.deepEqual([now.body0.face, now.origin?.facing], [face, 'flush'])
571
575
  })
572
576
 
573
577
  test('fastened to the plane: the plane is named by the placement, chosen, and set on only', async () => {
574
578
  await mount('rollers')
575
579
  await pick('bed')
576
580
  await change()
577
- const list = await target()
578
- assert.equal(list.value, '__plane')
579
- assert.equal(list.options.find(o => o.value === '__plane').text, 'figure.label.resize-place-floor')
581
+ const list = await targets()
582
+ const plane = list.find(t => t.target === '__plane')
583
+ assert.deepEqual([plane.selected, plane.text], [true, 'figure.label.resize-place-floor'])
584
+ assert.equal((await labels())[1], 'figure.label.fasten-face-on-plane', 'the face asked on the floor, not of a part')
580
585
  assert.deepEqual((await read(() => probe.fastenDrawing())).modes, [], 'one way only: no choice offered')
581
586
  })
582
587
 
588
+ /* The window is a popup over the work: closed, nothing written, before another part is picked. */
589
+ const cancel = async () => {
590
+ await press('[cancel]', { within: 'figure-fasten-dialog' })
591
+ await until(() => !probe.fastenTargets(), 'the window closed')
592
+ }
593
+
583
594
  test('a part that turns in place may have its centre on the face; a part that does not, not', async () => {
595
+ await cancel()
584
596
  await pick('roller')
585
597
  await change()
586
598
  assert.deepEqual((await read(() => probe.fastenDrawing())).modes, ['meet', 'flush', 'centre'])
599
+ await cancel()
587
600
  await pick('mark')
588
601
  await change()
589
602
  assert.deepEqual((await read(() => probe.fastenDrawing())).modes, ['meet', 'flush'])
@@ -592,19 +605,15 @@ describe('the fastening drawing in the fastening form', () => {
592
605
  test('a new fastening waits for 붙이기; the part\'s own face follows the target\'s face and how', async () => {
593
606
  await mount('door')
594
607
  await pick('leaf')
595
- await press('button[act]', { has: 'figure.button.fasten-start' })
596
- await until(() => probe.fastenForm().some(s => s.label === 'figure.label.target-part'), 'the target asked')
597
- const targetSelect = await page().evaluateHandle(() => {
598
- const deep = (root = document) => [...root.querySelectorAll('select')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
599
- return deep().find(s => s.previousElementSibling?.textContent?.trim() === 'figure.label.target-part') ?? null
600
- })
601
- await targetSelect.select('frame')
608
+ await press('button[fasten-start]')
609
+ await until(() => !!probe.fastenTargets(), 'the window')
610
+ await press('md-filter-chip[data-value="frame"]', { within: 'figure-fasten-dialog' })
602
611
  await until(() => !!probe.fastenDrawing(), 'the fastening drawing')
603
612
  const start = await state()
604
613
  await press('[face="front"]', { within: 'figure-fasten-drawing' })
605
614
  await sleep(300)
606
615
  assert.equal((await state()).revision, start.revision, 'nothing sent before 붙이기')
607
- await press('button[act]', { text: 'linkfigure.button.fasten' })
616
+ await press('[apply]', { within: 'figure-fasten-dialog', has: 'figure.button.fasten' })
608
617
  await until(r => probe.state().revision === r + 1, 'one step', start.revision)
609
618
  const joint = await read(() => probe.fastening('leaf'))
610
619
  assert.deepEqual(joint.body0, { part: 'frame', face: 'front' })
@@ -0,0 +1,77 @@
1
+ /*
2
+ * The fastening as it is chosen in the window (`fasten-form.ts`): opened on what the part has, aimed, changed by the
3
+ * drawing, and the one attach command worked out from it -- the command the window applies, its preview shows and the
4
+ * panel's 「정렬 고치기」 sends.
5
+ */
6
+ import { test } from 'node:test'
7
+ import assert from 'node:assert/strict'
8
+
9
+ import { addFigureBox, applyFigureAuthoring, createFigureAsset } from '@hatiolab/figure-model'
10
+ import type { FigureAsset } from '@hatiolab/figure-model'
11
+
12
+ import { aimFasten, fastenChanged, fastenDetailWordsOf, fastenFormOf, fastenPlanOf, fastenSentenceOf, fastenTargetsOf, freshFasten } from '../client/modeller/fasten-form.ts'
13
+ import { revisionOf } from './revision-of.ts'
14
+
15
+ const box = (asset: FigureAsset, id: string, [x, y, z]: number[], at = [0, 0, 0]) => addFigureBox(asset, { id, size: { x, y, z }, position: { x: at[0]!, y: at[1]!, z: at[2]! } })
16
+ const fasten = (asset: FigureAsset, part: string, to: string, more = {}) =>
17
+ applyFigureAuthoring(asset, { kind: 'attach', part, face: 'bottom', to: to === 'plane' ? { plane: 'mounting-plane' } : { part: to, face: 'top' }, ...more } as never)
18
+
19
+ /* A frame on the floor, a roller on the frame 300 mm off its centre, a loose lid beside them. */
20
+ function conveyor() {
21
+ let a = fasten(box(createFigureAsset('Conveyor'), 'frame', [2000, 100, 600], [0, 50, 0]), 'frame', 'plane')
22
+ a = fasten(box(a, 'roller', [60, 60, 560], [300, 130, 0]), 'roller', 'frame', { align: { x: { mm: 300 } } })
23
+ return revisionOf(box(a, 'lid', [100, 20, 100], [1500, 10, 0]))
24
+ }
25
+ const say = (key: string, params: Record<string, string> = {}) => `${key}(${Object.entries(params).map(([k, v]) => `${k}=${v}`).join(',')})`
26
+
27
+ test('opened on what the part has; a loose part opens on nothing chosen', () => {
28
+ const r = conveyor()
29
+ const own = fastenFormOf(r, 'roller')
30
+ assert.deepEqual([own.target, own.targetFace, own.toPlane, own.align.x, own.alignMm.x], ['frame', 'top', false, 'mm', 300])
31
+ assert.deepEqual(fastenFormOf(r, 'lid'), freshFasten())
32
+ assert.equal(fastenPlanOf(r, 'lid', fastenFormOf(r, 'lid')).action, undefined, 'nothing to apply until something is chosen')
33
+ })
34
+
35
+ test('the one it has, unchanged, comes to the attach that replaces it with the same place across the face', () => {
36
+ const r = conveyor()
37
+ const plan = fastenPlanOf(r, 'roller', fastenFormOf(r, 'roller'))
38
+ assert.deepEqual(plan.action, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' }, facing: 'meet', twist: 0, align: { x: { mm: 300 } }, replace: true })
39
+ assert.deepEqual(plan.face, { part: 'frame', face: 'top' }, 'the face it meets, lit while tried')
40
+ assert.equal(fastenSentenceOf('roller', fastenFormOf(r, 'roller'), plan, '바닥', say), 'figure.text.fasten-sentence-meet(part=roller,face=figure.label.face-bottom(),to=frame의 figure.label.face-top())')
41
+ })
42
+
43
+ test('aimed at a part, the loose lid keeps its place across the face; aimed at the plane, nothing is said of a part face', () => {
44
+ const r = conveyor()
45
+ const onFrame = aimFasten(r, 'lid', fastenFormOf(r, 'lid'), { part: 'frame' })
46
+ assert.deepEqual([onFrame.target, onFrame.toPlane, onFrame.align.x, onFrame.alignMm.x], ['frame', false, 'mm', 1500])
47
+ const plan = fastenPlanOf(r, 'lid', onFrame)
48
+ assert.equal((plan.action as { replace: boolean }).replace, false, 'a new fastening')
49
+ const onPlane = fastenPlanOf(r, 'lid', aimFasten(r, 'lid', onFrame, { plane: true }))
50
+ assert.deepEqual((onPlane.action as { to: unknown }).to, { plane: 'mounting-plane' })
51
+ assert.equal(onPlane.face, undefined)
52
+ })
53
+
54
+ test('the drawing\'s changes: another face aims again, the way, the gap, a place across the face', () => {
55
+ const r = conveyor()
56
+ let f = fastenFormOf(r, 'roller')
57
+ f = fastenChanged(r, 'roller', f, { mode: 'flush' })
58
+ assert.equal(f.facing, 'flush')
59
+ f = fastenChanged(r, 'roller', f, { gap: 40 })
60
+ assert.deepEqual([f.gap, f.gapKept], [40, undefined])
61
+ f = fastenChanged(r, 'roller', f, { axis: 'x', how: 'max' })
62
+ assert.equal(f.align.x, 'max')
63
+ assert.deepEqual(fastenDetailWordsOf(f, say), ['figure.text.fasten-summary-gap(mm=40)'])
64
+ const action = fastenPlanOf(r, 'roller', f).action as { facing: string; gap: number; align: unknown }
65
+ assert.deepEqual([action.facing, action.gap, action.align], ['flush', 40, { x: 'max' }])
66
+ f = fastenChanged(r, 'roller', f, { targetFace: 'front' })
67
+ assert.equal(fastenPlanOf(r, 'roller', f).targetFace, 'front')
68
+ })
69
+
70
+ test('the target list: every other part, the one hanging from this part listed as a loop', () => {
71
+ const r = conveyor()
72
+ assert.deepEqual(fastenTargetsOf(r, 'frame'), [
73
+ { part: 'roller', loops: true },
74
+ { part: 'lid', loops: false }
75
+ ])
76
+ assert.deepEqual(fastenTargetsOf(r, 'lid').map(t => t.part), ['frame', 'roller'])
77
+ })
@@ -87,6 +87,8 @@ test('the drawing\'s two names do not lie over each other (user 2026-10-16: 「r
87
87
  assert.deepEqual(thin.face, { x: 20, y: 55 })
88
88
  assert.equal(thin.part.inside, false)
89
89
  assert.ok(thin.part.y > 63 + 84 || thin.part.y < 63, 'outside the part')
90
+ /* As tall as the face, almost: under the part is the face's edge, and the name goes under that line (2026-10-18). */
91
+ assert.ok(thin.part.y - 11 >= face.y + face.h + 2, `clear of the face's edge: ${thin.part.y}`)
90
92
  /* A wide block: its name in it. */
91
93
  const wide = fastenLabelsOf(face, { x: 120, y: 80, w: 100, h: 40 }, 'lid', 180)
92
94
  assert.deepEqual(wide.part, { x: 170, y: 104, inside: true })
@@ -52,11 +52,14 @@ test('the start card and the figure panel choose the placement as the figure\'s
52
52
  })
53
53
 
54
54
  /* B.1 and the designer's items (2026-09-27): each target's panel is found as it was left; the fastening form says which it is. */
55
- test('the fastening form says whether it edits the current relation or fastens anew', () => {
56
- /* Anew is said, the one it has changed at once, 붙이기 only for a new one: the browser check's (test/browser/screen.check.mjs). Here, the words. */
55
+ test('the fastening window says by its button whether it changes the one the part has or fastens anew', () => {
56
+ /* 붙이기 for a new one, 적용 for the one it has, nothing more said: the browser check's (test/browser/screen.check.mjs). Here, the words. */
57
57
  const ko = JSON.parse(read('../translations/ko.json'))
58
- assert.equal(ko['figure.text.fasten-new'], '새로 붙이기')
59
- assert.equal(ko['figure.button.fasten-update'], undefined)
58
+ assert.equal(ko['figure.button.fasten'], '붙이기')
59
+ assert.equal(ko['figure.button.apply'], '적용')
60
+ assert.equal(ko['figure.text.fasten-new'], undefined)
61
+ /* The questions point at the result sentence's pieces: which part, which of its faces, how it is set there (user 2026-10-18). */
62
+ assert.deepEqual(['figure.label.fasten-what', 'figure.label.fasten-face-on-part', 'figure.label.fasten-face-on-plane', 'figure.label.fasten-how'].map(k => ko[k]), ['어느 부품에', '{part}의 어느 면에', '{plane}의 어디에', '어떻게 놓을지'])
60
63
  })
61
64
 
62
65
  test('a measure that follows something says what, where the row once said 「다른 규칙에 연결됨」 (user 2026-10-16)', () => {
@@ -13,7 +13,6 @@ import { addFigureBox, applyFigureAuthoring, createFigureAsset, inspectFigureAss
13
13
 
14
14
  import { tagsOf, figureFigureFacts } from '../client/modeller/figure-facts.ts'
15
15
  import { occupancyFromParts } from '../client/modeller/occupancy.ts'
16
- import { offeredMountingOf } from '../client/modeller/mounting-part.ts'
17
16
  import { revisionOf } from './revision-of.ts'
18
17
  import { wordsOf } from './words.ts'
19
18
 
@@ -25,11 +24,10 @@ const probe = () => {
25
24
  }
26
25
  const facts = (asset: ReturnType<typeof probe>) => figureFigureFacts(revisionOf(asset))
27
26
 
28
- test('탭이 읽는 사실 — 선언 전에는 배치 기준도 장착면도 「없음」이다', () => {
27
+ test('탭이 읽는 사실 — 이름, 놓이는 곳, 이름 붙은 장착면뿐이다', () => {
29
28
  const read = facts(probe())
29
+ assert.deepEqual(Object.keys(read).sort(), ['mounting', 'name', 'placement'])
30
30
  assert.equal(read.name, 'Probe')
31
- assert.deepEqual(read.parts, ['body'])
32
- assert.equal(read.declared, false)
33
31
  assert.equal(read.placement, undefined, '없는 것을 바닥으로 읽지 않는다')
34
32
  assert.equal(read.mounting, undefined)
35
33
  })
@@ -39,7 +37,6 @@ test('배치 기준을 고르면 그 기준으로 공간을 선언한다 — 판
39
37
  const declared = applyFigureAuthoring(parseFigureAsset(source), occupancyFromParts(parseFigureAsset(source), 'ceiling'))
40
38
 
41
39
  assert.equal(facts(declared).placement, 'ceiling')
42
- assert.equal(facts(declared).declared, true)
43
40
  assert.ok(!inspectFigureAsset(declared).violations.some(v => v.code === 'OCCUPANCY_UNDECLARED'))
44
41
  })
45
42
 
@@ -78,18 +75,13 @@ test('속성 탭의 말은 다섯 말 모두에 있다', () => {
78
75
  }
79
76
  })
80
77
 
81
- /* Walk 1 (2026-09-26): the mounting face offered is where the figure meets its plane -- a conveyor's legs, not its bed. */
82
- test('장착면은 도형이 평면에 닿는 부품으로 권한다 — 다리 위 바닥이 아니라 다리', async () => {
83
- const fm = await import('@hatiolab/figure-model')
84
- let asset = fm.addFigureBox(fm.createFigureAsset('Conveyor'), { id: 'bed', size: { x: 3000, y: 700, z: 800 }, position: { x: 0, y: 1050, z: 0 } })
85
- asset = fm.addFigureBox(asset, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: -1400, y: 350, z: -300 } })
86
- asset = fm.applyFigureAuthoring(asset, { kind: 'attach', part: 'leg', face: 'top', to: { part: 'bed', face: 'bottom' }, align: { x: 'min', z: 'min' } })
87
- const facts = figureFigureFacts(revisionOf(asset))
88
- assert.deepEqual(facts.suggested, { part: 'leg', face: 'bottom' })
89
- assert.equal(facts.parts[0], 'bed', '목록의 첫 부품은 바닥이다')
90
- assert.deepEqual(offeredMountingOf(facts), { part: 'leg', face: 'bottom' }, 'where it meets its plane')
91
- assert.deepEqual(offeredMountingOf(facts, { part: 'bed', face: 'top' }), { part: 'bed', face: 'top' }, 'a part picked comes first')
92
- assert.deepEqual(offeredMountingOf({ ...facts, mounting: { part: 'bed', face: 'bottom' } }), { part: 'bed', face: 'bottom' }, 'then the one set, before the suggestion')
93
- assert.deepEqual(offeredMountingOf(facts, { part: 'gone' }), { part: 'leg', face: 'bottom' }, 'a part no longer there is passed over')
94
- assert.deepEqual(offeredMountingOf({ parts: ['bed'] }), { part: 'bed', face: 'bottom' }, 'nothing to suggest: the first part')
78
+ /*
79
+ * The walk without the standing group (user 2026-10-18): nothing on the tab declares the room or names a face. The
80
+ * room is declared where release asks for it; whether the figure meets its plane the release check says from the parts.
81
+ */
82
+ test('속성 탭에는 공간을 정하거나 장착면을 고르는 명령이 없다 — 이름 붙은 장착면을 떼는 것만 있다', () => {
83
+ const tab = read('../client/modeller/figure-properties.ts')
84
+ const kinds = [...tab.matchAll(/kind: '([a-z-]+)'/g)].map(m => m[1]).sort()
85
+ assert.deepEqual(kinds, ['clear-mounting-face', 'rename', 'set-placement'])
86
+ assert.doesNotMatch(tab, /occupancyFromParts|declare-occupancy|set-mounting-face/)
95
87
  })
@@ -39,7 +39,6 @@
39
39
  "figure.button.save": "save",
40
40
  "figure.button.see-the-trend-chart": "see the trend chart",
41
41
  "figure.button.snap": "align to grid",
42
- "figure.label.declared-space": "space it takes",
43
42
  "figure.label.follows-expression": "set by an expression",
44
43
  "figure.label.gizmo-rotate": "turn",
45
44
  "figure.label.gizmo-scale": "size",
@@ -48,8 +47,6 @@
48
47
  "figure.label.position-in-proportion": "in proportion to the figure size",
49
48
  "figure.label.primitive-rounded-box": "rounded box",
50
49
  "figure.text.catalog-needs-a-save": "can be set once the figure is saved.",
51
- "figure.text.declared-space-declared": "fitted to the parts",
52
- "figure.text.declared-space-none": "not set",
53
50
  "figure.text.fasten-would-loop": "that part is already fastened to this one, so it cannot be fastened the other way.",
54
51
  "figure.text.follow-would-loop": "that dimension already follows this part, so the two cannot follow each other.",
55
52
  "figure.text.links-need-a-box": "size following works on box-shaped parts, fastening on boxes and cylinders.",
@@ -90,11 +87,9 @@
90
87
  "figure.text.size-range-part-gone": "at its size now, {at} mm on {axis}, a part already has no length. change the link or the size.",
91
88
  "figure.text.input-range-size": "figure size {axis} can only be set between {min} and {max} mm. value given: {value} mm",
92
89
  "figure.text.input-range": "this value can only be set between {min} and {max}. value given: {value}",
93
- "figure.text.mounting-face-needs-space": "declare the space first to choose one.",
94
90
  "figure.text.part-is-referenced": "'{part}' cannot be deleted: another part uses its values as they are.",
95
91
  "figure.text.part-name-invalid": "'{name}' cannot be a part name. Start with a letter and use letters, digits, - and _.",
96
92
  "figure.text.part-name-taken": "'{name}' is already used in this figure.",
97
- "figure.text.placement-redeclares": "changing it declares the space again from the parts as they are now.",
98
93
  "figure.text.show-affected-parts": "show affected parts",
99
94
  "figure.button.to-floor": "to floor",
100
95
  "figure.label.accepts": "accepts",
@@ -530,7 +525,6 @@
530
525
  "figure.label.fastened-to": "fastened to",
531
526
  "figure.label.not-fastened": "not fastened",
532
527
  "figure.label.my-face": "my face",
533
- "figure.label.target-part": "target part",
534
528
  "figure.label.target-face": "target face",
535
529
  "figure.label.stands-on-plane": "stand on the mounting plane",
536
530
  "figure.label.facing-meet": "faces meet",
@@ -560,6 +554,10 @@
560
554
  "figure.text.preview-face-unknown": "This face cannot be shown on the canvas.",
561
555
  "figure.text.target-would-loop": "{part} — attached to {mine}, cannot be chosen",
562
556
  "figure.label.fasten-detail": "Details",
557
+ "figure.label.fasten-what": "Which part",
558
+ "figure.label.fasten-face-on-part": "Which face of {part}",
559
+ "figure.label.fasten-face-on-plane": "Where on the {plane}",
560
+ "figure.label.fasten-how": "How it sits",
563
561
  "figure.label.facing": "How faces meet",
564
562
  "figure.text.fasten-summary-gap": "gap {mm} mm",
565
563
  "figure.text.fasten-summary-gap-worked-out": "gap worked out by a formula",
@@ -598,10 +596,8 @@
598
596
  "figure.label.room-undeclared": "not set",
599
597
  "figure.label.room-declared": "Declared now",
600
598
  "figure.label.mounting-face": "Mounting face",
601
- "figure.text.mounting-face-explained": "The face the figure is mounted on. A separate choice from the room it takes up.",
602
- "figure.button.set-mounting-face": "This face",
603
599
  "figure.button.clear-mounting-face": "Clear",
604
- "figure.label.mounting-face-none": "none",
600
+ "figure.text.mounting-face-unchecked-at-center": "not checked for a centred figure",
605
601
  "figure.label.dimensions": "Dimensions",
606
602
  "figure.label.part-id": "part id",
607
603
  "figure.text.part-id-is-stable": "The part id is what fastenings, motions and connections point at, so it does not change.",
@@ -675,7 +671,6 @@
675
671
  "figure.label.group-figure-size": "Size",
676
672
  "figure.label.group-part": "Part",
677
673
  "figure.label.group-figure": "Figure",
678
- "figure.label.group-standing": "Where it stands",
679
674
  "figure.label.group-catalog": "Catalogue",
680
675
  "figure.button.control-settings": "Name, range and time",
681
676
  "figure.label.align-x": "left-right",
@@ -877,10 +872,8 @@
877
872
  "figure.button.show-model-settings": "Show model settings",
878
873
  "figure.text.new-figure-name": "New figure",
879
874
  "figure.text.fasten-editing": "Editing the current attachment: its {face} on {target}",
880
- "figure.text.fasten-new": "Attach anew",
881
875
  "figure.text.fasten-start": "Attach this part to a face on another part so they keep their relationship when either changes.",
882
876
  "figure.button.fasten-start": "Attach to another part",
883
- "figure.label.target-part-placeholder": "Choose a part",
884
877
  "figure.text.fasten-target-first": "Choose the part to attach to first.",
885
878
  "figure.text.fasten-edit-on-plane": "This part is connected to the model's placement reference. Detach it before attaching it to another part.",
886
879
  "figure.button.align-to-floor": "set on the floor",
@@ -39,7 +39,6 @@
39
39
  "figure.button.save": "guardar",
40
40
  "figure.button.see-the-trend-chart": "ver el gráfico de tendencia",
41
41
  "figure.button.snap": "ajustar a la cuadrícula",
42
- "figure.label.declared-space": "espacio declarado",
43
42
  "figure.label.follows-expression": "definido por una expresión",
44
43
  "figure.label.gizmo-rotate": "girar",
45
44
  "figure.label.gizmo-scale": "tamaño",
@@ -48,8 +47,6 @@
48
47
  "figure.label.position-in-proportion": "en proporción al tamaño de la figura",
49
48
  "figure.label.primitive-rounded-box": "caja redondeada",
50
49
  "figure.text.catalog-needs-a-save": "se puede definir una vez guardada la figura.",
51
- "figure.text.declared-space-declared": "declarado a partir de las piezas.",
52
- "figure.text.declared-space-none": "aún no declarado.",
53
50
  "figure.text.fasten-would-loop": "esa pieza ya está sujeta a esta, por lo que no se puede sujetar en sentido inverso.",
54
51
  "figure.text.follow-would-loop": "esa dimensión ya sigue a esta pieza, por lo que no pueden seguirse mutuamente.",
55
52
  "figure.text.links-need-a-box": "el seguimiento de tamaño funciona en piezas con forma de caja; la sujeción, en cajas y cilindros.",
@@ -90,11 +87,9 @@
90
87
  "figure.text.size-range-part-gone": "con su tamaño actual, {at} mm en {axis}, una pieza ya no tiene longitud. cambie el vínculo o el tamaño.",
91
88
  "figure.text.input-range-size": "el tamaño de la figura en {axis} solo se puede definir entre {min} y {max} mm. valor introducido: {value} mm",
92
89
  "figure.text.input-range": "este valor solo se puede definir entre {min} y {max}. valor introducido: {value}",
93
- "figure.text.mounting-face-needs-space": "declare primero el espacio para poder elegir una.",
94
90
  "figure.text.part-is-referenced": "'{part}' no se puede eliminar: otra pieza usa sus valores tal cual.",
95
91
  "figure.text.part-name-invalid": "'{name}' no puede ser un nombre de pieza. Empiece por una letra y use letras, dígitos, - y _.",
96
92
  "figure.text.part-name-taken": "'{name}' ya se usa en esta figura.",
97
- "figure.text.placement-redeclares": "al cambiarlo se vuelve a declarar el espacio a partir de las piezas tal como están ahora.",
98
93
  "figure.text.show-affected-parts": "mostrar piezas afectadas",
99
94
  "figure.button.to-floor": "al suelo",
100
95
  "figure.label.accepts": "acepta",
@@ -530,7 +525,6 @@
530
525
  "figure.label.fastened-to": "sujeta a",
531
526
  "figure.label.not-fastened": "sin sujetar",
532
527
  "figure.label.my-face": "mi cara",
533
- "figure.label.target-part": "pieza de destino",
534
528
  "figure.label.target-face": "cara de destino",
535
529
  "figure.label.stands-on-plane": "apoyar en el plano de montaje",
536
530
  "figure.label.facing-meet": "caras en contacto",
@@ -560,6 +554,10 @@
560
554
  "figure.text.preview-face-unknown": "Esta cara no se puede mostrar en el lienzo.",
561
555
  "figure.text.target-would-loop": "{part}: unida a {mine}, no se puede elegir",
562
556
  "figure.label.fasten-detail": "Detalles",
557
+ "figure.label.fasten-what": "En qué pieza",
558
+ "figure.label.fasten-face-on-part": "En qué cara de {part}",
559
+ "figure.label.fasten-face-on-plane": "Dónde en el {plane}",
560
+ "figure.label.fasten-how": "Cómo se coloca",
563
561
  "figure.label.facing": "Cómo se unen las caras",
564
562
  "figure.text.fasten-summary-gap": "separación {mm} mm",
565
563
  "figure.text.fasten-summary-gap-worked-out": "separación calculada por una fórmula",
@@ -598,10 +596,8 @@
598
596
  "figure.label.room-undeclared": "aún no declarado",
599
597
  "figure.label.room-declared": "Declarado ahora",
600
598
  "figure.label.mounting-face": "Cara de montaje",
601
- "figure.text.mounting-face-explained": "La cara sobre la que se monta la figura. Es una elección independiente del espacio que ocupa.",
602
- "figure.button.set-mounting-face": "Esta cara",
603
599
  "figure.button.clear-mounting-face": "Borrar",
604
- "figure.label.mounting-face-none": "ninguna",
600
+ "figure.text.mounting-face-unchecked-at-center": "no se comprueba en una figura centrada",
605
601
  "figure.label.dimensions": "Dimensiones",
606
602
  "figure.label.part-id": "id de pieza",
607
603
  "figure.text.part-id-is-stable": "El id de pieza es al que apuntan las sujeciones, los movimientos y las conexiones, por lo que no cambia.",
@@ -675,7 +671,6 @@
675
671
  "figure.label.group-figure-size": "Tamaño de la figura",
676
672
  "figure.label.group-part": "Pieza",
677
673
  "figure.label.group-figure": "Figura",
678
- "figure.label.group-standing": "Dónde se apoya",
679
674
  "figure.label.group-catalog": "Catálogo",
680
675
  "figure.button.control-settings": "Nombre, rango y tiempo",
681
676
  "figure.label.align-x": "izquierda-derecha",
@@ -877,10 +872,8 @@
877
872
  "figure.button.show-model-settings": "Mostrar la configuración del modelo",
878
873
  "figure.text.new-figure-name": "Nueva figura",
879
874
  "figure.text.fasten-editing": "Editando la unión actual: su {face} en {target}",
880
- "figure.text.fasten-new": "Unir de nuevo",
881
875
  "figure.text.fasten-start": "Una esta pieza a una cara de otra pieza para que mantengan su relación cuando cualquiera de ellas cambie.",
882
876
  "figure.button.fasten-start": "Unir a otra pieza",
883
- "figure.label.target-part-placeholder": "Elija una pieza",
884
877
  "figure.text.fasten-target-first": "Elija primero la pieza a la que unirla.",
885
878
  "figure.text.fasten-edit-on-plane": "Esta pieza está conectada a la referencia de colocación del modelo. Sepárela antes de unirla a otra pieza.",
886
879
  "figure.button.align-to-floor": "apoyar en el suelo",
@@ -39,7 +39,6 @@
39
39
  "figure.button.save": "save",
40
40
  "figure.button.see-the-trend-chart": "see the trend chart",
41
41
  "figure.button.snap": "グリッドに合わせる",
42
- "figure.label.declared-space": "空間宣言",
43
42
  "figure.label.follows-expression": "式で決まる",
44
43
  "figure.label.gizmo-rotate": "回転",
45
44
  "figure.label.gizmo-scale": "サイズ",
@@ -48,8 +47,6 @@
48
47
  "figure.label.position-in-proportion": "図形サイズに比例",
49
48
  "figure.label.primitive-rounded-box": "角丸ボックス",
50
49
  "figure.text.catalog-needs-a-save": "保存した後に設定できます。",
51
- "figure.text.declared-space-declared": "部品から宣言しました。",
52
- "figure.text.declared-space-none": "まだ宣言していません。",
53
50
  "figure.text.fasten-would-loop": "その部品はすでにこの部品に取り付けられているため、逆向きに取り付けられません。",
54
51
  "figure.text.follow-would-loop": "その寸法はすでにこの部品に追従しているため、互いに追従させられません。",
55
52
  "figure.text.links-need-a-box": "サイズ追従は箱形、取り付けは箱形と円柱の部品で使えます。",
@@ -90,11 +87,9 @@
90
87
  "figure.text.size-range-part-gone": "現在のサイズ {at} mm では {axis} 軸の部品の長さがすでに 0 です。リンクかサイズを変えてください。",
91
88
  "figure.text.input-range-size": "図形サイズ {axis} は {min} ~ {max} mm の間でのみ設定できます。入力値: {value} mm",
92
89
  "figure.text.input-range": "この値は {min} ~ {max} の間でのみ設定できます。入力値: {value}",
93
- "figure.text.mounting-face-needs-space": "先に空間を宣言すると選べます。",
94
90
  "figure.text.part-is-referenced": "他の部品が '{part}' の値をそのまま使っているため削除できません。",
95
91
  "figure.text.part-name-invalid": "'{name}' は部品名に使えません。英字で始め、英字・数字・-・_ のみ使えます。",
96
92
  "figure.text.part-name-taken": "'{name}' はこの図形ですでに使われている名前です。",
97
- "figure.text.placement-redeclares": "変更すると、今の部品で空間を宣言し直します。",
98
93
  "figure.text.show-affected-parts": "影響する部品を見る",
99
94
  "figure.button.to-floor": "to floor",
100
95
  "figure.label.accepts": "受け入れるもの",
@@ -530,7 +525,6 @@
530
525
  "figure.label.fastened-to": "fastened to",
531
526
  "figure.label.not-fastened": "not fastened",
532
527
  "figure.label.my-face": "my face",
533
- "figure.label.target-part": "target part",
534
528
  "figure.label.target-face": "target face",
535
529
  "figure.label.stands-on-plane": "stand on the mounting plane",
536
530
  "figure.label.facing-meet": "faces meet",
@@ -560,6 +554,10 @@
560
554
  "figure.text.preview-face-unknown": "この面はキャンバスに表示できません。",
561
555
  "figure.text.target-would-loop": "{part} — {mine} に取り付いているため選べません",
562
556
  "figure.label.fasten-detail": "詳細",
557
+ "figure.label.fasten-what": "どの部品に",
558
+ "figure.label.fasten-face-on-part": "{part}のどの面に",
559
+ "figure.label.fasten-face-on-plane": "{plane}のどこに",
560
+ "figure.label.fasten-how": "どう置くか",
563
561
  "figure.label.facing": "接し方",
564
562
  "figure.text.fasten-summary-gap": "間隔 {mm} mm",
565
563
  "figure.text.fasten-summary-gap-worked-out": "間隔は式で決まる",
@@ -598,10 +596,8 @@
598
596
  "figure.label.room-undeclared": "not declared yet",
599
597
  "figure.label.room-declared": "現在の宣言",
600
598
  "figure.label.mounting-face": "Mounting face",
601
- "figure.text.mounting-face-explained": "The face the figure is mounted on. A separate choice from the room it takes up.",
602
- "figure.button.set-mounting-face": "This face",
603
599
  "figure.button.clear-mounting-face": "Clear",
604
- "figure.label.mounting-face-none": "none",
600
+ "figure.text.mounting-face-unchecked-at-center": "中心配置のため検査しません",
605
601
  "figure.label.dimensions": "Dimensions",
606
602
  "figure.label.part-id": "part id",
607
603
  "figure.text.part-id-is-stable": "The part id is what fastenings, motions and connections point at, so it does not change.",
@@ -675,7 +671,6 @@
675
671
  "figure.label.group-figure-size": "図形のサイズ",
676
672
  "figure.label.group-part": "部品",
677
673
  "figure.label.group-figure": "図形",
678
- "figure.label.group-standing": "置き方",
679
674
  "figure.label.group-catalog": "一覧情報",
680
675
  "figure.button.control-settings": "名前・範囲・時間",
681
676
  "figure.label.align-x": "左右の位置",
@@ -877,10 +872,8 @@
877
872
  "figure.button.show-model-settings": "モデル設定を表示",
878
873
  "figure.text.new-figure-name": "新しい Figure",
879
874
  "figure.text.fasten-editing": "現在の取り付けを編集: {face}を{target}に",
880
- "figure.text.fasten-new": "新しく取り付ける",
881
875
  "figure.text.fasten-start": "別の部品の面に取り付けると、相手の部品が変わっても関係を保ちます。",
882
876
  "figure.button.fasten-start": "別の部品に取り付ける",
883
- "figure.label.target-part-placeholder": "取り付ける部品を選択",
884
877
  "figure.text.fasten-target-first": "先に取り付ける部品を選んでください。",
885
878
  "figure.text.fasten-edit-on-plane": "この部品はモデルの設置基準に接続されています。別の部品に取り付ける前に外してください。",
886
879
  "figure.button.align-to-floor": "床に合わせる",
@@ -39,7 +39,6 @@
39
39
  "figure.button.save": "저장",
40
40
  "figure.button.see-the-trend-chart": "추이 그래프 보기",
41
41
  "figure.button.snap": "눈금에 정렬",
42
- "figure.label.declared-space": "차지하는 공간",
43
42
  "figure.label.follows-expression": "식으로 정해짐",
44
43
  "figure.label.gizmo-rotate": "돌리기",
45
44
  "figure.label.gizmo-scale": "크기",
@@ -48,8 +47,6 @@
48
47
  "figure.label.position-in-proportion": "도형 크기에 비례",
49
48
  "figure.label.primitive-rounded-box": "둥근 상자",
50
49
  "figure.text.catalog-needs-a-save": "저장한 뒤에 정할 수 있습니다.",
51
- "figure.text.declared-space-declared": "부품에 맞춤",
52
- "figure.text.declared-space-none": "정하지 않음",
53
50
  "figure.text.fasten-would-loop": "그 부품은 이미 이 부품에 붙어 있어서 거꾸로 붙일 수 없습니다.",
54
51
  "figure.text.follow-would-loop": "그 치수는 이미 이 부품을 따르고 있어서 서로 따르게 할 수 없습니다.",
55
52
  "figure.text.links-need-a-box": "「크기 따르기」는 상자 모양 부품에서, 「붙이기」는 상자·원기둥 부품에서 사용할 수 있습니다.",
@@ -90,11 +87,9 @@
90
87
  "figure.text.size-range-part-gone": "지금 크기 {at} mm 에서 {axis} 축의 부품 길이가 이미 0 입니다. 연결이나 크기를 바꿔 주세요.",
91
88
  "figure.text.input-range-size": "도형 크기 {axis} 는 {min} ~ {max} mm 사이에서만 정할 수 있습니다. 넣은 값: {value} mm",
92
89
  "figure.text.input-range": "이 값은 {min} ~ {max} 사이에서만 정할 수 있습니다. 넣은 값: {value}",
93
- "figure.text.mounting-face-needs-space": "공간을 먼저 선언해야 고를 수 있습니다.",
94
90
  "figure.text.part-is-referenced": "다른 부품이 '{part}' 의 값을 그대로 쓰고 있어 삭제할 수 없습니다.",
95
91
  "figure.text.part-name-invalid": "'{name}'은(는) 부품 이름으로 사용할 수 없습니다. 영문자로 시작하고 영문자·숫자·-·_만 포함해야 합니다.",
96
92
  "figure.text.part-name-taken": "'{name}' 은 이 도형에서 이미 쓰는 이름입니다.",
97
- "figure.text.placement-redeclares": "바꾸면 지금 부품으로 공간을 다시 선언합니다.",
98
93
  "figure.text.show-affected-parts": "영향 받는 부품 보기",
99
94
  "figure.button.to-floor": "바닥에",
100
95
  "figure.label.accepts": "받는 것",
@@ -530,7 +525,6 @@
530
525
  "figure.label.fastened-to": "붙은 곳",
531
526
  "figure.label.not-fastened": "붙지 않음",
532
527
  "figure.label.my-face": "내 면",
533
- "figure.label.target-part": "상대 부품",
534
528
  "figure.label.target-face": "상대 면",
535
529
  "figure.label.stands-on-plane": "장착 평면에 세우기",
536
530
  "figure.label.facing-meet": "맞닿게",
@@ -560,6 +554,10 @@
560
554
  "figure.text.preview-face-unknown": "이 면은 캔버스에 표시하지 못합니다.",
561
555
  "figure.text.target-would-loop": "{part} — {mine}에 붙어 있어 고를 수 없음",
562
556
  "figure.label.fasten-detail": "세부",
557
+ "figure.label.fasten-what": "어느 부품에",
558
+ "figure.label.fasten-face-on-part": "{part}의 어느 면에",
559
+ "figure.label.fasten-face-on-plane": "{plane}의 어디에",
560
+ "figure.label.fasten-how": "어떻게 놓을지",
563
561
  "figure.label.facing": "닿는 방식",
564
562
  "figure.text.fasten-summary-gap": "간격 {mm} mm",
565
563
  "figure.text.fasten-summary-gap-worked-out": "간격은 식으로 정해짐",
@@ -598,10 +596,8 @@
598
596
  "figure.label.room-undeclared": "정하지 않음",
599
597
  "figure.label.room-declared": "지금 선언",
600
598
  "figure.label.mounting-face": "장착면",
601
- "figure.text.mounting-face-explained": "도형을 배치할 때 기준이 되는 장착면을 지정합니다. 점유 공간 설정과는 별개입니다.",
602
- "figure.button.set-mounting-face": "이 면으로",
603
599
  "figure.button.clear-mounting-face": "지정 해제",
604
- "figure.label.mounting-face-none": "지정 안 함",
600
+ "figure.text.mounting-face-unchecked-at-center": "중심 배치라 검사하지 않음",
605
601
  "figure.label.dimensions": "치수",
606
602
  "figure.label.part-id": "부품 id",
607
603
  "figure.text.part-id-is-stable": "부품 id 는 붙이기·움직임·연결이 가리키는 이름이라 바꾸지 않습니다.",
@@ -675,7 +671,6 @@
675
671
  "figure.label.group-figure-size": "크기",
676
672
  "figure.label.group-part": "부품",
677
673
  "figure.label.group-figure": "도형",
678
- "figure.label.group-standing": "놓이는 자리",
679
674
  "figure.label.group-catalog": "목록 정보",
680
675
  "figure.button.control-settings": "이름 · 범위 · 시간",
681
676
  "figure.label.align-x": "좌우 자리",
@@ -877,10 +872,8 @@
877
872
  "figure.button.show-model-settings": "모델 설정 보기",
878
873
  "figure.text.new-figure-name": "새 Figure",
879
874
  "figure.text.fasten-editing": "현재 관계 수정 — {face} → {target}",
880
- "figure.text.fasten-new": "새로 붙이기",
881
875
  "figure.text.fasten-start": "이 부품을 다른 부품의 면에 붙여 두면, 상대 부품의 크기나 위치가 바뀌어도 함께 움직입니다.",
882
876
  "figure.button.fasten-start": "다른 부품에 붙이기",
883
- "figure.label.target-part-placeholder": "붙일 부품 선택",
884
877
  "figure.text.fasten-target-first": "먼저 붙일 부품을 선택해 주세요.",
885
878
  "figure.text.fasten-edit-on-plane": "이 부품은 현재 모델의 설치 기준면에 연결되어 있습니다. 다른 부품에 붙이려면 먼저 붙임을 푸세요.",
886
879
  "figure.button.align-to-floor": "바닥에 내려놓기",