@things-factory/figure-ui 10.1.198 → 10.1.201

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 (74) hide show
  1. package/client/modeller/editor.ts +7 -3
  2. package/client/modeller/fasten-form.ts +18 -35
  3. package/client/modeller/fasten-geometry.ts +67 -115
  4. package/client/modeller/figure-fasten-dialog.ts +4 -12
  5. package/client/modeller/figure-links.ts +6 -7
  6. package/client/modeller/joint-drag.ts +9 -11
  7. package/client/modeller/links.ts +35 -99
  8. package/client/modeller/refusals.ts +2 -6
  9. package/dist-client/modeller/editor.js +7 -2
  10. package/dist-client/modeller/editor.js.map +1 -1
  11. package/dist-client/modeller/fasten-form.d.ts +5 -8
  12. package/dist-client/modeller/fasten-form.js +16 -28
  13. package/dist-client/modeller/fasten-form.js.map +1 -1
  14. package/dist-client/modeller/fasten-geometry.d.ts +20 -19
  15. package/dist-client/modeller/fasten-geometry.js +54 -113
  16. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  17. package/dist-client/modeller/figure-fasten-dialog.js +4 -12
  18. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  19. package/dist-client/modeller/figure-links.d.ts +1 -1
  20. package/dist-client/modeller/figure-links.js +6 -7
  21. package/dist-client/modeller/figure-links.js.map +1 -1
  22. package/dist-client/modeller/joint-drag.js +9 -11
  23. package/dist-client/modeller/joint-drag.js.map +1 -1
  24. package/dist-client/modeller/links.d.ts +19 -26
  25. package/dist-client/modeller/links.js +36 -94
  26. package/dist-client/modeller/links.js.map +1 -1
  27. package/dist-client/modeller/refusals.d.ts +1 -1
  28. package/dist-client/modeller/refusals.js +2 -6
  29. package/dist-client/modeller/refusals.js.map +1 -1
  30. package/dist-client/tsconfig.tsbuildinfo +1 -1
  31. package/dist-server/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +6 -6
  33. package/test/ai-proposal.test.ts +1 -1
  34. package/test/browser/acceptance-page.js +11 -11
  35. package/test/browser/detail-page.js +1 -1
  36. package/test/browser/detail-tabs-page.js +1 -1
  37. package/test/browser/editing-page.js +1 -1
  38. package/test/browser/fix-alignment.check.mjs +3 -3
  39. package/test/browser/links.check.mjs +20 -8
  40. package/test/browser/modeller-page.js +3 -3
  41. package/test/browser/part-ids.check.mjs +2 -2
  42. package/test/browser/parts-page.js +2 -2
  43. package/test/browser/product-page.check.mjs +3 -3
  44. package/test/browser/product-page.js +1 -1
  45. package/test/browser/row-preview.check.mjs +2 -2
  46. package/test/browser/screen-page.js +3 -3
  47. package/test/browser/section-page.js +3 -3
  48. package/test/browser/section.check.mjs +4 -4
  49. package/test/clip-ai-proposal.test.ts +1 -1
  50. package/test/clip-consent.test.ts +1 -1
  51. package/test/command-preview.test.ts +8 -8
  52. package/test/face-mapping.test.ts +2 -2
  53. package/test/fasten-drawing.test.ts +14 -2
  54. package/test/fasten-form.test.ts +11 -3
  55. package/test/fasten-geometry.test.ts +22 -31
  56. package/test/fix-alignment.test.ts +16 -4
  57. package/test/gizmo.test.ts +2 -2
  58. package/test/joint-drag.test.ts +13 -2
  59. package/test/joint-fan.test.ts +1 -1
  60. package/test/joint-frames.test.ts +74 -45
  61. package/test/joints.ts +1 -1
  62. package/test/links.test.ts +68 -68
  63. package/test/live.test.ts +2 -2
  64. package/test/preview-size.test.ts +1 -1
  65. package/test/refusals.test.ts +18 -20
  66. package/test/resize-with-placement.test.ts +6 -6
  67. package/test/revision.test.ts +4 -5
  68. package/test/section-panel.test.ts +1 -1
  69. package/translations/en.json +9 -11
  70. package/translations/es.json +9 -11
  71. package/translations/ja.json +9 -11
  72. package/translations/ko.json +9 -11
  73. package/translations/ms.json +9 -11
  74. package/translations/zh.json +9 -11
package/test/live.test.ts CHANGED
@@ -70,9 +70,9 @@ test('끄는 동안은 세션이 지금 든 자세 위에 그린다 — 다른
70
70
  let asset = slider()
71
71
  /* An arm on a post of its own, turning about the post's left edge -- so a swing moves it, and the slide does not. */
72
72
  asset = addFigureBox(asset, { id: 'post', size: { x: 100, y: 100, z: 100 }, position: { x: 400, y: 50, z: 0 } })
73
- asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'post', face: 'bottom', to: { plane: 'mounting-plane' }, align: { x: { mm: 400 } } })
73
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'post', to: { plane: 'mounting-plane' }, align: { x: { mm: 400 } } })
74
74
  asset = addFigureBox(asset, { id: 'arm', size: { x: 100, y: 20, z: 20 }, position: { x: 400, y: 110, z: 0 } })
75
- asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'arm', face: 'bottom', to: { part: 'post', face: 'top' }, align: { x: 'min' } })
75
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'arm', to: { part: 'post', face: 'top' }, align: { x: 'min' } })
76
76
  asset = moving(asset, 'arm', { kind: 'turn', axis: 'y', id: 'swing', min: -180, max: 180 })
77
77
  const armAt = (parts: { id: string; pose: { t: number[] } }[]) => parts.find(p => p.id === 'arm')!.pose.t.map(v => Math.round(v))
78
78
  const committed = armAt(compileFigureAsset({ ...asset, stateDefaults: { ...asset.stateDefaults, swing: 90 } }).evaluate().geometry as never)
@@ -13,7 +13,7 @@ import { previewSizeWordsOf, rowCountsAt } from '../client/modeller/preview-size
13
13
  function conveyor(): FigureAsset {
14
14
  let asset = addFigureBox(createFigureAsset('Conveyor'), { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 50, z: 0 } })
15
15
  asset = addFigurePrimitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 30, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
16
- asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
16
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
17
17
  asset = applyFigureAuthoring(asset, { kind: 'follow-size', part: 'frame', axes: ['x'] })
18
18
  return applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
19
19
  }
