@things-factory/figure-ui 10.1.39 → 10.1.40

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 (33) hide show
  1. package/client/modeller/figure-v3-properties.ts +254 -0
  2. package/client/modeller/v3-canvas.ts +27 -3
  3. package/client/modeller/v3-editor.ts +39 -11
  4. package/client/modeller/v3-figure-facts.ts +53 -0
  5. package/client/modeller/v3-occupancy.ts +6 -2
  6. package/client/pages/figure-v3-modeller-page.ts +68 -13
  7. package/dist-client/modeller/figure-v3-properties.d.ts +46 -0
  8. package/dist-client/modeller/figure-v3-properties.js +259 -0
  9. package/dist-client/modeller/figure-v3-properties.js.map +1 -0
  10. package/dist-client/modeller/v3-canvas.d.ts +6 -0
  11. package/dist-client/modeller/v3-canvas.js +27 -3
  12. package/dist-client/modeller/v3-canvas.js.map +1 -1
  13. package/dist-client/modeller/v3-editor.js +39 -11
  14. package/dist-client/modeller/v3-editor.js.map +1 -1
  15. package/dist-client/modeller/v3-figure-facts.d.ts +26 -0
  16. package/dist-client/modeller/v3-figure-facts.js +34 -0
  17. package/dist-client/modeller/v3-figure-facts.js.map +1 -0
  18. package/dist-client/modeller/v3-occupancy.d.ts +5 -1
  19. package/dist-client/modeller/v3-occupancy.js +4 -2
  20. package/dist-client/modeller/v3-occupancy.js.map +1 -1
  21. package/dist-client/pages/figure-v3-modeller-page.d.ts +16 -2
  22. package/dist-client/pages/figure-v3-modeller-page.js +78 -22
  23. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  24. package/dist-client/route.d.ts +1 -1
  25. package/dist-client/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +4 -4
  27. package/test/v3-properties.test.ts +93 -0
  28. package/test/v3-theme.test.ts +67 -0
  29. package/translations/en.json +6 -0
  30. package/translations/ja.json +6 -0
  31. package/translations/ko.json +6 -0
  32. package/translations/ms.json +6 -0
  33. package/translations/zh.json +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.39",
3
+ "version": "10.1.40",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -26,7 +26,7 @@
26
26
  "clean": "npm run clean:server && npm run clean:client"
27
27
  },
28
28
  "dependencies": {
29
- "@hatiolab/figure-model": "^0.1.38",
29
+ "@hatiolab/figure-model": "^0.1.40",
30
30
  "@hatiolab/things-scene": "^10.1.26",
31
31
  "@material/web": "^2.0.0",
32
32
  "@operato/data-grist": "^10.0.0",
@@ -37,8 +37,8 @@
37
37
  "@operato/shell": "^10.0.0",
38
38
  "@operato/styles": "^10.0.0",
39
39
  "@operato/utils": "^10.0.0",
40
- "@things-factory/figure-service": "^10.1.39",
40
+ "@things-factory/figure-service": "^10.1.40",
41
41
  "three": "^0.185.1"
42
42
  },
43
- "gitHead": "fe5257ad8995fe7e79275e287f36e95b5fd32d1c"
43
+ "gitHead": "51c5d804c14ad8fcc6d4707e6ef80c9deede2766"
44
44
  }
