@things-factory/figure-ui 10.1.194 → 10.1.195

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.194",
3
+ "version": "10.1.195",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -48,5 +48,5 @@
48
48
  "@things-factory/setting-base": "^10.1.192",
49
49
  "three": "^0.185.1"
50
50
  },
51
- "gitHead": "489dd836ae99063bfed6706367ea1d7cd7ed004a"
51
+ "gitHead": "e22e6dec8be22ba1d8db7bf511f3bacee721ff8f"
52
52
  }
@@ -95,13 +95,21 @@ test('a tilted part is told it touches by a point or an edge, a ball by its lowe
95
95
  assert.ok(!/기울|기운/.test(ko['figure.text.aligned-extreme-floor'] + ko['figure.text.align-to-floor-extreme']), 'a ball is not said to be tilted')
96
96
  })
97
97
 
98
- /* Designer 2026-09-27: the gizmo's reason for a tilted part named the floor's lowest point in every placement. */
99
- test('the size gizmo\'s reason names what the placement keeps, in every language', () => {
98
+ /*
99
+ User 2026-10-19: the size drag's refusal told the face kept while dragging and the one written, and was not understood.
100
+ One line, why only -- attached turned, or tilted -- in every language; the way on is the notice's button.
101
+ */
102
+ test('the size drag\'s refusal is one line saying why, in every language', () => {
100
103
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
101
104
  const t = JSON.parse(read(`../translations/${lang}.json`))
102
- assert.ok(t['figure.text.gizmo-size-turned-fastening'].includes('{extreme}'), lang)
103
- for (const placement of ['floor', 'ceiling', 'wall', 'center']) assert.ok(t[`figure.label.kept-extreme-${placement}`], `${lang} ${placement}`)
105
+ for (const key of ['figure.text.gizmo-size-turned-fastening', 'figure.text.gizmo-size-tilted']) {
106
+ assert.ok(t[key]?.includes('{part}'), `${lang} ${key}`)
107
+ assert.ok(!t[key].includes('{extreme}'), `${lang} ${key}: no face kept is told`)
108
+ assert.equal(t[key].split(/[.。]\s*/).filter(Boolean).length, 1, `${lang} ${key}: one sentence`)
109
+ }
110
+ assert.ok(t['figure.button.to-size-fields'], lang)
104
111
  }
105
112
  const ko = JSON.parse(read('../translations/ko.json'))
106
- assert.equal(ko['figure.label.kept-extreme-ceiling'], '가장 높은 점')
113
+ assert.equal(ko['figure.text.gizmo-size-turned-fastening'], '{part} 부품은 돌려 붙어 있어 끌어서 크기를 바꿀 수 없습니다.')
114
+ assert.equal(ko['figure.button.to-size-fields'], '크기 칸으로')
107
115
  })
@@ -532,31 +532,27 @@ describe('the recipe catalog says nothing where a template has no guide', () =>
532
532
  })
533
533
 
534
534
  /*
535
- The size gizmo's reason says what the figure's placement keeps (designer 2026-09-27). The page loads no translations;
536
- here the two sentences are given words, so the word the reason carries can be read.
535
+ The size gizmo withheld from a tilted part says why in one line (user 2026-10-19: the faces kept while dragging and
536
+ when written were not understood), as a notice, not the error's red. The page loads no translations; the sentence is
537
+ given words here, so the part it names can be read.
537
538
  */
