@things-factory/figure-ui 10.1.72 → 10.1.74

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 (39) hide show
  1. package/client/modeller/figure-v3-links.ts +12 -5
  2. package/client/modeller/figure-v3-parts.ts +1 -0
  3. package/client/modeller/figure-v3-properties.ts +3 -2
  4. package/client/modeller/v3-editor.ts +86 -2
  5. package/client/modeller/v3-figure-facts.ts +26 -2
  6. package/client/modeller/v3-links.ts +50 -0
  7. package/client/modeller/v3-recipes.ts +16 -1
  8. package/dist-client/modeller/figure-v3-links.js +9 -3
  9. package/dist-client/modeller/figure-v3-links.js.map +1 -1
  10. package/dist-client/modeller/figure-v3-parts.js +1 -0
  11. package/dist-client/modeller/figure-v3-parts.js.map +1 -1
  12. package/dist-client/modeller/figure-v3-properties.js +3 -2
  13. package/dist-client/modeller/figure-v3-properties.js.map +1 -1
  14. package/dist-client/modeller/v3-clips.d.ts +1 -1
  15. package/dist-client/modeller/v3-editor.d.ts +9 -0
  16. package/dist-client/modeller/v3-editor.js +88 -1
  17. package/dist-client/modeller/v3-editor.js.map +1 -1
  18. package/dist-client/modeller/v3-figure-facts.d.ts +9 -0
  19. package/dist-client/modeller/v3-figure-facts.js +23 -2
  20. package/dist-client/modeller/v3-figure-facts.js.map +1 -1
  21. package/dist-client/modeller/v3-links.d.ts +17 -0
  22. package/dist-client/modeller/v3-links.js +38 -0
  23. package/dist-client/modeller/v3-links.js.map +1 -1
  24. package/dist-client/modeller/v3-recipes.js +21 -1
  25. package/dist-client/modeller/v3-recipes.js.map +1 -1
  26. package/dist-client/tsconfig.fui.tsbuildinfo +1 -1
  27. package/dist-client/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +5 -5
  29. package/test/browser/modeller-page.js +40 -1
  30. package/test/browser/modeller.check.mjs +65 -3
  31. package/test/v3-fields.test.ts +2 -1
  32. package/test/v3-links.test.ts +25 -0
  33. package/test/v3-properties.test.ts +13 -0
  34. package/test/v3-recipes.test.ts +7 -0
  35. package/translations/en.json +11 -2
  36. package/translations/ja.json +10 -1
  37. package/translations/ko.json +11 -2
  38. package/translations/ms.json +10 -1
  39. package/translations/zh.json +10 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.72",
3
+ "version": "10.1.74",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -27,8 +27,8 @@
27
27
  "clean": "npm run clean:server && npm run clean:client"
28
28
  },
29
29
  "dependencies": {
30
- "@hatiolab/figure-model": "^0.1.87",
31
- "@hatiolab/things-scene": "^10.1.56",
30
+ "@hatiolab/figure-model": "^0.1.90",
31
+ "@hatiolab/things-scene": "^10.1.60",
32
32
  "@material/web": "^2.0.0",
33
33
  "@operato/data-grist": "^10.0.0",
34
34
  "@operato/graphql": "^10.0.0",
@@ -41,8 +41,8 @@
41
41
  "@operato/styles": "^10.0.0",
42
42
  "@operato/utils": "^10.0.0",
43
43
  "@things-factory/ai-assistant": "^10.1.58",
44
- "@things-factory/figure-service": "^10.1.72",
44
+ "@things-factory/figure-service": "^10.1.74",
45
45
  "three": "^0.185.1"
46
46
  },
47
- "gitHead": "fdf6d4faac454dbdca62d8fe7409395dff1ada22"
47
+ "gitHead": "59d5114dc91f6dd746f114bc047f07d70313c776"
48
48
  }
@@ -4,8 +4,9 @@
4
4
  * `client/` by the runner.
5
5
  */
6
6
  import 'figure-ui-client/modeller/v3-editor.js'
7
+ import { budgetOf, v3ReportOf } from 'figure-ui-client/modeller/v3-maturity.js'
7
8
  import { createV3ProductSession } from 'figure-ui-client/modeller/v3-product-session.js'