@@ -61,8 +61,8 @@ function conveyor(): FigureAsset {
61
61
  let a = fm.createFigureAsset('Conveyor')
62
62
  a = fm.addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
63
63
  a = fm.addFigureBox(a, { id: 'roller', size: { x: 60, y: 60, z: 560 }, position: { x: 0, y: 0, z: 0 } })
64
- a = act(a, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
65
- a = act(a, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
64
+ a = act(a, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
65
+ a = act(a, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
66
66
  return act(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
67
67
  }
68
68
 
@@ -100,10 +100,10 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
100
100
  a = fm.addFigureBox(a, { id: 'top', size: { x: 1200, y: 40, z: 600 }, position: { x: 0, y: 0, z: 0 } })
101
101
  a = fm.addFigureBox(a, { id: 'lamp', size: { x: 80, y: 80, z: 80 }, position: { x: 0, y: 0, z: 0 } })
102
102
  a = fm.addFigureBox(a, { id: 'shade', size: { x: 40, y: 40, z: 40 }, position: { x: 0, y: 0, z: 0 } })
103
- a = act(a, { kind: 'attach', part: 'leg', face: 'bottom', to: { plane: 'mounting-plane' } })
104
- a = act(a, { kind: 'attach', part: 'top', face: 'bottom', to: { part: 'leg', face: 'top' } })
105
- a = act(a, { kind: 'attach', part: 'lamp', face: 'bottom', to: { part: 'top', face: 'top' }, align: { x: { mm: -300 } } })
106
- a = act(a, { kind: 'attach', part: 'shade', face: 'bottom', to: { part: 'top', face: 'top' }, align: { x: { mm: 300 } } })
103
+ a = act(a, { kind: 'attach', part: 'leg', to: { plane: 'mounting-plane' } })
104
+ a = act(a, { kind: 'attach', part: 'top', to: { part: 'leg', face: 'top' } })
105
+ a = act(a, { kind: 'attach', part: 'lamp', to: { part: 'top', face: 'top' }, align: { x: { mm: -300 } } })
106
+ a = act(a, { kind: 'attach', part: 'shade', to: { part: 'top', face: 'top' }, align: { x: { mm: 300 } } })
107
107
  const turn = (asset: typeof a, part: string, extra: object) => act(asset, { kind: 'set-joint', joint: { ...asset.joints!.find(j => j.body1.part === part)!, type: 'revolute', axis: 'Y', ...extra } })
108
108
  a = turn(a, 'lamp', { state: { id: 'lamp-turn' } })
109
109
  a = turn(a, 'shade', { mimic: { joint: 'lamp', multiplier: 1 } })
@@ -121,15 +121,15 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
121
121
  assert.match(why(declared, 'shade')!, /part-in-declared-space .*"part":"shade"/, '잎인데 공간 선언에 들어 있다')
122
122
  assert.match(why(conveyor(), 'frame')!, /part-row-along .*"part":"frame".*"rows":"roller"/, '줄이 따라 놓인 부품')
123
123
  let hung = fm.addFigureBox(conveyor(), { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 0, z: 0 } })
124
- hung = act(hung, { kind: 'attach', part: 'cap', face: 'bottom', to: { part: 'roller', face: 'top' } })
124
+ hung = act(hung, { kind: 'attach', part: 'cap', to: { part: 'roller', face: 'top' } })
125
125
  assert.match(why(hung, 'roller')!, /part-row-carries .*"part":"roller".*"others":"cap"/, '줄에 매달린 것')
126
126
  })
127
127
 
128
128
  test('공간 선언에 든 부품 — 한 번 눌러 지우고, 공간은 남은 부품에 다시 맞추며 그렇다고 한 줄 말한다(묻지 않는다, 2026-09-27)', () => {
129
129
  let a = fm.addFigureBox(fm.createFigureAsset('Gate'), { id: 'housing', size: { x: 300, y: 1000, z: 300 }, position: { x: 0, y: 0, z: 0 } })
130
130
  a = fm.addFigureBox(a, { id: 'stray', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
131
- a = act(a, { kind: 'attach', part: 'housing', face: 'bottom', to: { plane: 'mounting-plane' } })
132
- a = act(a, { kind: 'attach', part: 'stray', face: 'bottom', to: { plane: 'mounting-plane' } })
131
+ a = act(a, { kind: 'attach', part: 'housing', to: { plane: 'mounting-plane' } })
132
+ a = act(a, { kind: 'attach', part: 'stray', to: { plane: 'mounting-plane' } })
133
133
  a = act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'floor' })
134
134
  assert.equal(inDeclaredRoom(refusalOf(() => fm.editFigurePart(a, { kind: 'remove-part', target: 'stray' }))), true)
135
135
  assert.equal(inDeclaredRoom(refusalOf(() => fm.editFigurePart(conveyor(), { kind: 'remove-part', target: 'frame' }))), false, '줄은 이 길이 아니다')
@@ -144,12 +144,11 @@ test('배치를 바꿀 수 없는 까닭 — 평면에 붙은 부품과 새 평
144
144
  const a = act(fm.addFigureBox(fm.createFigureAsset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 0, z: 0 } }), {
145
145
  kind: 'attach',
146
146
  part: 'post',
147
- face: 'bottom',
148
147
  to: { plane: 'mounting-plane' }
149
148
  })
150
149
  const refused = refusalOf(() => act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'wall' }))
151
150
  const said = placementRefusalOf(refused, say)!
152
- assert.ok(said.includes('post (bottom)') && said.includes('벽') && said.includes('back'), said)
151
+ assert.equal(said, '장착 평면에 붙은 부품이 있어 놓이는 곳을 바꿀 수 없습니다: post. 먼저 이 부품을 떼어 주십시오.')
153
152
  assert.equal(editRefusalOf(refused, say), said, '편집 거절의 말도 같다')
154
153
  assert.equal(placementRefusalOf(refusalOf(() => act(box(), declaring({ min: 0, max: 4000 }))), say), null)
155
154
  /* The placement list sends its command through the page: said there in words, and the list back on the placement it has (product-page.check.mjs). */
@@ -191,13 +190,13 @@ test('줄이 한도를 넘으면 몇 개가 되는지와 한도를 말한다 —
191
190
  let a = fm.createFigureAsset('Conveyor')
192
191
  a = fm.addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
193
192
  a = fm.addFigureBox(a, { id: 'roller', size: { x: 10, y: 60, z: 560 }, position: { x: 0, y: 0, z: 0 } })
194
- a = act(a, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
195
- a = act(a, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
193
+ a = act(a, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
194
+ a = act(a, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
196
195
  const many = editRefusalOf(refusalOf(() => act(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 12, count: 150 })), say)
197
196
  assert.ok(many.includes('150') && many.includes('1~100') && !many.includes('GEOMETRY_DOMAIN'), many)
198
197
  /* A saved figure whose row would expand past what a figure draws: ten thousand copies, each a roller and its cap. */
199
198
  let capped = fm.addFigureBox(conveyor(), { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 0, z: 0 } })
200
- capped = structuredClone(act(capped, { kind: 'attach', part: 'cap', face: 'bottom', to: { part: 'roller', face: 'top' } }))
199
+ capped = structuredClone(act(capped, { kind: 'attach', part: 'cap', to: { part: 'roller', face: 'top' } }))
201
200
  const m = capped.document.model as unknown as { constants: Array<{ id: string; value: number }> }
202
201
  m.constants.find(c => c.id === 'limit.count.100')!.value = 10000
203
202
  m.constants.find(c => c.id === 'd.mm.200')!.value = 0.2
@@ -209,15 +208,15 @@ test('일상에서 코드로만 보이던 거절 넷 — 줄이 안 들어감 ·
209
208
  const shelf = () => {
210
209
  let a = fm.addFigureBox(fm.createFigureAsset('Shelf'), { id: 'board', size: { x: 400, y: 50, z: 1000 }, position: { x: 0, y: 0, z: 0 } })
211
210
  a = fm.addFigureBox(a, { id: 'bin', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 0, z: 0 } })
212
- a = act(a, { kind: 'attach', part: 'board', face: 'bottom', to: { plane: 'mounting-plane' } })
213
- return act(a, { kind: 'attach', part: 'bin', face: 'bottom', to: { part: 'board', face: 'top' } })
211
+ a = act(a, { kind: 'attach', part: 'board', to: { plane: 'mounting-plane' } })
212
+ return act(a, { kind: 'attach', part: 'bin', to: { part: 'board', face: 'top' } })
214
213
  }
215
214
  /* Five 100 mm bins asked for on 400 mm. */
216
215
  const fit = editRefusalOf(refusalOf(() => act(shelf(), { kind: 'make-repeat', part: 'bin', axis: 'x', pitch: 1, count: 5 })), keyed)
217
216
  assert.match(fit, /row-does-not-fit .*"count":"5".*"length":"400".*"extent":"100"/)
218
217
  /* A cap on the roller made a row along X, as the roller's row is. */
219
218
  let capped = fm.addFigureBox(conveyor(), { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 0, z: 0 } })
220
- capped = act(capped, { kind: 'attach', part: 'cap', face: 'bottom', to: { part: 'roller', face: 'top' } })
219
+ capped = act(capped, { kind: 'attach', part: 'cap', to: { part: 'roller', face: 'top' } })
221
220
  assert.match(editRefusalOf(refusalOf(() => act(capped, { kind: 'make-repeat', part: 'cap', axis: 'x', pitch: 10 })), keyed), /row-same-axis .*"part":"cap","outer":"roller","axis":"X"/)
222
221
  /* The bin's top holds the slot layout: made a row, the layout would stand on no one bin. */
223
222
  const slotted = act(shelf(), {
@@ -243,7 +242,6 @@ test('옮겨 적은 거절 — 기록은 커널의 거절과 같은 모양이라
243
242
  const a = act(fm.addFigureBox(fm.createFigureAsset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 0, z: 0 } }), {
244
243
  kind: 'attach',
245
244
  part: 'post',
246
- face: 'bottom',
247
245
  to: { plane: 'mounting-plane' }
248
246
  })
249
247
  const refused = refusalOf(() => act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'wall' }))
@@ -271,7 +269,7 @@ test('단면 · 속 공간 — 모서리, 벽, 막힌 끝, 붙은 면을 거절
271
269
  assert.match(editRefusalOf(refusalOf(() => act(box(), { kind: 'set-cavity', part: 'box', cavity: { wall: 10, start: { closed: 200 }, end: { closed: 200 } } })), keyed), /^figure\.text\.section-ends-too-thick/)
272
270
  let tray = act(box(), { kind: 'set-cavity', part: 'box', cavity: { wall: 10, start: { closed: 15 }, end: 'open' } })
273
271
  tray = fm.addFigureBox(tray, { id: 'divider', size: { x: 10, y: 100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
274
- tray = act(tray, { kind: 'attach', part: 'divider', face: 'bottom', to: { part: 'box', face: 'inside-bottom' } })
272
+ tray = act(tray, { kind: 'attach', part: 'divider', to: { part: 'box', face: 'inside-bottom' } })
275
273
  const faces = editRefusalOf(refusalOf(() => act(tray, { kind: 'set-cavity', part: 'box', cavity: { wall: 10, start: 'open', end: 'open' } })), say)
276
274
  assert.ok(faces.includes(ko['figure.label.face-inside-bottom']), faces)
277
275
  /* A section finding the screen has no words for is said with its code. */
@@ -309,7 +307,7 @@ test('도형의 한도 — 노드 수, 저장 크기, 중첩 깊이를 숫자로
309
307
  })
310
308
 
311
309
  test('레시피가 막히면 몇 번째 단계에서 왜 막혔는지 사람의 말로 — 단계 안의 거절도 값을 잃지 않는다 (2026-09-27)', () => {
312
- const refused = refusalOf(() => fm.applyFigureProposal(box(), [{ kind: 'attach', part: 'nowhere', face: 'bottom', to: { plane: 'mounting-plane' } }, declaring({ min: 500, max: 4000 })] as never))
310
+ const refused = refusalOf(() => fm.applyFigureProposal(box(), [{ kind: 'attach', part: 'nowhere', to: { plane: 'mounting-plane' } }, declaring({ min: 500, max: 4000 })] as never))
313
311
  assert.match(recipeRefusalOf(refused, keyed)!, /^figure\.text\.recipe-step-refused .*"step":"1"/, '첫째 단계, 코드와 함께')
314
312
  const sized = refusalOf(() => fm.applyFigureProposal(box(), [declaring({ min: 500, max: 4000 })] as never))
315
313
  const told = recipeRefusalOf(sized, say)!
@@ -152,14 +152,14 @@ test('a fastened part keeps its position and turn rows, with the numbers the rul
152
152
 
153
153
  test('size response and attachment are separate screen paths', () => {
154
154
  const ko = JSON.parse(readFileSync(new URL('../translations/ko.json', import.meta.url), 'utf-8'))
155
- assert.equal(ko['figure.text.held-on-plane'].replace('{face}', '아랫면').replace('{place}', '바닥').replace('{gap}', '0'), '아랫면을 바닥에 유지 중 (간격 0 mm)')
155
+ assert.equal(ko['figure.text.held-on-plane'].replace('{place}', '바닥').replace('{gap}', '0'), '바닥에 붙어 있음 (간격 0 mm)')
156
156
  })
157
157
 
158
- test('a part fastened twisted has no face figure-model can keep for a drag, so the size gizmo is withheld', async () => {
158
+ test('a part fastened tilted off a quarter turn has no side figure-model can keep for a drag, so the size gizmo is withheld', async () => {
159
159
  const { figureScaleAnchorOf } = await import('@hatiolab/figure-model')
160
160
  let a = addFigureBox(createFigureAsset('UX'), { id: 'hub', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
161
161
  a = addFigureBox(a, { id: 'blade', size: { x: 300, y: 20, z: 60 }, position: { x: 0, y: 110, z: 0 } })
162
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'blade', face: 'bottom', to: { part: 'hub', face: 'top' }, twist: 30 })
162
+ a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'blade', type: 'fixed', body0: { part: 'hub', face: 'top' }, body1: { part: 'blade' }, origin: { childRotate: { z: 30 } } } })
163
163
  assert.equal(figureScaleAnchorOf(a, 'blade'), undefined)
164
164
  })
165
165
 
@@ -174,7 +174,7 @@ test('"open attachment settings" opens this part\'s fastening, as it is now, and
174
174
  const kept = applyFigureProposal(a, planFigureResize(a, { part: 'box', gap: 'keep' }).steps)
175
175
  const form = fasteningOf(revisionOf(kept), 'box')!
176
176
  assert.equal(form.toPlane, true, 'on the plane')
177
- assert.equal(form.face, 'bottom')
177
+ assert.equal(form.facing, 'meet')
178
178
  assert.equal(form.gap, 100, 'the gap it keeps')
179
179
  })
180
180
 
@@ -189,8 +189,8 @@ test('the fastening form keeps a place that follows a position field, and fasten
189
189
  const kept = applyFigureProposal(a, planFigureResize(a, { part: 'box' }).steps)
190
190
  const form = fasteningOf(revisionOf(kept), 'box')!
191
191
  assert.deepEqual([form.align.x, form.alignInput.x, form.align.z, form.alignInput.z], ['input', 'box.position.x', 'input', 'box.position.z'])
192
- const align = alignOf(kept, 'box', 'plane', form.align, form.alignMm, false, form.alignInput)
193
- const again = applyFigureAuthoring(kept, { kind: 'attach', part: 'box', face: 'bottom', to: { plane: 'mounting-plane' }, ...(align ? { align } : {}), replace: true } as never)
192
+ const align = alignOf(kept, 'plane', form.align, form.alignMm, form.alignInput)
193
+ const again = applyFigureAuthoring(kept, { kind: 'attach', part: 'box', to: { plane: 'mounting-plane' }, ...(align ? { align } : {}), replace: true } as never)
194
194
  assert.ok('box.position.x' in again.designInputs && 'box.position.z' in again.designInputs, 'the fields stay')
195
195
  assert.deepEqual(JSON.stringify(again.joints), JSON.stringify(kept.joints), 'the same fastening')
196
196
  })
@@ -20,7 +20,7 @@ import { derivedHeld, derivedOf, type FigureRevision } from '../client/modeller/
20
20
  import { alignChanges, boxOf, canMirror, gridStepOf } from '../client/modeller/align.ts'
21
21
  import { jointFanOf } from '../client/modeller/joint-fan.ts'
22
22
  import { fasteningOf, measureNow, partSummaryOf, placeKeptOf } from '../client/modeller/links.ts'
23
- import { faceViewOf, myFaceFor } from '../client/modeller/fasten-geometry.ts'
23
+ import { faceViewOf } from '../client/modeller/fasten-geometry.ts'
24
24
  import { motionEndsOf } from '../client/modeller/motion-ends.ts'
25
25
  import { figureFigureFacts } from '../client/modeller/figure-facts.ts'
26
26
  import { gizmoReachOf } from '../client/modeller/gizmo.ts'
@@ -96,8 +96,7 @@ function selectionQuestions(shown: FigureRevision, part: string) {
96
96
  figureRowOf(asset, part, shown.inputs)
97
97
  fasteningOf(shown, part)
98
98
  placeKeptOf(shown, part, 'plane')
99
- myFaceFor(shown, part, 'plane', 'meet')
100
- faceViewOf(shown, part, 'plane')
99
+ faceViewOf(shown, part, 'plane', 'meet')
101
100
  figureFigureFacts(shown)
102
101
  }
103
102
 
@@ -144,7 +143,7 @@ test('a value changed, read again as the next version, is two compiles at most:
144
143
  function slide(): FigureAsset {
145
144
  let a = addFigureBox(createFigureAsset('Slide'), { id: 'rail', size: { x: 1000, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
146
145
  a = addFigureBox(a, { id: 'carriage', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 150, z: 0 } })
147
- a = applyFigureAuthoring(a, { kind: 'attach', part: 'carriage', face: 'bottom', to: { part: 'rail', face: 'top' } })
146
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'carriage', to: { part: 'rail', face: 'top' } })
148
147
  a = moving(a, 'carriage', { kind: 'slide', axis: 'x', id: 'travel', min: 0, max: 400 })
149
148
  return addFigureBox(a, { id: 'tag', size: { x: 50, y: 50, z: 50 }, position: { x: 300, y: 225, z: 0 } })
150
149
  }
@@ -159,7 +158,7 @@ test('fastening keeps a part where it stands against what is drawn: a target mov
159
158
  return w.open('x').then(() => {
160
159
  w.apply([{ kind: 'set-state', input: 'travel', unit: 'mm', value: 250 }])
161
160
  const before = boxOf(w.revisionNow()!, 'tag')!.centre.x
162
- w.command({ kind: 'attach', part: 'tag', face: 'bottom', to: { part: 'carriage', face: 'top' }, align: { x: { mm: placeKeptOf(shown, 'tag', { part: 'carriage', face: 'top' }).alignMm.x! } } })
161
+ w.command({ kind: 'attach', part: 'tag', to: { part: 'carriage', face: 'top' }, align: { x: { mm: placeKeptOf(shown, 'tag', { part: 'carriage', face: 'top' }).alignMm.x! } } })
163
162
  assert.equal(Math.round(boxOf(w.revisionNow()!, 'tag')!.centre.x), Math.round(before))
164
163
  })
165
164
  })
@@ -164,7 +164,7 @@ test('다각 단면에는 속 공간을 만들 수 없다고 그 자리에서
164
164
  test('붙어 있는 것이 기대는 면을 없애는 편집은 거절되고, 그 면을 사람의 말로 부른다', () => {
165
165
  let asset = applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: { wall: 10, start: { closed: 15 }, end: 'open' } })
166
166
  asset = addFigureBox(asset, { id: 'divider', size: { x: 10, y: 100, z: 200 }, position: { x: 0, y: 0, z: 0 } })
167
- asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'divider', face: 'bottom', to: { part: 'body', face: 'inside-bottom' as never } })
167
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'divider', to: { part: 'body', face: 'inside-bottom' as never } })
168
168
  const said = refusal(() => applyFigureAuthoring(asset, { kind: 'set-cavity', part: 'body', cavity: { wall: 10, start: 'open', end: 'open' } }))
169
169
  assert.equal(said, say('figure.text.section-face-in-use', { faces: ko['figure.label.face-inside-bottom']! }))
170
170
  })
