@things-factory/figure-ui 10.1.167 → 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/client/figure-assistant.ts +15 -1
- package/client/modeller/ai-proposal.ts +60 -11
- package/client/modeller/figure-properties.ts +1 -3
- package/client/modeller/product-session.ts +32 -13
- package/client/modeller/refusals.ts +14 -0
- package/client/pages/figure-modeller-page.ts +14 -10
- package/dist-client/figure-assistant.d.ts +6 -0
- package/dist-client/figure-assistant.js +14 -1
- package/dist-client/figure-assistant.js.map +1 -1
- package/dist-client/modeller/ai-proposal.d.ts +23 -5
- package/dist-client/modeller/ai-proposal.js +55 -13
- package/dist-client/modeller/ai-proposal.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/figure-properties.d.ts +1 -2
- package/dist-client/modeller/figure-properties.js +1 -3
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +14 -4
- package/dist-client/modeller/product-session.js +31 -13
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +1 -5
- package/dist-client/modeller/refusals.js +14 -1
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.js +14 -9
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/ai-proposal.test.ts +115 -5
- package/test/browser/acceptance-page.js +3 -1
- package/test/browser/product-page.check.mjs +63 -3
- package/test/browser/product-page.js +7 -2
- package/test/figure-assistant-surface.test.ts +65 -1
- package/test/proposal-record.test.ts +18 -6
- package/translations/en.json +6 -12
- package/translations/es.json +6 -12
- package/translations/ja.json +6 -12
- package/translations/ko.json +6 -12
- package/translations/ms.json +6 -12
- package/translations/zh.json +6 -12
- package/client/modeller/carrier-declaration.ts +0 -63
- package/client/modeller/figure-carrier.ts +0 -117
- package/dist-client/modeller/carrier-declaration.d.ts +0 -31
- package/dist-client/modeller/carrier-declaration.js +0 -53
- package/dist-client/modeller/carrier-declaration.js.map +0 -1
- package/dist-client/modeller/figure-carrier.d.ts +0 -26
- package/dist-client/modeller/figure-carrier.js +0 -123
- package/dist-client/modeller/figure-carrier.js.map +0 -1
- package/test/carrier-declaration.test.ts +0 -69
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-ui",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.169",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"clean": "npm run clean:server && npm run clean:client"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hatiolab/figure-model": "^0.
|
|
31
|
-
"@hatiolab/things-scene": "^10.1.
|
|
30
|
+
"@hatiolab/figure-model": "^0.7.1",
|
|
31
|
+
"@hatiolab/things-scene": "^10.1.90",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.0.0",
|
|
34
34
|
"@operato/graphql": "^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.
|
|
45
|
-
"@things-factory/figure-service": "^10.1.
|
|
46
|
-
"@things-factory/figure-viewer": "^10.1.
|
|
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": "
|
|
50
|
+
"gitHead": "9d71d02b443f3e0080b25de1768b982c51a64111"
|
|
51
51
|
}
|
package/test/ai-proposal.test.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { fileURLToPath } from 'node:url'
|
|
|
9
9
|
import { createFigureAsset, addFigureBox, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
10
10
|
|
|
11
11
|
import { createFigureProductSession } from '../client/modeller/product-session.js'
|
|
12
|
-
import {
|
|
12
|
+
import type { FigureProposalStep } from '@hatiolab/figure-model'
|
|
13
|
+
import { askingToLeave, pendNewFigure, runFigureProposal, takeFigureOffer, takePendingNewFigure, type FigureApplyDetail } from '../client/modeller/ai-proposal.js'
|
|
13
14
|
|
|
14
15
|
const ko = JSON.parse(readFileSync(fileURLToPath(new URL('../translations/ko.json', import.meta.url)), 'utf8'))
|
|
15
16
|
const say = (key: string, params: Record<string, string> = {}) => {
|
|
@@ -51,9 +52,18 @@ test('a person\'s edit after the proposal was asked makes it stale -- refused, n
|
|
|
51
52
|
assert.equal(s.read().name, 'A')
|
|
52
53
|
})
|
|
53
54
|
|
|
54
|
-
const taker = (result: Partial<FigureApplyDetail>) => ({
|
|
55
|
+
const taker = (result: Partial<FigureApplyDetail>, saved: Array<{ figureId: string; type?: string; name?: string }> = []) => ({
|
|
55
56
|
offered: [] as FigureApplyDetail[],
|
|
56
57
|
opened: [] as unknown[],
|
|
58
|
+
asked: [] as string[],
|
|
59
|
+
shown: [] as string[],
|
|
60
|
+
async savedInto(key: string) {
|
|
61
|
+
this.asked.push(key)
|
|
62
|
+
return saved
|
|
63
|
+
},
|
|
64
|
+
openSaved(figureId: string) {
|
|
65
|
+
this.shown.push(figureId)
|
|
66
|
+
},
|
|
57
67
|
offer(detail: FigureApplyDetail) {
|
|
58
68
|
this.offered.push(detail)
|
|
59
69
|
return Object.assign(detail, result)
|
|
@@ -72,12 +82,26 @@ test('applied: said with how many commands, and the card carries the version to
|
|
|
72
82
|
assert.equal(target.offered[0]!.revision, 3)
|
|
73
83
|
})
|
|
74
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
|
+
|
|
75
94
|
test('refused: a stale version is said as what happened; any other refusal carries its reason', async () => {
|
|
76
95
|
const stale = await runFigureProposal({ steps }, taker({ applied: false, code: 'REVISION_CONFLICT', error: 'editor changed' }), say)
|
|
77
96
|
assert.equal(stale.verdict, 'refused')
|
|
78
97
|
assert.match(stale.note!, /모델이 바뀌어/)
|
|
79
98
|
const other = await runFigureProposal({ steps }, taker({ applied: false, code: 'ATTACH_FACE', error: 'faces along x' }), say)
|
|
80
|
-
|
|
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/)
|
|
81
105
|
})
|
|
82
106
|
|
|
83
107
|
test('a revision with no modeller to take it is not applied anywhere else', async () => {
|
|
@@ -94,8 +118,71 @@ test('a new figure with no new modeller on screen opens one', async () => {
|
|
|
94
118
|
assert.equal(target.opened.length, 1)
|
|
95
119
|
})
|
|
96
120
|
|
|
97
|
-
|
|
98
|
-
|
|
121
|
+
/*
|
|
122
|
+
The same proposal is one figure (ADR-0061 decision 3, ruled 2026-10-16): a new figure taken from it in the modeller keeps
|
|
123
|
+
the figure's own creation id, so what says the proposal was already made is the save's record of it, read before the
|
|
124
|
+
card runs again.
|
|
125
|
+
*/
|
|
126
|
+
test('a new figure\'s card run again after it was saved opens that figure, and makes none', async () => {
|
|
127
|
+
const K = '11111111-1111-4111-8111-111111111111'
|
|
128
|
+
const target = taker({ applied: true }, [{ figureId: 'R', type: 'box', name: 'Box with lid' }])
|
|
129
|
+
const out = await runFigureProposal({ key: K, steps, create: true }, target, say)
|
|
130
|
+
assert.deepEqual(target.asked, [K])
|
|
131
|
+
assert.deepEqual(target.shown, ['R'])
|
|
132
|
+
assert.equal(target.offered.length + target.opened.length, 0, 'not offered to a modeller, no new one opened')
|
|
133
|
+
assert.deepEqual(out, { verdict: 'accepted', note: say('figure.text.proposal-saved-into', { names: 'Box with lid' }) })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('a new figure\'s card not saved anywhere yet runs as before; a revision is not looked up', async () => {
|
|
137
|
+
const K = '11111111-1111-4111-8111-111111111111'
|
|
138
|
+
const fresh = taker({})
|
|
139
|
+
assert.equal((await runFigureProposal({ key: K, steps, create: true }, fresh, say)).verdict, 'accepted')
|
|
140
|
+
assert.deepEqual({ asked: fresh.asked, opened: fresh.opened.length }, { asked: [K], opened: 1 })
|
|
141
|
+
const revision = taker({ applied: true }, [{ figureId: 'R' }])
|
|
142
|
+
await runFigureProposal({ key: K, steps, context: 'c', revision: 3 }, revision, say)
|
|
143
|
+
assert.deepEqual({ asked: revision.asked, shown: revision.shown }, { asked: [], shown: [] })
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
Open (ADR-0061 decision 3, ruled 2026-10-16, not prevented): the same new figure's card run in another tab before the
|
|
148
|
+
first figure is saved finds no record yet, and opens a second figure under the proposal's key.
|
|
149
|
+
*/
|
|
150
|
+
test('the same new figure\'s card in another tab before the first save makes a second figure under its key', async () => {
|
|
151
|
+
const K = '11111111-1111-4111-8111-111111111111'
|
|
152
|
+
const ids: string[] = []
|
|
153
|
+
const tab = () =>
|
|
154
|
+
createFigureProductSession({
|
|
155
|
+
load: store.load,
|
|
156
|
+
save: async (q: { id?: string; source: string; creationId?: string }) => (ids.push(q.creationId!), { id: q.id ?? q.creationId!, source: q.source })
|
|
157
|
+
})
|
|
158
|
+
const made = [{ kind: 'add-part', part: { id: 'lid', kind: 'box', dimensions: { width: 100, height: 10, depth: 100 } } }] as any[]
|
|
159
|
+
const first = tab()
|
|
160
|
+
first.startNew()
|
|
161
|
+
const detail: FigureApplyDetail = { key: K, steps: made, create: true }
|
|
162
|
+
const here = { ...taker({}), offer: (d: FigureApplyDetail) => (takeFigureOffer(first, d, { onSavedFigure: false, say }), d) }
|
|
163
|
+
assert.equal((await runFigureProposal(detail, here, say)).verdict, 'accepted')
|
|
164
|
+
|
|
165
|
+
const second = tab()
|
|
166
|
+
const there = {
|
|
167
|
+
...taker({}),
|
|
168
|
+
async openNew(s: FigureProposalStep[], key?: string) {
|
|
169
|
+
second.startNew()
|
|
170
|
+
second.createUnder(key!)
|
|
171
|
+
second.applyProposal(s, second.read().revision!, second.read().context!, key)
|
|
172
|
+
return { applied: true }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
assert.equal((await runFigureProposal({ key: K, steps: made, create: true }, there, say)).verdict, 'accepted')
|
|
176
|
+
await first.save()
|
|
177
|
+
await second.save()
|
|
178
|
+
assert.deepEqual(ids, [first.read().creationId, K])
|
|
179
|
+
assert.notEqual(ids[0], ids[1], 'two figures')
|
|
180
|
+
})
|
|
181
|
+
|
|
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 ?? '', /명령이 없어/)
|
|
99
186
|
})
|
|
100
187
|
|
|
101
188
|
test('a new figure\'s steps wait for the page once, and lapse if no page takes them', async () => {
|
|
@@ -110,3 +197,26 @@ test('a new figure\'s steps wait for the page once, and lapse if no page takes t
|
|
|
110
197
|
assert.deepEqual(await lapsing, { applied: false, code: 'not-opened' })
|
|
111
198
|
assert.equal(takePendingNewFigure(), undefined, 'a later new figure does not receive them')
|
|
112
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
|
+
})
|
|
@@ -202,7 +202,9 @@ async function start() {
|
|
|
202
202
|
})
|
|
203
203
|
const dock = {
|
|
204
204
|
offer: detail => (window.dispatchEvent(new CustomEvent('figure-apply', { detail })), detail),
|
|
205
|
-
openNew: async () => ({ applied: false, code: 'not-opened' })
|
|
205
|
+
openNew: async () => ({ applied: false, code: 'not-opened' }),
|
|
206
|
+
savedInto: async () => [],
|
|
207
|
+
openSaved: () => {}
|
|
206
208
|
}
|
|
207
209
|
window.acceptance = {
|
|
208
210
|
open: async id => (await session.open(id), editor.requestUpdate(), editor.dispatchEvent(new CustomEvent('changed'))),
|
|
@@ -541,14 +541,30 @@ describe('the page around the figure: the shell, the dock, the properties', () =
|
|
|
541
541
|
assert.equal(await beforeUnload(), true)
|
|
542
542
|
})
|
|
543
543
|
|
|
544
|
-
|
|
544
|
+
/*
|
|
545
|
+
A figure not saved yet is a subject (ADR-0061 decision 3): its conversation is anchored on the creation id its first
|
|
546
|
+
save inserts the row under, so the first save changes nothing the dock knows -- the same conversation goes on.
|
|
547
|
+
*/
|
|
548
|
+
test('a new figure: the dock talks about it by its creation id, and its first save keeps the conversation', async () => {
|
|
545
549
|
await press('button[save]', 'save')
|
|
546
550
|
await until(() => !product.view().dirty, 'saved')
|
|
547
551
|
await read(() => assistant.forget())
|
|
548
552
|
await open(null)
|
|
549
|
-
|
|
550
|
-
assert.
|
|
553
|
+
const creationId = await read(() => product.page().session.read().creationId)
|
|
554
|
+
assert.match(creationId, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
|
|
555
|
+
assert.deepEqual((await said()).filter(s => s.call !== 'notify'), [{ call: 'provide', key: `figure:${creationId}` }])
|
|
556
|
+
assert.deepEqual(await read(() => assistant.surface.anchor()), { anchorType: 'figure', anchorId: creationId })
|
|
557
|
+
assert.deepEqual(await read(() => { const { figureId, editorMode } = assistant.surface.hostContext(); return { figureId, editorMode } }), { figureId: creationId, editorMode: 'new' })
|
|
551
558
|
assert.equal((await read(() => product.context())).menu, undefined)
|
|
559
|
+
await rename('Gate')
|
|
560
|
+
await read(() => { assistant.forget(); product.forget() })
|
|
561
|
+
await press('button[save]', 'save')
|
|
562
|
+
await press('figure-first-save >>> md-filled-button[slot=confirm]', 'the first save')
|
|
563
|
+
await until(() => product.view().id && !product.view().dirty, 'saved')
|
|
564
|
+
assert.equal((await callsTo('saveFigure'))[0].creationId, creationId)
|
|
565
|
+
assert.equal(await read(() => product.view().id), creationId)
|
|
566
|
+
assert.deepEqual((await said()).filter(s => s.call !== 'notify'), [], 'no new conversation, none taken back')
|
|
567
|
+
assert.deepEqual(await read(() => { const { figureId, editorMode } = assistant.surface.hostContext(); return { figureId, editorMode } }), { figureId: creationId, editorMode: 'existing' })
|
|
552
568
|
})
|
|
553
569
|
|
|
554
570
|
test('inactive: the surface is taken back', async () => {
|
|
@@ -643,6 +659,7 @@ describe('AI proposals reaching the page', () => {
|
|
|
643
659
|
const outcome = await read(key => assistant.surface.proposalOutcome(key), K1)
|
|
644
660
|
assert.deepEqual(outcome, { verdict: 'accepted', note: 'figure.text.proposal-saved-into {"names":"Door"}' })
|
|
645
661
|
assert.deepEqual(await callsTo('figureProposalOutcome'), [{ proposalKey: K1 }])
|
|
662
|
+
await read(() => product.answer('figureProposalOutcome'))
|
|
646
663
|
})
|
|
647
664
|
|
|
648
665
|
test("a new figure proposed: the modeller opens a new one, takes the steps under the proposal's key, and the dock is told", async () => {
|
|
@@ -661,6 +678,7 @@ describe('AI proposals reaching the page', () => {
|
|
|
661
678
|
outcome: { verdict: 'accepted', applied: 1, note: 'figure.text.proposal-applied {"n":"1"}' },
|
|
662
679
|
told: [{ level: 'info', note: 'figure.text.proposal-applied {"n":"1"}' }]
|
|
663
680
|
})
|
|
681
|
+
assert.deepEqual(await callsTo('figureProposalOutcome'), [{ proposalKey: K2 }], 'asked whether it was saved before')
|
|
664
682
|
assert.deepEqual((await read(() => product.navigations())).map(n => n.path), ['/figure-modeller'])
|
|
665
683
|
const made = await read(() => product.view())
|
|
666
684
|
assert.equal(made.id, undefined)
|
|
@@ -671,6 +689,48 @@ describe('AI proposals reaching the page', () => {
|
|
|
671
689
|
const [saved] = await callsTo('saveFigure')
|
|
672
690
|
assert.deepEqual({ creationId: saved.creationId, proposalKeys: saved.proposalKeys }, { creationId: K2, proposalKeys: [K2] })
|
|
673
691
|
})
|
|
692
|
+
|
|
693
|
+
/*
|
|
694
|
+
The draft the dock made on a new figure stays that figure's conversation (ADR-0061 decision 3, ruled 2026-10-16): the
|
|
695
|
+
figure keeps its creation id, and the save records the proposal. Run again once saved, the card opens that figure --
|
|
696
|
+
the record, not the row id, is what makes one proposal one figure.
|
|
697
|
+
*/
|
|
698
|
+
test('a new figure\'s proposal taken on a new figure keeps its conversation; run again after the save, it opens that figure', async () => {
|
|
699
|
+
/* After the one above: the figure on the page is saved, so a new one opens without a question. */
|
|
700
|
+
await until(() => product.view().id && !product.view().dirty, 'the one above saved')
|
|
701
|
+
await open(null)
|
|
702
|
+
const creationId = await read(() => product.page().session.read().creationId)
|
|
703
|
+
await read(() => { assistant.forget(); product.forget() })
|
|
704
|
+
const run = () =>
|
|
705
|
+
read(
|
|
706
|
+
async ({ key, steps }) => {
|
|
707
|
+
const told = []
|
|
708
|
+
const outcome = await assistant.handlers.proposeFigureCandidate({ key, steps, create: true }, { notify: (level, note) => told.push({ level, note }) })
|
|
709
|
+
return { outcome, told }
|
|
710
|
+
},
|
|
711
|
+
{ key: K1, steps: LID }
|
|
712
|
+
)
|
|
713
|
+
assert.deepEqual((await run()).outcome, { verdict: 'accepted', applied: 1, note: 'figure.text.proposal-applied {"n":"1"}' })
|
|
714
|
+
assert.equal(await read(() => product.page().session.read().creationId), creationId, 'the proposal\'s key is not the figure\'s id')
|
|
715
|
+
assert.deepEqual((await said()).filter(s => s.call !== 'notify'), [], 'the same conversation: no surface given or taken back')
|
|
716
|
+
assert.deepEqual(await read(() => product.navigations()), [], 'taken where it was asked')
|
|
717
|
+
await press('button[save]', 'save')
|
|
718
|
+
await press('figure-first-save >>> md-filled-button[slot=confirm]', 'the first save')
|
|
719
|
+
await until(() => product.view().id && !product.view().dirty, 'saved')
|
|
720
|
+
const [saved] = await callsTo('saveFigure')
|
|
721
|
+
assert.deepEqual({ creationId: saved.creationId, proposalKeys: saved.proposalKeys }, { creationId, proposalKeys: [K1] })
|
|
722
|
+
|
|
723
|
+
await open(null)
|
|
724
|
+
await read(() => product.forget())
|
|
725
|
+
const before = await read(() => product.rows().length)
|
|
726
|
+
const names = await read(id => product.rows().find(r => r.id === id).name, creationId)
|
|
727
|
+
const note = `figure.text.proposal-saved-into ${JSON.stringify({ names })}`
|
|
728
|
+
assert.deepEqual(await run(), { outcome: { verdict: 'accepted', note }, told: [{ level: 'info', note }] })
|
|
729
|
+
assert.deepEqual(await callsTo('figureProposalOutcome'), [{ proposalKey: K1 }])
|
|
730
|
+
assert.deepEqual((await read(() => product.navigations())).map(n => n.path), [`/figure-modeller/${creationId}`])
|
|
731
|
+
assert.equal(await read(() => product.rows().length), before, 'no second figure')
|
|
732
|
+
assert.deepEqual(await callsTo('saveFigure'), [])
|
|
733
|
+
})
|
|
674
734
|
})
|
|
675
735
|
|
|
676
736
|
describe('the catalogue preview', () => {
|
|
@@ -61,6 +61,8 @@ const calls = []
|
|
|
61
61
|
/* Answers a check wants instead of the server's: field -> function(variables) returning data or throwing. */
|
|
62
62
|
const overrides = new Map()
|
|
63
63
|
let made = 0
|
|
64
|
+
/* What the saves recorded of AI proposals: { proposalKey, figureId }. */
|
|
65
|
+
const records = []
|
|
64
66
|
|
|
65
67
|
function row(id) {
|
|
66
68
|
const found = rows.get(id)
|
|
@@ -71,8 +73,10 @@ function row(id) {
|
|
|
71
73
|
const answers = {
|
|
72
74
|
figure: ({ id }) => ({ __typename: 'Figure', properties: null, tags: null, category: null, description: null, score: null, triangles: null, groups: null, thumbnailUpdatedAt: null, updatedAt: null, updater: null, thumbnailHash: null, ...row(id) }),
|
|
73
75
|
figureTypeNames: () => [...rows.values()].map(r => r.type),
|
|
74
|
-
saveFigure: async ({ id, source, type, creationId, thumbnail }) => {
|
|
76
|
+
saveFigure: async ({ id, source, type, creationId, thumbnail, proposalKeys }) => {
|
|
75
77
|
const saved = id ? row(id) : { id: creationId ?? `made-${++made}`, namespace: null, type: type ?? `figure-${made}`, state: 'draft', version: 0 }
|
|
78
|
+
/* The save's record of the proposals its source holds, as the server keeps it (figure-proposal.ts). */
|
|
79
|
+
for (const key of proposalKeys ?? []) if (!records.some(r => r.proposalKey === key && r.figureId === saved.id)) records.push({ proposalKey: key, figureId: saved.id })
|
|
76
80
|
/* The picture is kept by the hash of its bytes, as the server keeps it (ADR-0104 decision 6). */
|
|
77
81
|
Object.assign(saved, { source, name: JSON.parse(source).name, state: 'draft' }, thumbnail === undefined ? {} : { thumbnailHash: await thumbnailHashOf(thumbnail) })
|
|
78
82
|
rows.set(saved.id, saved)
|
|
@@ -87,7 +91,8 @@ const answers = {
|
|
|
87
91
|
return { __typename: 'Figure', id, version: released.version, state: 'released', updatedAt: null }
|
|
88
92
|
},
|
|
89
93
|
figureRecipeTemplates: () => [],
|
|
90
|
-
figureProposalOutcome: () =>
|
|
94
|
+
figureProposalOutcome: ({ proposalKey }) =>
|
|
95
|
+
records.filter(r => r.proposalKey === proposalKey).map(r => ({ figureId: r.figureId, type: rows.get(r.figureId)?.type, name: rows.get(r.figureId)?.name, recordedAt: null })),
|
|
91
96
|
figureReplacedDrafts: () => [],
|
|
92
97
|
figureVersions: () => [],
|
|
93
98
|
figureReleased: () => null,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The modeller's AI - the framework dock talks about the figure on screen (design/ai/shared-dock.md §3).
|
|
3
3
|
*
|
|
4
4
|
* What the page does with the dock -- ① a saved figure provides a surface anchored on its row ② the draft is read at the
|
|
5
|
-
* moment of sending ③ an unsaved figure provides
|
|
5
|
+
* moment of sending ③ an unsaved figure provides one anchored on its creation id ④ the surface is taken back when the page goes inactive or
|
|
6
6
|
* leaves ⑤ a proposal is taken through the page, and a new figure's opens a new figure -- is checked on the page itself
|
|
7
7
|
* in test/browser/product-page.check.mjs. Here: the offer the page takes it by, and the words every outcome is said in.
|
|
8
8
|
*/
|
|
@@ -43,6 +43,70 @@ 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
|
+
|
|
84
|
+
/*
|
|
85
|
+
A figure not saved yet is a subject too (ADR-0061 decision 3): its conversation is anchored on the creation id the
|
|
86
|
+
first save inserts the row under, so the anchor is the same before and after that save. Taking a new figure's
|
|
87
|
+
proposal on it must not change that id -- the conversation that asked for the draft would be left behind.
|
|
88
|
+
*/
|
|
89
|
+
test('a new figure keeps its creation id through a new figure\'s proposal and the first save', async () => {
|
|
90
|
+
const sent: Array<{ id?: string; creationId?: string; proposalKeys?: string[] }> = []
|
|
91
|
+
const session = createFigureProductSession({
|
|
92
|
+
load: async (id: string) => ({ id, source: '' }),
|
|
93
|
+
save: async (q: { id?: string; source: string; creationId?: string; proposalKeys?: string[] }) => {
|
|
94
|
+
sent.push({ id: q.id, creationId: q.creationId, proposalKeys: q.proposalKeys })
|
|
95
|
+
return { id: q.id ?? q.creationId!, source: q.source }
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
session.startNew()
|
|
99
|
+
const creationId = session.read().creationId
|
|
100
|
+
assert.match(creationId ?? '', /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
|
|
101
|
+
const create: FigureApplyDetail = { key: K1, steps: lid, create: true }
|
|
102
|
+
assert.equal(takeFigureOffer(session, create, { onSavedFigure: false, say }), true)
|
|
103
|
+
assert.equal(session.read().creationId, creationId, 'the proposal\'s key is not the figure\'s id')
|
|
104
|
+
assert.deepEqual(session.proposalsInEffect(), [K1], 'the save still records the proposal')
|
|
105
|
+
await session.save()
|
|
106
|
+
assert.deepEqual(sent, [{ id: undefined, creationId, proposalKeys: [K1] }])
|
|
107
|
+
assert.deepEqual({ id: session.read().id, creationId: session.read().creationId }, { id: creationId, creationId })
|
|
108
|
+
})
|
|
109
|
+
|
|
46
110
|
test('a figure proposal is answered by its handler, in words, never left without one', () => {
|
|
47
111
|
for (const lang of ['en', 'ko', 'ja', 'zh', 'ms', 'es']) {
|
|
48
112
|
const words = JSON.parse(readFileSync(fileURLToPath(new URL(`../translations/${lang}.json`, import.meta.url)), 'utf8'))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* What became of an AI proposal (ADR-0097 §5.6 item 5; AI lane, 2026-09-27). A save names the proposals whose steps its
|
|
3
|
-
* text holds -- undone, a proposal is not in it; a new figure
|
|
3
|
+
* text holds -- undone, a proposal is not in it; a new figure opened for a proposal is created under the proposal's key.
|
|
4
4
|
* The server side (recorded in the save's transaction, one figure per key) is tested on sqlite in figure-service
|
|
5
5
|
* (figure-proposal-db).
|
|
6
6
|
*/
|
|
@@ -59,16 +59,28 @@ test('a save names the proposals its text holds: undone, not; redone, again; und
|
|
|
59
59
|
assert.deepEqual(s.proposalsInEffect(), [K1])
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
/*
|
|
63
|
+
Two ways a new figure comes from a proposal (ADR-0061 decision 3, ruled 2026-10-16). Opened for a proposal from another
|
|
64
|
+
screen, it is created under the proposal's key. Taken on the new figure the modeller has open, its conversation is
|
|
65
|
+
anchored on that figure's creation id, which stays; the save records the proposal either way.
|
|
66
|
+
*/
|
|
67
|
+
test('a new figure opened for a proposal is created under its key; one taken on the open new figure keeps its id', async () => {
|
|
63
68
|
const { store, saves } = server()
|
|
64
69
|
const s = createFigureProductSession(store)
|
|
65
70
|
s.startNew()
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
s.createUnder(K1)
|
|
72
|
+
s.applyProposal(lid, s.read().revision!, s.read().context!, K1)
|
|
68
73
|
await s.save({ type: 'LIDDED' })
|
|
69
74
|
assert.equal(saves.at(-1)!.creationId, K1, 'created under the proposal key: taken twice, it meets the first figure')
|
|
70
75
|
assert.deepEqual(saves.at(-1)!.proposalKeys, [K1])
|
|
71
76
|
|
|
77
|
+
s.startNew({ discard: true })
|
|
78
|
+
const own = s.read().creationId
|
|
79
|
+
const detail: FigureApplyDetail = { key: K2, steps: lid, create: true }
|
|
80
|
+
assert.equal(takeFigureOffer(s, detail, { onSavedFigure: false, say }), true)
|
|
81
|
+
await s.save({ type: 'LIDDED_2' })
|
|
82
|
+
assert.deepEqual({ creationId: saves.at(-1)!.creationId, proposalKeys: saves.at(-1)!.proposalKeys }, { creationId: own, proposalKeys: [K2] })
|
|
83
|
+
|
|
72
84
|
/* A new figure the person has touched is theirs: the proposal is not put on it (NEW_FIGURE_UNSAVED), its id stays. */
|
|
73
85
|
s.startNew({ discard: true })
|
|
74
86
|
s.command({ kind: 'rename', name: 'Mine' } as never)
|
|
@@ -84,13 +96,13 @@ test('the key travels from the card to the modeller, on a revision and on a new
|
|
|
84
96
|
const opened: Array<string | undefined> = []
|
|
85
97
|
await runFigureProposal(
|
|
86
98
|
{ key: K1, steps: lid, create: false, context: 'c', revision: 1 },
|
|
87
|
-
{ offer: d => (offered.push(d), { ...d, applied: true }), openNew: async () => ({ applied: false }) },
|
|
99
|
+
{ offer: d => (offered.push(d), { ...d, applied: true }), openNew: async () => ({ applied: false }), savedInto: async () => [], openSaved: () => {} },
|
|
88
100
|
say
|
|
89
101
|
)
|
|
90
102
|
assert.equal(offered[0]!.key, K1)
|
|
91
103
|
await runFigureProposal(
|
|
92
104
|
{ key: K2, steps: lid, create: true },
|
|
93
|
-
{ offer: d => d, openNew: async (_steps, key) => (opened.push(key), { applied: true }) },
|
|
105
|
+
{ offer: d => d, openNew: async (_steps, key) => (opened.push(key), { applied: true }), savedInto: async () => [], openSaved: () => {} },
|
|
94
106
|
say
|
|
95
107
|
)
|
|
96
108
|
assert.deepEqual(opened, [K2])
|
package/translations/en.json
CHANGED
|
@@ -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.",
|
|
@@ -973,18 +979,6 @@
|
|
|
973
979
|
"figure.text.detail-unreleased": "Not released yet. Showing the saved draft.",
|
|
974
980
|
"figure.button.detail-edit": "Open editor",
|
|
975
981
|
"figure.button.download-package": "download package",
|
|
976
|
-
"figure.label.group-carrier": "Carrier",
|
|
977
|
-
"figure.label.carrier-type": "Carrier type",
|
|
978
|
-
"figure.text.carrier-type-placeholder": "CARRIERTYPE",
|
|
979
|
-
"figure.label.carrier-size": "Physical size",
|
|
980
|
-
"figure.label.carrier-width": "Width",
|
|
981
|
-
"figure.label.carrier-length": "Length",
|
|
982
|
-
"figure.label.carrier-height": "Height",
|
|
983
|
-
"figure.button.declare-carrier": "Declare carrier",
|
|
984
|
-
"figure.button.clear-carrier": "Clear",
|
|
985
|
-
"figure.text.carrier-type-required": "Enter the carrier type code.",
|
|
986
|
-
"figure.text.carrier-type-too-long": "A carrier type code is at most {max} characters.",
|
|
987
|
-
"figure.text.carrier-length-positive": "{axis} must be greater than 0.",
|
|
988
982
|
"figure.button.import-mesh": "import GLB",
|
|
989
983
|
"figure.text.import-mesh-hint": "brings a GLB file in as a part: the whole file, or one part of it.",
|
|
990
984
|
"figure.text.mesh-whole-file": "whole file",
|
package/translations/es.json
CHANGED
|
@@ -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.",
|
|
@@ -973,18 +979,6 @@
|
|
|
973
979
|
"figure.text.detail-unreleased": "Aún no publicada. Se muestra el borrador guardado.",
|
|
974
980
|
"figure.button.detail-edit": "Abrir editor",
|
|
975
981
|
"figure.button.download-package": "descargar paquete",
|
|
976
|
-
"figure.label.group-carrier": "Portador",
|
|
977
|
-
"figure.label.carrier-type": "Tipo de portador",
|
|
978
|
-
"figure.text.carrier-type-placeholder": "CARRIERTYPE",
|
|
979
|
-
"figure.label.carrier-size": "Tamaño físico",
|
|
980
|
-
"figure.label.carrier-width": "Ancho",
|
|
981
|
-
"figure.label.carrier-length": "Largo",
|
|
982
|
-
"figure.label.carrier-height": "Alto",
|
|
983
|
-
"figure.button.declare-carrier": "Declarar portador",
|
|
984
|
-
"figure.button.clear-carrier": "Quitar",
|
|
985
|
-
"figure.text.carrier-type-required": "Introduzca el código del tipo de portador.",
|
|
986
|
-
"figure.text.carrier-type-too-long": "El código del tipo de portador tiene como máximo {max} caracteres.",
|
|
987
|
-
"figure.text.carrier-length-positive": "{axis} debe ser mayor que 0.",
|
|
988
982
|
"figure.button.import-mesh": "importar GLB",
|
|
989
983
|
"figure.text.import-mesh-hint": "trae un archivo GLB como pieza: el archivo entero o una parte de él.",
|
|
990
984
|
"figure.text.mesh-whole-file": "archivo entero",
|
package/translations/ja.json
CHANGED
|
@@ -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 を超えています。",
|
|
@@ -973,18 +979,6 @@
|
|
|
973
979
|
"figure.text.detail-unreleased": "まだ公開されていません。保存された下書きを表示します。",
|
|
974
980
|
"figure.button.detail-edit": "編集画面を開く",
|
|
975
981
|
"figure.button.download-package": "パッケージをダウンロード",
|
|
976
|
-
"figure.label.group-carrier": "キャリア",
|
|
977
|
-
"figure.label.carrier-type": "キャリアタイプ",
|
|
978
|
-
"figure.text.carrier-type-placeholder": "CARRIERTYPE",
|
|
979
|
-
"figure.label.carrier-size": "実寸",
|
|
980
|
-
"figure.label.carrier-width": "幅",
|
|
981
|
-
"figure.label.carrier-length": "長さ",
|
|
982
|
-
"figure.label.carrier-height": "高さ",
|
|
983
|
-
"figure.button.declare-carrier": "キャリアとして宣言",
|
|
984
|
-
"figure.button.clear-carrier": "宣言を解除",
|
|
985
|
-
"figure.text.carrier-type-required": "キャリアタイプのコードを入力してください。",
|
|
986
|
-
"figure.text.carrier-type-too-long": "キャリアタイプのコードは {max} 文字までです。",
|
|
987
|
-
"figure.text.carrier-length-positive": "{axis} は 0 より大きい値にしてください。",
|
|
988
982
|
"figure.button.import-mesh": "GLB を取り込む",
|
|
989
983
|
"figure.text.import-mesh-hint": "GLB ファイルを部品として取り込みます。ファイル全体か、その中の 1 つを選びます。",
|
|
990
984
|
"figure.text.mesh-whole-file": "ファイル全体",
|
package/translations/ko.json
CHANGED
|
@@ -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 를 넘습니다.",
|
|
@@ -973,18 +979,6 @@
|
|
|
973
979
|
"figure.text.detail-unreleased": "아직 발행되지 않았습니다. 저장된 초안을 보여줍니다.",
|
|
974
980
|
"figure.button.detail-edit": "편집 화면 열기",
|
|
975
981
|
"figure.button.download-package": "꾸러미 내려받기",
|
|
976
|
-
"figure.label.group-carrier": "캐리어",
|
|
977
|
-
"figure.label.carrier-type": "캐리어 유형",
|
|
978
|
-
"figure.text.carrier-type-placeholder": "CARRIERTYPE",
|
|
979
|
-
"figure.label.carrier-size": "실물 크기",
|
|
980
|
-
"figure.label.carrier-width": "폭",
|
|
981
|
-
"figure.label.carrier-length": "길이",
|
|
982
|
-
"figure.label.carrier-height": "높이",
|
|
983
|
-
"figure.button.declare-carrier": "캐리어로 선언",
|
|
984
|
-
"figure.button.clear-carrier": "선언 해제",
|
|
985
|
-
"figure.text.carrier-type-required": "캐리어 유형 코드를 입력해 주세요.",
|
|
986
|
-
"figure.text.carrier-type-too-long": "캐리어 유형 코드는 {max}자까지입니다.",
|
|
987
|
-
"figure.text.carrier-length-positive": "{axis} 값은 0보다 커야 합니다.",
|
|
988
982
|
"figure.button.import-mesh": "GLB 가져오기",
|
|
989
983
|
"figure.text.import-mesh-hint": "GLB 파일을 부품으로 가져옵니다. 파일 전체나 그 안의 한 부분을 고릅니다.",
|
|
990
984
|
"figure.text.mesh-whole-file": "파일 전체",
|