@things-factory/figure-ui 10.1.75 → 10.1.76
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 +3 -3
- package/client/graphql/{v3-store.ts → store.ts} +6 -6
- package/client/modeller/{v3-ai-proposal.ts → ai-proposal.ts} +8 -8
- package/client/modeller/{v3-align.ts → align.ts} +14 -14
- package/client/modeller/{v3-canvas.ts → canvas.ts} +13 -13
- package/client/modeller/{v3-clips.ts → clips.ts} +5 -5
- package/client/modeller/{v3-controls.ts → controls.ts} +2 -2
- package/client/modeller/{v3-editor.ts → editor.ts} +117 -117
- package/client/modeller/{figure-v3-capability.ts → figure-capability-panel.ts} +7 -7
- package/client/modeller/{figure-v3-clips.ts → figure-clips.ts} +10 -10
- package/client/modeller/{figure-v3-controls.ts → figure-controls.ts} +9 -9
- package/client/modeller/{v3-figure-facts.ts → figure-facts.ts} +11 -11
- package/client/modeller/{figure-v3-first-save.ts → figure-first-save.ts} +3 -3
- package/client/modeller/figure-history-panel.ts +1 -1
- package/client/modeller/{figure-v3-links.ts → figure-links.ts} +35 -35
- package/client/modeller/{figure-v3-material.ts → figure-material-panel.ts} +20 -20
- package/client/modeller/{figure-v3-parts.ts → figure-parts.ts} +15 -15
- package/client/modeller/{figure-v3-preview.ts → figure-preview.ts} +14 -14
- package/client/modeller/{figure-v3-properties.ts → figure-properties.ts} +25 -25
- package/client/modeller/{figure-v3-recipe-catalog.ts → figure-recipe-catalog.ts} +10 -10
- package/client/modeller/{figure-v3-verdict.ts → figure-verdict.ts} +24 -24
- package/client/modeller/gate-findings.ts +2 -2
- package/client/modeller/{v3-gizmo.ts → gizmo.ts} +11 -11
- package/client/modeller/{v3-joint-drag.ts → joint-drag.ts} +11 -11
- package/client/modeller/{v3-joint-fan.ts → joint-fan.ts} +13 -13
- package/client/modeller/{v3-links.ts → links.ts} +59 -59
- package/client/modeller/{v3-live.ts → live.ts} +9 -9
- package/client/modeller/{v3-maturity.ts → maturity.ts} +65 -65
- package/client/modeller/modeller-route.ts +1 -1
- package/client/modeller/{v3-occupancy.ts → occupancy.ts} +6 -6
- package/client/modeller/{v3-palette.ts → palette.ts} +3 -3
- package/client/modeller/{v3-preview-model.ts → preview-model.ts} +8 -8
- package/client/modeller/{v3-product-session.ts → product-session.ts} +25 -25
- package/client/modeller/{v3-recipe-source.ts → recipe-source.ts} +2 -2
- package/client/modeller/{v3-recipes.ts → recipes.ts} +9 -9
- package/client/modeller/{v3-renderer.ts → renderer.ts} +16 -16
- package/client/modeller/{v3-stage-model.ts → stage-model.ts} +15 -15
- package/client/modeller/{v3-thumbnail.ts → thumbnail.ts} +8 -8
- package/client/pages/figure-list-page.ts +4 -4
- package/client/pages/{figure-v3-modeller-page.ts → figure-modeller-page.ts} +37 -37
- package/client/route.ts +1 -5
- package/dist-client/figure-assistant.js +3 -3
- package/dist-client/figure-assistant.js.map +1 -1
- package/dist-client/graphql/store.d.ts +3 -0
- package/dist-client/graphql/store.js +39 -0
- package/dist-client/graphql/store.js.map +1 -0
- package/dist-client/modeller/ai-proposal.d.ts +51 -0
- package/dist-client/modeller/ai-proposal.js +64 -0
- package/dist-client/modeller/ai-proposal.js.map +1 -0
- package/dist-client/modeller/align.d.ts +30 -0
- package/dist-client/modeller/align.js +75 -0
- package/dist-client/modeller/align.js.map +1 -0
- package/dist-client/modeller/canvas.d.ts +72 -0
- package/dist-client/modeller/canvas.js +379 -0
- package/dist-client/modeller/canvas.js.map +1 -0
- package/dist-client/modeller/clips.d.ts +39 -0
- package/dist-client/modeller/clips.js +58 -0
- package/dist-client/modeller/clips.js.map +1 -0
- package/dist-client/modeller/controls.d.ts +28 -0
- package/dist-client/modeller/controls.js +28 -0
- package/dist-client/modeller/controls.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +203 -0
- package/dist-client/modeller/editor.js +1526 -0
- package/dist-client/modeller/editor.js.map +1 -0
- package/dist-client/modeller/fields.d.ts +45 -0
- package/dist-client/modeller/fields.js +83 -0
- package/dist-client/modeller/fields.js.map +1 -0
- package/dist-client/modeller/figure-capability-panel.d.ts +22 -0
- package/dist-client/modeller/figure-capability-panel.js +141 -0
- package/dist-client/modeller/figure-capability-panel.js.map +1 -0
- package/dist-client/modeller/figure-clips.d.ts +31 -0
- package/dist-client/modeller/figure-clips.js +268 -0
- package/dist-client/modeller/figure-clips.js.map +1 -0
- package/dist-client/modeller/figure-controls.d.ts +36 -0
- package/dist-client/modeller/figure-controls.js +226 -0
- package/dist-client/modeller/figure-controls.js.map +1 -0
- package/dist-client/modeller/figure-facts.d.ts +35 -0
- package/dist-client/modeller/figure-facts.js +55 -0
- package/dist-client/modeller/figure-facts.js.map +1 -0
- package/dist-client/modeller/figure-first-save.d.ts +20 -0
- package/dist-client/modeller/figure-first-save.js +118 -0
- package/dist-client/modeller/figure-first-save.js.map +1 -0
- package/dist-client/modeller/figure-history-panel.d.ts +1 -1
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +77 -0
- package/dist-client/modeller/figure-links.js +649 -0
- package/dist-client/modeller/figure-links.js.map +1 -0
- package/dist-client/modeller/figure-material-panel.d.ts +39 -0
- package/dist-client/modeller/figure-material-panel.js +323 -0
- package/dist-client/modeller/figure-material-panel.js.map +1 -0
- package/dist-client/modeller/figure-parts.d.ts +69 -0
- package/dist-client/modeller/figure-parts.js +559 -0
- package/dist-client/modeller/figure-parts.js.map +1 -0
- package/dist-client/modeller/figure-preview.d.ts +34 -0
- package/dist-client/modeller/figure-preview.js +346 -0
- package/dist-client/modeller/figure-preview.js.map +1 -0
- package/dist-client/modeller/figure-properties.d.ts +45 -0
- package/dist-client/modeller/figure-properties.js +239 -0
- package/dist-client/modeller/figure-properties.js.map +1 -0
- package/dist-client/modeller/figure-recipe-catalog.d.ts +38 -0
- package/dist-client/modeller/figure-recipe-catalog.js +399 -0
- package/dist-client/modeller/figure-recipe-catalog.js.map +1 -0
- package/dist-client/modeller/figure-verdict.d.ts +64 -0
- package/dist-client/modeller/figure-verdict.js +787 -0
- package/dist-client/modeller/figure-verdict.js.map +1 -0
- package/dist-client/modeller/first-save.d.ts +17 -0
- package/dist-client/modeller/first-save.js +26 -0
- package/dist-client/modeller/first-save.js.map +1 -0
- package/dist-client/modeller/gate-findings.d.ts +2 -2
- package/dist-client/modeller/gate-findings.js +2 -2
- package/dist-client/modeller/gate-findings.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +72 -0
- package/dist-client/modeller/gizmo.js +111 -0
- package/dist-client/modeller/gizmo.js.map +1 -0
- package/dist-client/modeller/help.d.ts +18 -0
- package/dist-client/modeller/help.js +37 -0
- package/dist-client/modeller/help.js.map +1 -0
- package/dist-client/modeller/joint-drag.d.ts +19 -0
- package/dist-client/modeller/joint-drag.js +97 -0
- package/dist-client/modeller/joint-drag.js.map +1 -0
- package/dist-client/modeller/joint-fan.d.ts +9 -0
- package/dist-client/modeller/joint-fan.js +276 -0
- package/dist-client/modeller/joint-fan.js.map +1 -0
- package/dist-client/modeller/keys.d.ts +22 -0
- package/dist-client/modeller/keys.js +26 -0
- package/dist-client/modeller/keys.js.map +1 -0
- package/dist-client/modeller/links.d.ts +267 -0
- package/dist-client/modeller/links.js +480 -0
- package/dist-client/modeller/links.js.map +1 -0
- package/dist-client/modeller/live.d.ts +37 -0
- package/dist-client/modeller/live.js +68 -0
- package/dist-client/modeller/live.js.map +1 -0
- package/dist-client/modeller/maturity.d.ts +120 -0
- package/dist-client/modeller/maturity.js +313 -0
- package/dist-client/modeller/maturity.js.map +1 -0
- package/dist-client/modeller/modeller-route.d.ts +1 -1
- package/dist-client/modeller/modeller-route.js +1 -1
- package/dist-client/modeller/modeller-route.js.map +1 -1
- package/dist-client/modeller/occupancy.d.ts +18 -0
- package/dist-client/modeller/occupancy.js +24 -0
- package/dist-client/modeller/occupancy.js.map +1 -0
- package/dist-client/modeller/palette.d.ts +7 -0
- package/dist-client/modeller/palette.js +46 -0
- package/dist-client/modeller/palette.js.map +1 -0
- package/dist-client/modeller/preview-model.d.ts +47 -0
- package/dist-client/modeller/preview-model.js +68 -0
- package/dist-client/modeller/preview-model.js.map +1 -0
- package/dist-client/modeller/product-session.d.ts +510 -0
- package/dist-client/modeller/product-session.js +157 -0
- package/dist-client/modeller/product-session.js.map +1 -0
- package/dist-client/modeller/recipe-source.d.ts +8 -0
- package/dist-client/modeller/recipe-source.js +14 -0
- package/dist-client/modeller/recipe-source.js.map +1 -0
- package/dist-client/modeller/recipes.d.ts +37 -0
- package/dist-client/modeller/recipes.js +103 -0
- package/dist-client/modeller/recipes.js.map +1 -0
- package/dist-client/modeller/refusals.d.ts +68 -0
- package/dist-client/modeller/refusals.js +142 -0
- package/dist-client/modeller/refusals.js.map +1 -0
- package/dist-client/modeller/renderer.d.ts +40 -0
- package/dist-client/modeller/renderer.js +73 -0
- package/dist-client/modeller/renderer.js.map +1 -0
- package/dist-client/modeller/stage-model.d.ts +39 -0
- package/dist-client/modeller/stage-model.js +147 -0
- package/dist-client/modeller/stage-model.js.map +1 -0
- package/dist-client/modeller/thumbnail.d.ts +3 -0
- package/dist-client/modeller/thumbnail.js +75 -0
- package/dist-client/modeller/thumbnail.js.map +1 -0
- package/dist-client/pages/figure-list-page.d.ts +1 -1
- 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-modeller-page.d.ts +102 -0
- package/dist-client/pages/figure-modeller-page.js +548 -0
- package/dist-client/pages/figure-modeller-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +1 -5
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/docs/product-cutover.md +25 -0
- package/package.json +5 -5
- package/test/{v3-ai-proposal.test.ts → ai-proposal.test.ts} +6 -6
- package/test/{v3-align.test.ts → align.test.ts} +8 -8
- package/test/browser/modeller-page.js +30 -30
- package/test/browser/modeller.check.mjs +14 -14
- package/test/browser/serve.mjs +1 -1
- package/test/browser/thumbnail-page.js +6 -6
- package/test/{v3-clips.test.ts → clips.test.ts} +18 -18
- package/test/{v3-controls.test.ts → controls.test.ts} +15 -15
- package/test/{v3-fields.test.ts → fields.test.ts} +13 -13
- package/test/figure-assistant-surface.test.ts +3 -3
- package/test/{v3-first-save.test.ts → first-save.test.ts} +10 -10
- package/test/fixtures/joint-column.json +2 -2
- package/test/fixtures/joint-roller.json +2 -2
- package/test/fixtures/robot-arm-6dof.json +2 -2
- package/test/{v3-gizmo.test.ts → gizmo.test.ts} +33 -33
- package/test/{v3-joint-drag.test.ts → joint-drag.test.ts} +17 -17
- package/test/{v3-joint-fan.test.ts → joint-fan.test.ts} +18 -18
- package/test/joints.ts +6 -6
- package/test/{v3-keys.test.ts → keys.test.ts} +2 -2
- package/test/{v3-layout.test.ts → layout.test.ts} +8 -8
- package/test/{v3-links.test.ts → links.test.ts} +201 -201
- package/test/{v3-live.test.ts → live.test.ts} +18 -18
- package/test/{v3-material.test.ts → material.test.ts} +12 -12
- package/test/{v3-maturity.test.ts → maturity.test.ts} +74 -74
- package/test/modeller-route.test.ts +5 -6
- package/test/{v3-palette-icons.test.ts → palette-icons.test.ts} +1 -1
- package/test/{v3-parts-wiring.test.ts → parts-wiring.test.ts} +5 -5
- package/test/{v3-preview.test.ts → preview.test.ts} +12 -12
- package/test/{v3-product-session.test.ts → product-session.test.ts} +17 -17
- package/test/{v3-product-write.test.ts → product-write.test.ts} +20 -20
- package/test/{v3-properties.test.ts → properties.test.ts} +24 -24
- package/test/{v3-recipes.test.ts → recipes.test.ts} +9 -9
- package/test/{v3-refusals.test.ts → refusals.test.ts} +43 -43
- package/test/{v3-release-panel.test.ts → release-panel.test.ts} +21 -21
- package/test/renderer.test.ts +24 -0
- package/test/resolve-ts-siblings.mjs +1 -1
- package/test/{v3-scene-canvas.test.ts → scene-canvas.test.ts} +6 -6
- package/test/{v3-stage-model.test.ts → stage-model.test.ts} +5 -5
- package/test/{v3-theme.test.ts → theme.test.ts} +4 -4
- package/test/{v3-thumbnail.test.ts → thumbnail.test.ts} +4 -4
- package/test/{v3-verdict.test.ts → verdict.test.ts} +17 -17
- package/test/{v3-view.test.ts → view.test.ts} +2 -2
- package/things-factory.config.js +2 -3
- package/translations/en.json +2 -2
- package/translations/ja.json +2 -2
- package/translations/ko.json +2 -2
- package/translations/ms.json +2 -2
- package/translations/zh.json +2 -2
- package/docs/v3-product-cutover.md +0 -25
- package/test/v3-renderer.test.ts +0 -24
- /package/client/modeller/{v3-fields.ts → fields.ts} +0 -0
- /package/client/modeller/{v3-first-save.ts → first-save.ts} +0 -0
- /package/client/modeller/{v3-help.ts → help.ts} +0 -0
- /package/client/modeller/{v3-keys.ts → keys.ts} +0 -0
- /package/client/modeller/{v3-refusals.ts → refusals.ts} +0 -0
|
@@ -7,7 +7,7 @@ import assert from 'node:assert/strict'
|
|
|
7
7
|
import { readFileSync } from 'node:fs'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
|
|
10
|
-
import { groupFields, ownerOf, partLabelOf, repeatOf } from '../client/modeller/
|
|
10
|
+
import { groupFields, ownerOf, partLabelOf, repeatOf } from '../client/modeller/fields.ts'
|
|
11
11
|
|
|
12
12
|
const f = (id: string, value = 1, unit = 'mm') => ({ id, unit, value, role: 'design' })
|
|
13
13
|
|
|
@@ -30,7 +30,7 @@ test('도형은 크기(size.x · y · z)가 세 칸 한 줄', () => {
|
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
test('오른쪽 칸은 한 벌의 규격 — 요소마다 제 제목 · 칸 · 단추 규칙을 두지 않는다', () => {
|
|
33
|
-
for (const file of ['
|
|
33
|
+
for (const file of ['editor', 'figure-properties', 'figure-links', 'figure-material-panel', 'figure-controls', 'figure-clips', 'figure-capability-panel']) {
|
|
34
34
|
const source = readFileSync(fileURLToPath(new URL(`../client/modeller/${file}.ts`, import.meta.url)), 'utf8')
|
|
35
35
|
assert.ok(source.includes('SidePanelStyles'), `${file} 가 공용 규격을 쓴다`)
|
|
36
36
|
const styles = source.slice(source.indexOf('static styles'), source.indexOf('`]', source.indexOf('static styles')))
|
|
@@ -40,10 +40,10 @@ test('오른쪽 칸은 한 벌의 규격 — 요소마다 제 제목 · 칸 ·
|
|
|
40
40
|
|
|
41
41
|
test('긴 설명은 제목 옆 「?」 로 연다 — 늘 펼쳐 두지 않는다', () => {
|
|
42
42
|
for (const [file, key] of [
|
|
43
|
-
['figure-
|
|
44
|
-
['figure-
|
|
45
|
-
['figure-
|
|
46
|
-
['figure-
|
|
43
|
+
['figure-links', 'figure.text.motion-explained'],
|
|
44
|
+
['figure-links', 'figure.text.place-explained'],
|
|
45
|
+
['figure-links', 'figure.text.fastening-explained'],
|
|
46
|
+
['figure-clips', 'figure.text.clips-explained']
|
|
47
47
|
]) {
|
|
48
48
|
const source = readFileSync(fileURLToPath(new URL(`../client/modeller/${file}.ts`, import.meta.url)), 'utf8')
|
|
49
49
|
assert.ok(source.includes(`this.help.button('${key}')`) && source.includes(`this.help.hint('${key}')`), `${file}: ${key}`)
|
|
@@ -56,7 +56,7 @@ test('되풀이한 부품은 되풀이의 이름과 몇 번째인지로 읽는
|
|
|
56
56
|
assert.equal(partLabelOf('["roller.repeat",2,"mark.placed"]'), 'mark 3', '줄의 부품에 붙은 것은 제 이름으로 (ADR-0093, 2026-09-26)')
|
|
57
57
|
assert.equal(partLabelOf('deck'), 'deck', '보통 부품은 제 이름')
|
|
58
58
|
assert.equal(partLabelOf('[not json'), '[not json')
|
|
59
|
-
const parts = readFileSync(fileURLToPath(new URL('../client/modeller/figure-
|
|
59
|
+
const parts = readFileSync(fileURLToPath(new URL('../client/modeller/figure-parts.ts', import.meta.url)), 'utf8')
|
|
60
60
|
assert.ok(parts.includes('>${partLabelOf(part.id)}'), '목록이 이 이름으로 그린다')
|
|
61
61
|
})
|
|
62
62
|
|
|
@@ -67,7 +67,7 @@ test('되풀이한 부품의 속성 이름 칸도 사람의 이름이고, 몇
|
|
|
67
67
|
const carried = geometry.concat(Array.from({ length: 16 }, (_, i) => ({ id: JSON.stringify(['roller.repeat', i, 'mark.placed']) })))
|
|
68
68
|
assert.deepEqual(repeatOf('["roller.repeat",14,"roller.value"]', carried), { name: 'roller', count: 16, index: 15 }, '롤러에 붙은 것은 롤러 수에 안 든다')
|
|
69
69
|
assert.deepEqual(repeatOf('["roller.repeat",2,"mark.placed"]', carried), { name: 'mark', count: 16, index: 3 })
|
|
70
|
-
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/
|
|
70
|
+
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/editor.ts', import.meta.url)), 'utf8')
|
|
71
71
|
assert.ok(editor.includes('.value=${partLabelOf(this.selected)}'), '사용자 2026-09-24: 「부품 이름 ???」')
|
|
72
72
|
assert.ok(editor.includes('?disabled=${unavailable || !!repeatOf(this.selected, view.geometry)?.index}'), '복사본에는 바꿀 제 이름이 없고, 줄의 부품 자신은 이름을 바꿀 수 있다(walk 1, 2026-09-26)')
|
|
73
73
|
assert.equal(repeatOf('roller', geometry)?.index, 0, '줄의 부품 자신은 몇 번째가 아니다')
|
|
@@ -79,18 +79,18 @@ test('편집기는 저작한 부품 단위로 고르고 보인다 — 되풀이
|
|
|
79
79
|
assert.equal(ownerOf('["roller.repeat",2,"mark.placed"]'), 'mark', '롤러에 붙은 표시의 복사본을 누르면 그 표시를 고른다')
|
|
80
80
|
assert.deepEqual(repeatOf('roller', [{ id: '["roller.repeat",0,"roller.value"]' }, { id: '["roller.repeat",1,"roller.value"]' }, { id: 'deck' }]), { name: 'roller', count: 2, index: 0 })
|
|
81
81
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
82
|
-
const canvas = read('../client/modeller/
|
|
82
|
+
const canvas = read('../client/modeller/canvas.ts')
|
|
83
83
|
assert.ok(canvas.includes('.filter(([name]) => ownerOf(name) === this.selected)'), '사용자 2026-09-24: 「why repeatable parts are not selected?」')
|
|
84
|
-
const editor = read('../client/modeller/
|
|
84
|
+
const editor = read('../client/modeller/editor.ts')
|
|
85
85
|
assert.ok(editor.includes('this.selected = id ? ownerOf(id) : id'), '캔버스에서 복사본을 누르면 그 되풀이를 고른다')
|
|
86
86
|
assert.ok(editor.includes('.parts=${this.partRows(view.geometry)}'), '목록은 저작한 부품 한 줄씩')
|
|
87
87
|
assert.ok(editor.includes("this.session!.command({ kind: 'set-repeat', part: this.selected, pitch })"), 'pitch 는 명령으로 정한다')
|
|
88
|
-
const parts = read('../client/modeller/figure-
|
|
88
|
+
const parts = read('../client/modeller/figure-parts.ts')
|
|
89
89
|
assert.ok(parts.includes('?disabled=${this.locked || full}'), '등급 한도에 이르면 팔레트가 잠긴다')
|
|
90
90
|
})
|
|
91
91
|
|
|
92
92
|
test('pitch 는 한 곳에서 하나의 값이고, 늘 set-repeat 로 바꿔 최대 길이가 따라온다', () => {
|
|
93
|
-
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/
|
|
93
|
+
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/editor.ts', import.meta.url)), 'utf8')
|
|
94
94
|
assert.ok(editor.includes("groupFields(own, this.selected)"), '사용자 2026-09-24: 되풀이의 간격과 형상 치수의 repeat · pitch 가 같은 값으로 두 번 떴다')
|
|
95
95
|
assert.ok(editor.includes("!i.id.startsWith(this.selected + '.repeat.')"))
|
|
96
96
|
const pitch = editor.slice(editor.indexOf(' private setPitch('), editor.indexOf(' /** Choose a part,'))
|
|
@@ -104,7 +104,7 @@ test('pitch 는 한 곳에서 하나의 값이고, 늘 set-repeat 로 바꿔 최
|
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
test('기즈모로 끈, 규칙이 자리를 정하는 부품은 규칙을 지킨 채 옮겨지고, 관절·붙이기가 정하는 부품은 까닭을 말한다 — 튕겨 돌아가지 않는다', () => {
|
|
107
|
-
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/
|
|
107
|
+
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/editor.ts', import.meta.url)), 'utf8')
|
|
108
108
|
const moved = editor.slice(editor.indexOf(' private transformed('), editor.indexOf(' /** 맞추기'))
|
|
109
109
|
assert.ok(moved.includes("kind: 'nudge-part',"), '사용자 2026-09-24: 「기즈모 컨트롤도 전혀 작동이 안되는군」')
|
|
110
110
|
assert.ok(moved.includes('...(Object.keys(grow).length ? { grow } : {})'), '크기 끌기도 배율로 — 「되는 듯하다가 다시 원래 크기로 돌아간다」')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The
|
|
2
|
+
* The modeller's AI - the framework dock talks about the figure on screen (design/ai/shared-dock.md §3).
|
|
3
3
|
*
|
|
4
4
|
* What this holds: ① a saved figure provides a surface anchored on its row ② the draft is read at the moment of
|
|
5
5
|
* sending, not when the surface was provided ③ an unsaved figure provides nothing - no conversation without a row
|
|
@@ -11,7 +11,7 @@ import { readFileSync } from 'node:fs'
|
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
import { test } from 'node:test'
|
|
13
13
|
|
|
14
|
-
const source = readFileSync(fileURLToPath(new URL('../client/pages/figure-
|
|
14
|
+
const source = readFileSync(fileURLToPath(new URL('../client/pages/figure-modeller-page.ts', import.meta.url)), 'utf8')
|
|
15
15
|
|
|
16
16
|
test('a saved figure provides a surface anchored on its row', () => {
|
|
17
17
|
assert.match(source, /provideAssistantSurface\(this, \{/)
|
|
@@ -34,7 +34,7 @@ test('the surface is taken back when the page goes inactive or leaves', () => {
|
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
test('the dock applies a proposal through the page, on the version the model saw', () => {
|
|
37
|
-
assert.match(source, /window\.addEventListener\('figure-
|
|
37
|
+
assert.match(source, /window\.addEventListener\('figure-apply', this\.applyAI\)/)
|
|
38
38
|
assert.match(source, /this\.session\.applyProposal\(detail\.steps, detail\.revision, detail\.context\)/)
|
|
39
39
|
assert.match(source, /this\.takeNewFigureProposal\(\)/, 'a new figure opened for a proposal takes its steps')
|
|
40
40
|
})
|
|
@@ -5,8 +5,8 @@ import { test } from 'node:test'
|
|
|
5
5
|
import assert from 'node:assert/strict'
|
|
6
6
|
|
|
7
7
|
import { placeholderType } from '@hatiolab/figure-model'
|
|
8
|
-
import { firstSaveRefusalOf, releasableType, typeOf } from '../client/modeller/
|
|
9
|
-
import {
|
|
8
|
+
import { firstSaveRefusalOf, releasableType, typeOf } from '../client/modeller/first-save.ts'
|
|
9
|
+
import { createFigureProductSession, type FigureProductStore } from '../client/modeller/product-session.ts'
|
|
10
10
|
|
|
11
11
|
test('치는 대로 타입의 모양이 된다 — 대문자 · 숫자 · 밑줄', () => {
|
|
12
12
|
assert.equal(typeOf(' agv lifter '), 'AGV_LIFTER')
|
|
@@ -29,8 +29,8 @@ test('비었거나 이미 쓰는 이름이면 저장하지 않는다', () => {
|
|
|
29
29
|
|
|
30
30
|
/** A store that remembers what it was asked. */
|
|
31
31
|
function recording() {
|
|
32
|
-
const requests: Parameters<
|
|
33
|
-
const store:
|
|
32
|
+
const requests: Parameters<FigureProductStore['save']>[0][] = []
|
|
33
|
+
const store: FigureProductStore = {
|
|
34
34
|
load: async id => ({ id, source: saved ?? '' }),
|
|
35
35
|
save: async request => {
|
|
36
36
|
requests.push(request)
|
|
@@ -44,7 +44,7 @@ function recording() {
|
|
|
44
44
|
|
|
45
45
|
test('타입은 새 도형을 만드는 저장에만 실린다 — 이미 있는 도형의 저장은 타입을 보내지 않는다', async () => {
|
|
46
46
|
const { store, requests } = recording()
|
|
47
|
-
const session =
|
|
47
|
+
const session = createFigureProductSession(store)
|
|
48
48
|
session.startNew()
|
|
49
49
|
await session.save({ type: 'AGV_LIFTER' })
|
|
50
50
|
assert.equal(requests[0]!.type, 'AGV_LIFTER')
|
|
@@ -58,7 +58,7 @@ test('타입은 새 도형을 만드는 저장에만 실린다 — 이미 있는
|
|
|
58
58
|
|
|
59
59
|
test('타입을 주지 않으면 싣지 않는다 — 서버가 임시 이름을 붙인다', async () => {
|
|
60
60
|
const { store, requests } = recording()
|
|
61
|
-
const session =
|
|
61
|
+
const session = createFigureProductSession(store)
|
|
62
62
|
session.startNew()
|
|
63
63
|
await session.save()
|
|
64
64
|
assert.equal('type' in requests[0]!, false)
|
|
@@ -66,17 +66,17 @@ test('타입을 주지 않으면 싣지 않는다 — 서버가 임시 이름을
|
|
|
66
66
|
|
|
67
67
|
test('저장 전에 적은 타입은 처음 저장의 물음에 실려 온다 — 칸이 막혀 있어 적어도 사라졌다 (09-25)', async () => {
|
|
68
68
|
const { readFileSync } = await import('node:fs')
|
|
69
|
-
const page = readFileSync(new URL('../client/pages/figure-
|
|
69
|
+
const page = readFileSync(new URL('../client/pages/figure-modeller-page.ts', import.meta.url), 'utf8')
|
|
70
70
|
const field = page.slice(page.indexOf('<label slot="type" type-field>'), page.indexOf('</label>', page.indexOf('<label slot="type" type-field>')))
|
|
71
71
|
assert.doesNotMatch(field, /disabled=\$\{!view\.id\}/, '저장 전에도 칸에 적을 수 있다')
|
|
72
72
|
const rename = page.slice(page.indexOf('private renameType'), page.indexOf('private fitToParts'))
|
|
73
73
|
assert.match(rename, /if \(!id\) \{\s*this\.row = \{ \.\.\.this\.row, type: wanted \|\| undefined \}/, '저장 전에는 행 대신 화면이 타입을 쥔다')
|
|
74
|
-
assert.match(page, /<figure-
|
|
74
|
+
assert.match(page, /<figure-first-save \.type=\$\{this\.row\.type \?\? ''\}/, '물음은 그 타입으로 열린다')
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
test('처음 저장한 도형은 주소가 제 것이 된다 — 모델러를 다시 열면 새 도형이 열린다 (2026-09-25)', async () => {
|
|
78
78
|
const { readFileSync } = await import('node:fs')
|
|
79
|
-
const page = readFileSync(new URL('../client/pages/figure-
|
|
79
|
+
const page = readFileSync(new URL('../client/pages/figure-modeller-page.ts', import.meta.url), 'utf8')
|
|
80
80
|
const changed = page.slice(page.indexOf('private changed = () => {'), page.indexOf('private async readRow'))
|
|
81
81
|
assert.match(changed, /if \(view\.id && !this\.routeId && !view\.saving\) \{\s*this\.routeId = view\.id\s*navigate\(modellerPathOf\(\{ id: view\.id \}\), true\)/, '저장으로 id 를 얻으면 그 주소로 바꾼다(기록은 덮어쓴다)')
|
|
82
82
|
/* The early return that kept the old figure reads the route id; it now matches only the figure the address names. */
|
|
@@ -85,7 +85,7 @@ test('처음 저장한 도형은 주소가 제 것이 된다 — 모델러를
|
|
|
85
85
|
|
|
86
86
|
test('다른 도형을 열면 행을 먼저 비운다 — 앞 도형의 타입이 칸에 남지 않는다 (C2, 2026-09-25)', async () => {
|
|
87
87
|
const { readFileSync } = await import('node:fs')
|
|
88
|
-
const page = readFileSync(new URL('../client/pages/figure-
|
|
88
|
+
const page = readFileSync(new URL('../client/pages/figure-modeller-page.ts', import.meta.url), 'utf8')
|
|
89
89
|
const updated = page.slice(page.indexOf('async pageUpdated'), page.indexOf('private askFirstSave'))
|
|
90
90
|
const opening = updated.slice(updated.indexOf('if (id) {'), updated.indexOf('this.session.open(id'))
|
|
91
91
|
assert.match(opening, /this\.row = \{ version: 0 \}/, '열기 전에 행을 비운다')
|
|
@@ -9,16 +9,16 @@ import assert from 'node:assert/strict'
|
|
|
9
9
|
import { readFileSync } from 'node:fs'
|
|
10
10
|
import { fileURLToPath } from 'node:url'
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, compileFigureAsset, createFigureAsset, createFigureEditorWorkspace } from '@hatiolab/figure-model'
|
|
13
13
|
|
|
14
|
-
import { gizmoChanges, gizmoReachOf } from '../client/modeller/
|
|
14
|
+
import { gizmoChanges, gizmoReachOf } from '../client/modeller/gizmo.ts'
|
|
15
15
|
import { moving } from './joints.ts'
|
|
16
16
|
|
|
17
17
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
18
18
|
const probe = () => {
|
|
19
|
-
let asset =
|
|
20
|
-
asset =
|
|
21
|
-
asset =
|
|
19
|
+
let asset = createFigureAsset('Probe')
|
|
20
|
+
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
21
|
+
asset = addFigurePrimitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 50, length: 100 }, position: { x: 0, y: 150, z: 0 } })
|
|
22
22
|
return asset
|
|
23
23
|
}
|
|
24
24
|
const apply = (asset: ReturnType<typeof probe>, changes: ReturnType<typeof gizmoChanges>) => {
|
|
@@ -27,7 +27,7 @@ const apply = (asset: ReturnType<typeof probe>, changes: ReturnType<typeof gizmo
|
|
|
27
27
|
return next
|
|
28
28
|
}
|
|
29
29
|
const pose = (asset: ReturnType<typeof probe>, id: string) =>
|
|
30
|
-
(
|
|
30
|
+
(compileFigureAsset(asset).evaluate().geometry.find(g => (g as { id: string }).id === id) as unknown as { pose: { t: number[]; r: number[][] } }).pose
|
|
31
31
|
|
|
32
32
|
test('만든 상자는 셋 다, 원기둥도 셋 다 — 원기둥의 크기는 반지름과 길이로', () => {
|
|
33
33
|
assert.deepEqual(gizmoReachOf(probe() as never, 'body'), {
|
|
@@ -40,9 +40,9 @@ test('만든 상자는 셋 다, 원기둥도 셋 다 — 원기둥의 크기는
|
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
test('편집기의 도형 팔레트로 만든 상자 · 둥근 상자도 크기를 낸다 — 입력 이름이 dimension.* 이다', () => {
|
|
43
|
-
let asset =
|
|
44
|
-
asset =
|
|
45
|
-
asset =
|
|
43
|
+
let asset = createFigureAsset('Probe')
|
|
44
|
+
asset = addFigurePrimitive(asset, { id: 'box-1', kind: 'box', dimensions: { width: 100, height: 100, depth: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
45
|
+
asset = addFigurePrimitive(asset, { id: 'rb-1', kind: 'rounded-box', dimensions: { width: 100, height: 60, depth: 80, round: 10 }, position: { x: 0, y: 50, z: 0 } })
|
|
46
46
|
const box = gizmoReachOf(asset as never, 'box-1')
|
|
47
47
|
assert.equal(box.scale, true, '편집기에서 만든 상자를 키울 수 없었다 (2026-09-24, :3300)')
|
|
48
48
|
const changes = gizmoChanges('rb-1', gizmoReachOf(asset as never, 'rb-1'), { scale: [2, 1, 0.5] }, asset.designInputs)
|
|
@@ -51,9 +51,9 @@ test('편집기의 도형 팔레트로 만든 상자 · 둥근 상자도 크기
|
|
|
51
51
|
|
|
52
52
|
test('다른 부품에 붙인 부품은 옮기지 않는다 — 다음 평가가 제자리로 돌려놓을 것을 움직이지 않는다', () => {
|
|
53
53
|
/* attach takes box-shaped parts only. */
|
|
54
|
-
const lidded =
|
|
54
|
+
const lidded = addFigureBox(probe(), { id: 'lid', size: { x: 100, y: 20, z: 100 }, position: { x: 0, y: 300, z: 0 } })
|
|
55
55
|
assert.equal(gizmoReachOf(lidded as never, 'lid').translate, true, '전제: 붙이기 전에는 옮길 수 있다')
|
|
56
|
-
const fastened =
|
|
56
|
+
const fastened = applyFigureAuthoring(lidded, { kind: 'attach', part: 'lid', face: 'bottom', to: { part: 'body', face: 'top' } })
|
|
57
57
|
assert.equal(gizmoReachOf(fastened as never, 'lid').translate, false)
|
|
58
58
|
})
|
|
59
59
|
|
|
@@ -79,16 +79,16 @@ test('크기는 배율만큼 치수를 바꾸고 1mm 아래로 가지 않는다'
|
|
|
79
79
|
})
|
|
80
80
|
|
|
81
81
|
test('끈 것은 세션의 한 걸음이다 — 한 번의 되돌리기로 돌아간다', () => {
|
|
82
|
-
const workspace =
|
|
82
|
+
const workspace = createFigureEditorWorkspace() as unknown as Record<string, Function>
|
|
83
83
|
assert.ok(typeof workspace.apply === 'function')
|
|
84
|
-
const editor = read('../client/modeller/
|
|
84
|
+
const editor = read('../client/modeller/editor.ts')
|
|
85
85
|
const moved = editor.slice(editor.indexOf(' private transformed('), editor.indexOf(' /** 맞추기'))
|
|
86
86
|
assert.ok(moved.includes('this.run(() => this.session!.apply(changes))'), 'one apply, one step')
|
|
87
87
|
assert.ok(moved.includes("kind: 'nudge-part',"), 'a nudge is one command, one step')
|
|
88
88
|
})
|
|
89
89
|
|
|
90
90
|
test('캔버스 — 기즈모 · W E R · 스냅은 씬의 편집 모드가 한다', () => {
|
|
91
|
-
const canvas = read('../client/modeller/
|
|
91
|
+
const canvas = read('../client/modeller/canvas.ts')
|
|
92
92
|
assert.match(canvas, /mode: 1\n/)
|
|
93
93
|
assert.doesNotMatch(canvas, /TransformControls|OrbitControls/)
|
|
94
94
|
})
|
|
@@ -102,16 +102,16 @@ test('기즈모의 말은 다섯 말 모두에 있다', () => {
|
|
|
102
102
|
|
|
103
103
|
test('떼어 낸 부품은 다시 옮길 수 있다 — 움직임이 있는 부품은 아니다', () => {
|
|
104
104
|
let asset = probe()
|
|
105
|
-
asset =
|
|
106
|
-
const fastened =
|
|
107
|
-
const free =
|
|
105
|
+
asset = addFigureBox(asset, { id: 'lid', size: { x: 100, y: 20, z: 100 }, position: { x: 0, y: 300, z: 0 } })
|
|
106
|
+
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'lid', face: 'bottom', to: { part: 'body', face: 'top' } })
|
|
107
|
+
const free = applyFigureAuthoring(fastened, { kind: 'detach', part: 'lid' })
|
|
108
108
|
assert.equal(gizmoReachOf(free as never, 'lid').translate, true)
|
|
109
109
|
const slid = moving(free, 'lid', { kind: 'slide', axis: 'x', id: 'lid-slide', min: 0, max: 100, start: 0 })
|
|
110
|
-
assert.equal(gizmoReachOf(slid as never, 'lid').translate, false, 'not its own numbers: a drag on a joint edits the joint (
|
|
110
|
+
assert.equal(gizmoReachOf(slid as never, 'lid').translate, false, 'not its own numbers: a drag on a joint edits the joint (joint-drag)')
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
test('끄는 동안은 모아 두고 놓을 때 한 번 적는다 — 크기 기즈모가 편집기와 싸우지 않는다', () => {
|
|
114
|
-
const canvas = read('../client/modeller/
|
|
114
|
+
const canvas = read('../client/modeller/canvas.ts')
|
|
115
115
|
const watch = canvas.slice(canvas.indexOf(' private watchScene()'), canvas.indexOf(' /** Components the gizmo changed'))
|
|
116
116
|
assert.ok(watch.includes('this.pending.set(name, origin!)') && watch.includes('if (!this.pointerDown) this.commitSoon(80)'), '사용자 2026-09-24: 「기즈모 컨트롤 중에 scale은 제대로 작동하지 않는군」')
|
|
117
117
|
assert.ok(!watch.includes("new CustomEvent('part-transformed'"), '움직일 때마다 알리지 않는다')
|
|
@@ -120,12 +120,12 @@ test('끄는 동안은 모아 두고 놓을 때 한 번 적는다 — 크기 기
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
test('원기둥 · 구도 크기 기즈모가 먹는다 — 단면은 원으로', async () => {
|
|
123
|
-
const {
|
|
124
|
-
const { paletteRecipe } = await import('../client/modeller/
|
|
125
|
-
const { gizmoChanges, gizmoReachOf } = await import('../client/modeller/
|
|
126
|
-
let asset =
|
|
127
|
-
asset =
|
|
128
|
-
asset =
|
|
123
|
+
const { addFigurePrimitive, createFigureAsset } = await import('@hatiolab/figure-model')
|
|
124
|
+
const { paletteRecipe } = await import('../client/modeller/palette.ts')
|
|
125
|
+
const { gizmoChanges, gizmoReachOf } = await import('../client/modeller/gizmo.ts')
|
|
126
|
+
let asset = createFigureAsset('Round') as any
|
|
127
|
+
asset = addFigurePrimitive(asset, paletteRecipe('cylinder', 'drum') as any)
|
|
128
|
+
asset = addFigurePrimitive(asset, paletteRecipe('sphere', 'ball') as any)
|
|
129
129
|
const values = { ...asset.designInputs }
|
|
130
130
|
const drum = gizmoReachOf(asset, 'drum')
|
|
131
131
|
assert.equal(drum.scale, true, '원기둥도 크기를 받는다')
|
|
@@ -144,13 +144,13 @@ test('원기둥 · 구도 크기 기즈모가 먹는다 — 단면은 원으로'
|
|
|
144
144
|
* 묻혔다(09-26). 돌려 세운 부품은 그 높이가 위아래가 아니라 건드리지 않는다.
|
|
145
145
|
*/
|
|
146
146
|
test('높이 칸 — 아랫면을 지킨다, 손잡이처럼', async () => {
|
|
147
|
-
const { keepBottomChanges } = await import('../client/modeller/
|
|
148
|
-
let asset =
|
|
149
|
-
asset =
|
|
150
|
-
asset =
|
|
151
|
-
asset =
|
|
147
|
+
const { keepBottomChanges } = await import('../client/modeller/gizmo.ts')
|
|
148
|
+
let asset = createFigureAsset('Post')
|
|
149
|
+
asset = addFigureBox(asset, { id: 'post', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
150
|
+
asset = addFigurePrimitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 50, length: 100 }, position: { x: 0, y: 300, z: 0 } })
|
|
151
|
+
asset = addFigurePrimitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 30, length: 500 }, position: { x: 0, y: 30, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
|
|
152
152
|
const bottom = (a: typeof asset, part: string) => {
|
|
153
|
-
const g =
|
|
153
|
+
const g = compileFigureAsset(a).evaluate().geometry.find(o => o.id === part)! as unknown as { pose: { t: number[] } }
|
|
154
154
|
return g.pose.t[1]!
|
|
155
155
|
}
|
|
156
156
|
const grow = (part: string, input: string, value: number) => {
|
|
@@ -166,12 +166,12 @@ test('높이 칸 — 아랫면을 지킨다, 손잡이처럼', async () => {
|
|
|
166
166
|
const rollerLength = gizmoReachOf(asset, 'roller').round!.along[0]!
|
|
167
167
|
assert.deepEqual(keepBottomChanges('roller', gizmoReachOf(asset, 'roller'), rollerLength, 800, asset.designInputs), [], '누운 롤러의 길이는 위아래가 아니다')
|
|
168
168
|
assert.deepEqual(keepBottomChanges('post', gizmoReachOf(asset, 'post'), gizmoReachOf(asset, 'post').sizes![0], 800, asset.designInputs), [], '폭은 가운데 그대로')
|
|
169
|
-
assert.ok(read('../client/modeller/
|
|
169
|
+
assert.ok(read('../client/modeller/editor.ts').includes('keepBottomChanges('), '숫자 칸이 부른다')
|
|
170
170
|
})
|
|
171
171
|
|
|
172
172
|
/* 손잡이는 키를 몰라도 찾는다 — 도구 막대에 옮기기 · 돌리기 · 크기, 제 숫자로 놓인 부품에만 (09-26). */
|
|
173
173
|
test('도구 막대의 옮기기 · 돌리기 · 크기 — 키와 같은 길, 되쓸 수 있는 것만', () => {
|
|
174
|
-
const editor = read('../client/modeller/
|
|
174
|
+
const editor = read('../client/modeller/editor.ts')
|
|
175
175
|
const bar = editor.slice(editor.indexOf('return html`<ox-canvas-bar'), editor.indexOf('</ox-canvas-bar>'))
|
|
176
176
|
assert.ok(bar.includes("this.renderGizmoTools(!unavailable && this.middle === 'edit')"), '편집 중에만')
|
|
177
177
|
const tools = editor.slice(editor.indexOf(' private renderGizmoTools('), editor.indexOf(" /** The modeller's keys"))
|
|
@@ -5,24 +5,24 @@
|
|
|
5
5
|
import { test } from 'node:test'
|
|
6
6
|
import assert from 'node:assert/strict'
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import type {
|
|
10
|
-
import { jointDragOf } from '../client/modeller/
|
|
8
|
+
import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
|
|
9
|
+
import type { FigureAsset, FigureJoint } from '@hatiolab/figure-model'
|
|
10
|
+
import { jointDragOf } from '../client/modeller/joint-drag.ts'
|
|
11
11
|
|
|
12
12
|
/** A post on the floor, and a link on its top. */
|
|
13
|
-
function column(joint: Partial<
|
|
14
|
-
let asset =
|
|
15
|
-
asset =
|
|
16
|
-
asset =
|
|
17
|
-
asset =
|
|
18
|
-
return
|
|
13
|
+
function column(joint: Partial<FigureJoint>): FigureAsset {
|
|
14
|
+
let asset = createFigureAsset('Column')
|
|
15
|
+
asset = addFigureBox(asset, { id: 'post', size: { x: 100, y: 200, z: 100 }, position: { x: 0, y: 100, z: 0 } })
|
|
16
|
+
asset = addFigureBox(asset, { id: 'link', size: { x: 60, y: 300, z: 60 }, position: { x: 0, y: 350, z: 0 } })
|
|
17
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'post', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
18
|
+
return applyFigureAuthoring(asset, {
|
|
19
19
|
kind: 'set-joint',
|
|
20
|
-
joint: { id: 'bend', type: 'fixed', body0: { part: 'post', face: 'top' }, body1: { part: 'link', face: 'bottom' }, ...joint } as
|
|
20
|
+
joint: { id: 'bend', type: 'fixed', body0: { part: 'post', face: 'top' }, body1: { part: 'link', face: 'bottom' }, ...joint } as FigureJoint
|
|
21
21
|
})
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const drawn = (asset:
|
|
25
|
-
|
|
24
|
+
const drawn = (asset: FigureAsset, id: string, state: Record<string, number> = {}) =>
|
|
25
|
+
compileFigureAsset(asset).evaluate(state).geometry.find(g => g.id === id) as never as { id: string; pose: { t: number[]; r: number[][] } }
|
|
26
26
|
|
|
27
27
|
/** The link turned `deg` about Z, as the gizmo would report it (degrees, XYZ). */
|
|
28
28
|
const turnedZ = (deg: number) => [0, 0, deg] as [number, number, number]
|
|
@@ -58,7 +58,7 @@ test('고정 관절에 달린 부품을 옮기면 관절이 놓인 자리 —
|
|
|
58
58
|
const up = jointDragOf(asset, 'link', part as never, { position: [part.pose.t[0]!, part.pose.t[1]! + 25, part.pose.t[2]!] }, {})
|
|
59
59
|
assert.deepEqual(up, { kind: 'joint', joint: { ...asset.joints!.find(j => j.id === 'bend')!, origin: { gap: 25 } } })
|
|
60
60
|
const aside = jointDragOf(asset, 'link', part as never, { position: [part.pose.t[0]! + 30, part.pose.t[1]!, part.pose.t[2]!] }, {})
|
|
61
|
-
assert.deepEqual((aside as { joint:
|
|
61
|
+
assert.deepEqual((aside as { joint: FigureJoint }).joint.origin, { align: { x: { mm: 30 } } })
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
test('관계를 끊는 끌기는 거절하고 까닭을 댄다', () => {
|
|
@@ -72,16 +72,16 @@ test('관계를 끊는 끌기는 거절하고 까닭을 댄다', () => {
|
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
test('관절이 없는 부품은 이 길로 오지 않는다 — 제 숫자를 쓰는 길', () => {
|
|
75
|
-
let asset =
|
|
76
|
-
asset =
|
|
75
|
+
let asset = createFigureAsset('Free')
|
|
76
|
+
asset = addFigureBox(asset, { id: 'box', size: { x: 10, y: 10, z: 10 }, position: { x: 0, y: 5, z: 0 } })
|
|
77
77
|
assert.equal(jointDragOf(asset, 'box', drawn(asset, 'box') as never, { position: [10, 5, 0] }, {}), null)
|
|
78
78
|
})
|
|
79
79
|
|
|
80
80
|
test('크기 끌기는 관절이 아니라 부품의 치수 — 따라가는 치수는 두고 제 치수만 쓴다 (사용자 09-25: 「R 모드에서 크기가 변하지 않는다」)', async () => {
|
|
81
|
-
const { gizmoChanges, gizmoReachOf } = await import('../client/modeller/
|
|
81
|
+
const { gizmoChanges, gizmoReachOf } = await import('../client/modeller/gizmo.ts')
|
|
82
82
|
let asset = column({ type: 'revolute', axis: 'Z', lowerLimit: -90, upperLimit: 90 })
|
|
83
83
|
/* A square section: the link's depth follows its width. */
|
|
84
|
-
asset =
|
|
84
|
+
asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'link', axis: 'z', source: { of: 'part', part: 'link', axis: 'x' } })
|
|
85
85
|
const part = drawn(asset, 'link')
|
|
86
86
|
const scaled = { position: [part.pose.t[0]!, part.pose.t[1]! + 60, part.pose.t[2]!] as [number, number, number], scale: [1, 1.4, 1] as [number, number, number] }
|
|
87
87
|
assert.equal(jointDragOf(asset, 'link', part as never, scaled, {}), null, '관절 자리로 받지 않는다')
|
|
@@ -8,20 +8,20 @@ import assert from 'node:assert/strict'
|
|
|
8
8
|
import { readFileSync } from 'node:fs'
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import type {
|
|
11
|
+
import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
|
|
12
|
+
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
13
13
|
|
|
14
|
-
import { JOINT_FAN, jointFanOf } from '../client/modeller/
|
|
14
|
+
import { JOINT_FAN, jointFanOf } from '../client/modeller/joint-fan.ts'
|
|
15
15
|
import { moving } from './joints.ts'
|
|
16
16
|
|
|
17
17
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
18
18
|
|
|
19
|
-
function arm():
|
|
20
|
-
let asset =
|
|
21
|
-
asset =
|
|
22
|
-
asset =
|
|
19
|
+
function arm(): FigureAsset {
|
|
20
|
+
let asset = createFigureAsset('Probe')
|
|
21
|
+
asset = addFigureBox(asset, { id: 'base', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
|
|
22
|
+
asset = addFigureBox(asset, { id: 'arm', size: { x: 400, y: 50, z: 50 }, position: { x: 200, y: 125, z: 0 } })
|
|
23
23
|
/* On the base's top, level with its left edge: it turns about that edge, at x = -100. */
|
|
24
|
-
asset =
|
|
24
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'arm', face: 'bottom', to: { part: 'base', face: 'top' }, align: { x: 'min' } })
|
|
25
25
|
return moving(asset, 'arm', { kind: 'turn', axis: 'y', id: 'arm.turn', min: -45, max: 90 })
|
|
26
26
|
}
|
|
27
27
|
const PIVOT = [-100, 100, 0]
|
|
@@ -48,7 +48,7 @@ test('a part that turns gets a fan from the lowest value to the highest, where t
|
|
|
48
48
|
assert.deepEqual(profile[0], { x: 0, y: 0 }, 'not a full turn: the fan starts at the pivot')
|
|
49
49
|
|
|
50
50
|
const armAt = (deg: number) => {
|
|
51
|
-
const body =
|
|
51
|
+
const body = compileFigureAsset({ ...asset, stateDefaults: { ...asset.stateDefaults, 'arm.turn': deg } }).evaluate().geometry.find(g => g.id === 'arm')!
|
|
52
52
|
return direction((body as unknown as { pose: { t: number[] } }).pose.t)
|
|
53
53
|
}
|
|
54
54
|
const first = direction(world(fan, profile[1]!))
|
|
@@ -73,17 +73,17 @@ test('no fan for a part that does not turn, nor for a slide -- a fan in a guesse
|
|
|
73
73
|
const ofArm = jointFanOf(arm(), 'arm') as unknown as { shape: { profile: unknown[] } }
|
|
74
74
|
assert.deepEqual(onBase?.shape.profile, ofArm.shape.profile)
|
|
75
75
|
assert.equal(jointFanOf(arm(), ''), null)
|
|
76
|
-
let slider =
|
|
77
|
-
slider =
|
|
76
|
+
let slider = createFigureAsset('Probe')
|
|
77
|
+
slider = addFigureBox(slider, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
78
78
|
slider = moving(slider, 'block', { kind: 'slide', axis: 'x', id: 'shift', min: 0, max: 60 })
|
|
79
79
|
assert.equal(jointFanOf(slider, 'block'), null)
|
|
80
80
|
})
|
|
81
81
|
|
|
82
82
|
test('the editor draws it for the selected part, with the pose in hand; the canvas never lets it be picked', () => {
|
|
83
|
-
const editor = read('../client/modeller/
|
|
84
|
-
assert.ok(editor.includes('.fan=${this.fanOf(asset as
|
|
83
|
+
const editor = read('../client/modeller/editor.ts')
|
|
84
|
+
assert.ok(editor.includes('.fan=${this.fanOf(asset as FigureAsset | undefined)}'))
|
|
85
85
|
assert.ok(editor.includes('return jointFanOf(asset, this.selected, { ...this.currentInputs(), ...this.live.values })'), 'the fan reads the pose the session holds')
|
|
86
|
-
const canvas = read('../client/modeller/
|
|
86
|
+
const canvas = read('../client/modeller/canvas.ts')
|
|
87
87
|
assert.ok(canvas.includes('{ ...fan, capturable: false, locked: true }'))
|
|
88
88
|
})
|
|
89
89
|
|
|
@@ -94,8 +94,8 @@ test('the editor draws it for the selected part, with the pose in hand; the canv
|
|
|
94
94
|
* the hinge centred on the wrist's axis (as ROBOT_ARM's elbow-housing is the elbow's); `badge` rides the hinge and holds
|
|
95
95
|
* no pivot.
|
|
96
96
|
*/
|
|
97
|
-
const fixture = (name: string):
|
|
98
|
-
function column():
|
|
97
|
+
const fixture = (name: string): FigureAsset => JSON.parse(readFileSync(fileURLToPath(new URL(`./fixtures/${name}.json`, import.meta.url)), 'utf8'))
|
|
98
|
+
function column(): FigureAsset {
|
|
99
99
|
return fixture('joint-column')
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -124,7 +124,7 @@ for (const [part, joint] of [
|
|
|
124
124
|
const pivot = fan.pose.t
|
|
125
125
|
const axis = [0, 1, 2].map(i => fan.pose.r[i]![1]!)
|
|
126
126
|
const at = (s: number) =>
|
|
127
|
-
|
|
127
|
+
compileFigureAsset({ ...asset, stateDefaults: { ...asset.stateDefaults, [joint]: s } }).evaluate().geometry.find(g => g.id === part) as unknown as { pose: { t: number[]; r: number[][] } }
|
|
128
128
|
const onFan = (p: { x: number; y: number }) => [0, 1, 2].map(i => pivot[i]! + fan.pose.r[i]![0]! * p.x + fan.pose.r[i]![2]! * p.y)
|
|
129
129
|
const flat = (v: number[]) => sub(v, axis.map(a => a * dot(v, axis)))
|
|
130
130
|
|
|
@@ -160,7 +160,7 @@ test('a housing shows the joint it houses, not the one that carries it; a part t
|
|
|
160
160
|
const axis = [0, 1, 2].map(i => fan.pose.r[i]![1]!)
|
|
161
161
|
const flat = (v: number[]) => sub(v, axis.map(a => a * dot(v, axis)))
|
|
162
162
|
const onFan = (p: { x: number; y: number }) => [0, 1, 2].map(i => fan.pose.t[i]! + fan.pose.r[i]![0]! * p.x + fan.pose.r[i]![2]! * p.y)
|
|
163
|
-
const tipAt = (s: number) => (
|
|
163
|
+
const tipAt = (s: number) => (compileFigureAsset({ ...asset, stateDefaults: { ...asset.stateDefaults, wrist: s } }).evaluate().geometry.find(g => g.id === 'tip') as unknown as { pose: { t: number[] } }).pose.t
|
|
164
164
|
const edges = fan.shape.profile.filter(p => p.x !== 0 || p.y !== 0)
|
|
165
165
|
assert.ok(angle(flat(sub(onFan(edges[0]!), fan.pose.t)), flat(sub(tipAt(-120), fan.pose.t))) < 0.01)
|
|
166
166
|
assert.ok(angle(flat(sub(onFan(edges[edges.length - 1]!), fan.pose.t)), flat(sub(tipAt(120), fan.pose.t))) < 0.01)
|
package/test/joints.ts
CHANGED
|
@@ -2,24 +2,24 @@
|
|
|
2
2
|
* Moving a part in a test, as the modeller does it now (ADR-0093): the joint above the part is given a turn or a
|
|
3
3
|
* slide. A part fastened to nothing is stood on the floor first -- a joint has a parent.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import type {
|
|
5
|
+
import { applyFigureAuthoring } from '@hatiolab/figure-model'
|
|
6
|
+
import type { FigureAsset, FigureJoint } from '@hatiolab/figure-model'
|
|
7
7
|
|
|
8
8
|
export function moving(
|
|
9
|
-
asset:
|
|
9
|
+
asset: FigureAsset,
|
|
10
10
|
part: string,
|
|
11
11
|
spec: { kind: 'slide' | 'turn'; axis: 'x' | 'y' | 'z'; id: string; min: number; max: number; start?: number }
|
|
12
|
-
):
|
|
12
|
+
): FigureAsset {
|
|
13
13
|
/* Stood on the floor where it is: its X and Z kept as offsets on the plane. */
|
|
14
14
|
const at = (axis: 'x' | 'z') => ({ mm: asset.designInputs[`${part}.position.${axis}`] ?? 0 })
|
|
15
|
-
const above:
|
|
15
|
+
const above: FigureJoint = asset.joints?.find(j => j.body1.part === part) ?? {
|
|
16
16
|
id: part,
|
|
17
17
|
type: 'fixed',
|
|
18
18
|
body0: { plane: 'mounting-plane' },
|
|
19
19
|
body1: { part, face: 'bottom' },
|
|
20
20
|
origin: { align: { x: at('x'), z: at('z') } }
|
|
21
21
|
}
|
|
22
|
-
return
|
|
22
|
+
return applyFigureAuthoring(asset, {
|
|
23
23
|
kind: 'set-joint',
|
|
24
24
|
joint: {
|
|
25
25
|
...above,
|
|
@@ -6,7 +6,7 @@ import assert from 'node:assert/strict'
|
|
|
6
6
|
import { readFileSync } from 'node:fs'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
|
|
9
|
-
import { keyActionOf } from '../client/modeller/
|
|
9
|
+
import { keyActionOf } from '../client/modeller/keys.ts'
|
|
10
10
|
|
|
11
11
|
test('되돌리기 · 다시 하기 — Ctrl 과 Cmd 모두, 부품을 고르지 않아도', () => {
|
|
12
12
|
assert.equal(keyActionOf({ key: 'z', ctrlKey: true }, ''), 'undo')
|
|
@@ -31,7 +31,7 @@ test('칸에 치는 키는 칸의 것 — 이름 칸의 Backspace 는 글자를
|
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
test('편집기가 창의 키를 듣고, 떼어지면 놓는다', () => {
|
|
34
|
-
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/
|
|
34
|
+
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/editor.ts', import.meta.url)), 'utf8')
|
|
35
35
|
assert.ok(editor.includes("window.addEventListener('keydown', this.onKey, true)") && editor.includes("window.removeEventListener('keydown', this.onKey, true)"))
|
|
36
36
|
})
|
|
37
37
|
|