@things-factory/figure-ui 10.1.38 → 10.1.40

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 (50) hide show
  1. package/client/graphql/index.ts +53 -0
  2. package/client/modeller/figure-history-panel.ts +11 -85
  3. package/client/modeller/figure-v3-properties.ts +254 -0
  4. package/client/modeller/figure-v3-verdict.ts +247 -0
  5. package/client/modeller/gate-findings.ts +105 -0
  6. package/client/modeller/v3-canvas.ts +27 -3
  7. package/client/modeller/v3-editor.ts +39 -11
  8. package/client/modeller/v3-figure-facts.ts +53 -0
  9. package/client/modeller/v3-occupancy.ts +6 -2
  10. package/client/pages/figure-v3-modeller-page.ts +250 -17
  11. package/dist-client/graphql/index.d.ts +8 -0
  12. package/dist-client/graphql/index.js +51 -0
  13. package/dist-client/graphql/index.js.map +1 -1
  14. package/dist-client/modeller/figure-history-panel.d.ts +6 -0
  15. package/dist-client/modeller/figure-history-panel.js +9 -85
  16. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  17. package/dist-client/modeller/figure-v3-properties.d.ts +46 -0
  18. package/dist-client/modeller/figure-v3-properties.js +259 -0
  19. package/dist-client/modeller/figure-v3-properties.js.map +1 -0
  20. package/dist-client/modeller/figure-v3-verdict.d.ts +50 -0
  21. package/dist-client/modeller/figure-v3-verdict.js +268 -0
  22. package/dist-client/modeller/figure-v3-verdict.js.map +1 -0
  23. package/dist-client/modeller/gate-findings.d.ts +12 -0
  24. package/dist-client/modeller/gate-findings.js +102 -0
  25. package/dist-client/modeller/gate-findings.js.map +1 -0
  26. package/dist-client/modeller/v3-canvas.d.ts +6 -0
  27. package/dist-client/modeller/v3-canvas.js +27 -3
  28. package/dist-client/modeller/v3-canvas.js.map +1 -1
  29. package/dist-client/modeller/v3-editor.js +39 -11
  30. package/dist-client/modeller/v3-editor.js.map +1 -1
  31. package/dist-client/modeller/v3-figure-facts.d.ts +26 -0
  32. package/dist-client/modeller/v3-figure-facts.js +34 -0
  33. package/dist-client/modeller/v3-figure-facts.js.map +1 -0
  34. package/dist-client/modeller/v3-occupancy.d.ts +5 -1
  35. package/dist-client/modeller/v3-occupancy.js +4 -2
  36. package/dist-client/modeller/v3-occupancy.js.map +1 -1
  37. package/dist-client/pages/figure-v3-modeller-page.d.ts +40 -0
  38. package/dist-client/pages/figure-v3-modeller-page.js +264 -18
  39. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  40. package/dist-client/route.d.ts +1 -1
  41. package/dist-client/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +4 -4
  43. package/test/v3-properties.test.ts +93 -0
  44. package/test/v3-theme.test.ts +67 -0
  45. package/test/v3-verdict.test.ts +82 -0
  46. package/translations/en.json +12 -0
  47. package/translations/ja.json +12 -0
  48. package/translations/ko.json +12 -0
  49. package/translations/ms.json +12 -0
  50. package/translations/zh.json +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.38",
3
+ "version": "10.1.40",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -26,7 +26,7 @@
26
26
  "clean": "npm run clean:server && npm run clean:client"
27
27
  },
28
28
  "dependencies": {
29
- "@hatiolab/figure-model": "^0.1.38",
29
+ "@hatiolab/figure-model": "^0.1.40",
30
30
  "@hatiolab/things-scene": "^10.1.26",
31
31
  "@material/web": "^2.0.0",
32
32
  "@operato/data-grist": "^10.0.0",
@@ -37,8 +37,8 @@
37
37
  "@operato/shell": "^10.0.0",
38
38
  "@operato/styles": "^10.0.0",
39
39
  "@operato/utils": "^10.0.0",
40
- "@things-factory/figure-service": "^10.1.38",
40
+ "@things-factory/figure-service": "^10.1.40",
41
41
  "three": "^0.185.1"
42
42
  },
43
- "gitHead": "b947349fe1c27d11038b592ceadaba336f14785b"
43
+ "gitHead": "51c5d804c14ad8fcc6d4707e6ef80c9deede2766"
44
44
  }