@@ -532,8 +532,9 @@
532
532
  "figure.text.overhang-out": "It comes past the edge of {on}'s {face}: {sides}. This is apart from the figure's room.",
533
533
  "figure.text.overhang-larger": "Wider than {on}'s {face}, it comes past its edges: {sides}.",
534
534
  "figure.text.overhang-lopsided": "It sits off to one side along {axes}.",
535
- "figure.text.overhang-possible": "It may come past the edge of {on}'s {face}: it is fastened turned, so only its box was measured.",
535
+ "figure.text.overhang-possible": "It may come past the edge of {on}'s {face}.",
536
536
  "figure.text.overhang-unknown": "Whether it lies within the edges of {on}'s {face} could not be read.",
537
+ "figure.text.overhang-off": "Where it touches is off {on}'s {face}: it rests on nothing there.",
537
538
  "figure.text.overhang-held": "Parts fastened to this one ({parts}) come, or may come, past the edge of the face they are on.",
538
539
  "figure.text.overhang-side-low": "{axis} low side {mm} mm",
539
540
  "figure.text.overhang-side-high": "{axis} high side {mm} mm",
@@ -562,7 +563,6 @@
562
563
  "figure.label.facing": "How faces meet",
563
564
  "figure.text.fasten-summary-gap": "gap {mm} mm",