538
- describe('the size gizmo withheld from a tilted part says what the placement keeps', () => {
539
+ describe('the size gizmo withheld from a tilted part says why, in one line, as a notice', () => {
539
540
  before(async () => {
540
541
  await mount('box', 'tilted-ceiling')
541
- await read(() =>
542
- editing.words({
543
- 'figure.text.gizmo-size-turned-fastening': '{part} is tilted: its size is typed, keeping {extreme}',
544
- 'figure.label.kept-extreme-ceiling': 'its highest point',
545
- 'figure.label.kept-extreme-floor': 'its lowest point'
546
- })
547
- )
542
+ await read(() => editing.words({ 'figure.text.gizmo-size-tilted': '{part} is tilted, so its size cannot be dragged.' }))
548
543
  await pick('box')
549
544
  })
550
545
  after(() => read(() => editing.words(null)))
551
546
 
552
- test('hung from the ceiling, the tooltip and the refusal of R both name its highest point', async () => {
547
+ test('hung from the ceiling, the tooltip and the refusal of R say the same line; nothing is said of the faces it keeps', async () => {
553
548
  const resize = (await read(() => editing.tools())).find(t => t.label === 'figure.button.gizmo-scale')
554
549
  assert.equal(resize.disabled, true, 'not offered')
555
- assert.ok(resize.title.endsWith('box is tilted: its size is typed, keeping its highest point'), resize.title)
550
+ assert.ok(resize.title.endsWith('box is tilted, so its size cannot be dragged.'), resize.title)
556
551
  await read(() => editing.told())
557
552
  await press('KeyR')
558
- assert.deepEqual((await read(() => editing.told())).filter(n => n.level === 'error').map(n => n.message), ['box is tilted: its size is typed, keeping its highest point'])
553
+ assert.deepEqual(await read(() => editing.told()), [{ level: 'info', message: 'box is tilted, so its size cannot be dragged.' }])
559
554
  assert.equal((await state()).attached, null, 'no gizmo on it')
555
+ assert.equal((await state()).error, '', 'no error')
560
556
  })
561
557
  })
562
558
 
@@ -370,7 +370,7 @@ describe('the fastening window on the robot arm', () => {
370
370
 
371
371
  test('the parts hanging from it are said in one line, and the chips show the part names as they are written', async () => {
372
372
  const said = await read(() => {
373
- const root = document.querySelector('figure-fasten-dialog').shadowRoot
373
+ const root = window.fastenDialog().shadowRoot
374
374
  const chip = root.querySelector('ox-input-select-buttons').shadowRoot.querySelector('md-filter-chip')
375
375
  const label = chip.shadowRoot.querySelector('.label-text') ?? chip.shadowRoot.querySelector('.label')
376
376
  return { lines: root.querySelectorAll('p[loops]').length, transform: getComputedStyle(label).textTransform }
@@ -388,14 +388,17 @@ describe('a new fastening for a door leaf: chosen in the window, seen before it
388
388
  })
389
389
  const ghosts = () => read(() => look.drawn().filter(n => n.startsWith('__preview')))
390
390
 
391
- test('started, the window opens in the standard popup, asking what to fasten to, nothing chosen', async () => {
391
+ test('started, the window opens over the side panel, asking what to fasten to, nothing chosen; the canvas stays in sight', async () => {
392
392
  await pick('leaf')
393
393
  await press('button[fasten-start]')
394
394
  const list = await until(() => probe.fastenTargets(), [], 'the fastening window')
395
395
  assert.deepEqual(list.filter(t => t.selected), [], 'nothing chosen')
396
396
  assert.ok(list.some(t => t.target === 'frame' && !t.disabled), 'the frame offered')
397
397
  assert.deepEqual((await all('[apply]')).map(b => b.disabled), [true], 'nothing to apply yet')
398
- assert.deepEqual(await read(() => probe.fastenPopup()), { title: 'figure.label.fastening', movable: true, backdrop: true, role: 'dialog' })
398
+ /* Not the modal popup (user 2026-10-19: it covered the 3D it was choosing for): over the side panel, at most its edge over the canvas. */
399
+ const panel = await read(() => probe.fastenPanel())
400
+ assert.deepEqual({ ...panel, overCanvas: undefined }, { title: 'figure.label.fastening', role: 'dialog', popup: false, overCanvas: undefined })
401
+ assert.ok(panel.overCanvas <= 0.1, `the canvas stays in sight: ${panel.overCanvas} of its width covered`)
399
402
  })
400
403
 
401
404
  test('the frame chosen: the editor is told the command, and the canvas draws where the leaf would go and the face it meets', async () => {
@@ -421,14 +424,15 @@ describe('a new fastening for a door leaf: chosen in the window, seen before it
421
424
  assert.deepEqual(await ghosts(), [], 'nothing of the earlier result drawn')
422
425
  })
423
426
 
424
- test('a press outside the window closes it with nothing written, and nothing it tried is drawn', async () => {
427
+ test('another part picked in the list closes the window with nothing written, and nothing it tried is drawn', async () => {
425
428
  const start = await state()
426
- /* The motion tab, under the popup's backdrop: the press closes the popup, it does not reach the tab. */
427
- await click(await read(l => probe.tab(l), 'figure.label.tab-short-motion'), 'outside the window')
429
+ /* No backdrop over the page: the parts list stays reachable. The window stands over the side panel, its tabs too, as the view settings do. */
430
+ assert.ok((await read(() => probe.fastenPanel())).overCanvas <= 0.1)
431
+ await pick('frame')
428
432
  await until(() => !probe.fastenTargets(), [], 'the window closed')
429
433
  await until(() => !look.drawn().some(n => n.startsWith('__preview')), [], 'nothing it tried drawn')
430
- assert.equal(await read(() => look.tab()), 'figure.label.tab-short-properties', 'the press did not reach the tab')
431
434
  assert.equal((await state()).revision, start.revision, 'nothing written')
435
+ await pick('leaf')
432
436
  })
433
437
 
434
438
  test('"fasten it first" from the motion tab opens this part\'s window, on what it has, on the properties tab', async () => {
@@ -599,23 +603,32 @@ function twisted() {
599
603
  }
600
604
 
601
605
  describe('a part fastened twisted is not sized by a drag', () => {
602
- test('the resize tool is off, and a size drag that still arrives is not written and is said', async () => {
606
+ test('the resize tool is off, and a size drag that still arrives is not written: a notice says why, in one line, with the way to its size', async () => {
603
607
  await mount('twisted', twisted())
604
608
  await pick('blade')
605
609
  assert.equal((await at('button[tool]', 'figure.button.gizmo-scale'))?.disabled, true, 'the resize tool is off')
610
+ await read(() => probe.told())
606
611
  const start = await state()
607
612
  await read(() => look.transformed('blade', { scale: [1.5, 1, 1] }))
608
613
  await settled()
609
614
  assert.equal((await state()).revision, start.revision, 'not written')
610
- assert.equal((await state()).error, 'figure.text.gizmo-size-turned-fastening')
615
+ /* A notice, not the error's red (user 2026-10-19: the reason read as a fault, and was not understood). */
616
+ assert.deepEqual(await read(() => probe.told()), [{ level: 'info', message: 'figure.text.gizmo-size-turned-fastening', action: 'figure.button.to-size-fields' }])
617
+ assert.equal((await state()).error, '', 'no error')
611
618
  })
612
619
 
613
- test('the R key says why', async () => {
620
+ test('the R key says why, and the notice\'s button puts the cursor in the part\'s size field', async () => {
614
621
  await mount('twisted', twisted())
615
622
  await pick('blade')
616
- assert.equal((await state()).error, '')
623
+ await read(() => probe.told())
617
624
  await page().keyboard.press('KeyR')
618
- await until(() => probe.state().error === 'figure.text.gizmo-size-turned-fastening', [], 'the reason said')
625
+ await until(() => probe.told().some(n => n.message === 'figure.text.gizmo-size-turned-fastening' && n.action === 'figure.button.to-size-fields'), [], 'the reason said with its way on')
626
+ assert.equal((await state()).error, '', 'no error')
627
+ /* From another tab: the button brings the properties tab and the size field. */
628
+ await tab('motion')
629
+ assert.equal(await read(() => probe.pressNoticeButton()), true)
630
+ await until(() => probe.focused()?.label?.startsWith('blade.size.'), [], 'the blade\'s size field focused')
631
+ assert.equal(await read(() => look.tab()), 'figure.label.tab-short-properties')
619
632
  })
620
633
  })
621
634
 
@@ -741,26 +754,27 @@ describe('the fastening window: tried freely, written once', () => {
741
754
  await until(w => JSON.stringify(JSON.parse(document.querySelector('figure-editor').session.source()).joints.find(j => j.body1.part === 'lamp')) === w, [JSON.stringify(was)], 'one undo: the fastening as it was')
742
755
  })
743
756
 
744
- test('moved by the popup\'s head out of the way of the canvas: nothing written, still open', async () => {
757
+ test('the canvas is turned with the window open: nothing written, still open, the window where it was', async () => {
745
758
  await pick('lamp')
746
759
  await changeFastening()
747
760
  const rect = () => read(() => {
748
- const r = document.querySelector('figure-fasten-dialog')?.getBoundingClientRect()
761
+ const r = window.fastenDialog()?.getBoundingClientRect()
749
762
  return r ? { x: Math.round(r.left), y: Math.round(r.top) } : null
750
763
  })
751
764
  const was = await rect()
752
- const head = await read(() => {
753
- const h = document.querySelector('figure-fasten-dialog').closest('ox-floating-overlay').shadowRoot.querySelector('[header] h1').getBoundingClientRect()
754
- return { x: h.left + h.width / 2, y: h.top + h.height / 2 }
765
+ /* A drag on the empty canvas, left of the parts: the camera turns, nothing is picked. */
766
+ const box = await read(() => {
767
+ const r = document.querySelector('figure-editor').shadowRoot.querySelector('figure-canvas').getBoundingClientRect()
768
+ return { x: r.left + 40, y: r.top + r.height / 2 }
755
769
  })
756
770
  const start = await state()
757
- await page().mouse.move(head.x, head.y)
771
+ await page().mouse.move(box.x, box.y)
758
772
  await page().mouse.down()
759
- for (let i = 1; i <= 6; i++) await page().mouse.move(head.x - 30 * i, head.y + 20 * i)
773
+ for (let i = 1; i <= 6; i++) await page().mouse.move(box.x + 20 * i, box.y + 5 * i)
760
774
  await page().mouse.up()
761
- const moved = await rect()
762
- assert.deepEqual([moved.x - was.x, moved.y - was.y], [-180, 120], `moved with the pointer: ${JSON.stringify([was, moved])}`)
775
+ await settled()
763
776
  assert.equal(await windowOpen(), true, 'still open')
777
+ assert.deepEqual(await rect(), was, 'the window where it was')
764
778
  assert.equal((await state()).revision, start.revision, 'nothing written')
765
779
  await press('[cancel]')
766
780
  await until(() => !probe.fastenTargets(), [], 'the window closed')
@@ -771,7 +785,7 @@ describe('the fastening window: tried freely, written once', () => {
771
785
  await changeFastening()
772
786
  const litAt = y => Math.abs((document.querySelector('figure-editor').shadowRoot.querySelector('figure-canvas').highlight?.pose.t[1] ?? 0) - y) < 2
773
787
  /* The list has the focus, on the top it is fastened to. */
774
- await until(() => document.querySelector('figure-fasten-dialog')?.shadowRoot.querySelector('ox-input-select-buttons')?.shadowRoot.activeElement?.getAttribute('data-value') === 'top', [], 'the focus on the chosen chip')
788
+ await until(() => window.fastenDialog()?.shadowRoot.querySelector('ox-input-select-buttons')?.shadowRoot.activeElement?.getAttribute('data-value') === 'top', [], 'the focus on the chosen chip')
775
789
  await page().keyboard.press('ArrowLeft')
776
790
  /* The leg's top, 700 high (the lit plate a little over it); moving the focus lights, it does not choose. */
777
791
  await until(litAt, [702], 'the leg\'s top lit')
@@ -87,6 +87,20 @@ function deep(selector, root = document) {
87
87
 
88
88
  const editor = () => document.querySelector('figure-editor')
89
89
  const canvas = () => deep('figure-canvas')[0]
90
+ /** The fastening window, wherever the editor draws it. */
91
+ window.fastenDialog = () => deep('figure-fasten-dialog')[0] ?? null
92
+
93
+ /*
94
+ * What the app's notification surface is handed (`notify`), in order. The page draws no snackbar, so a notice's button
95
+ * is pressed here, as the snackbar's would call it.
96
+ */
97
+ const notices = []
98
+ const pressable = []
99
+ document.addEventListener('notify', e => {
100
+ notices.push(e.detail)
101
+ const action = e.detail?.option?.action
102
+ if (action?.callback) pressable.push(action.callback)
103
+ })
90
104
  const three = () => canvas().scene.root._threeCapability
91
105
  const controls = () => three().gizmoManager.controls
92
106
 
@@ -218,10 +232,40 @@ window.probe = {
218
232
  const chip = deep('figure-fasten-dialog')[0]?.shadowRoot.querySelector('ox-input-select-buttons')?.shadowRoot?.querySelector(`md-filter-chip[data-value="${CSS.escape(part)}"]`)
219
233
  return chip ? centreOf(chip) : null
220
234
  },
221
- /** The fastening window's popup, as the standard overlay holds it: its title, whether its head moves it, its backdrop. */
222
- fastenPopup() {
223
- const overlay = deep('figure-fasten-dialog')[0]?.closest('ox-floating-overlay')
224
- return overlay ? { title: overlay.title, movable: overlay.movable, backdrop: !!overlay.backdrop, role: overlay.querySelector('figure-fasten-dialog').getAttribute('role') } : null
235
+ /**
236
+ * The fastening window as it stands: its title, its role, whether a popup or its backdrop is anywhere on the page, and
237
+ * how much of the canvas it covers (a share of the canvas's width).
238
+ */
239
+ fastenPanel() {
240
+ const dialog = deep('figure-fasten-dialog')[0]
241
+ const panel = dialog?.closest('[fasten-panel]')
242
+ if (!panel) return null
243
+ const p = panel.getBoundingClientRect(),
244
+ c = canvas().getBoundingClientRect()
245
+ const across = Math.max(0, Math.min(p.right, c.right) - Math.max(p.left, c.left))
246
+ return {
247
+ title: panel.querySelector('h3')?.textContent.trim(),
248
+ role: dialog.getAttribute('role'),
249
+ popup: deep('ox-floating-overlay').length > 0,
250
+ overCanvas: Math.round((across / c.width) * 100) / 100
251
+ }
252
+ },
253
+ /** What the notification surface was handed since the last call: its level, its words, its button's label. */
254
+ told() {
255
+ return notices.splice(0).map(n => ({ level: n.level, message: n.message, action: n.option?.action?.label ?? null }))
256
+ },
257
+ /** The button of the last notice that had one, pressed. */
258
+ pressNoticeButton() {
259
+ const last = [...pressable].pop()
260
+ if (!last) return false
261
+ last()
262
+ return true
263
+ },
264
+ /** The element that has the focus, through shadow roots: its tag and aria-label. */
265
+ focused() {
266
+ let at = document.activeElement
267
+ while (at?.shadowRoot?.activeElement) at = at.shadowRoot.activeElement
268
+ return at ? { tag: at.localName, label: at.getAttribute('aria-label') } : null
225
269
  },
226
270
  /** The page point of an input by its aria-label, or of the part name field. */
227
271
  nameField() {
@@ -155,6 +155,12 @@ function shellMoves(move) {
155
155
  shellMoving = true
156
156
  try {
157
157
  move()
158
+ /*
159
+ The address layer adopts a raw History write the next time the address is read or written, and tells its
160
+ subscribers then (@operato/utils 10.29.43, \`adoptForeignWrite\`). Read it here, so the shell's own move is
161
+ adopted while it is still known as the shell's -- not at the product's next step, recorded as the product's.
162
+ */
163
+ void addressController().entry
158
164
  } finally {
159
165
  shellMoving = false
160
166
  }
@@ -184,11 +184,11 @@ describe('a part\'s groups in one order, whatever its state', () => {
184
184
  })
185
185
  assert.deepEqual(await place(), { sentence: true, change: true, drawing: false, cells: false })
186
186
  await press('button[change-fastening]', '「바꾸기」')
187
- await until(() => !!document.querySelector('figure-fasten-dialog'), [], 'the window')
187
+ await until(() => !!window.fastenDialog(), [], 'the window')
188
188
  assert.deepEqual(await place(), { sentence: true, change: true, drawing: false, cells: false }, 'the panel still says the result')
189
189
  /* Closed with nothing written; another part chosen and back: one line. */
190
190
  await page().keyboard.press('Escape')
191
- await until(() => !document.querySelector('figure-fasten-dialog'), [], 'the window closed')
191
+ await until(() => !window.fastenDialog(), [], 'the window closed')
192
192
  await pick('bed')
193
193
  await pick('mark')
194
194
  assert.equal((await place()).change, true, 'said in one line again')
@@ -843,11 +843,9 @@
843
843
  "figure.text.held-on-part": "Attached by its {face} to the {otherFace} of {other}",
844
844
  "figure.button.open-fastening": "Open attachment settings",
845
845
  "figure.button.detach-held": "Detach",
846
- "figure.text.gizmo-size-turned-fastening": "{part} is turned off its plane or attached twisted, so the face kept while dragging and the one kept when written would differ. Type the size in its field instead -- the placement's face (or {extreme}) stays where it is.",
847
- "figure.label.kept-extreme-floor": "lowest point",
848
- "figure.label.kept-extreme-ceiling": "highest point",
849
- "figure.label.kept-extreme-wall": "backmost point",
850
- "figure.label.kept-extreme-center": "centre",
846
+ "figure.text.gizmo-size-turned-fastening": "{part} is attached turned, so its size cannot be dragged.",
847
+ "figure.text.gizmo-size-tilted": "{part} is tilted, so its size cannot be dragged.",
848
+ "figure.button.to-size-fields": "Go to size",
851
849
  "figure.label.align-input": "Follows its field ({field})",
852
850
  "figure.label.tab-short-properties": "Properties",
853
851
  "figure.label.tab-short-capability": "Capability",
@@ -843,11 +843,9 @@
843
843
  "figure.text.held-on-part": "Unida por su {face} a {otherFace} de {other}",
844
844
  "figure.button.open-fastening": "Abrir la configuración de unión",
845
845
  "figure.button.detach-held": "Separar",
846
- "figure.text.gizmo-size-turned-fastening": "{part} está girada fuera de su plano o unida con torsión, por lo que la cara que se mantiene al arrastrar y la que se mantiene al guardar serían distintas. Escriba el tamaño en su campo; la cara de la colocación (o {extreme}) se queda donde está.",
847
- "figure.label.kept-extreme-floor": "punto más bajo",
848
- "figure.label.kept-extreme-ceiling": "punto más alto",
849
- "figure.label.kept-extreme-wall": "punto más trasero",
850
- "figure.label.kept-extreme-center": "centro",
846
+ "figure.text.gizmo-size-turned-fastening": "{part} está unida girada, así que su tamaño no se puede arrastrar.",
847
+ "figure.text.gizmo-size-tilted": "{part} está inclinada, así que su tamaño no se puede arrastrar.",
848
+ "figure.button.to-size-fields": "Ir al tamaño",
851
849
  "figure.label.align-input": "Sigue su campo ({field})",
852
850
  "figure.label.tab-short-properties": "Propiedades",
853
851
  "figure.label.tab-short-capability": "Capacidad",
@@ -843,11 +843,9 @@
843
843
  "figure.text.held-on-part": "{other}の{otherFace}に{face}で取り付け",
844
844
  "figure.button.open-fastening": "取り付け設定を開く",
845
845
  "figure.button.detach-held": "取り付けを外す",
846
- "figure.text.gizmo-size-turned-fastening": "{part} は傾けて回転しているか、ひねって取り付けられているため、ドラッグ中に見える面と確定する面が異なります。ギズモではなくサイズ欄に数値を入力してください。配置が保つ面(または{extreme})はそのまま残ります。",
847
- "figure.label.kept-extreme-floor": "最も低い点",
848
- "figure.label.kept-extreme-ceiling": "最も高い点",
849
- "figure.label.kept-extreme-wall": "最も奥の点",
850
- "figure.label.kept-extreme-center": "中心",
846
+ "figure.text.gizmo-size-turned-fastening": "{part} は回して取り付けられているため、ドラッグでサイズを変えられません。",
847
+ "figure.text.gizmo-size-tilted": "{part} は傾いているため、ドラッグでサイズを変えられません。",
848
+ "figure.button.to-size-fields": "サイズ欄へ",
851
849
  "figure.label.align-input": "位置欄に従う ({field})",
852
850
  "figure.label.tab-short-properties": "属性",
853
851
  "figure.label.tab-short-capability": "能力",
@@ -843,11 +843,9 @@
843
843
  "figure.text.held-on-part": "{other}의 {otherFace}에 {face}으로 붙어 있음",
844
844
  "figure.button.open-fastening": "붙임 설정 열기",
845
845
  "figure.button.detach-held": "붙임 풀기",
846
- "figure.text.gizmo-size-turned-fastening": "{part} 부품은 회전하거나 비틀린 상태로 부착되어 드래그 중 표시되는 면과 최종 적용되는 면이 다릅니다. 크기 항목에 숫자를 직접 입력해 주세요. 배치 기준면(또는 {extreme})의 위치는 유지됩니다.",
847
- "figure.label.kept-extreme-floor": "가장 낮은 점",
848
- "figure.label.kept-extreme-ceiling": "가장 높은 점",
849
- "figure.label.kept-extreme-wall": "가장 뒤쪽 점",
850
- "figure.label.kept-extreme-center": "중심",
846
+ "figure.text.gizmo-size-turned-fastening": "{part} 부품은 돌려 붙어 있어 끌어서 크기를 바꿀 수 없습니다.",
847
+ "figure.text.gizmo-size-tilted": "{part} 부품은 기울어 있어 끌어서 크기를 바꿀 수 없습니다.",
848
+ "figure.button.to-size-fields": "크기 칸으로",
851
849
  "figure.label.align-input": "위치 칸을 따름 ({field})",
852
850
  "figure.label.tab-short-properties": "속성",
853
851
  "figure.label.tab-short-capability": "능력",
@@ -843,11 +843,9 @@
843
843
  "figure.text.held-on-part": "Dilekatkan dengan {face} pada {otherFace} {other}",
844
844
  "figure.button.open-fastening": "Buka tetapan lekatan",
845
845
  "figure.button.detach-held": "Tanggalkan",
846
- "figure.text.gizmo-size-turned-fastening": "{part} diputar condong atau dilekatkan dipulas, jadi muka yang dikekalkan semasa diseret dan semasa ditulis akan berbeza. Taip saiz dalam medannya -- muka letakan (atau {extreme}) kekal di tempatnya.",
847
- "figure.label.kept-extreme-floor": "titik terendah",
848
- "figure.label.kept-extreme-ceiling": "titik tertinggi",
849
- "figure.label.kept-extreme-wall": "titik paling belakang",
850
- "figure.label.kept-extreme-center": "tengah",
846
+ "figure.text.gizmo-size-turned-fastening": "{part} dilekatkan dalam keadaan berpusing, jadi saiznya tidak boleh diseret.",
847
+ "figure.text.gizmo-size-tilted": "{part} condong, jadi saiznya tidak boleh diseret.",
848
+ "figure.button.to-size-fields": "Ke medan saiz",
851
849
  "figure.label.align-input": "Mengikut medannya ({field})",
852
850
  "figure.label.tab-short-properties": "Sifat",
853
851
  "figure.label.tab-short-capability": "Keupayaan",
@@ -843,11 +843,9 @@
843
843
  "figure.text.held-on-part": "以{face}附着在{other}的{otherFace}上",
844
844
  "figure.button.open-fastening": "打开附着设置",
845
845
  "figure.button.detach-held": "解除附着",
846
- "figure.text.gizmo-size-turned-fastening": "{part} 已倾斜旋转或扭转附着,拖动时保持的面与确定时保持的面会不同。请在尺寸栏中输入数值,而不是使用操纵器——放置所保持的面(或{extreme})会留在原位。",
847
- "figure.label.kept-extreme-floor": "最低点",
848
- "figure.label.kept-extreme-ceiling": "最高点",
849
- "figure.label.kept-extreme-wall": "最靠后的点",
850
- "figure.label.kept-extreme-center": "中心",
846
+ "figure.text.gizmo-size-turned-fastening": "{part} 是旋转后附着的,无法拖动改变尺寸。",
847
+ "figure.text.gizmo-size-tilted": "{part} 是倾斜的,无法拖动改变尺寸。",
848
+ "figure.button.to-size-fields": "转到尺寸栏",
851
849
  "figure.label.align-input": "跟随位置栏 ({field})",
852
850
  "figure.label.tab-short-properties": "属性",
853
851
  "figure.label.tab-short-capability": "能力",