@things-factory/figure-ui 10.1.88 → 10.1.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/client/modeller/editor.ts +29 -3
  2. package/client/modeller/figure-parts.ts +2 -0
  3. package/client/modeller/figure-section-editor.ts +93 -8
  4. package/client/modeller/figure-section-panel.ts +3 -1
  5. package/client/modeller/maturity.ts +8 -1
  6. package/client/modeller/palette.ts +13 -0
  7. package/client/modeller/section-drawing.ts +25 -5
  8. package/client/modeller/starting-shape.ts +5 -3
  9. package/dist-client/modeller/editor.d.ts +6 -0
  10. package/dist-client/modeller/editor.js +27 -3
  11. package/dist-client/modeller/editor.js.map +1 -1
  12. package/dist-client/modeller/figure-parts.js +2 -0
  13. package/dist-client/modeller/figure-parts.js.map +1 -1
  14. package/dist-client/modeller/figure-section-editor.d.ts +8 -0
  15. package/dist-client/modeller/figure-section-editor.js +89 -8
  16. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  17. package/dist-client/modeller/figure-section-panel.js +3 -1
  18. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  19. package/dist-client/modeller/maturity.js +8 -1
  20. package/dist-client/modeller/maturity.js.map +1 -1
  21. package/dist-client/modeller/palette.js +13 -0
  22. package/dist-client/modeller/palette.js.map +1 -1
  23. package/dist-client/modeller/section-drawing.d.ts +12 -0
  24. package/dist-client/modeller/section-drawing.js +22 -5
  25. package/dist-client/modeller/section-drawing.js.map +1 -1
  26. package/dist-client/modeller/starting-shape.d.ts +3 -2
  27. package/dist-client/modeller/starting-shape.js +4 -0
  28. package/dist-client/modeller/starting-shape.js.map +1 -1
  29. package/dist-client/route.d.ts +1 -1
  30. package/dist-client/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +3 -3
  32. package/test/browser/acceptance-page.js +9 -0
  33. package/test/palette-icons.test.ts +5 -2
  34. package/test/parts-wiring.test.ts +1 -1
  35. package/test/section-editor.test.ts +57 -3
  36. package/translations/en.json +7 -1
  37. package/translations/ja.json +7 -1
  38. package/translations/ko.json +7 -1
  39. package/translations/ms.json +7 -1
  40. package/translations/zh.json +7 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.88",
3
+ "version": "10.1.89",
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.3",
30
+ "@hatiolab/figure-model": "^0.2.4",
31
31
  "@hatiolab/things-scene": "^10.1.63",
32
32
  "@material/web": "^2.0.0",
33
33
  "@operato/data-grist": "^10.0.0",
@@ -44,5 +44,5 @@
44
44
  "@things-factory/figure-service": "^10.1.84",
45
45
  "three": "^0.185.1"
46
46
  },
47
- "gitHead": "11ece44d38c469f73aa28f4789f4baa244545097"
47
+ "gitHead": "ffffb5c90772ce88679be2f0192a6fddc4e48a1d"
48
48
  }
@@ -47,6 +47,15 @@ export const MODELS = {
47
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
48
  return serializeFigureAsset(a)
49
49
  },
50
+ /* The same triangle with a plate on its top: drawing its end to one point is refused while the plate stands there (A5-1). */
51
+ 'capped-wedge': () => {
52
+ let a = createFigureAsset('Capped wedge')
53
+ a.palette = { ...a.palette, ...PALETTE }
54
+ 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 } })
55
+ a = addFigureBox(a, { id: 'cap', size: { x: 100, y: 20, z: 100 }, position: { x: 0, y: 0, z: 0 } })
56
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'cap', face: 'bottom', to: { part: 'wedge', face: 'top' } })
57
+ return serializeFigureAsset(a)
58
+ },
50
59
  /* A box and a drum: rounded corners and an inside from the properties tab (ADR-0100 stage 1). */