564
565
  "figure.text.fasten-summary-gap-worked-out": "gap worked out by a formula",
565
- "figure.text.fasten-summary-twist": "twist {degrees}°",
566
566
  "figure.label.gap": "gap (mm)",
567
567
  "figure.label.align": "within the face",
568
568
  "figure.label.align-centre": "centred",
@@ -709,7 +709,6 @@
709
709
  "figure.text.row-ends": "{low} mm left at the low end · {high} mm at the high end",
710
710
  "figure.text.row-ends-unknown": "What is left at the ends could not be read.",
711
711
  "figure.text.row-companions": "copied along: {parts}",
712
- "figure.label.twist": "twist (°)",
713
712
  "figure.text.instance-size-undeclared": "set a range and a drawing can resize it.",
714
713
  "figure.button.declare-instance-size": "set the range it stretches over",
715
714
  "figure.text.proposal-applied": "applied {n} commands as one step.",
@@ -739,7 +738,7 @@
739
738
  "figure.button.follow-size-in-proportion": "make every part follow",
740
739
  "figure.text.proportion-kept": "keeps its own number: {what}",
741
740
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
742
- "figure.text.placement-plane-refused": "{parts} cannot go on a {placement} by the face fastened to the mounting plane. detach it or fasten it by its {faces} first.",
741
+ "figure.text.placement-plane-refused": "Parts are fastened to the mounting plane, so the placement cannot change: {parts}. Detach them first.",
743
742
  "figure.label.face-centre": "centre",
