@things-factory/figure-ui 10.1.84 → 10.1.86

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 (40) hide show
  1. package/client/modeller/editor.ts +46 -2
  2. package/client/modeller/figure-parts.ts +4 -4
  3. package/client/modeller/figure-section-editor.ts +484 -0
  4. package/client/modeller/figure-section-panel.ts +211 -75
  5. package/client/modeller/live.ts +13 -4
  6. package/client/modeller/section-controls.ts +89 -8
  7. package/client/modeller/section-drawing.ts +97 -0
  8. package/dist-client/modeller/clips.d.ts +1 -1
  9. package/dist-client/modeller/editor.d.ts +8 -0
  10. package/dist-client/modeller/editor.js +51 -1
  11. package/dist-client/modeller/editor.js.map +1 -1
  12. package/dist-client/modeller/figure-parts.js +1 -3
  13. package/dist-client/modeller/figure-parts.js.map +1 -1
  14. package/dist-client/modeller/figure-section-editor.d.ts +62 -0
  15. package/dist-client/modeller/figure-section-editor.js +495 -0
  16. package/dist-client/modeller/figure-section-editor.js.map +1 -0
  17. package/dist-client/modeller/figure-section-panel.d.ts +26 -12
  18. package/dist-client/modeller/figure-section-panel.js +193 -69
  19. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  20. package/dist-client/modeller/live.d.ts +3 -1
  21. package/dist-client/modeller/live.js +13 -3
  22. package/dist-client/modeller/live.js.map +1 -1
  23. package/dist-client/modeller/section-controls.d.ts +33 -0
  24. package/dist-client/modeller/section-controls.js +71 -7
  25. package/dist-client/modeller/section-controls.js.map +1 -1
  26. package/dist-client/modeller/section-drawing.d.ts +33 -0
  27. package/dist-client/modeller/section-drawing.js +82 -0
  28. package/dist-client/modeller/section-drawing.js.map +1 -0
  29. package/dist-client/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +3 -3
  31. package/test/browser/acceptance-page.js +7 -0
  32. package/test/palette-icons.test.ts +1 -1
  33. package/test/parts-wiring.test.ts +6 -2
  34. package/test/section-editor.test.ts +192 -0
  35. package/test/section-panel.test.ts +108 -6
  36. package/translations/en.json +37 -2
  37. package/translations/ja.json +37 -2
  38. package/translations/ko.json +37 -2
  39. package/translations/ms.json +37 -2
  40. package/translations/zh.json +37 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.84",
3
+ "version": "10.1.86",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -27,7 +27,7 @@
27
27
  "clean": "npm run clean:server && npm run clean:client"
28
28
  },
29
29
  "dependencies": {
30
- "@hatiolab/figure-model": "^0.2.0",
30
+ "@hatiolab/figure-model": "^0.2.2",
31
31
  "@hatiolab/things-scene": "^10.1.63",
32
32
  "@material/web": "^2.0.0",
33
33
  "@operato/data-grist": "^10.0.0",
@@ -44,5 +44,5 @@
44
44
  "@things-factory/figure-service": "^10.1.84",
45
45
  "three": "^0.185.1"
46
46
  },
47
- "gitHead": "12df439ce09de2bc9aa87d4578766f46bfad48fd"
47
+ "gitHead": "8e56ed31b1c81934257f2938b9b1a51f1852773f"
48
48
  }