8
- import { addV3Box, addV3Primitive, applyV3Authoring, createV3Asset, robotArmV3, serializeV3Asset } from '@hatiolab/figure-model'
9
+ import { addV3Box, addV3Primitive, applyV3Authoring, createV3Asset, parseV3Asset, robotArmV3, serializeV3Asset } from '@hatiolab/figure-model'
9
10
 
10
11
  const PALETTE = {
11
12
  'palette.dark': '#2b2b2b',
@@ -153,10 +154,38 @@ window.probe = {
153
154
  const l = Math.hypot(b.x - a.x, b.y - a.y) || 1
154
155
  return { x: (b.x - a.x) / l, y: (b.y - a.y) / l }
155
156
  },
157
+ /** The page point of an input or checkbox by its aria-label. */
158
+ labelled(label) {
159
+ const input = deep('input').find(i => i.getAttribute('aria-label') === label)
160
+ return input ? centreOf(input) : null
161
+ },
162
+ /** The page point of a button by an attribute it carries. */
163
+ button(attribute) {
164
+ const b = deep(`button[${attribute}]`)[0]
165
+ return b ? { ...centreOf(b), disabled: b.disabled } : null
166
+ },
167
+ /** The figure as the session holds it. */
168
+ source() {
169
+ return JSON.parse(editor().session.source())
170
+ },
156
171
  /** The ids of every body drawn. */
157
172
  ids() {
158
173
  return editor().session.read().geometry.map(g => g.id)
159
174
  },
175
+ /** The value of the selects that offer this option, as they stand. */
176
+ selectsOffering(value) {
177
+ return deep('select').filter(s => [...s.options].some(o => o.value === value)).map(s => s.value)
178
+ },
179
+ /** The page point of an input by its aria-label, or of the part name field. */
180
+ nameField() {
181
+ const input = deep('input[name-field]')[0]
182
+ return input ? { ...centreOf(input), disabled: input.disabled } : null
183
+ },
184
+ /** The cost line's grade and words. */
185
+ budgetLine() {
186
+ const b = deep('button[budget]')[0]
187
+ return b ? { grade: b.getAttribute('grade'), text: b.textContent.replace(/\s+/g, ' ').trim() } : null
188
+ },
160
189
  /** Whether an input with this label is on the page. */
161
190
  hasInput(label) {
162
191
  return deep('input').some(i => i.getAttribute('aria-label') === label)
@@ -222,6 +251,16 @@ window.harness = {
222
251
  await session.open('probe')
223
252
  const editor = document.createElement('figure-v3-editor')
224
253
  editor.session = session
254
+ /* The cost line as the modeller page gives it: the maturity report of what is authored (figure-v3-verdict). */
255
+ const report = () => {
256
+ try {
257
+ return budgetOf(v3ReportOf(parseV3Asset(session.source())))
258
+ } catch {
259
+ return undefined
260
+ }
261
+ }
262
+ editor.budget = report()
263
+ editor.addEventListener('v3-changed', () => (editor.budget = report()))
225
264
  editor.templates = templates
226
265
  editor.guideOf = async name => guides[name] ?? ''
227
266
  editor.revision = 0
@@ -334,7 +334,7 @@ describe('the fastening form belongs to the part it is for', () => {
334
334
  return deep().find(s => s.previousElementSibling?.textContent?.trim() === label) ?? null
335
335
  }, label)
336
336
 
337
- test('a choice made for one part is not carried to the next part picked (셔터 커튼, 2026-09-25)', async () => {
337
+ test('a choice made for one part is not carried to the next part picked; the next opens on its own fastening (셔터 커튼 2026-09-25, walk 1 2026-09-26)', async () => {
338
338
  /* The placing across the target face is asked once there is a target: the form starts with none. */
339
339
  await click(await read(() => probe.row('forearm')))
340
340
  const target = await alignSelect('figure.label.target-part')
@@ -346,10 +346,15 @@ describe('the fastening form belongs to the part it is for', () => {
346
346
  assert.equal(await select.evaluate(s => s.value), 'max', 'chosen for the forearm')
347
347
 
348
348
  await click(await read(() => probe.row('elbow')))
349
+ /* The elbow opens on its own fastening (walk 1, 2026-09-26), not on the forearm's choices. */
350
+ const own = await read(() => {
351
+ const joint = JSON.parse(document.querySelector('figure-v3-editor').session.source()).joints.find(j => j.body1.part === 'elbow')
352
+ return { target: joint.body0.part, x: joint.origin?.align?.x ?? 'centre' }
353
+ })
349
354
  const nextTarget = await alignSelect('figure.label.target-part')
350
- assert.equal(await nextTarget.evaluate(s => s.value), '', 'the elbow starts with no target')
355
+ assert.equal(await nextTarget.evaluate(s => s.value), own.target, 'the elbow opens on what it is fastened to')
351
356
  const next = await alignSelect('figure.label.align-x')
352
- assert.ok(await next.evaluate(s => !s || s.value === 'centre'), 'nor with the forearm\'s edge placing')
357
+ assert.ok(await next.evaluate((s, x) => !s || s.value === (typeof x === 'string' ? x : 'mm') || s.value === 'centre', own.x), 'and on its own placing, not the forearm\'s edge')
353
358
  })
354
359
  })
355
360
 
@@ -754,10 +759,54 @@ describe('the recipe catalog: a template chosen, filled in, put onto the chosen
754
759
  })
755
760
  })
756
761
 
762
+ describe('a row\'s pitch made a board\'s to set (ADR-0096)', () => {
763
+ const typeInto = async (label, text) => {
764
+ const at = await read(l => probe.labelled(l), label)
765
+ assert.ok(at, `the field ${label}`)
766
+ await page().mouse.click(at.x, at.y, { clickCount: 3 })
767
+ await page().keyboard.type(text)
768
+ await page().keyboard.press('Tab')
769
+ await sleep(400)
770
+ }
771
+ test('published in one step with a name and a range; the pitch then sets the input; withdrawn, the pitch stays as it is', async () => {
772
+ await mount('rollers')
773
+ await click(await read(() => probe.row('roller')))
774
+ const before = (await read(() => probe.ids())).filter(id => id.includes('"roller.placed"')).length
775
+ await click(await read(() => probe.labelled('figure.label.board-sets')))
776
+ await typeInto('figure.label.board-name', '롤러 간격')
777
+ await typeInto('figure.label.least', '100')
778
+ await typeInto('figure.label.most', '400')
779
+ const start = await state()
780
+ await click(await read(() => probe.button('publish-pitch')))
781
+ assert.equal((await state()).revision, start.revision + 1, 'one step')
782
+ let source = await read(() => probe.source())
783
+ assert.deepEqual(source.publicInputs, [{ input: 'roller.pitch', label: '롤러 간격' }])
784
+ const input = source.document.model.inputs.find(i => i.id === 'roller.pitch')
785
+ assert.deepEqual([input.min, input.max, source.designInputs['roller.pitch']], [100, 400, 200])
786
+ assert.deepEqual(source.joints.find(j => j.body1.part === 'roller').repeat.pitch, { from: { of: 'input', input: 'roller.pitch' } })
787
+ assert.equal((await read(() => probe.ids())).filter(id => id.includes('"roller.placed"')).length, before, 'the row as it was')
788
+ await typeInto('roller.repeat.pitch', '400')
789
+ source = await read(() => probe.source())
790
+ assert.equal(source.designInputs['roller.pitch'], 400, 'the pitch field sets the input')
791
+ assert.ok((await read(() => probe.ids())).filter(id => id.includes('"roller.placed"')).length < before, 'fewer rollers at a wider pitch')
792
+ await click(await read(() => probe.labelled('figure.label.board-sets')))
793
+ source = await read(() => probe.source())
794
+ assert.equal(source.publicInputs, undefined, 'withdrawn')
795
+ assert.equal(source.joints.find(j => j.body1.part === 'roller').repeat.pitch, 400, 'the pitch stays as it is')
796
+ })
797
+ })
798
+
757
799
  describe('a row that carries a part (ADR-0093, 2026-09-26)', () => {
758
800
  test('the list has the roller and its mark, each once; a click on a mark on the canvas picks the mark, and only the roller offers the row', async () => {
759
801
  await mount('rollers')
760
802
  assert.ok(await read(() => probe.row('roller')), 'the roller, once')
803
+ /*
804
+ The grade counts what the renderer draws (ADR-0087 amended, 2026-09-26): ten turning rollers and their marks are
805
+ two batches and twenty moving bodies -- within the budgets -- and the line names it a performance cost.
806
+ */
807
+ const line = await read(() => probe.budgetLine())
808
+ assert.ok(line.text.includes('figure.label.performance-cost'), `the line says performance cost: ${line.text}`)
809
+ assert.notEqual(line.grade, 'D', `a turning roller row is not over its budgets: ${line.text}`)
761
810
  assert.ok(await read(() => probe.row('mark')), 'the mark it carries, once')
762
811
  const copy = (await read(() => probe.ids())).find(id => id.includes('mark.') && id.includes(',3,'))
763
812
  assert.ok(copy, 'a mark on the fourth roller is drawn')
@@ -767,6 +816,19 @@ describe('a row that carries a part (ADR-0093, 2026-09-26)', () => {
767
816
  await click(await read(() => probe.row('roller')))
768
817
  assert.equal((await state()).selected, 'roller')
769
818
  assert.ok(await read(() => probe.hasInput('roller.repeat.pitch')), 'the roller offers its row')
819
+ /* Walk 1 (2026-09-26): the fastening form opens on the roller's own fastening -- by its centre. */
820
+ assert.ok((await read(() => probe.selectsOffering('centre'))).includes('centre'), 'my face reads centre')
821
+ /* And the row's part keeps a name of its own to change. */
822
+ const name = await read(() => probe.nameField())
823
+ assert.equal(name.disabled, false, 'the roller can be renamed')
824
+ await page().mouse.click(name.x, name.y, { clickCount: 3 })
825
+ await page().keyboard.type('idler')
826
+ await page().keyboard.press('Tab')
827
+ await sleep(600)
828
+ assert.ok(await read(() => probe.row('idler')), 'renamed in the list')
829
+ const ids = await read(() => probe.ids())
830
+ assert.equal(ids.filter(id => id.includes('"idler.placed"')).length, 10, 'still a row of ten')
831
+ assert.equal(ids.filter(id => id.includes('"mark.placed"')).length, 10, 'each still carries its mark')
770
832
  })
771
833
  })
772
834
 
@@ -69,7 +69,8 @@ test('되풀이한 부품의 속성 이름 칸도 사람의 이름이고, 몇
69
69
  assert.deepEqual(repeatOf('["roller.repeat",2,"mark.placed"]', carried), { name: 'mark', count: 16, index: 3 })
70
70
  const editor = readFileSync(fileURLToPath(new URL('../client/modeller/v3-editor.ts', import.meta.url)), 'utf8')
71
71
  assert.ok(editor.includes('.value=${partLabelOf(this.selected)}'), '사용자 2026-09-24: 「부품 이름 ???」')
72
- assert.ok(editor.includes('?disabled=${unavailable || !!repeatOf(this.selected, view.geometry)}'), '복사본에는 바꿀 제 이름이 없다')
72
+ assert.ok(editor.includes('?disabled=${unavailable || !!repeatOf(this.selected, view.geometry)?.index}'), '복사본에는 바꿀 제 이름이 없고, 줄의 부품 자신은 이름을 바꿀 수 있다(walk 1, 2026-09-26)')
73
+ assert.equal(repeatOf('roller', geometry)?.index, 0, '줄의 부품 자신은 몇 번째가 아니다')
73
74
  })
74
75
 
75
76
  test('편집기는 저작한 부품 단위로 고르고 보인다 — 되풀이는 한 부품, 캔버스에서는 복사본 모두가 골라진다', () => {
@@ -630,3 +630,28 @@ test('팔레트의 통은 통의 크기로 서고, 세 축 모두 크기를 따
630
630
  const ko = JSON.parse(read('../translations/ko.json'))
631
631
  assert.equal(ko['figure.label.face-inside-bottom'], '안쪽 바닥')
632
632
  })
633
+
634
+ /*
635
+ * The fastening form opens on the part's own fastening (walk 1, 2026-09-26): a roller fastened by its centre was shown
636
+ * as fastened by its bottom, and fastening again from there broke its turning in place.
637
+ */
638
+ import { fasteningOf } from '../client/modeller/v3-links.ts'
639
+
640
+ test('붙이기 칸은 부품 제 붙임으로 열린다 — 가운데로 붙은 롤러는 가운데 · 상대 · 간격 그대로', () => {
641
+ let asset = addV3Box(createV3Asset('Rollers'), { id: 'bed', size: { x: 2000, y: 400, z: 600 }, position: { x: 0, y: 200, z: 0 } })
642
+ asset = applyV3Authoring(asset, { kind: 'attach', part: 'bed', face: 'bottom', to: { plane: 'mounting-plane' } })
643
+ asset = addV3Primitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 40, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
644
+ asset = applyV3Authoring(asset, { kind: 'attach', part: 'roller', face: 'centre', to: { part: 'bed', face: 'top' }, gap: 40, align: { x: 'min' } })
645
+ const roller = fasteningOf(asset, 'roller')!
646
+ assert.equal(roller.face, 'centre')
647
+ assert.equal(roller.target, 'bed')
648
+ assert.equal(roller.targetFace, 'top')
649
+ assert.equal(roller.gap, 40)
650
+ assert.equal(roller.align.x, 'min', '기록의 정렬')
651
+ const bed = fasteningOf(asset, 'bed')!
652
+ assert.deepEqual([bed.face, bed.toPlane], ['bottom', true])
653
+ assert.equal(fasteningOf(addV3Box(createV3Asset('Loose'), { id: 'b', size: { x: 1, y: 1, z: 1 }, position: { x: 0, y: 0, z: 0 } }), 'b'), null, '안 붙은 부품은 기본값으로')
654
+ const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-v3-links.ts', import.meta.url)), 'utf8')
655
+ assert.ok(panel.includes('fasteningOf(this.asset, this.partId)'), '부품을 고르면 제 붙임으로 연다')
656
+ assert.ok(panel.includes("...(f.toPlane ? [] : (['centre'] as const))"), '내 면에 가운데가 있다')
657
+ })
@@ -93,3 +93,16 @@ test('속성 탭의 말은 다섯 말 모두에 있다', () => {
93
93
  for (const key of keys) assert.ok(table[key], `${lang}: ${key}`)
94
94
  }
95
95
  })
96
+
97
+ /* Walk 1 (2026-09-26): the mounting face offered is where the figure meets its plane -- a conveyor's legs, not its bed. */
98
+ test('장착면은 도형이 평면에 닿는 부품으로 권한다 — 다리 위 바닥이 아니라 다리', async () => {
99
+ const fm = await import('@hatiolab/figure-model')
100
+ let asset = fm.addV3Box(fm.createV3Asset('Conveyor'), { id: 'bed', size: { x: 3000, y: 700, z: 800 }, position: { x: 0, y: 1050, z: 0 } })
101
+ asset = fm.addV3Box(asset, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: -1400, y: 350, z: -300 } })
102
+ asset = fm.applyV3Authoring(asset, { kind: 'attach', part: 'leg', face: 'top', to: { part: 'bed', face: 'bottom' }, align: { x: 'min', z: 'min' } })
103
+ const facts = v3FigureFacts(fm.serializeV3Asset(asset))
104
+ assert.deepEqual(facts.suggested, { part: 'leg', face: 'bottom' })
105
+ assert.equal(facts.parts[0], 'bed', '목록의 첫 부품은 바닥이다')
106
+ const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-v3-properties.ts', import.meta.url)), 'utf8')
107
+ assert.ok(panel.includes('[this.pickedPart, mounting?.part, facts.suggested?.part, facts.parts[0]]'), '고른 것 · 정한 것 다음이 권하는 것')
108
+ })
@@ -80,3 +80,10 @@ test('넣기는 카탈로그에서 온다 — 편집기는 카탈로그 요소
80
80
  const catalog = editor.slice(editor.indexOf('<figure-v3-recipe-catalog'), editor.indexOf('</figure-v3-recipe-catalog>'))
81
81
  assert.ok(catalog.includes('@insert-recipe=${(e: CustomEvent) => this.insertRecipe(e.detail.recipe, e.detail.values)}'))
82
82
  })