@@ -0,0 +1,93 @@
1
+ /*
2
+ * **V3 화면에서 도형의 속성을 고친다** (V3 동등표 ②, 2026-09-24).
3
+ *
4
+ * V2 인스펙터는 이름 · 배치 기준 · 차지하는 공간 · 분류 · 태그를 맨 위에 둔다. V3 화면에는 타입 칸 하나뿐이었다.
5
+ * 두 축을 본다: 탭이 내는 명령이 figure-model 에서 **정말로** 그 일을 하는가, 그리고 화면이 명령과 행 쓰기를 잇는가.
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 { addV3Box, applyV3Authoring, createV3Asset, inspectV3Asset, parseV3Asset, serializeV3Asset } from '@hatiolab/figure-model'
13
+
14
+ import { tagsOf, v3FigureFacts } from '../client/modeller/v3-figure-facts.ts'
15
+ import { occupancyFromParts } from '../client/modeller/v3-occupancy.ts'
16
+
17
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
18
+ const probe = () => {
19
+ let asset = createV3Asset('Probe')
20
+ asset = addV3Box(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
21
+ return asset
22
+ }
23
+ const facts = (asset: ReturnType<typeof probe>) => v3FigureFacts(serializeV3Asset(asset))
24
+
25
+ test('탭이 읽는 사실 — 선언 전에는 배치 기준도 장착면도 「없음」이다', () => {
26
+ const read = facts(probe())
27
+ assert.equal(read.name, 'Probe')
28
+ assert.deepEqual(read.parts, ['body'])
29
+ assert.equal(read.declared, false)
30
+ assert.equal(read.placement, undefined, '없는 것을 바닥으로 읽지 않는다')
31
+ assert.equal(read.mounting, undefined)
32
+ })
33
+
34
+ test('배치 기준을 고르면 그 기준으로 공간을 선언한다 — 판정도 풀린다', () => {
35
+ const source = serializeV3Asset(probe())
36
+ const declared = applyV3Authoring(parseV3Asset(source), occupancyFromParts(source, 'ceiling'))
37
+
38
+ assert.equal(facts(declared).placement, 'ceiling')
39
+ assert.equal(facts(declared).declared, true)
40
+ assert.ok(!inspectV3Asset(declared).violations.some(v => v.code === 'OCCUPANCY_UNDECLARED'))
41
+ })
42
+
43
+ test('장착면을 정하면 탭이 그 부품과 면을 다시 읽고, 해제하면 없어진다', () => {
44
+ const source = serializeV3Asset(probe())
45
+ const declared = applyV3Authoring(parseV3Asset(source), occupancyFromParts(source))
46
+ const mounted = applyV3Authoring(declared, { kind: 'set-mounting-face', part: 'body', face: 'bottom' })
47
+
48
+ assert.deepEqual(facts(mounted).mounting, { part: 'body', face: 'bottom' })
49
+ assert.equal(facts(applyV3Authoring(mounted, { kind: 'clear-mounting-face' })).mounting, undefined)
50
+ })
51
+
52
+ test('이름을 바꾸면 자산의 이름이 바뀐다 — 저장하면 목록 이름이 이것을 따른다', () => {
53
+ assert.equal(facts(applyV3Authoring(probe(), { kind: 'rename', name: 'Roller conveyor' })).name, 'Roller conveyor')
54
+ })
55
+
56
+ test('태그 줄은 V2 와 같은 규칙으로 목록이 된다', () => {
57
+ assert.deepEqual(tagsOf('#반출, 컨베이어,, 반출 '), ['반출', '컨베이어'])
58
+ assert.deepEqual(tagsOf(''), [], '빈 줄은 전부 지우는 것이다')
59
+ })
60
+
61
+ const page = read('../client/pages/figure-v3-modeller-page.ts')
62
+
63
+ test('화면이 속성 탭을 걸고, 명령은 세션의 이력 하나로, 분류 · 태그는 행으로 간다', () => {
64
+ assert.ok(/renderTab\('properties', 'tune'/.test(page), '속성 탭')
65
+ assert.ok(/<figure-v3-properties\s+\?off=/.test(page), '감추되 떼지 않는다')
66
+ assert.ok(/@v3-command=\$\{this\.onCommand\}/.test(page) && /@v3-catalog=\$\{this\.onCatalog\}/.test(page))
67
+ const run = page.slice(page.indexOf('private runCommand'), page.indexOf('private onCatalog'))
68
+ assert.ok(/this\.session\.command\(actionOf\(\)\)/.test(run), '명령은 세션을 지난다 — 되돌리기가 먹는다')
69
+ const catalog = page.slice(page.indexOf('private onCatalog'), page.indexOf('private released'))
70
+ assert.ok(/updateFigure\(id, patch\)/.test(catalog), '분류 · 태그는 행에 쓴다')
71
+ })
72
+
73
+ test('이름을 바꾸면 제목 줄도 바뀐다 — 셸은 알려 줄 때만 제목을 다시 읽는다', () => {
74
+ const changed = page.slice(page.indexOf('private changed = () =>'), page.indexOf('private async readRow'))
75
+ assert.ok(/view\.name !== this\.titled[\s\S]*this\.updateContext\(\)/.test(changed))
76
+ })
77
+
78
+ test('발행한 뒤에도 행의 타입이 남는다 — 새로 짜면 잠긴 타입 칸이 빈다', () => {
79
+ const released = page.slice(page.indexOf('private released'), page.indexOf('private reverted'))
80
+ assert.ok(/this\.row = \{ \.\.\.this\.row, state:/.test(released))
81
+ })
82
+
83
+ test('속성 탭의 말은 다섯 말 모두에 있다', () => {
84
+ const tab = read('../client/modeller/figure-v3-properties.ts')
85
+ assert.ok(tab.includes('occupancyFromParts(this.source, placement)'), '고른 배치 기준으로 선언한다')
86
+ const keys = [...tab.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
87
+ for (const face of ['left', 'right', 'bottom', 'top', 'back', 'front']) keys.push(`figure.label.face-${face}`)
88
+ for (const one of ['floor', 'ceiling', 'center']) keys.push(`figure.label.placement-${one}`)
89
+ for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
90
+ const table = JSON.parse(read(`../translations/${lang}.json`))
91
+ for (const key of keys) assert.ok(table[key], `${lang}: ${key}`)
92
+ }
93
+ })
@@ -0,0 +1,67 @@
1
+ /*
2
+ * **V3 모델러는 테마를 따른다** (V3 동등표 ⑦, 2026-09-24).
3
+ *
4
+ * V3 캔버스와 편집기는 색을 박아 두었다. 어두운 테마에서 가운데만 밝은 판이 떴고, 경계선과 눌린 단추 · 오류 칸도
5
+ * 밝은 색 그대로였다. 색은 셸의 토큰에서 온다(UX 결정 — 바탕은 surface-container, 눌림은 secondary-container,
6
+ * 오류는 error-container).
7
+ */
8
+ import { test } from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+ import { readFileSync } from 'node:fs'
11
+ import { fileURLToPath } from 'node:url'
12
+
13
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
14
+ const canvas = read('../client/modeller/v3-canvas.ts')
15
+ const editor = read('../client/modeller/v3-editor.ts')
16
+
17
+ /** Hex colours that are not a `var(--token, #fallback)` fallback. */
18
+ const fixed = (text: string) =>
19
+ text
20
+ .replace(/var\(--[\w-]+,\s*#[0-9a-fA-F]{3,8}\)/g, '')
21
+ .match(/#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b(?![\w-])/g) ?? []
22
+
23
+ test('캔버스와 편집기에 박힌 색이 없다 — 토큰의 대체값만 남는다', () => {
24
+ assert.deepEqual(fixed(canvas), [], 'v3-canvas.ts')
25
+ assert.deepEqual(fixed(editor), [], 'v3-editor.ts')
26
+ })
27
+
28
+ test('캔버스 바탕은 CSS 와 3D 가 같은 토큰에서 온다', () => {
29
+ assert.match(canvas, /background: var\(--md-sys-color-surface-container\)/)
30
+ assert.match(canvas, /this\.scene\.background = new THREE\.Color\(colour\)/)
31
+ assert.match(canvas, /getComputedStyle\(this\)\.backgroundColor/, '3D 바탕이 호스트의 바탕을 읽는다')
32
+ })
33
+
34
+ test('테마를 바꾸면 3D 바탕을 다시 읽는다 — 한 번만 읽으면 가운데만 밝은 판이 남는다', () => {
35
+ assert.match(canvas, /observe\(document\.body, \{ attributes: true, attributeFilter: \['class'\] \}\)/)
36
+ const onChange = canvas.slice(canvas.indexOf('new MutationObserver'), canvas.indexOf('this.themeObserver.observe'))
37
+ assert.match(onChange, /this\.paintBackground\(\)/)
38
+ assert.match(onChange, /this\.draw\(\)/, '다시 읽고 그리지 않으면 다음 움직임까지 옛 색이다')
39
+ const leave = canvas.slice(canvas.indexOf('disconnectedCallback()'))
40
+ assert.match(leave, /this\.themeObserver\?\.disconnect\(\)/, '떠난 캔버스가 body 를 계속 지켜본다')
41
+ })
42
+
43
+ test('눌린 단추와 오류 칸은 셸이 두 테마 대비를 맞춘 칸을 쓴다', () => {
44
+ const pressed = editor.slice(editor.indexOf("button[aria-pressed='true']"), editor.indexOf('}', editor.indexOf("button[aria-pressed='true']")))
45
+ assert.match(pressed, /--md-sys-color-secondary-container\)/)
46
+ assert.match(pressed, /--md-sys-color-on-secondary-container\)/)
47
+ assert.match(pressed, /border-color: var\(--md-sys-color-primary\)/)
48
+ const error = editor.slice(editor.indexOf('.error {'), editor.indexOf('}', editor.indexOf('.error {')))
49
+ assert.match(error, /--md-sys-color-error-container\)/)
50
+ assert.match(error, /--md-sys-color-on-error-container\)/)
51
+ })
52
+
53
+ test('편집기는 창이 아니라 자기 폭으로 접는다 — 레일과 오른쪽 칸이 폭을 먼저 가져간다', () => {
54
+ assert.match(editor, /container-type: inline-size/)
55
+ assert.doesNotMatch(editor, /@media \(max-width/, '창 폭 기준이 남아 있다 — 레일을 펼치면 편집기가 오른쪽 칸 위로 넘친다')
56
+ assert.match(editor, /@container \(max-width: 659px\)/, '세 칸의 최소 폭 합(660) 아래에서 두 칸')
57
+ const one = editor.slice(editor.indexOf('@container (max-width: 419px)'))
58
+ assert.match(one, /grid-template-columns: minmax\(0, 1fr\)/, '420 아래에서 한 줄')
59
+ assert.match(one, /figure-v3-canvas \{\s*order: -1;/, '한 줄에서는 캔버스가 먼저')
60
+ })
61
+
62
+ test('넘침이 없으니 오른쪽 칸은 편집기를 가리지 않는다 — 가림은 우회였다', () => {
63
+ const page = read('../client/pages/figure-v3-modeller-page.ts')
64
+ const aside = page.slice(page.indexOf(' aside {'), page.indexOf('}', page.indexOf(' aside {')))
65
+ assert.doesNotMatch(aside, /z-index/)
66
+ assert.match(aside, /border-left: 1px solid var\(--md-sys-color-outline-variant\)/, '칸 경계선은 남는다')
67
+ })
@@ -36,6 +36,12 @@
36
36
  "figure.button.settle-on-floor": "settle on the floor",
37
37
  "figure.button.snap": "snap",
38
38
  "figure.button.take-n-changes": "apply {n}",
39
+ "figure.label.declared-space": "declared space",
40
+ "figure.text.catalog-needs-a-save": "can be set once the figure is saved.",
41
+ "figure.text.declared-space-declared": "declared from the parts.",
42
+ "figure.text.declared-space-none": "not declared yet.",
43
+ "figure.text.mounting-face-needs-space": "declare the space first to choose one.",
44
+ "figure.text.placement-redeclares": "changing it declares the space again from the parts as they are now.",
39
45
  "figure.text.recommended-fix-explained": "{n} changes rechecked against every resize case. They apply to this draft only; review and save when ready.",
40
46
  "figure.text.show-affected-parts": "show affected parts",
41
47
  "figure.button.to-floor": "to floor",
@@ -34,6 +34,12 @@
34
34
  "figure.button.settle-on-floor": "床に置く",
35
35
  "figure.button.snap": "snap",
36
36
  "figure.button.take-n-changes": "apply {n}",
37
+ "figure.label.declared-space": "空間宣言",
38
+ "figure.text.catalog-needs-a-save": "保存した後に設定できます。",
39
+ "figure.text.declared-space-declared": "部品から宣言しました。",
40
+ "figure.text.declared-space-none": "まだ宣言していません。",
41
+ "figure.text.mounting-face-needs-space": "先に空間を宣言すると選べます。",
42
+ "figure.text.placement-redeclares": "変更すると、今の部品で空間を宣言し直します。",
37
43
  "figure.text.recommended-fix-explained": "すべてのサイズ変更ケースを再検査した {n} 件の修正です。下書きだけに適用され、保存前に確認できます。",
38
44
  "figure.text.show-affected-parts": "影響する部品を見る",
39
45
  "figure.button.to-floor": "to floor",
@@ -36,6 +36,12 @@
36
36
  "figure.button.settle-on-floor": "바닥에 앉히기",
37
37
  "figure.button.snap": "눈금에",
38
38
  "figure.button.take-n-changes": "{n}개 적용",
39
+ "figure.label.declared-space": "공간 선언",
40
+ "figure.text.catalog-needs-a-save": "저장한 뒤에 정할 수 있습니다.",
41
+ "figure.text.declared-space-declared": "부품으로 선언했습니다.",
42
+ "figure.text.declared-space-none": "아직 선언하지 않았습니다.",
43
+ "figure.text.mounting-face-needs-space": "공간을 먼저 선언해야 고를 수 있습니다.",
44
+ "figure.text.placement-redeclares": "바꾸면 지금 부품으로 공간을 다시 선언합니다.",
39
45
  "figure.text.recommended-fix-explained": "서버가 전체 크기 변경을 다시 검사한 {n}개 수정입니다. 초안에만 적용되며 저장 전 확인할 수 있습니다.",
40
46
  "figure.text.show-affected-parts": "영향 받는 부품 보기",
41
47
  "figure.button.to-floor": "바닥에",
@@ -34,6 +34,12 @@
34
34
  "figure.button.settle-on-floor": "letak di lantai",
35
35
  "figure.button.snap": "snap",
36
36
  "figure.button.take-n-changes": "apply {n}",
37
+ "figure.label.declared-space": "ruang yang diisytiharkan",
38
+ "figure.text.catalog-needs-a-save": "boleh ditetapkan selepas disimpan.",
39
+ "figure.text.declared-space-declared": "diisytiharkan daripada bahagian.",
40
+ "figure.text.declared-space-none": "belum diisytiharkan.",
41
+ "figure.text.mounting-face-needs-space": "isytiharkan ruang dahulu untuk memilih.",
42
+ "figure.text.placement-redeclares": "menukarnya mengisytiharkan semula ruang daripada bahagian semasa.",
37
43
  "figure.text.recommended-fix-explained": "{n} pembetulan telah diperiksa semula untuk semua keadaan saiz. Ia hanya digunakan pada draf ini; semak dan simpan apabila sedia.",
38
44
  "figure.text.show-affected-parts": "tunjukkan bahagian terjejas",
39
45
  "figure.button.to-floor": "to floor",
@@ -34,6 +34,12 @@
34
34
  "figure.button.settle-on-floor": "落到地面",
35
35
  "figure.button.snap": "snap",
36
36
  "figure.button.take-n-changes": "apply {n}",
37
+ "figure.label.declared-space": "空间声明",
38
+ "figure.text.catalog-needs-a-save": "保存后即可设置。",
39
+ "figure.text.declared-space-declared": "已根据部件声明。",
40
+ "figure.text.declared-space-none": "尚未声明。",
41
+ "figure.text.mounting-face-needs-space": "请先声明空间,然后才能选择。",
42
+ "figure.text.placement-redeclares": "更改后将按当前部件重新声明空间。",
37
43
  "figure.text.recommended-fix-explained": "已针对所有尺寸变化情况重新检查的 {n} 项修正。它们只应用到草稿;请确认后再保存。",
38
44
  "figure.text.show-affected-parts": "查看受影响部件",
39
45
  "figure.button.to-floor": "to floor",