@@ -40,6 +40,13 @@ export const MODELS = {
40
40
  ceiling: () => box('ceiling', { x: 200, y: -200, z: 150 }),
41
41
  wall: () => box('wall', { x: 200, y: 200, z: 200 }),
42
42
  center: () => box('center', { x: 200, y: 200, z: 150 }),
43
+ /* A triangle pushed up: its section edited point by point (ADR-0100 stage 2). */
44
+ wedge: () => {
45
+ let a = createFigureAsset('Wedge')
46
+ a.palette = { ...a.palette, ...PALETTE }
47
+ a = addFigurePrimitive(a, { id: 'wedge', kind: 'polygon', dimensions: { height: 200 }, points: [{ x: -300, z: -200 }, { x: 300, z: -200 }, { x: 0, z: 250 }], position: { x: 0, y: 100, z: 0 } })
48
+ return serializeFigureAsset(a)
49
+ },
43
50
  /* A box and a drum: rounded corners and an inside from the properties tab (ADR-0100 stage 1). */
44
51
  shapes: () => {
45
52
  let a = createFigureAsset('Box and drum')
@@ -17,6 +17,6 @@ test('every shape in the palette has an icon the font draws', () => {
17
17
  const parts = readFileSync(fileURLToPath(new URL('../client/modeller/figure-parts.ts', import.meta.url)), 'utf8')
18
18
  const recipes = parts.slice(parts.indexOf('const RECIPES'), parts.indexOf(']', parts.indexOf('const RECIPES')))
19
19
  const icons = [...recipes.matchAll(/icon: '([a-z_]+)'/g)].map(m => m[1]!)
20
- assert.ok(icons.length >= 9)
20
+ assert.ok(icons.length >= 8, 'the eight starting shapes (ADR-0100 §2.1)')
21
21
  assert.deepEqual(icons.filter(icon => !RENDERS.has(icon)), [])
22
22
  })
@@ -57,8 +57,12 @@ test('좁은 칸에서도 고른 줄의 이름과 조작이 다 보인다 —
57
57
  assert.match(narrow, /li\[selected\] div\[ops\] \{\s*flex-basis: 100%;/)
58
58
  })
59
59
 
60
- test('둥근 상자는 제 이름으로 부른다 — 「사각 단면」이 아니다', () => {
61
- assert.match(panel, /kind: 'rounded-box', icon: 'crop_square', label: 'figure\.label\.primitive-rounded-box'/)
60
+ test('팔레트는 시작 모양만 낸다 — 둥근 상자와 통은 상자의 모서리·속 공간으로 만든다 (ADR-0100 §2.1·§3.3)', () => {
61
+ const recipes = panel.slice(panel.indexOf('const RECIPES'), panel.indexOf(']', panel.indexOf('const RECIPES')))
62
+ const kinds = [...recipes.matchAll(/kind: '([\w-]+)'/g)].map(m => m[1])
63
+ assert.deepEqual(kinds, ['box', 'cylinder', 'sphere', 'frustum', 'polygon', 'plate', 'wall', 'portal'])
64
+ /* A part made so is still called by what it is: its icon in the parts list, its name over the properties (links.ts). */
65
+ assert.match(panel, /'rounded-box': 'crop_square'/)
62
66
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms'])
63
67
  assert.ok(JSON.parse(read(`../translations/${lang}.json`))['figure.label.primitive-rounded-box'], lang)
64
68
  })
@@ -0,0 +1,192 @@
1
+ /*
2
+ * **단면 편집 — 점을 옮기고 더하고 지운다** (ADR-0100 2단계, 2026-09-30).
3
+ *
4
+ * 편집 창이 하는 점 편집이 figure-model 의 한 명령으로 받아들여지는지, 잘못된 단면을 커널의 말 그대로 사람의 말로 하는지,
5
+ * 점 번호가 편집 내내 제 이름의 번호인지를 잰다. 2단계 종료 조건: 삼각형 → 사다리꼴, 점 추가·삭제, 오목 단면, 자기 교차 거절.
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 { addFigurePrimitive, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
13
+ import type { FigureAsset, FigureEvaluatedShape, FigureSectionPoint } from '@hatiolab/figure-model'
14
+
15
+ import { extentOf, insertAfter, issueWords, issuesOf, markedBy, movePoint, nextPointId, pointLabel, removePoint } from '../client/modeller/section-drawing.ts'
16
+ import { livePartsOf } from '../client/modeller/live.ts'
17
+
18
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
19
+ /* A source file is a big string: a failing assert.match on it spends minutes building the diff and looks hung. */
20
+ const has = (source: string, re: RegExp, message?: string) => assert.ok(re.test(source), message ?? `missing: ${re}`)
21
+ const hasNot = (source: string, re: RegExp, message?: string) => assert.ok(!re.test(source), message ?? `present: ${re}`)
22
+ const editorPanel = read('../client/modeller/figure-section-editor.ts')
23
+ const sectionPanel = read('../client/modeller/figure-section-panel.ts')
24
+ const editor = read('../client/modeller/editor.ts')
25
+ const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
26
+ const say = (key: string, params: Record<string, string>) => Object.entries(params).reduce((s, [k, v]) => s.replace(`{${k}}`, v), ko[key] ?? key)
27
+
28
+ const triangle = () =>
29
+ addFigurePrimitive(createFigureAsset('Drawn'), {
30
+ id: 'wedge',
31
+ kind: 'polygon',
32
+ dimensions: { height: 100 },
33
+ points: [{ x: -50, z: -40 }, { x: 50, z: -40 }, { x: 0, z: 40 }],
34
+ position: { x: 0, y: 50, z: 0 }
35
+ })
36
+ const shapeOf = (asset: FigureAsset) =>
37
+ (compileFigureAsset(asset).evaluate().geometry.find(g => (g as { id: string }).id === 'wedge') as unknown as { shape: FigureEvaluatedShape }).shape
38
+ const pointsOf = (asset: FigureAsset) => {
39
+ const s = shapeOf(asset).extrusion!.section
40
+ return s.kind === 'polygon' ? s.points : []
41
+ }
42
+ const apply = (asset: FigureAsset, points: readonly FigureSectionPoint[]) =>
43
+ applyFigureAuthoring(asset, { kind: 'set-section-points', part: 'wedge', points: [...points] })
44
+
45
+ test('삼각형 → 사다리꼴: 선을 눌러 점을 더하고 옮기면, 한 명령으로 들어가고 옛 점은 제 번호를 지킨다', () => {
46
+ const t = triangle()
47
+ const start = pointsOf(t)
48
+ const apex = start.find(p => p.z === 40)!
49
+ /* The side from the apex to the next point gains a point in its middle, then both move out to the top corners. */
50
+ const added = insertAfter(start, apex.id, 'y')
51
+ assert.equal(added.id, 'p4', 'one past the highest number')
52
+ /* The loop runs 1 → 3 (apex) → 2, so the new point stands between the apex and 2: the apex goes left, the new one right. */
53
+ let working = movePoint(added.points, apex.id, { x: -25, z: 40 })
54
+ working = movePoint(working, added.id, { x: 25, z: 40 })
55
+ const next = apply(t, working)
56
+ const after = pointsOf(next)
57
+ assert.equal(after.length, 4)
58
+ assert.deepEqual(after.find(p => p.id === apex.id), { id: apex.id, x: -25, z: 40 })
59
+ assert.deepEqual(after.map(p => pointLabel(p.id)).sort(), ['1', '2', '3', '4'])
60
+ })
61
+
62
+ test('점을 지우면 남은 점은 제 번호 그대로이고, 세 점 밑으로는 까닭과 함께 막는다', () => {
63
+ const t = triangle()
64
+ /* A point added on a side is valid where it stands (a straight angle); moved out, the section is a quadrilateral. */
65
+ const added = insertAfter(pointsOf(t), 'p1', 'y')
66
+ assert.deepEqual(issuesOf(added.points, 'y'), [], 'a point on a side is taken before it is moved')
67
+ const four = apply(t, movePoint(added.points, added.id, { x: -60, z: 10 }))
68
+ const three = removePoint(pointsOf(four), 'p2')
69
+ assert.ok(Array.isArray(three))
70
+ const after = pointsOf(apply(four, three as FigureSectionPoint[]))
71
+ assert.deepEqual(after.map(p => p.id).sort(), ['p1', 'p3', 'p4'], 'deleting 2 leaves 1, 3 and 4')
72
+ assert.deepEqual(removePoint(after, 'p1'), { refused: 'too-few' })
73
+ assert.equal(nextPointId(after), 'p5', 'a new point does not take the gap')
74
+ })
75
+
76
+ test('오목 단면은 받고, 교차하는 선은 표시한 선·점과 사람의 말로 거절한다', () => {
77
+ const t = triangle()
78
+ const notch: FigureSectionPoint[] = [
79
+ { id: 'p1', x: -50, z: -40 },
80
+ { id: 'p2', x: 50, z: -40 },
81
+ { id: 'p3', x: 50, z: 40 },
82
+ { id: 'p4', x: 0, z: 0 },
83
+ { id: 'p5', x: -50, z: 40 }
84
+ ]
85
+ assert.deepEqual(issuesOf(notch, 'y'), [])
86
+ assert.equal(pointsOf(apply(t, notch)).length, 5)
87
+ const bow: FigureSectionPoint[] = [
88
+ { id: 'p1', x: -50, z: -40 },
89
+ { id: 'p2', x: 50, z: 40 },
90
+ { id: 'p3', x: 50, z: -40 },
91
+ { id: 'p4', x: -50, z: 40 }
92
+ ]
93
+ const issues = issuesOf(bow, 'y')
94
+ assert.equal(issues[0]!.code, 'SIDES_CROSS')
95
+ const words = issueWords(issues[0]!, bow, 'y')
96
+ assert.equal(say(words.key, words.params), '1–2 선과 3–4 선이 교차합니다. 표시된 점을 옮겨 주세요.')
97
+ const marked = markedBy(issues)
98
+ assert.deepEqual([...marked.points].sort(), ['p1', 'p2', 'p3', 'p4'])
99
+ assert.throws(() => apply(t, bow), /SECTION_INVALID/)
100
+ })
101
+
102
+ test('치는 동안 입체는 그 점으로 그려지고, 잘못된 동안은 마지막으로 유효했던 모양으로 그린다', () => {
103
+ const t = triangle()
104
+ const square: FigureSectionPoint[] = [
105
+ { id: 'p1', x: -50, z: -40 },
106
+ { id: 'p2', x: 50, z: -40 },
107
+ { id: 'p3', x: 50, z: 40 },
108
+ { id: 'p4', x: -50, z: 40 }
109
+ ]
110
+ const drawn = livePartsOf(t, { values: {}, action: { kind: 'set-section-points', part: 'wedge', points: square } })!
111
+ const body = drawn.find(p => p.id === 'wedge') as unknown as { shape: FigureEvaluatedShape }
112
+ assert.equal((body.shape.extrusion!.section as { points: unknown[] }).points.length, 4)
113
+ assert.equal(pointsOf(t).length, 3, 'the figure itself is not changed')
114
+ has(editorPanel, /detail: \{ action: this\.action\(this\.lastValid\) \}/, 'the preview is always of a valid shape')
115
+ has(editorPanel, /figure\.text\.section-editor-last-valid/)
116
+ })
117
+
118
+ test('적용은 한 명령, 취소·Esc 는 아무것도 보내지 않는다 — 창을 연 뒤 도형이 바뀌면 적용을 막는다', () => {
119
+ has(editorPanel, /private apply\(\) \{\s*if \(this\.stale \|\| issuesOf\(this\.working, this\.axis\)\.length\) return/)
120
+ has(editorPanel, /private cancel\(\) \{\s*this\.dispatchEvent\(new CustomEvent\('command-live', \{ detail: \{ action: null \}/)
121
+ has(editorPanel, /return this\.revision !== this\.openedAt/)
122
+ has(editor, /<figure-section-editor[\s\S]*?\.revision=\$\{this\.session\?\.read\(\)\.revision \?\? 0\}[\s\S]*?@command=\$\{\(e: CustomEvent\) => this\.linkCommand\(e\.detail\.action\)\}/)
123
+ has(editor,
124
+ /protected willUpdate[\s\S]*?if \(this\.sectionEditing && this\.sectionEditing !== this\.selected\) this\.closeSectionEditor\(\)\s*\}/,
125
+ 'the selection moved off the part by any way closes it and lets go of the preview'
126
+ )
127
+ has(editor, /private closeSectionEditor\(\) \{\s*this\.sectionEditing = null\s*this\.holdAction\(null\)/)
128
+ has(sectionPanel, /section-editor-open/)
129
+ })
130
+
131
+ test('Delete 키는 칸 안에서는 글자를, 밖에서는 고른 점을 지운다 — 창이 먼저 듣고, 받은 키는 뒤의 장면에 넘기지 않는다', () => {
132
+ has(editorPanel, /\(e\.key === 'Delete' \|\| e\.key === 'Backspace'\) && !inField && this\.selected/)
133
+ /* The scene listens on its own element and stops the key there: heard later, Esc selected nothing and Delete took the part. */
134
+ has(editorPanel, /window\.addEventListener\('keydown', this\.keys, true\)/)
135
+ has(editorPanel, /const take = \(\) => \(e\.preventDefault\(\), e\.stopImmediatePropagation\(\)\)/)
136
+ has(editorPanel, /if \(e\.key === 'Escape'\) \{\s*take\(\)\s*this\.cancel\(\)/)
137
+ })
138
+
139
+ test('그림은 점을 다 담고 여유를 둔다 — 50 mm 단위로', () => {
140
+ assert.equal(extentOf([{ id: 'p1', x: -50, z: -40 }, { id: 'p2', x: 50, z: -40 }, { id: 'p3', x: 0, z: 40 }], 'y'), 150)
141
+ })
142
+
143
+ test('편집 창의 말은 다섯 말 모두에 있다', () => {
144
+ const keys = [...new Set([...editorPanel.matchAll(/'(figure\.(?:label|text|button)\.section-[\w-]+)'/g)].map(m => m[1]!))]
145
+ const drawing = read('../client/modeller/section-drawing.ts')
146
+ keys.push(...new Set([...drawing.matchAll(/'(figure\.text\.section-issue-[\w-]+)'/g)].map(m => m[1]!)))
147
+ assert.ok(keys.length >= 20, String(keys.length))
148
+ for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
149
+ const words = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
150
+ for (const key of keys) assert.ok(words[key], `${lang} has ${key}`)
151
+ }
152
+ })
153
+
154
+ test('창의 네 줄은 줄 번호로 선다 — 넓은 화면에서 탭 줄이 숨어도 적용·취소가 밀려 나가지 않는다', () => {
155
+ for (const [part, row] of [['header', 1], ['nav\\[tabs\\]', 2], ['div\\[body\\]', 3], ['footer', 4]] as const)
156
+ has(editorPanel, new RegExp(`${part} \\{\\s*grid-row: ${row};`))
157
+ })
158
+
159
+ test('치는 동안의 명령이 쓴 입력은 명령의 값으로 그린다 — 지금 값이 옮긴 점·다시 친 반지름을 되돌리지 않는다', async () => {
160
+ const { liveOverSession } = await import('../client/modeller/live.ts')
161
+ const t = triangle()
162
+ const moved = pointsOf(t).map(p => (p.id === 'p3' ? { ...p, x: -30 } : p))
163
+ const drawn = liveOverSession(t, { ...t.designInputs }, { values: {}, action: { kind: 'set-section-points', part: 'wedge', points: moved } })!
164
+ const wedge = drawn.find(p => p.id === 'wedge') as unknown as { shape: FigureEvaluatedShape }
165
+ assert.equal((wedge.shape.extrusion!.section as { points: FigureSectionPoint[] }).points.find(p => p.id === 'p3')!.x, -30)
166
+ /* Stage 1's corner, typed a second time: the radius drawn is the one typed, not the one applied before. */
167
+ const { addFigureBox } = await import('@hatiolab/figure-model')
168
+ const rounded = applyFigureAuthoring(addFigureBox(createFigureAsset('B'), { id: 'b', size: { x: 400, y: 100, z: 300 }, position: { x: 0, y: 50, z: 0 } }), { kind: 'set-section-corner', part: 'b', corner: 20 })
169
+ const again = liveOverSession(rounded, { ...rounded.designInputs }, { values: {}, action: { kind: 'set-section-corner', part: 'b', corner: 80 } })!
170
+ const b = again.find(p => p.id === 'b') as unknown as { shape: FigureEvaluatedShape }
171
+ assert.deepEqual((b.shape.extrusion!.section as { corner: unknown }).corner, { x: 80, z: 80 })
172
+ })
173
+
174
+ test('편집 창이 열린 동안 모델러의 키는 비켜선다 — Esc 가 선택을, Delete 가 부품을, Ctrl+Z 가 도형의 기록을 건드리지 않는다', () => {
175
+ const onKey = editor.slice(editor.indexOf(' private onKey = '), editor.indexOf(' connectedCallback() {', editor.indexOf(' private onKey = ')))
176
+ has(onKey, /if \(this\.sectionEditing\) return\s*const canvas/)
177
+ assert.ok(onKey.indexOf('if (this.sectionEditing) return') < onKey.indexOf('keyActionOf('), 'before any key is taken')
178
+ })
179
+
180
+ test('속성 칸의 점 좌표도 커널 이름이 아니라 「점 1 Z」로 부른다 — 편집 창과 같은 말', () => {
181
+ has(editor, /const point = \/\^point\\\.p\(\\d\+\)\\\.\(\[xyz\]\)\$\/\.exec\(name\)/)
182
+ has(editor, /i18next\.t\('figure\.label\.section-editor-coordinate', \{ point: point\[1\]!, axis: point\[2\]!\.toUpperCase\(\) \}\)/)
183
+ assert.equal(say('figure.label.section-editor-coordinate', { point: '1', axis: 'Z' }), '점 1 Z')
184
+ })
185
+
186
+ test('편집 창 옆에 입체를 볼 자리가 없으면 윤곽과 입체 결과를 번갈아 본다 (§7)', async () => {
187
+ const { NARROW_BELOW } = { NARROW_BELOW: Number(/export const NARROW_BELOW = (\d+)/.exec(editorPanel)![1]) }
188
+ assert.ok(NARROW_BELOW > 560, 'wider than the editor itself')
189
+ has(editorPanel, /:host\(\[narrow\]\) nav\[tabs\] \{\s*display: flex;/)
190
+ has(editorPanel, /:host\(\[narrow\]\[show='solid'\]\) div\[body\] \{\s*display: none;/)
191
+ has(editorPanel, /this\.room\.observe\(this\.parentElement\)/)
192
+ })
@@ -12,10 +12,23 @@ import { fileURLToPath } from 'node:url'
12
12
  import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
13
13
  import type { FigureAsset, FigureEvaluatedShape } from '@hatiolab/figure-model'
14
14
 
15
- import { cavityRequestOf, defaultWallOf } from '../client/modeller/section-controls.ts'
15
+ import {
16
+ cavityDepthOf,
17
+ cavityRefusalOf,
18
+ cavityRequestOf,
19
+ cavityResultOf,
20
+ cornerLimitOf,
21
+ cornerRefusalOf,
22
+ defaultWallOf,
23
+ wallLimitOf
24
+ } from '../client/modeller/section-controls.ts'
25
+ import { livePartsOf } from '../client/modeller/live.ts'
16
26
  import { editRefusalOf } from '../client/modeller/refusals.ts'
17
27
 
18
28
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
29
+ /* A source file is a big string: a failing assert.match on it spends minutes building the diff and looks hung. */
30
+ const has = (source: string, re: RegExp, message?: string) => assert.ok(re.test(source), message ?? `missing: ${re}`)
31
+ const hasNot = (source: string, re: RegExp, message?: string) => assert.ok(!re.test(source), message ?? `present: ${re}`)
19
32
  const panel = read('../client/modeller/figure-section-panel.ts')
20
33
  const editor = read('../client/modeller/editor.ts')
21
34
  const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
@@ -40,7 +53,7 @@ test('모서리 반지름은 네 모서리를 한 값으로 둥글게 하고, 0
40
53
  const sharp = applyFigureAuthoring(round, { kind: 'set-section-corner', part: 'body', corner: null })
41
54
  assert.equal(shapeOf(sharp).extrusion!.section.kind, 'rectangle')
42
55
  assert.equal('corner' in shapeOf(sharp).extrusion!.section, false)
43
- assert.match(panel, /corner: value > 0 \? value : null/, 'the field sends 0 as no rounding')
56
+ has(panel, /corner: value > 0 \? value : null/, 'the field sends 0 as no rounding')
44
57
  })
45
58
 
46
59
  test('속을 비우면 기본 벽·막힌 바닥·열린 윗면의 쟁반이 되고, 끝과 벽을 하나씩 바꾼다', () => {
@@ -73,17 +86,73 @@ test('벽이 단면을 다 채우거나 모서리가 변의 절반을 넘으면,
73
86
  assert.equal(refusal(() => applyFigureAuthoring(probe(), { kind: 'set-section-corner', part: 'body', corner: 160 })), ko['figure.text.section-corner-too-large'])
74
87
  })
75
88
 
76
- test('편집기는 단면 칸의 명령을 세션의 이력 하나로 돌리고, 칸은 둥글린 사각·타원 단면에만 선다', () => {
77
- assert.match(editor, /<figure-section-panel[\s\S]*?@command=\$\{\(e: CustomEvent\) => this\.linkCommand\(e\.detail\.action\)\}/)
78
- assert.match(panel, /if \(kind !== 'rectangle' && kind !== 'ellipse'\) return nothing/)
89
+ test('편집기는 단면 칸의 명령을 세션의 이력 하나로 돌리고, 치는 동안의 값은 그리기만 한다', () => {
90
+ has(editor, /<figure-section-panel[\s\S]*?@command=\$\{\(e: CustomEvent\) => this\.linkCommand\(e\.detail\.action\)\}/)
91
+ has(editor, /@command-live=\$\{\(e: CustomEvent\) => this\.holdAction\(e\.detail\.action\)\}/)
92
+ has(editor, /private linkCommand\(action: FigureAuthoringAction\) \{\s*\/\*[^*]*\*\/\s*this\.holdAction\(null\)/, 'a command sent lets go of the one in hand')
79
93
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
80
94
  const words = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
81
95
  const used = [...new Set([...panel.matchAll(/'(figure\.(?:label|text)\.section-[\w-]+|figure\.label\.group-section)'/g)].map(m => m[1]!))]
82
- assert.ok(used.length >= 8)
96
+ /* The keys the panel builds: each result, and the wall's and the ends' over-limit words. */
97
+ used.push(...['pipe', 'container', 'sealed'].map(r => `figure.label.section-result-${r}`), 'figure.text.section-wall-over', 'figure.text.section-ends-over')
98
+ assert.ok(used.length >= 16)
83
99
  for (const key of used) assert.ok(words[key], `${lang} has ${key}`)
84
100
  }
85
101
  })
86
102
 
103
+ test('화면에 커널 말을 쓰지 않는다 — 「속 비우기」·「중공」·「공동」이 아니라 「속 공간」 (ADR-0100 §2.1)', () => {
104
+ const words = Object.entries(ko).filter(([k]) => k.startsWith('figure.label.section-') || k.startsWith('figure.text.section-'))
105
+ for (const [key, text] of words) assert.doesNotMatch(text, /속\s*비우|속을 비움|중공|공동/, key)
106
+ assert.equal(ko['figure.label.section-cavity'], '속 공간 만들기')
107
+ })
108
+
109
+ test('모서리 반지름의 허용값은 커널이 정한 그대로다 — 짧은 변의 절반까지', () => {
110
+ const shape = shapeOf(probe())
111
+ assert.equal(cornerLimitOf(shape), 150)
112
+ assert.equal(cornerRefusalOf(shape, 150), null)
113
+ assert.deepEqual(cornerRefusalOf(shape, 151), { max: 150 })
114
+ /* The screen's limit is the kernel's: the command takes 150 and refuses 151. */
115
+ assert.doesNotThrow(() => shapeOf(applyFigureAuthoring(probe(), { kind: 'set-section-corner', part: 'body', corner: 150 })))
116
+ assert.throws(() => applyFigureAuthoring(probe(), { kind: 'set-section-corner', part: 'body', corner: 151 }))
117
+ })
118
+
119
+ test('벽과 막힌 끝의 허용값도 커널이 정한 그대로다 — 칸은 고칠 값을 말하고 보내지 않는다', () => {
120
+ const tray = applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: cavityRequestOf(shapeOf(probe())) })
121
+ const shape = shapeOf(tray)
122
+ assert.equal(wallLimitOf(shape), 150)
123
+ assert.deepEqual(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: 150 })), { code: 'wall-too-thick', max: 150 })
124
+ assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: 149 })), null)
125
+ assert.doesNotThrow(() => applyFigureAuthoring(tray, { kind: 'set-cavity', part: 'body', cavity: cavityRequestOf(shape, { wall: 149 }) }))
126
+ assert.throws(() => applyFigureAuthoring(tray, { kind: 'set-cavity', part: 'body', cavity: cavityRequestOf(shape, { wall: 150 }) }))
127
+ assert.deepEqual(cavityRefusalOf(shape, cavityRequestOf(shape, { start: { closed: 120 }, end: { closed: 80 } })), { code: 'ends-too-thick', max: 200 })
128
+ /* One side thicker than the rest: the kernel still says whether the inside has room. */
129
+ assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: { 'x-': 396, 'x+': 5, 'z-': 5, 'z+': 5 } })).code, 'wall-too-thick')
130
+ assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: { 'x-': 100, 'x+': 5, 'z-': 5, 'z+': 5 } })), null)
131
+ })
132
+
133
+ test('속 공간은 무엇이 되었는지와 깊이로 보인다 — 한쪽이 열리면 용기, 양쪽이면 관, 양쪽이 막히면 닫힌 속 공간', () => {
134
+ const tray = shapeOf(applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: cavityRequestOf(shapeOf(probe())) }))
135
+ assert.equal(cavityResultOf(tray.cavity!), 'container')
136
+ assert.equal(cavityDepthOf(tray), 170, 'the 200 mm push less its 30 mm floor')
137
+ assert.equal(cavityResultOf({ ...tray.cavity!, start: 'open' }), 'pipe')
138
+ assert.equal(cavityResultOf({ ...tray.cavity!, end: { closed: 5 } }), 'sealed')
139
+ has(panel, /figure\.text\.section-depth/)
140
+ })
141
+
142
+ test('치는 동안의 값은 사본에 그려지고 원본은 그대로다 — 받을 수 없는 값은 그리지 않는다', () => {
143
+ const asset = probe()
144
+ const drawn = livePartsOf(asset, { values: {}, action: { kind: 'set-section-corner', part: 'body', corner: 40 } })!
145
+ const body = drawn.find(p => p.id === 'body')! as unknown as { shape: FigureEvaluatedShape }
146
+ assert.deepEqual(body.shape.extrusion!.section, { kind: 'rectangle', size: { x: 400, z: 300 }, corner: { x: 40, z: 40 } })
147
+ assert.equal('corner' in shapeOf(asset).extrusion!.section, false, 'the asset itself is not changed')
148
+ assert.equal(livePartsOf(asset, { values: {}, action: { kind: 'set-section-corner', part: 'body', corner: 999 } }), null)
149
+ })
150
+
151
+ test('다각 단면에는 속 공간을 만들 수 없다고 그 자리에서 말한다', () => {
152
+ has(panel, /kind === 'rectangle' \|\| kind === 'ellipse'\s*\? this\.renderCavity\(body, e\.axis\)\s*: html`[\s\S]*?figure\.text\.section-cavity-unavailable/)
153
+ assert.match(ko['figure.text.section-cavity-unavailable']!, /이 윤곽에는 속 공간을 만들 수 없습니다/)
154
+ })
155
+
87
156
  test('붙어 있는 것이 기대는 면을 없애는 편집은 거절되고, 그 면을 사람의 말로 부른다', () => {
88
157
  let asset = applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: { wall: 10, start: { closed: 15 }, end: 'open' } })
89
158
  asset = addFigureBox(asset, { id: 'divider', size: { x: 10, y: 100, z: 200 }, position: { x: 0, y: 0, z: 0 } })
@@ -91,3 +160,36 @@ test('붙어 있는 것이 기대는 면을 없애는 편집은 거절되고,
91
160
  const said = refusal(() => applyFigureAuthoring(asset, { kind: 'set-cavity', part: 'body', cavity: { wall: 10, start: 'open', end: 'open' } }))
92
161
  assert.equal(said, say('figure.text.section-face-in-use', { faces: ko['figure.label.face-inside-bottom']! }))
93
162
  })
163
+
164
+ test('치는 동안 그려진 도형이 칸의 글자를 덮어쓰지 않는다 — 친 것은 보낼 때까지 칸이 든다', () => {
165
+ has(panel, /\.value=\$\{live\(this\.typed\[key\] \?\? shown\)\}/)
166
+ has(panel, /this\.typed = \{ \.\.\.this\.typed, \[key\]: \(ev\.target as HTMLInputElement\)\.value \}/)
167
+ })
168
+
169
+ test('면마다 벽 두께를 치는 동안에도 그려진다 — 명령이 없앤 입력(한 벽 두께)의 지금 값은 미리보기를 막지 않는다', async () => {
170
+ const { liveOverSession } = await import('../client/modeller/live.ts')
171
+ const tray = applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: { wall: 30, start: { closed: 30 }, end: 'open' } })
172
+ /* The editor draws over every value as it stands: the one wall is among them. */
173
+ const current = { ...tray.designInputs }
174
+ assert.ok('body.dimension.wall' in current)
175
+ const drawn = liveOverSession(tray, current, {
176
+ values: {},
177
+ action: { kind: 'set-cavity', part: 'body', cavity: { wall: { 'x-': 100, 'x+': 30, 'z-': 30, 'z+': 30 }, start: { closed: 30 }, end: 'open' } }
178
+ })
179
+ assert.ok(drawn, 'the preview is drawn')
180
+ const body = drawn!.find(p => p.id === 'body') as unknown as { shape: FigureEvaluatedShape }
181
+ assert.equal(body.shape.cavity!.wall['x-'], 100)
182
+ })
183
+
184
+ test('한 면의 벽 한도는 그 방향의 너비에서 맞은편 벽을 뺀 값이다 — 네 면이 같을 때의 한도가 아니다', async () => {
185
+ const { sideWallLimitOf } = await import('../client/modeller/section-controls.ts')
186
+ const tray = applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: { wall: 30, start: { closed: 30 }, end: 'open' } })
187
+ const shape = shapeOf(tray)
188
+ const walls = { 'x-': 30, 'x+': 40, 'z-': 30, 'z+': 30 }
189
+ assert.equal(sideWallLimitOf(shape, walls, 'x-'), 360, 'the 400 mm width less the 40 mm wall opposite')
190
+ assert.equal(sideWallLimitOf(shape, walls, 'z+'), 270)
191
+ /* The kernel agrees: just under takes, the limit refuses. */
192
+ assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: { ...walls, 'x-': 359 } })), null)
193
+ assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: { ...walls, 'x-': 360 } }))!.code, 'wall-too-thick')
194
+ has(panel, /sideWallLimitOf\(body\.shape, cavity\.wall, side\)/)
195
+ })
@@ -188,12 +188,47 @@
188
188
  "figure.label.palette-token": "palette token",
189
189
  "figure.label.part": "part",
190
190
  "figure.label.part-material": "material",
191
+ "figure.button.section-editor-open": "edit section",
192
+ "figure.label.section-editor": "edit section",
193
+ "figure.text.section-editor-length": "pushed {length} mm",
194
+ "figure.label.section-editor-outline": "outline",
195
+ "figure.label.section-editor-solid": "solid",
196
+ "figure.button.section-editor-back": "undo",
197
+ "figure.text.section-editor-hint": "Drag a point or type its coordinates. Press a side to add a point in its middle.",
198
+ "figure.text.section-editor-stale": "The figure changed after this was opened. Close it and open it again.",
199
+ "figure.button.section-editor-cancel": "cancel",
200
+ "figure.button.section-editor-apply": "apply",
201
+ "figure.text.section-editor-last-valid": "The solid shows the last valid shape.",
202
+ "figure.text.section-editor-units": "mm · from the origin · {h} right, {v} down",
203
+ "figure.text.section-editor-add-here": "add a point in the middle of side {a}–{b}",
204
+ "figure.label.section-editor-points": "points",
205
+ "figure.label.section-editor-coordinate": "point {point} {axis}",
206
+ "figure.button.section-editor-remove": "delete {point}",
207
+ "figure.text.section-issue-cross": "Sides {a} and {b} cross. Move the marked points.",
208
+ "figure.text.section-issue-touch": "Sides {a} and {b} touch. Move the marked points.",
209
+ "figure.text.section-issue-fold": "Side {b} turns back over side {a}. Move the marked points.",
210
+ "figure.text.section-issue-zero-length": "The two points of side {a} are in one place. Move one or delete it.",
211
+ "figure.text.section-issue-zero-area": "The points lie on one line and enclose nothing.",
212
+ "figure.text.section-issue-too-few": "A section needs three points or more.",
213
+ "figure.text.section-issue-too-many": "A section can have up to {limit} points.",
214
+ "figure.text.section-issue-number": "Type a number.",
215
+ "figure.text.section-cavity-unavailable": "This outline cannot have an inside. An inside is made in a rectangle or circle section.",
216
+ "figure.text.section-corner-over": "The corner radius can be at most {max} mm.",
217
+ "figure.text.section-at-most": "at most {max} mm",
218
+ "figure.text.section-not-positive": "The thickness must be more than 0.",
219
+ "figure.text.section-wall-over": "The wall must be thinner than {max} mm.",
220
+ "figure.text.section-ends-over": "The two closed ends together must be thinner than {max} mm.",
221
+ "figure.text.section-depth": "{result} · {depth} mm deep",
222
+ "figure.label.section-result-pipe": "pipe",
223
+ "figure.label.section-result-container": "container",
224
+ "figure.label.section-result-sealed": "closed inside",
225
+ "figure.text.section-wall-at-most": "thinner than {max} mm",
226
+ "figure.label.section-wall-per-side": "per side",
191
227
  "figure.label.group-section": "section",
192
228
  "figure.label.section-corner": "corner radius",
193
229
  "figure.text.section-corner-differs": "differs by axis",
194
- "figure.label.section-cavity": "hollow",
230
+ "figure.label.section-cavity": "make an inside",
195
231
  "figure.label.section-wall": "wall thickness",
196
- "figure.text.section-wall-differs": "differs by side",
197
232
  "figure.label.section-end-closed": "closed",
198
233
  "figure.label.section-end-open": "open",
199
234
  "figure.text.section-face-in-use": "Not changed: something is attached to {faces}. Detach it first.",
@@ -188,12 +188,47 @@
188
188
  "figure.label.palette-token": "palette token",
189
189
  "figure.label.part": "part",
190
190
  "figure.label.part-material": "material",
191
+ "figure.button.section-editor-open": "断面を編集",
192
+ "figure.label.section-editor": "断面を編集",
193
+ "figure.text.section-editor-length": "押し出し {length} mm",
194
+ "figure.label.section-editor-outline": "輪郭",
195
+ "figure.label.section-editor-solid": "立体",
196
+ "figure.button.section-editor-back": "元に戻す",
197
+ "figure.text.section-editor-hint": "点をドラッグするか座標を入力します。辺を押すと中点に点が追加されます。",
198
+ "figure.text.section-editor-stale": "開いた後に図形が変わりました。閉じて開き直してください。",
199
+ "figure.button.section-editor-cancel": "キャンセル",
200
+ "figure.button.section-editor-apply": "適用",
201
+ "figure.text.section-editor-last-valid": "立体は最後に有効だった形です。",
202
+ "figure.text.section-editor-units": "mm · 原点基準 · 右 {h}、下 {v}",
203
+ "figure.text.section-editor-add-here": "辺 {a}–{b} の中点に点を追加",
204
+ "figure.label.section-editor-points": "点",
205
+ "figure.label.section-editor-coordinate": "点 {point} {axis}",
206
+ "figure.button.section-editor-remove": "{point} を削除",
207
+ "figure.text.section-issue-cross": "辺 {a} と辺 {b} が交差しています。示された点を動かしてください。",
208
+ "figure.text.section-issue-touch": "辺 {a} と辺 {b} が接しています。示された点を動かしてください。",
209
+ "figure.text.section-issue-fold": "辺 {b} が辺 {a} の上に折り返しています。示された点を動かしてください。",
210
+ "figure.text.section-issue-zero-length": "辺 {a} の二点が同じ位置にあります。一方を動かすか削除してください。",
211
+ "figure.text.section-issue-zero-area": "点が一直線上にあり面積がありません。",
212
+ "figure.text.section-issue-too-few": "断面には三点以上が必要です。",
213
+ "figure.text.section-issue-too-many": "点は {limit} 個までです。",
214
+ "figure.text.section-issue-number": "数値を入力してください。",
215
+ "figure.text.section-cavity-unavailable": "この輪郭には内部の空間をつくれません。内部の空間は四角形と円の断面につくります。",
216
+ "figure.text.section-corner-over": "角の半径は {max} mm までです。",
217
+ "figure.text.section-at-most": "最大 {max} mm",
218
+ "figure.text.section-not-positive": "厚さは 0 より大きくしてください。",
219
+ "figure.text.section-wall-over": "壁の厚さは {max} mm より薄くしてください。",
220
+ "figure.text.section-ends-over": "閉じた両端の厚さの合計は {max} mm より薄くしてください。",
221
+ "figure.text.section-depth": "{result} · 深さ {depth} mm",
222
+ "figure.label.section-result-pipe": "管",
223
+ "figure.label.section-result-container": "容器",
224
+ "figure.label.section-result-sealed": "閉じた内部",
225
+ "figure.text.section-wall-at-most": "{max} mm より薄く",
226
+ "figure.label.section-wall-per-side": "面ごとに",
191
227
  "figure.label.group-section": "断面",
192
228
  "figure.label.section-corner": "角の半径",
193
229
  "figure.text.section-corner-differs": "軸ごとに異なる",
194
- "figure.label.section-cavity": "中空",
230
+ "figure.label.section-cavity": "内部の空間をつくる",
195
231
  "figure.label.section-wall": "壁の厚さ",
196
- "figure.text.section-wall-differs": "面ごとに異なる",
197
232
  "figure.label.section-end-closed": "閉じる",
198
233
  "figure.label.section-end-open": "開く",
199
234
  "figure.text.section-face-in-use": "変更しませんでした: {faces} に取り付けられたものがあります。先に外してください。",
@@ -188,12 +188,47 @@
188
188
  "figure.label.palette-token": "팔레트 토큰",
189
189
  "figure.label.part": "부품",
190
190
  "figure.label.part-material": "재질",
191
+ "figure.button.section-editor-open": "단면 편집",
192
+ "figure.label.section-editor": "단면 편집",
193
+ "figure.text.section-editor-length": "돌출 길이 {length} mm",
194
+ "figure.label.section-editor-outline": "윤곽",
195
+ "figure.label.section-editor-solid": "입체 결과",
196
+ "figure.button.section-editor-back": "되돌리기",
197
+ "figure.text.section-editor-hint": "점을 끌거나 좌표를 입력합니다. 선을 누르면 가운데에 점이 생깁니다.",
198
+ "figure.text.section-editor-stale": "창을 연 뒤 도형이 바뀌었습니다. 닫고 다시 여세요.",
199
+ "figure.button.section-editor-cancel": "취소",
200
+ "figure.button.section-editor-apply": "적용",
201
+ "figure.text.section-editor-last-valid": "입체 결과는 마지막으로 유효했던 모양입니다.",
202
+ "figure.text.section-editor-units": "mm · 원점 기준 · 오른쪽 {h}, 아래 {v}",
203
+ "figure.text.section-editor-add-here": "{a}–{b} 선 가운데에 점 추가",
204
+ "figure.label.section-editor-points": "점",
205
+ "figure.label.section-editor-coordinate": "점 {point} {axis}",
206
+ "figure.button.section-editor-remove": "{point} 지우기",
207
+ "figure.text.section-issue-cross": "{a} 선과 {b} 선이 교차합니다. 표시된 점을 옮겨 주세요.",
208
+ "figure.text.section-issue-touch": "{a} 선과 {b} 선이 서로 닿습니다. 표시된 점을 옮겨 주세요.",
209
+ "figure.text.section-issue-fold": "{b} 선이 {a} 선 위로 되돌아갑니다. 표시된 점을 옮겨 주세요.",
210
+ "figure.text.section-issue-zero-length": "{a} 선의 두 점이 같은 자리에 있습니다. 한 점을 옮기거나 지워 주세요.",
211
+ "figure.text.section-issue-zero-area": "점들이 한 줄에 있어 면적이 없습니다.",
212
+ "figure.text.section-issue-too-few": "점이 세 개보다 적으면 단면이 되지 않습니다.",
213
+ "figure.text.section-issue-too-many": "점은 {limit}개까지 둘 수 있습니다.",
214
+ "figure.text.section-issue-number": "숫자를 입력해 주세요.",
215
+ "figure.text.section-cavity-unavailable": "이 윤곽에는 속 공간을 만들 수 없습니다. 속 공간은 사각형과 원 단면에 만듭니다.",
216
+ "figure.text.section-corner-over": "모서리 반지름은 {max} mm 까지입니다.",
217
+ "figure.text.section-at-most": "최대 {max} mm",
218
+ "figure.text.section-not-positive": "두께는 0 보다 커야 합니다.",
219
+ "figure.text.section-wall-over": "벽 두께는 {max} mm 보다 얇아야 합니다.",
220
+ "figure.text.section-ends-over": "막힌 두 끝의 두께를 더해 {max} mm 보다 얇아야 합니다.",
221
+ "figure.text.section-depth": "{result} · 깊이 {depth} mm",
222
+ "figure.label.section-result-pipe": "관",
223
+ "figure.label.section-result-container": "용기",
224
+ "figure.label.section-result-sealed": "닫힌 속 공간",
225
+ "figure.text.section-wall-at-most": "{max} mm 보다 얇게",
226
+ "figure.label.section-wall-per-side": "면마다 따로",
191
227
  "figure.label.group-section": "단면",
192
228
  "figure.label.section-corner": "모서리 반지름",
193
229
  "figure.text.section-corner-differs": "축마다 다름",
194
- "figure.label.section-cavity": "속을 비움",
230
+ "figure.label.section-cavity": "속 공간 만들기",
195
231
  "figure.label.section-wall": "벽 두께",
196
- "figure.text.section-wall-differs": "면마다 다름",
197
232
  "figure.label.section-end-closed": "막힘",
198
233
  "figure.label.section-end-open": "열림",
199
234
  "figure.text.section-face-in-use": "이 면에 붙어 있는 것이 있어 바꾸지 않았습니다: {faces}. 먼저 떼어 냅니다.",
@@ -188,12 +188,47 @@
188
188
  "figure.label.palette-token": "palette token",
189
189
  "figure.label.part": "part",
190
190
  "figure.label.part-material": "material",
191
+ "figure.button.section-editor-open": "sunting keratan",
192
+ "figure.label.section-editor": "sunting keratan",
193
+ "figure.text.section-editor-length": "tolakan {length} mm",
194
+ "figure.label.section-editor-outline": "garis luar",
195
+ "figure.label.section-editor-solid": "pepejal",
196
+ "figure.button.section-editor-back": "buat asal",
197
+ "figure.text.section-editor-hint": "Seret titik atau taip koordinatnya. Tekan sisi untuk menambah titik di tengahnya.",
198
+ "figure.text.section-editor-stale": "Rajah berubah selepas ini dibuka. Tutup dan buka semula.",
199
+ "figure.button.section-editor-cancel": "batal",
200
+ "figure.button.section-editor-apply": "guna",
201
+ "figure.text.section-editor-last-valid": "Pepejal menunjukkan bentuk sah yang terakhir.",
202
+ "figure.text.section-editor-units": "mm · dari asalan · {h} ke kanan, {v} ke bawah",
203
+ "figure.text.section-editor-add-here": "tambah titik di tengah sisi {a}–{b}",
204
+ "figure.label.section-editor-points": "titik",
205
+ "figure.label.section-editor-coordinate": "titik {point} {axis}",
206
+ "figure.button.section-editor-remove": "padam {point}",
207
+ "figure.text.section-issue-cross": "Sisi {a} dan {b} bersilang. Alihkan titik yang ditanda.",
208
+ "figure.text.section-issue-touch": "Sisi {a} dan {b} bersentuhan. Alihkan titik yang ditanda.",
209
+ "figure.text.section-issue-fold": "Sisi {b} berpatah balik di atas sisi {a}. Alihkan titik yang ditanda.",
210
+ "figure.text.section-issue-zero-length": "Dua titik sisi {a} berada di tempat yang sama. Alihkan atau padam satu.",
211
+ "figure.text.section-issue-zero-area": "Titik-titik berada pada satu garis dan tiada luas.",
212
+ "figure.text.section-issue-too-few": "Keratan memerlukan tiga titik atau lebih.",
213
+ "figure.text.section-issue-too-many": "Keratan boleh mempunyai sehingga {limit} titik.",
214
+ "figure.text.section-issue-number": "Taip nombor.",
215
+ "figure.text.section-cavity-unavailable": "Garis luar ini tidak boleh mempunyai ruang dalam. Ruang dalam dibuat dalam keratan segi empat atau bulatan.",
216
+ "figure.text.section-corner-over": "Jejari sudut paling banyak {max} mm.",
217
+ "figure.text.section-at-most": "paling banyak {max} mm",
218
+ "figure.text.section-not-positive": "Ketebalan mesti lebih daripada 0.",
219
+ "figure.text.section-wall-over": "Dinding mesti lebih nipis daripada {max} mm.",
220
+ "figure.text.section-ends-over": "Jumlah ketebalan dua hujung tertutup mesti kurang daripada {max} mm.",
221
+ "figure.text.section-depth": "{result} · dalam {depth} mm",
222
+ "figure.label.section-result-pipe": "paip",
223
+ "figure.label.section-result-container": "bekas",
224
+ "figure.label.section-result-sealed": "ruang dalam tertutup",
225
+ "figure.text.section-wall-at-most": "lebih nipis daripada {max} mm",
226
+ "figure.label.section-wall-per-side": "setiap sisi",
191
227
  "figure.label.group-section": "keratan",
192
228
  "figure.label.section-corner": "jejari sudut",
193
229
  "figure.text.section-corner-differs": "berbeza mengikut paksi",
194
- "figure.label.section-cavity": "berongga",
230
+ "figure.label.section-cavity": "buat ruang dalam",
195
231
  "figure.label.section-wall": "ketebalan dinding",
196
- "figure.text.section-wall-differs": "berbeza mengikut sisi",
197
232
  "figure.label.section-end-closed": "tertutup",
198
233
  "figure.label.section-end-open": "terbuka",
199
234
  "figure.text.section-face-in-use": "Tidak diubah: ada sesuatu dipasang pada {faces}. Tanggalkannya dahulu.",