51
60
  shapes: () => {
52
61
  let a = createFigureAsset('Box and drum')
@@ -10,8 +10,11 @@ import assert from 'node:assert/strict'
10
10
  import { readFileSync } from 'node:fs'
11
11
  import { fileURLToPath } from 'node:url'
12
12
 
13
- /** Measured in the app's icon font on :3300, 2026-09-24: each draws one cell wide. `wall` drew 52px -- its letters. */
14
- const RENDERS = new Set(['deployed_code', 'database', 'crop_square', 'circle', 'change_history', 'hexagon', 'check_box_outline_blank', 'crop_portrait', 'door_front'])
13
+ /**
14
+ * Measured in the app's icon font on :3300, 2026-09-24: each draws one cell wide. `wall` drew 52px -- its letters.
15
+ * 2026-09-30: `details` (the pyramid) 24px at 24px; `pyramid` is not in the font (168px).
16
+ */
17
+ const RENDERS = new Set(['deployed_code', 'database', 'crop_square', 'circle', 'change_history', 'hexagon', 'details', 'check_box_outline_blank', 'crop_portrait', 'door_front'])
15
18
 
16
19
  test('every shape in the palette has an icon the font draws', () => {
17
20
  const parts = readFileSync(fileURLToPath(new URL('../client/modeller/figure-parts.ts', import.meta.url)), 'utf8')
@@ -60,7 +60,7 @@ test('좁은 칸에서도 고른 줄의 이름과 조작이 다 보인다 —
60
60
  test('팔레트는 시작 모양만 낸다 — 둥근 상자와 통은 상자의 모서리·속 공간으로 만든다 (ADR-0100 §2.1·§3.3)', () => {
61
61
  const recipes = panel.slice(panel.indexOf('const RECIPES'), panel.indexOf(']', panel.indexOf('const RECIPES')))
62
62
  const kinds = [...recipes.matchAll(/kind: '([\w-]+)'/g)].map(m => m[1])
63
- assert.deepEqual(kinds, ['box', 'cylinder', 'sphere', 'frustum', 'polygon', 'plate', 'wall', 'portal'])
63
+ assert.deepEqual(kinds, ['box', 'cylinder', 'sphere', 'frustum', 'polygon', 'pyramid', 'plate', 'wall', 'portal'])
64
64
  /* A part made so is still called by what it is: its icon in the parts list, its name over the properties (links.ts). */
65
65
  assert.match(panel, /'rounded-box': 'crop_square'/)
66
66
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms'])
@@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url'
12
12
  import { addFigurePrimitive, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
13
13
  import type { FigureAsset, FigureEvaluatedShape, FigureSectionPoint } from '@hatiolab/figure-model'
14
14
 
15
- import { extentOf, insertAfter, issueWords, issuesOf, markedBy, movePoint, nextPointId, pointLabel, removePoint } from '../client/modeller/section-drawing.ts'
15
+ import { apexOver, extentOf, sectionOf as sectionOfWorking, workingIssuesOf as issuesOfWorking, workingOf as workingOfShape, insertAfter, issueWords, issuesOf, markedBy, movePoint, nextPointId, pointLabel, removePoint } from '../client/modeller/section-drawing.ts'
16
16
  import { livePartsOf } from '../client/modeller/live.ts'
17
17
 
18
18
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
@@ -197,7 +197,7 @@ test('사각형·원도 같은 창에서 — 폭·지름·길이를 한 명령
197
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
198
  const shape = (compileFigureAsset(box).evaluate().geometry.find(g => (g as { id: string }).id === 'b') as unknown as { shape: FigureEvaluatedShape }).shape
199
199
  const w = workingOf(shape)
200
- assert.deepEqual(w, { points: [], across: { x: 400, z: 300 }, length: 200 })
200
+ assert.deepEqual(w, { points: [], across: { x: 400, z: 300 }, length: 200, apex: null })
201
201
  assert.deepEqual(sectionOf(shape, { ...w, across: { x: 500, z: 300 } }), { kind: 'rectangle', size: { x: 500, z: 300 } })
202
202
  assert.deepEqual(figureSectionMeasuresOf(box, 'b')!.measures, { y: 'editable', x: 'editable', z: 'editable' })
203
203
  /* The editor sends only what changed: a measure left alone may follow another value and must not be written. */
@@ -225,7 +225,7 @@ test('속 공간이 있으면 폭·길이가 그 벽과 막힌 끝을 남기는
225
225
  })
226
226
 
227
227
  test('길이를 바꿀 때 남는 면은 부품의 관계가 정한 것을 읽어 보여 준다 (§4.1) — 다른 값을 따르는 치수는 칸을 막는다', () => {
228
- has(editor, /const kept = figureScaleAnchorOf\(asset, this\.selected\)\?\.\[part\.shape\.extrusion\.axis\]/)
228
+ has(editor, /const kept = figureScaleAnchorOf\(asset, this\.selected\)\?\.\[drawn\.axis\]/)
229
229
  has(editorPanel, /m\?\.\[this\.axis\] === 'linked' \? linked : this\.keptWords\(\)/)
230
230
  has(editorPanel, /m\?\.\[this\.axis\] !== 'editable'/)
231
231
  assert.equal(say('figure.text.section-editor-keeps', { face: ko['figure.label.face-bottom']! }), '아랫면 유지')
@@ -249,3 +249,57 @@ test('ADR-0100 A1: 상자·판·벽은 같은 돌출에 처음 치수만 다르
249
249
  assert.notDeepEqual(sizes.box, sizes.plate)
250
250
  assert.notDeepEqual(sizes.plate, sizes.wall)
251
251
  })
252
+
253
+ test('ADR-0100 A5-1: 팔레트의 뿔은 삼각 단면과 꼭짓점 하나 — 사면체이고, 목록과 제목에서 「뿔」로 불린다', async () => {
254
+ const { paletteRecipe } = await import('../client/modeller/palette.ts')
255
+ const { startingShapeKindOf, startingShapeKindOfBody } = await import('../client/modeller/starting-shape.ts')
256
+ const asset = addFigurePrimitive(createFigureAsset('P'), paletteRecipe('pyramid', 'tip'))
257
+ const node = asset.document.model.nodes.find(n => n.id === 'tip.shape')!
258
+ assert.equal(node.op, 'loft@1')
259
+ const s = (compileFigureAsset(asset).evaluate().geometry.find(g => (g as { id: string }).id === 'tip') as unknown as { shape: FigureEvaluatedShape }).shape
260
+ assert.equal(s.loft!.section.points.length, 3)
261
+ assert.deepEqual(s.loft!.apex, { x: 0, z: 0 })
262
+ assert.equal(startingShapeKindOf(node as never), 'pyramid')
263
+ assert.equal(startingShapeKindOfBody(s as never), 'pyramid')
264
+ for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
265
+ const t = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
266
+ for (const key of ['figure.label.primitive-pyramid', 'figure.label.section-editor-apex', 'figure.label.section-editor-apex-at', 'figure.label.section-editor-apex-mark', 'figure.text.section-editor-apex-blocked', 'figure.label.section-editor-height'])
267
+ assert.ok(t[key], `${lang} ${key}`)
268
+ }
269
+ assert.equal(ko['figure.label.primitive-pyramid'], '뿔')
270
+ })
271
+
272
+ test('편집 창에서 끝을 한 점으로: 켜면 점들의 가운데 위에 꼭짓점, 끄면 돌출로 — 바뀐 것만 한 명령으로 보낸다', () => {
273
+ const wedge = triangle()
274
+ const shape = shapeOf(wedge)
275
+ const w = workingOfShape(shape)
276
+ assert.equal(w.apex, null)
277
+ const on = { ...w, apex: apexOver(w.points, 'y') }
278
+ assert.deepEqual(on.apex, { z: 0, x: 0 })
279
+ assert.deepEqual(issuesOfWorking(shape, on).words, [])
280
+ /* The editor sends apex only when it changed, null to go back to a push. */
281
+ has(editorPanel, /\.\.\.\(apexMoved \? \{ apex: w\.apex \? \{ \.\.\.w\.apex \} : null \} : \{\}\)/)
282
+ const pyramid = applyFigureAuthoring(wedge, { kind: 'set-section', part: 'wedge', apex: on.apex })
283
+ const p = shapeOf(pyramid)
284
+ assert.equal(p.provider, 'loft')
285
+ const again = workingOfShape(p)
286
+ assert.deepEqual(again.apex, { x: 0, z: 0 })
287
+ assert.deepEqual(sectionOfWorking(p, again), { kind: 'polygon', points: again.points })
288
+ assert.equal(issuesOfWorking(p, { ...again, apex: { x: Number.NaN, z: 0 } }).words[0]!.key, 'figure.text.section-issue-number')
289
+ /* In the properties its apex is called as the editor calls it: 꼭짓점 X. */
290
+ has(editor, /\? \[`\$\{i18next\.t\('figure\.label\.section-editor-apex-mark'\)\} \$\{apex\[1\]!\.toUpperCase\(\)\}`\]/)
291
+ /* Its length is its height: the same field, called so. */
292
+ has(editorPanel, /this\.working\.apex \? 'figure\.label\.section-editor-height' : 'figure\.label\.section-editor-length'/)
293
+ const back = applyFigureAuthoring(pyramid, { kind: 'set-section', part: 'wedge', apex: null })
294
+ assert.equal(shapeOf(back).provider, 'extrude')
295
+ })
296
+
297
+ test('끝면에 붙은 부품이 있으면 한 점으로 모으기를 막고 그 면을 말한다 — 커널의 거절로 판정한다', () => {
298
+ has(editor, /this\.session\.previewCommand\(\{ kind: 'set-section', part: this\.selected!, apex: apexOver\(drawn\.section\.points, drawn\.axis\) \}\)/)
299
+ has(editor, /\.code === 'FEATURE_MAPPING_REQUIRED'\)/)
300
+ has(editorPanel, /const blocked = !apex && !this\.opened\?\.apex \? this\.pointBlocked : ''/)
301
+ has(editorPanel, /\?disabled=\$\{!!blocked\}/)
302
+ assert.equal(say('figure.text.section-editor-apex-blocked', { face: ko['figure.label.face-top']! }), '윗면에 붙은 부품이 있어 한 점으로 모을 수 없습니다.')
303
+ /* A pyramid in the section panel: its editor opens, and it has no corners and no inside. */
304
+ has(sectionPanel, /const e = body\?\.shape \? drawnOf\(body\.shape\) : undefined/)
305
+ })
@@ -939,5 +939,11 @@
939
939
  "figure.text.release-state-released": "Released · v{version}",
940
940
  "figure.text.release-state-draft-over": "Draft · boards use v{version}",
941
941
  "figure.text.release-state-draft-new": "Draft · never released",
942
- "figure.button.release": "Release"
942
+ "figure.button.release": "Release",
943
+ "figure.label.primitive-pyramid": "pyramid",
944
+ "figure.label.section-editor-apex": "draw the end to one point",
945
+ "figure.label.section-editor-apex-at": "apex {axis} (mm)",
946
+ "figure.label.section-editor-apex-mark": "apex",
947
+ "figure.text.section-editor-apex-blocked": "A part is fastened to its {face}, so it cannot be drawn to one point.",
948
+ "figure.label.section-editor-height": "height (mm)"
943
949
  }
@@ -939,5 +939,11 @@
939
939
  "figure.text.release-state-released": "発行済み · v{version}",
940
940
  "figure.text.release-state-draft-over": "下書き · ボードは v{version} を使用",
941
941
  "figure.text.release-state-draft-new": "下書き · 未発行",
942
- "figure.button.release": "発行"
942
+ "figure.button.release": "発行",
943
+ "figure.label.primitive-pyramid": "角錐",
944
+ "figure.label.section-editor-apex": "端を一点にまとめる",
945
+ "figure.label.section-editor-apex-at": "頂点 {axis} (mm)",
946
+ "figure.label.section-editor-apex-mark": "頂点",
947
+ "figure.text.section-editor-apex-blocked": "{face}に部品が付いているため、一点にまとめられません。",
948
+ "figure.label.section-editor-height": "高さ (mm)"
943
949
  }
@@ -939,5 +939,11 @@
939
939
  "figure.text.release-state-released": "발행됨 · v{version}",
940
940
  "figure.text.release-state-draft-over": "초안 · 보드는 v{version} 사용",
941
941
  "figure.text.release-state-draft-new": "초안 · 발행한 적 없음",
942
- "figure.button.release": "발행"
942
+ "figure.button.release": "발행",
943
+ "figure.label.primitive-pyramid": "뿔",
944
+ "figure.label.section-editor-apex": "끝을 한 점으로 모으기",
945
+ "figure.label.section-editor-apex-at": "꼭짓점 {axis} (mm)",
946
+ "figure.label.section-editor-apex-mark": "꼭짓점",
947
+ "figure.text.section-editor-apex-blocked": "{face}에 붙은 부품이 있어 한 점으로 모을 수 없습니다.",
948
+ "figure.label.section-editor-height": "높이 (mm)"
943
949
  }
@@ -939,5 +939,11 @@
939
939
  "figure.text.release-state-released": "Diterbitkan · v{version}",
940
940
  "figure.text.release-state-draft-over": "Draf · papan menggunakan v{version}",
941
941
  "figure.text.release-state-draft-new": "Draf · belum pernah diterbitkan",
942
- "figure.button.release": "Terbitkan"
942
+ "figure.button.release": "Terbitkan",
943
+ "figure.label.primitive-pyramid": "piramid",
944
+ "figure.label.section-editor-apex": "himpunkan hujung ke satu titik",
945
+ "figure.label.section-editor-apex-at": "puncak {axis} (mm)",
946
+ "figure.label.section-editor-apex-mark": "puncak",
947
+ "figure.text.section-editor-apex-blocked": "Ada bahagian yang dipasang pada {face}, jadi ia tidak boleh dihimpunkan ke satu titik.",
948
+ "figure.label.section-editor-height": "tinggi (mm)"
943
949
  }
@@ -939,5 +939,11 @@
939
939
  "figure.text.release-state-released": "已发布 · v{version}",
940
940
  "figure.text.release-state-draft-over": "草稿 · 看板使用 v{version}",
941
941
  "figure.text.release-state-draft-new": "草稿 · 从未发布",
942
- "figure.button.release": "发布"
942
+ "figure.button.release": "发布",
943
+ "figure.label.primitive-pyramid": "棱锥",
944
+ "figure.label.section-editor-apex": "将末端收为一点",
945
+ "figure.label.section-editor-apex-at": "顶点 {axis} (mm)",
946
+ "figure.label.section-editor-apex-mark": "顶点",
947
+ "figure.text.section-editor-apex-blocked": "{face}上连接着部件,无法收为一点。",
948
+ "figure.label.section-editor-height": "高度 (mm)"
943
949
  }