744
743
  "figure.label.budget-moving": "parts recomputed every frame",
745
744
  "figure.label.performance-cost": "performance cost",
@@ -838,9 +837,9 @@
838
837
  "figure.button.resize-scoped-center": "{scope}: follow size, keeping the current centre",
839
838
  "figure.text.resize-move": "(moves {mm} mm on {axis})",
840
839
  "figure.text.held-by-rule": "A rule places this part.",
841
- "figure.text.held-on-plane": "{face} kept on the {place} (gap {gap} mm)",
842
- "figure.text.held-on-plane-worked-out": "{face} kept on the {place} (gap worked out by a formula)",
843
- "figure.text.held-on-part": "Attached by its {face} to the {otherFace} of {other}",
840
+ "figure.text.held-on-plane": "Kept on the {place} (gap {gap} mm)",
841
+ "figure.text.held-on-plane-worked-out": "Kept on the {place} (gap worked out by a formula)",
842
+ "figure.text.held-on-part": "Attached to the {otherFace} of {other}",
844
843
  "figure.button.open-fastening": "Open attachment settings",
845
844
  "figure.button.detach-held": "Detach",
846
845
  "figure.text.gizmo-size-turned-fastening": "{part} is attached turned, so its size cannot be dragged.",
@@ -870,7 +869,6 @@
870
869
  "figure.text.placement-is-intent": "What the figure is authored from. You can choose it before any part; it does not change the declared space.",
871
870
  "figure.button.show-model-settings": "Show model settings",
872
871
  "figure.text.new-figure-name": "New figure",
873
- "figure.text.fasten-editing": "Editing the current attachment: its {face} on {target}",
874
872
  "figure.text.fasten-start": "Attach this part to a face on another part so they keep their relationship when either changes.",
875
873
  "figure.button.fasten-start": "Attach to another part",
876
874
  "figure.text.fasten-target-first": "Choose the part to attach to first.",
@@ -941,9 +939,9 @@
941
939
  "figure.label.fasten-mode-centre": "centre on it",
942
940
  "figure.text.fasten-off-face": "It does not touch {face}.",
943
941
  "figure.text.fasten-past-edges": "It comes past the edges: {sides}",
944
- "figure.text.fasten-sentence-meet": "{part}'s {face} set on {to}",
945
- "figure.text.fasten-sentence-flush": "{part}'s {face} flush with {to}",
946
- "figure.text.fasten-sentence-centre": "{part}'s centre on {to}",
942
+ "figure.text.fasten-sentence-meet": "Set on {to}",
943
+ "figure.text.fasten-sentence-flush": "Flush with {to}",
944
+ "figure.text.fasten-sentence-centre": "By its middle on {to}",
947
945
  "figure.label.measure-x": "width",
948
946
  "figure.label.measure-y": "height",
949
947
  "figure.label.measure-z": "depth",
@@ -532,8 +532,9 @@
532
532
  "figure.text.overhang-out": "Sobresale del borde de {face} de {on}: {sides}. Esto es independiente del espacio de la figura.",
533
533
  "figure.text.overhang-larger": "Al ser más ancha que {face} de {on}, sobresale de sus bordes: {sides}.",
534
534
  "figure.text.overhang-lopsided": "Está desplazada hacia un lado a lo largo de {axes}.",
535
- "figure.text.overhang-possible": "Puede sobresalir del borde de {face} de {on}: está sujeta girada, por lo que solo se midió su caja envolvente.",
535
+ "figure.text.overhang-possible": "Puede sobresalir del borde de {face} de {on}.",
536
536
  "figure.text.overhang-unknown": "No se pudo determinar si queda dentro de los bordes de {face} de {on}.",
537
+ "figure.text.overhang-off": "Donde toca queda fuera de {face} de {on}: ahí no se apoya en nada.",
537
538
  "figure.text.overhang-held": "Las piezas sujetas a esta ({parts}) sobresalen, o pueden sobresalir, del borde de la cara en la que están.",
538
539
  "figure.text.overhang-side-low": "{axis} lado inferior {mm} mm",
539
540
  "figure.text.overhang-side-high": "{axis} lado superior {mm} mm",
@@ -562,7 +563,6 @@
562
563
  "figure.label.facing": "Cómo se unen las caras",
563
564
  "figure.text.fasten-summary-gap": "separación {mm} mm",
564
565
  "figure.text.fasten-summary-gap-worked-out": "separación calculada por una fórmula",
565
- "figure.text.fasten-summary-twist": "torsión {degrees}°",
566
566
  "figure.label.gap": "separación (mm)",
567
567
  "figure.label.align": "dentro de la cara",
568
568
  "figure.label.align-centre": "centrada",
@@ -709,7 +709,6 @@
709
709
  "figure.text.row-ends": "quedan {low} mm en el extremo inferior · {high} mm en el superior",
710
710
  "figure.text.row-ends-unknown": "No se pudo determinar lo que queda en los extremos.",
711
711
  "figure.text.row-companions": "copiadas con ella: {parts}",
712
- "figure.label.twist": "torsión (°)",
713
712
  "figure.text.instance-size-undeclared": "la figura no tiene tamaño, por lo que un tablero no puede cambiar su tamaño.",
714
713
  "figure.button.declare-instance-size": "declarar el tamaño de la figura",
715
714
  "figure.text.proposal-applied": "se aplicaron {n} comandos como un solo paso.",
@@ -739,7 +738,7 @@
739
738
  "figure.button.follow-size-in-proportion": "hacer que sigan todas las piezas",
740
739
  "figure.text.proportion-kept": "mantiene su propio número: {what}",
741
740
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
742
- "figure.text.placement-plane-refused": "{parts} no puede ir en {placement} por la cara sujeta al plano de montaje. sepárela o sujétela primero por sus caras {faces}.",
741
+ "figure.text.placement-plane-refused": "Hay piezas sujetas al plano de montaje, así que la colocación no puede cambiar: {parts}. Sepárelas primero.",
743
742
  "figure.label.face-centre": "centro",
744
743
  "figure.label.budget-moving": "piezas recalculadas en cada fotograma",
745
744
  "figure.label.performance-cost": "coste de rendimiento",
@@ -838,9 +837,9 @@
838
837
  "figure.button.resize-scoped-center": "{scope}: seguir el tamaño, manteniendo el centro actual",
839
838
  "figure.text.resize-move": "(se mueve {mm} mm en {axis})",
840
839
  "figure.text.held-by-rule": "Una regla coloca esta pieza.",
