@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
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# V3 제품 현황
|
|
2
|
+
|
|
3
|
+
형식은 V3(`figure-asset-1`) 하나다. V1·V2 형식은 2026-09-26 코드와 명세에서 제거했다.
|
|
4
|
+
|
|
5
|
+
## 제품 코드
|
|
6
|
+
|
|
7
|
+
- `client/modeller/product-session.ts`: 모델 ID·로드·저장 경계를 workspace에 연결한다. 다른 모델의 지연 응답을 버리며 로드 실패 시 이전 형상을 노출하지 않는다. UI/AI는 같은 리비전과 undo/redo를 사용한다. AI 편집은 요청 당시 context/revision을 필수로 받는다.
|
|
8
|
+
- `client/modeller/canvas.ts`: things-scene 편집 모드의 판 위에 평가된 V3 형상을 부품마다 `figure-part` 컴포넌트로 세운다. 새 문맥에서 렌더링이 실패하면 이전 모델을 지운다.
|
|
9
|
+
- `client/modeller/editor.ts`: 부품 목록 · 캔버스 · 오른쪽 탭(속성 · 능력 · 움직임 · 판정 · 환경 · 이력). 실행 취소/다시 실행과 저장 연결.
|
|
10
|
+
- `client/graphql/store.ts`: 정본을 그대로 `saveFigure`로 보낸다.
|
|
11
|
+
- figure-service의 `saveFigure`: 도메인 권한 검사, V3 검증, 정확한 이전 정본을 조건으로 하는 atomic UPDATE, 생성 요청 ID를 이용한 재시도. SQLite에서 지원하지 않는 행 잠금은 사용하지 않는다. 재지 않은 성능 점수/삼각형 수는 null로 둔다. 실제 운영 DB 저장 테스트는 아직 하지 않았다.
|
|
12
|
+
- `client/pages/figure-modeller-page.ts`: 위 요소를 묶는 페이지. `route.ts`의 `figure-modeller`와 `figure-modeller`가 이 페이지를 연다.
|
|
13
|
+
|
|
14
|
+
## 남은 항목
|
|
15
|
+
|
|
16
|
+
1. operato-figure의 AI-dock과 figure-service 도구/제안 생성기를 V3 정본 및 승인된 트랜잭션 계약으로 연결한다. 페이지 이벤트만 추가한 것은 AI 통합 완료가 아니다.
|
|
17
|
+
2. 발행 게이트, 썸네일, 카탈로그/배치 소비처가 V3 평가 결과를 사용하는지 확인한다.
|
|
18
|
+
|
|
19
|
+
## 검증
|
|
20
|
+
|
|
21
|
+
- figure-ui `npm test`: 251개 통과(2026-09-26).
|
|
22
|
+
- SQLite in-memory 테스트: 테넌트 경계·이전 정본 비교·오래된 편집 거절. 실제 TypeORM 리졸버/제품 DB end-to-end 검증을 대체하지 않는다.
|
|
23
|
+
- 브라우저 검사(`npm run test:browser`): 기즈모 끌기 · 자세 슬라이더를 실제 마우스로 조작한다.
|
|
24
|
+
|
|
25
|
+
배포는 사용자가 담당한다.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-ui",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.76",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"clean": "npm run clean:server && npm run clean:client"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hatiolab/figure-model": "^0.1.
|
|
31
|
-
"@hatiolab/things-scene": "^10.1.
|
|
30
|
+
"@hatiolab/figure-model": "^0.1.93",
|
|
31
|
+
"@hatiolab/things-scene": "^10.1.61",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.0.0",
|
|
34
34
|
"@operato/graphql": "^10.0.0",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"@operato/styles": "^10.0.0",
|
|
42
42
|
"@operato/utils": "^10.0.0",
|
|
43
43
|
"@things-factory/ai-assistant": "^10.1.58",
|
|
44
|
-
"@things-factory/figure-service": "^10.1.
|
|
44
|
+
"@things-factory/figure-service": "^10.1.76",
|
|
45
45
|
"three": "^0.185.1"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "66a70458dfea95fe8c835067f0fd857b38736203"
|
|
48
48
|
}
|
|
@@ -6,10 +6,10 @@ import { test } from 'node:test'
|
|
|
6
6
|
import assert from 'node:assert/strict'
|
|
7
7
|
import { readFileSync } from 'node:fs'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
|
-
import {
|
|
9
|
+
import { createFigureAsset, addFigureBox, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import { pendNewFigure, runFigureProposal, takePendingNewFigure, type FigureApplyDetail } from '../client/modeller/
|
|
11
|
+
import { createFigureProductSession } from '../client/modeller/product-session.js'
|
|
12
|
+
import { pendNewFigure, runFigureProposal, takePendingNewFigure, type FigureApplyDetail } from '../client/modeller/ai-proposal.js'
|
|
13
13
|
|
|
14
14
|
const ko = JSON.parse(readFileSync(fileURLToPath(new URL('../translations/ko.json', import.meta.url)), 'utf8'))
|
|
15
15
|
const say = (key: string, params: Record<string, string> = {}) => {
|
|
@@ -18,7 +18,7 @@ const say = (key: string, params: Record<string, string> = {}) => {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const source = () =>
|
|
21
|
-
|
|
21
|
+
serializeFigureAsset(addFigureBox(createFigureAsset('A'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } }))
|
|
22
22
|
const store = {
|
|
23
23
|
load: async (id: string) => ({ id, source: source() }),
|
|
24
24
|
save: async (q: { id?: string; source: string }) => ({ id: q.id ?? 'created', source: q.source })
|
|
@@ -30,7 +30,7 @@ const steps = [
|
|
|
30
30
|
] as any[]
|
|
31
31
|
|
|
32
32
|
test('a proposal is one step of the history on the version the model saw, undone whole', async () => {
|
|
33
|
-
const s =
|
|
33
|
+
const s = createFigureProductSession(store)
|
|
34
34
|
await s.open('A')
|
|
35
35
|
const seen = s.read()
|
|
36
36
|
s.applyProposal(steps, seen.revision!, seen.context!)
|
|
@@ -43,7 +43,7 @@ test('a proposal is one step of the history on the version the model saw, undone
|
|
|
43
43
|
})
|
|
44
44
|
|
|
45
45
|
test('a person\'s edit after the proposal was asked makes it stale -- refused, nothing applied', async () => {
|
|
46
|
-
const s =
|
|
46
|
+
const s = createFigureProductSession(store)
|
|
47
47
|
await s.open('A')
|
|
48
48
|
const seen = s.read()
|
|
49
49
|
s.apply([{ kind: 'set-design', input: 'body.size.y', unit: 'mm', value: 150 }])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* **부품 맞추기 — 가운데 · 바닥 · 대칭 · 눈금** (2026-09-24).
|
|
3
3
|
*
|
|
4
|
-
* 「맞추기」 여섯 단추.
|
|
4
|
+
* 「맞추기」 여섯 단추. 이 모델의 틀은 바닥이 y 0, X · Z 가 0 에 가운데다. 각 도구가 부품의 **실제로 차지하는
|
|
5
5
|
* 상자**(돌린 부품이면 돈 부피)를 기준으로 맞추는지, 그 결과를 잰다.
|
|
6
6
|
*/
|
|
7
7
|
import { test } from 'node:test'
|
|
@@ -9,15 +9,15 @@ 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, createFigureAsset, editFigurePart } from '@hatiolab/figure-model'
|
|
13
13
|
|
|
14
|
-
import { alignChanges, boxOf, canMirror, gridStepOf, mirrorChanges, niceStep } from '../client/modeller/
|
|
15
|
-
import { gizmoReachOf } from '../client/modeller/
|
|
14
|
+
import { alignChanges, boxOf, canMirror, gridStepOf, mirrorChanges, niceStep } from '../client/modeller/align.ts'
|
|
15
|
+
import { gizmoReachOf } from '../client/modeller/gizmo.ts'
|
|
16
16
|
|
|
17
17
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
18
18
|
const probe = (at = { x: 130, y: 90, z: -70 }, rotation?: { x: number; y: number; z: number }) => {
|
|
19
|
-
let asset =
|
|
20
|
-
asset =
|
|
19
|
+
let asset = createFigureAsset('Probe')
|
|
20
|
+
asset = addFigureBox(asset, { id: 'body', size: { x: 200, y: 100, z: 100 }, position: at, ...(rotation ? { rotation } : {}) })
|
|
21
21
|
return asset
|
|
22
22
|
}
|
|
23
23
|
const applied = (asset: ReturnType<typeof probe>, changes: ReturnType<typeof alignChanges>) => {
|
|
@@ -52,7 +52,7 @@ test('눈금은 1 · 2 · 5 간격으로 자리와 크기를 맞추고, 크기
|
|
|
52
52
|
|
|
53
53
|
test('대칭은 복제본을 가운데 면 반대편 같은 거리에 둔다 · 가운데에 있으면 내지 않는다', () => {
|
|
54
54
|
const asset = probe()
|
|
55
|
-
const copied =
|
|
55
|
+
const copied = editFigurePart(asset, { kind: 'duplicate-part', target: 'body' })
|
|
56
56
|
const mirrored = applied(copied, mirrorChanges(copied, 'body', 'body-copy', 'x'))
|
|
57
57
|
assert.equal(Math.round(boxOf(mirrored, 'body-copy')!.centre.x), -Math.round(boxOf(mirrored, 'body')!.centre.x))
|
|
58
58
|
assert.equal(canMirror(probe({ x: 0, y: 50, z: 0 }) as never, 'body', 'x', gizmoReachOf(probe() as never, 'body')), false)
|
|
@@ -60,7 +60,7 @@ test('대칭은 복제본을 가운데 면 반대편 같은 거리에 둔다 ·
|
|
|
60
60
|
|
|
61
61
|
test('붙인 부품처럼 옮길 수 없는 것은 맞추기도 없다', () => {
|
|
62
62
|
assert.deepEqual(alignChanges(probe(), 'body', { translate: false, rotate: false, scale: false }, 'floor'), [])
|
|
63
|
-
assert.match(read('../client/modeller/
|
|
63
|
+
assert.match(read('../client/modeller/editor.ts'), /if \(!this\.selected \|\| !reach\.translate\) return null/)
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
test('the grid step reads well -- 27 or 33 would make coordinates unreadable -- and is never 0', () => {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The page the browser check drives: the
|
|
2
|
+
* The page the browser check drives: the editor as the modeller page mounts it, over a store in memory, so the check
|
|
3
3
|
* needs no server and no sign-in. Everything the editor draws and does is the product's own code, emitted fresh from
|
|
4
4
|
* `client/` by the runner.
|
|
5
5
|
*/
|
|
6
|
-
import 'figure-ui-client/modeller/
|
|
7
|
-
import { budgetOf,
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import 'figure-ui-client/modeller/editor.js'
|
|
7
|
+
import { budgetOf, figureReportOf } from 'figure-ui-client/modeller/maturity.js'
|
|
8
|
+
import { createFigureProductSession } from 'figure-ui-client/modeller/product-session.js'
|
|
9
|
+
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset, parseFigureAsset, robotArmFigure, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
10
10
|
|
|
11
11
|
const PALETTE = {
|
|
12
12
|
'palette.dark': '#2b2b2b',
|
|
@@ -18,38 +18,38 @@ const PALETTE = {
|
|
|
18
18
|
|
|
19
19
|
function sourceOf(kind, text) {
|
|
20
20
|
if (text) return text
|
|
21
|
-
if (kind === 'arm') return
|
|
21
|
+
if (kind === 'arm') return serializeFigureAsset(robotArmFigure())
|
|
22
22
|
if (kind === 'box') {
|
|
23
|
-
const asset =
|
|
23
|
+
const asset = createFigureAsset('Probe')
|
|
24
24
|
asset.palette = { ...asset.palette, ...PALETTE }
|
|
25
|
-
return
|
|
25
|
+
return serializeFigureAsset(addFigureBox(asset, { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 200, z: 0 } }))
|
|
26
26
|
}
|
|
27
27
|
if (kind === 'door') {
|
|
28
28
|
/* A frame, and a leaf set off it: 250 mm to the side and 100 mm in front, by its own numbers. */
|
|
29
|
-
let asset =
|
|
29
|
+
let asset = createFigureAsset('Door')
|
|
30
30
|
asset.palette = { ...asset.palette, ...PALETTE }
|
|
31
|
-
asset =
|
|
32
|
-
return
|
|
31
|
+
asset = addFigureBox(asset, { id: 'frame', size: { x: 1600, y: 100, z: 150 }, position: { x: 0, y: 50, z: 0 } })
|
|
32
|
+
return serializeFigureAsset(addFigureBox(asset, { id: 'leaf', size: { x: 1400, y: 2200, z: 40 }, position: { x: 250, y: 1100, z: 100 } }))
|
|
33
33
|
}
|
|
34
34
|
if (kind === 'room') {
|
|
35
35
|
/* A room a recipe is put on: a 4 x 3 x 6 m box standing on the floor. */
|
|
36
|
-
const asset =
|
|
36
|
+
const asset = createFigureAsset('Room')
|
|
37
37
|
asset.palette = { ...asset.palette, ...PALETTE }
|
|
38
|
-
return
|
|
38
|
+
return serializeFigureAsset(addFigureBox(asset, { id: 'room', size: { x: 4000, y: 3000, z: 6000 }, position: { x: 0, y: 1500, z: 0 } }))
|
|
39
39
|
}
|
|
40
40
|
if (kind === 'rollers') {
|
|
41
41
|
/* A roller row whose rollers spin in place, each carrying a mark (ADR-0093, 2026-09-26). */
|
|
42
|
-
let asset =
|
|
42
|
+
let asset = createFigureAsset('Rollers')
|
|
43
43
|
asset.palette = { ...asset.palette, ...PALETTE }
|
|
44
|
-
asset =
|
|
45
|
-
asset =
|
|
46
|
-
asset =
|
|
47
|
-
asset =
|
|
48
|
-
asset =
|
|
49
|
-
asset =
|
|
44
|
+
asset = addFigureBox(asset, { id: 'bed', size: { x: 2000, y: 400, z: 600 }, position: { x: 0, y: 200, z: 0 } })
|
|
45
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bed', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
46
|
+
asset = addFigurePrimitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 40, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
|
|
47
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', face: 'centre', to: { part: 'bed', face: 'top' }, gap: 40 })
|
|
48
|
+
asset = addFigureBox(asset, { id: 'mark', size: { x: 30, y: 30, z: 560 }, position: { x: 0, y: 0, z: 0 } })
|
|
49
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'mark', face: 'left', to: { part: 'roller', face: 'right' } })
|
|
50
50
|
const j = asset.joints.find(o => o.body1.part === 'roller')
|
|
51
|
-
asset =
|
|
52
|
-
return
|
|
51
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Z', lowerLimit: -360, upperLimit: 360, state: { id: 'spin' } } })
|
|
52
|
+
return serializeFigureAsset(applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 }))
|
|
53
53
|
}
|
|
54
54
|
throw Error(`no figure for ${kind}`)
|
|
55
55
|
}
|
|
@@ -60,8 +60,8 @@ function deep(selector, root = document) {
|
|
|
60
60
|
return found
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const editor = () => document.querySelector('figure-
|
|
64
|
-
const canvas = () => deep('figure-
|
|
63
|
+
const editor = () => document.querySelector('figure-editor')
|
|
64
|
+
const canvas = () => deep('figure-canvas')[0]
|
|
65
65
|
const three = () => canvas().scene.root._threeCapability
|
|
66
66
|
const controls = () => three().gizmoManager.controls
|
|
67
67
|
|
|
@@ -244,28 +244,28 @@ window.probe = {
|
|
|
244
244
|
window.harness = {
|
|
245
245
|
async mount(kind, text, templates = [], guides = {}) {
|
|
246
246
|
const source = sourceOf(kind, text)
|
|
247
|
-
const session =
|
|
247
|
+
const session = createFigureProductSession({
|
|
248
248
|
load: async id => ({ id, source }),
|
|
249
249
|
save: async request => ({ id: request.id ?? 'probe', source: request.source })
|
|
250
250
|
})
|
|
251
251
|
await session.open('probe')
|
|
252
|
-
const editor = document.createElement('figure-
|
|
252
|
+
const editor = document.createElement('figure-editor')
|
|
253
253
|
editor.session = session
|
|
254
|
-
/* The cost line as the modeller page gives it: the maturity report of what is authored (figure-
|
|
254
|
+
/* The cost line as the modeller page gives it: the maturity report of what is authored (figure-verdict). */
|
|
255
255
|
const report = () => {
|
|
256
256
|
try {
|
|
257
|
-
return budgetOf(
|
|
257
|
+
return budgetOf(figureReportOf(parseFigureAsset(session.source())))
|
|
258
258
|
} catch {
|
|
259
259
|
return undefined
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
editor.budget = report()
|
|
263
|
-
editor.addEventListener('
|
|
263
|
+
editor.addEventListener('changed', () => (editor.budget = report()))
|
|
264
264
|
editor.templates = templates
|
|
265
265
|
editor.guideOf = async name => guides[name] ?? ''
|
|
266
266
|
editor.revision = 0
|
|
267
267
|
/* The modeller page counts a change the same way: the editor re-reads the session on a new revision. */
|
|
268
|
-
editor.addEventListener('
|
|
268
|
+
editor.addEventListener('changed', () => (editor.revision = editor.revision + 1))
|
|
269
269
|
document.body.replaceChildren(editor)
|
|
270
270
|
await editor.updateComplete
|
|
271
271
|
return session.read().revision
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The
|
|
2
|
+
* The modeller driven with the mouse, in a real browser.
|
|
3
3
|
*
|
|
4
4
|
* The unit suite proves the rules; it cannot prove what happens between a pointer going down on a gizmo handle and
|
|
5
5
|
* coming up -- the scene writes the component on every move, the editor must not fight it, and the drag must land as
|
|
@@ -252,7 +252,7 @@ describe('a pose slider dragged with the mouse (user 2026-09-24: the arm jumped
|
|
|
252
252
|
})
|
|
253
253
|
})
|
|
254
254
|
|
|
255
|
-
describe('the
|
|
255
|
+
describe('the robot arm: a drag on a part edits its joint (ADR-0093)', () => {
|
|
256
256
|
before(async () => {
|
|
257
257
|
await mount('arm')
|
|
258
258
|
await click(await read(() => probe.row('upper-arm')))
|
|
@@ -348,7 +348,7 @@ describe('the fastening form belongs to the part it is for', () => {
|
|
|
348
348
|
await click(await read(() => probe.row('elbow')))
|
|
349
349
|
/* The elbow opens on its own fastening (walk 1, 2026-09-26), not on the forearm's choices. */
|
|
350
350
|
const own = await read(() => {
|
|
351
|
-
const joint = JSON.parse(document.querySelector('figure-
|
|
351
|
+
const joint = JSON.parse(document.querySelector('figure-editor').session.source()).joints.find(j => j.body1.part === 'elbow')
|
|
352
352
|
return { target: joint.body0.part, x: joint.origin?.align?.x ?? 'centre' }
|
|
353
353
|
})
|
|
354
354
|
const nextTarget = await alignSelect('figure.label.target-part')
|
|
@@ -390,7 +390,7 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
390
390
|
}, id)
|
|
391
391
|
const declared = () =>
|
|
392
392
|
page().evaluate(() => {
|
|
393
|
-
const deep = (root = document) => [...root.querySelectorAll('figure-
|
|
393
|
+
const deep = (root = document) => [...root.querySelectorAll('figure-editor')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
394
394
|
const inputs = JSON.parse(deep()[0].session.source()).document.model.inputs
|
|
395
395
|
return Object.fromEntries(inputs.filter(i => i.id.startsWith('size.')).map(i => [i.id, [i.min, i.max]]))
|
|
396
396
|
})
|
|
@@ -479,7 +479,7 @@ describe('a figure made in the modeller is given its size from its parts (user 2
|
|
|
479
479
|
assert.equal(after.error, '', 'not refused')
|
|
480
480
|
assert.equal(after.revision, start.revision + 1, 'one step')
|
|
481
481
|
const follows = await page().evaluate(() => {
|
|
482
|
-
const deep = (root = document) => [...root.querySelectorAll('figure-
|
|
482
|
+
const deep = (root = document) => [...root.querySelectorAll('figure-editor')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
483
483
|
const model = JSON.parse(deep()[0].session.source()).document.model
|
|
484
484
|
return ['x', 'y', 'z'].map(axis => {
|
|
485
485
|
const ref = String(model.nodes.find(n => n.id === `box.dimension.${axis}`)?.args[0] ?? '')
|
|
@@ -499,7 +499,7 @@ describe('a motion is changed where it is (2026-09-25)', () => {
|
|
|
499
499
|
|
|
500
500
|
test('the shoulder\'s range is changed from the motion tab: one step, the same joint, its new limit', async () => {
|
|
501
501
|
await click(await read(() => probe.tab('figure.label.motion')))
|
|
502
|
-
const jointOf = part => read(part => (JSON.parse(document.querySelector('figure-
|
|
502
|
+
const jointOf = part => read(part => (JSON.parse(document.querySelector('figure-editor').session.source()).joints ?? []).find(j => j.body1.part === part) ?? null, part)
|
|
503
503
|
const was = await jointOf('upper-arm')
|
|
504
504
|
assert.ok(was, 'the upper arm has its joint')
|
|
505
505
|
const button = async text =>
|
|
@@ -514,7 +514,7 @@ describe('a motion is changed where it is (2026-09-25)', () => {
|
|
|
514
514
|
await click(await button('figure.button.change-motion'))
|
|
515
515
|
const max = await page().evaluateHandle(() => {
|
|
516
516
|
const deep = (root = document) => [...root.querySelectorAll('input[type=number]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
517
|
-
const inputs = deep().filter(i => i.closest('div[pair]') && i.getRootNode().host?.tagName === 'FIGURE-
|
|
517
|
+
const inputs = deep().filter(i => i.closest('div[pair]') && i.getRootNode().host?.tagName === 'FIGURE-LINKS')
|
|
518
518
|
return inputs[inputs.length - 1] ?? null
|
|
519
519
|
})
|
|
520
520
|
assert.ok(await max.evaluate(i => !!i), 'the form is open with the range')
|
|
@@ -620,9 +620,9 @@ describe('the modeller made easier to find and to keep (2026-09-26 baseline)', (
|
|
|
620
620
|
await select.select(control)
|
|
621
621
|
await sleep(500)
|
|
622
622
|
assert.equal((await state()).revision, start.revision + 1, 'one step')
|
|
623
|
-
const drawn = () => read(() => document.querySelector('figure-
|
|
623
|
+
const drawn = () => read(() => document.querySelector('figure-editor').session.read().geometry.find(g => g.id === 'upper-arm')?.visible)
|
|
624
624
|
const { value, atLeast } = await read(control => {
|
|
625
|
-
const asset = JSON.parse(document.querySelector('figure-
|
|
625
|
+
const asset = JSON.parse(document.querySelector('figure-editor').session.source())
|
|
626
626
|
const node = asset.document.model.nodes.find(n => n.op === 'at-least@1' && n.args[0] === control)
|
|
627
627
|
return { value: asset.stateDefaults[control], atLeast: asset.document.model.constants.find(c => c.id === node.args[1]).value }
|
|
628
628
|
}, control)
|
|
@@ -670,7 +670,7 @@ describe('a part follows the figure size in one step (follow-size, 2026-09-26)',
|
|
|
670
670
|
})
|
|
671
671
|
assert.ok(notice?.includes('figure.text.size-declared-to-follow'), `a line says so: ${notice}`)
|
|
672
672
|
const grown = await read(() => {
|
|
673
|
-
const editor = document.querySelector('figure-
|
|
673
|
+
const editor = document.querySelector('figure-editor')
|
|
674
674
|
editor.session.apply([{ kind: 'set-design', input: 'size.x', unit: 'mm', value: 800 }])
|
|
675
675
|
return editor.session.read().geometry.find(g => g.id === 'box').shape.dimensions
|
|
676
676
|
})
|
|
@@ -694,7 +694,7 @@ describe('the recipe catalog: a template chosen, filled in, put onto the chosen
|
|
|
694
694
|
const r = e.getBoundingClientRect()
|
|
695
695
|
return { x: r.left + r.width / 2, y: r.top + r.height / 2, disabled: !!e.disabled, text: e.textContent.trim() }
|
|
696
696
|
}, selector)
|
|
697
|
-
const parts = () => read(() => document.querySelector('figure-
|
|
697
|
+
const parts = () => read(() => document.querySelector('figure-editor').session.read().geometry.map(g => g.id))
|
|
698
698
|
const type = async (selector, text) => {
|
|
699
699
|
const field = await page().evaluateHandle(selector => {
|
|
700
700
|
const deep = (root = document) => [...root.querySelectorAll(selector)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
@@ -712,7 +712,7 @@ describe('the recipe catalog: a template chosen, filled in, put onto the chosen
|
|
|
712
712
|
await sleep(2500)
|
|
713
713
|
await click(await read(() => probe.row('room')))
|
|
714
714
|
await click(await at('button[recipe-catalog]'))
|
|
715
|
-
assert.ok(await at('figure-
|
|
715
|
+
assert.ok(await at('figure-recipe-catalog'), 'the catalog is open')
|
|
716
716
|
await click(await at('button[card="hinged-door"]'))
|
|
717
717
|
/* A search that takes the chosen card away tells the first card it leaves (:3300, 2026-09-26). */
|
|
718
718
|
await type('input[search]', 'sliding')
|
|
@@ -734,13 +734,13 @@ describe('the recipe catalog: a template chosen, filled in, put onto the chosen
|
|
|
734
734
|
await type('input[param="share"]', '50')
|
|
735
735
|
const start = await state()
|
|
736
736
|
await click(await at('button[insert]'))
|
|
737
|
-
assert.equal(await read(() => document.querySelector('figure-
|
|
737
|
+
assert.equal(await read(() => document.querySelector('figure-editor').recipeError), '', 'not refused')
|
|
738
738
|
assert.equal((await state()).revision, start.revision + 1, 'one step')
|
|
739
739
|
const ids = await parts()
|
|
740
740
|
for (const part of ['frame', 'left', 'right', 'lamp']) assert.ok(ids.includes(`sliding-door-1-${part}`), `${part} is in`)
|
|
741
741
|
const frame = await body('sliding-door-1-frame')
|
|
742
742
|
assert.equal(Math.round(frame.dimensions.right - frame.dimensions.left), 2000, "half the room's 4000 mm")
|
|
743
|
-
assert.equal(await at('figure-
|
|
743
|
+
assert.equal(await at('figure-recipe-catalog'), null, 'the catalog closes')
|
|
744
744
|
})
|
|
745
745
|
|
|
746
746
|
test('a door the room cannot take is refused in the catalog, with the step it stopped at, and nothing is added', async () => {
|
package/test/browser/serve.mjs
CHANGED
|
@@ -10,7 +10,7 @@ const HTML = `<!DOCTYPE html>
|
|
|
10
10
|
<html><head><meta charset="utf-8"><style>
|
|
11
11
|
html, body { margin: 0; height: 100%; }
|
|
12
12
|
body { display: flex; font-family: sans-serif; }
|
|
13
|
-
figure-
|
|
13
|
+
figure-editor { flex: 1; min-width: 0; min-height: 0; }
|
|
14
14
|
</style></head>
|
|
15
15
|
<body><script type="module" src="/page.js"></script></body></html>`
|
|
16
16
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* The page that draws recipe template thumbnails (ADR-0095 amended): a model -- the host and the template's parts -- drawn
|
|
3
|
-
* the way the modeller draws a figure's card, by the same code (`
|
|
3
|
+
* the way the modeller draws a figure's card, by the same code (`captureFigureThumbnail`).
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { captureFigureThumbnail } from 'figure-ui-client/modeller/thumbnail.js'
|
|
6
|
+
import { drawableFigureParts } from 'figure-ui-client/modeller/renderer.js'
|
|
7
|
+
import { compileFigureAsset } from '@hatiolab/figure-model'
|
|
8
8
|
|
|
9
9
|
window.harness = { ready: true }
|
|
10
10
|
window.shoot = async (text, state = {}) => {
|
|
11
11
|
const asset = JSON.parse(text)
|
|
12
|
-
const evaluation =
|
|
13
|
-
return await
|
|
12
|
+
const evaluation = compileFigureAsset(asset).evaluate({ ...asset.stateDefaults, ...state })
|
|
13
|
+
return await captureFigureThumbnail(drawableFigureParts(evaluation.geometry), evaluation.palette ?? asset.palette ?? {})
|
|
14
14
|
}
|
|
@@ -8,25 +8,25 @@ 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, createFigureAsset, editFigurePart } from '@hatiolab/figure-model'
|
|
12
|
+
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
13
13
|
|
|
14
|
-
import { clipsOf, poseAt, trackEdit, undrivenOf, withKey, withoutKey } from '../client/modeller/
|
|
14
|
+
import { clipsOf, poseAt, trackEdit, undrivenOf, withKey, withoutKey } from '../client/modeller/clips.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
|
-
const machine = ():
|
|
20
|
-
let asset =
|
|
21
|
-
asset =
|
|
19
|
+
const machine = (): FigureAsset => {
|
|
20
|
+
let asset = createFigureAsset('Machine')
|
|
21
|
+
asset = addFigureBox(asset, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
22
22
|
asset = moving(asset, 'block', { kind: 'slide', axis: 'x', id: 'shift', min: 0, max: 60, start: 0 })
|
|
23
|
-
asset =
|
|
23
|
+
asset = addFigureBox(asset, { id: 'arm', size: { x: 40, y: 200, z: 40 }, position: { x: 0, y: 200, z: 0 } })
|
|
24
24
|
return moving(asset, 'arm', { kind: 'turn', axis: 'y', id: 'spin', min: -180, max: 180, start: 0 })
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
test('클립마다 길이와 트랙을, 트랙마다 키를 초로 읽는다', () => {
|
|
28
|
-
let asset =
|
|
29
|
-
asset =
|
|
28
|
+
let asset = editFigurePart(machine(), { kind: 'set-state-input', input: 'shift', label: '밀기' })
|
|
29
|
+
asset = editFigurePart(asset, { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 60 }, { seconds: 4, value: 0 }] })
|
|
30
30
|
assert.deepEqual(undrivenOf(asset), ['spin'], '재생하지 않는 조작값만 새 트랙이 된다')
|
|
31
31
|
const [clip] = clipsOf(asset)
|
|
32
32
|
assert.equal(clip!.name, 'cycle')
|
|
@@ -35,19 +35,19 @@ test('클립마다 길이와 트랙을, 트랙마다 키를 초로 읽는다', (
|
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
test('막대를 옮기면 그 순간의 자세를 한 묶음으로 — 드라이버와 같은 값', () => {
|
|
38
|
-
const asset =
|
|
38
|
+
const asset = editFigurePart(machine(), { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
|
|
39
39
|
assert.deepEqual(poseAt(asset, 'cycle', 1), [{ kind: 'set-state', input: 'shift', unit: 'mm', value: 15 }])
|
|
40
40
|
assert.deepEqual(poseAt(asset, 'nothing', 1), [])
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
test('키를 그 순간에 더하고 빼며, 쓰면 figure-model 이 받는다 — 양 끝은 남는다', () => {
|
|
44
|
-
let asset =
|
|
44
|
+
let asset = editFigurePart(machine(), { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
|
|
45
45
|
const [clip] = clipsOf(asset)
|
|
46
46
|
const track = clip!.tracks[0]!
|
|
47
47
|
const keys = withKey(track.keys, { seconds: 2, value: 30 })
|
|
48
48
|
assert.deepEqual(keys.map(k => k.seconds), [0, 2, 4])
|
|
49
49
|
assert.deepEqual(withKey(keys, { seconds: 2, value: 45 }).map(k => k.value), [0, 45, 60], '같은 순간이면 바꾼다')
|
|
50
|
-
asset =
|
|
50
|
+
asset = editFigurePart(asset, trackEdit(clip!, track, { keys }))
|
|
51
51
|
assert.equal(clipsOf(asset)[0]!.tracks[0]!.keys.length, 3)
|
|
52
52
|
assert.equal(withoutKey(track.keys, 1), null, '두 개뿐이면 뺄 수 없다')
|
|
53
53
|
assert.equal(withoutKey(keys, 0), null, '처음 키(0초)는 뺄 수 없다')
|
|
@@ -55,13 +55,13 @@ test('키를 그 순간에 더하고 빼며, 쓰면 figure-model 이 받는다
|
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
test('움직임 탭에 서고, 막대는 입력 한 묶음 · 나머지는 부품 편집으로 편집기에 닿는다', () => {
|
|
58
|
-
const editor = read('../client/modeller/
|
|
59
|
-
const start = editor.indexOf('<figure-
|
|
60
|
-
const block = editor.slice(start, editor.indexOf('</figure-
|
|
58
|
+
const editor = read('../client/modeller/editor.ts')
|
|
59
|
+
const start = editor.indexOf('<figure-clips')
|
|
60
|
+
const block = editor.slice(start, editor.indexOf('</figure-clips>', start))
|
|
61
61
|
assert.ok(start >= 0)
|
|
62
|
-
assert.ok(block.includes('@
|
|
63
|
-
assert.ok(block.includes('@
|
|
64
|
-
const clips = read('../client/modeller/figure-
|
|
62
|
+
assert.ok(block.includes('@states=${(e: CustomEvent) => this.run(() => this.session!.apply(e.detail.changes))}'))
|
|
63
|
+
assert.ok(block.includes('@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'))
|
|
64
|
+
const clips = read('../client/modeller/figure-clips.ts')
|
|
65
65
|
assert.ok(clips.includes("@change=${(e: Event) => this.scrub(clip, number(e))}"), '막대는 놓을 때 한 번')
|
|
66
66
|
const keys = [...clips.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
|
|
67
67
|
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
|
|
@@ -9,25 +9,25 @@ import assert from 'node:assert/strict'
|
|
|
9
9
|
import { readFileSync } from 'node:fs'
|
|
10
10
|
import { fileURLToPath } from 'node:url'
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
import type {
|
|
12
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart } from '@hatiolab/figure-model'
|
|
13
|
+
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
14
14
|
|
|
15
|
-
import { controlsOf, poseAsDefault, poseDiffers } from '../client/modeller/
|
|
15
|
+
import { controlsOf, poseAsDefault, poseDiffers } from '../client/modeller/controls.ts'
|
|
16
16
|
import { moving } from './joints.ts'
|
|
17
17
|
|
|
18
18
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
19
19
|
|
|
20
|
-
const slider = ():
|
|
21
|
-
let asset =
|
|
22
|
-
asset =
|
|
20
|
+
const slider = (): FigureAsset => {
|
|
21
|
+
let asset = createFigureAsset('Slider')
|
|
22
|
+
asset = addFigureBox(asset, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
23
23
|
asset = moving(asset, 'block', { kind: 'slide', axis: 'x', id: 'shift', min: 0, max: 60, start: 20 })
|
|
24
|
-
asset =
|
|
24
|
+
asset = addFigureBox(asset, { id: 'arm', size: { x: 40, y: 200, z: 40 }, position: { x: 0, y: 200, z: 0 } })
|
|
25
25
|
return moving(asset, 'arm', { kind: 'turn', axis: 'y', id: 'spin', min: -90, max: 90, start: 0 })
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
test('조작값마다 이름 · 단위 · 범위 · 지금 값 · 시작 값 · 이동 시간을 읽는다', () => {
|
|
29
29
|
let asset = slider()
|
|
30
|
-
asset =
|
|
30
|
+
asset = editFigurePart(asset, { kind: 'set-state-input', input: 'shift', label: '밀기', sweep: 2 })
|
|
31
31
|
const [shift, spin] = controlsOf(asset, { shift: 45 })
|
|
32
32
|
assert.deepEqual(
|
|
33
33
|
{ ...shift, step: undefined },
|
|
@@ -45,21 +45,21 @@ test('지금 자세를 기본으로 — 보고 있는 자세가 시작 자세와
|
|
|
45
45
|
assert.equal(poseDiffers(controlsOf(asset)), false)
|
|
46
46
|
const looking = controlsOf(asset, { shift: 50, spin: 30 })
|
|
47
47
|
assert.equal(poseDiffers(looking), true)
|
|
48
|
-
const kept =
|
|
48
|
+
const kept = editFigurePart(asset, poseAsDefault(looking))
|
|
49
49
|
assert.deepEqual([kept.stateDefaults['shift'], kept.stateDefaults['spin']], [50, 30])
|
|
50
50
|
assert.equal(poseDiffers(controlsOf(kept, { shift: 50, spin: 30 })), false)
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
test('편집기에 닿는다 — 값 바꾸기는 입력으로, 이름 · 범위 · 시간 · 기본 자세는 부품 편집으로', () => {
|
|
54
|
-
const editor = read('../client/modeller/
|
|
55
|
-
const start = editor.indexOf('<figure-
|
|
56
|
-
const block = editor.slice(start, editor.indexOf('</figure-
|
|
54
|
+
const editor = read('../client/modeller/editor.ts')
|
|
55
|
+
const start = editor.indexOf('<figure-controls')
|
|
56
|
+
const block = editor.slice(start, editor.indexOf('</figure-controls>', start))
|
|
57
57
|
assert.ok(start >= 0)
|
|
58
|
-
assert.ok(block.includes("@
|
|
59
|
-
assert.ok(block.includes('@
|
|
58
|
+
assert.ok(block.includes("@state=${(e: CustomEvent) => this.run(() => this.session!.apply([{ kind: 'set-state'"))
|
|
59
|
+
assert.ok(block.includes('@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'))
|
|
60
60
|
assert.ok(editor.includes("const design = view.fields.filter(i => i.role !== 'state')"), '조작값은 설계 값 칸에 두 번 나오지 않는다')
|
|
61
61
|
|
|
62
|
-
const controls = read('../client/modeller/figure-
|
|
62
|
+
const controls = read('../client/modeller/figure-controls.ts')
|
|
63
63
|
assert.ok(controls.includes("@change=${(e: Event) => this.try(c, number(e))}"), '슬라이더는 놓을 때 한 번 — 끄는 동안 기록이 쌓이지 않는다')
|
|
64
64
|
const keys = [...controls.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
|
|
65
65
|
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
|