83
+
84
+ test('검색은 제목이 찾는 말인 것부터 — 「다리」에 다리 넷이 사다리보다 먼저 (walk 1, 2026-09-26)', () => {
85
+ const t = (name: string, title: string, description = '') => ({ name, category: 'support', title, description, tags: [] as string[] })
86
+ const list = [t('ladder', '사다리', '벽에 세우는 사다리'), t('platform', '발판', '다리 위 발판'), t('legs-four', '다리 넷', '상판 아래 다리')]
87
+ assert.deepEqual(recipesMatching(list, '', '다리').map(x => x.name), ['legs-four', 'ladder', 'platform'], '제목이 그 말로 시작 → 제목에 든 것 → 설명에만 있는 것')
88
+ assert.deepEqual(recipesMatching(list, '', '').map(x => x.name), ['ladder', 'platform', 'legs-four'], '찾는 말이 없으면 원래 순서')
89
+ })
@@ -359,7 +359,7 @@
359
359
  "figure.text.floor-explained": "overlaps the coordinate ground, so it is usually off",
360
360
  "figure.text.follow-camera-explained": "turn off to fix the shadow direction",
361
361
  "figure.text.grade-decided-by": "what decided the grade",
362
- "figure.text.grade-explained": "the grade is the headroom against placing {target} of this figure on one screen. the item closest to its limit decides it.",
362
+ "figure.text.grade-explained": "the performance cost grade is the headroom against placing {target} of this figure on one screen. the item closest to its limit decides it. it does not judge what the figure does or how good it is.",
363
363
  "figure.text.grade-scale-lead": "grade scale (limit used)",