841
- "figure.text.held-on-plane": "{face} mantenida en {place} (separación {gap} mm)",
842
- "figure.text.held-on-plane-worked-out": "{face} mantenida en {place} (separación calculada por una fórmula)",
843
- "figure.text.held-on-part": "Unida por su {face} a {otherFace} de {other}",
840
+ "figure.text.held-on-plane": "Mantenida en {place} (separación {gap} mm)",
841
+ "figure.text.held-on-plane-worked-out": "Mantenida en {place} (separación calculada por una fórmula)",
842
+ "figure.text.held-on-part": "Unida a {otherFace} de {other}",
844
843
  "figure.button.open-fastening": "Abrir la configuración de unión",
845
844
  "figure.button.detach-held": "Separar",
846
845
  "figure.text.gizmo-size-turned-fastening": "{part} está unida girada, así que su tamaño no se puede arrastrar.",
@@ -870,7 +869,6 @@
870
869
  "figure.text.placement-is-intent": "La referencia con la que se crea la figura. Puede elegirla antes de añadir piezas; no cambia el espacio declarado.",
871
870
  "figure.button.show-model-settings": "Mostrar la configuración del modelo",
872
871
  "figure.text.new-figure-name": "Nueva figura",
873
- "figure.text.fasten-editing": "Editando la unión actual: su {face} en {target}",
874
872
  "figure.text.fasten-start": "Una esta pieza a una cara de otra pieza para que mantengan su relación cuando cualquiera de ellas cambie.",
875
873
  "figure.button.fasten-start": "Unir a otra pieza",
876
874
  "figure.text.fasten-target-first": "Elija primero la pieza a la que unirla.",
@@ -941,9 +939,9 @@
941
939
  "figure.label.fasten-mode-centre": "centrada sobre ella",
942
940
  "figure.text.fasten-off-face": "No toca {face}.",
943
941
  "figure.text.fasten-past-edges": "Sobresale de los bordes: {sides}",
944
- "figure.text.fasten-sentence-meet": "{face} de {part} apoyada en {to}",
945
- "figure.text.fasten-sentence-flush": "{face} de {part} a ras con {to}",
946
- "figure.text.fasten-sentence-centre": "centro de {part} sobre {to}",
942
+ "figure.text.fasten-sentence-meet": "Apoyada en {to}",
943
+ "figure.text.fasten-sentence-flush": "A ras con {to}",
944
+ "figure.text.fasten-sentence-centre": "Por su centro en {to}",
947
945
  "figure.label.measure-x": "ancho",
948
946
  "figure.label.measure-y": "altura",
949
947
  "figure.label.measure-z": "profundidad",
@@ -532,8 +532,9 @@
532
532
  "figure.text.overhang-out": "{on} の {face} の縁から出ています: {sides}。モデルの占有空間とは別に扱います。",
533
533
  "figure.text.overhang-larger": "{on} の {face} より広いため縁から出ています: {sides}。",
534
534
  "figure.text.overhang-lopsided": "{axes} 方向で片側に寄っています。",
535
- "figure.text.overhang-possible": "{on} の {face} の縁から出る可能性があります。回して取り付けた部品なので外接箱でのみ測りました。",
535
+ "figure.text.overhang-possible": "{on} の {face} の縁から出る可能性があります。",
536
536
  "figure.text.overhang-unknown": "{on} の {face} の縁の内側にあるか確認できませんでした。",
537
+ "figure.text.overhang-off": "{on} の {face} の外で接しています。そこでは何にも載っていません。",
537
538
  "figure.text.overhang-held": "この部品に取り付いた {parts} が取り付け面の縁から出ているか、出る可能性があります。",
538
539
  "figure.text.overhang-side-low": "{axis} 低い側 {mm} mm",
539
540
  "figure.text.overhang-side-high": "{axis} 高い側 {mm} mm",
@@ -562,7 +563,6 @@
562
563
  "figure.label.facing": "接し方",
563
564
  "figure.text.fasten-summary-gap": "間隔 {mm} mm",
564
565
  "figure.text.fasten-summary-gap-worked-out": "間隔は式で決まる",
565
- "figure.text.fasten-summary-twist": "ひねり {degrees}°",
566
566
  "figure.label.gap": "gap (mm)",
567
567
  "figure.label.align": "within the face",
568
568
  "figure.label.align-centre": "中央",
@@ -709,7 +709,6 @@
709
709
  "figure.text.row-ends": "低い側の端 {low} mm · 高い側の端 {high} mm 残る",
710
710
  "figure.text.row-ends-unknown": "両端に残る長さは確認できませんでした。",
711
711
  "figure.text.row-companions": "一緒に複製: {parts}",
712
- "figure.label.twist": "ひねり (°)",
713
712
  "figure.text.instance-size-undeclared": "図形サイズがないため、ボードでサイズを変えられません。",
714
713
  "figure.button.declare-instance-size": "図形サイズを宣言",
715
714
  "figure.text.proposal-applied": "{n} 個のコマンドを 1 ステップとして適用しました。",
@@ -739,7 +738,7 @@
739
738
  "figure.button.follow-size-in-proportion": "すべての部品を追従させる",
740
739
  "figure.text.proportion-kept": "固有の数値のまま: {what}",
741
740
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
742
- "figure.text.placement-plane-refused": "{parts} は取付平面に付いている面では {placement} に付けられません。外すか {faces} 面で付け直してから変更してください。",
741
+ "figure.text.placement-plane-refused": "取付平面に付いている部品があるため、配置を変えられません: {parts}。先にこの部品を外してください。",
743
742
  "figure.label.face-centre": "中心",
744
743
  "figure.label.budget-moving": "毎フレーム再計算する部品",
745
744
  "figure.label.performance-cost": "性能コスト",
@@ -838,9 +837,9 @@
838
837
  "figure.button.resize-scoped-center": "{scope}を今の中心を保ってサイズに追従",
839
838
  "figure.text.resize-move": "({axis} {mm} mm 移動)",
840
839
  "figure.text.held-by-rule": "規則がこの部品の位置を決めます。",
841
- "figure.text.held-on-plane": "{face}を{place}に保持中 (間隔 {gap} mm)",
842
- "figure.text.held-on-plane-worked-out": "{face}を{place}に保持中 (間隔は式で決まる)",
843
- "figure.text.held-on-part": "{other}の{otherFace}に{face}で取り付け",
840
+ "figure.text.held-on-plane": "{place}に保持中 (間隔 {gap} mm)",
841
+ "figure.text.held-on-plane-worked-out": "{place}に保持中 (間隔は式で決まる)",
842
+ "figure.text.held-on-part": "{other}の{otherFace}に取り付け",
844
843
  "figure.button.open-fastening": "取り付け設定を開く",
845
844
  "figure.button.detach-held": "取り付けを外す",
846
845
  "figure.text.gizmo-size-turned-fastening": "{part} は回して取り付けられているため、ドラッグでサイズを変えられません。",
@@ -870,7 +869,6 @@
870
869
  "figure.text.placement-is-intent": "作成の基準です。部品がなくても決められ、空間の宣言は変えません。",
871
870
  "figure.button.show-model-settings": "モデル設定を表示",
872
871
  "figure.text.new-figure-name": "新しい Figure",
873
- "figure.text.fasten-editing": "現在の取り付けを編集: {face}を{target}に",
874
872
  "figure.text.fasten-start": "別の部品の面に取り付けると、相手の部品が変わっても関係を保ちます。",
875
873
  "figure.button.fasten-start": "別の部品に取り付ける",
