@things-factory/figure-ui 10.1.85 → 10.1.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.85",
3
+ "version": "10.1.87",
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.3",
31
31
  "@hatiolab/things-scene": "^10.1.63",
32
32
  "@material/web": "^2.0.0",
33
33
  "@operato/data-grist": "^10.0.0",
@@ -40,9 +40,9 @@
40
40
  "@operato/shell": "^10.0.0",
41
41
  "@operato/styles": "^10.0.0",
42
42
  "@operato/utils": "^10.0.0",
43
- "@things-factory/ai-assistant": "^10.1.81",
43
+ "@things-factory/ai-assistant": "^10.1.87",
44
44
  "@things-factory/figure-service": "^10.1.84",
45
45
  "three": "^0.185.1"
46
46
  },
47
- "gitHead": "cbca21a8021b950f558f5547d5df4d12c199a3ef"
47
+ "gitHead": "dc552c2a7b7f97e3377a26c378ee15d3a1de612a"
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')
@@ -0,0 +1,232 @@
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', 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', 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 \|\| !this\.valid\(this\.working\)\) 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', 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
+ })
193
+
194
+ test('사각형·원도 같은 창에서 — 폭·지름·길이를 한 명령으로, 바꾸지 않은 치수는 보내지 않는다', async () => {
195
+ const { addFigureBox, figureSectionMeasuresOf } = await import('@hatiolab/figure-model')
196
+ const { workingOf, workingIssuesOf, sectionOf } = await import('../client/modeller/section-drawing.ts')
197
+ const box = applyFigureAuthoring(addFigureBox(createFigureAsset('B'), { id: 'b', size: { x: 400, y: 200, z: 300 }, position: { x: 0, y: 100, z: 0 } }), { kind: 'set-placement', placement: 'floor' })
198
+ const shape = (compileFigureAsset(box).evaluate().geometry.find(g => (g as { id: string }).id === 'b') as unknown as { shape: FigureEvaluatedShape }).shape
199
+ const w = workingOf(shape)
200
+ assert.deepEqual(w, { points: [], across: { x: 400, z: 300 }, length: 200 })
201
+ assert.deepEqual(sectionOf(shape, { ...w, across: { x: 500, z: 300 } }), { kind: 'rectangle', size: { x: 500, z: 300 } })
202
+ assert.deepEqual(figureSectionMeasuresOf(box, 'b')!.measures, { y: 'editable', x: 'editable', z: 'editable' })
203
+ /* The editor sends only what changed: a measure left alone may follow another value and must not be written. */
204
+ has(editorPanel, /const across = Object\.fromEntries\(Object\.entries\(w\.across\)\.filter\(\(\[k, v\]\) => changed\(v, o\.across\[k as Axis\]\)\)\)/)
205
+ const next = applyFigureAuthoring(box, { kind: 'set-section', part: 'b', across: { x: 500 }, length: 260 })
206
+ const b = (compileFigureAsset(next).evaluate().geometry.find(g => (g as { id: string }).id === 'b') as unknown as { shape: FigureEvaluatedShape; pose: { t: number[] } })
207
+ assert.deepEqual(b.shape.extrusion!.section, { kind: 'rectangle', size: { x: 500, z: 300 } })
208
+ assert.equal(b.pose.t[1]! + b.shape.extrusion!.start, 0, 'a floor figure keeps its bottom')
209
+ /* A corner that no longer fits, and a length of 0, are said before anything is sent. */
210
+ const rounded = applyFigureAuthoring(box, { kind: 'set-section-corner', part: 'b', corner: 100 })
211
+ const r = (compileFigureAsset(rounded).evaluate().geometry.find(g => (g as { id: string }).id === 'b') as unknown as { shape: FigureEvaluatedShape }).shape
212
+ const narrow = workingIssuesOf(r, { ...workingOf(r), across: { x: 150, z: 300 } }).words
213
+ assert.equal(say(narrow[0]!.key, narrow[0]!.params), '이 폭에서는 모서리 반지름이 75 mm 까지입니다. 폭을 넓히거나 모서리를 줄입니다.')
214
+ assert.equal(workingIssuesOf(r, { ...workingOf(r), length: 0 }).words[0]!.key, 'figure.text.section-issue-length')
215
+ })
216
+
217
+ test('속 공간이 있으면 폭·길이가 그 벽과 막힌 끝을 남기는지도 커널로 묻는다', async () => {
218
+ const { addFigureBox } = await import('@hatiolab/figure-model')
219
+ const { workingOf, workingIssuesOf } = await import('../client/modeller/section-drawing.ts')
220
+ const tray = applyFigureAuthoring(addFigureBox(createFigureAsset('T'), { id: 't', size: { x: 400, y: 200, z: 300 }, position: { x: 0, y: 100, z: 0 } }), { kind: 'set-cavity', part: 't', cavity: { wall: 40, start: { closed: 30 }, end: 'open' } })
221
+ const shape = (compileFigureAsset(tray).evaluate().geometry.find(g => (g as { id: string }).id === 't') as unknown as { shape: FigureEvaluatedShape }).shape
222
+ assert.equal(workingIssuesOf(shape, { ...workingOf(shape), across: { x: 70, z: 300 } }).words[0]!.key, 'figure.text.section-issue-cavity-walls')
223
+ assert.equal(workingIssuesOf(shape, { ...workingOf(shape), length: 25 }).words[0]!.key, 'figure.text.section-issue-cavity-length')
224
+ assert.deepEqual(workingIssuesOf(shape, { ...workingOf(shape), across: { x: 90, z: 300 } }).words, [])
225
+ })
226
+
227
+ test('길이를 바꿀 때 남는 면은 부품의 관계가 정한 것을 읽어 보여 준다 (§4.1) — 다른 값을 따르는 치수는 칸을 막는다', () => {
228
+ has(editor, /const kept = figureScaleAnchorOf\(asset, this\.selected\)\?\.\[part\.shape\.extrusion\.axis\]/)
229
+ has(editorPanel, /m\?\.\[this\.axis\] === 'linked' \? linked : this\.keptWords\(\)/)
230
+ has(editorPanel, /m\?\.\[this\.axis\] !== 'editable'/)
231
+ assert.equal(say('figure.text.section-editor-keeps', { face: ko['figure.label.face-bottom']! }), '아랫면 유지')
232
+ })
@@ -26,6 +26,9 @@ import { livePartsOf } from '../client/modeller/live.ts'
26
26
  import { editRefusalOf } from '../client/modeller/refusals.ts'
