@things-factory/figure-ui 10.1.104 → 10.1.106

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/modeller/canvas.ts +6 -13
  2. package/client/modeller/editor.ts +15 -4
  3. package/client/modeller/fields.ts +7 -1
  4. package/client/modeller/figure-cavity-drawing.ts +80 -18
  5. package/client/modeller/figure-links.ts +49 -35
  6. package/client/modeller/figure-material-panel.ts +0 -1
  7. package/client/modeller/figure-parts.ts +3 -3
  8. package/client/modeller/figure-section-editor.ts +106 -11
  9. package/client/modeller/figure-section-panel.ts +15 -103
  10. package/client/modeller/section-drawing.ts +27 -4
  11. package/dist-client/modeller/canvas.d.ts +0 -2
  12. package/dist-client/modeller/canvas.js +6 -17
  13. package/dist-client/modeller/canvas.js.map +1 -1
  14. package/dist-client/modeller/editor.js +17 -4
  15. package/dist-client/modeller/editor.js.map +1 -1
  16. package/dist-client/modeller/fields.js +7 -1
  17. package/dist-client/modeller/fields.js.map +1 -1
  18. package/dist-client/modeller/figure-cavity-drawing.d.ts +6 -0
  19. package/dist-client/modeller/figure-cavity-drawing.js +81 -16
  20. package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
  21. package/dist-client/modeller/figure-links.d.ts +2 -2
  22. package/dist-client/modeller/figure-links.js +49 -34
  23. package/dist-client/modeller/figure-links.js.map +1 -1
  24. package/dist-client/modeller/figure-material-panel.js +0 -1
  25. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  26. package/dist-client/modeller/figure-parts.d.ts +2 -1
  27. package/dist-client/modeller/figure-parts.js +2 -2
  28. package/dist-client/modeller/figure-parts.js.map +1 -1
  29. package/dist-client/modeller/figure-section-editor.d.ts +13 -0
  30. package/dist-client/modeller/figure-section-editor.js +109 -9
  31. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  32. package/dist-client/modeller/figure-section-panel.d.ts +0 -1
  33. package/dist-client/modeller/figure-section-panel.js +14 -73
  34. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  35. package/dist-client/modeller/section-drawing.d.ts +12 -1
  36. package/dist-client/modeller/section-drawing.js +23 -3
  37. package/dist-client/modeller/section-drawing.js.map +1 -1
  38. package/dist-client/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +6 -6
  40. package/test/cavity-drawing.test.ts +10 -1
  41. package/test/command-preview.test.ts +11 -4
  42. package/test/first-minute.test.ts +6 -2
  43. package/test/layout.test.ts +11 -0
  44. package/test/section-editor.test.ts +32 -3
  45. package/test/section-panel.test.ts +5 -3
  46. package/translations/en.json +2 -10
  47. package/translations/ja.json +2 -10
  48. package/translations/ko.json +2 -10
  49. package/translations/ms.json +2 -10
  50. package/translations/zh.json +2 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.104",
3
+ "version": "10.1.106",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -27,8 +27,8 @@
27
27
  "clean": "npm run clean:server && npm run clean:client"
28
28
  },
29
29
  "dependencies": {
30
- "@hatiolab/figure-model": "^0.2.4",
31
- "@hatiolab/things-scene": "^10.1.64",
30
+ "@hatiolab/figure-model": "^0.2.7",
31
+ "@hatiolab/things-scene": "^10.1.65",
32
32
  "@material/web": "^2.0.0",
33
33
  "@operato/data-grist": "^10.0.0",
34
34
  "@operato/graphql": "^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.104",
44
- "@things-factory/figure-service": "^10.1.84",
43
+ "@things-factory/ai-assistant": "^10.1.106",
44
+ "@things-factory/figure-service": "^10.1.106",
45
45
  "three": "^0.185.1"
46
46
  },
47
- "gitHead": "0d05c7573220c6da2dec2654a4b86d7bd8e3c4fb"
47
+ "gitHead": "8d78c14708420773ec32d18b8dd44c300771c44d"
48
48
  }
@@ -61,9 +61,18 @@ test('커널이 거절할 모양은 보내지 않고 까닭을 말한다 — 끄
61
61
  })
62
62
 
