@things-factory/figure-ui 10.1.28 → 10.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/angle-dial.ts +70 -0
- package/client/modeller/figure-animations.ts +9 -9
- package/client/modeller/figure-canvas.ts +6 -3
- package/client/modeller/figure-inspector.ts +18 -3
- package/client/modeller/figure-preview.ts +55 -3
- package/client/modeller/figure-report.ts +11 -28
- package/client/modeller/figure-side.ts +1 -0
- package/client/modeller/joint-gizmo.ts +63 -1
- package/client/modeller/part-edits.ts +75 -9
- package/client/pages/figure-modeller-page.ts +20 -2
- package/client/types.ts +2 -0
- package/dist-client/modeller/angle-dial.d.ts +32 -0
- package/dist-client/modeller/angle-dial.js +41 -0
- package/dist-client/modeller/angle-dial.js.map +1 -0
- package/dist-client/modeller/figure-animations.d.ts +4 -4
- package/dist-client/modeller/figure-animations.js +9 -9
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-canvas.js +6 -3
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +11 -0
- package/dist-client/modeller/figure-inspector.js +20 -3
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +11 -0
- package/dist-client/modeller/figure-preview.js +53 -3
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +8 -4
- package/dist-client/modeller/figure-report.js +11 -31
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-side.js +1 -0
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/joint-gizmo.d.ts +4 -1
- package/dist-client/modeller/joint-gizmo.js +54 -0
- package/dist-client/modeller/joint-gizmo.js.map +1 -1
- package/dist-client/modeller/part-edits.d.ts +29 -4
- package/dist-client/modeller/part-edits.js +61 -9
- package/dist-client/modeller/part-edits.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +7 -0
- package/dist-client/pages/figure-modeller-page.js +20 -2
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +2 -0
- package/dist-client/types.js.map +1 -1
- package/package.json +5 -5
- package/test/angle-dial.test.ts +78 -0
- package/test/base-box.test.ts +11 -0
- package/test/expect.ts +2 -0
- package/test/i18n-prefix-guard.test.ts +17 -0
- package/test/joint-needle.test.ts +162 -0
- package/test/part-edits.test.ts +100 -0
- package/test/scene-budget.test.ts +29 -0
- package/translations/en.json +3 -7
- package/translations/ja.json +3 -7
- package/translations/ko.json +3 -7
- package/translations/ms.json +3 -7
- package/translations/zh.json +3 -7
package/dist-client/types.d.ts
CHANGED
package/dist-client/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../client/types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * 화면이 다루는 모양.\n *\n * 정본 형식(`FigureSource`)의 타입은 **여기서 다시 정의하지 않는다** —\n * `@hatiolab/figure-model` 에서 가져온다. 두 벌이면 갈리고, 갈리면 화면이 서버가\n * 거절할 것을 만들어 낸다.\n */\nexport type { FigureSource, FigureBlueprint, FigurePart, ErrorCode, ViolationCode } from '@hatiolab/figure-model'\n\n/** 서버가 돌려주는 Figure 한 벌. `source` 는 저작면에서만 받는다. */\nexport interface Figure {\n id: string\n /** 컴포넌트 타입 이름. **만든 뒤에 고칠 수 없다.** */\n type: string\n name: string\n description?: string\n /** 분류 하나 — 팔레트에서 묶어 보이는 기준. */\n category?: string\n /** 태그 여럿 — 분류로는 못 하는 찾기. */\n tags?: string[]\n state?: 'draft' | 'released'\n /** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */\n score?: number\n triangles?: number\n groups?: number\n thumbnail?: string\n thumbnailUpdatedAt?: string\n updatedAt?: string\n updater?: { id: string; name: string }\n /** `FigureSource` JSON 문자열. 목록 질의에는 실려 오지 않는다. */\n source?: string\n properties?: string\n version?: number\n}\n\n/**\n * 발행된 판 하나.\n *\n * 저작 중 저장은 여기 오지 않는다 — 판본은 발행에서만 생긴다.\n */\nexport interface FigureVersion {\n version: number\n /** 발행하며 남긴 말. */\n comment?: string\n state?: 'draft' | 'released'\n updatedAt?: string\n updater?: { id: string; name: string }\n score?: number\n triangles?: number\n groups?: number\n}\n\nexport interface FigureListResult {\n items: Figure[]\n total: number\n}\n\n/**\n * 새 Figure.\n *\n * `score`·`triangles`·`groups` 가 없다 — **서버가 정본에서 계산한다.** 화면이 보내면\n * 거짓이 들어오고 목록이 실물과 다른 것을 보여 준다.\n */\nexport interface NewFigure {\n type: string\n name: string\n description?: string\n category?: string\n tags?: string[]\n source: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/**\n * 수정.\n *\n * `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된 도면이 그 이름으로 타입을\n * 찾으므로, 바꾸면 그 도면이 빈 화면이 된다.\n */\nexport interface FigurePatch {\n name?: string\n description?: string\n category?: string\n /** 빈 배열은 「전부 지운다」, 안 주는 것은 「건드리지 않는다」. */\n tags?: string[]\n source?: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/** 검증 결과 한 줄. 코드는 figure-model 의 것을 그대로 쓴다. */\nexport interface FigureFinding {\n code: string\n message: string\n at?: string\n}\n\n/**\n * 저작 보조가 낸 후보.\n *\n * **정본이 아니다.** 저작자가 보고 받아야 정본이 된다 — 서버는 저장하지 않고\n * 돌려주기만 한다.\n */\nexport interface FigureProposal {\n /** 후보 정본. JSON 문자열이다 — 화면이 파싱한다. */\n source: string\n score: number\n grade: string\n triangles: number\n /** 재질 묶음 = 인스턴스 하나가 무는 draw call. */\n groups: number\n /** Rule-based review of measurable design concerns; it is not an aesthetic verdict. */\n quality: {\n status: 'ready' | 'review'\n summary: string\n findings: Array<{ dimension: string; code: string; message: string }>\n visualRegions: number\n visualCoverage: number\n visualReadability: number\n }\n /** 형식은 맞으나 정책을 넘은 것. 막지 않는다 — 저작자가 정한다. */\n violations: FigureFinding[]\n /** 몇 번 만에 됐나. 진단용이다. */\n attempts: number\n}\n\n/** 사람이 후보를 수락하거나 버릴 때 직접 남긴 현재 저작 세션의 참고 신호. */\nexport interface ProposalFeedback {\n outcome: 'accepted' | 'discarded'\n selectedChanges: number\n totalChanges: number\n note?: string\n grade?: string\n quality?: { status?: 'ready' | 'review'; findingCodes?: string[] }\n}\n\n/**\n * 발행 판정 한 줄.\n *\n * `FigureFinding` 과 달리 **`blocking` 이 있다.** 저장 쪽 발견은 정의상 막지 않는 것들이고,\n * 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고\n * 화면이 다시 분류하면 규칙이 두 벌이 된다.\n */\n/** One reversible anchor edit the sizing gate has measured against this exact source. */\nexport interface FigureGateFix {\n findingWay: string\n part: string\n axis: 'x' | 'y' | 'z'\n from?: string\n to: 'min' | 'center' | 'max' | 'span'\n resolved: number\n /** All blocking sizing cases clear after this exact edit was remeasured server-side. */\n safe: boolean\n}\n\nexport interface FigureGateFinding {\n code: string\n message: string\n /** 왜 그것이 문제인가 — 사람이 무엇을 보게 되나. */\n why?: string\n /** 무엇을 바꾸면 되나 — 부품 이름과 값으로. */\n how?: string\n /** 갈라진 부품 이름들, 또는 정본 안의 경로. */\n at?: string\n /** Candidate edits; the UI must preview and explicitly apply these, never auto-save them. */\n fixes?: FigureGateFix[]\n blocking: boolean\n}\n\n/** 발행해도 되나. `blocked` 면 서버가 거절한다. */\nexport interface FigureInspection {\n blocked: boolean\n findings: FigureGateFinding[]\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../client/types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * 화면이 다루는 모양.\n *\n * 정본 형식(`FigureSource`)의 타입은 **여기서 다시 정의하지 않는다** —\n * `@hatiolab/figure-model` 에서 가져온다. 두 벌이면 갈리고, 갈리면 화면이 서버가\n * 거절할 것을 만들어 낸다.\n */\nexport type { FigureSource, FigureBlueprint, FigurePart, ErrorCode, ViolationCode } from '@hatiolab/figure-model'\n\n/** 서버가 돌려주는 Figure 한 벌. `source` 는 저작면에서만 받는다. */\nexport interface Figure {\n id: string\n /** 컴포넌트 타입 이름. **만든 뒤에 고칠 수 없다.** */\n type: string\n name: string\n description?: string\n /** 분류 하나 — 팔레트에서 묶어 보이는 기준. */\n category?: string\n /** 태그 여럿 — 분류로는 못 하는 찾기. */\n tags?: string[]\n state?: 'draft' | 'released'\n /** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */\n score?: number\n triangles?: number\n groups?: number\n thumbnail?: string\n thumbnailUpdatedAt?: string\n updatedAt?: string\n updater?: { id: string; name: string }\n /** `FigureSource` JSON 문자열. 목록 질의에는 실려 오지 않는다. */\n source?: string\n properties?: string\n version?: number\n}\n\n/**\n * 발행된 판 하나.\n *\n * 저작 중 저장은 여기 오지 않는다 — 판본은 발행에서만 생긴다.\n */\nexport interface FigureVersion {\n version: number\n /** 발행하며 남긴 말. */\n comment?: string\n state?: 'draft' | 'released'\n updatedAt?: string\n updater?: { id: string; name: string }\n score?: number\n triangles?: number\n groups?: number\n}\n\nexport interface FigureListResult {\n items: Figure[]\n total: number\n}\n\n/**\n * 새 Figure.\n *\n * `score`·`triangles`·`groups` 가 없다 — **서버가 정본에서 계산한다.** 화면이 보내면\n * 거짓이 들어오고 목록이 실물과 다른 것을 보여 준다.\n */\nexport interface NewFigure {\n type: string\n name: string\n description?: string\n category?: string\n tags?: string[]\n source: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/**\n * 수정.\n *\n * `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된 도면이 그 이름으로 타입을\n * 찾으므로, 바꾸면 그 도면이 빈 화면이 된다.\n */\nexport interface FigurePatch {\n /** 발행 전 초안에서만 받는다 — 발행된 뒤에는 문이 거절한다(ADR-0063 결정 4). */\n type?: string\n name?: string\n description?: string\n category?: string\n /** 빈 배열은 「전부 지운다」, 안 주는 것은 「건드리지 않는다」. */\n tags?: string[]\n source?: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/** 검증 결과 한 줄. 코드는 figure-model 의 것을 그대로 쓴다. */\nexport interface FigureFinding {\n code: string\n message: string\n at?: string\n}\n\n/**\n * 저작 보조가 낸 후보.\n *\n * **정본이 아니다.** 저작자가 보고 받아야 정본이 된다 — 서버는 저장하지 않고\n * 돌려주기만 한다.\n */\nexport interface FigureProposal {\n /** 후보 정본. JSON 문자열이다 — 화면이 파싱한다. */\n source: string\n score: number\n grade: string\n triangles: number\n /** 재질 묶음 = 인스턴스 하나가 무는 draw call. */\n groups: number\n /** Rule-based review of measurable design concerns; it is not an aesthetic verdict. */\n quality: {\n status: 'ready' | 'review'\n summary: string\n findings: Array<{ dimension: string; code: string; message: string }>\n visualRegions: number\n visualCoverage: number\n visualReadability: number\n }\n /** 형식은 맞으나 정책을 넘은 것. 막지 않는다 — 저작자가 정한다. */\n violations: FigureFinding[]\n /** 몇 번 만에 됐나. 진단용이다. */\n attempts: number\n}\n\n/** 사람이 후보를 수락하거나 버릴 때 직접 남긴 현재 저작 세션의 참고 신호. */\nexport interface ProposalFeedback {\n outcome: 'accepted' | 'discarded'\n selectedChanges: number\n totalChanges: number\n note?: string\n grade?: string\n quality?: { status?: 'ready' | 'review'; findingCodes?: string[] }\n}\n\n/**\n * 발행 판정 한 줄.\n *\n * `FigureFinding` 과 달리 **`blocking` 이 있다.** 저장 쪽 발견은 정의상 막지 않는 것들이고,\n * 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고\n * 화면이 다시 분류하면 규칙이 두 벌이 된다.\n */\n/** One reversible anchor edit the sizing gate has measured against this exact source. */\nexport interface FigureGateFix {\n findingWay: string\n part: string\n axis: 'x' | 'y' | 'z'\n from?: string\n to: 'min' | 'center' | 'max' | 'span'\n resolved: number\n /** All blocking sizing cases clear after this exact edit was remeasured server-side. */\n safe: boolean\n}\n\nexport interface FigureGateFinding {\n code: string\n message: string\n /** 왜 그것이 문제인가 — 사람이 무엇을 보게 되나. */\n why?: string\n /** 무엇을 바꾸면 되나 — 부품 이름과 값으로. */\n how?: string\n /** 갈라진 부품 이름들, 또는 정본 안의 경로. */\n at?: string\n /** Candidate edits; the UI must preview and explicitly apply these, never auto-save them. */\n fixes?: FigureGateFix[]\n blocking: boolean\n}\n\n/** 발행해도 되나. `blocked` 면 서버가 거절한다. */\nexport interface FigureInspection {\n blocked: boolean\n findings: FigureGateFinding[]\n}\n"]}
|
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.29",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"clean": "npm run clean:server && npm run clean:client"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@hatiolab/figure-model": "^0.1.
|
|
30
|
-
"@hatiolab/things-scene": "^10.1.
|
|
29
|
+
"@hatiolab/figure-model": "^0.1.27",
|
|
30
|
+
"@hatiolab/things-scene": "^10.1.22",
|
|
31
31
|
"@material/web": "^2.0.0",
|
|
32
32
|
"@operato/data-grist": "^10.0.0",
|
|
33
33
|
"@operato/graphql": "^10.0.0",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"@operato/shell": "^10.0.0",
|
|
38
38
|
"@operato/styles": "^10.0.0",
|
|
39
39
|
"@operato/utils": "^10.0.0",
|
|
40
|
-
"@things-factory/figure-service": "^10.1.
|
|
40
|
+
"@things-factory/figure-service": "^10.1.29",
|
|
41
41
|
"three": "^0.185.1"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "e6bf9dec003fe5e794746aaeb5852b3b50478232"
|
|
44
44
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { dialShape } from '../client/modeller/angle-dial.ts'
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* 막대 옆의 각도 눈.
|
|
11
|
+
*
|
|
12
|
+
* 관절이 돌았다는 것을 3D 안에서만 보이려 했다가 잰 값에 막혔다 — 미리보기의 부품에 단 바늘이
|
|
13
|
+
* 4 × 2 픽셀이었다. 여덟 대를 한 화면에 세우는 자리라 한 대가 20 픽셀쯤이고, 그 안의 표시는
|
|
14
|
+
* 무엇이든 몇 픽셀을 못 넘는다. 그래서 각은 3D 의 배율에서 떼어 이 눈이 읽는다.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const near = (got: number, want: number, why: string) => assert.ok(Math.abs(got - want) < 0.05, `${why}: ${got}`)
|
|
18
|
+
|
|
19
|
+
describe('각도 눈', () => {
|
|
20
|
+
it('0 도는 위를 가리킨다', () => {
|
|
21
|
+
const dial = dialShape(-90, 90, 0)
|
|
22
|
+
|
|
23
|
+
near(dial.needle.x, dial.centre.x, '가운데에서 안 벗어난다')
|
|
24
|
+
near(dial.needle.y, dial.centre.y - dial.radius, '위로 반지름만큼')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('양수는 시계 방향이다 — 화면의 각과 같은 쪽', () => {
|
|
28
|
+
const dial = dialShape(-180, 180, 90)
|
|
29
|
+
|
|
30
|
+
near(dial.needle.x, dial.centre.x + dial.radius, '오른쪽')
|
|
31
|
+
near(dial.needle.y, dial.centre.y, '높이는 가운데')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('음수는 반대쪽이다', () => {
|
|
35
|
+
const dial = dialShape(-180, 180, -90)
|
|
36
|
+
near(dial.needle.x, dial.centre.x - dial.radius, '왼쪽')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('범위 밖의 값은 범위 끝에 선다 — 밖을 가리키는 바늘은 없는 값을 말한다', () => {
|
|
40
|
+
const over = dialShape(-90, 90, 200)
|
|
41
|
+
const under = dialShape(-90, 90, -200)
|
|
42
|
+
|
|
43
|
+
assert.deepEqual(over.needle, dialShape(-90, 90, 90).needle)
|
|
44
|
+
assert.deepEqual(under.needle, dialShape(-90, 90, -90).needle)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('180 도를 넘는 범위는 큰 호로 그린다 — 안 그러면 부채꼴이 반대쪽으로 패인다', () => {
|
|
48
|
+
const wide = dialShape(-150, 150, 0)
|
|
49
|
+
const narrow = dialShape(-45, 45, 0)
|
|
50
|
+
|
|
51
|
+
assert.ok(/A 9 9 0 1 1/.test(wide.sector), `큰 호: ${wide.sector}`)
|
|
52
|
+
assert.ok(/A 9 9 0 0 1/.test(narrow.sector), `작은 호: ${narrow.sector}`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('한 바퀴는 원이다 — 부채꼴의 두 변이 겹쳐 그어지지 않는다', () => {
|
|
56
|
+
const full = dialShape(-180, 180, 45)
|
|
57
|
+
|
|
58
|
+
assert.ok(!full.sector.includes('L'), `변을 긋지 않는다: ${full.sector}`)
|
|
59
|
+
assert.equal((full.sector.match(/A /g) ?? []).length, 2, '반원 둘로 원을 만든다')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('눈이 화면에 붙어 있다', () => {
|
|
64
|
+
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
65
|
+
|
|
66
|
+
it('미리보기의 파라미터 줄이 눈을 그린다', () => {
|
|
67
|
+
const preview = read('../client/modeller/figure-preview.ts')
|
|
68
|
+
|
|
69
|
+
assert.ok(/this\.renderDial\(range, value\)/.test(preview), '막대 옆에 있다')
|
|
70
|
+
assert.ok(/dialShape\(range\.min, range\.max, value\)/.test(preview), '값으로 그린다')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('각이 아닌 파라미터에는 안 단다', () => {
|
|
74
|
+
const preview = read('../client/modeller/figure-preview.ts')
|
|
75
|
+
|
|
76
|
+
assert.ok(/range\.unit !== 'deg'/.test(preview), 'deg 만')
|
|
77
|
+
})
|
|
78
|
+
})
|
package/test/base-box.test.ts
CHANGED
|
@@ -138,6 +138,17 @@ describe('화면에 붙어 있다', () => {
|
|
|
138
138
|
assert.ok(/private resizeBase\(draft: FigureDraft, parts: PartModel\[\]\) \{\s*\n\s*this\.recordHistory\('base'\)/.test(page))
|
|
139
139
|
})
|
|
140
140
|
|
|
141
|
+
it('발행 전에는 타입 칸이 열려 있다 — 기계가 지은 이름을 고칠 자리가 있어야 한다', () => {
|
|
142
|
+
const inspector = source('../client/modeller/figure-inspector.ts')
|
|
143
|
+
const side = source('../client/modeller/figure-side.ts')
|
|
144
|
+
const page = source('../client/pages/figure-modeller-page.ts')
|
|
145
|
+
|
|
146
|
+
/* 판 번호가 0 이면 고칠 수 있다. 「저장됐나」로 가르면 저장하는 순간 굳는다. */
|
|
147
|
+
assert.ok(/figureVersion \?\? 0\) > 0/.test(inspector), '판 번호로 가른다')
|
|
148
|
+
assert.ok(/<figure-inspector[\s\S]{0,200}\.figureVersion=/.test(side), '판 번호가 검사기까지 온다')
|
|
149
|
+
assert.ok(/type: this\.figure\.type/.test(page), '저장할 때 행의 타입도 함께 보낸다')
|
|
150
|
+
})
|
|
151
|
+
|
|
141
152
|
it('발행된 도형을 고치는 동안 그 자리에서 말한다', () => {
|
|
142
153
|
const page = source('../client/pages/figure-modeller-page.ts')
|
|
143
154
|
const inspector = source('../client/modeller/figure-inspector.ts')
|
package/test/expect.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface Expectation {
|
|
|
29
29
|
toBeUndefined(): void
|
|
30
30
|
toBeGreaterThan(expected: number): void
|
|
31
31
|
toBeLessThan(expected: number): void
|
|
32
|
+
toBeGreaterThanOrEqual(expected: number): void
|
|
32
33
|
toBeLessThanOrEqual(expected: number): void
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -50,6 +51,7 @@ export function expect(actual: any): Expectation {
|
|
|
50
51
|
toBeUndefined: () => assert.strictEqual(actual, undefined),
|
|
51
52
|
toBeGreaterThan: expected => assert.ok(actual > expected, `${actual} is not greater than ${expected}`),
|
|
52
53
|
toBeLessThan: expected => assert.ok(actual < expected, `${actual} is not less than ${expected}`),
|
|
54
|
+
toBeGreaterThanOrEqual: expected => assert.ok(actual >= expected, `${actual} is not >= ${expected}`),
|
|
53
55
|
toBeLessThanOrEqual: expected => assert.ok(actual <= expected, `${actual} is not <= ${expected}`)
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -105,6 +105,23 @@ test('통째로 부르는 키가 번역 파일에 있다 — 없으면 화면에
|
|
|
105
105
|
assert.deepEqual(missing, [], `부르는 키가 번역에 없다:\n ${missing.join('\n ')}`)
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
+
test('★ 한국어를 defaultValue 로 들고 있지 않다 — 번역 파일을 지나지 않는 문구가 된다', () => {
|
|
109
|
+
const hard: string[] = []
|
|
110
|
+
for (const path of FILES) {
|
|
111
|
+
const text = readFileSync(path, 'utf-8')
|
|
112
|
+
for (const match of text.matchAll(/defaultValue:\s*(['"`])([^'"`]*)\1/g)) {
|
|
113
|
+
if (/[가-힣]/.test(match[2]!)) hard.push(`${path.slice(CLIENT.length + 1)} ${match[2]}`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
`t(key, { defaultValue })` 는 키가 없을 때 그 값을 그대로 쓴다. 한국어를 적어 두면 다른
|
|
119
|
+
언어에서도 그 한국어가 뜨고, 번역 파일에 키를 넣어도 아무도 그 자리를 다시 안 본다.
|
|
120
|
+
ADR-0049 ②.
|
|
121
|
+
*/
|
|
122
|
+
assert.deepEqual(hard, [], `한국어 defaultValue:\n ${hard.join('\n ')}`)
|
|
123
|
+
})
|
|
124
|
+
|
|
108
125
|
/*
|
|
109
126
|
이어 붙이는 자리는 값을 코드에서 읽어야 알 수 있어 여기서 전수 검사하지 않는다. 대신 **통이
|
|
110
127
|
갈린 짝**을 못 박는다 — 사다리 이름은 `label`, 그 설명은 `text` 다. 한 번 어긋났고, 그때
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { compile, validate } from '@hatiolab/figure-model'
|
|
8
|
+
import type { FigureSource } from '@hatiolab/figure-model'
|
|
9
|
+
|
|
10
|
+
import { jointNeedles, NEEDLE } from '../client/modeller/joint-gizmo.ts'
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* 미리보기에서 관절이 돌았다는 것을 보이게 하는 바늘.
|
|
14
|
+
*
|
|
15
|
+
* 이 시험이 지키는 것은 하나다 — **축에서 비켜나 있을 것.** 축 위에 붙으면 대칭인 부품과 같이
|
|
16
|
+
* 돌면서 같은 그림이 되고, 그러면 바늘을 붙인 뜻이 없어진다. 없는 것보다 나쁘다: 화면에 무엇이
|
|
17
|
+
* 하나 더 생겼는데 여전히 아무것도 안 움직인다.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const source = (over: Partial<FigureSource> = {}): FigureSource => ({
|
|
21
|
+
version: 2,
|
|
22
|
+
type: 'ARM',
|
|
23
|
+
base: { x: 400, y: 1200, z: 400 },
|
|
24
|
+
placement: 'floor',
|
|
25
|
+
detailLevel: 'M',
|
|
26
|
+
parts: [
|
|
27
|
+
{
|
|
28
|
+
name: 'post',
|
|
29
|
+
primitive: 'cylinder',
|
|
30
|
+
transform: { position: { x: 0, y: 100, z: 0 }, size: { x: 200, y: 200, z: 200 } },
|
|
31
|
+
material: { token: 'palette.dark' }
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'arm',
|
|
35
|
+
primitive: 'cylinder',
|
|
36
|
+
transform: { position: { x: 0, y: 400, z: 0 }, size: { x: 120, y: 400, z: 120 } },
|
|
37
|
+
material: { token: 'palette.primary' },
|
|
38
|
+
parent: 'post'
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
joints: [
|
|
42
|
+
{
|
|
43
|
+
name: 'turn',
|
|
44
|
+
child: 'arm',
|
|
45
|
+
type: 'revolute',
|
|
46
|
+
origin: { x: 0, y: 200, z: 0 },
|
|
47
|
+
axis: { x: 0, y: 1, z: 0 },
|
|
48
|
+
limits: { min: -90, max: 90 }
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
...over
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('관절 바늘', () => {
|
|
55
|
+
it('관절이 모는 부품에 자식으로 붙는다 — 그래야 관절이 도는 만큼 같이 돈다', () => {
|
|
56
|
+
const [needle] = jointNeedles(source())
|
|
57
|
+
|
|
58
|
+
assert.equal(needle!.parent, 'arm')
|
|
59
|
+
assert.equal(needle!.name, `${NEEDLE}turn`)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('축에서 비켜나 선다 — 축 위에 있으면 대칭인 부품과 같은 그림이다', () => {
|
|
63
|
+
const [needle] = jointNeedles(source())
|
|
64
|
+
const { position, size } = needle!.transform
|
|
65
|
+
|
|
66
|
+
/* y 축으로 도는 관절: 바늘은 x 로 비켜나고 x 로 길다. */
|
|
67
|
+
assert.ok(position.x > 0, '축에서 비켜났다')
|
|
68
|
+
assert.equal(position.y, 200, '기준점 높이 그대로')
|
|
69
|
+
assert.ok(size.x > size.y && size.x > size.z, 'x 로 길다')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('가로축으로 도는 관절은 z 로 비켜난다', () => {
|
|
73
|
+
const [needle] = jointNeedles(source({ joints: [{ ...source().joints![0]!, axis: { x: 1, y: 0, z: 0 } }] }))
|
|
74
|
+
const { position, size } = needle!.transform
|
|
75
|
+
|
|
76
|
+
assert.ok(position.z > 0)
|
|
77
|
+
assert.ok(size.z > size.x && size.z > size.y)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('세로축(z)으로 도는 관절은 x 로 비켜난다', () => {
|
|
81
|
+
const [needle] = jointNeedles(source({ joints: [{ ...source().joints![0]!, axis: { x: 0, y: 0, z: 1 } }] }))
|
|
82
|
+
assert.ok(needle!.transform.position.x > 0)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('비스듬한 축은 그리지 않는다 — 비켜날 쪽을 못 고른다', () => {
|
|
86
|
+
const oblique = source({ joints: [{ ...source().joints![0]!, axis: { x: 1, y: 1, z: 0 } }] })
|
|
87
|
+
assert.deepEqual(jointNeedles(oblique), [])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('길이는 도는 부품에서 온다. 작은 부품에서도 보일 만큼은 된다', () => {
|
|
91
|
+
const [big] = jointNeedles(source())
|
|
92
|
+
assert.equal(big!.transform.size.x, Math.round(400 * 0.6))
|
|
93
|
+
|
|
94
|
+
const small = source({
|
|
95
|
+
parts: source().parts.map(part =>
|
|
96
|
+
part.name === 'arm'
|
|
97
|
+
? { ...part, transform: { ...part.transform, size: { x: 10, y: 10, z: 10 } } }
|
|
98
|
+
: part
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
assert.equal(jointNeedles(small)[0]!.transform.size.x, 60)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('굵기는 길이에 견준다 — 8mm 로 고정했더니 미리보기에서 한 픽셀이 안 됐다', () => {
|
|
105
|
+
const [needle] = jointNeedles(source())
|
|
106
|
+
const { size } = needle!.transform
|
|
107
|
+
|
|
108
|
+
/* 길이 240 짜리 막대는 48 로 두껍다. 부품이 작아도 6 밑으로는 안 내려간다. */
|
|
109
|
+
assert.equal(size.y, Math.round(size.x / 5))
|
|
110
|
+
assert.equal(size.z, size.y)
|
|
111
|
+
|
|
112
|
+
const small = source({
|
|
113
|
+
parts: source().parts.map(part =>
|
|
114
|
+
part.name === 'arm' ? { ...part, transform: { ...part.transform, size: { x: 10, y: 10, z: 10 } } } : part
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
assert.equal(jointNeedles(small)[0]!.transform.size.y, Math.max(6, Math.round(60 / 5)))
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('관절이 없으면 바늘도 없다', () => {
|
|
121
|
+
assert.deepEqual(jointNeedles(source({ joints: [] })), [])
|
|
122
|
+
assert.deepEqual(jointNeedles(source({ joints: undefined })), [])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('바늘을 붙인 형식이 그대로 선다 — 미리보기가 이것을 compile 한다', () => {
|
|
126
|
+
const shown: FigureSource = { ...source(), parts: [...source().parts, ...jointNeedles(source())] }
|
|
127
|
+
|
|
128
|
+
assert.deepEqual(validate(shown).errors, [])
|
|
129
|
+
assert.doesNotThrow(() => compile(shown))
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* 바늘은 **미리보기에만** 있다. 저장 형식으로 새어 나가면 발행된 도형에 없던 막대가 생기고,
|
|
135
|
+
* 계약 판정에서는 부품이 하나 늘어난 것으로 읽힌다.
|
|
136
|
+
*/
|
|
137
|
+
describe('바늘이 미리보기 밖으로 안 나간다', () => {
|
|
138
|
+
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
139
|
+
|
|
140
|
+
it('미리보기가 붙여서 세운다', () => {
|
|
141
|
+
const preview = read('../client/modeller/figure-preview.ts')
|
|
142
|
+
|
|
143
|
+
assert.ok(/jointNeedles\(source\)/.test(preview), '붙인다')
|
|
144
|
+
assert.ok(/registerFigure\(compile\(shown\)/.test(preview), '붙인 것을 세운다')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('저작면과 저장은 원본 그대로다', () => {
|
|
148
|
+
const page = read('../client/pages/figure-modeller-page.ts')
|
|
149
|
+
const canvas = read('../client/modeller/figure-canvas.ts')
|
|
150
|
+
|
|
151
|
+
assert.ok(!/jointNeedles/.test(page), '페이지는 바늘을 모른다')
|
|
152
|
+
assert.ok(!/jointNeedles/.test(canvas), '저작 캔버스는 바늘을 모른다')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('원본 목록을 건드리지 않는다 — 같은 배열에 밀어 넣으면 저장에 들어간다', () => {
|
|
156
|
+
const original = source()
|
|
157
|
+
const before = original.parts.length
|
|
158
|
+
jointNeedles(original)
|
|
159
|
+
|
|
160
|
+
assert.equal(original.parts.length, before)
|
|
161
|
+
})
|
|
162
|
+
})
|
package/test/part-edits.test.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { describe, it, test } from 'node:test'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
3
5
|
|
|
4
6
|
import { expect } from './expect.ts'
|
|
5
7
|
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
niceStep,
|
|
18
20
|
partLimit,
|
|
19
21
|
removePart,
|
|
22
|
+
roundPart,
|
|
20
23
|
snapPart,
|
|
21
24
|
updatePart
|
|
22
25
|
} from '../client/modeller/part-edits.ts'
|
|
@@ -55,6 +58,53 @@ const DECK: PartModel = {
|
|
|
55
58
|
token: 'palette.primary'
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
/*
|
|
62
|
+
* 기즈모에서 온 숫자.
|
|
63
|
+
*
|
|
64
|
+
* 화면에서 끌면 씬이 비율을 곱한 값을 돌려준다. 그 값이 그대로 부품에 들어가면 저장 형식에
|
|
65
|
+
* 소수 넷째 자리가 남고, 크기 칸은 잘려서 보인다.
|
|
66
|
+
*/
|
|
67
|
+
describe('씬에서 온 부품을 밀리미터로 맞춘다', () => {
|
|
68
|
+
const part = (over: Partial<PartModel> = {}): PartModel => ({ ...DECK, ...over })
|
|
69
|
+
|
|
70
|
+
it('끈 크기와 자리를 정수로 맞춘다', () => {
|
|
71
|
+
const dragged = part({ left: 384.99999999999994, top: 380, zPos: 930, width: 368.98743869823886, height: 140, depth: 722.4180046035418 })
|
|
72
|
+
const fixed = roundPart(dragged)
|
|
73
|
+
|
|
74
|
+
expect(fixed.left).toBe(385)
|
|
75
|
+
expect(fixed.width).toBe(369)
|
|
76
|
+
expect(fixed.depth).toBe(722)
|
|
77
|
+
expect(fixed.top).toBe(380)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('크기는 1 밑으로 안 내려간다 — 0 이면 부피가 사라지고 배율이 0 으로 나뉜다', () => {
|
|
81
|
+
const tiny = roundPart(part({ width: 0.2, height: 0.4, depth: 0 }))
|
|
82
|
+
expect(tiny.width).toBe(1)
|
|
83
|
+
expect(tiny.height).toBe(1)
|
|
84
|
+
expect(tiny.depth).toBe(1)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('맞춘 값을 다시 맞춰도 같다', () => {
|
|
88
|
+
const once = roundPart(part({ left: 12.5, width: 7.5 }))
|
|
89
|
+
expect(roundPart(once)).toEqual(once)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('캔버스가 씬의 변경을 받을 때 그것을 지난다 — 안 부르면 위 넷이 전부 초록인 채로 소수가 남는다', () => {
|
|
93
|
+
const canvas = readFileSync(fileURLToPath(new URL('../client/modeller/figure-canvas.ts', import.meta.url)), 'utf8')
|
|
94
|
+
|
|
95
|
+
assert.ok(/roundPart/.test(canvas), 'import')
|
|
96
|
+
assert.ok(/this\.tell\('part-changed', \{ name, part: roundPart\(/.test(canvas), '올려 보내기 전에 맞춘다')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('크기·자리 밖의 값은 건드리지 않는다', () => {
|
|
100
|
+
const kept = roundPart(part({ name: 'arm', token: 'palette.dark', segments: 12, zPos: 3.4 }))
|
|
101
|
+
expect(kept.name).toBe('arm')
|
|
102
|
+
expect(kept.token).toBe('palette.dark')
|
|
103
|
+
expect(kept.segments).toBe(12)
|
|
104
|
+
expect(kept.zPos).toBe(3)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
58
108
|
describe('부품 생성', () => {
|
|
59
109
|
it('기준 상자의 3분의 1 크기로 생성된다 — 추가하자마자 보여야 한다', () => {
|
|
60
110
|
const part = newPart(DRAFT, [], 'cube')
|
|
@@ -98,6 +148,56 @@ describe('부품 생성', () => {
|
|
|
98
148
|
expect(newPart(DRAFT, [], 'polygon').path).toHaveLength(3)
|
|
99
149
|
})
|
|
100
150
|
|
|
151
|
+
/*
|
|
152
|
+
* 둘째 부품이 어디에 서는가.
|
|
153
|
+
*
|
|
154
|
+
* 언제나 바닥 한가운데였을 때, 둘째 부품은 첫 부품 **속에** 들어갔다 — 같은 자리에 같은
|
|
155
|
+
* 크기라 화면에는 덩어리 하나뿐이고 저작자는 무엇이 늘었는지 못 봤다.
|
|
156
|
+
*/
|
|
157
|
+
describe('고른 부품 위에 얹는다', () => {
|
|
158
|
+
/* 갑판을 바닥에 내려 둔다 — 그 위에 3분의 1 짜리가 들어가야 얹는 갈래를 볼 수 있다. */
|
|
159
|
+
const FLOOR_DECK = { ...DECK, zPos: 0 }
|
|
160
|
+
|
|
161
|
+
it('윗면에 서고 발자국이 가운데로 맞는다', () => {
|
|
162
|
+
const part = newPart(DRAFT, [FLOOR_DECK], 'cube', FLOOR_DECK)
|
|
163
|
+
|
|
164
|
+
expect(part.zPos).toBe(FLOOR_DECK.zPos + FLOOR_DECK.depth)
|
|
165
|
+
// 자리도 크기도 정수로 맞추므로 0.5 까지는 어긋난다
|
|
166
|
+
const middle = { x: FLOOR_DECK.left + FLOOR_DECK.width / 2, y: FLOOR_DECK.top + FLOOR_DECK.height / 2 }
|
|
167
|
+
expect(Math.abs(part.left + part.width / 2 - middle.x)).toBeLessThanOrEqual(0.5)
|
|
168
|
+
expect(Math.abs(part.top + part.height / 2 - middle.y)).toBeLessThanOrEqual(0.5)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('얹으면 상자를 넘을 때는 바닥 한가운데로 간다 — 추가가 발행을 막지 않는다', () => {
|
|
172
|
+
/* 갑판이 이미 높다: 500 + 200 위에 267 을 더 쌓으면 상자 높이 800 을 넘는다. */
|
|
173
|
+
const part = newPart(DRAFT, [DECK], 'cube', DECK)
|
|
174
|
+
|
|
175
|
+
expect(part.zPos).toBe(0)
|
|
176
|
+
expect(Math.abs(part.left + part.width / 2 - DRAFT.width / 2)).toBeLessThanOrEqual(0.5)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('가장자리 부품 위에서도 상자 안에 남는다', () => {
|
|
180
|
+
const corner = { ...DECK, left: 0, top: 0, width: 200, height: 200, zPos: 0, depth: 100 }
|
|
181
|
+
const part = newPart(DRAFT, [corner], 'cube', corner)
|
|
182
|
+
|
|
183
|
+
expect(part.left).toBe(0)
|
|
184
|
+
expect(part.top).toBe(0)
|
|
185
|
+
expect(part.left + part.width).toBeLessThanOrEqual(DRAFT.width)
|
|
186
|
+
expect(part.top + part.height).toBeLessThanOrEqual(DRAFT.height)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('고른 것이 없으면 전과 같이 바닥 한가운데다', () => {
|
|
190
|
+
const alone = newPart(DRAFT, [DECK], 'cube')
|
|
191
|
+
expect(alone.zPos).toBe(0)
|
|
192
|
+
expect(Math.abs(alone.left + alone.width / 2 - DRAFT.width / 2)).toBeLessThanOrEqual(0.5)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('쌓은 둘이 서로 겹치지 않는다 — 이 규칙이 지키는 것이 그것이다', () => {
|
|
196
|
+
const [, stacked] = addPart(DRAFT, [FLOOR_DECK], 'cylinder', FLOOR_DECK)
|
|
197
|
+
expect(stacked!.zPos).toBeGreaterThanOrEqual(FLOOR_DECK.zPos + FLOOR_DECK.depth)
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
101
201
|
/*
|
|
102
202
|
화면에서 멀쩡해 보여도 저장이 거부되면 사용자는 이유를 알 수 없다. 새로 만든 부품은
|
|
103
203
|
손대지 않은 상태로도 검증을 통과해야 한다.
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { describe, it } from 'node:test'
|
|
3
3
|
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
4
8
|
import { capacityFrom, MEASURED, SCENE_BUDGET } from '../client/modeller/scene-budget.ts'
|
|
5
9
|
|
|
6
10
|
/*
|
|
@@ -51,3 +55,28 @@ describe('한 화면이 감당하는 양', () => {
|
|
|
51
55
|
assert.ok(conveyor.instances >= 1600 && conveyor.instances <= 2500)
|
|
52
56
|
})
|
|
53
57
|
})
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* 놓는 개수로 값을 말하지 않는다 — ADR-0051 ③.
|
|
61
|
+
*
|
|
62
|
+
* 보고서에 「100개 기준 draw call」 · 「1만개 기준」이 오래 남아 있었다. 놓는 개수는 값을 치르지
|
|
63
|
+
* 않는다(가만히 선 도형은 프레임 비용 0, 카메라 밖은 컬링). 치르는 것은 한 화면에 보이는 개수 ×
|
|
64
|
+
* 그 도형의 묶음 수이고, 그 답은 위의 실측 분모가 한다.
|
|
65
|
+
*/
|
|
66
|
+
describe('놓는 개수 기준 문구가 화면에 없다', () => {
|
|
67
|
+
const HERE = fileURLToPath(new URL('.', import.meta.url))
|
|
68
|
+
|
|
69
|
+
it('보고서가 「N개 기준」 칸을 그리지 않는다', () => {
|
|
70
|
+
const report = readFileSync(join(HERE, '../client/modeller/figure-report.ts'), 'utf8')
|
|
71
|
+
|
|
72
|
+
assert.ok(!/at-100|at-10k/.test(report), '개수 기준 칸')
|
|
73
|
+
assert.ok(!/costOf\(blueprint, \d/.test(report), '개수를 넣어 셈하지 않는다')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('번역에 그 낱말이 남아 있지 않다', () => {
|
|
77
|
+
const ko = JSON.parse(readFileSync(join(HERE, '../translations/ko.json'), 'utf8')) as Record<string, string>
|
|
78
|
+
const left = Object.entries(ko).filter(([key]) => /at-100|at-10k/.test(key))
|
|
79
|
+
|
|
80
|
+
assert.deepEqual(left, [])
|
|
81
|
+
})
|
|
82
|
+
})
|
package/translations/en.json
CHANGED
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
"figure.label.axis-scale-short": "scale",
|
|
86
86
|
"figure.label.axis-width": "width",
|
|
87
87
|
"figure.label.base-box": "base box (mm)",
|
|
88
|
-
"figure.label.budget-animations": "moving parts",
|
|
89
88
|
"figure.label.budget-groups": "material groups",
|
|
89
|
+
"figure.label.budget-independent": "independently moved parts",
|
|
90
90
|
"figure.label.budget-parts": "part count",
|
|
91
91
|
"figure.label.budget-surface": "surfaces outside the palette",
|
|
92
92
|
"figure.label.budget-transparent": "transparent materials",
|
|
@@ -113,8 +113,6 @@
|
|
|
113
113
|
"figure.label.distinct-materials": "distinct materials",
|
|
114
114
|
"figure.label.distinct-shapes": "distinct shapes",
|
|
115
115
|
"figure.label.draft": "draft",
|
|
116
|
-
"figure.label.draw-calls-at-100": "draw calls for 100",
|
|
117
|
-
"figure.label.draw-calls-at-10k": "draw calls at 10k instances",
|
|
118
116
|
"figure.label.emissive": "emissive",
|
|
119
117
|
"figure.label.emissive-color": "lit color",
|
|
120
118
|
"figure.label.environment": "environment",
|
|
@@ -263,8 +261,6 @@
|
|
|
263
261
|
"figure.label.tools": "align",
|
|
264
262
|
"figure.label.transparent": "transparent",
|
|
265
263
|
"figure.label.triangles": "triangles",
|
|
266
|
-
"figure.label.triangles-at-100": "triangles for 100",
|
|
267
|
-
"figure.label.triangles-at-10k": "triangles at 10k instances",
|
|
268
264
|
"figure.label.unit-none": "no unit",
|
|
269
265
|
"figure.label.unit-type-it": "type it…",
|
|
270
266
|
"figure.label.weight": "drawing cost",
|
|
@@ -276,8 +272,6 @@
|
|
|
276
272
|
"figure.text.already-released-edit-to-release-again": "already released. edit and save to go back to draft, then release again.",
|
|
277
273
|
"figure.text.anchor-explained": "where this part stays as the box grows. two touching parts on different faces drift apart as it grows.",
|
|
278
274
|
"figure.text.anchors-are-not-drawn": "slots and ports are not drawn on a drawing; only the editor shows them.",
|
|
279
|
-
"figure.text.animated-parts-cost": "each moving part adds one draw call.",
|
|
280
|
-
"figure.text.animated-parts-over": "over the limit. many of these on one drawing will slow it down.",
|
|
281
275
|
"figure.text.animation-empty-examples": "a turning roller · a sliding door leaf · a rising lift · a blinking beacon.",
|
|
282
276
|
"figure.text.animation-empty-what-it-is": "no motion yet. add a clip to move parts.",
|
|
283
277
|
"figure.text.animation-needs-a-named-part": "add a part first.",
|
|
@@ -344,6 +338,8 @@
|
|
|
344
338
|
"figure.text.hide-all-parts": "hide all",
|
|
345
339
|
"figure.text.hide-this-part": "hide this part while you work. not saved",
|
|
346
340
|
"figure.text.how-many-fit": "how many can be on screen before it slows down",
|
|
341
|
+
"figure.text.independent-parts-cost": "each part that carries its own transform adds one draw call.",
|
|
342
|
+
"figure.text.independent-parts-over": "over the limit. fewer of these fit on one screen at once.",
|
|
347
343
|
"figure.text.instances-unit": "instances",
|
|
348
344
|
"figure.text.interpolation-explained": "how the gap between keys is filled.",
|
|
349
345
|
"figure.text.joint-channel-value-deg": "key values are joint angles in degrees.",
|
package/translations/ja.json
CHANGED
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
"figure.label.axis-scale-short": "scale",
|
|
86
86
|
"figure.label.axis-width": "width",
|
|
87
87
|
"figure.label.base-box": "基準ボックス (mm)",
|
|
88
|
-
"figure.label.budget-animations": "animation points",
|
|
89
88
|
"figure.label.budget-groups": "material groups",
|
|
89
|
+
"figure.label.budget-independent": "個別に動く部品",
|
|
90
90
|
"figure.label.budget-parts": "part count",
|
|
91
91
|
"figure.label.budget-surface": "surfaces outside the palette",
|
|
92
92
|
"figure.label.budget-transparent": "transparent materials",
|
|
@@ -113,8 +113,6 @@
|
|
|
113
113
|
"figure.label.distinct-materials": "distinct materials",
|
|
114
114
|
"figure.label.distinct-shapes": "distinct shapes",
|
|
115
115
|
"figure.label.draft": "下書き",
|
|
116
|
-
"figure.label.draw-calls-at-100": "draw calls for 100",
|
|
117
|
-
"figure.label.draw-calls-at-10k": "draw calls at 10k instances",
|
|
118
116
|
"figure.label.emissive": "emissive",
|
|
119
117
|
"figure.label.emissive-color": "lit color",
|
|
120
118
|
"figure.label.environment": "environment",
|
|
@@ -263,8 +261,6 @@
|
|
|
263
261
|
"figure.label.tools": "align",
|
|
264
262
|
"figure.label.transparent": "transparent",
|
|
265
263
|
"figure.label.triangles": "triangles",
|
|
266
|
-
"figure.label.triangles-at-100": "triangles for 100",
|
|
267
|
-
"figure.label.triangles-at-10k": "triangles at 10k instances",
|
|
268
264
|
"figure.label.unit-none": "単位なし",
|
|
269
265
|
"figure.label.unit-type-it": "直接入力…",
|
|
270
266
|
"figure.label.weight": "weight",
|
|
@@ -276,8 +272,6 @@
|
|
|
276
272
|
"figure.text.already-released-edit-to-release-again": "すでに公開済みです。修正して保存すると下書きに戻り、そこで再び公開できます。",
|
|
277
273
|
"figure.text.anchor-explained": "ボックスが伸びるときにこの部品が付いている面です。接する部品と違う面に付くと、伸びたときに間が空きます。",
|
|
278
274
|
"figure.text.anchors-are-not-drawn": "スロットとポートは図面には描かれず、編集画面にだけ表示されます。",
|
|
279
|
-
"figure.text.animated-parts-cost": "動く部品ごとに draw call が 1 増えます。",
|
|
280
|
-
"figure.text.animated-parts-over": "上限を超えました。1 つの図面に多く置くと画面が遅くなります。",
|
|
281
275
|
"figure.text.animation-empty-examples": "回るローラー · スライドする扉 · 上下するリフト · 点滅する表示灯。",
|
|
282
276
|
"figure.text.animation-empty-what-it-is": "まだ動きがありません。clip を追加して部品を動かしてください。",
|
|
283
277
|
"figure.text.animation-needs-a-named-part": "先に部品を追加してください。",
|
|
@@ -344,6 +338,8 @@
|
|
|
344
338
|
"figure.text.hide-all-parts": "hide all",
|
|
345
339
|
"figure.text.hide-this-part": "hide this part while you work. not saved",
|
|
346
340
|
"figure.text.how-many-fit": "画面に出しても遅くならない数",
|
|
341
|
+
"figure.text.independent-parts-cost": "個別に動く部品ごとに draw call が 1 増えます。",
|
|
342
|
+
"figure.text.independent-parts-over": "上限を超えました。1 つの画面に一緒に置ける台数がその分減ります。",
|
|
347
343
|
"figure.text.instances-unit": "instances",
|
|
348
344
|
"figure.text.interpolation-explained": "キーの間をどうつなぐか。",
|
|
349
345
|
"figure.text.joint-channel-value-deg": "キーの値は関節の角度 (度) です。",
|