@things-factory/figure-ui 10.1.83 → 10.1.84

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 (83) hide show
  1. package/client/modeller/command-preview.ts +4 -2
  2. package/client/modeller/drawn-shapes.ts +29 -0
  3. package/client/modeller/editor.ts +8 -1
  4. package/client/modeller/figure-material-panel.ts +2 -3
  5. package/client/modeller/figure-parts.ts +9 -11
  6. package/client/modeller/figure-section-panel.ts +175 -0
  7. package/client/modeller/gizmo.ts +26 -16
  8. package/client/modeller/joint-fan.ts +4 -3
  9. package/client/modeller/links.ts +24 -24
  10. package/client/modeller/maturity.ts +12 -6
  11. package/client/modeller/motion-ends.ts +2 -1
  12. package/client/modeller/refusals.ts +20 -0
  13. package/client/modeller/renderer.ts +3 -2
  14. package/client/modeller/section-controls.ts +34 -0
  15. package/client/modeller/starting-shape.ts +34 -0
  16. package/dist-client/modeller/command-preview.js +4 -2
  17. package/dist-client/modeller/command-preview.js.map +1 -1
  18. package/dist-client/modeller/drawn-shapes.d.ts +44 -0
  19. package/dist-client/modeller/drawn-shapes.js +12 -0
  20. package/dist-client/modeller/drawn-shapes.js.map +1 -0
  21. package/dist-client/modeller/editor.d.ts +1 -0
  22. package/dist-client/modeller/editor.js +8 -1
  23. package/dist-client/modeller/editor.js.map +1 -1
  24. package/dist-client/modeller/figure-capability-panel.d.ts +1 -1
  25. package/dist-client/modeller/figure-clips.d.ts +1 -1
  26. package/dist-client/modeller/figure-controls.d.ts +1 -1
  27. package/dist-client/modeller/figure-links.d.ts +1 -1
  28. package/dist-client/modeller/figure-material-panel.d.ts +1 -1
  29. package/dist-client/modeller/figure-material-panel.js +2 -3
  30. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  31. package/dist-client/modeller/figure-parts.d.ts +3 -2
  32. package/dist-client/modeller/figure-parts.js +6 -9
  33. package/dist-client/modeller/figure-parts.js.map +1 -1
  34. package/dist-client/modeller/figure-properties.d.ts +1 -1
  35. package/dist-client/modeller/figure-section-panel.d.ts +28 -0
  36. package/dist-client/modeller/figure-section-panel.js +172 -0
  37. package/dist-client/modeller/figure-section-panel.js.map +1 -0
  38. package/dist-client/modeller/gizmo.d.ts +3 -3
  39. package/dist-client/modeller/gizmo.js +24 -11
  40. package/dist-client/modeller/gizmo.js.map +1 -1
  41. package/dist-client/modeller/joint-fan.js +4 -3
  42. package/dist-client/modeller/joint-fan.js.map +1 -1
  43. package/dist-client/modeller/links.d.ts +4 -1
  44. package/dist-client/modeller/links.js +23 -25
  45. package/dist-client/modeller/links.js.map +1 -1
  46. package/dist-client/modeller/maturity.d.ts +3 -2
  47. package/dist-client/modeller/maturity.js +9 -6
  48. package/dist-client/modeller/maturity.js.map +1 -1
  49. package/dist-client/modeller/motion-ends.js +2 -1
  50. package/dist-client/modeller/motion-ends.js.map +1 -1
  51. package/dist-client/modeller/refusals.d.ts +0 -5
  52. package/dist-client/modeller/refusals.js +25 -0
  53. package/dist-client/modeller/refusals.js.map +1 -1
  54. package/dist-client/modeller/renderer.d.ts +3 -4
  55. package/dist-client/modeller/renderer.js +3 -2
  56. package/dist-client/modeller/renderer.js.map +1 -1
  57. package/dist-client/modeller/section-controls.d.ts +11 -0
  58. package/dist-client/modeller/section-controls.js +27 -0
  59. package/dist-client/modeller/section-controls.js.map +1 -0
  60. package/dist-client/modeller/starting-shape.d.ts +32 -0
  61. package/dist-client/modeller/starting-shape.js +37 -0
  62. package/dist-client/modeller/starting-shape.js.map +1 -0
  63. package/dist-client/tsconfig.tsbuildinfo +1 -1
  64. package/dist-server/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +5 -5
  66. package/test/browser/acceptance-page.js +8 -0
  67. package/test/command-preview.test.ts +3 -2
  68. package/test/fixtures/joint-column.json +110 -15
  69. package/test/fixtures/joint-roller.json +66 -9
  70. package/test/fixtures/robot-arm-6dof.json +203 -27
  71. package/test/joint-fan.test.ts +10 -7
  72. package/test/links.test.ts +4 -4
  73. package/test/material.test.ts +7 -2
  74. package/test/renderer.test.ts +2 -1
  75. package/test/resize-with-placement.test.ts +3 -3
  76. package/test/scene-canvas.test.ts +2 -1
  77. package/test/section-panel.test.ts +93 -0
  78. package/translations/en.json +13 -0
  79. package/translations/ja.json +13 -0
  80. package/translations/ko.json +13 -0
  81. package/translations/ms.json +13 -0
  82. package/translations/zh.json +13 -0
  83. package/dist-client/tsconfig.localkernel.tsbuildinfo +0 -1
