@things-factory/figure-ui 10.1.168 → 10.1.169

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.168",
3
+ "version": "10.1.169",
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.7.0",
30
+ "@hatiolab/figure-model": "^0.7.1",
31
31
  "@hatiolab/things-scene": "^10.1.90",
32
32
  "@material/web": "^2.0.0",
33
33
  "@operato/data-grist": "^10.0.0",
@@ -41,11 +41,11 @@
41
41
  "@operato/slot-layout-editor": "^10.29.25",
42
42
  "@operato/styles": "^10.0.0",
43
43
  "@operato/utils": "^10.0.0",
44
- "@things-factory/ai-assistant": "^10.1.168",
45
- "@things-factory/figure-service": "^10.1.168",
46
- "@things-factory/figure-viewer": "^10.1.168",
44
+ "@things-factory/ai-assistant": "^10.1.169",
45
+ "@things-factory/figure-service": "^10.1.169",
46
+ "@things-factory/figure-viewer": "^10.1.169",
47
47
  "@things-factory/setting-base": "^10.1.167",
48
48
  "three": "^0.185.1"
49
49
  },
50
- "gitHead": "fe067ff11d90effffc2523374453d9b8313b489f"
50
+ "gitHead": "9d71d02b443f3e0080b25de1768b982c51a64111"
51
51
  }
@@ -10,7 +10,7 @@ import { createFigureAsset, addFigureBox, serializeFigureAsset } from '@hatiolab
10
10
 
11
11
  import { createFigureProductSession } from '../client/modeller/product-session.js'
12
12
  import type { FigureProposalStep } from '@hatiolab/figure-model'
13
- import { pendNewFigure, runFigureProposal, takeFigureOffer, takePendingNewFigure, type FigureApplyDetail } from '../client/modeller/ai-proposal.js'
13
+ import { askingToLeave, pendNewFigure, runFigureProposal, takeFigureOffer, takePendingNewFigure, type FigureApplyDetail } from '../client/modeller/ai-proposal.js'
14
14
 
15
15
  const ko = JSON.parse(readFileSync(fileURLToPath(new URL('../translations/ko.json', import.meta.url)), 'utf8'))
16
16
  const say = (key: string, params: Record<string, string> = {}) => {
@@ -82,12 +82,26 @@ test('applied: said with how many commands, and the card carries the version to
82
82
  assert.equal(target.offered[0]!.revision, 3)
83
83
  })
84
84
 
85
+ test('a revision carries the figure it was made for, and only a revision does (audit N3)', async () => {
86
+ const target = taker({ applied: true })
87
+ await runFigureProposal({ steps, figureId: 'a', context: 'c', revision: 0 }, target, say)
88
+ assert.equal(target.offered[0]!.figureId, 'a')
89
+ const fresh = taker({ applied: true })
90
+ await runFigureProposal({ steps, create: true, figureId: 'a' }, fresh, say)
91
+ assert.equal(fresh.offered[0]!.figureId, undefined)
92
+ })
93
+
85
94
  test('refused: a stale version is said as what happened; any other refusal carries its reason', async () => {
86
95
  const stale = await runFigureProposal({ steps }, taker({ applied: false, code: 'REVISION_CONFLICT', error: 'editor changed' }), say)
87
96
  assert.equal(stale.verdict, 'refused')
88
97
  assert.match(stale.note!, /모델이 바뀌어/)
89
98
  const other = await runFigureProposal({ steps }, taker({ applied: false, code: 'ATTACH_FACE', error: 'faces along x' }), say)
90
- assert.match(other.note!, /적용하지 않았습니다: faces along x/)
99
+ /* Said from the code in the reader's words, as the modeller says an edit it refuses -- never the kernel's English (audit N6). */
100
+ assert.match(other.note!, /적용하지 않았습니다: 이 편집은 할 수 없습니다\. \(ATTACH_FACE\)/)
101
+ assert.doesNotMatch(other.note!, /faces along x/)
102
+ const busy = await runFigureProposal({ steps }, taker({ applied: false, code: 'SESSION_SAVING', error: 'save first' }), say)
103
+ assert.match(busy.note!, /저장이 끝난 뒤/)
104
+ assert.doesNotMatch(busy.note!, /save first/)
91
105
  })
92
106
 