364
364
  "figure.text.grade-target": "target: {target} on one screen — up to {groups} groups · {triangles} triangles each",
365
365
  "figure.text.parts-budget": "{triangles} triangles · {groups} groups · {instances} on one screen",
@@ -646,5 +646,14 @@
646
646
  "figure.button.remove-and-refit-room": "delete {part} and fit the space to the parts left",
647
647
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
648
648
  "figure.label.placement-wall": "wall (wall)",
649
- "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."
649
+ "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.",
650
+ "figure.label.face-centre": "centre",
651
+ "figure.label.budget-moving": "parts recomputed every frame",
652
+ "figure.label.performance-cost": "performance cost",
653
+ "figure.label.board-sets": "a board may change it",
654
+ "figure.label.board-name": "name on the board",
655
+ "figure.label.least": "least",
656
+ "figure.label.most": "most",
657
+ "figure.button.publish-input": "publish to boards",
658
+ "figure.button.apply": "apply"
650
659
  }
@@ -646,5 +646,14 @@
646
646
  "figure.button.remove-and-refit-room": "「{part}」を削除し、空間を残りの部品に合わせる",
647
647
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
648
648
  "figure.label.placement-wall": "壁基準 (wall)",
649
- "figure.text.placement-plane-refused": "{parts} は取付平面に付いている面では {placement} に付けられません。外すか {faces} 面で付け直してから変更してください。"
649
+ "figure.text.placement-plane-refused": "{parts} は取付平面に付いている面では {placement} に付けられません。外すか {faces} 面で付け直してから変更してください。",
650
+ "figure.label.face-centre": "中心",
651
+ "figure.label.budget-moving": "毎フレーム再計算する部品",
652
+ "figure.label.performance-cost": "性能コスト",
653
+ "figure.label.board-sets": "ボードで変更可能",
654
+ "figure.label.board-name": "ボードでの名前",
655
+ "figure.label.least": "最小",
656
+ "figure.label.most": "最大",
657
+ "figure.button.publish-input": "ボードに公開",
658
+ "figure.button.apply": "適用"
650
659
  }
