@things-factory/figure-ui 10.1.77 → 10.1.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.77",
3
+ "version": "10.1.79",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -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.58",
44
- "@things-factory/figure-service": "^10.1.76",
43
+ "@things-factory/ai-assistant": "^10.1.79",
44
+ "@things-factory/figure-service": "^10.1.79",
45
45
  "three": "^0.185.1"
46
46
  },
47
- "gitHead": "f5ff07816a0f96325cc599c73d7fba06dd719c6f"
47
+ "gitHead": "94cde9ba17dfdddb0a16139b71e5a7e370977e4a"
48
48
  }
@@ -432,6 +432,11 @@ describe('a figure made in the modeller is given its size from its parts (user 2
432
432
  assert.ok(await width.evaluate(s => !!s), 'the width can follow the figure size')
433
433
  await width.select('instance:x')
434
434
  await sleep(600)
435
+ /* Refusals and notices go to the app's notification: heard here as the event it is. */
436
+ await page().evaluate(() => {
437
+ window.__told = []
438
+ document.addEventListener('notify', e => window.__told.push(e.detail))
439
+ })
435
440
  const plus = await handle('input')
436
441
  assert.ok(await plus.evaluate(i => !!i), 'a following width offers its difference')
437
442
  await plus.evaluate(i => i.scrollIntoView({ block: 'center' }))
@@ -441,15 +446,15 @@ describe('a figure made in the modeller is given its size from its parts (user 2
441
446
  await sleep(600)
442
447
  /* The declared sizes start at 1 mm: less 50 the box would be gone below 50, so the link is refused -- with the way on. */
443
448
  assert.match((await state()).error, /figure.text.link-needs-smaller-range/, 'refused, said in words')
444
- const narrow = await page().evaluateHandle(() => {
445
- const deep = (root = document) => [...root.querySelectorAll('button')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
446
- return deep().find(b => b.textContent.includes('figure.button.narrow-range-from')) ?? null
449
+ const told = await read(() => {
450
+ const last = window.__told.at(-1)
451
+ return last && { level: last.level, message: last.message, label: last.option?.action?.label }
447
452
  })
448
- assert.ok(await narrow.evaluate(b => !!b), 'the same link is offered with the sizes narrowed')
449
- await click(await narrow.evaluate(b => {
450
- const r = b.getBoundingClientRect()
451
- return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
452
- }))
453
+ assert.equal(told?.level, 'error', 'said in the app\'s notification')
454
+ assert.match(told?.message ?? '', /figure.text.link-needs-smaller-range/)
455
+ assert.match(told?.label ?? '', /figure.button.narrow-range-from/, 'the same link is offered with the sizes narrowed')
456
+ await read(() => window.__told.at(-1).option.action.callback())
457
+ await sleep(600)
453
458
  assert.equal((await state()).error, '', 'taken')
454
459
  assert.equal((await body('box')).dimensions.width, 350, 'the figure\'s 400 less 50')
455
460
  })
@@ -658,16 +663,17 @@ describe('a part follows the figure size in one step (follow-size, 2026-09-26)',
658
663
  return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
659
664
  })
660
665
  assert.ok(at, 'the button is offered for a box that follows nothing')
666
+ await page().evaluate(() => {
667
+ window.__told = []
668
+ document.addEventListener('notify', e => window.__told.push(e.detail))
669
+ })
661
670
  const start = await state()
662
671
  await click(at)
663
672
  const after = await state()
664
673
  assert.equal(after.error, '', 'not refused')
665
674
  assert.equal(after.revision, start.revision + 1, 'one step')
666
675
  assert.deepEqual(await read(() => probe.fields('size.')), { 'size.x': 400, 'size.y': 400, 'size.z': 400 }, 'the size is the box as it stands')
667
- const notice = await page().evaluate(() => {
668
- const deep = (root = document) => [...root.querySelectorAll('.notice')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
669
- return deep()[0]?.textContent?.trim() ?? null
670
- })
676
+ const notice = await read(() => (window.__told ?? []).filter(n => n.level === 'info').at(-1)?.message ?? null)
671
677
  assert.ok(notice?.includes('figure.text.size-declared-to-follow'), `a line says so: ${notice}`)
672
678
  const grown = await read(() => {
673
679
  const editor = document.querySelector('figure-editor')
@@ -614,7 +614,7 @@ test('치수 칸 — 「도형 크기를 따라감」 단추는 follow-size 한
614
614
  assert.ok(dims.includes("followOf(asset, this.partId, axis).of === 'instance'"), '다 따르면 단추가 없다')
615
615
  const editor = read('../client/modeller/editor.ts')
616
616
  assert.ok(editor.includes("if (action.kind === 'follow-size' && !hadSize)"), '크기를 대신 정했을 때만')
617
- assert.ok(editor.includes('<div role="status" class="notice">'))
617
+ assert.ok(editor.includes("notify({ level: 'info', message: this.notice })"), '그 한 줄은 앱의 알림으로')
618
618
  const ko = JSON.parse(read('../translations/ko.json'))
619
619
  assert.match(ko['figure.text.size-declared-to-follow'], /\{x\} × \{y\} × \{z\} mm/)
620
620
  })
@@ -72,3 +72,16 @@ test('부품 이름은 figure-model 의 rename-part 로 바꾸고, 거절은 까
72
72
  for (const key of ['figure.text.part-name-taken', 'figure.text.part-name-invalid']) assert.ok(table[key]?.includes('{name}'), `${lang}: ${key}`)
73
73
  }
74
74
  })
75
+
76
+ test('거절은 앱의 알림(notify)이나 확인 창(OxPrompt)으로 — 모델러가 따로 그리는 띠는 없다 (2026-09-27)', () => {
77
+ assert.doesNotMatch(editor, /role="alert" class="error"/, '따로 그리던 빨간 띠가 없다')
78
+ assert.doesNotMatch(editor, /class="notice"/, '따로 그리던 알림 줄이 없다')
79
+ const tell = editor.slice(editor.indexOf(' private tell('), editor.indexOf(' protected willUpdate('))
80
+ assert.match(tell, /else notify\(\{ level: 'error', message: this\.error \}\)/, '길이 없는 거절은 error 알림 — 닫을 때까지 남는다')
81
+ assert.match(tell, /if \(retry\) this\.ask\(this\.error, retry\)/, '이어 할 길이 있으면 묻는다')
82
+ const ask = editor.slice(editor.indexOf(' private async ask('), editor.indexOf(' protected willUpdate('))
83
+ assert.match(ask, /OxPrompt\.open\(\{[\s\S]*confirmButton: \{ text: retry\.label \}/, '앱의 확인 창에서, 그 길의 이름으로')
84
+ assert.match(ask, /if \(!agreed\) return[\s\S]*retry\.run\(\)/, '확인하면 바로 진행한다')
85
+ assert.match(tell, /notify\(\{ level: 'info', message: this\.notice \}\)/, '지우며 풀어 준 것은 info 로')
86
+ assert.match(editor, /updated\(changed: PropertyValues<this>\) \{\s*super\.updated\?\.\(changed\)\s*this\.tell\(changed\)/)
87
+ })
@@ -98,7 +98,7 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
98
98
  assert.doesNotMatch(unknown, /something in English/)
99
99
  })
100
100
 
101
- test('공간 선언에 든 부품 — 지우고 공간을 남은 부품에 맞추는 길을 한 번에 권한다(차단기의 떠도는 상자, 09-25)', async () => {
101
+ test('공간 선언에 든 부품 — 한 번 눌러 지우고, 공간은 남은 부품에 다시 맞추며 그렇다고 한 줄 말한다(묻지 않는다, 2026-09-27)', async () => {
102
102
  const { inDeclaredRoom } = await import('../client/modeller/refusals.ts')
103
103
  const fm = await import('@hatiolab/figure-model')
104
104
  let a = fm.addFigureBox(fm.createFigureAsset('Gate'), { id: 'housing', size: { x: 300, y: 1000, z: 300 }, position: { x: 0, y: 0, z: 0 } })
@@ -118,8 +118,11 @@ test('공간 선언에 든 부품 — 지우고 공간을 남은 부품에 맞
118
118
  assert.equal(after.document.model.nodes.some(n => n.id === 'stray'), false)
119
119
  assert.ok(after.occupancy?.bounds, '공간은 남은 부품으로 다시 선언된다')
120
120
  const editor = readFileSync(fileURLToPath(new URL('../client/modeller/editor.ts', import.meta.url)), 'utf8')
121
- assert.ok(editor.includes("if (!refitRoom && inDeclaredRoom(error)) this.retry = { label: i18next.t('figure.button.remove-and-refit-room', { part: id }), run: () => this.removePart(id, true) }"))
122
- assert.ok(ko['figure.button.remove-and-refit-room'].includes('{part}'))
121
+ const remove = editor.slice(editor.indexOf(' private removePart('), editor.indexOf(' private toggleHiddenPart('))
122
+ assert.match(remove, /if \(!refitRoom && inDeclaredRoom\(error\)\) \{\s*this\.removePart\(id, true\)/, '거절 없이 곧바로 다시 맞춰 지운다')
123
+ assert.match(remove, /figure\.text\.removal-space-refitted/, '그렇다고 한 줄')
124
+ assert.doesNotMatch(remove, /this\.retry = \{/, '묻거나 권하지 않는다')
125
+ assert.ok(ko['figure.text.removal-space-refitted'])
123
126
  })
124
127
 
125
128
  test('배치를 바꿀 수 없는 까닭 — 평면에 붙은 부품과 새 평면이 받는 면을 말한다(벽 배치, 09-25)', async () => {
@@ -30,14 +30,12 @@ test('캔버스 바탕은 작업대 회색 — 판의 하늘색이라 환경 탭
30
30
  assert.match(read('../client/modeller/figure-view.ts'), /skyColor: WORKBENCH/)
31
31
  })
32
32
 
33
- test('눌린 단추와 오류 칸은 셸이 두 테마 대비를 맞춘 칸을 쓴다', () => {
33
+ test('눌린 단추는 셸이 두 테마 대비를 맞춘 칸을 쓴다 — 오류는 앱의 알림이 그린다 (2026-09-27)', () => {
34
34
  const pressed = editor.slice(editor.indexOf("button[aria-pressed='true']"), editor.indexOf('}', editor.indexOf("button[aria-pressed='true']")))
35
35
  assert.match(pressed, /--md-sys-color-secondary-container\)/)
36
36
  assert.match(pressed, /--md-sys-color-on-secondary-container\)/)
37
37
  assert.match(pressed, /border-color: var\(--md-sys-color-primary\)/)
38
- const error = editor.slice(editor.indexOf('.error {'), editor.indexOf('}', editor.indexOf('.error {')))
39
- assert.match(error, /--md-sys-color-error-container\)/)
40
- assert.match(error, /--md-sys-color-on-error-container\)/)
38
+ assert.equal(editor.includes('.error {'), false, '모델러가 따로 칠하는 오류 칸이 없다')
41
39
  })
42
40
 
43
41
  test('편집기는 창이 아니라 자기 폭으로 접는다 — 레일이 폭을 먼저 가져간다', () => {
@@ -640,7 +640,6 @@
640
640
  "figure.text.proportion-to-follow": "{count} lengths still have their own number. made to follow, they grow by the figure's ratio.",
641
641
  "figure.button.follow-size-in-proportion": "make every part follow",
642
642
  "figure.text.proportion-kept": "keeps its own number: {what}",
643
- "figure.button.remove-and-refit-room": "delete {part} and fit the space to the parts left",
644
643
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
645
644
  "figure.label.placement-wall": "wall (wall)",
646
645
  "figure.text.placement-plane-refused": "{parts} cannot go on a {placement} by the face fastened to the mounting plane. detach it or fasten it by its {faces} first.",
@@ -663,5 +662,6 @@
663
662
  "figure.text.removal-mounting-face": "The mounting face is released.",
664
663
  "figure.text.edit-refused": "This edit cannot be made. ({code})",
665
664
  "figure.text.edit-refused-node-limit": "The figure is too large to add more. It holds up to {max} nodes.",
666
- "figure.text.recipe-step-refused": "Stopped at step {step} of the recipe. {why} ({code})"
665
+ "figure.text.recipe-step-refused": "Stopped at step {step} of the recipe. {why} ({code})",
666
+ "figure.text.removal-space-refitted": "The space is fitted to the parts left."
667
667
  }
@@ -640,7 +640,6 @@
640
640
  "figure.text.proportion-to-follow": "長さ {count} 個がまだ固有の数値です。追従させると図形と同じ比率で大きくなります。",
641
641
  "figure.button.follow-size-in-proportion": "すべての部品を追従させる",
642
642
  "figure.text.proportion-kept": "固有の数値のまま: {what}",
643
- "figure.button.remove-and-refit-room": "「{part}」を削除し、空間を残りの部品に合わせる",
644
643
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
645
644
  "figure.label.placement-wall": "壁基準 (wall)",
646
645
  "figure.text.placement-plane-refused": "{parts} は取付平面に付いている面では {placement} に付けられません。外すか {faces} 面で付け直してから変更してください。",
@@ -663,5 +662,6 @@
663
662
  "figure.text.removal-mounting-face": "取付面が外れました。",
664
663
  "figure.text.edit-refused": "この編集はできません。({code})",
665
664
  "figure.text.edit-refused-node-limit": "図形が大きいためこれ以上追加できません。ノードは {max} 個までです。",
666
- "figure.text.recipe-step-refused": "レシピの {step} 番目の手順で止まりました。{why} ({code})"
665
+ "figure.text.recipe-step-refused": "レシピの {step} 番目の手順で止まりました。{why} ({code})",
666
+ "figure.text.removal-space-refitted": "空間を残りの部品に合わせ直しました。"
667
667
  }
@@ -640,7 +640,6 @@
640
640
  "figure.text.proportion-to-follow": "길이 {count}개가 아직 자기 숫자입니다. 따라가게 하면 도형과 같은 비율로 커집니다.",
641
641
  "figure.button.follow-size-in-proportion": "모든 부품이 따라가게",
642
642
  "figure.text.proportion-kept": "자기 숫자로 남는 것: {what}",
643
- "figure.button.remove-and-refit-room": "'{part}' 지우고 공간을 남은 부품에 맞추기",
644
643
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
645
644
  "figure.label.placement-wall": "벽 기반 (wall)",
646
645
  "figure.text.placement-plane-refused": "{parts} 가 장착 평면에 붙은 면으로는 {placement} 에 붙일 수 없습니다. 떼거나 {faces} 면으로 다시 붙인 뒤 바꾸세요.",
@@ -663,5 +662,6 @@
663
662
  "figure.text.removal-mounting-face": "장착면이 풀렸습니다.",
664
663
  "figure.text.edit-refused": "이 편집은 할 수 없습니다. ({code})",
665
664
  "figure.text.edit-refused-node-limit": "도형이 커서 더 넣을 수 없습니다. 노드는 {max} 개까지입니다.",
666
- "figure.text.recipe-step-refused": "레시피의 {step} 번째 단계에서 막혔습니다. {why} ({code})"
665
+ "figure.text.recipe-step-refused": "레시피의 {step} 번째 단계에서 막혔습니다. {why} ({code})",
666
+ "figure.text.removal-space-refitted": "공간을 남은 부품에 다시 맞췄습니다."
667
667
  }
@@ -640,7 +640,6 @@
640
640
  "figure.text.proportion-to-follow": "{count} panjang masih mempunyai nombor sendiri. apabila dijadikan mengikut, ia membesar mengikut nisbah rajah.",
641
641
  "figure.button.follow-size-in-proportion": "jadikan semua bahagian mengikut",
642
642
  "figure.text.proportion-kept": "kekal dengan nombor sendiri: {what}",
643
- "figure.button.remove-and-refit-room": "padam '{part}' dan muatkan ruang pada bahagian yang tinggal",
644
643
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
645
644
  "figure.label.placement-wall": "dinding (wall)",
646
645
  "figure.text.placement-plane-refused": "{parts} tidak boleh dipasang pada {placement} dengan muka yang dipasang pada satah pemasangan. tanggalkan atau pasang semula dengan muka {faces} dahulu.",
@@ -663,5 +662,6 @@
663
662
  "figure.text.removal-mounting-face": "Muka pemasangan dilepaskan.",
664
663
  "figure.text.edit-refused": "Suntingan ini tidak boleh dibuat. ({code})",
665
664
  "figure.text.edit-refused-node-limit": "Rajah terlalu besar untuk ditambah. Ia memuatkan sehingga {max} nod.",
666
- "figure.text.recipe-step-refused": "Terhenti pada langkah {step} resipi. {why} ({code})"
665
+ "figure.text.recipe-step-refused": "Terhenti pada langkah {step} resipi. {why} ({code})",
666
+ "figure.text.removal-space-refitted": "Ruang dipadankan semula dengan bahagian yang tinggal."
667
667
  }
@@ -640,7 +640,6 @@
640
640
  "figure.text.proportion-to-follow": "还有 {count} 个长度是自身数值。设为跟随后,将按图形的同一比例变大。",
641
641
  "figure.button.follow-size-in-proportion": "让所有部件跟随",
642
642
  "figure.text.proportion-kept": "保留自身数值: {what}",
643
- "figure.button.remove-and-refit-room": "删除 '{part}' 并使空间适配剩余部件",
644
643
  "figure.text.preview-clamped": "{axis} {asked} → {used}",
645
644
  "figure.label.placement-wall": "墙面基准 (wall)",
646
645
  "figure.text.placement-plane-refused": "{parts} 贴在安装平面上的面不能用于 {placement}。请先拆下或改用 {faces} 面贴合后再更改。",
@@ -663,5 +662,6 @@
663
662
  "figure.text.removal-mounting-face": "安装面已解除。",
664
663
  "figure.text.edit-refused": "无法进行此编辑。({code})",
665
664
  "figure.text.edit-refused-node-limit": "图形过大,无法再添加。节点上限为 {max} 个。",
666
- "figure.text.recipe-step-refused": "配方在第 {step} 步停止。{why} ({code})"
665
+ "figure.text.recipe-step-refused": "配方在第 {step} 步停止。{why} ({code})",
666
+ "figure.text.removal-space-refitted": "空间已重新贴合剩余部件。"
667
667
  }