@things-factory/figure-ui 10.1.52 → 10.1.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/client/figure-assistant.ts +20 -0
  2. package/client/index.ts +1 -0
  3. package/client/modeller/figure-v3-links.ts +56 -4
  4. package/client/modeller/v3-editor.ts +18 -2
  5. package/client/modeller/v3-links.ts +42 -9
  6. package/client/modeller/v3-refusals.ts +19 -0
  7. package/client/pages/figure-v3-modeller-page.ts +46 -16
  8. package/dist-client/figure-assistant.d.ts +1 -0
  9. package/dist-client/figure-assistant.js +19 -0
  10. package/dist-client/figure-assistant.js.map +1 -0
  11. package/dist-client/index.d.ts +1 -0
  12. package/dist-client/index.js +1 -0
  13. package/dist-client/index.js.map +1 -1
  14. package/dist-client/modeller/figure-v3-capability.d.ts +1 -1
  15. package/dist-client/modeller/figure-v3-clips.d.ts +1 -1
  16. package/dist-client/modeller/figure-v3-controls.d.ts +1 -1
  17. package/dist-client/modeller/figure-v3-links.d.ts +2 -0
  18. package/dist-client/modeller/figure-v3-links.js +57 -4
  19. package/dist-client/modeller/figure-v3-links.js.map +1 -1
  20. package/dist-client/modeller/figure-v3-material.d.ts +1 -1
  21. package/dist-client/modeller/figure-v3-properties.d.ts +1 -1
  22. package/dist-client/modeller/v3-editor.d.ts +2 -0
  23. package/dist-client/modeller/v3-editor.js +20 -2
  24. package/dist-client/modeller/v3-editor.js.map +1 -1
  25. package/dist-client/modeller/v3-links.d.ts +24 -2
  26. package/dist-client/modeller/v3-links.js +29 -5
  27. package/dist-client/modeller/v3-links.js.map +1 -1
  28. package/dist-client/modeller/v3-refusals.d.ts +13 -0
  29. package/dist-client/modeller/v3-refusals.js +20 -0
  30. package/dist-client/modeller/v3-refusals.js.map +1 -1
  31. package/dist-client/pages/figure-v3-modeller-page.d.ts +4 -1
  32. package/dist-client/pages/figure-v3-modeller-page.js +41 -15
  33. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  34. package/dist-client/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +6 -5
  36. package/test/browser/modeller.check.mjs +39 -0
  37. package/test/figure-assistant-surface.test.ts +52 -0
  38. package/test/v3-links.test.ts +62 -2
  39. package/test/v3-refusals.test.ts +21 -0
  40. package/translations/en.json +11 -1
  41. package/translations/ja.json +11 -1
  42. package/translations/ko.json +11 -1
  43. package/translations/ms.json +11 -1
  44. package/translations/zh.json +11 -1
  45. package/client/modeller/figure-ai-target.ts +0 -23
  46. package/dist-client/modeller/figure-ai-target.d.ts +0 -18
  47. package/dist-client/modeller/figure-ai-target.js +0 -22
  48. package/dist-client/modeller/figure-ai-target.js.map +0 -1
  49. 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.52",
3
+ "version": "10.1.53",
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.60",
31
- "@hatiolab/things-scene": "^10.1.37",
30
+ "@hatiolab/figure-model": "^0.1.62",
31
+ "@hatiolab/things-scene": "^10.1.39",
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/figure-service": "^10.1.52",
42
+ "@things-factory/ai-assistant": "^10.1.53",
43
+ "@things-factory/figure-service": "^10.1.53",
43
44
  "three": "^0.185.1"
44
45
  },
45
- "gitHead": "b3ec4f0a38f0759d396f49b1f47c547bff4657be"
46
+ "gitHead": "1bdb0c604614735fd5a5467aee1e56f53be6b89e"
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
+ })
@@ -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
- assert.deepEqual(followOf(fill as V3Asset, 'flange', 'y'), { of: 'expression' })
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,62 @@ 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
+ })
@@ -26,3 +26,24 @@ 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
+ })
@@ -710,5 +710,15 @@
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"
714
724
  }
@@ -710,5 +710,15 @@
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 までに絞って連結"
714
724
  }
@@ -710,5 +710,15 @@
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 까지로 좁히고 연결"
714
724
  }
@@ -710,5 +710,15 @@
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"
714
724
  }
@@ -710,5 +710,15 @@
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 以内并连接"
714
724
  }
@@ -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"]}
@@ -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
- })