@@ -12,6 +12,7 @@ import { fileURLToPath } from 'node:url'
12
12
 
13
13
  import { changedKeys, componentOf, gizmoResultOf } from '../client/modeller/stage-model.ts'
14
14
  import type { FigureRenderPart } from '../client/modeller/renderer.ts'
15
+ import { boxShape } from '../client/modeller/drawn-shapes.ts'
15
16
 
16
17
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
17
18
  const canvas = read('../client/modeller/canvas.ts')
@@ -27,7 +28,7 @@ const turned = (deg: number) => {
27
28
  }
28
29
  const box = (t: number[], r = turned(0)): FigureRenderPart => ({
29
30
  id: 'block',
30
- shape: { provider: 'box', dimensions: { width: 200, height: 100, depth: 60 } },
31
+ shape: boxShape(200, 100, 60),
31
32
  pose: { r, t },
32
33
  material: { token: 'palette.primary' }
33
34
  })
@@ -0,0 +1,93 @@
1
+ /*
2
+ * **단면 칸 — 모서리와 속 공간** (ADR-0100 1단계, 2026-09-29).
3
+ *
4
+ * 칸이 보내는 명령이 figure-model 에서 정말로 받아들여져 그려지는 몸체를 바꾸는지, 받아들여지지 않을 때 화면이 그 까닭을
5
+ * 사람의 말로 하는지를 잰다.
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 { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
13
+ import type { FigureAsset, FigureEvaluatedShape } from '@hatiolab/figure-model'
14
+
15
+ import { cavityRequestOf, defaultWallOf } from '../client/modeller/section-controls.ts'
16
+ import { editRefusalOf } from '../client/modeller/refusals.ts'
17
+
18
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
19
+ const panel = read('../client/modeller/figure-section-panel.ts')
20
+ const editor = read('../client/modeller/editor.ts')
21
+ const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
22
+
23
+ const probe = () => addFigureBox(createFigureAsset('Probe'), { id: 'body', size: { x: 400, y: 200, z: 300 }, position: { x: 0, y: 100, z: 0 } })
24
+ const shapeOf = (asset: FigureAsset, id = 'body') =>
25
+ (compileFigureAsset(asset).evaluate().geometry.find(g => (g as { id: string }).id === id) as unknown as { shape: FigureEvaluatedShape }).shape
26
+ const say = (key: string, params: Record<string, string>) => Object.entries(params).reduce((s, [k, v]) => s.replace(`{${k}}`, v), ko[key] ?? key)
27
+ const refusal = (run: () => unknown) => {
28
+ try {
29
+ run()
30
+ } catch (error) {
31
+ return editRefusalOf(error, say)
32
+ }
33
+ assert.fail('the command was taken')
34
+ }
35
+
36
+ test('모서리 반지름은 네 모서리를 한 값으로 둥글게 하고, 0 은 둥글림을 없앤다', () => {
37
+ const round = applyFigureAuthoring(probe(), { kind: 'set-section-corner', part: 'body', corner: 40 })
38
+ const shape = shapeOf(round)
39
+ assert.deepEqual(shape.extrusion!.section, { kind: 'rectangle', size: { x: 400, z: 300 }, corner: { x: 40, z: 40 } })
40
+ const sharp = applyFigureAuthoring(round, { kind: 'set-section-corner', part: 'body', corner: null })
41
+ assert.equal(shapeOf(sharp).extrusion!.section.kind, 'rectangle')
42
+ 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')
44
+ })
45
+
46
+ test('속을 비우면 기본 벽·막힌 바닥·열린 윗면의 쟁반이 되고, 끝과 벽을 하나씩 바꾼다', () => {
47
+ const before = shapeOf(probe())
48
+ assert.equal(defaultWallOf(before), 30, 'a tenth of the narrower side (300)')
49
+ const tray = applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: cavityRequestOf(before) })
50
+ const shape = shapeOf(tray)
51
+ assert.deepEqual(shape.cavity!.start, { closed: 30 })
52
+ assert.equal(shape.cavity!.end, 'open')
53
+ assert.deepEqual(Object.values(shape.cavity!.wall), [30, 30, 30, 30])
54
+ const thin = applyFigureAuthoring(tray, { kind: 'set-cavity', part: 'body', cavity: cavityRequestOf(shape, { wall: 5 }) })
55
+ assert.deepEqual(Object.values(shapeOf(thin).cavity!.wall), [5, 5, 5, 5])
56
+ assert.deepEqual(shapeOf(thin).cavity!.start, { closed: 30 }, 'the ends stay as they were')
57
+ const lidded = applyFigureAuthoring(thin, { kind: 'set-cavity', part: 'body', cavity: cavityRequestOf(shapeOf(thin), { end: { closed: 8 } }) })
58
+ assert.deepEqual(shapeOf(lidded).cavity!.end, { closed: 8 })
59
+ const solid = applyFigureAuthoring(lidded, { kind: 'set-cavity', part: 'body', cavity: null })
60
+ assert.equal(shapeOf(solid).cavity, undefined)
61
+ })
62
+
63
+ test('벽이 단면을 다 채우거나 모서리가 변의 절반을 넘으면, 그 까닭을 말로 한다', () => {
64
+ const tray = applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: cavityRequestOf(shapeOf(probe())) })
65
+ assert.equal(
66
+ refusal(() => applyFigureAuthoring(tray, { kind: 'set-cavity', part: 'body', cavity: cavityRequestOf(shapeOf(tray), { wall: 150 }) })),
67
+ ko['figure.text.section-wall-too-thick']
68
+ )
69
+ assert.equal(
70
+ refusal(() => applyFigureAuthoring(tray, { kind: 'set-cavity', part: 'body', cavity: cavityRequestOf(shapeOf(tray), { start: { closed: 120 }, end: { closed: 90 } }) })),
71
+ ko['figure.text.section-ends-too-thick']
72
+ )
73
+ assert.equal(refusal(() => applyFigureAuthoring(probe(), { kind: 'set-section-corner', part: 'body', corner: 160 })), ko['figure.text.section-corner-too-large'])
74
+ })
75
+
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/)
79
+ for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
80
+ const words = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
81
+ 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)
83
+ for (const key of used) assert.ok(words[key], `${lang} has ${key}`)
84
+ }
85
+ })
86
+
87
+ test('붙어 있는 것이 기대는 면을 없애는 편집은 거절되고, 그 면을 사람의 말로 부른다', () => {
88
+ let asset = applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: { wall: 10, start: { closed: 15 }, end: 'open' } })
89
+ asset = addFigureBox(asset, { id: 'divider', size: { x: 10, y: 100, z: 200 }, position: { x: 0, y: 0, z: 0 } })
90
+ asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'divider', face: 'bottom', to: { part: 'body', face: 'inside-bottom' as never } })
91
+ const said = refusal(() => applyFigureAuthoring(asset, { kind: 'set-cavity', part: 'body', cavity: { wall: 10, start: 'open', end: 'open' } }))
92
+ assert.equal(said, say('figure.text.section-face-in-use', { faces: ko['figure.label.face-inside-bottom']! }))
93
+ })
@@ -188,6 +188,19 @@
188
188
  "figure.label.palette-token": "palette token",
189
189
  "figure.label.part": "part",
190
190
  "figure.label.part-material": "material",
191
+ "figure.label.group-section": "section",
192
+ "figure.label.section-corner": "corner radius",
193
+ "figure.text.section-corner-differs": "differs by axis",
194
+ "figure.label.section-cavity": "hollow",
195
+ "figure.label.section-wall": "wall thickness",
196
+ "figure.text.section-wall-differs": "differs by side",
197
+ "figure.label.section-end-closed": "closed",
198
+ "figure.label.section-end-open": "open",
199
+ "figure.text.section-face-in-use": "Not changed: something is attached to {faces}. Detach it first.",
200
+ "figure.text.section-dimension-linked": "This dimension follows another value. Unlink it before changing it.",
201
+ "figure.text.section-corner-too-large": "The corner radius is more than half the side.",
202
+ "figure.text.section-wall-too-thick": "The walls are too thick to leave an inside.",
203
+ "figure.text.section-ends-too-thick": "The closed ends are too thick to leave an inside.",
191
204
  "figure.label.part-position": "position (centre)",
192
205
  "figure.label.part-size": "size",
193
206
  "figure.label.parts": "parts",
@@ -188,6 +188,19 @@
188
188
  "figure.label.palette-token": "palette token",
189
189
  "figure.label.part": "part",
190
190
  "figure.label.part-material": "material",
191
+ "figure.label.group-section": "断面",
192
+ "figure.label.section-corner": "角の半径",
193
+ "figure.text.section-corner-differs": "軸ごとに異なる",
194
+ "figure.label.section-cavity": "中空",
195
+ "figure.label.section-wall": "壁の厚さ",
196
+ "figure.text.section-wall-differs": "面ごとに異なる",
197
+ "figure.label.section-end-closed": "閉じる",
198
+ "figure.label.section-end-open": "開く",
199
+ "figure.text.section-face-in-use": "変更しませんでした: {faces} に取り付けられたものがあります。先に外してください。",
200
+ "figure.text.section-dimension-linked": "この寸法は他の値に従います。連動を解除してから変更してください。",
201
+ "figure.text.section-corner-too-large": "角の半径が辺の半分より大きいです。",
202
+ "figure.text.section-wall-too-thick": "壁が厚く、内部の空間が残りません。",
203
+ "figure.text.section-ends-too-thick": "閉じた端が厚く、内部の空間が残りません。",
191
204
  "figure.label.part-position": "position (centre)",
192
205
  "figure.label.part-size": "size",
193
206
  "figure.label.parts": "parts",
@@ -188,6 +188,19 @@
188
188
  "figure.label.palette-token": "팔레트 토큰",
189
189
  "figure.label.part": "부품",
190
190
  "figure.label.part-material": "재질",
191
+ "figure.label.group-section": "단면",
192
+ "figure.label.section-corner": "모서리 반지름",
193
+ "figure.text.section-corner-differs": "축마다 다름",
194
+ "figure.label.section-cavity": "속을 비움",
195
+ "figure.label.section-wall": "벽 두께",
196
+ "figure.text.section-wall-differs": "면마다 다름",
197
+ "figure.label.section-end-closed": "막힘",
198
+ "figure.label.section-end-open": "열림",
199
+ "figure.text.section-face-in-use": "이 면에 붙어 있는 것이 있어 바꾸지 않았습니다: {faces}. 먼저 떼어 냅니다.",
200
+ "figure.text.section-dimension-linked": "이 치수는 다른 값을 따릅니다. 따르기를 풀고 고칩니다.",
201
+ "figure.text.section-corner-too-large": "모서리 반지름이 변 길이의 절반보다 큽니다.",
202
+ "figure.text.section-wall-too-thick": "벽이 두꺼워 속 공간이 남지 않습니다.",
203
+ "figure.text.section-ends-too-thick": "막힌 끝이 두꺼워 속 공간이 남지 않습니다.",
191
204
  "figure.label.part-position": "위치 (중심)",
192
205
  "figure.label.part-size": "크기",
193
206
  "figure.label.parts": "부품",
@@ -188,6 +188,19 @@
188
188
  "figure.label.palette-token": "palette token",
189
189
  "figure.label.part": "part",
190
190
  "figure.label.part-material": "material",
191
+ "figure.label.group-section": "keratan",
192
+ "figure.label.section-corner": "jejari sudut",
193
+ "figure.text.section-corner-differs": "berbeza mengikut paksi",
194
+ "figure.label.section-cavity": "berongga",
195
+ "figure.label.section-wall": "ketebalan dinding",
196
+ "figure.text.section-wall-differs": "berbeza mengikut sisi",
197
+ "figure.label.section-end-closed": "tertutup",
198
+ "figure.label.section-end-open": "terbuka",
199
+ "figure.text.section-face-in-use": "Tidak diubah: ada sesuatu dipasang pada {faces}. Tanggalkannya dahulu.",
200
+ "figure.text.section-dimension-linked": "Dimensi ini mengikut nilai lain. Nyahpaut sebelum mengubahnya.",
201
+ "figure.text.section-corner-too-large": "Jejari sudut melebihi separuh panjang sisi.",
202
+ "figure.text.section-wall-too-thick": "Dinding terlalu tebal sehingga tiada ruang di dalam.",
203
+ "figure.text.section-ends-too-thick": "Hujung tertutup terlalu tebal sehingga tiada ruang di dalam.",
191
204
  "figure.label.part-position": "position (centre)",
192
205
  "figure.label.part-size": "size",
193
206
  "figure.label.parts": "parts",
@@ -188,6 +188,19 @@
188
188
  "figure.label.palette-token": "palette token",
189
189
  "figure.label.part": "part",
190
190
  "figure.label.part-material": "material",
191
+ "figure.label.group-section": "截面",
192
+ "figure.label.section-corner": "圆角半径",
193
+ "figure.text.section-corner-differs": "各轴不同",
194
+ "figure.label.section-cavity": "中空",
195
+ "figure.label.section-wall": "壁厚",
196
+ "figure.text.section-wall-differs": "各面不同",
197
+ "figure.label.section-end-closed": "封闭",
198
+ "figure.label.section-end-open": "开口",
199
+ "figure.text.section-face-in-use": "未更改:{faces} 上附着有其他部件。请先拆下。",
200
+ "figure.text.section-dimension-linked": "此尺寸跟随其他值。请先解除关联再修改。",
201
+ "figure.text.section-corner-too-large": "圆角半径超过边长的一半。",
202
+ "figure.text.section-wall-too-thick": "壁太厚,内部没有空间。",
203
+ "figure.text.section-ends-too-thick": "封闭端太厚,内部没有空间。",
191
204
  "figure.label.part-position": "position (centre)",
192
205
  "figure.label.part-size": "size",
193
206
  "figure.label.parts": "parts",