@@ -0,0 +1,93 @@
1
+ /*
2
+ * **V3 화면에서 도형의 속성을 고친다** (V3 동등표 ②, 2026-09-24).
3
+ *
4
+ * V2 인스펙터는 이름 · 배치 기준 · 차지하는 공간 · 분류 · 태그를 맨 위에 둔다. V3 화면에는 타입 칸 하나뿐이었다.
5
+ * 두 축을 본다: 탭이 내는 명령이 figure-model 에서 **정말로** 그 일을 하는가, 그리고 화면이 명령과 행 쓰기를 잇는가.
6
+ */
7
+ import { test } from 'node:test'
8
+ import assert from 'node:assert/strict'
9
+ import { readFileSync } from 'node:fs'
10
+ import { fileURLToPath } from 'node:url'
11
+
12
+ import { addV3Box, applyV3Authoring, createV3Asset, inspectV3Asset, parseV3Asset, serializeV3Asset } from '@hatiolab/figure-model'
13
+
14
+ import { tagsOf, v3FigureFacts } from '../client/modeller/v3-figure-facts.ts'
15
+ import { occupancyFromParts } from '../client/modeller/v3-occupancy.ts'
16
+
17
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
18
+ const probe = () => {
19
+ let asset = createV3Asset('Probe')
20
+ asset = addV3Box(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
21
+ return asset
22
+ }
23
+ const facts = (asset: ReturnType<typeof probe>) => v3FigureFacts(serializeV3Asset(asset))
24
+
25
+ test('탭이 읽는 사실 — 선언 전에는 배치 기준도 장착면도 「없음」이다', () => {
26
+ const read = facts(probe())
27
+ assert.equal(read.name, 'Probe')
28
+ assert.deepEqual(read.parts, ['body'])
29
+ assert.equal(read.declared, false)
30
+ assert.equal(read.placement, undefined, '없는 것을 바닥으로 읽지 않는다')
31
+ assert.equal(read.mounting, undefined)
32
+ })
33
+
34
+ test('배치 기준을 고르면 그 기준으로 공간을 선언한다 — 판정도 풀린다', () => {
35
+ const source = serializeV3Asset(probe())
36
+ const declared = applyV3Authoring(parseV3Asset(source), occupancyFromParts(source, 'ceiling'))
37
+
38
+ assert.equal(facts(declared).placement, 'ceiling')
39
+ assert.equal(facts(declared).declared, true)
40
+ assert.ok(!inspectV3Asset(declared).violations.some(v => v.code === 'OCCUPANCY_UNDECLARED'))
41
+ })
42
+
43
+ test('장착면을 정하면 탭이 그 부품과 면을 다시 읽고, 해제하면 없어진다', () => {
44
+ const source = serializeV3Asset(probe())
45
+ const declared = applyV3Authoring(parseV3Asset(source), occupancyFromParts(source))
46
+ const mounted = applyV3Authoring(declared, { kind: 'set-mounting-face', part: 'body', face: 'bottom' })
47
+
48
+ assert.deepEqual(facts(mounted).mounting, { part: 'body', face: 'bottom' })
49
+ assert.equal(facts(applyV3Authoring(mounted, { kind: 'clear-mounting-face' })).mounting, undefined)
50
+ })
51
+
52
+ test('이름을 바꾸면 자산의 이름이 바뀐다 — 저장하면 목록 이름이 이것을 따른다', () => {
53
+ assert.equal(facts(applyV3Authoring(probe(), { kind: 'rename', name: 'Roller conveyor' })).name, 'Roller conveyor')
54
+ })
55
+
56
+ test('태그 줄은 V2 와 같은 규칙으로 목록이 된다', () => {
57
+ assert.deepEqual(tagsOf('#반출, 컨베이어,, 반출 '), ['반출', '컨베이어'])
58
+ assert.deepEqual(tagsOf(''), [], '빈 줄은 전부 지우는 것이다')
59
+ })
60
+
61
+ const page = read('../client/pages/figure-v3-modeller-page.ts')
62
+
63
+ test('화면이 속성 탭을 걸고, 명령은 세션의 이력 하나로, 분류 · 태그는 행으로 간다', () => {
64
+ assert.ok(/renderTab\('properties', 'tune'/.test(page), '속성 탭')
65
+ assert.ok(/<figure-v3-properties\s+\?off=/.test(page), '감추되 떼지 않는다')
66
+ assert.ok(/@v3-command=\$\{this\.onCommand\}/.test(page) && /@v3-catalog=\$\{this\.onCatalog\}/.test(page))
67
+ const run = page.slice(page.indexOf('private runCommand'), page.indexOf('private onCatalog'))
68
+ assert.ok(/this\.session\.command\(actionOf\(\)\)/.test(run), '명령은 세션을 지난다 — 되돌리기가 먹는다')
69
+ const catalog = page.slice(page.indexOf('private onCatalog'), page.indexOf('private released'))
70
+ assert.ok(/updateFigure\(id, patch\)/.test(catalog), '분류 · 태그는 행에 쓴다')
71
+ })
72
+
73
+ test('이름을 바꾸면 제목 줄도 바뀐다 — 셸은 알려 줄 때만 제목을 다시 읽는다', () => {
74
+ const changed = page.slice(page.indexOf('private changed = () =>'), page.indexOf('private async readRow'))
75
+ assert.ok(/view\.name !== this\.titled[\s\S]*this\.updateContext\(\)/.test(changed))
76
+ })
77
+
78
+ test('발행한 뒤에도 행의 타입이 남는다 — 새로 짜면 잠긴 타입 칸이 빈다', () => {
79
+ const released = page.slice(page.indexOf('private released'), page.indexOf('private reverted'))
80
+ assert.ok(/this\.row = \{ \.\.\.this\.row, state:/.test(released))
81
+ })
82
+
83
+ test('속성 탭의 말은 다섯 말 모두에 있다', () => {
84
+ const tab = read('../client/modeller/figure-v3-properties.ts')
85
+ assert.ok(tab.includes('occupancyFromParts(this.source, placement)'), '고른 배치 기준으로 선언한다')
86
+ const keys = [...tab.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
87
+ for (const face of ['left', 'right', 'bottom', 'top', 'back', 'front']) keys.push(`figure.label.face-${face}`)
88
+ for (const one of ['floor', 'ceiling', 'center']) keys.push(`figure.label.placement-${one}`)
89
+ for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
90
+ const table = JSON.parse(read(`../translations/${lang}.json`))
91
+ for (const key of keys) assert.ok(table[key], `${lang}: ${key}`)
92
+ }
93
+ })
@@ -0,0 +1,67 @@
1
+ /*
2
+ * **V3 모델러는 테마를 따른다** (V3 동등표 ⑦, 2026-09-24).
3
+ *
4
+ * V3 캔버스와 편집기는 색을 박아 두었다. 어두운 테마에서 가운데만 밝은 판이 떴고, 경계선과 눌린 단추 · 오류 칸도
5
+ * 밝은 색 그대로였다. 색은 셸의 토큰에서 온다(UX 결정 — 바탕은 surface-container, 눌림은 secondary-container,
6
+ * 오류는 error-container).
7
+ */
8
+ import { test } from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+ import { readFileSync } from 'node:fs'
11
+ import { fileURLToPath } from 'node:url'
12
+
13
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
14
+ const canvas = read('../client/modeller/v3-canvas.ts')
15
+ const editor = read('../client/modeller/v3-editor.ts')
16
+
17
+ /** Hex colours that are not a `var(--token, #fallback)` fallback. */
18
+ const fixed = (text: string) =>
19
+ text
20
+ .replace(/var\(--[\w-]+,\s*#[0-9a-fA-F]{3,8}\)/g, '')
21
+ .match(/#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b(?![\w-])/g) ?? []
22
+
23
+ test('캔버스와 편집기에 박힌 색이 없다 — 토큰의 대체값만 남는다', () => {
24
+ assert.deepEqual(fixed(canvas), [], 'v3-canvas.ts')
25
+ assert.deepEqual(fixed(editor), [], 'v3-editor.ts')
26
+ })
27
+
28
+ test('캔버스 바탕은 CSS 와 3D 가 같은 토큰에서 온다', () => {
29
+ assert.match(canvas, /background: var\(--md-sys-color-surface-container\)/)
30
+ assert.match(canvas, /this\.scene\.background = new THREE\.Color\(colour\)/)
31
+ assert.match(canvas, /getComputedStyle\(this\)\.backgroundColor/, '3D 바탕이 호스트의 바탕을 읽는다')
32
+ })
33
+
34
+ test('테마를 바꾸면 3D 바탕을 다시 읽는다 — 한 번만 읽으면 가운데만 밝은 판이 남는다', () => {
35
+ assert.match(canvas, /observe\(document\.body, \{ attributes: true, attributeFilter: \['class'\] \}\)/)
36
+ const onChange = canvas.slice(canvas.indexOf('new MutationObserver'), canvas.indexOf('this.themeObserver.observe'))
37
+ assert.match(onChange, /this\.paintBackground\(\)/)
38
+ assert.match(onChange, /this\.draw\(\)/, '다시 읽고 그리지 않으면 다음 움직임까지 옛 색이다')
39
+ const leave = canvas.slice(canvas.indexOf('disconnectedCallback()'))
40
+ assert.match(leave, /this\.themeObserver\?\.disconnect\(\)/, '떠난 캔버스가 body 를 계속 지켜본다')
41
+ })
42
+
43
+ test('눌린 단추와 오류 칸은 셸이 두 테마 대비를 맞춘 칸을 쓴다', () => {
44
+ const pressed = editor.slice(editor.indexOf("button[aria-pressed='true']"), editor.indexOf('}', editor.indexOf("button[aria-pressed='true']")))
45
+ assert.match(pressed, /--md-sys-color-secondary-container\)/)
46
+ assert.match(pressed, /--md-sys-color-on-secondary-container\)/)
47
+ assert.match(pressed, /border-color: var\(--md-sys-color-primary\)/)
48
+ const error = editor.slice(editor.indexOf('.error {'), editor.indexOf('}', editor.indexOf('.error {')))
49
+ assert.match(error, /--md-sys-color-error-container\)/)
50
+ assert.match(error, /--md-sys-color-on-error-container\)/)
51
+ })
52
+
53
+ test('편집기는 창이 아니라 자기 폭으로 접는다 — 레일과 오른쪽 칸이 폭을 먼저 가져간다', () => {
54
+ assert.match(editor, /container-type: inline-size/)
55
+ assert.doesNotMatch(editor, /@media \(max-width/, '창 폭 기준이 남아 있다 — 레일을 펼치면 편집기가 오른쪽 칸 위로 넘친다')
56
+ assert.match(editor, /@container \(max-width: 659px\)/, '세 칸의 최소 폭 합(660) 아래에서 두 칸')
57
+ const one = editor.slice(editor.indexOf('@container (max-width: 419px)'))
58
+ assert.match(one, /grid-template-columns: minmax\(0, 1fr\)/, '420 아래에서 한 줄')
59
+ assert.match(one, /figure-v3-canvas \{\s*order: -1;/, '한 줄에서는 캔버스가 먼저')
60
+ })
61
+
62
+ test('넘침이 없으니 오른쪽 칸은 편집기를 가리지 않는다 — 가림은 우회였다', () => {
63
+ const page = read('../client/pages/figure-v3-modeller-page.ts')
64
+ const aside = page.slice(page.indexOf(' aside {'), page.indexOf('}', page.indexOf(' aside {')))
65
+ assert.doesNotMatch(aside, /z-index/)
66
+ assert.match(aside, /border-left: 1px solid var\(--md-sys-color-outline-variant\)/, '칸 경계선은 남는다')
67
+ })
@@ -0,0 +1,82 @@
1
+ /*
2
+ * **V3 화면이 편집하는 동안 판정한다** (V3 동등표 ①, 2026-09-24).
3
+ *
4
+ * V2 모델러는 고치는 동안 「한 화면에 몇 개」와 「무엇이 발행을 막나」를 보여 준다. V3 화면은 둘 다 없어서 막힌
5
+ * 까닭을 저장한 뒤 발행 패널에서야 알았다. 판정 탭이 편집 중인 원본으로 둘을 답한다.
6
+ *
7
+ * 세 축을 본다: 개수가 목록 카드와 같은 셈인가, 판정이 발행 패널과 같은 문 · 같은 문장인가, 화면이 탭과 조치를
8
+ * 실제로 잇는가.
9
+ */
10
+ import { test } from 'node:test'
11
+ import assert from 'node:assert/strict'
12
+ import { readFileSync } from 'node:fs'
13
+ import { fileURLToPath } from 'node:url'
14
+
15
+ import { addV3Box, createV3Asset, v3CostOf } from '@hatiolab/figure-model'
16
+
17
+ import { capacityFrom, SCENE_BUDGET } from '../client/modeller/scene-budget.ts'
18
+
19
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
20
+ const verdict = read('../client/modeller/figure-v3-verdict.ts')
21
+ const page = read('../client/pages/figure-v3-modeller-page.ts')
22
+ const graphql = read('../client/graphql/index.ts')
23
+
24
+ test('개수는 목록 카드와 같은 셈이다 — V3 원가를 같은 capacityFrom 에 넣는다', () => {
25
+ let asset = createV3Asset('Probe')
26
+ asset = addV3Box(asset, { id: 'a', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
27
+ asset = addV3Box(asset, { id: 'b', size: { x: 100, y: 100, z: 100 }, position: { x: 200, y: 50, z: 0 } })
28
+ const cost = v3CostOf(asset)
29
+
30
+ assert.equal(cost.groups, 2, '전제: 그려지는 부품 둘이 묶음 둘')
31
+ assert.equal(capacityFrom(cost.groups, cost.triangles)?.instances, Math.floor(SCENE_BUDGET.groupsOnScreen / 2))
32
+ assert.ok(/capacityFrom\(cost\.groups, cost\.triangles\)/.test(verdict), '판정 탭이 카드와 같은 함수로 센다')
33
+ assert.ok(!/grade/.test(verdict.replace(/\/\*[\s\S]*?\*\//g, '')), 'V3 에는 잰 것이 없는 등급을 그리지 않는다')
34
+ })
35
+
36
+ test('판정은 발행 패널과 같은 서버 문에서 온다 — 화면이 따로 판정하지 않는다', () => {
37
+ assert.ok(verdict.includes('inspectFigureDraft(source, this.figureId)'), '편집 중인 원본을 보낸다')
38
+ assert.ok(!/inspectV3Asset/.test(verdict), '화면이 제 판정을 가지면 발행 패널과 다른 말을 한다')
39
+
40
+ const fields = (name: string) => {
41
+ const start = graphql.indexOf(`${name}(`, graphql.indexOf(`query ${name[0].toUpperCase()}${name.slice(1)}`))
42
+ return graphql.slice(start, graphql.indexOf('`', start)).replace(/\s+/g, ' ').replace(/^[^{]*/, '')
43
+ }
44
+ assert.equal(fields('inspectFigureDraft'), fields('inspectFigure'), '두 질의가 같은 칸을 받는다 — 한쪽만 why·how 를 잃지 않게')
45
+ })
46
+
47
+ test('늦게 온 옛 답이 새 답을 덮지 않는다', () => {
48
+ const guards = verdict.match(/question !== this\.asked/g) ?? []
49
+ assert.ok(guards.length >= 2, '성공과 실패 둘 다 가장 새 물음의 답인지 본다')
50
+ })
51
+
52
+ test('판정 탭은 발행 패널과 같은 두 판정에 「부품에 맞추기」를 낸다', () => {
53
+ const panel = read('../client/modeller/figure-history-panel.ts')
54
+ const codes = (text: string) => [...text.matchAll(/'(part-outside-occupancy|occupancy-undeclared)'/g)].map(m => m[1]).sort()
55
+ assert.deepEqual(codes(verdict), codes(panel.slice(panel.indexOf('private offersFit'))))
56
+ assert.ok(/<figure-v3-verdict[\s\S]*?@fit-base-to-parts=\$\{this\.fitToParts\}/.test(page), '누르면 공간 선언으로 이어진다')
57
+ })
58
+
59
+ test('화면이 탭 둘을 걸고, 감추되 떼지 않는다', () => {
60
+ assert.ok(/<figure-v3-verdict[\s\S]*?\.source=\$\{this\.draftSource\(\)\}/.test(page), '편집 중인 원본을 넘긴다')
61
+ assert.ok(/<figure-v3-verdict[\s\S]*?\.figureType=/.test(page), '타입을 바꾸면 다시 판정한다')
62
+ assert.ok(/<figure-v3-verdict\s+\?off=/.test(page) && /<figure-history-panel\s+\?off=/.test(page), '고르지 않은 탭은 감춘다')
63
+ assert.ok(/@capacity-changed=/.test(page) && /this\.instances\)/.test(page), '개수가 탭 이름표에 남는다')
64
+ })
65
+
66
+ test('판정 탭의 말은 다섯 말 모두에 있다 — 없으면 키가 그대로 찍힌다', () => {
67
+ const keys = [...verdict.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
68
+ assert.ok(keys.includes('figure.text.verdict-asking'), '전제: 삼항 안의 키도 센다')
69
+ for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
70
+ const table = JSON.parse(read(`../translations/${lang}.json`))
71
+ for (const key of keys) assert.ok(table[key], `${lang}: ${key}`)
72
+ }
73
+ })
74
+
75
+ test('부품이 없는 도형에는 「부품에 맞추기」를 내지 않고, 실패해도 편집기를 지우지 않는다', () => {
76
+ /* 새 도형은 부품 없이도 「공간 선언 없음」으로 막힌다. 그때 맞추기는 OCCUPANCY_NO_PARTS 로 던진다. */
77
+ assert.ok(/if \(!this\.bodies\) return nothing/.test(verdict), '잴 부품이 없으면 단추를 내지 않는다')
78
+ assert.ok(/bodies = cost \? cost\.groups \+ cost\.hidden : 0/.test(verdict), '감춘 부품도 잴 수 있는 부품이다')
79
+ const fit = page.slice(page.indexOf('private fitToParts'), page.indexOf('private released'))
80
+ assert.ok(!/this\.error\s*=/.test(fit), '맞추기 실패가 화면 전체를 오류 한 줄로 바꾸지 않는다')
81
+ assert.ok(fit.includes("i18next.t('figure.text.fit-needs-parts')"), '부품이 없다는 것을 사람의 말로 한다')
82
+ })
@@ -36,6 +36,12 @@
36
36
  "figure.button.settle-on-floor": "settle on the floor",
37
37
  "figure.button.snap": "snap",
38
38
  "figure.button.take-n-changes": "apply {n}",
39
+ "figure.label.declared-space": "declared space",
40
+ "figure.text.catalog-needs-a-save": "can be set once the figure is saved.",
41
+ "figure.text.declared-space-declared": "declared from the parts.",
42
+ "figure.text.declared-space-none": "not declared yet.",
43
+ "figure.text.mounting-face-needs-space": "declare the space first to choose one.",
44
+ "figure.text.placement-redeclares": "changing it declares the space again from the parts as they are now.",
39
45
  "figure.text.recommended-fix-explained": "{n} changes rechecked against every resize case. They apply to this draft only; review and save when ready.",
40
46
  "figure.text.show-affected-parts": "show affected parts",
41
47
  "figure.button.to-floor": "to floor",
@@ -228,6 +234,7 @@
228
234
  "figure.label.recipe-lift": "a lift changes height",
229
235
  "figure.label.recipe-pusher": "a pusher moves into place",
230
236
  "figure.label.release": "release",
237
+ "figure.label.release-verdict": "release check",
231
238
  "figure.label.released-versions": "released versions",
232
239
  "figure.label.repeat-axis": "axis it repeats along",
233
240
  "figure.label.repeat-count": "count",
@@ -289,6 +296,11 @@
289
296
  "figure.text.budget-measured-on": "Measured on {gpu}, {on}. A figure standing still costs nothing — only what is on screen and redrawn counts.",
290
297
  "figure.text.budget-not-measured-yet": "this target has not been measured yet.",
291
298
  "figure.text.cannot-release-now": "cannot be released yet",
299
+ "figure.text.fit-needs-parts": "there are no parts to fit to — add a shape first.",
300
+ "figure.text.nothing-blocks-release": "nothing in the figure as it stands blocks releasing it.",
301
+ "figure.text.verdict-asking": "checking…",
302
+ "figure.text.verdict-not-received": "the check did not come back — {reason}",
303
+ "figure.text.verdict-waiting": "the check follows your edits.",
292
304
  "figure.text.canvas-appears-once-a-part-exists": "pick a shape on the left to add the first part.",
293
305
  "figure.text.capability-hand-written": "set by hand, so it matches none of the levels above. it is saved under the names below.",
294
306
  "figure.text.capacity-from-repeat": "capacity comes from the repeat setting and the instance size",
@@ -34,6 +34,12 @@
34
34
  "figure.button.settle-on-floor": "床に置く",
35
35
  "figure.button.snap": "snap",
36
36
  "figure.button.take-n-changes": "apply {n}",
37
+ "figure.label.declared-space": "空間宣言",
38
+ "figure.text.catalog-needs-a-save": "保存した後に設定できます。",
39
+ "figure.text.declared-space-declared": "部品から宣言しました。",
40
+ "figure.text.declared-space-none": "まだ宣言していません。",
41
+ "figure.text.mounting-face-needs-space": "先に空間を宣言すると選べます。",
42
+ "figure.text.placement-redeclares": "変更すると、今の部品で空間を宣言し直します。",
37
43
  "figure.text.recommended-fix-explained": "すべてのサイズ変更ケースを再検査した {n} 件の修正です。下書きだけに適用され、保存前に確認できます。",
38
44
  "figure.text.show-affected-parts": "影響する部品を見る",
39
45
  "figure.button.to-floor": "to floor",
@@ -226,6 +232,7 @@
226
232
  "figure.label.recipe-lift": "リフトの高さが変わる",
227
233
  "figure.label.recipe-pusher": "プッシャーが位置に移動する",
228
234
  "figure.label.release": "公開",
235
+ "figure.label.release-verdict": "公開判定",
229
236
  "figure.label.released-versions": "発行済みバージョン",
230
237
  "figure.label.repeat-axis": "axis it repeats along",
231
238
  "figure.label.repeat-count": "個数",
@@ -287,6 +294,11 @@
287
294
  "figure.text.budget-measured-on": "{gpu} で {on} に測定した値です。静止している図形は負荷になりません — 画面に見えて再描画されるものだけを数えます。",
288
295
  "figure.text.budget-not-measured-yet": "this target has not been measured yet.",
289
296
  "figure.text.cannot-release-now": "今は公開できません",
297
+ "figure.text.fit-needs-parts": "合わせる部品がありません。先に形状を追加してください。",
298
+ "figure.text.nothing-blocks-release": "今の形のままなら公開を妨げるものはありません。",
299
+ "figure.text.verdict-asking": "判定中…",
300
+ "figure.text.verdict-not-received": "判定を受け取れませんでした — {reason}",
301
+ "figure.text.verdict-waiting": "編集すると判定します。",
290
302
  "figure.text.canvas-appears-once-a-part-exists": "左の「図形」から 1 つ選んで最初の部品を追加してください。",
291
303
  "figure.text.capability-hand-written": "手動で指定した能力のため、上の段階のどれにも当てはまりません。下の名前で保存されます。",
292
304
  "figure.text.capacity-from-repeat": "収容数は繰り返し設定とインスタンスのサイズで決まります",
@@ -36,6 +36,12 @@
36
36
  "figure.button.settle-on-floor": "바닥에 앉히기",
37
37
  "figure.button.snap": "눈금에",
38
38
  "figure.button.take-n-changes": "{n}개 적용",
39
+ "figure.label.declared-space": "공간 선언",
40
+ "figure.text.catalog-needs-a-save": "저장한 뒤에 정할 수 있습니다.",
41
+ "figure.text.declared-space-declared": "부품으로 선언했습니다.",
42
+ "figure.text.declared-space-none": "아직 선언하지 않았습니다.",
43
+ "figure.text.mounting-face-needs-space": "공간을 먼저 선언해야 고를 수 있습니다.",
44
+ "figure.text.placement-redeclares": "바꾸면 지금 부품으로 공간을 다시 선언합니다.",
39
45
  "figure.text.recommended-fix-explained": "서버가 전체 크기 변경을 다시 검사한 {n}개 수정입니다. 초안에만 적용되며 저장 전 확인할 수 있습니다.",
40
46
  "figure.text.show-affected-parts": "영향 받는 부품 보기",
41
47
  "figure.button.to-floor": "바닥에",
@@ -228,6 +234,7 @@
228
234
  "figure.label.recipe-lift": "리프트 높이 변경",
229
235
  "figure.label.recipe-pusher": "푸셔 위치 이동",
230
236
  "figure.label.release": "발행",
237
+ "figure.label.release-verdict": "발행 판정",
231
238
  "figure.label.released-versions": "발행된 버전",
232
239
  "figure.label.repeat-axis": "늘어나는 축",
233
240
  "figure.label.repeat-count": "개수",
@@ -289,6 +296,11 @@
289
296
  "figure.text.budget-measured-on": "{gpu} 에서 {on} 에 잰 값입니다. 가만히 서 있는 도형은 값을 치르지 않습니다 — 화면에 보이고 다시 그려지는 것만 셉니다.",
290
297
  "figure.text.budget-not-measured-yet": "아직 실측 전 목표치입니다.",
291
298
  "figure.text.cannot-release-now": "지금은 발행할 수 없습니다",
299
+ "figure.text.fit-needs-parts": "맞출 부품이 없습니다. 먼저 도형을 추가하세요.",
300
+ "figure.text.nothing-blocks-release": "지금 모양 그대로면 발행을 막는 것이 없습니다.",
301
+ "figure.text.verdict-asking": "판정을 받는 중입니다…",
302
+ "figure.text.verdict-not-received": "판정을 받지 못했습니다 — {reason}",
303
+ "figure.text.verdict-waiting": "고치면 곧 판정합니다.",
292
304
  "figure.text.canvas-appears-once-a-part-exists": "왼쪽 「도형」에서 하나를 선택해 첫 부품을 추가하세요.",
293
305
  "figure.text.capability-hand-written": "직접 지정한 능력이라 위 단계 중 어디에도 해당하지 않습니다. 아래 이름으로 저장됩니다.",
294
306
  "figure.text.capacity-from-repeat": "칸 수는 반복 설정과 인스턴스 크기로 정해집니다",
@@ -34,6 +34,12 @@
34
34
  "figure.button.settle-on-floor": "letak di lantai",
35
35
  "figure.button.snap": "snap",
36
36
  "figure.button.take-n-changes": "apply {n}",
37
+ "figure.label.declared-space": "ruang yang diisytiharkan",
38
+ "figure.text.catalog-needs-a-save": "boleh ditetapkan selepas disimpan.",
39
+ "figure.text.declared-space-declared": "diisytiharkan daripada bahagian.",
40
+ "figure.text.declared-space-none": "belum diisytiharkan.",
41
+ "figure.text.mounting-face-needs-space": "isytiharkan ruang dahulu untuk memilih.",
42
+ "figure.text.placement-redeclares": "menukarnya mengisytiharkan semula ruang daripada bahagian semasa.",
37
43
  "figure.text.recommended-fix-explained": "{n} pembetulan telah diperiksa semula untuk semua keadaan saiz. Ia hanya digunakan pada draf ini; semak dan simpan apabila sedia.",
38
44
  "figure.text.show-affected-parts": "tunjukkan bahagian terjejas",
39
45
  "figure.button.to-floor": "to floor",
@@ -226,6 +232,7 @@
226
232
  "figure.label.recipe-lift": "lif menukar ketinggian",
227
233
  "figure.label.recipe-pusher": "penolak bergerak ke kedudukan",
228
234
  "figure.label.release": "terbitkan",
235
+ "figure.label.release-verdict": "semakan terbitan",
229
236
  "figure.label.released-versions": "versi yang diterbitkan",
230
237
  "figure.label.repeat-axis": "axis it repeats along",
231
238
  "figure.label.repeat-count": "bilangan",
@@ -287,6 +294,11 @@
287
294
  "figure.text.budget-measured-on": "Diukur pada {gpu}, {on}. Figura yang diam tidak menelan kos — hanya yang kelihatan dan dilukis semula dikira.",
288
295
  "figure.text.budget-not-measured-yet": "this target has not been measured yet.",
289
296
  "figure.text.cannot-release-now": "belum boleh diterbitkan",
297
+ "figure.text.fit-needs-parts": "tiada bahagian untuk dipadankan — tambah bentuk dahulu.",
298
+ "figure.text.nothing-blocks-release": "tiada apa pada bentuk semasa yang menghalang penerbitan.",
299
+ "figure.text.verdict-asking": "sedang menyemak…",
300
+ "figure.text.verdict-not-received": "semakan tidak diterima — {reason}",
301
+ "figure.text.verdict-waiting": "semakan mengikut suntingan anda.",
290
302
  "figure.text.canvas-appears-once-a-part-exists": "pilih bentuk di sebelah kiri untuk menambah bahagian pertama.",
291
303
  "figure.text.capability-hand-written": "ditetapkan secara manual, jadi tidak sepadan dengan mana-mana tahap di atas. ia disimpan dengan nama di bawah.",
292
304
  "figure.text.capacity-from-repeat": "kapasiti ditentukan oleh tetapan ulangan dan saiz instans",
@@ -34,6 +34,12 @@
34
34
  "figure.button.settle-on-floor": "落到地面",
35
35
  "figure.button.snap": "snap",
36
36
  "figure.button.take-n-changes": "apply {n}",
37
+ "figure.label.declared-space": "空间声明",
38
+ "figure.text.catalog-needs-a-save": "保存后即可设置。",
39
+ "figure.text.declared-space-declared": "已根据部件声明。",
40
+ "figure.text.declared-space-none": "尚未声明。",
41
+ "figure.text.mounting-face-needs-space": "请先声明空间,然后才能选择。",
42
+ "figure.text.placement-redeclares": "更改后将按当前部件重新声明空间。",
37
43
  "figure.text.recommended-fix-explained": "已针对所有尺寸变化情况重新检查的 {n} 项修正。它们只应用到草稿;请确认后再保存。",
38
44
  "figure.text.show-affected-parts": "查看受影响部件",
39
45
  "figure.button.to-floor": "to floor",
@@ -226,6 +232,7 @@
226
232
  "figure.label.recipe-lift": "升降机改变高度",
227
233
  "figure.label.recipe-pusher": "推杆移动到位",
228
234
  "figure.label.release": "发布",
235
+ "figure.label.release-verdict": "发布检查",
229
236
  "figure.label.released-versions": "已发布版本",
230
237
  "figure.label.repeat-axis": "axis it repeats along",
231
238
  "figure.label.repeat-count": "数量",
@@ -287,6 +294,11 @@
287
294
  "figure.text.budget-measured-on": "于 {on} 在 {gpu} 上实测。静止的图形不产生开销 — 只计算屏幕上被重绘的部分。",
288
295
  "figure.text.budget-not-measured-yet": "this target has not been measured yet.",
289
296
  "figure.text.cannot-release-now": "现在无法发布",
297
+ "figure.text.fit-needs-parts": "没有可适配的部件,请先添加形状。",
298
+ "figure.text.nothing-blocks-release": "按当前形状,没有阻止发布的问题。",
299
+ "figure.text.verdict-asking": "正在检查…",
300
+ "figure.text.verdict-not-received": "未能获得检查结果 — {reason}",
301
+ "figure.text.verdict-waiting": "编辑后即会检查。",
290
302
  "figure.text.canvas-appears-once-a-part-exists": "从左侧「形状」中选择一个来添加第一个部件。",
291
303
  "figure.text.capability-hand-written": "这是手动指定的能力,不属于上面的任何级别。将以下方的名称保存。",
292
304
  "figure.text.capacity-from-repeat": "容量由重复设置和实例尺寸决定",