@things-factory/figure-ui 10.1.52 → 10.1.54
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 +20 -0
- package/client/index.ts +1 -0
- package/client/modeller/figure-v3-links.ts +56 -4
- package/client/modeller/v3-editor.ts +19 -8
- package/client/modeller/v3-links.ts +45 -12
- package/client/modeller/v3-refusals.ts +38 -0
- package/client/pages/figure-v3-modeller-page.ts +49 -16
- package/dist-client/figure-assistant.d.ts +1 -0
- package/dist-client/figure-assistant.js +19 -0
- package/dist-client/figure-assistant.js.map +1 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +1 -0
- package/dist-client/index.js.map +1 -1
- package/dist-client/modeller/figure-v3-capability.d.ts +1 -1
- package/dist-client/modeller/figure-v3-clips.d.ts +1 -1
- package/dist-client/modeller/figure-v3-controls.d.ts +1 -1
- package/dist-client/modeller/figure-v3-links.d.ts +2 -0
- package/dist-client/modeller/figure-v3-links.js +57 -4
- package/dist-client/modeller/figure-v3-links.js.map +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +1 -1
- package/dist-client/modeller/figure-v3-properties.d.ts +1 -1
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.d.ts +2 -0
- package/dist-client/modeller/v3-editor.js +21 -8
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-links.d.ts +24 -2
- package/dist-client/modeller/v3-links.js +32 -8
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-refusals.d.ts +19 -0
- package/dist-client/modeller/v3-refusals.js +43 -0
- package/dist-client/modeller/v3-refusals.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +4 -1
- package/dist-client/pages/figure-v3-modeller-page.js +44 -15
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/test/browser/modeller.check.mjs +39 -0
- package/test/figure-assistant-surface.test.ts +52 -0
- package/test/v3-first-save.test.ts +8 -0
- package/test/v3-links.test.ts +74 -2
- package/test/v3-parts-wiring.test.ts +4 -2
- package/test/v3-refusals.test.ts +51 -0
- package/translations/en.json +15 -1
- package/translations/ja.json +15 -1
- package/translations/ko.json +15 -1
- package/translations/ms.json +15 -1
- package/translations/zh.json +15 -1
- package/client/modeller/figure-ai-target.ts +0 -23
- package/dist-client/modeller/figure-ai-target.d.ts +0 -18
- package/dist-client/modeller/figure-ai-target.js +0 -22
- package/dist-client/modeller/figure-ai-target.js.map +0 -1
- package/test/ai-target.test.ts +0 -25
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.54",
|
|
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.1.
|
|
31
|
-
"@hatiolab/things-scene": "^10.1.
|
|
30
|
+
"@hatiolab/figure-model": "^0.1.63",
|
|
31
|
+
"@hatiolab/things-scene": "^10.1.40",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.0.0",
|
|
34
34
|
"@operato/graphql": "^10.0.0",
|
|
@@ -39,8 +39,9 @@
|
|
|
39
39
|
"@operato/shell": "^10.0.0",
|
|
40
40
|
"@operato/styles": "^10.0.0",
|
|
41
41
|
"@operato/utils": "^10.0.0",
|
|
42
|
-
"@things-factory/
|
|
42
|
+
"@things-factory/ai-assistant": "^10.1.53",
|
|
43
|
+
"@things-factory/figure-service": "^10.1.54",
|
|
43
44
|
"three": "^0.185.1"
|
|
44
45
|
},
|
|
45
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "0bc2238c6df9cb041b22146c85181ed7efe38f40"
|
|
46
47
|
}
|
|
@@ -376,6 +376,45 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
376
376
|
assert.equal(after.revision, start.revision + 1, 'one step')
|
|
377
377
|
assert.deepEqual(await read(() => probe.fields('size.')), { 'size.x': 400, 'size.y': 400, 'size.z': 400 }, 'the size it is authored at: its parts now')
|
|
378
378
|
})
|
|
379
|
+
|
|
380
|
+
test('a part follows the figure\'s width less a difference: chosen and typed, the width is the size less it', async () => {
|
|
381
|
+
await click(await read(() => probe.partOnPage('box')))
|
|
382
|
+
assert.equal((await state()).selected, 'box')
|
|
383
|
+
const handle = (what, label) =>
|
|
384
|
+
page().evaluateHandle(
|
|
385
|
+
(what, label) => {
|
|
386
|
+
const deep = (root = document) => [...root.querySelectorAll(what)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
387
|
+
if (what === 'select') return deep().find(s => s.previousElementSibling?.textContent?.trim() === label && [...s.options].some(o => o.value === 'instance:x')) ?? null
|
|
388
|
+
return deep().find(i => i.closest('label[inline-plus]')) ?? null
|
|
389
|
+
},
|
|
390
|
+
what,
|
|
391
|
+
label
|
|
392
|
+
)
|
|
393
|
+
const width = await handle('select', 'X')
|
|
394
|
+
assert.ok(await width.evaluate(s => !!s), 'the width can follow the figure size')
|
|
395
|
+
await width.select('instance:x')
|
|
396
|
+
await sleep(600)
|
|
397
|
+
const plus = await handle('input')
|
|
398
|
+
assert.ok(await plus.evaluate(i => !!i), 'a following width offers its difference')
|
|
399
|
+
await plus.evaluate(i => i.scrollIntoView({ block: 'center' }))
|
|
400
|
+
await plus.click({ clickCount: 3 })
|
|
401
|
+
await page().keyboard.type('-50')
|
|
402
|
+
await page().keyboard.press('Tab')
|
|
403
|
+
await sleep(600)
|
|
404
|
+
/* 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. */
|
|
405
|
+
assert.match((await state()).error, /figure.text.link-needs-smaller-range/, 'refused, said in words')
|
|
406
|
+
const narrow = await page().evaluateHandle(() => {
|
|
407
|
+
const deep = (root = document) => [...root.querySelectorAll('button')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
408
|
+
return deep().find(b => b.textContent.includes('figure.button.narrow-range-from')) ?? null
|
|
409
|
+
})
|
|
410
|
+
assert.ok(await narrow.evaluate(b => !!b), 'the same link is offered with the sizes narrowed')
|
|
411
|
+
await click(await narrow.evaluate(b => {
|
|
412
|
+
const r = b.getBoundingClientRect()
|
|
413
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
414
|
+
}))
|
|
415
|
+
assert.equal((await state()).error, '', 'taken')
|
|
416
|
+
assert.equal((await body('box')).dimensions.width, 350, 'the figure\'s 400 less 50')
|
|
417
|
+
})
|
|
379
418
|
})
|
|
380
419
|
|
|
381
420
|
test('the page raised no error', () => {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The V3 modeller's AI - the framework dock talks about the figure on screen (design/ai/shared-dock.md §3).
|
|
3
|
+
*
|
|
4
|
+
* What this holds: ① a saved figure provides a surface anchored on its row ② the draft is read at the moment of
|
|
5
|
+
* sending, not when the surface was provided ③ an unsaved figure provides nothing - no conversation without a row
|
|
6
|
+
* ④ the page takes the surface back when it goes inactive or leaves. The page pulls in the shell and things-scene,
|
|
7
|
+
* so this reads its source.
|
|
8
|
+
*/
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
import { test } from 'node:test'
|
|
13
|
+
|
|
14
|
+
const source = readFileSync(fileURLToPath(new URL('../client/pages/figure-v3-modeller-page.ts', import.meta.url)), 'utf8')
|
|
15
|
+
|
|
16
|
+
test('a saved figure provides a surface anchored on its row', () => {
|
|
17
|
+
assert.match(source, /provideAssistantSurface\(this, \{/)
|
|
18
|
+
assert.match(source, /subject: 'figure'/)
|
|
19
|
+
assert.match(source, /anchor: \(\) => \(\{ anchorType: 'figure', anchorId: id \}\)/)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('the draft is read when the message is sent, as host context the figure tools read', () => {
|
|
23
|
+
assert.match(source, /hostContext: \(\) => \(\{[\s\S]*?figureId: id,[\s\S]*?draftSource: JSON\.parse\(this\.session\.source\(\)\),[\s\S]*?palette: Object\.keys\(activePalette\(\)\)/)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('an unsaved figure provides nothing', () => {
|
|
27
|
+
assert.match(source, /if \(!view\.id\) \{\s*this\.withdrawAssistant\(\)/)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('the surface is taken back when the page goes inactive or leaves', () => {
|
|
31
|
+
const withdraws = source.match(/this\.withdrawAssistant\(\)/g) ?? []
|
|
32
|
+
assert.ok(withdraws.length >= 3, 'unsaved, inactive and disconnected each withdraw')
|
|
33
|
+
assert.doesNotMatch(source, /figure-ai-context/, 'the old window-event channel is gone')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('the V3 apply entry stays for the proposer that writes V3 changes', () => {
|
|
37
|
+
assert.match(source, /window\.addEventListener\('figure-v3-apply', this\.applyAI\)/)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('a figure candidate is answered - unavailable, in words - never left without a handler', () => {
|
|
41
|
+
const handler = readFileSync(fileURLToPath(new URL('../client/figure-assistant.ts', import.meta.url)), 'utf8')
|
|
42
|
+
assert.match(handler, /registerProposalHandler\(FIGURE_CANDIDATE_TOOL,/)
|
|
43
|
+
assert.match(handler, /export const FIGURE_CANDIDATE_TOOL = 'proposeFigureCandidate'/)
|
|
44
|
+
assert.match(handler, /verdict: 'unavailable'/)
|
|
45
|
+
assert.match(handler, /figure\.text\.candidate-cannot-open/)
|
|
46
|
+
for (const lang of ['en', 'ko', 'ja', 'zh', 'ms']) {
|
|
47
|
+
const words = JSON.parse(readFileSync(fileURLToPath(new URL(`../translations/${lang}.json`, import.meta.url)), 'utf8'))
|
|
48
|
+
assert.ok(words['figure.text.candidate-cannot-open'], `${lang} has no words for it`)
|
|
49
|
+
}
|
|
50
|
+
const index = readFileSync(fileURLToPath(new URL('../client/index.ts', import.meta.url)), 'utf8')
|
|
51
|
+
assert.match(index, /figure-assistant\.js/)
|
|
52
|
+
})
|
|
@@ -82,3 +82,11 @@ test('처음 저장한 도형은 주소가 제 것이 된다 — 모델러를
|
|
|
82
82
|
/* The early return that kept the old figure reads the route id; it now matches only the figure the address names. */
|
|
83
83
|
assert.match(page, /if \(this\.session\.read\(\)\.context && id === this\.routeId && !changes\.active\) return/)
|
|
84
84
|
})
|
|
85
|
+
|
|
86
|
+
test('다른 도형을 열면 행을 먼저 비운다 — 앞 도형의 타입이 칸에 남지 않는다 (C2, 2026-09-25)', async () => {
|
|
87
|
+
const { readFileSync } = await import('node:fs')
|
|
88
|
+
const page = readFileSync(new URL('../client/pages/figure-v3-modeller-page.ts', import.meta.url), 'utf8')
|
|
89
|
+
const updated = page.slice(page.indexOf('async pageUpdated'), page.indexOf('private askFirstSave'))
|
|
90
|
+
const opening = updated.slice(updated.indexOf('if (id) {'), updated.indexOf('this.session.open(id'))
|
|
91
|
+
assert.match(opening, /this\.row = \{ version: 0 \}/, '열기 전에 행을 비운다')
|
|
92
|
+
})
|
package/test/v3-links.test.ts
CHANGED
|
@@ -30,7 +30,7 @@ test('크기 — 제 치수 · 인스턴스 크기 · 다른 부품의 치수를
|
|
|
30
30
|
const byInstance = applyV3Authoring(asset, { kind: 'link-dimension', part: 'plate', axis: 'x', source: { of: 'instance', axis: 'x' } })
|
|
31
31
|
assert.deepEqual(followOf(byInstance, 'plate', 'x'), { of: 'instance', axis: 'x', times: 1 })
|
|
32
32
|
const byPart = applyV3Authoring(byInstance, { kind: 'link-dimension', part: 'block', axis: 'x', source: { of: 'part', part: 'plate', axis: 'x' }, times: 0.5 })
|
|
33
|
-
assert.deepEqual(followOf(byPart, 'block', 'x'), { of: 'part', part: 'plate', axis: 'x' }, '배율이 끼어도 원천을
|
|
33
|
+
assert.deepEqual(followOf(byPart, 'block', 'x'), { of: 'part', part: 'plate', axis: 'x', times: 0.5 }, '배율이 끼어도 원천을 찾고, 배율도 읽는다')
|
|
34
34
|
const back = applyV3Authoring(byPart, { kind: 'unlink-dimension', part: 'block', axis: 'x', mm: 300 })
|
|
35
35
|
assert.deepEqual(followOf(back, 'block', 'x'), { of: 'own' }, '떼면 다시 제 치수')
|
|
36
36
|
assert.equal(hasInstanceSize(asset, 'y'), true)
|
|
@@ -182,7 +182,8 @@ test('비례 — 인스턴스 크기의 몫을 따르는 치수는 비례로 읽
|
|
|
182
182
|
m.nodes.push({ id: 'flange.free', op: 'add@1', args: ['size.y', 'k.gaps'], outputs: { value: 'flange.free.value' } })
|
|
183
183
|
m.nodes.push({ id: 'flange.dimension.y', op: 'mul@1', args: ['flange.free.value', m.constants.find(c => c.value === 1)?.id ?? 'k.one'], outputs: { value: 'flange.dimension.y.value' } })
|
|
184
184
|
if (!m.constants.some(c => c.value === 1)) m.constants.push({ id: 'k.one', unit: 'ratio', value: 1 })
|
|
185
|
-
|
|
185
|
+
/* Read as what it is since the plus is read (2026-09-25): the size less 90 -- following the size, not a share of it. */
|
|
186
|
+
assert.deepEqual(followOf(fill as V3Asset, 'flange', 'y'), { of: 'instance', axis: 'y', times: 1, plus: -90 })
|
|
186
187
|
|
|
187
188
|
/* Before any link the shape reads the measure directly; a worked-out one there is not 「고정」 (ROBOT_ARM on :3300). */
|
|
188
189
|
const direct = structuredClone(asset)
|
|
@@ -363,3 +364,74 @@ test('모델러에서 만든 도형은 부품에서 재어 도형 크기를 선
|
|
|
363
364
|
const at = (size: number) => (compileV3Asset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'frame')!.shape.dimensions as { width: number }).width
|
|
364
365
|
assert.equal(at(3000), 3000, '보드가 3000 으로 놓으면 틀도 3000')
|
|
365
366
|
})
|
|
367
|
+
|
|
368
|
+
test('따르는 치수의 더하기를 읽는다 — 문짝 폭 = 개구부 − 틈 (사이징, 2026-09-25)', async () => {
|
|
369
|
+
const { followOf, choiceOf } = await import('../client/modeller/v3-links.ts')
|
|
370
|
+
let asset = createV3Asset('Door')
|
|
371
|
+
asset = addV3Box(asset, { id: 'head', size: { x: 1100, y: 100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
372
|
+
asset = addV3Box(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
373
|
+
asset = applyV3Authoring(asset, { kind: 'link-dimension', part: 'leaf', axis: 'x', source: { of: 'part', part: 'head', axis: 'x' }, plus: -210 })
|
|
374
|
+
assert.deepEqual(followOf(asset, 'leaf', 'x'), { of: 'part', part: 'head', axis: 'x', plus: -210 })
|
|
375
|
+
assert.equal(choiceOf(followOf(asset, 'leaf', 'x')), 'part:head:x', '선택은 그대로 — 더하기는 따로')
|
|
376
|
+
const { compileV3Asset } = await import('@hatiolab/figure-model')
|
|
377
|
+
const wide = { ...asset, designInputs: { ...asset.designInputs, 'head.size.x': 1500 } }
|
|
378
|
+
assert.equal((compileV3Asset(wide).evaluate().geometry.find(g => g.id === 'leaf')!.shape.dimensions as { width: number }).width, 1290, '상인방 1500 이면 문짝 1290')
|
|
379
|
+
asset = applyV3Authoring(asset, { kind: 'declare-instance-size', range: { x: { min: 500, max: 5000 }, y: { min: 500, max: 5000 }, z: { min: 50, max: 500 } }, at: { x: 1100, y: 2200, z: 150 } })
|
|
380
|
+
asset = applyV3Authoring(asset, { kind: 'link-dimension', part: 'head', axis: 'x', source: { of: 'instance', axis: 'x' }, times: 0.5, plus: 20 })
|
|
381
|
+
assert.deepEqual(followOf(asset, 'head', 'x'), { of: 'instance', axis: 'x', times: 0.5, plus: 20 })
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
test('미끄러짐의 이동 거리가 부품 길이를 따른다 — 레일이 길어지면 브리지가 끝까지 간다 (사이징, 2026-09-25)', async () => {
|
|
385
|
+
const { motionJoint, motionOf } = await import('../client/modeller/v3-links.ts')
|
|
386
|
+
const { compileV3Asset } = await import('@hatiolab/figure-model')
|
|
387
|
+
let asset = createV3Asset('Gantry')
|
|
388
|
+
asset = addV3Box(asset, { id: 'rail', size: { x: 3000, y: 120, z: 120 }, position: { x: 0, y: 0, z: 0 } })
|
|
389
|
+
asset = addV3Box(asset, { id: 'bridge', size: { x: 160, y: 160, z: 1300 }, position: { x: 0, y: 0, z: 0 } })
|
|
390
|
+
asset = applyV3Authoring(asset, { kind: 'attach', part: 'rail', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
391
|
+
asset = applyV3Authoring(asset, { kind: 'attach', part: 'bridge', face: 'bottom', to: { part: 'rail', face: 'top' }, align: { x: 'min' } })
|
|
392
|
+
const joint = asset.joints!.find(j => j.body1.part === 'bridge')!
|
|
393
|
+
const form = { kind: 'slide' as const, axis: 'x' as const, id: 'x', min: 0, max: 500, follow: '', multiplier: 1, offset: 0, travel: 'rail:x', travelPlus: -160 }
|
|
394
|
+
const record = motionJoint(joint, form, 'bridge')
|
|
395
|
+
assert.deepEqual(record.travel, { source: { of: 'part', part: 'rail', axis: 'x' }, plus: -160 })
|
|
396
|
+
assert.equal(record.lowerLimit, undefined, '거리는 레일이 정한다 — mm 범위는 없다')
|
|
397
|
+
asset = applyV3Authoring(asset, { kind: 'set-joint', joint: record })
|
|
398
|
+
assert.deepEqual(motionOf(asset, 'bridge')?.travel, { part: 'rail', axis: 'x', plus: -160 }, '화면은 무엇을 따르는지 읽는다')
|
|
399
|
+
const end = (railLength: number) => {
|
|
400
|
+
const a = { ...asset, designInputs: { ...asset.designInputs, 'rail.size.x': railLength } }
|
|
401
|
+
const g = (id: string) => compileV3Asset(a).evaluate({ x: 1 }).geometry.find(x => x.id === id)! as unknown as { pose: { t: number[] }; shape: { dimensions: { width: number } } }
|
|
402
|
+
return Math.round(g('bridge').pose.t[0]! + 80 - (g('rail').pose.t[0]! + railLength / 2))
|
|
403
|
+
}
|
|
404
|
+
assert.equal(end(3000), 0, '끝까지(1) 가면 브리지 끝이 레일 끝')
|
|
405
|
+
assert.equal(end(4500), 0, '레일이 길어져도 끝까지')
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자리도 비율로 옮긴다 (사이징, 2026-09-25)', async () => {
|
|
409
|
+
const { alignOf } = await import('../client/modeller/v3-links.ts')
|
|
410
|
+
const { compileV3Asset } = await import('@hatiolab/figure-model')
|
|
411
|
+
let asset = createV3Asset('Panel')
|
|
412
|
+
asset = addV3Box(asset, { id: 'panel', size: { x: 1000, y: 800, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
413
|
+
asset = addV3Box(asset, { id: 'lamp', size: { x: 60, y: 60, z: 30 }, position: { x: 0, y: 0, z: 0 } })
|
|
414
|
+
asset = applyV3Authoring(asset, { kind: 'attach', part: 'panel', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
415
|
+
const front = { part: 'panel', face: 'front' }
|
|
416
|
+
assert.deepEqual(alignOf(asset, 'lamp', front, { x: 'share' }, { x: 300 }), undefined, '도형 크기가 없으면 비율을 보낼 수 없다')
|
|
417
|
+
asset = applyV3Authoring(asset, { kind: 'declare-instance-size', range: { x: { min: 200, max: 5000 }, y: { min: 200, max: 5000 }, z: { min: 10, max: 500 } }, at: { x: 1000, y: 800, z: 40 } })
|
|
418
|
+
asset = applyV3Authoring(asset, { kind: 'link-dimension', part: 'panel', axis: 'x', source: { of: 'instance', axis: 'x' } })
|
|
419
|
+
const align = alignOf(asset, 'lamp', front, { x: 'share' }, { x: 300 })!
|
|
420
|
+
assert.deepEqual(align, { x: { mm: { from: { of: 'instance', axis: 'x' }, times: 0.3 } } })
|
|
421
|
+
asset = applyV3Authoring(asset, { kind: 'attach', part: 'lamp', face: 'back', to: front, align })
|
|
422
|
+
const x = (size: number) => Math.round(compileV3Asset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'lamp')!.pose.t[0]!)
|
|
423
|
+
assert.equal(x(1000), 300)
|
|
424
|
+
assert.equal(x(2000), 600, '도형이 두 배면 거리도 두 배')
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이 도는 자리까지 재지 않는다 (2026-09-25)', async () => {
|
|
428
|
+
const { instanceRangeToDeclare } = await import('../client/modeller/v3-links.ts')
|
|
429
|
+
let asset = createV3Asset('Door')
|
|
430
|
+
asset = addV3Box(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
431
|
+
asset = addV3Box(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
432
|
+
asset = applyV3Authoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
433
|
+
asset = applyV3Authoring(asset, { kind: 'attach', part: 'leaf', face: 'left', to: { part: 'jamb', face: 'right' }, align: { y: 'min', z: 'max' }, gap: 5 })
|
|
434
|
+
const j = asset.joints!.find(o => o.id === 'leaf')!
|
|
435
|
+
asset = applyV3Authoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 0, state: { id: 'open' } } })
|
|
436
|
+
assert.deepEqual(instanceRangeToDeclare(asset)!.at, { x: 995, y: 2100, z: 150 })
|
|
437
|
+
})
|
|
@@ -37,10 +37,12 @@ test('작업대에서 가린 부품은 캔버스에서 빠지고, 다른 도형
|
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
test('지울 수 없는 부품은 사람의 말로 거절한다 — 다섯 말 모두', () => {
|
|
40
|
-
|
|
40
|
+
/* Since 2026-09-25 the reason is told apart (partRefusalOf, tested in v3-refusals); the editor hands the refusal to it. */
|
|
41
|
+
assert.match(editor, /private removePart[\s\S]*?partRefusalOf\(error, id,/)
|
|
41
42
|
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
|
|
42
43
|
const table = JSON.parse(read(`../translations/${lang}.json`))
|
|
43
|
-
|
|
44
|
+
for (const key of ['part-is-referenced', 'part-holds-others', 'part-in-declared-space', 'part-is-followed', 'part-is-led'])
|
|
45
|
+
assert.ok(table[`figure.text.${key}`]?.includes('{part}'), `${lang} ${key}`)
|
|
44
46
|
}
|
|
45
47
|
})
|
|
46
48
|
|
package/test/v3-refusals.test.ts
CHANGED
|
@@ -26,3 +26,54 @@ test('위쪽 끝도, 부품 이름이 없어도', () => {
|
|
|
26
26
|
test('다른 거절은 건드리지 않는다', () => {
|
|
27
27
|
assert.equal(sizeRangeRefusalOf(Object.assign(new Error('x'), { code: 'EDIT_TARGET' }), say), null)
|
|
28
28
|
})
|
|
29
|
+
|
|
30
|
+
test('범위가 받을 수 없는 연결은 같은 연결을 범위를 좁혀 다시 권한다 — 다시 재기로는 풀리지 않는다 (사이징, 2026-09-25)', async () => {
|
|
31
|
+
const { narrowingRetryOf } = await import('../client/modeller/v3-refusals.ts')
|
|
32
|
+
const { addV3Box, applyV3Authoring, createV3Asset } = await import('@hatiolab/figure-model')
|
|
33
|
+
let asset = addV3Box(createV3Asset('Panel'), { id: 'panel', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 0, z: 0 } })
|
|
34
|
+
asset = applyV3Authoring(asset, { kind: 'declare-instance-size', range: { x: { min: 1, max: 4000 }, y: { min: 1, max: 4000 }, z: { min: 1, max: 4000 } }, at: { x: 400, y: 400, z: 400 } })
|
|
35
|
+
const link = { kind: 'link-dimension' as const, part: 'panel', axis: 'x' as const, source: { of: 'instance' as const, axis: 'x' as const }, plus: -50 }
|
|
36
|
+
let error: unknown
|
|
37
|
+
try {
|
|
38
|
+
applyV3Authoring(asset, link)
|
|
39
|
+
} catch (e) {
|
|
40
|
+
error = e
|
|
41
|
+
}
|
|
42
|
+
const say = (key: string, p: Record<string, string>) => `${key} ${JSON.stringify(p)}`
|
|
43
|
+
const offer = narrowingRetryOf(link, error, say)!
|
|
44
|
+
assert.ok(offer, '권한다')
|
|
45
|
+
assert.match(offer.label, /figure.button.narrow-range-from .*"axis":"X"/)
|
|
46
|
+
assert.deepEqual(offer.retry, { ...link, narrowRange: true })
|
|
47
|
+
assert.doesNotThrow(() => applyV3Authoring(asset, offer.retry as never), '권한 것은 받아들여진다')
|
|
48
|
+
assert.equal(narrowingRetryOf({ kind: 'attach' }, error, say), null, '연결이 아닌 명령에는 권하지 않는다')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('부품을 못 지우는 까닭을 넷으로 가려 말한다 — 매단 것 · 공간 선언 · 크기를 따르는 것 · 따라 움직이는 것 (2026-09-25)', async () => {
|
|
52
|
+
const { partRefusalOf } = await import('../client/modeller/v3-refusals.ts')
|
|
53
|
+
const fm = await import('@hatiolab/figure-model')
|
|
54
|
+
const say = (key: string, p: Record<string, string>) => `${key} ${JSON.stringify(p)}`
|
|
55
|
+
let a = fm.createV3Asset('Bench')
|
|
56
|
+
a = fm.addV3Box(a, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: 0, y: 0, z: 0 } })
|
|
57
|
+
a = fm.addV3Box(a, { id: 'top', size: { x: 1200, y: 40, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
58
|
+
a = fm.addV3Box(a, { id: 'lamp', size: { x: 80, y: 80, z: 80 }, position: { x: 0, y: 0, z: 0 } })
|
|
59
|
+
a = fm.addV3Box(a, { id: 'shade', size: { x: 40, y: 40, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
60
|
+
a = fm.applyV3Authoring(a, { kind: 'attach', part: 'leg', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
61
|
+
a = fm.applyV3Authoring(a, { kind: 'attach', part: 'top', face: 'bottom', to: { part: 'leg', face: 'top' } })
|
|
62
|
+
a = fm.applyV3Authoring(a, { kind: 'attach', part: 'lamp', face: 'bottom', to: { part: 'top', face: 'top' }, align: { x: { mm: -300 } } })
|
|
63
|
+
a = fm.applyV3Authoring(a, { kind: 'attach', part: 'shade', face: 'bottom', to: { part: 'top', face: 'top' }, align: { x: { mm: 300 } } })
|
|
64
|
+
const turn = (asset: typeof a, part: string, extra: object) => fm.applyV3Authoring(asset, { kind: 'set-joint', joint: { ...asset.joints!.find(j => j.body1.part === part)!, type: 'revolute', axis: 'Y', ...extra } as never })
|
|
65
|
+
a = turn(a, 'lamp', { state: { id: 'lamp-turn' } })
|
|
66
|
+
a = turn(a, 'shade', { mimic: { joint: 'lamp', multiplier: 1 } })
|
|
67
|
+
const why = (asset: typeof a, part: string) => {
|
|
68
|
+
try {
|
|
69
|
+
fm.editV3Part(asset, { kind: 'remove-part', target: part })
|
|
70
|
+
return 'removed'
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return partRefusalOf(e, part, say)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
assert.match(why(a, 'top')!, /part-holds-others .*"others":"lamp, shade"/)
|
|
76
|
+
assert.match(why(a, 'lamp')!, /part-is-led .*"others":"shade"/)
|
|
77
|
+
const declared = fm.applyV3Authoring(a, { kind: 'declare-occupancy', proposal: fm.proposeV3Occupancy(a), placement: 'floor' })
|
|
78
|
+
assert.match(why(declared, 'shade')!, /part-in-declared-space .*"part":"shade"/, '잎인데 공간 선언에 들어 있다')
|
|
79
|
+
})
|
package/translations/en.json
CHANGED
|
@@ -710,5 +710,19 @@
|
|
|
710
710
|
"figure.button.remove-repeat": "remove the row",
|
|
711
711
|
"figure.label.twist": "twist (°)",
|
|
712
712
|
"figure.text.instance-size-undeclared": "the figure has no size, so a board cannot resize it.",
|
|
713
|
-
"figure.button.declare-instance-size": "declare the figure size"
|
|
713
|
+
"figure.button.declare-instance-size": "declare the figure size",
|
|
714
|
+
"figure.text.candidate-cannot-open": "this candidate is a V2 figure and the modeller opens V3 figures only, so it cannot be opened for review yet.",
|
|
715
|
+
"figure.label.follows-plus": "plus (mm)",
|
|
716
|
+
"figure.label.motion-travel": "travel",
|
|
717
|
+
"figure.label.motion-travel-mm": "its own range (mm)",
|
|
718
|
+
"figure.label.motion-travel-is": "travel: {part} · {axis} + {plus} mm",
|
|
719
|
+
"figure.label.align-share": "offset from centre, as a share of the figure size",
|
|
720
|
+
"figure.text.link-needs-smaller-range": "with this link {part} is gone when the figure's {axis} is under {mm} mm, and the declared sizes go below that.",
|
|
721
|
+
"figure.button.narrow-range-from": "narrow {axis} to {mm} mm and up, and link",
|
|
722
|
+
"figure.text.link-needs-larger-range": "with this link the figure cannot be drawn when its {axis} is over {mm} mm, and the declared sizes go above that.",
|
|
723
|
+
"figure.button.narrow-range-to": "narrow {axis} to {mm} mm and under, and link",
|
|
724
|
+
"figure.text.part-holds-others": "'{part}' holds {others}; take them off first.",
|
|
725
|
+
"figure.text.part-in-declared-space": "'{part}' is in the declared space; declare the space again without it, or fit it to the parts, first.",
|
|
726
|
+
"figure.text.part-is-followed": "{others} follow the size or place of '{part}'; make them their own numbers first.",
|
|
727
|
+
"figure.text.part-is-led": "{others} move with '{part}'; give them their own control first."
|
|
714
728
|
}
|
package/translations/ja.json
CHANGED
|
@@ -710,5 +710,19 @@
|
|
|
710
710
|
"figure.button.remove-repeat": "繰り返しをなくす",
|
|
711
711
|
"figure.label.twist": "ひねり (°)",
|
|
712
712
|
"figure.text.instance-size-undeclared": "図形サイズがないため、ボードでサイズを変えられません。",
|
|
713
|
-
"figure.button.declare-instance-size": "図形サイズを宣言"
|
|
713
|
+
"figure.button.declare-instance-size": "図形サイズを宣言",
|
|
714
|
+
"figure.text.candidate-cannot-open": "この候補は V2 図形で、モデラーは V3 図形しか開けないため、まだレビュー用に開けません。",
|
|
715
|
+
"figure.label.follows-plus": "加算 (mm)",
|
|
716
|
+
"figure.label.motion-travel": "移動距離",
|
|
717
|
+
"figure.label.motion-travel-mm": "自分の範囲 (mm)",
|
|
718
|
+
"figure.label.motion-travel-is": "移動距離: {part} · {axis} + {plus} mm",
|
|
719
|
+
"figure.label.align-share": "中央からの距離 — 図形サイズの比率で",
|
|
720
|
+
"figure.text.link-needs-smaller-range": "この連結で図形の {axis} が {mm} mm 未満だと {part} がなくなります。宣言された置けるサイズはそれより小さくまで開いています。",
|
|
721
|
+
"figure.button.narrow-range-from": "{axis} を {mm} mm からに絞って連結",
|
|
722
|
+
"figure.text.link-needs-larger-range": "この連結で図形の {axis} が {mm} mm を超えると描けません。宣言された置けるサイズはそれより大きくまで開いています。",
|
|
723
|
+
"figure.button.narrow-range-to": "{axis} を {mm} mm までに絞って連結",
|
|
724
|
+
"figure.text.part-holds-others": "'{part}' に {others} が付いているため削除できません。先に外してください。",
|
|
725
|
+
"figure.text.part-in-declared-space": "'{part}' は宣言された空間に含まれるため削除できません。この部品なしで空間を宣言し直すか「部品に合わせる」をしてから削除してください。",
|
|
726
|
+
"figure.text.part-is-followed": "{others} が '{part}' のサイズか位置に従っているため削除できません。先に自分の数値に変えてください。",
|
|
727
|
+
"figure.text.part-is-led": "{others} が '{part}' に連動しているため削除できません。先に自分の操作値に変えてください。"
|
|
714
728
|
}
|
package/translations/ko.json
CHANGED
|
@@ -710,5 +710,19 @@
|
|
|
710
710
|
"figure.button.remove-repeat": "되풀이 없애기",
|
|
711
711
|
"figure.label.twist": "비틀기 (°)",
|
|
712
712
|
"figure.text.instance-size-undeclared": "도형 크기가 없어 보드에서 크기를 바꿀 수 없습니다.",
|
|
713
|
-
"figure.button.declare-instance-size": "도형 크기 선언"
|
|
713
|
+
"figure.button.declare-instance-size": "도형 크기 선언",
|
|
714
|
+
"figure.text.candidate-cannot-open": "이 후보는 V2 도형이고 모델러는 V3 도형만 열 수 있어, 아직 검토하도록 열 수 없습니다.",
|
|
715
|
+
"figure.label.follows-plus": "더하기 (mm)",
|
|
716
|
+
"figure.label.motion-travel": "이동 거리",
|
|
717
|
+
"figure.label.motion-travel-mm": "제 범위 (mm)",
|
|
718
|
+
"figure.label.motion-travel-is": "이동 거리: {part} · {axis} + {plus} mm",
|
|
719
|
+
"figure.label.align-share": "가운데서 거리 — 도형 크기 비율로",
|
|
720
|
+
"figure.text.link-needs-smaller-range": "이 연결로 {part} 부품은 도형 {axis} 가 {mm} mm 보다 작으면 사라집니다. 지금 선언된 놓일 수 있는 크기는 그보다 작은 데까지 열려 있습니다.",
|
|
721
|
+
"figure.button.narrow-range-from": "{axis} 를 {mm} mm 부터로 좁히고 연결",
|
|
722
|
+
"figure.text.link-needs-larger-range": "이 연결로 도형 {axis} 가 {mm} mm 보다 크면 그릴 수 없습니다. 지금 선언된 놓일 수 있는 크기는 그보다 큰 데까지 열려 있습니다.",
|
|
723
|
+
"figure.button.narrow-range-to": "{axis} 를 {mm} mm 까지로 좁히고 연결",
|
|
724
|
+
"figure.text.part-holds-others": "'{part}' 에 {others} 이 매달려 있어 지울 수 없습니다. 먼저 떼어 주세요.",
|
|
725
|
+
"figure.text.part-in-declared-space": "'{part}' 는 선언된 공간에 들어 있어 지울 수 없습니다. 이 부품 없이 공간을 다시 선언하거나 「부품에 맞추기」를 한 뒤 지우세요.",
|
|
726
|
+
"figure.text.part-is-followed": "{others} 이 '{part}' 의 크기나 자리를 따르고 있어 지울 수 없습니다. 먼저 그 부품을 제 숫자로 바꾸세요.",
|
|
727
|
+
"figure.text.part-is-led": "{others} 이 '{part}' 를 따라 움직여 지울 수 없습니다. 먼저 제 조작값으로 바꾸세요."
|
|
714
728
|
}
|
package/translations/ms.json
CHANGED
|
@@ -710,5 +710,19 @@
|
|
|
710
710
|
"figure.button.remove-repeat": "buang baris",
|
|
711
711
|
"figure.label.twist": "pusingan (°)",
|
|
712
712
|
"figure.text.instance-size-undeclared": "rajah tiada saiz, jadi papan tidak boleh mengubah saiznya.",
|
|
713
|
-
"figure.button.declare-instance-size": "isytihar saiz rajah"
|
|
713
|
+
"figure.button.declare-instance-size": "isytihar saiz rajah",
|
|
714
|
+
"figure.text.candidate-cannot-open": "calon ini ialah rajah V2 dan pemodel hanya membuka rajah V3, jadi ia belum boleh dibuka untuk semakan.",
|
|
715
|
+
"figure.label.follows-plus": "tambah (mm)",
|
|
716
|
+
"figure.label.motion-travel": "jarak gerak",
|
|
717
|
+
"figure.label.motion-travel-mm": "julat sendiri (mm)",
|
|
718
|
+
"figure.label.motion-travel-is": "jarak gerak: {part} · {axis} + {plus} mm",
|
|
719
|
+
"figure.label.align-share": "jarak dari tengah — nisbah saiz rajah",
|
|
720
|
+
"figure.text.link-needs-smaller-range": "dengan pautan ini {part} hilang apabila {axis} rajah di bawah {mm} mm, dan saiz yang diisytihar pergi lebih rendah.",
|
|
721
|
+
"figure.button.narrow-range-from": "sempitkan {axis} dari {mm} mm dan pautkan",
|
|
722
|
+
"figure.text.link-needs-larger-range": "dengan pautan ini rajah tidak boleh dilukis apabila {axis} melebihi {mm} mm, dan saiz yang diisytihar pergi lebih tinggi.",
|
|
723
|
+
"figure.button.narrow-range-to": "sempitkan {axis} hingga {mm} mm dan pautkan",
|
|
724
|
+
"figure.text.part-holds-others": "'{part}' memegang {others}; tanggalkan dahulu.",
|
|
725
|
+
"figure.text.part-in-declared-space": "'{part}' berada dalam ruang yang diisytihar; isytihar semula ruang tanpanya, atau muatkan pada bahagian, dahulu.",
|
|
726
|
+
"figure.text.part-is-followed": "{others} mengikut saiz atau tempat '{part}'; jadikan nombor sendiri dahulu.",
|
|
727
|
+
"figure.text.part-is-led": "{others} bergerak bersama '{part}'; beri kawalan sendiri dahulu."
|
|
714
728
|
}
|
package/translations/zh.json
CHANGED
|
@@ -710,5 +710,19 @@
|
|
|
710
710
|
"figure.button.remove-repeat": "取消重复",
|
|
711
711
|
"figure.label.twist": "扭转 (°)",
|
|
712
712
|
"figure.text.instance-size-undeclared": "图形没有尺寸,看板上无法调整大小。",
|
|
713
|
-
"figure.button.declare-instance-size": "声明图形尺寸"
|
|
713
|
+
"figure.button.declare-instance-size": "声明图形尺寸",
|
|
714
|
+
"figure.text.candidate-cannot-open": "此候选为 V2 图形,而建模器只能打开 V3 图形,因此暂时无法打开以供审阅。",
|
|
715
|
+
"figure.label.follows-plus": "加 (mm)",
|
|
716
|
+
"figure.label.motion-travel": "行程",
|
|
717
|
+
"figure.label.motion-travel-mm": "自身范围 (mm)",
|
|
718
|
+
"figure.label.motion-travel-is": "行程: {part} · {axis} + {plus} mm",
|
|
719
|
+
"figure.label.align-share": "距中心 — 按图形尺寸比例",
|
|
720
|
+
"figure.text.link-needs-smaller-range": "此连接下,图形 {axis} 小于 {mm} mm 时 {part} 会消失,而声明的可放置尺寸低于此值。",
|
|
721
|
+
"figure.button.narrow-range-from": "将 {axis} 收窄为 {mm} mm 起并连接",
|
|
722
|
+
"figure.text.link-needs-larger-range": "此连接下,图形 {axis} 大于 {mm} mm 时无法绘制,而声明的可放置尺寸高于此值。",
|
|
723
|
+
"figure.button.narrow-range-to": "将 {axis} 收窄为 {mm} mm 以内并连接",
|
|
724
|
+
"figure.text.part-holds-others": "'{part}' 上挂着 {others},无法删除。请先卸下。",
|
|
725
|
+
"figure.text.part-in-declared-space": "'{part}' 在已声明的空间内,无法删除。请先在不含它的情况下重新声明空间或「贴合部件」。",
|
|
726
|
+
"figure.text.part-is-followed": "{others} 跟随 '{part}' 的尺寸或位置,无法删除。请先改为自身数值。",
|
|
727
|
+
"figure.text.part-is-led": "{others} 跟随 '{part}' 运动,无法删除。请先改为自身控制值。"
|
|
714
728
|
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/** Explicit AI entry survives lazy creation of the app-level dock. */
|
|
2
|
-
let pending: { figureId?: string; source?: unknown } | undefined
|
|
3
|
-
export function requestFigureAI(target: { figureId?: string; source?: unknown }) {
|
|
4
|
-
pending = structuredClone(target)
|
|
5
|
-
window.dispatchEvent(new CustomEvent('figure-ai-target'))
|
|
6
|
-
}
|
|
7
|
-
export function takeFigureAITarget() {
|
|
8
|
-
const target = pending
|
|
9
|
-
pending = undefined
|
|
10
|
-
return target
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let pendingStage: { figureId?: string; source?: any; proposal?: any } | undefined
|
|
14
|
-
export function stageFigureProposal(data: { figureId?: string; source?: any; proposal?: any }) {
|
|
15
|
-
pendingStage = structuredClone(data)
|
|
16
|
-
window.dispatchEvent(new CustomEvent('figure-ai-stage', { detail: data }))
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function consumePendingFigureProposal() {
|
|
20
|
-
const target = pendingStage
|
|
21
|
-
pendingStage = undefined
|
|
22
|
-
return target
|
|
23
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export declare function requestFigureAI(target: {
|
|
2
|
-
figureId?: string;
|
|
3
|
-
source?: unknown;
|
|
4
|
-
}): void;
|
|
5
|
-
export declare function takeFigureAITarget(): {
|
|
6
|
-
figureId?: string;
|
|
7
|
-
source?: unknown;
|
|
8
|
-
} | undefined;
|
|
9
|
-
export declare function stageFigureProposal(data: {
|
|
10
|
-
figureId?: string;
|
|
11
|
-
source?: any;
|
|
12
|
-
proposal?: any;
|
|
13
|
-
}): void;
|
|
14
|
-
export declare function consumePendingFigureProposal(): {
|
|
15
|
-
figureId?: string;
|
|
16
|
-
source?: any;
|
|
17
|
-
proposal?: any;
|
|
18
|
-
} | undefined;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/** Explicit AI entry survives lazy creation of the app-level dock. */
|
|
2
|
-
let pending;
|
|
3
|
-
export function requestFigureAI(target) {
|
|
4
|
-
pending = structuredClone(target);
|
|
5
|
-
window.dispatchEvent(new CustomEvent('figure-ai-target'));
|
|
6
|
-
}
|
|
7
|
-
export function takeFigureAITarget() {
|
|
8
|
-
const target = pending;
|
|
9
|
-
pending = undefined;
|
|
10
|
-
return target;
|
|
11
|
-
}
|
|
12
|
-
let pendingStage;
|
|
13
|
-
export function stageFigureProposal(data) {
|
|
14
|
-
pendingStage = structuredClone(data);
|
|
15
|
-
window.dispatchEvent(new CustomEvent('figure-ai-stage', { detail: data }));
|
|
16
|
-
}
|
|
17
|
-
export function consumePendingFigureProposal() {
|
|
18
|
-
const target = pendingStage;
|
|
19
|
-
pendingStage = undefined;
|
|
20
|
-
return target;
|
|
21
|
-
}
|
|
22
|
-
//# sourceMappingURL=figure-ai-target.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"figure-ai-target.js","sourceRoot":"","sources":["../../client/modeller/figure-ai-target.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,IAAI,OAA4D,CAAA;AAChE,MAAM,UAAU,eAAe,CAAC,MAA+C;IAC7E,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,CAAA;IACjC,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,kBAAkB,CAAC,CAAC,CAAA;AAC3D,CAAC;AACD,MAAM,UAAU,kBAAkB;IAChC,MAAM,MAAM,GAAG,OAAO,CAAA;IACtB,OAAO,GAAG,SAAS,CAAA;IACnB,OAAO,MAAM,CAAA;AACf,CAAC;AAED,IAAI,YAA6E,CAAA;AACjF,MAAM,UAAU,mBAAmB,CAAC,IAAyD;IAC3F,YAAY,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA;IACpC,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,iBAAiB,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;AAC5E,CAAC;AAED,MAAM,UAAU,4BAA4B;IAC1C,MAAM,MAAM,GAAG,YAAY,CAAA;IAC3B,YAAY,GAAG,SAAS,CAAA;IACxB,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["/** Explicit AI entry survives lazy creation of the app-level dock. */\nlet pending: { figureId?: string; source?: unknown } | undefined\nexport function requestFigureAI(target: { figureId?: string; source?: unknown }) {\n pending = structuredClone(target)\n window.dispatchEvent(new CustomEvent('figure-ai-target'))\n}\nexport function takeFigureAITarget() {\n const target = pending\n pending = undefined\n return target\n}\n\nlet pendingStage: { figureId?: string; source?: any; proposal?: any } | undefined\nexport function stageFigureProposal(data: { figureId?: string; source?: any; proposal?: any }) {\n pendingStage = structuredClone(data)\n window.dispatchEvent(new CustomEvent('figure-ai-stage', { detail: data }))\n}\n\nexport function consumePendingFigureProposal() {\n const target = pendingStage\n pendingStage = undefined\n return target\n}\n"]}
|
package/test/ai-target.test.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { test } from 'node:test'
|
|
3
|
-
import { requestFigureAI, takeFigureAITarget } from '../client/modeller/figure-ai-target.ts'
|
|
4
|
-
|
|
5
|
-
test('AI 진입은 늦게 생성되는 Dock에도 복제된 원본을 한 번 전달한다', () => {
|
|
6
|
-
const oldWindow = globalThis.window
|
|
7
|
-
globalThis.window = new EventTarget() as any
|
|
8
|
-
try {
|
|
9
|
-
const source = { type: 'TEST', parts: [{ name: 'cube', size: { y: 100 } }] }
|
|
10
|
-
requestFigureAI({ figureId: 'a', source })
|
|
11
|
-
source.parts[0].size.y = 200
|
|
12
|
-
assert.equal((takeFigureAITarget()?.source as any).parts[0].size.y, 100)
|
|
13
|
-
assert.equal(takeFigureAITarget(), undefined)
|
|
14
|
-
} finally { globalThis.window = oldWindow }
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('연속된 명시적 진입에서는 최신 대상만 지연 Dock에 전달한다', () => {
|
|
18
|
-
const oldWindow = globalThis.window
|
|
19
|
-
globalThis.window = new EventTarget() as any
|
|
20
|
-
try {
|
|
21
|
-
requestFigureAI({ figureId: 'a' })
|
|
22
|
-
requestFigureAI({ figureId: 'b' })
|
|
23
|
-
assert.equal(takeFigureAITarget()?.figureId, 'b')
|
|
24
|
-
} finally { globalThis.window = oldWindow }
|
|
25
|
-
})
|