63
63
  test('단면 칸에 그림이 들어간다: 속 공간이 있을 때, 면마다 벽 토글을 따른다', () => {
64
- has(panel, /cavity \? html`<figure-cavity-drawing \.body=\$\{body\} \.perSide=\$\{this\.perSide\} \?locked=\$\{this\.locked\}><\/figure-cavity-drawing>` : nothing/)
64
+ has(panel, /\? html`<figure-cavity-drawing \.body=\$\{body\} \.perSide=\$\{this\.perSide\} \?locked=\$\{this\.locked\}><\/figure-cavity-drawing>/)
65
+ /* The number fields under the drawing went: a wall's number or a closed end's is pressed and typed in the drawing. */
66
+ assert.ok(!panel.includes("cavityField(") && !panel.includes('renderEnd('), 'no number fields repeating the drawing')
67
+ has(drawing, /@pointerdown=\$\{\(ev: PointerEvent\) => this\.type\(ev, \{ kind: 'wall', side \}, 'top', w\.wall\[side\]!\)\}/)
68
+ has(drawing, /!open && this\.type\(ev, \{ kind: 'end', end \}, 'side'/)
65
69
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
66
70
  const t = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
67
71
  for (const key of ['figure.label.section-cavity-top', 'figure.label.section-cavity-side']) assert.ok(t[key], `${lang} ${key}`)
68
72
  }
69
73
  })
74
+
75
+ test('맞은편 벽의 숫자와 손잡이가 겹치지 않는다: 숫자는 단면 바깥, 좁은 속에서는 손잡이가 비킨다', () => {
76
+ has(drawing, /const tx = a === h \? ox \+ sign \* \(half\[a\] \+ 5\) : ox,\s*ty = a === v \? oy \+ sign \* \(half\[a\] \+ \(low \? 5 : 13\)\) : oy \+ 4/, 'past the outer edge on its own side')
77
+ has(drawing, /const aside = room < 16 \? sign \* 9 : 0/, 'two handles close together step apart')
78
+ })
@@ -86,13 +86,16 @@ test('the editor shows it only for the figure, version and part it was worked ou
86
86
  assert.ok(links.includes('<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>'), 'the button applies the command the preview showed')
87
87
  })
88
88
 
89
- test('the details fold, and what they hold that is not the default stays said', () => {
89
+ test('the details fold, and what they hold that is not the default stays said -- nothing for the default', () => {
90
90
  const links = read('../client/modeller/figure-links.ts')
91
91
  assert.ok(links.includes('<details fasten-detail>'))
92
- assert.ok(links.includes("detail.length ? detail.join(' · ') : i18next.t('figure.text.fasten-summary-default')"))
92
+ assert.ok(links.includes("${detail.length ? html` · <span fasten-summary>${detail.join(' · ')}</span>` : nothing}"))
93
+ /* The way and the place on the face are the drawing's: not said again in the summary, nor chosen again in the details. */
94
+ assert.ok(!links.includes('fasten-summary-edge') && !links.includes('fasten-summary-off-centre') && !links.includes("'figure.label.facing-flush'"))
95
+ assert.ok(links.includes('({ axis }) => !view || !view.axes.includes(axis) || !!f.alignInput?.[axis] || hasInstanceSize(asset, axis)'), 'a row only where the drawing cannot set it')
93
96
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh']) {
94
97
  const t = JSON.parse(read(`../translations/${lang}.json`))
95
- for (const key of ['preview-refused', 'preview-stays', 'preview-moves', 'preview-turns-named', 'preview-turns-slanted', 'preview-carries', 'preview-copies', 'preview-clips', 'preview-face-unknown', 'target-would-loop', 'fasten-summary-default', 'fasten-summary-gap', 'fasten-summary-twist', 'fasten-summary-off-centre', 'fasten-summary-edge-min', 'fasten-summary-edge-max', 'fasten-summary-follows'])
98
+ for (const key of ['preview-refused', 'preview-stays', 'preview-moves', 'preview-turns-named', 'preview-turns-slanted', 'preview-carries', 'preview-copies', 'preview-clips', 'preview-face-unknown', 'target-would-loop', 'fasten-summary-gap', 'fasten-summary-twist'])
96
99
  assert.ok(t[`figure.text.${key}`], `${lang} ${key}`)
97
100
  assert.ok(t['figure.label.fasten-detail'] && t['figure.label.facing'], lang)
98
101
  }
@@ -153,6 +156,9 @@ test('a row is seen before it is made, read as figure-model draws it -- and said
153
156
  assert.deepEqual(rowWordsOf(three, say), ['roller 3개 · X 방향 · pitch 970 mm (중심 간격)', '가운데 정렬 · 양 끝에 0 mm씩 남음'])
154
157
  const links = read('../client/modeller/figure-links.ts')
155
158
  assert.ok(links.includes("<p hint row-blocked>${i18next.t(`figure.text.row-blocked-${blocked ?? 'unfastened'}`, { part: this.partId })}</p>"), 'not hidden: why')
159
+ /* Folded, its head the section's: opened, it says why; open by itself only while it is worked. */
160
+ assert.ok(links.includes("const head = html`<summary><h3>${i18next.t('figure.label.repeat')}</h3></summary>`"))
161
+ assert.ok(links.includes("<section row><details row-fold>") && links.includes("<details row-fold ?open=${this.working === 'row'}>"))
156
162
  assert.ok(links.includes("this.candidate = this.working === 'row' && this.rowCandidate ? { action: this.rowCandidate } : this.fastenCandidate"), 'the form being worked on is the one seen')
157
163
  const editor = read('../client/modeller/editor.ts')
158
164
  assert.ok(editor.includes("const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, this.selected, this.currentInputs()) : undefined"))
@@ -231,7 +237,8 @@ test('past the edge of its face is said beside the fastening, with the way to ch
231
237
  assert.ok(editor.includes("else if (own?.state === 'possible') words.push(say('figure.text.overhang-possible', where(own)))"))
232
238
  assert.ok(editor.includes("else if (own?.state === 'unknown') words.push(say('figure.text.overhang-unknown', where(own)))"))
233
239
  const links = read('../client/modeller/figure-links.ts')
234
- assert.ok(links.includes("<button act @click=${() => this.openDetail()}><md-icon>tune</md-icon>${i18next.t('figure.button.fix-alignment')}</button>"))
240
+ assert.ok(links.includes("<button act @click=${() => this.showWhere()}><md-icon>tune</md-icon>${i18next.t('figure.button.fix-alignment')}</button>"))
241
+ assert.ok(links.includes("shadowRoot?.querySelector<HTMLElement>('div[where]')"), 'to the face in the drawing, where the place is dragged or keyed')
235
242
  assert.equal(say('figure.text.overhang-out', { on: 'top', face: '윗면', sides: say('figure.text.overhang-side-high', { axis: 'X', mm: '100' }) }), 'top의 윗면 가장자리 밖으로 나와 있습니다: X 높은 쪽 100 mm. 모델의 점유 공간과는 따로 봅니다.')
236
243
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh']) {
237
244
  const t = JSON.parse(read(`../translations/${lang}.json`))
@@ -98,8 +98,12 @@ test('the fastening form says whether it edits the current relation or fastens a
98
98
  /* Anew is said; the one it has is said once, by the sentence over the drawing (user 2026-10-01: no third line). */
99
99
  assert.ok(links.includes("${current ? nothing : html`<p fasten-mode>${editing}</p>`}"))
100
100
  assert.ok(links.includes('<p fasten-sentence>'))
101
- assert.ok(links.includes("i18next.t(current ? 'figure.button.fasten-update' : 'figure.button.fasten')"))
101
+ /* A fastening the part has is changed as soon as something is chosen (2026-10-02): 붙이기 only for a new one. */
102
+ assert.ok(links.includes("${held\n ? nothing\n : html`<button act ?disabled=${this.locked || !action} @click=${() => action && this.command(action)}>"))
103
+ assert.ok(links.includes('if (value) settle()'), 'another target taken at once')
104
+ assert.ok(links.includes("action && !held && this.working === 'fasten' ? this.renderPreviewWords() : nothing"), 'no words for a command no button sends')
105
+ assert.ok(links.includes('const settle = () => this.onFastenRelease(!!held)'))
102
106
  const ko = JSON.parse(read('../translations/ko.json'))
103
107
  assert.equal(ko['figure.text.fasten-new'], '새로 붙이기')
104
- assert.equal(ko['figure.button.fasten-update'], '이 붙임 수정')
108
+ assert.equal(ko['figure.button.fasten-update'], undefined)
105
109
  })
@@ -97,3 +97,14 @@ test('맞추기는 글자 단추로 — 옮기는 넷(가운데 X·Z, 면에 대
97
97
  )
98
98
  assert.ok(!ko['figure.text.center-on-x']!.includes('기준 상자'), 'says what the centre is')
99
99
  })