93
107
  test('a revision with no modeller to take it is not applied anywhere else', async () => {
@@ -165,8 +179,10 @@ test('the same new figure\'s card in another tab before the first save makes a s
165
179
  assert.notEqual(ids[0], ids[1], 'two figures')
166
180
  })
167
181
 
168
- test('an empty card is not applied', async () => {
169
- assert.equal((await runFigureProposal({ steps: [] }, taker({ applied: true }), say)).verdict, 'unavailable')
182
+ test('an empty card is not applied, and says so (audit N7)', async () => {
183
+ const out = await runFigureProposal({ steps: [] }, taker({ applied: true }), say)
184
+ assert.equal(out.verdict, 'unavailable')
185
+ assert.match(out.note ?? '', /명령이 없어/)
170
186
  })
171
187
 
172
188
  test('a new figure\'s steps wait for the page once, and lapse if no page takes them', async () => {
@@ -181,3 +197,26 @@ test('a new figure\'s steps wait for the page once, and lapse if no page takes t
181
197
  assert.deepEqual(await lapsing, { applied: false, code: 'not-opened' })
182
198
  assert.equal(takePendingNewFigure(), undefined, 'a later new figure does not receive them')
183
199
  })
200
+
201
+ /*
202
+ A new figure's card run from a modeller with unsaved edits waits behind the question whether to leave them (figure
203
+ audit 2026-10-16 N4). Its clock stopped for that question: before, a person who took longer than it to say leave lost
204
+ their edits and got a blank new figure, the steps gone; one who said stay got "could not be opened" ten seconds later.
205
+ */
206
+ test('a new figure waiting behind the question whether to leave: no clock while asked, said at once when the person stays', async () => {
207
+ const leaving = pendNewFigure(steps, undefined, 10)
208
+ const answer = askingToLeave()!
209
+ await new Promise(resolve => setTimeout(resolve, 40))
210
+ answer(true)
211
+ const taken = takePendingNewFigure()
212
+ assert.ok(taken, 'still waiting for the page that opens')
213
+ taken!.settle({ applied: true })
214
+ assert.deepEqual(await leaving, { applied: true })
215
+
216
+ const staying = pendNewFigure(steps, undefined, 10_000)
217
+ askingToLeave()!(false)
218
+ assert.deepEqual(await staying, { applied: false, code: 'not-opened' })
219
+ assert.equal(takePendingNewFigure(), undefined)
220
+
221
+ assert.equal(askingToLeave(), undefined, 'nothing waiting: nothing to hold')
222
+ })
@@ -43,6 +43,44 @@ test('the dock applies a proposal through the page, on the version the model saw
43
43
  assert.equal(create.applied, undefined)
44
44
  })
45
45
 
46
+ /*
47
+ The dock reads the draft when the person sends (figure audit 2026-10-16 N5). A save in flight is no edit: reading the
48
+ draft then was refused with the edit guard's Korean "save first" line, which the dock showed as the turn's error.
49
+ */
50
+ test('the draft is read while a save is in flight', async () => {
51
+ const source = serializeFigureAsset(addFigureBox(createFigureAsset('A'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } }))
52
+ let finish: (value: { id: string; source: string }) => void = () => {}
53
+ const session = createFigureProductSession({
54
+ load: async (id: string) => ({ id, source }),
55
+ save: (q: { id?: string; source: string }) => new Promise(resolve => (finish = () => resolve({ id: q.id ?? 'a', source: q.source })))
56
+ })
57
+ await session.open('a')
58
+ session.command({ kind: 'rename', name: 'B' } as never)
59
+ const saving = session.save()
60
+ assert.equal(session.read().saving, true)
61
+ assert.equal(JSON.parse(session.source()).name, 'B')
62
+ finish({ id: 'a', source })
63
+ await saving
64
+ })
65
+
66
+ /*
67
+ A revision is for the figure the model read, and a page with another figure open leaves it untaken (figure audit
68
+ 2026-10-16 N3): the handler then says it needs that figure open. The version alone did not tell them apart -- two
69
+ figures opened in two page loads were both at their first context and revision 0.
70
+ */
71
+ test('a revision made for another figure is left untaken, whatever version it names', async () => {
72
+ const source = serializeFigureAsset(addFigureBox(createFigureAsset('A'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } }))
73
+ const session = createFigureProductSession({ load: async (id: string) => ({ id, source }), save: async (q: { id?: string; source: string }) => ({ id: q.id ?? 'a', source: q.source }) })
74
+ await session.open('a')
75
+ const now = session.read()
76
+ const other: FigureApplyDetail = { key: K1, steps: lid, create: false, figureId: 'b', context: now.context, revision: now.revision }
77
+ assert.equal(takeFigureOffer(session, other, { onSavedFigure: true, say }), false)
78
+ assert.equal(other.applied, undefined)
79
+ assert.equal(session.read().revision, now.revision)
80
+ const mine: FigureApplyDetail = { key: K1, steps: lid, create: false, figureId: 'a', context: now.context, revision: now.revision }
81
+ assert.equal(takeFigureOffer(session, mine, { onSavedFigure: true, say }), true)
82
+ })
83
+
46
84
  /*
47
85
  A figure not saved yet is a subject too (ADR-0061 decision 3): its conversation is anchored on the creation id the
48
86
  first save inserts the row under, so the anchor is the same before and after that save. Taking a new figure's
@@ -712,6 +712,7 @@
712
712
  "figure.text.proposal-applied": "applied {n} commands as one step.",
713
713
  "figure.text.proposal-saved-into": "Saved into: {names}",
714
714
  "figure.text.proposal-needs-modeller": "this proposal can be applied only while its figure is open in the modeller.",
715
+ "figure.text.proposal-empty": "this proposal has no commands, so nothing was applied. ask again.",
715
716
  "figure.text.proposal-new-figure-unsaved": "a new figure is being made, so the proposal was not applied. save or discard it, then run it again.",
716
717
  "figure.text.proposal-not-opened": "a new figure could not be opened in the modeller, so the proposal was not applied.",
717
718
  "figure.text.proposal-refused": "the modeller did not apply the proposal: {reason}",
@@ -754,6 +755,11 @@
754
755
  "figure.text.removal-bindings": "{places} went with it.",
755
756
  "figure.text.removal-mounting-face": "The mounting face is released.",
756
757
  "figure.text.edit-refused": "This edit cannot be made. ({code})",
758
+ "figure.text.session-not-open": "open the model first.",
759
+ "figure.text.session-saving": "wait for the save to finish, then edit.",
760
+ "figure.text.session-version-missing": "an AI edit needs the model version it was asked on.",
761
+ "figure.text.session-new-figure-touched": "this can be used only on a new model not yet edited.",
762
+ "figure.text.session-proposal-key": "the proposal's identifier is not valid.",
757
763
  "figure.text.edit-refused-node-limit": "The figure is too large to add more. It holds up to {max} nodes.",
758
764
  "figure.text.document-too-large": "The figure is {at} KiB, over the {max} KiB it can be saved at.",
759
765
  "figure.text.document-too-large-unmeasured": "The figure is over the {max} KiB it can be saved at.",
@@ -712,6 +712,7 @@
712
712
  "figure.text.proposal-applied": "se aplicaron {n} comandos como un solo paso.",
713
713
  "figure.text.proposal-saved-into": "Guardado en: {names}",
714
714
  "figure.text.proposal-needs-modeller": "esta propuesta solo se puede aplicar mientras su figura esté abierta en el modelador.",
715
+ "figure.text.proposal-empty": "esta propuesta no tiene comandos, por lo que no se aplicó. vuelva a pedirla.",
715
716
  "figure.text.proposal-new-figure-unsaved": "se está creando una nueva figura, por lo que no se aplicó la propuesta. guárdela o descártela y vuelva a ejecutarla.",
716
717
  "figure.text.proposal-not-opened": "no se pudo abrir una nueva figura en el modelador, por lo que no se aplicó la propuesta.",
717
718
  "figure.text.proposal-refused": "el modelador no aplicó la propuesta: {reason}",
@@ -754,6 +755,11 @@
754
755
  "figure.text.removal-bindings": "{places} se eliminaron con ella.",
755
756
  "figure.text.removal-mounting-face": "La cara de montaje se ha liberado.",
756
757
  "figure.text.edit-refused": "No se puede realizar esta edición. ({code})",
758
+ "figure.text.session-not-open": "abra primero el modelo.",
759
+ "figure.text.session-saving": "espere a que termine de guardarse y luego edite.",
760
+ "figure.text.session-version-missing": "una edición de IA necesita la versión del modelo en la que se pidió.",
761
+ "figure.text.session-new-figure-touched": "solo se puede usar en un modelo nuevo que aún no se ha editado.",
762
+ "figure.text.session-proposal-key": "el identificador de la propuesta no es válido.",
757
763
  "figure.text.edit-refused-node-limit": "La figura es demasiado grande para añadir más. Admite hasta {max} nodos.",
758
764
  "figure.text.document-too-large": "La figura ocupa {at} KiB, más que los {max} KiB con los que se puede guardar.",
759
765
  "figure.text.document-too-large-unmeasured": "La figura supera los {max} KiB con los que se puede guardar.",
@@ -712,6 +712,7 @@
712
712
  "figure.text.proposal-applied": "{n} 個のコマンドを 1 ステップとして適用しました。",
713
713
  "figure.text.proposal-saved-into": "保存された図形に含まれています: {names}",
714
714
  "figure.text.proposal-needs-modeller": "この提案は、その図形がモデラーで開いているときだけ適用できます。",
715
+ "figure.text.proposal-empty": "この提案にはコマンドがないため、適用しませんでした。もう一度依頼してください。",
715
716
  "figure.text.proposal-new-figure-unsaved": "作成中の新しい図形があるため、提案を適用しませんでした。保存または破棄してから、もう一度実行してください。",
716
717
  "figure.text.proposal-not-opened": "新しい図形をモデラーで開けなかったため、提案を適用しませんでした。",
717
718
  "figure.text.proposal-refused": "モデラーは提案を適用しませんでした: {reason}",
@@ -754,6 +755,11 @@
754
755
  "figure.text.removal-bindings": "{places} も一緒に削除しました。",
755
756
  "figure.text.removal-mounting-face": "取付面が外れました。",
756
757
  "figure.text.edit-refused": "この編集はできません。({code})",
758
+ "figure.text.session-not-open": "先にモデルを正しく開いてください。",
759
+ "figure.text.session-saving": "保存が終わってから編集してください。",
760
+ "figure.text.session-version-missing": "AI の編集には、依頼した時点のモデルのバージョンが必要です。",
761
+ "figure.text.session-new-figure-touched": "まだ編集していない新しいモデルにだけ使えます。",
762
+ "figure.text.session-proposal-key": "提案の識別子が正しくありません。",
757
763
  "figure.text.edit-refused-node-limit": "図形が大きいためこれ以上追加できません。ノードは {max} 個までです。",
758
764
  "figure.text.document-too-large": "図形が {at} KiB で、保存できる上限 {max} KiB を超えています。",
759
765
  "figure.text.document-too-large-unmeasured": "図形が保存できる上限 {max} KiB を超えています。",
@@ -712,6 +712,7 @@
712
712
  "figure.text.proposal-applied": "명령 {n}개를 한 단계로 적용했습니다.",
713
713
  "figure.text.proposal-saved-into": "저장된 도형에 들어 있습니다: {names}",
714
714
  "figure.text.proposal-needs-modeller": "이 제안은 모델러에 그 도형이 열려 있을 때만 적용할 수 있습니다.",
715
+ "figure.text.proposal-empty": "이 제안에는 명령이 없어 적용하지 않았습니다. 다시 요청해 주세요.",
715
716
  "figure.text.proposal-new-figure-unsaved": "만들고 있는 새 Figure가 있어 제안을 적용하지 않았습니다. 저장하거나 버린 뒤 다시 실행해 주세요.",
716
717
  "figure.text.proposal-not-opened": "새 Figure를 모델러에 열지 못해 제안을 적용하지 않았습니다.",
717
718
  "figure.text.proposal-refused": "모델러가 제안을 적용하지 않았습니다: {reason}",
@@ -754,6 +755,11 @@
754
755
  "figure.text.removal-bindings": "{places} 도 함께 지웠습니다.",
755
756
  "figure.text.removal-mounting-face": "장착면이 풀렸습니다.",
756
757
  "figure.text.edit-refused": "이 편집은 할 수 없습니다. ({code})",
758
+ "figure.text.session-not-open": "먼저 모델을 정상적으로 열어 주세요.",
759
+ "figure.text.session-saving": "저장이 끝난 뒤 편집해 주세요.",
760
+ "figure.text.session-version-missing": "AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.",
761
+ "figure.text.session-new-figure-touched": "손대지 않은 새 모델에만 쓸 수 있습니다.",
762
+ "figure.text.session-proposal-key": "제안 식별자가 올바르지 않습니다.",
757
763
  "figure.text.edit-refused-node-limit": "노드 수 제한으로 부품을 더 추가할 수 없습니다. 최대 노드 수는 {max}개입니다.",
758
764
  "figure.text.document-too-large": "도형이 {at} KiB 로 저장 한도 {max} KiB 를 넘습니다.",
759
765
  "figure.text.document-too-large-unmeasured": "도형이 저장 한도 {max} KiB 를 넘습니다.",
@@ -712,6 +712,7 @@
712
712
  "figure.text.proposal-applied": "{n} arahan digunakan sebagai satu langkah.",
713
713
  "figure.text.proposal-saved-into": "Disimpan ke dalam: {names}",
714
714
  "figure.text.proposal-needs-modeller": "cadangan ini hanya boleh digunakan semasa rajahnya dibuka dalam pemodel.",
715
+ "figure.text.proposal-empty": "cadangan ini tiada arahan, jadi tiada apa-apa digunakan. minta sekali lagi.",
715
716
  "figure.text.proposal-new-figure-unsaved": "rajah baharu sedang dibuat, jadi cadangan tidak digunakan. simpan atau buang dahulu, kemudian jalankan semula.",
716
717
  "figure.text.proposal-not-opened": "rajah baharu tidak dapat dibuka dalam pemodel, jadi cadangan tidak digunakan.",
717
718
  "figure.text.proposal-refused": "pemodel tidak menggunakan cadangan: {reason}",
@@ -754,6 +755,11 @@
754
755
  "figure.text.removal-bindings": "{places} turut dipadam.",
755
756
  "figure.text.removal-mounting-face": "Muka pemasangan dilepaskan.",
756
757
  "figure.text.edit-refused": "Suntingan ini tidak boleh dibuat. ({code})",
758
+ "figure.text.session-not-open": "buka model terlebih dahulu.",
759
+ "figure.text.session-saving": "tunggu sehingga simpanan selesai, kemudian sunting.",
760
+ "figure.text.session-version-missing": "suntingan AI memerlukan versi model semasa ia diminta.",
761
+ "figure.text.session-new-figure-touched": "ini hanya boleh digunakan pada model baharu yang belum disunting.",
762
+ "figure.text.session-proposal-key": "pengecam cadangan tidak sah.",
757
763
  "figure.text.edit-refused-node-limit": "Rajah terlalu besar untuk ditambah. Ia memuatkan sehingga {max} nod.",
758
764
  "figure.text.document-too-large": "Rajah ini {at} KiB, melebihi had simpanan {max} KiB.",
759
765
  "figure.text.document-too-large-unmeasured": "Rajah ini melebihi had simpanan {max} KiB.",
@@ -712,6 +712,7 @@
712
712
  "figure.text.proposal-applied": "已将 {n} 条命令作为一步应用。",
713
713
  "figure.text.proposal-saved-into": "已保存到图形: {names}",
714
714
  "figure.text.proposal-needs-modeller": "只有在建模器中打开该图形时才能应用此提案。",
715
+ "figure.text.proposal-empty": "此提案没有任何命令,因此未应用。请重新请求。",
715
716
  "figure.text.proposal-new-figure-unsaved": "正在制作一个新图形,因此未应用该提案。请保存或放弃后再运行。",
716
717
  "figure.text.proposal-not-opened": "无法在建模器中打开新图形,因此未应用该提案。",
717
718
  "figure.text.proposal-refused": "建模器未应用该提案:{reason}",
@@ -754,6 +755,11 @@
754
755
  "figure.text.removal-bindings": "{places} 也一并删除。",
755
756
  "figure.text.removal-mounting-face": "安装面已解除。",
756
757
  "figure.text.edit-refused": "无法进行此编辑。({code})",
758
+ "figure.text.session-not-open": "请先正常打开模型。",
759
+ "figure.text.session-saving": "请在保存完成后再编辑。",
760
+ "figure.text.session-version-missing": "AI 编辑需要提出请求时的模型版本。",
761
+ "figure.text.session-new-figure-touched": "只能用于尚未编辑的新模型。",
762
+ "figure.text.session-proposal-key": "提案标识符无效。",
757
763
  "figure.text.edit-refused-node-limit": "图形过大,无法再添加。节点上限为 {max} 个。",
758
764
  "figure.text.document-too-large": "图形为 {at} KiB,超过可保存的上限 {max} KiB。",
759
765
  "figure.text.document-too-large-unmeasured": "图形超过可保存的上限 {max} KiB。",