876
874
  "figure.text.fasten-target-first": "先に取り付ける部品を選んでください。",
@@ -941,9 +939,9 @@
941
939
  "figure.label.fasten-mode-centre": "中心を面に",
942
940
  "figure.text.fasten-off-face": "{face}に接していません。",
943
941
  "figure.text.fasten-past-edges": "縁からはみ出しています: {sides}",
944
- "figure.text.fasten-sentence-meet": "{part}の{face}を{to}に接して",
945
- "figure.text.fasten-sentence-flush": "{part}の{face}を{to}と同じ面に",
946
- "figure.text.fasten-sentence-centre": "{part}の中心を{to}の上に",
942
+ "figure.text.fasten-sentence-meet": "{to}に載せる",
943
+ "figure.text.fasten-sentence-flush": "{to}と同じ面に付ける",
944
+ "figure.text.fasten-sentence-centre": "{to}の中心に付ける",
947
945
  "figure.label.measure-x": "幅",
948
946
  "figure.label.measure-y": "高さ",
949
947
  "figure.label.measure-z": "奥行き",
@@ -532,8 +532,9 @@
532
532
  "figure.text.overhang-out": "{on}의 {face} 가장자리 밖으로 나와 있습니다: {sides}. 모델의 점유 공간과는 따로 봅니다.",
533
533
  "figure.text.overhang-larger": "{on}의 {face}보다 넓어 가장자리 밖으로 나와 있습니다: {sides}.",
534
534
  "figure.text.overhang-lopsided": "{axes} 방향으로 한쪽에 치우쳐 있습니다.",
535
- "figure.text.overhang-possible": "{on}의 {face} 가장자리 밖으로 나올 수 있습니다. 돌려 붙인 부품이라 둘러싼 상자로만 쟀습니다.",
535
+ "figure.text.overhang-possible": "{on}의 {face} 가장자리 밖으로 나올 수 있습니다.",
536
536
  "figure.text.overhang-unknown": "{on}의 {face} 가장자리 안에 있는지 확인하지 못했습니다.",
537
+ "figure.text.overhang-off": "닿는 곳이 {on}의 {face} 밖에 있어 그 자리에서는 받쳐지지 않습니다.",
537
538
  "figure.text.overhang-held": "이 부품에 붙은 {parts}이(가) 붙은 면 가장자리 밖으로 나와 있거나 나올 수 있습니다.",
538
539
  "figure.text.overhang-side-low": "{axis} 낮은 쪽 {mm} mm",
539
540
  "figure.text.overhang-side-high": "{axis} 높은 쪽 {mm} mm",
@@ -562,7 +563,6 @@
562
563
  "figure.label.facing": "닿는 방식",
563
564
  "figure.text.fasten-summary-gap": "간격 {mm} mm",
564
565
  "figure.text.fasten-summary-gap-worked-out": "간격은 식으로 정해짐",
565
- "figure.text.fasten-summary-twist": "비틀기 {degrees}°",
566
566
  "figure.label.gap": "간격 (mm)",
567
567
  "figure.label.align": "면 안 위치",
568
568
  "figure.label.align-centre": "가운데",
@@ -709,7 +709,6 @@
709
709
  "figure.text.row-ends": "낮은 쪽 끝 {low} mm · 높은 쪽 끝 {high} mm 남음",
710
710
  "figure.text.row-ends-unknown": "양 끝에 남는 길이는 확인하지 못했습니다.",
711
711
  "figure.text.row-companions": "함께 복제: {parts}",
712
- "figure.label.twist": "비틀기 (°)",
713
712
  "figure.text.instance-size-undeclared": "범위를 정하면 도면에서 크기를 바꿀 수 있습니다.",
714
713
  "figure.button.declare-instance-size": "늘어나는 범위 정하기",
715
714
  "figure.text.proposal-applied": "명령 {n}개를 한 단계로 적용했습니다.",
@@ -739,7 +738,7 @@
739
738
  "figure.button.follow-size-in-proportion": "모든 부품이 따라가게",
740
739
  "figure.text.proportion-kept": "자기 숫자로 남는 것: {what}",
741
740
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
742
- "figure.text.placement-plane-refused": "{parts} 가 장착 평면에 붙은 면으로는 {placement} 에 붙일 수 없습니다. 떼거나 {faces} 면으로 다시 붙인 뒤 바꾸세요.",
741
+ "figure.text.placement-plane-refused": "장착 평면에 붙은 부품이 있어 놓이는 곳을 바꿀 수 없습니다: {parts}. 먼저 이 부품을 떼어 주십시오.",
743
742
  "figure.label.face-centre": "가운데",
744
743
  "figure.label.budget-moving": "매 프레임 다시 계산하는 부품",
745
744
  "figure.label.performance-cost": "성능 비용",
@@ -838,9 +837,9 @@
838
837
  "figure.button.resize-scoped-center": "{scope} 현재 중심을 유지하며 크기 따라가기",
839
838
  "figure.text.resize-move": "({axis} {mm} mm 이동)",
840
839
  "figure.text.held-by-rule": "규칙이 이 부품의 자리를 정합니다.",
841
- "figure.text.held-on-plane": "{face}을 {place}에 유지 중 (간격 {gap} mm)",
842
- "figure.text.held-on-plane-worked-out": "{face}을 {place}에 유지 중 (간격은 식으로 정해짐)",
843
- "figure.text.held-on-part": "{other}의 {otherFace}에 {face}으로 붙어 있음",
840
+ "figure.text.held-on-plane": "{place}에 붙어 있음 (간격 {gap} mm)",
841
+ "figure.text.held-on-plane-worked-out": "{place}에 붙어 있음 (간격은 식으로 정해짐)",
842
+ "figure.text.held-on-part": "{other}의 {otherFace}에 붙어 있음",
844
843
  "figure.button.open-fastening": "붙임 설정 열기",
845
844
  "figure.button.detach-held": "붙임 풀기",
846
845
  "figure.text.gizmo-size-turned-fastening": "{part} 부품은 돌려 붙어 있어 끌어서 크기를 바꿀 수 없습니다.",
@@ -870,7 +869,6 @@
870
869
  "figure.text.placement-is-intent": "작성 기준입니다. 부품이 없어도 정할 수 있고, 공간 선언은 바꾸지 않습니다.",
871
870
  "figure.button.show-model-settings": "모델 설정 보기",
872
871
  "figure.text.new-figure-name": "새 Figure",
873
- "figure.text.fasten-editing": "현재 관계 수정 — {face} → {target}",
874
872
  "figure.text.fasten-start": "이 부품을 다른 부품의 면에 붙여 두면, 상대 부품의 크기나 위치가 바뀌어도 함께 움직입니다.",
875
873
  "figure.button.fasten-start": "다른 부품에 붙이기",
876
874
  "figure.text.fasten-target-first": "먼저 붙일 부품을 선택해 주세요.",
@@ -941,9 +939,9 @@
941
939
  "figure.label.fasten-mode-centre": "가운데를 면에",
942
940
  "figure.text.fasten-off-face": "{face}과 닿지 않습니다.",
943
941
  "figure.text.fasten-past-edges": "가장자리 밖으로 나와 있습니다: {sides}",