100
+
101
+ test('불러오는 동안 빈 무대에 「도형을 추가해…」가 깜빡이지 않고, 좁은 화면 알림은 이 브라우저에서 한 번', () => {
102
+ const canvas = read('../client/modeller/canvas.ts')
103
+ /* A new figure's start card says how to begin; an empty stage otherwise is one loading, or with every part hidden. */
104
+ assert.ok(canvas.includes('return html`<div stage></div>`'))
105
+ assert.ok(!canvas.includes('add-a-shape-to-start') && !editor.includes('startShown'))
106
+ for (const lang of ['ko', 'en', 'ja', 'zh', 'ms'])
107
+ assert.equal((JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>)['figure.text.add-a-shape-to-start'], undefined, lang)
108
+ assert.ok(editor.includes("if (!foldSaid()) notify({ level: 'info', message: i18next.t('figure.text.parts-panel-folded-narrow') })"))
109
+ assert.ok(/function foldSaid\(\): boolean \{\s*try \{\s*if \(localStorage\.getItem\('figure-fold-said'\)\) return true\s*localStorage\.setItem\('figure-fold-said', '1'\)\s*\} catch/.test(editor), 'storage refused: said, as before')
110
+ })
@@ -111,7 +111,8 @@ test('치는 동안 입체는 그 점으로 그려지고, 잘못된 동안은
111
111
  const body = drawn.find(p => p.id === 'wedge') as unknown as { shape: FigureEvaluatedShape }
112
112
  assert.equal((body.shape.extrusion!.section as { points: unknown[] }).points.length, 4)
113
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')
114
+ has(editorPanel, /private preview\(w = this\.lastValid\) \{\s*this\.dispatchEvent\(new CustomEvent\('command-live', \{ detail: \{ action: this\.action\(w\) \}/, 'the preview is always of a valid shape')
115
+ has(editorPanel, /this\.preview\(c && this\.valid\(\{ \.\.\.this\.working, cavity: c \}\) \? \{ \.\.\.this\.working, cavity: c \} : this\.lastValid\)/, 'the one other: the inside being dragged, when valid')
115
116
  has(editorPanel, /figure\.text\.section-editor-last-valid/)
116
117
  })
117
118
 
@@ -181,7 +182,9 @@ test('편집 창이 열린 동안 모델러의 키는 비켜선다 — Esc 가
181
182
  test('속성의 형상 치수에는 점·꼭짓점 칸이 없다 (사용자 10-01) — 높이만 남고, 단면 칸이 점 개수를 말한다', async () => {
182
183
  const { groupFields } = await import('../client/modeller/fields.ts')
183
184
  const f = (id: string) => ({ id, value: 0, unit: 'mm' })
184
- const ids = ['tip.dimension.height', 'tip.point.p1.x', 'tip.point.p1.z', 'tip.point.p12.x', 'tip.apex.x', 'tip.apex.z', 'tip.position.x']
185
+ const ids = ['tip.dimension.height', 'tip.point.p1.x', 'tip.point.p1.z', 'tip.point.p12.x', 'tip.apex.x', 'tip.apex.z', 'tip.position.x',
186
+ /* A tote's corners and inside are the 단면 group's too, not the shape's list under inner names. */
187
+ 'tip.dimension.round', 'tip.dimension.wall', 'tip.dimension.closed.start', 'tip.dimension.wallX', 'tip.corner.x', 'tip.wall.x-']
185
188
  const { singles } = groupFields(ids.map(f) as never, 'tip')
186
189
  assert.deepEqual(singles.map(g => (g as { field: { id: string } }).field.id), ['tip.dimension.height'])
187
190
  has(sectionPanel, /e\.apex \? 'figure\.text\.section-points-apex' : 'figure\.text\.section-points', \{ points: String\(e\.section\.points\.length\) \}/)
@@ -202,7 +205,7 @@ test('사각형·원도 같은 창에서 — 폭·지름·길이를 한 명령
202
205
  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' })
203
206
  const shape = (compileFigureAsset(box).evaluate().geometry.find(g => (g as { id: string }).id === 'b') as unknown as { shape: FigureEvaluatedShape }).shape
204
207
  const w = workingOf(shape)
205
- assert.deepEqual(w, { points: [], across: { x: 400, z: 300 }, length: 200, apex: null })
208
+ assert.deepEqual(w, { points: [], across: { x: 400, z: 300 }, length: 200, apex: null, cavity: null })
206
209
  assert.deepEqual(sectionOf(shape, { ...w, across: { x: 500, z: 300 } }), { kind: 'rectangle', size: { x: 500, z: 300 } })
207
210
  assert.deepEqual(figureSectionMeasuresOf(box, 'b')!.measures, { y: 'editable', x: 'editable', z: 'editable' })
208
211
  /* The editor sends only what changed: a measure left alone may follow another value and must not be written. */
@@ -229,6 +232,32 @@ test('속 공간이 있으면 폭·길이가 그 벽과 막힌 끝을 남기는
229
232
  assert.deepEqual(workingIssuesOf(shape, { ...workingOf(shape), across: { x: 90, z: 300 } }).words, [])
230
233
  })
231
234
 
235
+ test('창 안에서 폭과 속 공간을 함께 — 넓힌 폭에서만 들어가는 벽이 한 명령으로 들어가고, 그림은 고치는 중인 폭으로 판정한다', async () => {
236
+ const { addFigureBox } = await import('@hatiolab/figure-model')
237
+ const { workingOf, workingIssuesOf, workingShapeOf } = await import('../client/modeller/section-drawing.ts')
238
+ const { cavityRefusalOf } = await import('../client/modeller/section-controls.ts')
239
+ 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' } })
240
+ const shape = (compileFigureAsset(tray).evaluate().geometry.find(g => (g as { id: string }).id === 't') as unknown as { shape: FigureEvaluatedShape }).shape
241
+ const w = workingOf(shape)
242
+ assert.deepEqual(w.cavity, { wall: { 'x-': 40, 'x+': 40, 'z-': 40, 'z+': 40 }, start: { closed: 30 }, end: 'open' })
243
+ /* 160 mm walls leave no room in 300 deep; widened to 600 × 400 they do. */
244
+ const thick = { ...w.cavity!, wall: { 'x-': 160, 'x+': 160, 'z-': 160, 'z+': 160 } }
245
+ assert.equal(cavityRefusalOf(shape, thick)?.code, 'wall-too-thick', 'judged on the opened widths: refused')
246
+ const wide = { ...w, across: { x: 600, z: 400 }, cavity: thick }
247
+ assert.equal(cavityRefusalOf(workingShapeOf(shape, wide), thick), null, 'judged on the widths being typed: taken')
248
+ assert.deepEqual(workingIssuesOf(shape, wide).words, [])
249
+ assert.equal(workingIssuesOf(shape, { ...wide, across: { x: 400, z: 300 } }).words[0]!.key, 'figure.text.section-issue-cavity-walls')
250
+ assert.deepEqual(workingIssuesOf(shape, { ...w, across: { x: 70, z: 300 }, cavity: null }).words, [], 'no inside, no walls to fit')
251
+ const next = applyFigureAuthoring(tray, { kind: 'set-section', part: 't', across: { x: 600, z: 400 }, cavity: thick })
252
+ const t = (compileFigureAsset(next).evaluate().geometry.find(g => (g as { id: string }).id === 't') as unknown as { shape: FigureEvaluatedShape }).shape
253
+ assert.deepEqual(t.cavity!.wall, thick.wall)
254
+ /* The drawing in the editor is the panel's, its commands kept in the editor as working state, sent with the widths. */
255
+ has(editorPanel, /<figure-cavity-drawing\s+\.body=\$\{\{ id: this\.body!\.id, shape \}\}/)
256
+ has(editorPanel, /@command=\$\{\(ev: CustomEvent\) => \{\s*ev\.stopPropagation\(\)\s*const c = this\.cavityOf\(ev\.detail\.action\)\s*if \(c\) this\.change\(\{ \.\.\.this\.working, cavity: c \}\)/)
257
+ has(editorPanel, /\.\.\.\(cavityMoved \? \{ cavity: w\.cavity \? \{ wall: \{ \.\.\.w\.cavity\.wall \}, start: w\.cavity\.start, end: w\.cavity\.end \} : null \} : \{\}\)/, 'only when changed')
258
+ has(editorPanel, /private get hollowable\(\) \{\s*return !!this\.body\?\.shape\.extrusion && \(this\.kind === 'rectangle' \|\| this\.kind === 'ellipse'\)/, 'no inside for a drawn section')
259
+ })
260
+
232
261
  test('길이를 바꿀 때 남는 면은 부품의 관계가 정한 것을 읽어 보여 준다 (§4.1) — 다른 값을 따르는 치수는 칸을 막는다', () => {
233
262
  has(editor, /const kept = figureScaleAnchorOf\(asset, this\.selected\)\?\.\[drawn\.axis\]/)
234
263
  has(editorPanel, /m\?\.\[this\.axis\] === 'linked' \? linked : this\.keptWords\(\)/)
@@ -30,6 +30,8 @@ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.m
30
30
  const has = (source: string, re: RegExp, message?: string) => assert.ok(re.test(source), message ?? `missing: ${re}`)
31
31
  const hasNot = (source: string, re: RegExp, message?: string) => assert.ok(!re.test(source), message ?? `present: ${re}`)
32
32
  const panel = read('../client/modeller/figure-section-panel.ts')
33
+ /* The inside is drawn and typed in its own element (user 2026-10-01): its words and limits are read there. */
34
+ const drawing = read('../client/modeller/figure-cavity-drawing.ts')
33
35
  const editor = read('../client/modeller/editor.ts')
34
36
  const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
35
37
 
@@ -92,7 +94,7 @@ test('편집기는 단면 칸의 명령을 세션의 이력 하나로 돌리고,
92
94
  has(editor, /private linkCommand\(action: FigureAuthoringAction\) \{\s*\/\*[^*]*\*\/\s*this\.holdAction\(null\)/, 'a command sent lets go of the one in hand')
93
95
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
94
96
  const words = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
95
- const used = [...new Set([...panel.matchAll(/'(figure\.(?:label|text)\.section-[\w-]+|figure\.label\.group-section)'/g)].map(m => m[1]!))]
97
+ const used = [...new Set([...(panel + drawing).matchAll(/'(figure\.(?:label|text)\.section-[\w-]+|figure\.label\.group-section)'/g)].map(m => m[1]!))]
96
98
  /* The keys the panel builds: each result, and the wall's and the ends' over-limit words. */
97
99
  used.push(...['pipe', 'container', 'sealed'].map(r => `figure.label.section-result-${r}`), 'figure.text.section-wall-over', 'figure.text.section-ends-over')
98
100
  assert.ok(used.length >= 16)
@@ -150,7 +152,7 @@ test('치는 동안의 값은 사본에 그려지고 원본은 그대로다 —
150
152
 
151
153
  test('다각 단면에는 속 공간을 만들 수 없다고 그 자리에서 말한다', () => {
152
154
  /* A drawn section has no inside control at all, rather than a line saying it cannot have one (user 2026-10-01). */
153
- has(panel, /kind === 'rectangle' \|\| kind === 'ellipse' \? this\.renderCavity\(body, e\.axis\) : nothing/)
155
+ has(panel, /kind === 'rectangle' \|\| kind === 'ellipse' \? this\.renderCavity\(body\) : nothing/)
154
156
  })
155
157
 
156
158
  test('붙어 있는 것이 기대는 면을 없애는 편집은 거절되고, 그 면을 사람의 말로 부른다', () => {
@@ -191,5 +193,5 @@ test('한 면의 벽 한도는 그 방향의 너비에서 맞은편 벽을 뺀
191
193
  /* The kernel agrees: just under takes, the limit refuses. */
192
194
  assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: { ...walls, 'x-': 359 } })), null)
193
195
  assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: { ...walls, 'x-': 360 } }))!.code, 'wall-too-thick')
194
- has(panel, /sideWallLimitOf\(body\.shape, cavity\.wall, side\)/)
196
+ has(drawing, /this\.perSide && side \? mm\(sideWallLimitOf\(this\.body!\.shape, w\.wall, side\)\) : ask\.max/)
195
197
  })
@@ -116,6 +116,7 @@
116
116
  "figure.label.clips": "animation (plays over time)",
117
117
  "figure.label.coordinate-ground": "coordinate ground",
118
118
  "figure.label.detail-L": "detailed",
119
+ "figure.label.detail-XL": "fine",
119
120
  "figure.label.detail-M": "normal",
120
121
  "figure.label.detail-S": "simple",
121
122
  "figure.label.detail-level": "detail level",
@@ -398,7 +399,7 @@
398
399
  "figure.text.coordinate-ground-explained": "draws the origin and a grid on the floor",
399
400
  "figure.text.delete-figure-confirm": "delete {name}? drawings already using this type will no longer find it.",
400
401
  "figure.text.designate-in-properties": "nothing designated yet. select a part and set its role in the properties tab.",
401
- "figure.text.detail-level-explained": "S for signs and posts, M for most equipment, L for the large equipment a view centres on. the level sets the part limit.",
402
+ "figure.text.detail-level-explained": "S for signs and posts, M for most equipment, L for the large equipment a view centres on, XL for a mechanism of many joints such as a legged robot. the level sets the part limit.",
402
403
  "figure.text.discard-changes-and-start-new": "there are unsaved changes. discard them and start a new figure?",
403
404
  "figure.text.leave-without-saving": "there are unsaved changes. discard them and leave?",
404
405
  "figure.button.leave-without-saving": "discard and leave",
@@ -467,7 +468,6 @@
467
468
  "figure.text.nothing-released-yet": "nothing released yet.",
468
469
  "figure.text.on-by-default-explained": "the starting state when placed on a drawing. a value set on the instance takes priority.",
469
470
  "figure.text.other-segment-count": "enter a value",
470
- "figure.text.palette-purpose-explained": "choose the color role this part plays on screen.",
471
471
  "figure.text.part-limit-reached": "the detail level allows no more parts — raise the level or remove a part.",
472
472
  "figure.text.part-limit-reached-short": "level {level} allows {limit} parts. raise the level or remove a part.",
473
473
  "figure.text.part-name-cannot-be-empty": "give the part a name.",
@@ -535,7 +535,6 @@
535
535
  "figure.label.target-face": "target face",
536
536
  "figure.label.stands-on-plane": "stand on the mounting plane",
537
537
  "figure.label.facing-meet": "faces meet",
538
- "figure.label.facing-flush": "same plane",
539
538
  "figure.text.overhang-out": "It comes past the edge of {on}'s {face}: {sides}. This is apart from the figure's room.",
540
539
  "figure.text.overhang-larger": "Wider than {on}'s {face}, it comes past its edges: {sides}.",
541
540
  "figure.text.overhang-lopsided": "It sits off to one side along {axes}.",
@@ -561,11 +560,6 @@
561
560
  "figure.text.target-would-loop": "{part} — attached to {mine}, cannot be chosen",
562
561
  "figure.label.fasten-detail": "Details",
563
562
  "figure.label.facing": "How faces meet",
564
- "figure.text.fasten-summary-default": "Defaults (meeting · centred · gap 0 · twist 0)",
565
- "figure.text.fasten-summary-edge-min": "{axis} lower edge",
566
- "figure.text.fasten-summary-edge-max": "{axis} upper edge",
567
- "figure.text.fasten-summary-off-centre": "{axis} {mm} mm off centre",
568
- "figure.text.fasten-summary-follows": "{axis} follows a value",
569
563
  "figure.text.fasten-summary-gap": "gap {mm} mm",
570
564
  "figure.text.fasten-summary-twist": "twist {degrees}°",
571
565
  "figure.label.gap": "gap (mm)",
@@ -661,7 +655,6 @@
661
655
  "figure.label.clip": "Clip",
662
656
  "figure.label.new-clip": "New clip",
663
657
  "figure.button.add-track": "Add",
664
- "figure.text.add-a-shape-to-start": "Add a shape to start modelling.",
665
658
  "figure.label.primitive-frustum": "Cone",
666
659
  "figure.label.primitive-plate": "Plate",
667
660
  "figure.label.field-width": "Width",
@@ -882,7 +875,6 @@
882
875
  "figure.label.target-part-placeholder": "Choose a part",
883
876
  "figure.text.fasten-target-first": "Choose the part to attach to first.",
884
877
  "figure.text.fasten-edit-on-plane": "This part is connected to the model's placement reference. Detach it before attaching it to another part.",
885
- "figure.button.fasten-update": "Update this attachment",
886
878
  "figure.button.align-to-floor": "set on the floor",
887
879
  "figure.button.align-to-ceiling": "up to the ceiling",
888
880
  "figure.button.align-to-wall": "back to the wall",
@@ -116,6 +116,7 @@
116
116
  "figure.label.clips": "アニメーション (時間に沿って再生)",
117
117
  "figure.label.coordinate-ground": "coordinate ground",
118
118
  "figure.label.detail-L": "detailed",
119
+ "figure.label.detail-XL": "fine",
119
120
  "figure.label.detail-M": "normal",
120
121
  "figure.label.detail-S": "simple",
121
122
  "figure.label.detail-level": "detail level",
@@ -398,7 +399,7 @@
398
399
  "figure.text.coordinate-ground-explained": "原点と目盛りを床に表示します",
399
400
  "figure.text.delete-figure-confirm": "delete {name}? drawings already using this type will no longer find it.",
400
401
  "figure.text.designate-in-properties": "まだ指定された部品がありません。部品を選び、プロパティタブで「能力での役割」を指定してください。",
401
- "figure.text.detail-level-explained": "S は標識・柱、M はほとんどの設備、L は画面の中心となる大きな設備に使います。等級で部品数の上限が決まります。",
402
+ "figure.text.detail-level-explained": "S は標識・柱、M はほとんどの設備、L は画面の中心となる大きな設備、XL は脚のあるロボットのように関節の多い装置に使います。等級で部品数の上限が決まります。",
402
403
  "figure.text.discard-changes-and-start-new": "保存していない変更があります。破棄して新しく始めますか?",
403
404
  "figure.text.leave-without-saving": "保存していない変更があります。破棄して移動しますか?",
404
405
  "figure.button.leave-without-saving": "破棄して移動",
@@ -467,7 +468,6 @@
467
468
  "figure.text.nothing-released-yet": "まだ発行したバージョンがありません。",
468
469
  "figure.text.on-by-default-explained": "図面に置いたときの最初の状態です。インスタンスで切り替えた値が優先されます。",
469
470
  "figure.text.other-segment-count": "enter a value",
470
- "figure.text.palette-purpose-explained": "この部品が画面で担う色の役割を選びます。",
471
471
  "figure.text.part-limit-reached": "the detail level allows no more parts — raise the level or remove a part.",
472
472
  "figure.text.part-limit-reached-short": "level {level} allows {limit} parts. Raise the level or remove a part.",
473
473
  "figure.text.part-name-cannot-be-empty": "部品に名前を付けてください。",
@@ -535,7 +535,6 @@
535
535
  "figure.label.target-face": "target face",
536
536
  "figure.label.stands-on-plane": "stand on the mounting plane",
537
537
  "figure.label.facing-meet": "faces meet",
538
- "figure.label.facing-flush": "same plane",
539
538
  "figure.text.overhang-out": "{on} の {face} の縁から出ています: {sides}。モデルの占有空間とは別に扱います。",
540
539
  "figure.text.overhang-larger": "{on} の {face} より広いため縁から出ています: {sides}。",
541
540
  "figure.text.overhang-lopsided": "{axes} 方向で片側に寄っています。",
@@ -561,11 +560,6 @@
561
560
  "figure.text.target-would-loop": "{part} — {mine} に取り付いているため選べません",
562
561
  "figure.label.fasten-detail": "詳細",
563
562
  "figure.label.facing": "接し方",
564
- "figure.text.fasten-summary-default": "既定値(接する · 中央 · 間隔 0 · ひねり 0)",
565
- "figure.text.fasten-summary-edge-min": "{axis} 低い側の縁",
566
- "figure.text.fasten-summary-edge-max": "{axis} 高い側の縁",
567
- "figure.text.fasten-summary-off-centre": "{axis} 中央から {mm} mm",
568
- "figure.text.fasten-summary-follows": "{axis} 値に従う",
569
563
  "figure.text.fasten-summary-gap": "間隔 {mm} mm",
570
564
  "figure.text.fasten-summary-twist": "ひねり {degrees}°",
571
565
  "figure.label.gap": "gap (mm)",
@@ -661,7 +655,6 @@
661
655
  "figure.label.clip": "クリップ",
662
656
  "figure.label.new-clip": "新しいクリップ",
663
657
  "figure.button.add-track": "追加",
664
- "figure.text.add-a-shape-to-start": "図形を追加してモデリングを始めましょう。",
665
658
  "figure.label.primitive-frustum": "円錐",
666
659
  "figure.label.primitive-plate": "板",
667
660
  "figure.label.field-width": "幅",
@@ -882,7 +875,6 @@
882
875
  "figure.label.target-part-placeholder": "取り付ける部品を選択",
883
876
  "figure.text.fasten-target-first": "先に取り付ける部品を選んでください。",
884
877
  "figure.text.fasten-edit-on-plane": "この部品はモデルの設置基準に接続されています。別の部品に取り付ける前に外してください。",
885
- "figure.button.fasten-update": "この取り付けを更新",
886
878
  "figure.button.align-to-floor": "床に合わせる",
887
879
  "figure.button.align-to-ceiling": "天井に合わせる",
888
880
  "figure.button.align-to-wall": "壁に合わせる",
@@ -116,6 +116,7 @@
116
116
  "figure.label.clips": "애니메이션 (시간에 따라 재생)",
117
117
  "figure.label.coordinate-ground": "좌표 바탕",
118
118
  "figure.label.detail-L": "상세",
119
+ "figure.label.detail-XL": "정밀",
119
120
  "figure.label.detail-M": "보통",
120
121
  "figure.label.detail-S": "단순",
121
122
  "figure.label.detail-level": "디테일 등급",
@@ -398,7 +399,7 @@
398
399
  "figure.text.coordinate-ground-explained": "원점과 눈금을 바닥에 표시합니다",
399
400
  "figure.text.delete-figure-confirm": "{name} 을 삭제하시겠습니까? 이 타입을 쓰고 있는 도면은 더 이상 찾지 못합니다.",
400
401
  "figure.text.designate-in-properties": "아직 지정된 부품이 없습니다. 부품을 선택하고 속성 탭에서 「능력에서의 역할」을 지정하세요.",
401
- "figure.text.detail-level-explained": "S 는 표지·기둥, M 은 대부분의 설비, L 은 화면의 중심이 되는 큰 설비에 씁니다. 등급에 따라 부품 수 한도가 정해집니다.",
402
+ "figure.text.detail-level-explained": "S 는 표지·기둥, M 은 대부분의 설비, L 은 화면의 중심이 되는 큰 설비, XL 은 다리 달린 로봇처럼 관절이 많은 장치에 씁니다. 등급에 따라 부품 수 한도가 정해집니다.",
402
403
  "figure.text.discard-changes-and-start-new": "저장하지 않은 변경이 있습니다. 버리고 새로 시작할까요?",
403
404
  "figure.text.leave-without-saving": "저장하지 않은 변경이 있습니다. 버리고 나갈까요?",
404
405
  "figure.button.leave-without-saving": "버리고 나가기",
@@ -467,7 +468,6 @@
467
468
  "figure.text.nothing-released-yet": "아직 발행한 버전이 없습니다.",
468
469
  "figure.text.on-by-default-explained": "도면에 놓았을 때의 처음 상태입니다. 인스턴스에서 켜고 끈 값이 우선합니다.",
469
470
  "figure.text.other-segment-count": "직접 입력",
470
- "figure.text.palette-purpose-explained": "이 부품이 화면에서 맡을 색 역할을 고릅니다.",
471
471
  "figure.text.part-limit-reached": "디테일 등급이 허용하는 부품 수를 모두 사용했습니다. 등급을 올리거나 부품을 삭제하세요.",
472
472
  "figure.text.part-limit-reached-short": "{level} 등급은 부품 {limit}개까지입니다. 등급을 올리거나 부품을 삭제하세요.",
473
473
  "figure.text.part-name-cannot-be-empty": "부품 이름을 입력해 주세요.",
@@ -535,7 +535,6 @@
535
535
  "figure.label.target-face": "상대 면",
536
536
  "figure.label.stands-on-plane": "장착 평면에 세우기",
537
537
  "figure.label.facing-meet": "맞닿게",
538
- "figure.label.facing-flush": "같은 평면에",
539
538
  "figure.text.overhang-out": "{on}의 {face} 가장자리 밖으로 나와 있습니다: {sides}. 모델의 점유 공간과는 따로 봅니다.",
540
539
  "figure.text.overhang-larger": "{on}의 {face}보다 넓어 가장자리 밖으로 나와 있습니다: {sides}.",
541
540
  "figure.text.overhang-lopsided": "{axes} 방향으로 한쪽에 치우쳐 있습니다.",
@@ -561,11 +560,6 @@
561
560
  "figure.text.target-would-loop": "{part} — {mine}에 붙어 있어 고를 수 없음",
562
561
  "figure.label.fasten-detail": "세부",
563
562
  "figure.label.facing": "닿는 방식",
564
- "figure.text.fasten-summary-default": "기본값 (맞닿게 · 가운데 · 간격 0 · 비틀기 0)",
565
- "figure.text.fasten-summary-edge-min": "{axis} 낮은 쪽 모서리",
566
- "figure.text.fasten-summary-edge-max": "{axis} 높은 쪽 모서리",
567
- "figure.text.fasten-summary-off-centre": "{axis} 가운데서 {mm} mm",
568
- "figure.text.fasten-summary-follows": "{axis} 값을 따름",
569
563
  "figure.text.fasten-summary-gap": "간격 {mm} mm",
570
564
  "figure.text.fasten-summary-twist": "비틀기 {degrees}°",
571
565
  "figure.label.gap": "간격 (mm)",
@@ -661,7 +655,6 @@
661
655
  "figure.label.clip": "클립",
662
656
  "figure.label.new-clip": "새 클립",
663
657
  "figure.button.add-track": "재생 추가",
664
- "figure.text.add-a-shape-to-start": "도형을 추가해 모델링을 시작하세요.",
665
658
  "figure.label.primitive-frustum": "원뿔",
666
659
  "figure.label.primitive-plate": "판",
667
660
  "figure.label.field-width": "너비",
@@ -882,7 +875,6 @@
882
875
  "figure.label.target-part-placeholder": "붙일 부품 선택",
883
876
  "figure.text.fasten-target-first": "먼저 붙일 부품을 고르세요.",
884
877
  "figure.text.fasten-edit-on-plane": "이 부품은 현재 모델의 설치 기준면에 연결되어 있습니다. 다른 부품에 붙이려면 먼저 붙임을 푸세요.",
885
- "figure.button.fasten-update": "이 붙임 수정",
886
878
  "figure.button.align-to-floor": "바닥에 내려놓기",
887
879
  "figure.button.align-to-ceiling": "천장에 대기",
888
880
  "figure.button.align-to-wall": "벽에 대기",
@@ -116,6 +116,7 @@
116
116
  "figure.label.clips": "animasi (dimainkan mengikut masa)",
117
117
  "figure.label.coordinate-ground": "coordinate ground",
118
118
  "figure.label.detail-L": "detailed",
119
+ "figure.label.detail-XL": "fine",
119
120
  "figure.label.detail-M": "normal",
120
121
  "figure.label.detail-S": "simple",
121
122
  "figure.label.detail-level": "detail level",
@@ -398,7 +399,7 @@
398
399
  "figure.text.coordinate-ground-explained": "melukis asalan dan grid pada lantai",
399
400
  "figure.text.delete-figure-confirm": "delete {name}? drawings already using this type will no longer find it.",
400
401
  "figure.text.designate-in-properties": "belum ada yang ditetapkan. pilih bahagian dan tetapkan peranannya dalam tab sifat.",
401
- "figure.text.detail-level-explained": "S untuk papan tanda dan tiang, M untuk kebanyakan peralatan, L untuk peralatan besar yang menjadi tumpuan. tahap menentukan had bahagian.",
402
+ "figure.text.detail-level-explained": "S untuk papan tanda dan tiang, M untuk kebanyakan peralatan, L untuk peralatan besar yang menjadi tumpuan, XL untuk mekanisme banyak sendi seperti robot berkaki. tahap menentukan had bahagian.",
402
403
  "figure.text.discard-changes-and-start-new": "ada perubahan belum disimpan. buang dan mulakan yang baharu?",
403
404
  "figure.text.leave-without-saving": "ada perubahan yang belum disimpan. buang dan keluar?",
404
405
  "figure.button.leave-without-saving": "buang dan keluar",
@@ -467,7 +468,6 @@
467
468
  "figure.text.nothing-released-yet": "belum ada yang diterbitkan.",
468
469
  "figure.text.on-by-default-explained": "keadaan permulaan apabila diletakkan pada lukisan. nilai yang ditetapkan pada instans diutamakan.",
469
470
  "figure.text.other-segment-count": "enter a value",
470
- "figure.text.palette-purpose-explained": "pilih peranan warna yang dimainkan bahagian ini pada skrin.",
471
471
  "figure.text.part-limit-reached": "the detail level allows no more parts — raise the level or remove a part.",
472
472
  "figure.text.part-limit-reached-short": "level {level} allows {limit} parts. Raise the level or remove a part.",
473
473
  "figure.text.part-name-cannot-be-empty": "beri nama kepada bahagian.",
@@ -535,7 +535,6 @@
535
535
  "figure.label.target-face": "target face",
536
536
  "figure.label.stands-on-plane": "stand on the mounting plane",
537
537
  "figure.label.facing-meet": "faces meet",
538
- "figure.label.facing-flush": "same plane",
539
538
  "figure.text.overhang-out": "Ia terkeluar melepasi tepi {face} {on}: {sides}. Ini berasingan daripada ruang rajah.",
540
539
  "figure.text.overhang-larger": "Lebih lebar daripada {face} {on}, ia melepasi tepinya: {sides}.",
541
540
  "figure.text.overhang-lopsided": "Ia terletak ke satu sisi sepanjang {axes}.",
@@ -561,11 +560,6 @@
561
560
  "figure.text.target-would-loop": "{part} — dilekatkan pada {mine}, tidak boleh dipilih",
562
561
  "figure.label.fasten-detail": "Butiran",
563
562
  "figure.label.facing": "Cara muka bertemu",
564
- "figure.text.fasten-summary-default": "Lalai (bertemu · tengah · jarak 0 · pulasan 0)",
565
- "figure.text.fasten-summary-edge-min": "{axis} tepi bawah",
566
- "figure.text.fasten-summary-edge-max": "{axis} tepi atas",
567
- "figure.text.fasten-summary-off-centre": "{axis} {mm} mm dari tengah",
568
- "figure.text.fasten-summary-follows": "{axis} mengikut nilai",
569
563
  "figure.text.fasten-summary-gap": "jarak {mm} mm",
570
564
  "figure.text.fasten-summary-twist": "pulasan {degrees}°",
571
565
  "figure.label.gap": "gap (mm)",
@@ -661,7 +655,6 @@
661
655
  "figure.label.clip": "Klip",
662
656
  "figure.label.new-clip": "Klip baharu",
663
657
  "figure.button.add-track": "Tambah",
664
- "figure.text.add-a-shape-to-start": "Tambah bentuk untuk mula memodel.",
665
658
  "figure.label.primitive-frustum": "Kon",
666
659
  "figure.label.primitive-plate": "Plat",
667
660
  "figure.label.field-width": "Lebar",
@@ -882,7 +875,6 @@
882
875
  "figure.label.target-part-placeholder": "Pilih bahagian",
883
876
  "figure.text.fasten-target-first": "Pilih bahagian yang hendak dilekatkan dahulu.",
884
877
  "figure.text.fasten-edit-on-plane": "Bahagian ini disambungkan kepada rujukan peletakan model. Tanggalkan dahulu sebelum melekatkannya pada bahagian lain.",
885
- "figure.button.fasten-update": "Kemas kini lekatan ini",
886
878
  "figure.button.align-to-floor": "Jajarkan ke lantai",
887
879
  "figure.button.align-to-ceiling": "Jajarkan ke siling",
888
880
  "figure.button.align-to-wall": "Jajarkan ke dinding",
@@ -116,6 +116,7 @@
116
116
  "figure.label.clips": "动画 (随时间播放)",
117
117
  "figure.label.coordinate-ground": "coordinate ground",
118
118
  "figure.label.detail-L": "detailed",
119
+ "figure.label.detail-XL": "fine",
119
120
  "figure.label.detail-M": "normal",
120
121
  "figure.label.detail-S": "simple",
121
122
  "figure.label.detail-level": "detail level",
@@ -398,7 +399,7 @@
398
399
  "figure.text.coordinate-ground-explained": "在地面显示原点和网格",
399
400
  "figure.text.delete-figure-confirm": "delete {name}? drawings already using this type will no longer find it.",
400
401
  "figure.text.designate-in-properties": "还没有指定部件。请选择部件,在属性标签页中设置「在能力中的角色」。",
401
- "figure.text.detail-level-explained": "S 用于标志和立柱,M 用于大多数设备,L 用于作为画面中心的大型设备。等级决定部件数量上限。",
402
+ "figure.text.detail-level-explained": "S 用于标志和立柱,M 用于大多数设备,L 用于作为画面中心的大型设备,XL 用于腿式机器人等关节众多的装置。等级决定部件数量上限。",
402
403
  "figure.text.discard-changes-and-start-new": "有未保存的更改。放弃并新建吗?",
403
404
  "figure.text.leave-without-saving": "有未保存的更改。放弃并离开吗?",
404
405
  "figure.button.leave-without-saving": "放弃并离开",
@@ -467,7 +468,6 @@
467
468
  "figure.text.nothing-released-yet": "还没有已发布的版本。",
468
469
  "figure.text.on-by-default-explained": "放置到图面时的初始状态。实例上设置的值优先。",
469
470
  "figure.text.other-segment-count": "enter a value",
470
- "figure.text.palette-purpose-explained": "选择此部件在画面上承担的颜色角色。",
471
471
  "figure.text.part-limit-reached": "the detail level allows no more parts — raise the level or remove a part.",
472
472
  "figure.text.part-limit-reached-short": "level {level} allows {limit} parts. Raise the level or remove a part.",
473
473
  "figure.text.part-name-cannot-be-empty": "请为部件命名。",
@@ -535,7 +535,6 @@
535
535
  "figure.label.target-face": "target face",
536
536
  "figure.label.stands-on-plane": "stand on the mounting plane",
537
537
  "figure.label.facing-meet": "faces meet",
538
- "figure.label.facing-flush": "same plane",
539
538
  "figure.text.overhang-out": "超出了 {on} 的 {face} 边缘:{sides}。这与模型的占用空间分开看待。",
540
539
  "figure.text.overhang-larger": "比 {on} 的 {face} 宽,超出边缘:{sides}。",
541
540
  "figure.text.overhang-lopsided": "沿 {axes} 偏向一侧。",
@@ -561,11 +560,6 @@
561
560
  "figure.text.target-would-loop": "{part} — 已附着于 {mine},无法选择",
562
561
  "figure.label.fasten-detail": "详细",
563
562
  "figure.label.facing": "接触方式",
564
- "figure.text.fasten-summary-default": "默认值(贴合 · 居中 · 间距 0 · 扭转 0)",
565
- "figure.text.fasten-summary-edge-min": "{axis} 低侧边缘",
566
- "figure.text.fasten-summary-edge-max": "{axis} 高侧边缘",
567
- "figure.text.fasten-summary-off-centre": "{axis} 距中心 {mm} mm",
568
- "figure.text.fasten-summary-follows": "{axis} 跟随数值",
569
563
  "figure.text.fasten-summary-gap": "间距 {mm} mm",
570
564
  "figure.text.fasten-summary-twist": "扭转 {degrees}°",
571
565
  "figure.label.gap": "gap (mm)",
@@ -661,7 +655,6 @@
661
655
  "figure.label.clip": "片段",
662
656
  "figure.label.new-clip": "新片段",
663
657
  "figure.button.add-track": "添加",
664
- "figure.text.add-a-shape-to-start": "添加一个形状开始建模。",
665
658
  "figure.label.primitive-frustum": "圆锥",
666
659
  "figure.label.primitive-plate": "板",
667
660
  "figure.label.field-width": "宽度",
@@ -882,7 +875,6 @@
882
875
  "figure.label.target-part-placeholder": "选择部件",
883
876
  "figure.text.fasten-target-first": "请先选择要附着的部件。",
884
877
  "figure.text.fasten-edit-on-plane": "此部件当前连接到模型的安装基准。附着到另一部件前请先解除连接。",
885
- "figure.button.fasten-update": "更新此附着",
886
878
  "figure.button.align-to-floor": "对齐地面",
887
879
  "figure.button.align-to-ceiling": "对齐天花板",
888
880
  "figure.button.align-to-wall": "对齐墙面",