27
27
 
28
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}`)
29
32
  const panel = read('../client/modeller/figure-section-panel.ts')
30
33
  const editor = read('../client/modeller/editor.ts')
31
34
  const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
@@ -50,7 +53,7 @@ test('모서리 반지름은 네 모서리를 한 값으로 둥글게 하고, 0
50
53
  const sharp = applyFigureAuthoring(round, { kind: 'set-section-corner', part: 'body', corner: null })
51
54
  assert.equal(shapeOf(sharp).extrusion!.section.kind, 'rectangle')
52
55
  assert.equal('corner' in shapeOf(sharp).extrusion!.section, false)
53
- 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')
54
57
  })
55
58
 
56
59
  test('속을 비우면 기본 벽·막힌 바닥·열린 윗면의 쟁반이 되고, 끝과 벽을 하나씩 바꾼다', () => {
@@ -84,9 +87,9 @@ test('벽이 단면을 다 채우거나 모서리가 변의 절반을 넘으면,
84
87
  })
85
88
 
86
89
  test('편집기는 단면 칸의 명령을 세션의 이력 하나로 돌리고, 치는 동안의 값은 그리기만 한다', () => {
87
- assert.match(editor, /<figure-section-panel[\s\S]*?@command=\$\{\(e: CustomEvent\) => this\.linkCommand\(e\.detail\.action\)\}/)
88
- assert.match(editor, /@command-live=\$\{\(e: CustomEvent\) => this\.holdAction\(e\.detail\.action\)\}/)
89
- assert.match(editor, /private linkCommand\(action: FigureAuthoringAction\) \{\s*\/\*[^*]*\*\/\s*this\.holdAction\(null\)/, 'a command sent lets go of the one in hand')
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')
90
93
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
91
94
  const words = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
92
95
  const used = [...new Set([...panel.matchAll(/'(figure\.(?:label|text)\.section-[\w-]+|figure\.label\.group-section)'/g)].map(m => m[1]!))]
@@ -133,7 +136,7 @@ test('속 공간은 무엇이 되었는지와 깊이로 보인다 — 한쪽이
133
136
  assert.equal(cavityDepthOf(tray), 170, 'the 200 mm push less its 30 mm floor')
134
137
  assert.equal(cavityResultOf({ ...tray.cavity!, start: 'open' }), 'pipe')
135
138
  assert.equal(cavityResultOf({ ...tray.cavity!, end: { closed: 5 } }), 'sealed')
136
- assert.match(panel, /figure\.text\.section-depth/)
139
+ has(panel, /figure\.text\.section-depth/)
137
140
  })
138
141
 
139
142
  test('치는 동안의 값은 사본에 그려지고 원본은 그대로다 — 받을 수 없는 값은 그리지 않는다', () => {
@@ -146,7 +149,7 @@ test('치는 동안의 값은 사본에 그려지고 원본은 그대로다 —
146
149
  })
147
150
 
148
151
  test('다각 단면에는 속 공간을 만들 수 없다고 그 자리에서 말한다', () => {
149
- assert.match(panel, /kind === 'rectangle' \|\| kind === 'ellipse'\s*\? this\.renderCavity\(body, e\.axis\)\s*: html`[\s\S]*?figure\.text\.section-cavity-unavailable/)
152
+ has(panel, /kind === 'rectangle' \|\| kind === 'ellipse'\s*\? this\.renderCavity\(body, e\.axis\)\s*: html`[\s\S]*?figure\.text\.section-cavity-unavailable/)
150
153
  assert.match(ko['figure.text.section-cavity-unavailable']!, /이 윤곽에는 속 공간을 만들 수 없습니다/)
151
154
  })
152
155
 
@@ -159,8 +162,8 @@ test('붙어 있는 것이 기대는 면을 없애는 편집은 거절되고,
159
162
  })
160
163
 
161
164
  test('치는 동안 그려진 도형이 칸의 글자를 덮어쓰지 않는다 — 친 것은 보낼 때까지 칸이 든다', () => {
162
- assert.match(panel, /\.value=\$\{live\(this\.typed\[key\] \?\? shown\)\}/)
163
- assert.match(panel, /this\.typed = \{ \.\.\.this\.typed, \[key\]: \(ev\.target as HTMLInputElement\)\.value \}/)
165
+ has(panel, /\.value=\$\{live\(this\.typed\[key\] \?\? shown\)\}/)
166
+ has(panel, /this\.typed = \{ \.\.\.this\.typed, \[key\]: \(ev\.target as HTMLInputElement\)\.value \}/)
164
167
  })
165
168
 
166
169
  test('면마다 벽 두께를 치는 동안에도 그려진다 — 명령이 없앤 입력(한 벽 두께)의 지금 값은 미리보기를 막지 않는다', async () => {
@@ -188,5 +191,5 @@ test('한 면의 벽 한도는 그 방향의 너비에서 맞은편 벽을 뺀
188
191
  /* The kernel agrees: just under takes, the limit refuses. */
189
192
  assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: { ...walls, 'x-': 359 } })), null)
190
193
  assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: { ...walls, 'x-': 360 } }))!.code, 'wall-too-thick')
191
- assert.match(panel, /sideWallLimitOf\(body\.shape, cavity\.wall, side\)/)
194
+ has(panel, /sideWallLimitOf\(body\.shape, cavity\.wall, side\)/)
192
195
  })
@@ -188,6 +188,40 @@
188
188
  "figure.label.palette-token": "palette token",
189
189
  "figure.label.part": "part",
190
190
  "figure.label.part-material": "material",
191
+ "figure.text.section-editor-hint-sized": "Drag a handle or type the measures.",
192
+ "figure.label.section-editor-length": "push length (mm)",
193
+ "figure.label.section-editor-across": "{axis} width (mm)",
194
+ "figure.label.section-editor-diameter": "diameter (mm)",
195
+ "figure.text.section-editor-linked": "Follows another value.",
196
+ "figure.text.section-editor-keeps": "keeps {face}",
197
+ "figure.text.section-editor-keeps-centre": "keeps the centre",
198
+ "figure.text.section-issue-corner": "At this width the corner radius can be at most {max} mm. Widen it or make the corner smaller.",
199
+ "figure.text.section-issue-length": "The push length must be more than 0.",
200
+ "figure.text.section-issue-cavity-walls": "The inside walls take the whole width. Widen it or make the walls thinner.",
201
+ "figure.text.section-issue-cavity-length": "The closed ends take the whole length. Lengthen it or make the ends thinner.",
202
+ "figure.button.section-editor-open": "edit section",
203
+ "figure.label.section-editor": "edit section",
204
+ "figure.label.section-editor-outline": "outline",
205
+ "figure.label.section-editor-solid": "solid",
206
+ "figure.button.section-editor-back": "undo",
207
+ "figure.text.section-editor-hint": "Drag a point or type its coordinates. Press a side to add a point in its middle.",
208
+ "figure.text.section-editor-stale": "The figure changed after this was opened. Close it and open it again.",
209
+ "figure.button.section-editor-cancel": "cancel",
210
+ "figure.button.section-editor-apply": "apply",
211
+ "figure.text.section-editor-last-valid": "The solid shows the last valid shape.",
212
+ "figure.text.section-editor-units": "mm · from the origin · {h} right, {v} down",
213
+ "figure.text.section-editor-add-here": "add a point in the middle of side {a}–{b}",
214
+ "figure.label.section-editor-points": "points",
215
+ "figure.label.section-editor-coordinate": "point {point} {axis}",
216
+ "figure.button.section-editor-remove": "delete {point}",
217
+ "figure.text.section-issue-cross": "Sides {a} and {b} cross. Move the marked points.",
218
+ "figure.text.section-issue-touch": "Sides {a} and {b} touch. Move the marked points.",
219
+ "figure.text.section-issue-fold": "Side {b} turns back over side {a}. Move the marked points.",
220
+ "figure.text.section-issue-zero-length": "The two points of side {a} are in one place. Move one or delete it.",
221
+ "figure.text.section-issue-zero-area": "The points lie on one line and enclose nothing.",
222
+ "figure.text.section-issue-too-few": "A section needs three points or more.",
223
+ "figure.text.section-issue-too-many": "A section can have up to {limit} points.",
224
+ "figure.text.section-issue-number": "Type a number.",
191
225
  "figure.text.section-cavity-unavailable": "This outline cannot have an inside. An inside is made in a rectangle or circle section.",
192
226
  "figure.text.section-corner-over": "The corner radius can be at most {max} mm.",
193
227
  "figure.text.section-at-most": "at most {max} mm",
@@ -188,6 +188,40 @@
188
188
  "figure.label.palette-token": "palette token",
189
189
  "figure.label.part": "part",
190
190
  "figure.label.part-material": "material",
191
+ "figure.text.section-editor-hint-sized": "ハンドルをドラッグするか寸法を入力します。",
192
+ "figure.label.section-editor-length": "押し出し長さ (mm)",
193
+ "figure.label.section-editor-across": "{axis} 幅 (mm)",
194
+ "figure.label.section-editor-diameter": "直径 (mm)",
195
+ "figure.text.section-editor-linked": "他の値に従います。",
196
+ "figure.text.section-editor-keeps": "{face}を保持",
197
+ "figure.text.section-editor-keeps-centre": "中心を保持",
198
+ "figure.text.section-issue-corner": "この幅では角の半径は {max} mm までです。幅を広げるか角を小さくしてください。",
199
+ "figure.text.section-issue-length": "押し出し長さは 0 より大きくしてください。",
200
+ "figure.text.section-issue-cavity-walls": "内部の壁が幅全体を占めています。幅を広げるか壁を薄くしてください。",
201
+ "figure.text.section-issue-cavity-length": "閉じた端が長さ全体を占めています。長さを伸ばすか端を薄くしてください。",
202
+ "figure.button.section-editor-open": "断面を編集",
203
+ "figure.label.section-editor": "断面を編集",
204
+ "figure.label.section-editor-outline": "輪郭",
205
+ "figure.label.section-editor-solid": "立体",
206
+ "figure.button.section-editor-back": "元に戻す",
207
+ "figure.text.section-editor-hint": "点をドラッグするか座標を入力します。辺を押すと中点に点が追加されます。",
208
+ "figure.text.section-editor-stale": "開いた後に図形が変わりました。閉じて開き直してください。",
209
+ "figure.button.section-editor-cancel": "キャンセル",
210
+ "figure.button.section-editor-apply": "適用",
211
+ "figure.text.section-editor-last-valid": "立体は最後に有効だった形です。",
212
+ "figure.text.section-editor-units": "mm · 原点基準 · 右 {h}、下 {v}",
213
+ "figure.text.section-editor-add-here": "辺 {a}–{b} の中点に点を追加",
214
+ "figure.label.section-editor-points": "点",
215
+ "figure.label.section-editor-coordinate": "点 {point} {axis}",
216
+ "figure.button.section-editor-remove": "{point} を削除",
217
+ "figure.text.section-issue-cross": "辺 {a} と辺 {b} が交差しています。示された点を動かしてください。",
218
+ "figure.text.section-issue-touch": "辺 {a} と辺 {b} が接しています。示された点を動かしてください。",
219
+ "figure.text.section-issue-fold": "辺 {b} が辺 {a} の上に折り返しています。示された点を動かしてください。",
220
+ "figure.text.section-issue-zero-length": "辺 {a} の二点が同じ位置にあります。一方を動かすか削除してください。",
221
+ "figure.text.section-issue-zero-area": "点が一直線上にあり面積がありません。",
222
+ "figure.text.section-issue-too-few": "断面には三点以上が必要です。",
223
+ "figure.text.section-issue-too-many": "点は {limit} 個までです。",
224
+ "figure.text.section-issue-number": "数値を入力してください。",
191
225
  "figure.text.section-cavity-unavailable": "この輪郭には内部の空間をつくれません。内部の空間は四角形と円の断面につくります。",
192
226
  "figure.text.section-corner-over": "角の半径は {max} mm までです。",
193
227
  "figure.text.section-at-most": "最大 {max} mm",
@@ -188,6 +188,40 @@
188
188
  "figure.label.palette-token": "팔레트 토큰",
189
189
  "figure.label.part": "부품",
190
190
  "figure.label.part-material": "재질",
191
+ "figure.text.section-editor-hint-sized": "손잡이를 끌거나 치수를 입력합니다.",
192
+ "figure.label.section-editor-length": "돌출 길이 (mm)",
193
+ "figure.label.section-editor-across": "{axis} 폭 (mm)",
194
+ "figure.label.section-editor-diameter": "지름 (mm)",
195
+ "figure.text.section-editor-linked": "다른 값을 따릅니다.",
196
+ "figure.text.section-editor-keeps": "{face} 유지",
197
+ "figure.text.section-editor-keeps-centre": "가운데 유지",
198
+ "figure.text.section-issue-corner": "이 폭에서는 모서리 반지름이 {max} mm 까지입니다. 폭을 넓히거나 모서리를 줄입니다.",
199
+ "figure.text.section-issue-length": "돌출 길이는 0 보다 커야 합니다.",
200
+ "figure.text.section-issue-cavity-walls": "속 공간의 벽이 폭을 다 차지합니다. 폭을 넓히거나 벽을 얇게 합니다.",
201
+ "figure.text.section-issue-cavity-length": "막힌 끝이 길이를 다 차지합니다. 길이를 늘리거나 끝을 얇게 합니다.",
202
+ "figure.button.section-editor-open": "단면 편집",
203
+ "figure.label.section-editor": "단면 편집",
204
+ "figure.label.section-editor-outline": "윤곽",
205
+ "figure.label.section-editor-solid": "입체 결과",
206
+ "figure.button.section-editor-back": "되돌리기",
207
+ "figure.text.section-editor-hint": "점을 끌거나 좌표를 입력합니다. 선을 누르면 가운데에 점이 생깁니다.",
208
+ "figure.text.section-editor-stale": "창을 연 뒤 도형이 바뀌었습니다. 닫고 다시 여세요.",
209
+ "figure.button.section-editor-cancel": "취소",
210
+ "figure.button.section-editor-apply": "적용",
211
+ "figure.text.section-editor-last-valid": "입체 결과는 마지막으로 유효했던 모양입니다.",
212
+ "figure.text.section-editor-units": "mm · 원점 기준 · 오른쪽 {h}, 아래 {v}",
213
+ "figure.text.section-editor-add-here": "{a}–{b} 선 가운데에 점 추가",
214
+ "figure.label.section-editor-points": "점",
215
+ "figure.label.section-editor-coordinate": "점 {point} {axis}",
216
+ "figure.button.section-editor-remove": "{point} 지우기",
217
+ "figure.text.section-issue-cross": "{a} 선과 {b} 선이 교차합니다. 표시된 점을 옮겨 주세요.",
218
+ "figure.text.section-issue-touch": "{a} 선과 {b} 선이 서로 닿습니다. 표시된 점을 옮겨 주세요.",
219
+ "figure.text.section-issue-fold": "{b} 선이 {a} 선 위로 되돌아갑니다. 표시된 점을 옮겨 주세요.",
220
+ "figure.text.section-issue-zero-length": "{a} 선의 두 점이 같은 자리에 있습니다. 한 점을 옮기거나 지워 주세요.",
221
+ "figure.text.section-issue-zero-area": "점들이 한 줄에 있어 면적이 없습니다.",
222
+ "figure.text.section-issue-too-few": "점이 세 개보다 적으면 단면이 되지 않습니다.",
223
+ "figure.text.section-issue-too-many": "점은 {limit}개까지 둘 수 있습니다.",
224
+ "figure.text.section-issue-number": "숫자를 입력해 주세요.",
191
225
  "figure.text.section-cavity-unavailable": "이 윤곽에는 속 공간을 만들 수 없습니다. 속 공간은 사각형과 원 단면에 만듭니다.",
192
226
  "figure.text.section-corner-over": "모서리 반지름은 {max} mm 까지입니다.",
193
227
  "figure.text.section-at-most": "최대 {max} mm",
@@ -188,6 +188,40 @@
188
188
  "figure.label.palette-token": "palette token",
189
189
  "figure.label.part": "part",
190
190
  "figure.label.part-material": "material",
191
+ "figure.text.section-editor-hint-sized": "Seret pemegang atau taip ukurannya.",
192
+ "figure.label.section-editor-length": "panjang tolakan (mm)",
193
+ "figure.label.section-editor-across": "lebar {axis} (mm)",
194
+ "figure.label.section-editor-diameter": "diameter (mm)",
195
+ "figure.text.section-editor-linked": "Mengikut nilai lain.",
196
+ "figure.text.section-editor-keeps": "kekalkan {face}",
197
+ "figure.text.section-editor-keeps-centre": "kekalkan tengah",
198
+ "figure.text.section-issue-corner": "Pada lebar ini jejari sudut paling banyak {max} mm. Lebarkan atau kecilkan sudut.",
199
+ "figure.text.section-issue-length": "Panjang tolakan mesti lebih daripada 0.",
200
+ "figure.text.section-issue-cavity-walls": "Dinding dalam memenuhi seluruh lebar. Lebarkan atau nipiskan dinding.",
201
+ "figure.text.section-issue-cavity-length": "Hujung tertutup memenuhi seluruh panjang. Panjangkan atau nipiskan hujung.",
202
+ "figure.button.section-editor-open": "sunting keratan",
203
+ "figure.label.section-editor": "sunting keratan",
204
+ "figure.label.section-editor-outline": "garis luar",
205
+ "figure.label.section-editor-solid": "pepejal",
206
+ "figure.button.section-editor-back": "buat asal",
207
+ "figure.text.section-editor-hint": "Seret titik atau taip koordinatnya. Tekan sisi untuk menambah titik di tengahnya.",
208
+ "figure.text.section-editor-stale": "Rajah berubah selepas ini dibuka. Tutup dan buka semula.",
209
+ "figure.button.section-editor-cancel": "batal",
210
+ "figure.button.section-editor-apply": "guna",
211
+ "figure.text.section-editor-last-valid": "Pepejal menunjukkan bentuk sah yang terakhir.",
212
+ "figure.text.section-editor-units": "mm · dari asalan · {h} ke kanan, {v} ke bawah",
213
+ "figure.text.section-editor-add-here": "tambah titik di tengah sisi {a}–{b}",
214
+ "figure.label.section-editor-points": "titik",
215
+ "figure.label.section-editor-coordinate": "titik {point} {axis}",
216
+ "figure.button.section-editor-remove": "padam {point}",
217
+ "figure.text.section-issue-cross": "Sisi {a} dan {b} bersilang. Alihkan titik yang ditanda.",
218
+ "figure.text.section-issue-touch": "Sisi {a} dan {b} bersentuhan. Alihkan titik yang ditanda.",
219
+ "figure.text.section-issue-fold": "Sisi {b} berpatah balik di atas sisi {a}. Alihkan titik yang ditanda.",
220
+ "figure.text.section-issue-zero-length": "Dua titik sisi {a} berada di tempat yang sama. Alihkan atau padam satu.",
221
+ "figure.text.section-issue-zero-area": "Titik-titik berada pada satu garis dan tiada luas.",
222
+ "figure.text.section-issue-too-few": "Keratan memerlukan tiga titik atau lebih.",
223
+ "figure.text.section-issue-too-many": "Keratan boleh mempunyai sehingga {limit} titik.",
224
+ "figure.text.section-issue-number": "Taip nombor.",
191
225
  "figure.text.section-cavity-unavailable": "Garis luar ini tidak boleh mempunyai ruang dalam. Ruang dalam dibuat dalam keratan segi empat atau bulatan.",
192
226
  "figure.text.section-corner-over": "Jejari sudut paling banyak {max} mm.",
193
227
  "figure.text.section-at-most": "paling banyak {max} mm",
@@ -188,6 +188,40 @@
188
188
  "figure.label.palette-token": "palette token",
189
189
  "figure.label.part": "part",
190
190
  "figure.label.part-material": "material",
191
+ "figure.text.section-editor-hint-sized": "拖动手柄或输入尺寸。",
192
+ "figure.label.section-editor-length": "拉伸长度 (mm)",
193
+ "figure.label.section-editor-across": "{axis} 宽度 (mm)",
194
+ "figure.label.section-editor-diameter": "直径 (mm)",
195
+ "figure.text.section-editor-linked": "跟随其他值。",
196
+ "figure.text.section-editor-keeps": "保持{face}",
197
+ "figure.text.section-editor-keeps-centre": "保持中心",
198
+ "figure.text.section-issue-corner": "此宽度下圆角半径最大为 {max} mm。请加宽或减小圆角。",
199
+ "figure.text.section-issue-length": "拉伸长度必须大于 0。",
200
+ "figure.text.section-issue-cavity-walls": "内壁占满了整个宽度。请加宽或减薄壁。",
201
+ "figure.text.section-issue-cavity-length": "封闭端占满了整个长度。请加长或减薄端部。",
202
+ "figure.button.section-editor-open": "编辑截面",
203
+ "figure.label.section-editor": "编辑截面",
204
+ "figure.label.section-editor-outline": "轮廓",
205
+ "figure.label.section-editor-solid": "立体",
206
+ "figure.button.section-editor-back": "撤销",
207
+ "figure.text.section-editor-hint": "拖动点或输入坐标。点击边可在其中点添加点。",
208
+ "figure.text.section-editor-stale": "打开后图形已更改。请关闭后重新打开。",
209
+ "figure.button.section-editor-cancel": "取消",
210
+ "figure.button.section-editor-apply": "应用",
211
+ "figure.text.section-editor-last-valid": "立体显示的是最后一个有效形状。",
212
+ "figure.text.section-editor-units": "mm · 以原点为准 · 右 {h},下 {v}",
213
+ "figure.text.section-editor-add-here": "在边 {a}–{b} 的中点添加点",
214
+ "figure.label.section-editor-points": "点",
215
+ "figure.label.section-editor-coordinate": "点 {point} {axis}",
216
+ "figure.button.section-editor-remove": "删除 {point}",
217
+ "figure.text.section-issue-cross": "边 {a} 与边 {b} 相交。请移动标出的点。",
218
+ "figure.text.section-issue-touch": "边 {a} 与边 {b} 相接。请移动标出的点。",
219
+ "figure.text.section-issue-fold": "边 {b} 折回到边 {a} 上。请移动标出的点。",
220
+ "figure.text.section-issue-zero-length": "边 {a} 的两个点在同一位置。请移动或删除其中一个。",
221
+ "figure.text.section-issue-zero-area": "点在一条直线上,没有面积。",
222
+ "figure.text.section-issue-too-few": "截面至少需要三个点。",
223
+ "figure.text.section-issue-too-many": "最多可有 {limit} 个点。",
224
+ "figure.text.section-issue-number": "请输入数字。",
191
225
  "figure.text.section-cavity-unavailable": "此轮廓不能创建内部空间。内部空间只能在矩形和圆形截面中创建。",
192
226
  "figure.text.section-corner-over": "圆角半径最大为 {max} mm。",
193
227
  "figure.text.section-at-most": "最大 {max} mm",