@@ -359,7 +359,7 @@
359
359
  "figure.text.floor-explained": "좌표 바탕과 겹쳐 보통 끕니다",
360
360
  "figure.text.follow-camera-explained": "끄면 그림자 방향이 고정됩니다",
361
361
  "figure.text.grade-decided-by": "등급 결정에 영향을 준 항목",
362
- "figure.text.grade-explained": "등급은 한 화면에 이 도형 {target} 대를 놓는 목표에 견준 여유입니다. 한도에 가장 가까운 항목 하나가 정합니다.",
362
+ "figure.text.grade-explained": "성능 비용 등급은 한 화면에 이 도형 {target} 대를 놓는 목표에 견준 여유입니다. 한도에 가장 가까운 항목 하나가 정합니다. 도형의 기능이나 품질에 대한 평가가 아닙니다.",
363
363
  "figure.text.grade-scale-lead": "등급 눈금 (한도 사용률)",
364
364
  "figure.text.grade-target": "한 화면 {target} 대 목표 — 한 대에 묶음 {groups} · 삼각형 {triangles} 까지",
365
365
  "figure.text.parts-budget": "삼각형 {triangles} · 묶음 {groups} · 한 화면 {instances} 대",
@@ -646,5 +646,14 @@
646
646
  "figure.button.remove-and-refit-room": "'{part}' 지우고 공간을 남은 부품에 맞추기",