944
- "figure.text.fasten-sentence-meet": "{part}의 {face}을 {to}에 맞닿게",
945
- "figure.text.fasten-sentence-flush": "{part}의 {face}을 {to}과 같은 면으로",
946
- "figure.text.fasten-sentence-centre": "{part}의 가운데를 {to} 위에",
942
+ "figure.text.fasten-sentence-meet": "{to}에 올려놓기",
943
+ "figure.text.fasten-sentence-flush": "{to}과 같은 면에 붙이기",
944
+ "figure.text.fasten-sentence-centre": "{to} 가운데에 붙이기",
947
945
  "figure.label.measure-x": "너비",
948
946
  "figure.label.measure-y": "높이",
949
947
  "figure.label.measure-z": "깊이",
@@ -532,8 +532,9 @@
532
532
  "figure.text.overhang-out": "Ia terkeluar melepasi tepi {face} {on}: {sides}. Ini berasingan daripada ruang rajah.",
533
533
  "figure.text.overhang-larger": "Lebih lebar daripada {face} {on}, ia melepasi tepinya: {sides}.",
534
534
  "figure.text.overhang-lopsided": "Ia terletak ke satu sisi sepanjang {axes}.",
535
- "figure.text.overhang-possible": "Ia mungkin terkeluar melepasi tepi {face} {on}: ia dilekatkan berpusing, jadi hanya kotaknya diukur.",
535
+ "figure.text.overhang-possible": "Ia mungkin terkeluar melepasi tepi {face} {on}.",
536
536
  "figure.text.overhang-unknown": "Sama ada ia berada dalam tepi {face} {on} tidak dapat dibaca.",
537
+ "figure.text.overhang-off": "Tempat ia bersentuh berada di luar {face} {on}: di situ ia tidak disokong.",
537
538
  "figure.text.overhang-held": "Bahagian yang dilekatkan padanya ({parts}) terkeluar, atau mungkin terkeluar, melepasi tepi muka tempatnya.",
538
539
  "figure.text.overhang-side-low": "{axis} sisi bawah {mm} mm",
539
540
  "figure.text.overhang-side-high": "{axis} sisi atas {mm} mm",
@@ -562,7 +563,6 @@
562
563
  "figure.label.facing": "Cara muka bertemu",
563
564
  "figure.text.fasten-summary-gap": "jarak {mm} mm",
564
565
  "figure.text.fasten-summary-gap-worked-out": "jarak ditentukan oleh formula",
565
- "figure.text.fasten-summary-twist": "pulasan {degrees}°",
566
566
  "figure.label.gap": "gap (mm)",
567
567
  "figure.label.align": "within the face",
568
568
  "figure.label.align-centre": "di tengah",
@@ -709,7 +709,6 @@
709
709
  "figure.text.row-ends": "{low} mm tinggal di hujung bawah · {high} mm di hujung atas",
710
710
  "figure.text.row-ends-unknown": "Baki di hujung tidak dapat dibaca.",
711
711
  "figure.text.row-companions": "disalin bersama: {parts}",
712
- "figure.label.twist": "pusingan (°)",
713
712
  "figure.text.instance-size-undeclared": "rajah tiada saiz, jadi papan tidak boleh mengubah saiznya.",
714
713
  "figure.button.declare-instance-size": "isytihar saiz rajah",
715
714
  "figure.text.proposal-applied": "{n} arahan digunakan sebagai satu langkah.",
@@ -739,7 +738,7 @@
739
738
  "figure.button.follow-size-in-proportion": "jadikan semua bahagian mengikut",
740
739
  "figure.text.proportion-kept": "kekal dengan nombor sendiri: {what}",
741
740
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
742
- "figure.text.placement-plane-refused": "{parts} tidak boleh dipasang pada {placement} dengan muka yang dipasang pada satah pemasangan. tanggalkan atau pasang semula dengan muka {faces} dahulu.",
741
+ "figure.text.placement-plane-refused": "Ada bahagian yang dilekatkan pada satah pemasangan, jadi penempatan tidak boleh ditukar: {parts}. Tanggalkan bahagian itu dahulu.",
743
742
  "figure.label.face-centre": "tengah",
744
743
  "figure.label.budget-moving": "bahagian dikira semula setiap bingkai",
745
744
  "figure.label.performance-cost": "kos prestasi",
@@ -838,9 +837,9 @@
838
837
  "figure.button.resize-scoped-center": "{scope}: ikut saiz, kekalkan tengah sekarang",
839
838
  "figure.text.resize-move": "(bergerak {mm} mm pada {axis})",
840
839
  "figure.text.held-by-rule": "Satu peraturan meletakkan bahagian ini.",
841
- "figure.text.held-on-plane": "{face} dikekalkan pada {place} (jarak {gap} mm)",
842
- "figure.text.held-on-plane-worked-out": "{face} dikekalkan pada {place} (jarak ditentukan oleh formula)",
843
- "figure.text.held-on-part": "Dilekatkan dengan {face} pada {otherFace} {other}",
840
+ "figure.text.held-on-plane": "Dikekalkan pada {place} (jarak {gap} mm)",
841
+ "figure.text.held-on-plane-worked-out": "Dikekalkan pada {place} (jarak ditentukan oleh formula)",
842
+ "figure.text.held-on-part": "Dilekatkan pada {otherFace} {other}",
844
843
  "figure.button.open-fastening": "Buka tetapan lekatan",
845
844
  "figure.button.detach-held": "Tanggalkan",
846
845
  "figure.text.gizmo-size-turned-fastening": "{part} dilekatkan dalam keadaan berpusing, jadi saiznya tidak boleh diseret.",
@@ -870,7 +869,6 @@
870
869
  "figure.text.placement-is-intent": "Asas pengarangan rajah. Boleh dipilih sebelum ada bahagian; ia tidak mengubah ruang yang diisytiharkan.",
871
870
  "figure.button.show-model-settings": "Tunjukkan tetapan model",
872
871
  "figure.text.new-figure-name": "Rajah baharu",
873
- "figure.text.fasten-editing": "Menyunting lekatan semasa: {face} pada {target}",
874
872
  "figure.text.fasten-start": "Lekatkan bahagian ini pada muka bahagian lain supaya hubungannya kekal apabila salah satu berubah.",
875
873
  "figure.button.fasten-start": "Lekatkan pada bahagian lain",
876
874
  "figure.text.fasten-target-first": "Pilih bahagian yang hendak dilekatkan dahulu.",
@@ -941,9 +939,9 @@
941
939
  "figure.label.fasten-mode-centre": "tengah pada muka",
942
940
  "figure.text.fasten-off-face": "Ia tidak menyentuh {face}.",
943
941
  "figure.text.fasten-past-edges": "Ia melepasi tepi: {sides}",
944
- "figure.text.fasten-sentence-meet": "{face} {part} diletakkan pada {to}",
945
- "figure.text.fasten-sentence-flush": "{face} {part} sama rata dengan {to}",
946
- "figure.text.fasten-sentence-centre": "Tengah {part} pada {to}",
942
+ "figure.text.fasten-sentence-meet": "Diletakkan pada {to}",
943
+ "figure.text.fasten-sentence-flush": "Sama rata dengan {to}",
944
+ "figure.text.fasten-sentence-centre": "Dengan tengahnya pada {to}",
947
945
  "figure.label.measure-x": "lebar",
948
946
  "figure.label.measure-y": "tinggi",
949
947
  "figure.label.measure-z": "dalam",