@things-factory/figure-ui 10.1.45 → 10.1.47
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/modeller/angle-dial.ts +70 -0
- package/client/modeller/figure-settings.ts +328 -0
- package/client/modeller/figure-v3-capability.ts +137 -0
- package/client/modeller/figure-v3-clips.ts +253 -0
- package/client/modeller/figure-v3-controls.ts +212 -0
- package/client/modeller/figure-v3-links.ts +150 -79
- package/client/modeller/figure-v3-material.ts +108 -35
- package/client/modeller/figure-v3-parts.ts +96 -3
- package/client/modeller/figure-v3-preview.ts +326 -0
- package/client/modeller/figure-v3-properties.ts +29 -57
- package/client/modeller/figure-v3-verdict.ts +598 -60
- package/client/modeller/figure-view.ts +152 -0
- package/client/modeller/side-panel-styles.ts +326 -0
- package/client/modeller/v3-canvas.ts +221 -263
- package/client/modeller/v3-clips.ts +89 -0
- package/client/modeller/v3-controls.ts +54 -0
- package/client/modeller/v3-editor.ts +532 -223
- package/client/modeller/v3-fields.ts +62 -0
- package/client/modeller/v3-help.ts +41 -0
- package/client/modeller/v3-keys.ts +34 -0
- package/client/modeller/v3-links.ts +126 -10
- package/client/modeller/v3-live.ts +79 -0
- package/client/modeller/v3-maturity.ts +415 -0
- package/client/modeller/v3-palette.ts +45 -0
- package/client/modeller/v3-preview-model.ts +54 -0
- package/client/modeller/v3-stage-model.ts +159 -0
- package/client/pages/figure-list-page.ts +4 -4
- package/client/pages/figure-v3-modeller-page.ts +66 -134
- package/dist-client/modeller/angle-dial.d.ts +32 -0
- package/dist-client/modeller/angle-dial.js +41 -0
- package/dist-client/modeller/angle-dial.js.map +1 -0
- package/dist-client/modeller/figure-settings.d.ts +39 -0
- package/dist-client/modeller/figure-settings.js +317 -0
- package/dist-client/modeller/figure-settings.js.map +1 -0
- package/dist-client/modeller/figure-v3-capability.d.ts +22 -0
- package/dist-client/modeller/figure-v3-capability.js +141 -0
- package/dist-client/modeller/figure-v3-capability.js.map +1 -0
- package/dist-client/modeller/figure-v3-clips.d.ts +31 -0
- package/dist-client/modeller/figure-v3-clips.js +268 -0
- package/dist-client/modeller/figure-v3-clips.js.map +1 -0
- package/dist-client/modeller/figure-v3-controls.d.ts +36 -0
- package/dist-client/modeller/figure-v3-controls.js +226 -0
- package/dist-client/modeller/figure-v3-controls.js.map +1 -0
- package/dist-client/modeller/figure-v3-links.d.ts +10 -2
- package/dist-client/modeller/figure-v3-links.js +134 -79
- package/dist-client/modeller/figure-v3-links.js.map +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +5 -2
- package/dist-client/modeller/figure-v3-material.js +109 -35
- package/dist-client/modeller/figure-v3-material.js.map +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +10 -0
- package/dist-client/modeller/figure-v3-parts.js +88 -3
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-preview.d.ts +34 -0
- package/dist-client/modeller/figure-v3-preview.js +322 -0
- package/dist-client/modeller/figure-v3-preview.js.map +1 -0
- package/dist-client/modeller/figure-v3-properties.d.ts +2 -2
- package/dist-client/modeller/figure-v3-properties.js +29 -57
- package/dist-client/modeller/figure-v3-properties.js.map +1 -1
- package/dist-client/modeller/figure-v3-verdict.d.ts +31 -17
- package/dist-client/modeller/figure-v3-verdict.js +579 -59
- package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +118 -0
- package/dist-client/modeller/figure-view.js +78 -0
- package/dist-client/modeller/figure-view.js.map +1 -0
- package/dist-client/modeller/side-panel-styles.d.ts +23 -0
- package/dist-client/modeller/side-panel-styles.js +326 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -0
- package/dist-client/modeller/v3-canvas.d.ts +29 -37
- package/dist-client/modeller/v3-canvas.js +188 -260
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-clips.d.ts +39 -0
- package/dist-client/modeller/v3-clips.js +58 -0
- package/dist-client/modeller/v3-clips.js.map +1 -0
- package/dist-client/modeller/v3-controls.d.ts +28 -0
- package/dist-client/modeller/v3-controls.js +28 -0
- package/dist-client/modeller/v3-controls.js.map +1 -0
- package/dist-client/modeller/v3-editor.d.ts +58 -1
- package/dist-client/modeller/v3-editor.js +556 -218
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +30 -0
- package/dist-client/modeller/v3-fields.js +46 -0
- package/dist-client/modeller/v3-fields.js.map +1 -0
- package/dist-client/modeller/v3-help.d.ts +18 -0
- package/dist-client/modeller/v3-help.js +37 -0
- package/dist-client/modeller/v3-help.js.map +1 -0
- package/dist-client/modeller/v3-keys.d.ts +14 -0
- package/dist-client/modeller/v3-keys.js +23 -0
- package/dist-client/modeller/v3-keys.js.map +1 -0
- package/dist-client/modeller/v3-links.d.ts +44 -2
- package/dist-client/modeller/v3-links.js +125 -8
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-live.d.ts +31 -0
- package/dist-client/modeller/v3-live.js +58 -0
- package/dist-client/modeller/v3-live.js.map +1 -0
- package/dist-client/modeller/v3-maturity.d.ts +120 -0
- package/dist-client/modeller/v3-maturity.js +317 -0
- package/dist-client/modeller/v3-maturity.js.map +1 -0
- package/dist-client/modeller/v3-palette.d.ts +7 -0
- package/dist-client/modeller/v3-palette.js +42 -0
- package/dist-client/modeller/v3-palette.js.map +1 -0
- package/dist-client/modeller/v3-preview-model.d.ts +32 -0
- package/dist-client/modeller/v3-preview-model.js +48 -0
- package/dist-client/modeller/v3-preview-model.js.map +1 -0
- package/dist-client/modeller/v3-stage-model.d.ts +31 -0
- package/dist-client/modeller/v3-stage-model.js +121 -0
- package/dist-client/modeller/v3-stage-model.js.map +1 -0
- package/dist-client/pages/figure-list-page.js +4 -4
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -6
- package/dist-client/pages/figure-v3-modeller-page.js +64 -136
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.chk.tsbuildinfo +1 -0
- package/dist-client/tsconfig.scratch-fm.tsbuildinfo +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/v3-clips.test.ts +70 -0
- package/test/v3-controls.test.ts +68 -0
- package/test/v3-fields.test.ts +62 -0
- package/test/v3-gizmo.test.ts +3 -5
- package/test/v3-keys.test.ts +37 -0
- package/test/v3-layout.test.ts +78 -0
- package/test/v3-links.test.ts +118 -7
- package/test/v3-live.test.ts +99 -0
- package/test/v3-material.test.ts +6 -0
- package/test/v3-maturity.test.ts +268 -0
- package/test/v3-parts-wiring.test.ts +3 -1
- package/test/v3-preview.test.ts +42 -0
- package/test/v3-properties.test.ts +4 -2
- package/test/v3-scene-canvas.test.ts +85 -0
- package/test/v3-stage-model.test.ts +25 -0
- package/test/v3-theme.test.ts +16 -25
- package/test/v3-verdict.test.ts +12 -6
- package/test/v3-view.test.ts +20 -0
- package/translations/en.json +98 -5
- package/translations/ja.json +100 -7
- package/translations/ko.json +98 -5
- package/translations/ms.json +100 -7
- package/translations/zh.json +100 -7
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.47",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -26,19 +26,19 @@
|
|
|
26
26
|
"clean": "npm run clean:server && npm run clean:client"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@hatiolab/figure-model": "^0.1.
|
|
30
|
-
"@hatiolab/things-scene": "^10.1.
|
|
29
|
+
"@hatiolab/figure-model": "^0.1.53",
|
|
30
|
+
"@hatiolab/things-scene": "^10.1.30",
|
|
31
31
|
"@material/web": "^2.0.0",
|
|
32
32
|
"@operato/data-grist": "^10.0.0",
|
|
33
33
|
"@operato/graphql": "^10.0.0",
|
|
34
34
|
"@operato/i18n": "^10.0.0",
|
|
35
|
-
"@operato/layout": "^10.
|
|
35
|
+
"@operato/layout": "^10.28.0",
|
|
36
36
|
"@operato/p13n": "^10.0.0",
|
|
37
37
|
"@operato/shell": "^10.0.0",
|
|
38
38
|
"@operato/styles": "^10.0.0",
|
|
39
39
|
"@operato/utils": "^10.0.0",
|
|
40
|
-
"@things-factory/figure-service": "^10.1.
|
|
40
|
+
"@things-factory/figure-service": "^10.1.47",
|
|
41
41
|
"three": "^0.185.1"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "185f20d7d9a15715a0ee2757a6a8189ec36cfb9d"
|
|
44
44
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **클립 — V2 의 애니메이션** (V3 동등표 ⑥, 2026-09-24).
|
|
3
|
+
*
|
|
4
|
+
* 조작값을 시간에 따라 움직이는 자리다. 읽는 값은 figure-model 편집을 실제로 적용한 자산에서 재고, 배선은 소스에서 본다.
|
|
5
|
+
*/
|
|
6
|
+
import { test } from 'node:test'
|
|
7
|
+
import assert from 'node:assert/strict'
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
import { addV3Box, applyV3Authoring, createV3Asset, editV3Part } from '@hatiolab/figure-model'
|
|
12
|
+
import type { V3Asset } from '@hatiolab/figure-model'
|
|
13
|
+
|
|
14
|
+
import { clipsOf, poseAt, trackEdit, undrivenOf, withKey, withoutKey } from '../client/modeller/v3-clips.ts'
|
|
15
|
+
|
|
16
|
+
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
17
|
+
|
|
18
|
+
const machine = (): V3Asset => {
|
|
19
|
+
let asset = createV3Asset('Machine')
|
|
20
|
+
asset = addV3Box(asset, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
21
|
+
asset = applyV3Authoring(asset, { kind: 'add-motion', part: 'block', motion: { kind: 'slide', axis: 'x' }, state: { id: 'shift', unit: 'mm', min: 0, max: 60, start: 0 } })
|
|
22
|
+
asset = addV3Box(asset, { id: 'arm', size: { x: 40, y: 200, z: 40 }, position: { x: 0, y: 200, z: 0 } })
|
|
23
|
+
return applyV3Authoring(asset, { kind: 'add-motion', part: 'arm', motion: { kind: 'turn', axis: 'y' }, state: { id: 'spin', unit: 'deg', min: -180, max: 180, start: 0 } })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('클립마다 길이와 트랙을, 트랙마다 키를 초로 읽는다', () => {
|
|
27
|
+
let asset = editV3Part(machine(), { kind: 'set-state-input', input: 'shift', label: '밀기' })
|
|
28
|
+
asset = editV3Part(asset, { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 60 }, { seconds: 4, value: 0 }] })
|
|
29
|
+
assert.deepEqual(undrivenOf(asset), ['spin'], '재생하지 않는 조작값만 새 트랙이 된다')
|
|
30
|
+
const [clip] = clipsOf(asset)
|
|
31
|
+
assert.equal(clip!.name, 'cycle')
|
|
32
|
+
assert.equal(clip!.duration, 4)
|
|
33
|
+
assert.deepEqual(clip!.tracks.map(t => [t.state, t.label, t.unit, t.keys]), [['shift', '밀기', 'mm', [{ seconds: 0, value: 0 }, { seconds: 2, value: 60 }, { seconds: 4, value: 0 }]]])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('막대를 옮기면 그 순간의 자세를 한 묶음으로 — 드라이버와 같은 값', () => {
|
|
37
|
+
const asset = editV3Part(machine(), { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
|
|
38
|
+
assert.deepEqual(poseAt(asset, 'cycle', 1), [{ kind: 'set-state', input: 'shift', unit: 'mm', value: 15 }])
|
|
39
|
+
assert.deepEqual(poseAt(asset, 'nothing', 1), [])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('키를 그 순간에 더하고 빼며, 쓰면 figure-model 이 받는다 — 양 끝은 남는다', () => {
|
|
43
|
+
let asset = editV3Part(machine(), { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
|
|
44
|
+
const [clip] = clipsOf(asset)
|
|
45
|
+
const track = clip!.tracks[0]!
|
|
46
|
+
const keys = withKey(track.keys, { seconds: 2, value: 30 })
|
|
47
|
+
assert.deepEqual(keys.map(k => k.seconds), [0, 2, 4])
|
|
48
|
+
assert.deepEqual(withKey(keys, { seconds: 2, value: 45 }).map(k => k.value), [0, 45, 60], '같은 순간이면 바꾼다')
|
|
49
|
+
asset = editV3Part(asset, trackEdit(clip!, track, { keys }))
|
|
50
|
+
assert.equal(clipsOf(asset)[0]!.tracks[0]!.keys.length, 3)
|
|
51
|
+
assert.equal(withoutKey(track.keys, 1), null, '두 개뿐이면 뺄 수 없다')
|
|
52
|
+
assert.equal(withoutKey(keys, 0), null, '처음 키(0초)는 뺄 수 없다')
|
|
53
|
+
assert.deepEqual(withoutKey(keys, 1)!.map(k => k.seconds), [0, 4])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('움직임 탭에 서고, 막대는 입력 한 묶음 · 나머지는 부품 편집으로 편집기에 닿는다', () => {
|
|
57
|
+
const editor = read('../client/modeller/v3-editor.ts')
|
|
58
|
+
const start = editor.indexOf('<figure-v3-clips')
|
|
59
|
+
const block = editor.slice(start, editor.indexOf('</figure-v3-clips>', start))
|
|
60
|
+
assert.ok(start >= 0)
|
|
61
|
+
assert.ok(block.includes('@v3-states=${(e: CustomEvent) => this.run(() => this.session!.apply(e.detail.changes))}'))
|
|
62
|
+
assert.ok(block.includes('@v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'))
|
|
63
|
+
const clips = read('../client/modeller/figure-v3-clips.ts')
|
|
64
|
+
assert.ok(clips.includes("@change=${(e: Event) => this.scrub(clip, number(e))}"), '막대는 놓을 때 한 번')
|
|
65
|
+
const keys = [...clips.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
|
|
66
|
+
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
|
|
67
|
+
const table = JSON.parse(read(`../translations/${lang}.json`))
|
|
68
|
+
for (const key of keys) assert.ok(table[key], `${lang}: ${key}`)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **조작값 — V2 의 파라미터와 자세 도구** (V3 동등표 ⑥, 2026-09-24).
|
|
3
|
+
*
|
|
4
|
+
* 보드가 값을 바꿔 도형을 움직이는 자리다. 화면이 읽는 값을 명령을 실제로 적용한 자산에서 재고, 화면의 배선은
|
|
5
|
+
* 소스에서 확인한다.
|
|
6
|
+
*/
|
|
7
|
+
import { test } from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
import { readFileSync } from 'node:fs'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
|
|
12
|
+
import { addV3Box, applyV3Authoring, createV3Asset, editV3Part } from '@hatiolab/figure-model'
|
|
13
|
+
import type { V3Asset } from '@hatiolab/figure-model'
|
|
14
|
+
|
|
15
|
+
import { controlsOf, poseAsDefault, poseDiffers } from '../client/modeller/v3-controls.ts'
|
|
16
|
+
|
|
17
|
+
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
18
|
+
|
|
19
|
+
const slider = (): V3Asset => {
|
|
20
|
+
let asset = createV3Asset('Slider')
|
|
21
|
+
asset = addV3Box(asset, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
22
|
+
asset = applyV3Authoring(asset, { kind: 'add-motion', part: 'block', motion: { kind: 'slide', axis: 'x' }, state: { id: 'shift', unit: 'mm', min: 0, max: 60, start: 20 } })
|
|
23
|
+
asset = addV3Box(asset, { id: 'arm', size: { x: 40, y: 200, z: 40 }, position: { x: 0, y: 200, z: 0 } })
|
|
24
|
+
return applyV3Authoring(asset, { kind: 'add-motion', part: 'arm', motion: { kind: 'turn', axis: 'y' }, state: { id: 'spin', unit: 'deg', min: -90, max: 90, start: 0 } })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('조작값마다 이름 · 단위 · 범위 · 지금 값 · 시작 값 · 이동 시간을 읽는다', () => {
|
|
28
|
+
let asset = slider()
|
|
29
|
+
asset = editV3Part(asset, { kind: 'set-state-input', input: 'shift', label: '밀기', sweep: 2 })
|
|
30
|
+
const [shift, spin] = controlsOf(asset, { shift: 45 })
|
|
31
|
+
assert.deepEqual(
|
|
32
|
+
{ ...shift, step: undefined },
|
|
33
|
+
{ id: 'shift', label: '밀기', named: true, unit: 'mm', min: 0, max: 60, value: 45, start: 20, sweep: 2, step: undefined }
|
|
34
|
+
)
|
|
35
|
+
assert.equal(shift!.step, 0.6, '길이는 범위의 백분의 일씩')
|
|
36
|
+
assert.equal(spin!.label, 'spin', '이름이 없으면 id')
|
|
37
|
+
assert.equal(spin!.named, false)
|
|
38
|
+
assert.equal(spin!.step, 1, '각도는 1도씩')
|
|
39
|
+
assert.equal(spin!.value, 0, '편집기 값이 없으면 시작 값')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('지금 자세를 기본으로 — 보고 있는 자세가 시작 자세와 다를 때만, 한 번의 편집으로', () => {
|
|
43
|
+
const asset = slider()
|
|
44
|
+
assert.equal(poseDiffers(controlsOf(asset)), false)
|
|
45
|
+
const looking = controlsOf(asset, { shift: 50, spin: 30 })
|
|
46
|
+
assert.equal(poseDiffers(looking), true)
|
|
47
|
+
const kept = editV3Part(asset, poseAsDefault(looking))
|
|
48
|
+
assert.deepEqual([kept.stateDefaults['shift'], kept.stateDefaults['spin']], [50, 30])
|
|
49
|
+
assert.equal(poseDiffers(controlsOf(kept, { shift: 50, spin: 30 })), false)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('편집기에 닿는다 — 값 바꾸기는 입력으로, 이름 · 범위 · 시간 · 기본 자세는 부품 편집으로', () => {
|
|
53
|
+
const editor = read('../client/modeller/v3-editor.ts')
|
|
54
|
+
const start = editor.indexOf('<figure-v3-controls')
|
|
55
|
+
const block = editor.slice(start, editor.indexOf('</figure-v3-controls>', start))
|
|
56
|
+
assert.ok(start >= 0)
|
|
57
|
+
assert.ok(block.includes("@v3-state=${(e: CustomEvent) => this.run(() => this.session!.apply([{ kind: 'set-state'"))
|
|
58
|
+
assert.ok(block.includes('@v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'))
|
|
59
|
+
assert.ok(editor.includes("const design = view.fields.filter(i => i.role !== 'state')"), '조작값은 설계 값 칸에 두 번 나오지 않는다')
|
|
60
|
+
|
|
61
|
+
const controls = read('../client/modeller/figure-v3-controls.ts')
|
|
62
|
+
assert.ok(controls.includes("@change=${(e: Event) => this.try(c, number(e))}"), '슬라이더는 놓을 때 한 번 — 끄는 동안 기록이 쌓이지 않는다')
|
|
63
|
+
const keys = [...controls.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
|
|
64
|
+
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
|
|
65
|
+
const table = JSON.parse(read(`../translations/${lang}.json`))
|
|
66
|
+
for (const key of keys) assert.ok(table[key], `${lang}: ${key}`)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **값 칸의 묶음 — V2 인스펙터처럼** (사용자 2026-09-24: 「적절한 그루핑이 없어서 가시성이 좋지 않다. 크기는 왜
|
|
3
|
+
* 한줄씩 차지하는가」). 위치 · 회전 · 크기는 X · Y · Z 세 칸 한 줄, 형상의 나머지 치수는 제 제목 아래 한 줄씩.
|
|
4
|
+
*/
|
|
5
|
+
import { test } from 'node:test'
|
|
6
|
+
import assert from 'node:assert/strict'
|
|
7
|
+
import { readFileSync } from 'node:fs'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
import { groupFields, partLabelOf } from '../client/modeller/v3-fields.ts'
|
|
11
|
+
|
|
12
|
+
const f = (id: string, value = 1, unit = 'mm') => ({ id, unit, value, role: 'design' })
|
|
13
|
+
|
|
14
|
+
test('부품의 위치 · 회전 · 크기는 세 칸 한 줄씩, 나머지는 한 줄씩', () => {
|
|
15
|
+
const fields = [
|
|
16
|
+
f('arm.position.x'), f('arm.position.y'), f('arm.position.z'),
|
|
17
|
+
f('arm.rotation.x', 0, 'deg'), f('arm.rotation.y', 0, 'deg'), f('arm.rotation.z', 0, 'deg'),
|
|
18
|
+
f('arm.dimension.radius'), f('arm.dimension.length'),
|
|
19
|
+
f('other.position.x'), f('size.x')
|
|
20
|
+
]
|
|
21
|
+
const { vectors, singles } = groupFields(fields, 'arm')
|
|
22
|
+
assert.deepEqual(vectors.map(g => g.kind === 'vector' && g.name), ['position', 'rotation'])
|
|
23
|
+
assert.deepEqual(singles.map(g => g.kind === 'single' && g.name), ['radius', 'length'], '다른 부품 · 도형의 값은 섞이지 않는다')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('도형은 크기(size.x · y · z)가 세 칸 한 줄', () => {
|
|
27
|
+
const { vectors, singles } = groupFields([f('size.x'), f('size.y'), f('size.z'), f('opening'), f('arm.position.x')])
|
|
28
|
+
assert.deepEqual(vectors.map(g => g.kind === 'vector' && g.name), ['size'])
|
|
29
|
+
assert.deepEqual(singles.map(g => g.kind === 'single' && g.name), ['opening'])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('오른쪽 칸은 한 벌의 규격 — 요소마다 제 제목 · 칸 · 단추 규칙을 두지 않는다', () => {
|
|
33
|
+
for (const file of ['v3-editor', 'figure-v3-properties', 'figure-v3-links', 'figure-v3-material', 'figure-v3-controls', 'figure-v3-clips', 'figure-v3-capability']) {
|
|
34
|
+
const source = readFileSync(fileURLToPath(new URL(`../client/modeller/${file}.ts`, import.meta.url)), 'utf8')
|
|
35
|
+
assert.ok(source.includes('SidePanelStyles'), `${file} 가 공용 규격을 쓴다`)
|
|
36
|
+
const styles = source.slice(source.indexOf('static styles'), source.indexOf('`]', source.indexOf('static styles')))
|
|
37
|
+
assert.doesNotMatch(styles, /\n h3 \{|\n button\[act\] \{|\n div\[form\] \{/, `${file} 가 제 규칙을 다시 적는다`)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('긴 설명은 제목 옆 「?」 로 연다 — 늘 펼쳐 두지 않는다 (V2)', () => {
|
|
42
|
+
for (const [file, key] of [
|
|
43
|
+
['figure-v3-links', 'figure.text.motion-explained'],
|
|
44
|
+
['figure-v3-links', 'figure.text.place-explained'],
|
|
45
|
+
['figure-v3-links', 'figure.text.fastening-explained'],
|
|
46
|
+
['figure-v3-clips', 'figure.text.clips-explained']
|
|
47
|
+
]) {
|
|
48
|
+
const source = readFileSync(fileURLToPath(new URL(`../client/modeller/${file}.ts`, import.meta.url)), 'utf8')
|
|
49
|
+
assert.ok(source.includes(`this.help.button('${key}')`) && source.includes(`this.help.hint('${key}')`), `${file}: ${key}`)
|
|
50
|
+
assert.ok(!source.includes(`<p hint>\${i18next.t('${key}')}</p>`), `${file}: ${key} 가 늘 펼쳐져 있다`)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('되풀이한 부품은 되풀이의 이름과 몇 번째인지로 읽는다 — 날 id 를 보이지 않는다', () => {
|
|
55
|
+
assert.equal(partLabelOf('["roller.repeat",0,"roller.value"]'), 'roller 1')
|
|
56
|
+
assert.equal(partLabelOf('["rack.repeat",1,"shelf.repeat",2,"shelf.value"]'), 'rack 2·3')
|
|
57
|
+
assert.equal(partLabelOf('deck'), 'deck', '보통 부품은 제 이름')
|
|
58
|
+
assert.equal(partLabelOf('[not json'), '[not json')
|
|
59
|
+
const parts = readFileSync(fileURLToPath(new URL('../client/modeller/figure-v3-parts.ts', import.meta.url)), 'utf8')
|
|
60
|
+
assert.ok(parts.includes('>${partLabelOf(part.id)}'), '목록이 이 이름으로 그린다')
|
|
61
|
+
})
|
|
62
|
+
|
package/test/v3-gizmo.test.ts
CHANGED
|
@@ -81,12 +81,10 @@ test('끈 것은 세션의 한 걸음이다 — 한 번의 되돌리기로 돌
|
|
|
81
81
|
assert.match(editor, /if \(changes\.length\) this\.run\(\(\) => this\.session!\.apply\(changes\)\)/)
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
-
test('캔버스 —
|
|
84
|
+
test('캔버스 — 기즈모 · W E R · 스냅은 씬의 편집 모드가 한다, V2 처럼', () => {
|
|
85
85
|
const canvas = read('../client/modeller/v3-canvas.ts')
|
|
86
|
-
assert.match(canvas,
|
|
87
|
-
assert.
|
|
88
|
-
assert.match(canvas, /if \(this\.justDragged \|\| this\.transform\?\.dragging\) return/)
|
|
89
|
-
assert.match(canvas, /setFromQuaternion\(this\.proxy\.quaternion, 'XYZ'\)/)
|
|
86
|
+
assert.match(canvas, /mode: 1\n/)
|
|
87
|
+
assert.doesNotMatch(canvas, /TransformControls|OrbitControls/)
|
|
90
88
|
})
|
|
91
89
|
|
|
92
90
|
test('기즈모의 말은 다섯 말 모두에 있다', () => {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **모델러의 키** — V2 의 Ctrl+Z · Ctrl+Y 에 더해 복제 · 지우기 · 도형 전체로. 칸에 치는 키는 칸의 것이다.
|
|
3
|
+
*/
|
|
4
|
+
import { test } from 'node:test'
|
|
5
|
+
import assert from 'node:assert/strict'
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
import { keyActionOf } from '../client/modeller/v3-keys.ts'
|
|
10
|
+
|
|
11
|
+
test('되돌리기 · 다시 하기 — Ctrl 과 Cmd 모두, 부품을 고르지 않아도', () => {
|
|
12
|
+
assert.equal(keyActionOf({ key: 'z', ctrlKey: true }, ''), 'undo')
|
|
13
|
+
assert.equal(keyActionOf({ key: 'Z', metaKey: true, shiftKey: true }, ''), 'redo')
|
|
14
|
+
assert.equal(keyActionOf({ key: 'y', ctrlKey: true }, ''), 'redo')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('고른 부품에만 — 복제 · 지우기 · 도형 전체로', () => {
|
|
18
|
+
assert.equal(keyActionOf({ key: 'd', ctrlKey: true }, 'arm'), 'duplicate')
|
|
19
|
+
assert.equal(keyActionOf({ key: 'Delete' }, 'arm'), 'remove')
|
|
20
|
+
assert.equal(keyActionOf({ key: 'Backspace' }, 'arm'), 'remove')
|
|
21
|
+
assert.equal(keyActionOf({ key: 'Escape' }, 'arm'), 'deselect')
|
|
22
|
+
for (const key of ['Delete', 'Backspace', 'Escape']) assert.equal(keyActionOf({ key }, ''), null, `${key} 는 고른 부품이 없으면 아무 일도 안 한다`)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('칸에 치는 키는 칸의 것 — 이름 칸의 Backspace 는 글자를 지운다', () => {
|
|
26
|
+
for (const tagName of ['INPUT', 'select', 'TEXTAREA'])
|
|
27
|
+
assert.equal(keyActionOf({ key: 'Backspace', target: { tagName } }, 'arm'), null, tagName)
|
|
28
|
+
assert.equal(keyActionOf({ key: 'z', ctrlKey: true, target: { tagName: 'INPUT' } }, ''), null, '칸 안의 Ctrl+Z 는 칸의 되돌리기')
|
|
29
|
+
assert.equal(keyActionOf({ key: 'Delete', target: { tagName: 'DIV', isContentEditable: true } }, 'arm'), null)
|
|
30
|
+
assert.equal(keyActionOf({ key: 'Delete', target: { tagName: 'CANVAS' } }, 'arm'), 'remove')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('편집기가 창의 키를 듣고, 떼어지면 놓는다', () => {
|
|
34
|
+
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/v3-editor.ts', import.meta.url)), 'utf8')
|
|
35
|
+
assert.ok(editor.includes("window.addEventListener('keydown', this.onKey)") && editor.includes("window.removeEventListener('keydown', this.onKey)"))
|
|
36
|
+
assert.ok(editor.includes('target: e.composedPath()[0] as HTMLElement'), '그림자 DOM 안의 칸도 칸으로 안다')
|
|
37
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **V3 모델러의 구성 — V2 의 세 칸과 오른쪽 탭, 그보다 낫게** (사용자 2026-09-24: 「v2 모델러의 구성과 레이아웃이
|
|
3
|
+
* 좋았다. 그 보다도 더 좋아야지.」).
|
|
4
|
+
*
|
|
5
|
+
* V2 는 부품 목록 · 캔버스 · 오른쪽 칸(아이콘 탭 여섯)이었다. V3 는 부품 속성이 캔버스 아래 좁은 칸에 끼어 있었다.
|
|
6
|
+
* 이 시험은 소스에서 그 구성을 확인한다.
|
|
7
|
+
*/
|
|
8
|
+
import { test } from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
14
|
+
const editor = read('../client/modeller/v3-editor.ts')
|
|
15
|
+
const renderBody = editor.slice(editor.indexOf(' render() {'), editor.indexOf(' private renderField('))
|
|
16
|
+
|
|
17
|
+
test('세 칸 — 부품 목록 · 캔버스 · 오른쪽 칸, 캔버스 아래에는 아무것도 없다', () => {
|
|
18
|
+
/* The three panes are ox-three-pane's (ADR-0060 decision 8): parts in start, the canvas in the middle, the side in end. */
|
|
19
|
+
const main = renderBody.slice(renderBody.indexOf('<ox-three-pane'), renderBody.indexOf('</ox-three-pane>'))
|
|
20
|
+
const order = ['<figure-v3-parts', '<figure-v3-canvas', '<aside slot="end">'].map(tag => main.indexOf(tag))
|
|
21
|
+
assert.match(main, /<figure-v3-parts\s+slot="start"/, '부품 목록은 왼쪽 칸')
|
|
22
|
+
assert.ok(order.every(i => i >= 0) && order[0]! < order[1]! && order[1]! < order[2]!, '부품 · 캔버스 · 오른쪽 칸 순서')
|
|
23
|
+
assert.equal((main.match(/<aside/g) ?? []).length, 1, '오른쪽 칸은 하나 — 페이지가 따로 칸을 두지 않는다')
|
|
24
|
+
assert.doesNotMatch(read('../client/pages/figure-v3-modeller-page.ts'), /<aside/)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('오른쪽 칸의 탭 — V2 의 여섯: 속성 · 능력 · 움직임 · 판정 · 환경 · 이력, 판정의 개수는 탭에 남는다', () => {
|
|
28
|
+
const tabs = [...renderBody.matchAll(/this\.renderTab\('([a-z]+)'/g)].map(m => m[1])
|
|
29
|
+
assert.deepEqual(tabs, ['properties', 'capability', 'motion', 'verdict', 'view', 'history'])
|
|
30
|
+
for (const tab of tabs) assert.ok(renderBody.includes(`?off=\${tab !== '${tab}'}`), `${tab} 는 감추되 떼지 않는다`)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('부품을 고르면 — 도형 전체의 탭(판정 · 이력)은 그 부품의 속성으로 넘어가고, 부품 탭은 새 부품을 보여 준다', () => {
|
|
34
|
+
const choose = editor.slice(editor.indexOf(' private choose('), editor.indexOf(' private asset()'))
|
|
35
|
+
assert.match(choose, /this\.selected = id/)
|
|
36
|
+
assert.match(choose, /if \(id && \(this\.tab === 'verdict' \|\| this\.tab === 'view' \|\| this\.tab === 'history'\)\) this\.tab = 'properties'/)
|
|
37
|
+
for (const from of ['@select-part=${(e: CustomEvent) => this.choose(e.detail.id)}', '@part-selected=${(e: CustomEvent<string>) => this.choose(e.detail)}'])
|
|
38
|
+
assert.ok(renderBody.includes(from), from)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('부품을 보고 있으면 도형 전체로 돌아가는 칩이 탭마다 있다', () => {
|
|
42
|
+
assert.ok((renderBody.match(/\$\{this\.renderSubject\(\)\}/g) ?? []).length >= 3)
|
|
43
|
+
assert.match(editor.slice(editor.indexOf(' private renderSubject()')), /@click=\$\{\(\) => \(this\.selected = ''\)\}/)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('탭마다 제자리 — 속성엔 부품의 모양 · 재질, 자리와 능력엔 사다리와 자리, 움직임엔 움직임 · 조작값', () => {
|
|
47
|
+
const panel = (tab: string) => {
|
|
48
|
+
const start = renderBody.indexOf(`?off=\${tab !== '${tab}'}`)
|
|
49
|
+
return renderBody.slice(start, renderBody.indexOf('<div panel', start + 1))
|
|
50
|
+
}
|
|
51
|
+
assert.ok(panel('properties').includes("this.renderLinks(unavailable, 'shape')") && panel('properties').includes('<figure-v3-material'))
|
|
52
|
+
assert.ok(panel('capability').includes("this.renderLinks(unavailable, 'places')") && panel('capability').includes('<figure-v3-capability'))
|
|
53
|
+
assert.ok(panel('motion').includes("this.renderLinks(unavailable, 'motion')") && panel('motion').includes('this.renderControls('))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('띠 — 칸 접기 · 되돌리기 · 전체 보기 · 저장 상태 · 저장, 말은 번역에서', () => {
|
|
57
|
+
/* The bar is ox-canvas-bar: folds at its edges (it folds the frame it is given), undo · redo · fit in the suite's places. */
|
|
58
|
+
const bar = renderBody.slice(renderBody.indexOf('<ox-canvas-bar'), renderBody.indexOf('</ox-canvas-bar>'))
|
|
59
|
+
for (const piece of ['.pane=${this.panes}', 'history', 'fit', 'undo-shortcut="Ctrl+Z"', 'redo-shortcut="Ctrl+Y"', 'session!.undo()', 'session!.redo()', '.fit()', 'role="status"', '@click=${this.save}'])
|
|
60
|
+
assert.ok(bar.includes(piece), piece)
|
|
61
|
+
assert.doesNotMatch(renderBody, /[가-힣]/, '편집기의 화면 말에 한국어가 박혀 있지 않다')
|
|
62
|
+
const keys = [...editor.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
|
|
63
|
+
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
|
|
64
|
+
const table = JSON.parse(read(`../translations/${lang}.json`))
|
|
65
|
+
for (const key of keys) assert.ok(table[key], `${lang}: ${key}`)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('탭 아이콘은 오른쪽 칸 맨 위 — 타입 칸은 V2 처럼 속성 탭 안의 첫 칸', () => {
|
|
70
|
+
const aside = renderBody.slice(renderBody.indexOf('<aside slot="end">'), renderBody.indexOf('</aside>'))
|
|
71
|
+
assert.match(aside, /^<aside slot="end">\s*<div tabs>/, '탭이 첫째')
|
|
72
|
+
const properties = aside.slice(aside.indexOf("?off=${tab !== 'properties'}"), aside.indexOf("?off=${tab !== 'capability'}"))
|
|
73
|
+
assert.ok(properties.includes('<slot name="figure"></slot>'))
|
|
74
|
+
const figure = read('../client/modeller/figure-v3-properties.ts')
|
|
75
|
+
assert.match(figure, /<h3>\$\{i18next\.t\('figure\.label\.group-figure'\)\}<\/h3>\s*<slot name="type"><\/slot>/, '타입은 「도형」 절의 첫 칸')
|
|
76
|
+
const page = read('../client/pages/figure-v3-modeller-page.ts')
|
|
77
|
+
assert.ok(page.includes('<label slot="type" type-field>'))
|
|
78
|
+
})
|
package/test/v3-links.test.ts
CHANGED
|
@@ -10,7 +10,7 @@ import assert from 'node:assert/strict'
|
|
|
10
10
|
import { addV3Box, addV3Primitive, applyV3Authoring, createV3Asset } from '@hatiolab/figure-model'
|
|
11
11
|
import type { V3Asset } from '@hatiolab/figure-model'
|
|
12
12
|
|
|
13
|
-
import { canFasten, canFollow, fastenedTo, followOf, hasInstanceSize, motionOf, positionsWorkedOut } from '../client/modeller/v3-links.ts'
|
|
13
|
+
import { acceptsOf, canFasten, canFollow, dimensionNow, instanceRangeOf, rangeDiffers, fastenedTo, followableAxes, followOf, hasInstanceSize, positionIsShare, holdsThings, motionOf, placeFacesOf, placeIdFor, placesOn, positionsWorkedOut } from '../client/modeller/v3-links.ts'
|
|
14
14
|
|
|
15
15
|
const pair = (): V3Asset => {
|
|
16
16
|
let asset = createV3Asset('Pair')
|
|
@@ -28,7 +28,7 @@ test('크기 — 제 치수 · 인스턴스 크기 · 다른 부품의 치수를
|
|
|
28
28
|
const asset = pair()
|
|
29
29
|
assert.deepEqual(followOf(asset, 'plate', 'x'), { of: 'own' })
|
|
30
30
|
const byInstance = applyV3Authoring(asset, { kind: 'link-dimension', part: 'plate', axis: 'x', source: { of: 'instance', axis: 'x' } })
|
|
31
|
-
assert.deepEqual(followOf(byInstance, 'plate', 'x'), { of: 'instance', axis: 'x' })
|
|
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
33
|
assert.deepEqual(followOf(byPart, 'block', 'x'), { of: 'part', part: 'plate', axis: 'x' }, '배율이 끼어도 원천을 찾는다')
|
|
34
34
|
const back = applyV3Authoring(byPart, { kind: 'unlink-dimension', part: 'block', axis: 'x', mm: 300 })
|
|
@@ -46,7 +46,8 @@ test('붙이기 — 누구에게 붙었나, 붙일 수 있는 모양인가', ()
|
|
|
46
46
|
const withDrum = addV3Primitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 50, length: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
47
47
|
assert.equal(canFasten(withDrum, 'block'), true)
|
|
48
48
|
assert.equal(canFasten(withDrum, 'drum'), true, '원기둥도 면으로 붙인다(figure-model 0.1.47)')
|
|
49
|
-
assert.equal(canFollow(withDrum, 'drum'),
|
|
49
|
+
assert.equal(canFollow(withDrum, 'drum'), true, '원기둥은 반지름(X)과 길이(Y)를 따를 수 있다')
|
|
50
|
+
assert.deepEqual(followableAxes(withDrum, 'drum'), ['x', 'y'], '원기둥에는 제 Z 가 없다')
|
|
50
51
|
})
|
|
51
52
|
|
|
52
53
|
test('움직임 — 미끄러짐 · 돌기, 축, 무엇이 모느냐를 읽는다', () => {
|
|
@@ -81,7 +82,8 @@ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.m
|
|
|
81
82
|
|
|
82
83
|
test('고르개의 값은 지금 따르는 것을 가리킨다', () => {
|
|
83
84
|
assert.equal(choiceOf({ of: 'own' }), 'own')
|
|
84
|
-
assert.equal(choiceOf({ of: 'instance', axis: 'y' }), 'instance:y')
|
|
85
|
+
assert.equal(choiceOf({ of: 'instance', axis: 'y', times: 1 }), 'instance:y')
|
|
86
|
+
assert.equal(choiceOf({ of: 'instance', axis: 'y', times: 0.25 }), 'share:y', '몫은 비례로')
|
|
85
87
|
assert.equal(choiceOf({ of: 'part', part: 'plate', axis: 'x' }), 'part:plate:x')
|
|
86
88
|
})
|
|
87
89
|
|
|
@@ -95,8 +97,8 @@ test('편집기는 세 칸의 명령을 세션의 이력 하나로 돌리고,
|
|
|
95
97
|
|
|
96
98
|
test('떼면 지금 크기로 고정한다 — 따르던 값을 잃지 않는다', () => {
|
|
97
99
|
const links = read('../client/modeller/figure-v3-links.ts')
|
|
98
|
-
assert.
|
|
99
|
-
assert.
|
|
100
|
+
assert.ok(links.includes("this.command({ kind: 'unlink-dimension', part: this.partId, axis, mm: Math.max(1, Math.round(current)) })"))
|
|
101
|
+
assert.ok(links.includes('const current = dimensionNow(asset, this.partId, axis) ?? this.dims?.[AXES.indexOf(axis)] ?? 100'))
|
|
100
102
|
})
|
|
101
103
|
|
|
102
104
|
test('세 칸의 말은 다섯 말 모두에 있다', () => {
|
|
@@ -104,7 +106,7 @@ test('세 칸의 말은 다섯 말 모두에 있다', () => {
|
|
|
104
106
|
const keys = [...links.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
|
|
105
107
|
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
|
|
106
108
|
const table = JSON.parse(read(`../translations/${lang}.json`))
|
|
107
|
-
for (const key of [...keys, 'figure.text.follow-would-loop', 'figure.text.fasten-would-loop', 'figure.label.motion-slide', 'figure.label.motion-turn'])
|
|
109
|
+
for (const key of [...keys, 'figure.text.follow-would-loop', 'figure.text.fasten-would-loop', 'figure.label.motion-slide', 'figure.label.motion-turn', 'figure.label.place-grip', 'figure.label.place-seat'])
|
|
108
110
|
assert.ok(table[key], `${lang}: ${key}`)
|
|
109
111
|
}
|
|
110
112
|
})
|
|
@@ -121,3 +123,112 @@ test('도형 크기를 따라 움직이는 자리를 가려 읽고, 여기에
|
|
|
121
123
|
assert.deepEqual(positionsWorkedOut(fixed, 'lamp'), [])
|
|
122
124
|
assert.equal(fixed.designInputs['lamp.position.y'], 110, '지금 자리 그대로 — 100 + 10')
|
|
123
125
|
})
|
|
126
|
+
|
|
127
|
+
test('자리 — 부품의 면에 둔 집는 자리·놓는 자리를 그 부품에서 읽는다', () => {
|
|
128
|
+
let asset = pair()
|
|
129
|
+
asset = addV3Primitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 100, length: 300 }, position: { x: 0, y: 400, z: 0 } } as never)
|
|
130
|
+
assert.deepEqual(placeFacesOf(asset, 'block'), ['left', 'right', 'bottom', 'top', 'back', 'front'])
|
|
131
|
+
assert.deepEqual(placeFacesOf(asset, 'drum'), [], '둥근 몸에는 자리를 권하지 않는다 — 명령이 거절할 것이다')
|
|
132
|
+
|
|
133
|
+
assert.equal(placeIdFor(asset, 'block', 'grip'), 'block-grip')
|
|
134
|
+
asset = applyV3Authoring(asset, { kind: 'add-binding', id: placeIdFor(asset, 'block', 'grip'), label: '집는 자리', role: 'grip', part: 'block', face: 'top', accepts: acceptsOf(' BOX, TOTE,, BOX '), capacity: 2 })
|
|
135
|
+
assert.equal(placeIdFor(asset, 'block', 'grip'), 'block-grip-2', '같은 부품에 둘째 자리는 다른 id')
|
|
136
|
+
assert.deepEqual(
|
|
137
|
+
placesOn(asset, 'block').map(p => [p.id, p.role, p.face, p.accepts, p.capacity]),
|
|
138
|
+
[['block-grip', 'grip', 'top', ['BOX', 'TOTE'], 2]]
|
|
139
|
+
)
|
|
140
|
+
assert.deepEqual(placesOn(asset, 'plate'), [], '다른 부품의 자리는 안 섞인다')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('담는 능력이 없는 도형은 자리가 있어도 보드가 아무것도 넘기지 않는다 — 화면이 그것을 말한다', () => {
|
|
144
|
+
const asset = pair()
|
|
145
|
+
assert.equal(holdsThings(asset), false)
|
|
146
|
+
asset.document.capabilities.names = ['Capacity', 'Holdable']
|
|
147
|
+
assert.equal(holdsThings(asset), true)
|
|
148
|
+
asset.document.capabilities.names = ['FlowNode']
|
|
149
|
+
assert.equal(holdsThings(asset), true, 'FlowNode 는 Capacity 를 품는다')
|
|
150
|
+
|
|
151
|
+
const links = read('../client/modeller/figure-v3-links.ts')
|
|
152
|
+
assert.match(links, /holdsThings\(asset\)/)
|
|
153
|
+
assert.match(links, /kind: 'set-capabilities', names/)
|
|
154
|
+
const editor = read('../client/modeller/v3-editor.ts')
|
|
155
|
+
/* A slice and `includes`, not a lazy regex over the whole editor: that hangs when it does not match. */
|
|
156
|
+
const start = editor.indexOf('<figure-v3-links')
|
|
157
|
+
const block = editor.slice(start, editor.indexOf('</figure-v3-links>', start))
|
|
158
|
+
assert.ok(start >= 0 && block.includes('@v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'), '자리 절의 능력 선언이 편집기에 닿는다')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('받는 종류가 없으면 자리를 만들지 않는다 — 형식에 「아무거나」가 없다', () => {
|
|
162
|
+
const links = read('../client/modeller/figure-v3-links.ts')
|
|
163
|
+
assert.match(links, /\?disabled=\$\{this\.locked \|\| !accepts\.length \|\| !capacityOk\}/)
|
|
164
|
+
assert.deepEqual(acceptsOf(' , ,'), [])
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
/* ── V2's proportional rules (user 2026-09-24: ROBOT_ARM at twice the size looked broken) ───────────── */
|
|
168
|
+
|
|
169
|
+
const sizedPair = () => {
|
|
170
|
+
let asset = createV3Asset('Probe')
|
|
171
|
+
asset = addV3Box(asset, { id: 'base', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
|
|
172
|
+
asset = addV3Primitive(asset, { id: 'flange', kind: 'cylinder', dimensions: { radius: 20, length: 10 }, position: { x: 0, y: 95, z: 0 } })
|
|
173
|
+
asset.document.model.inputs.push({ id: 'size.y', unit: 'mm', min: 1, max: 20000, role: 'design' } as never)
|
|
174
|
+
asset.designInputs['size.y'] = 100
|
|
175
|
+
return asset
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
test('비례 — 인스턴스 크기의 몫을 따르는 치수는 비례로 읽고, 「남은 만큼 채우기」는 비례가 아니다', () => {
|
|
179
|
+
const asset = sizedPair()
|
|
180
|
+
assert.equal(dimensionNow(asset, 'flange', 'y'), 10, '원기둥의 Y 는 길이')
|
|
181
|
+
assert.equal(dimensionNow(asset, 'flange', 'x'), 20, '원기둥의 X 는 반지름')
|
|
182
|
+
const share = applyV3Authoring(asset, { kind: 'link-dimension', part: 'flange', axis: 'y', source: { of: 'instance', axis: 'y' }, times: 0.1 })
|
|
183
|
+
assert.deepEqual(followOf(share, 'flange', 'y'), { of: 'instance', axis: 'y', times: 0.1 })
|
|
184
|
+
|
|
185
|
+
/* V2's span: the size less what the others take -- a sum, not a share (ROBOT_ARM's tool flange). */
|
|
186
|
+
const fill = structuredClone(asset)
|
|
187
|
+
const m = fill.document.model as unknown as { nodes: { id: string; op: string; args: string[]; outputs: Record<string, string> }[]; constants: { id: string; unit: string; value: number }[] }
|
|
188
|
+
m.constants.push({ id: 'k.gaps', unit: 'mm', value: -90 })
|
|
189
|
+
m.nodes.push({ id: 'flange.free', op: 'add@1', args: ['size.y', 'k.gaps'], outputs: { value: 'flange.free.value' } })
|
|
190
|
+
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' } })
|
|
191
|
+
if (!m.constants.some(c => c.value === 1)) m.constants.push({ id: 'k.one', unit: 'ratio', value: 1 })
|
|
192
|
+
assert.deepEqual(followOf(fill as V3Asset, 'flange', 'y'), { of: 'expression' })
|
|
193
|
+
|
|
194
|
+
/* Before any link the shape reads the measure directly; a worked-out one there is not 「고정」 (ROBOT_ARM on :3300). */
|
|
195
|
+
const direct = structuredClone(asset)
|
|
196
|
+
const d = direct.document.model as unknown as typeof m
|
|
197
|
+
d.constants.push({ id: 'k.gaps', unit: 'mm', value: -90 })
|
|
198
|
+
d.nodes.push({ id: 'flange.free', op: 'add@1', args: ['size.y', 'k.gaps'], outputs: { value: 'flange.free.value' } })
|
|
199
|
+
d.nodes.find(n => n.id === 'flange.shape')!.args[1] = 'flange.free.value'
|
|
200
|
+
assert.deepEqual(followOf(direct as V3Asset, 'flange', 'y'), { of: 'expression' })
|
|
201
|
+
assert.deepEqual(followOf(asset, 'flange', 'y'), { of: 'own' }, '제 숫자는 고정')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('자리를 크기에 비례로 두면 그렇게 읽고, 다시 권하지 않는다', () => {
|
|
205
|
+
let asset = sizedPair()
|
|
206
|
+
asset = applyV3Authoring(asset, { kind: 'scale-position', part: 'flange', axis: 'y' })
|
|
207
|
+
assert.equal(positionIsShare(asset, 'flange', 'y'), true)
|
|
208
|
+
assert.equal(positionIsShare(asset, 'base', 'y'), false)
|
|
209
|
+
const links = read('../client/modeller/figure-v3-links.ts')
|
|
210
|
+
assert.ok(links.includes("hasInstanceSize(asset, axis) && !positionIsShare(asset, this.partId, axis)"))
|
|
211
|
+
assert.ok(links.includes("this.command({ kind: 'scale-position', part: this.partId, axis })"))
|
|
212
|
+
assert.ok(links.includes("const times = Math.round((current / size) * 1e6) / 1e6"), '비례는 지금 치수의 몫')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('놓일 수 있는 크기 — 남은 만큼 채우던 부품을 비례로 바꾸면 다시 재어 넓힌다', () => {
|
|
216
|
+
assert.equal(instanceRangeOf(createV3Asset('Probe')), undefined, '크기를 선언하지 않은 도형에는 범위가 없다')
|
|
217
|
+
let asset = sizedPair()
|
|
218
|
+
for (const axis of ['x', 'z'] as const) {
|
|
219
|
+
asset.document.model.inputs.push({ id: `size.${axis}`, unit: 'mm', min: 1, max: 20000, role: 'design' } as never)
|
|
220
|
+
asset.designInputs[`size.${axis}`] = 200
|
|
221
|
+
}
|
|
222
|
+
const y = asset.document.model.inputs.find(i => i.id === 'size.y')!
|
|
223
|
+
y.min = 90
|
|
224
|
+
y.max = 1000
|
|
225
|
+
const range = instanceRangeOf(asset)!
|
|
226
|
+
assert.deepEqual(range.declared.y, { min: 90, max: 1000 })
|
|
227
|
+
assert.deepEqual(range.measured.y, { min: 1, max: 1000 }, '기본 범위: 1 mm 에서 열 배')
|
|
228
|
+
assert.equal(rangeDiffers(range.declared, range.measured), true)
|
|
229
|
+
asset = applyV3Authoring(asset, { kind: 'declare-instance-size', range: range.measured, replace: true })
|
|
230
|
+
assert.equal(rangeDiffers(instanceRangeOf(asset)!.declared, instanceRangeOf(asset)!.measured), false, '다시 재면 같다')
|
|
231
|
+
const editor = read('../client/modeller/v3-editor.ts')
|
|
232
|
+
assert.ok(editor.includes("this.linkCommand({ kind: 'declare-instance-size', range: range.measured, replace: true })"))
|
|
233
|
+
})
|
|
234
|
+
|