647
647
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
648
648
  "figure.label.placement-wall": "벽 기반 (wall)",
649
- "figure.text.placement-plane-refused": "{parts} 가 장착 평면에 붙은 면으로는 {placement} 에 붙일 수 없습니다. 떼거나 {faces} 면으로 다시 붙인 뒤 바꾸세요."
649
+ "figure.text.placement-plane-refused": "{parts} 가 장착 평면에 붙은 면으로는 {placement} 에 붙일 수 없습니다. 떼거나 {faces} 면으로 다시 붙인 뒤 바꾸세요.",
650
+ "figure.label.face-centre": "가운데",
651
+ "figure.label.budget-moving": "매 프레임 다시 계산하는 부품",
652
+ "figure.label.performance-cost": "성능 비용",
653
+ "figure.label.board-sets": "보드에서 바꿀 수 있음",
654
+ "figure.label.board-name": "보드에 보이는 이름",
655
+ "figure.label.least": "최소",
656
+ "figure.label.most": "최대",
657
+ "figure.button.publish-input": "보드에 공개",
658
+ "figure.button.apply": "적용"
650
659
  }
@@ -646,5 +646,14 @@
646
646
  "figure.button.remove-and-refit-room": "padam '{part}' dan muatkan ruang pada bahagian yang tinggal",
647
647
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
648
648
  "figure.label.placement-wall": "dinding (wall)",
649
- "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."
649
+ "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.",
650
+ "figure.label.face-centre": "tengah",
651
+ "figure.label.budget-moving": "bahagian dikira semula setiap bingkai",
652
+ "figure.label.performance-cost": "kos prestasi",
653
+ "figure.label.board-sets": "papan boleh mengubahnya",
654
+ "figure.label.board-name": "nama pada papan",
655
+ "figure.label.least": "terkecil",
656
+ "figure.label.most": "terbesar",
657
+ "figure.button.publish-input": "terbitkan ke papan",
658
+ "figure.button.apply": "guna"
650
659
  }
@@ -646,5 +646,14 @@
646
646
  "figure.button.remove-and-refit-room": "删除 '{part}' 并使空间适配剩余部件",
647
647
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
648
648
  "figure.label.placement-wall": "墙面基准 (wall)",
649
- "figure.text.placement-plane-refused": "{parts} 贴在安装平面上的面不能用于 {placement}。请先拆下或改用 {faces} 面贴合后再更改。"
649
+ "figure.text.placement-plane-refused": "{parts} 贴在安装平面上的面不能用于 {placement}。请先拆下或改用 {faces} 面贴合后再更改。",
650
+ "figure.label.face-centre": "中心",
651
+ "figure.label.budget-moving": "每帧重新计算的部件",
652
+ "figure.label.performance-cost": "性能成本",
653
+ "figure.label.board-sets": "可在看板中修改",
654
+ "figure.label.board-name": "看板中显示的名称",
655
+ "figure.label.least": "最小",
656
+ "figure.label.most": "最大",
657
+ "figure.button.publish-input": "公开到看板",
658
+ "figure.button.apply": "应用"
650
659
  }