@things-factory/figure-ui 10.1.27 → 10.1.28
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/graphql/index.ts +23 -0
- package/client/modeller/base-box.ts +104 -0
- package/client/modeller/figure-animations.ts +81 -8
- package/client/modeller/figure-canvas.ts +18 -3
- package/client/modeller/figure-inspector.ts +151 -2
- package/client/modeller/figure-preview.ts +55 -0
- package/client/modeller/figure-report.ts +9 -4
- package/client/modeller/figure-side.ts +9 -1
- package/client/modeller/figure-source.ts +8 -0
- package/client/modeller/joint-edits.ts +66 -5
- package/client/modeller/joint-gizmo.ts +118 -0
- package/client/modeller/pose-defaults.ts +67 -0
- package/client/modeller/proposal.ts +9 -3
- package/client/modeller/scene-budget.ts +31 -40
- package/client/pages/figure-list-page.ts +4 -9
- package/client/pages/figure-modeller-page.ts +124 -41
- package/dist-client/graphql/index.d.ts +7 -0
- package/dist-client/graphql/index.js +21 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/base-box.d.ts +47 -0
- package/dist-client/modeller/base-box.js +52 -0
- package/dist-client/modeller/base-box.js.map +1 -0
- package/dist-client/modeller/figure-animations.d.ts +15 -0
- package/dist-client/modeller/figure-animations.js +77 -8
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-canvas.d.ts +9 -0
- package/dist-client/modeller/figure-canvas.js +14 -3
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +26 -0
- package/dist-client/modeller/figure-inspector.js +139 -2
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +15 -0
- package/dist-client/modeller/figure-preview.js +46 -0
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +3 -2
- package/dist-client/modeller/figure-report.js +9 -4
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +5 -1
- package/dist-client/modeller/figure-side.js +12 -0
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +7 -0
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/joint-edits.d.ts +23 -3
- package/dist-client/modeller/joint-edits.js +53 -5
- package/dist-client/modeller/joint-edits.js.map +1 -1
- package/dist-client/modeller/joint-gizmo.d.ts +31 -0
- package/dist-client/modeller/joint-gizmo.js +104 -0
- package/dist-client/modeller/joint-gizmo.js.map +1 -0
- package/dist-client/modeller/pose-defaults.d.ts +9 -0
- package/dist-client/modeller/pose-defaults.js +35 -0
- package/dist-client/modeller/pose-defaults.js.map +1 -0
- package/dist-client/modeller/proposal.d.ts +1 -1
- package/dist-client/modeller/proposal.js +8 -3
- package/dist-client/modeller/proposal.js.map +1 -1
- package/dist-client/modeller/scene-budget.d.ts +31 -34
- package/dist-client/modeller/scene-budget.js +28 -37
- package/dist-client/modeller/scene-budget.js.map +1 -1
- package/dist-client/pages/figure-list-page.js +4 -9
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +43 -0
- package/dist-client/pages/figure-modeller-page.js +121 -40
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/test/ai-proposal-contract.test.ts +16 -11
- package/test/base-box.test.ts +154 -0
- package/test/joint-edits.test.ts +37 -2
- package/test/joint-gizmo.test.ts +174 -0
- package/test/pose-defaults.test.ts +71 -0
- package/test/scene-budget.test.ts +53 -0
- package/translations/en.json +13 -2
- package/translations/ja.json +13 -2
- package/translations/ko.json +13 -2
- package/translations/ms.json +13 -2
- package/translations/zh.json +13 -2
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.28",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -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.28",
|
|
41
41
|
"three": "^0.185.1"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "3a260a0f79bc6b41a692a103955bfcc541aff60e"
|
|
44
44
|
}
|
|
@@ -128,21 +128,22 @@ test('피드백은 후보 세션의 선택과 측정값만 전송한다', () =>
|
|
|
128
128
|
})
|
|
129
129
|
|
|
130
130
|
/*
|
|
131
|
-
* The modeller
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
131
|
+
* The modeller opens no AI door of its own. The application header already has one and it opens the
|
|
132
|
+
* same dock, and two doors to one room on one screen make a reader look for the difference between
|
|
133
|
+
* them (2026-09-18, on the author's reading of the screen). `figure-ask` — a second, inline form
|
|
134
|
+
* that called `proposeFigure` directly — went on 2026-09-16.
|
|
135
135
|
*
|
|
136
|
-
* What the
|
|
137
|
-
*
|
|
138
|
-
*
|
|
136
|
+
* What the page owes is unchanged and checked here: when the dock asks, it answers with the source
|
|
137
|
+
* as it stands right now — unsaved edits included — so the candidate is a revision of what is open
|
|
138
|
+
* rather than something invented from the prompt alone. And it saves nothing by itself.
|
|
139
139
|
*/
|
|
140
|
-
test('
|
|
140
|
+
test('모델러는 AI 문을 스스로 두지 않고, 물으면 지금 정본을 답한다', () => {
|
|
141
141
|
const path = fileURLToPath(new URL('../client/pages/figure-modeller-page.ts', import.meta.url))
|
|
142
142
|
const source = readFileSync(path, 'utf8')
|
|
143
143
|
|
|
144
|
-
assert.
|
|
145
|
-
assert.match(source, /
|
|
144
|
+
assert.doesNotMatch(source, /openOverlay\('figure-ai-dock'/, '앱 머리줄에 이미 있는 문을 여기 또 두지 않는다')
|
|
145
|
+
assert.match(source, /addEventListener\('figure-ai-context-request'/)
|
|
146
|
+
assert.match(source, /detail: \{ figureId: this\.figure\?\.id, source: this\.folded \}/)
|
|
146
147
|
assert.doesNotMatch(source, /<figure-ask/, 'the inline ask form is gone; the dock is the entrance')
|
|
147
148
|
assert.doesNotMatch(source, /proposeFigure\(/, 'the page must not call the propose mutation itself')
|
|
148
149
|
})
|
|
@@ -568,7 +569,11 @@ test('구조 · 저작 가능한 모든 칸은 검토를 지난다 — 조용히
|
|
|
568
569
|
styleKit: ['plain', 'factory'],
|
|
569
570
|
animations: [undefined, animationOnly().animations],
|
|
570
571
|
parameters: [undefined, parameterOnly().parameters],
|
|
571
|
-
capabilities: [['Holdable'], ['Holdable', 'FlowNode']]
|
|
572
|
+
capabilities: [['Holdable'], ['Holdable', 'FlowNode']],
|
|
573
|
+
joints: [
|
|
574
|
+
undefined,
|
|
575
|
+
[{ name: 'motor-joint', child: 'motor', type: 'revolute', origin: { x: 0, y: 50, z: 0 }, axis: { x: 0, y: 1, z: 0 }, limits: { min: -90, max: 90 } }]
|
|
576
|
+
]
|
|
572
577
|
}
|
|
573
578
|
|
|
574
579
|
const unsampled = mustBeReviewable.filter(f => !(f in sample))
|
|
@@ -0,0 +1,154 @@
|
|
|
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 { baseFittingParts, baseOf, resizeBase } from '../client/modeller/base-box.ts'
|
|
8
|
+
import { toFigureSource } from '../client/modeller/figure-source.ts'
|
|
9
|
+
import type { FigureDraft, PartModel } from '../client/modeller/figure-source.ts'
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* 기준 상자를 고치는 일.
|
|
13
|
+
*
|
|
14
|
+
* 저작면의 자리는 판의 왼쪽 위에서 재고 저장 형식의 자리는 상자 한가운데에서 잰다. 그래서 상자만
|
|
15
|
+
* 바꾸면 **저장되는 부품 자리가 전부 달라진다** — 이 시험이 지키는 것이 그 한 가지다.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const DRAFT: FigureDraft = { version: 2, figureType: 'BOX', width: 1000, height: 1000, depth: 500, detailLevel: 'M' }
|
|
19
|
+
|
|
20
|
+
const part = (over: Partial<PartModel> = {}): PartModel => ({
|
|
21
|
+
type: 'figure-part',
|
|
22
|
+
name: 'body',
|
|
23
|
+
primitive: 'cube',
|
|
24
|
+
left: 400,
|
|
25
|
+
top: 400,
|
|
26
|
+
width: 200,
|
|
27
|
+
height: 200,
|
|
28
|
+
zPos: 0,
|
|
29
|
+
depth: 300,
|
|
30
|
+
token: 'palette.primary',
|
|
31
|
+
...over
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/** 저장 형식에서 본 부품 자리. 상자를 고쳐도 이 값이 그대로여야 한다. */
|
|
35
|
+
const placed = (draft: FigureDraft, parts: readonly PartModel[]) =>
|
|
36
|
+
toFigureSource(draft, [...parts]).parts.map(one => one.transform.position)
|
|
37
|
+
|
|
38
|
+
describe('기준 상자', () => {
|
|
39
|
+
it('지금 크기를 읽는다', () => {
|
|
40
|
+
assert.deepEqual(baseOf(DRAFT), { width: 1000, height: 1000, depth: 500 })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('상자를 키워도 부품의 저장 자리는 한 값도 안 바뀐다', () => {
|
|
44
|
+
const parts = [part(), part({ name: 'lamp', left: 450, top: 450, width: 100, height: 100, zPos: 300, depth: 60 })]
|
|
45
|
+
const before = placed(DRAFT, parts)
|
|
46
|
+
|
|
47
|
+
const next = resizeBase(DRAFT, parts, { width: 1600, height: 1200, depth: 500 })
|
|
48
|
+
|
|
49
|
+
assert.deepEqual(placed(next.draft, next.parts), before)
|
|
50
|
+
assert.equal(next.draft.width, 1600)
|
|
51
|
+
/* 늘어난 절반만큼 부품이 함께 옮겨 간다 — 저작면의 값은 달라진다. */
|
|
52
|
+
assert.equal(next.parts[0]!.left, 400 + 300)
|
|
53
|
+
assert.equal(next.parts[0]!.top, 400 + 100)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('높이만 바꾸면 부품은 안 움직인다 — 밑면이 바닥이다', () => {
|
|
57
|
+
const parts = [part()]
|
|
58
|
+
const next = resizeBase(DRAFT, parts, { width: 1000, height: 1000, depth: 900 })
|
|
59
|
+
|
|
60
|
+
assert.equal(next.parts, parts, '같은 배열 그대로다')
|
|
61
|
+
assert.equal(next.draft.depth, 900)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('달라질 것이 없으면 받은 것을 그대로 돌려준다', () => {
|
|
65
|
+
const parts = [part()]
|
|
66
|
+
const same = resizeBase(DRAFT, parts, { width: 1000, height: 1000, depth: 500 })
|
|
67
|
+
|
|
68
|
+
assert.equal(same.draft, DRAFT)
|
|
69
|
+
assert.equal(same.parts, parts)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('0 으로 만들 수 없다 — 배율이 0 으로 나뉜다', () => {
|
|
73
|
+
const next = resizeBase(DRAFT, [part()], { width: 0, height: -20, depth: 0 })
|
|
74
|
+
assert.deepEqual(baseOf(next.draft), { width: 1, height: 1, depth: 1 })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('부품에 맞추기', () => {
|
|
78
|
+
it('부품을 감싸는 가장 작은 상자를 낸다', () => {
|
|
79
|
+
/* 가운데(500,500)에서 가장 먼 모서리가 600 이므로 반지름 100 → 200. 높이는 제일 높은 끝. */
|
|
80
|
+
const fitted = baseFittingParts(DRAFT, [part()])
|
|
81
|
+
assert.deepEqual(fitted, { width: 200, height: 200, depth: 300 })
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('치우친 부품은 한가운데를 지키느라 반대쪽에 여유가 남는다', () => {
|
|
85
|
+
/*
|
|
86
|
+
부품을 가운데로 끌어오지 않는다. 끌어오면 저작자가 놓은 자리가 소리 없이 달라지고,
|
|
87
|
+
발행된 도형에서는 문의 자리가 함께 움직인다.
|
|
88
|
+
*/
|
|
89
|
+
const off = part({ left: 700, top: 400, width: 200, height: 200 })
|
|
90
|
+
const fitted = baseFittingParts(DRAFT, [off])!
|
|
91
|
+
|
|
92
|
+
assert.equal(fitted.width, 800, '가운데에서 900 까지가 400 이므로 양쪽으로 800')
|
|
93
|
+
const next = resizeBase(DRAFT, [off], fitted)
|
|
94
|
+
assert.deepEqual(placed(next.draft, next.parts), placed(DRAFT, [off]))
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('부품이 없으면 잴 것이 없다', () => {
|
|
98
|
+
assert.equal(baseFittingParts(DRAFT, []), undefined)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('맞춘 상자 안에서는 부품이 상자를 넘지 않는다', () => {
|
|
102
|
+
const parts = [part(), part({ name: 'arm', left: 300, top: 250, width: 120, height: 500, zPos: 200, depth: 420 })]
|
|
103
|
+
const fitted = baseFittingParts(DRAFT, parts)!
|
|
104
|
+
const next = resizeBase(DRAFT, parts, fitted)
|
|
105
|
+
const source = toFigureSource(next.draft, [...next.parts])
|
|
106
|
+
|
|
107
|
+
for (const one of source.parts) {
|
|
108
|
+
const { position, size } = one.transform
|
|
109
|
+
assert.ok(Math.abs(position.x) + size.x / 2 <= source.base.x / 2 + 0.001, `${one.name} x`)
|
|
110
|
+
assert.ok(Math.abs(position.z) + size.z / 2 <= source.base.z / 2 + 0.001, `${one.name} z`)
|
|
111
|
+
assert.ok(position.y + size.y / 2 <= source.base.y + 0.001, `${one.name} y`)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
* 셈이 맞아도 **화면에 안 붙어 있으면** 저작자는 아무것도 못 본다. 이 묶음은 붙어 있는지를 본다 —
|
|
119
|
+
* 이 파일의 다른 시험들이 전부 초록인 채로 칸이 사라질 수 있는 자리다.
|
|
120
|
+
*/
|
|
121
|
+
describe('화면에 붙어 있다', () => {
|
|
122
|
+
const source = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
123
|
+
|
|
124
|
+
it('검사기에 기준 상자 칸과 「부품에 맞추기」가 있다', () => {
|
|
125
|
+
const inspector = source('../client/modeller/figure-inspector.ts')
|
|
126
|
+
|
|
127
|
+
assert.ok(/figure\.label\.base-box/.test(inspector), '칸 이름')
|
|
128
|
+
assert.ok(/this\.resizeBase\(\{ width: value \}\)/.test(inspector), 'X 가 가로를 고친다')
|
|
129
|
+
assert.ok(/this\.resizeBase\(\{ depth: value \}\)/.test(inspector), 'Y 가 높이를 고친다')
|
|
130
|
+
assert.ok(/this\.fitBaseToParts\(\)/.test(inspector), '부품에 맞추기')
|
|
131
|
+
assert.ok(/new CustomEvent\('resize-base'/.test(inspector), '초안과 부품을 함께 올린다')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('초안과 부품이 한 걸음으로 기록된다 — 한 번 되돌리면 둘 다 돌아온다', () => {
|
|
135
|
+
const page = source('../client/pages/figure-modeller-page.ts')
|
|
136
|
+
|
|
137
|
+
assert.ok(/@resize-base=\$\{\(e: CustomEvent\) => this\.resizeBase\(e\.detail\.draft, e\.detail\.parts\)\}/.test(page))
|
|
138
|
+
assert.ok(/private resizeBase\(draft: FigureDraft, parts: PartModel\[\]\) \{\s*\n\s*this\.recordHistory\('base'\)/.test(page))
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('발행된 도형을 고치는 동안 그 자리에서 말한다', () => {
|
|
142
|
+
const page = source('../client/pages/figure-modeller-page.ts')
|
|
143
|
+
const inspector = source('../client/modeller/figure-inspector.ts')
|
|
144
|
+
|
|
145
|
+
/* 견줄 상대(직전 발행판)를 가져오고, 고칠 때마다 다시 잰다. */
|
|
146
|
+
assert.ok(/fetchReleasedSource\(id\)/.test(page), '직전 발행판을 읽는다')
|
|
147
|
+
assert.ok(/contractChanges\(this\.releasedSource, this\.folded\)/.test(page), '고치는 동안 견준다')
|
|
148
|
+
assert.ok(/\.contract=\$\{this\.contract\}/.test(page), '화면으로 내려보낸다')
|
|
149
|
+
|
|
150
|
+
/* 기준 상자에 대한 것은 그 칸 아래에서 말한다. */
|
|
151
|
+
assert.ok(/renderBaseContract\(\)/.test(inspector))
|
|
152
|
+
assert.ok(/base-ratio-changed|base-size-changed/.test(inspector))
|
|
153
|
+
})
|
|
154
|
+
})
|
package/test/joint-edits.test.ts
CHANGED
|
@@ -42,8 +42,8 @@ describe('joint edits', () => {
|
|
|
42
42
|
assert.deepEqual(parentChoices(PARTS, 'tip'), ['base', 'arm'])
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
-
it('adds a joint
|
|
46
|
-
const draft = setJointType(DRAFT, figurePart(DRAFT, 'arm'), 'revolute')
|
|
45
|
+
it('adds a joint where the part meets its parent, with a parameter that drives it', () => {
|
|
46
|
+
const draft = setJointType(DRAFT, figurePart(DRAFT, 'arm'), 'revolute', figurePart(DRAFT, 'base'))
|
|
47
47
|
const joint = jointOf(draft, 'arm')!
|
|
48
48
|
assert.deepEqual(joint, {
|
|
49
49
|
name: 'arm-joint',
|
|
@@ -120,6 +120,41 @@ describe('joint edits', () => {
|
|
|
120
120
|
assert.deepEqual(errorsOf(cleared), [])
|
|
121
121
|
})
|
|
122
122
|
|
|
123
|
+
/*
|
|
124
|
+
* 관절은 두 부품이 **맞닿는 자리**에 선다. 위로 쌓은 부품에서는 아랫면이지만, 옆에 달린
|
|
125
|
+
* 부품에서는 옆면이다 — 그 구분이 없으면 캐비닛 문의 축이 문 바닥에 생긴다.
|
|
126
|
+
*/
|
|
127
|
+
describe('새 관절이 서는 자리', () => {
|
|
128
|
+
const at = (over: Partial<PartModel>): PartModel => ({ ...part('door', 0, 100), ...over })
|
|
129
|
+
|
|
130
|
+
it('위에 쌓인 부품은 아랫면에서 만난다', () => {
|
|
131
|
+
/* arm 은 base(0..100) 위 100..500 이다. 맞닿는 면이 100 이고, 가로로는 둘이 겹치는 가운데다. */
|
|
132
|
+
const joint = jointOf(setJointType(DRAFT, figurePart(DRAFT, 'arm'), 'revolute', figurePart(DRAFT, 'base')), 'arm')!
|
|
133
|
+
assert.deepEqual(joint.origin, { x: 0, y: 100, z: 0 })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('옆에 달린 부품은 옆면에서 만난다 — 아랫면이 아니다', () => {
|
|
137
|
+
/* 문은 몸통(가운데 0, 폭 100)의 오른쪽에 붙어 선다. 축은 문의 왼쪽 면, 높이는 겹치는 가운데. */
|
|
138
|
+
const body = at({ name: 'body', left: 0, top: 0, width: 100, height: 100, zPos: 0, depth: 400 })
|
|
139
|
+
const door = at({ name: 'door', left: 100, top: 0, width: 20, height: 100, zPos: 40, depth: 320, parent: 'body' })
|
|
140
|
+
const parts = [body, door]
|
|
141
|
+
const draft: FigureDraft = { ...DRAFT, width: 400, height: 400, depth: 400 }
|
|
142
|
+
const shown = (one: PartModel) => partToFigure(one, draft)
|
|
143
|
+
|
|
144
|
+
const joint = jointOf(setJointType(draft, shown(door), 'revolute', shown(body)), 'door')!
|
|
145
|
+
const half = draft.width / 2
|
|
146
|
+
|
|
147
|
+
assert.equal(joint.origin.x, 100 - half, '문의 왼쪽 면에 선다')
|
|
148
|
+
assert.equal(joint.origin.y, 200, '겹치는 높이(40..360 과 0..400)의 한가운데다')
|
|
149
|
+
assert.deepEqual(parts.map(one => one.name), ['body', 'door'])
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('붙은 부품이 없으면 아랫면이다 — 그때 맞닿는 것은 바닥이다', () => {
|
|
153
|
+
const joint = jointOf(setJointType(DRAFT, figurePart(DRAFT, 'base'), 'revolute'), 'base')!
|
|
154
|
+
assert.equal(joint.origin.y, 0)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
123
158
|
it('a joint survives opening and saving', () => {
|
|
124
159
|
const turning = setJointType(DRAFT, figurePart(DRAFT, 'arm'), 'revolute')
|
|
125
160
|
const saved = toFigureSource(turning, PARTS)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import type { FigureJoint } from '@hatiolab/figure-model'
|
|
5
|
+
|
|
6
|
+
import { axisName, gizmoPart, JOINT_GIZMO } from '../client/modeller/joint-gizmo.ts'
|
|
7
|
+
import type { FigureDraft, PartModel } from '../client/modeller/figure-source.ts'
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* 관절이 도는 자리를 저작면에 그리는 부채꼴.
|
|
11
|
+
*
|
|
12
|
+
* 저작자가 축을 고를 때 보는 것이라, **잘못 그리면 틀린 쪽을 가리킨다** — 아무것도 안 그리는
|
|
13
|
+
* 것보다 나쁘다. 그래서 그릴 수 없는 축은 그리지 않는다.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DRAFT: FigureDraft = { version: 2, figureType: 'ARM', width: 400, height: 400, depth: 1200, detailLevel: 'M' }
|
|
17
|
+
|
|
18
|
+
const ARM: PartModel = {
|
|
19
|
+
type: 'figure-part',
|
|
20
|
+
name: 'arm',
|
|
21
|
+
primitive: 'cube',
|
|
22
|
+
left: 150,
|
|
23
|
+
top: 150,
|
|
24
|
+
width: 100,
|
|
25
|
+
height: 100,
|
|
26
|
+
zPos: 200,
|
|
27
|
+
depth: 500,
|
|
28
|
+
token: 'palette.primary'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const joint = (over: Partial<FigureJoint> = {}): FigureJoint => ({
|
|
32
|
+
name: 'arm-joint',
|
|
33
|
+
child: 'arm',
|
|
34
|
+
type: 'revolute',
|
|
35
|
+
origin: { x: 0, y: 200, z: 0 },
|
|
36
|
+
axis: { x: 0, y: 1, z: 0 },
|
|
37
|
+
limits: { min: -90, max: 90 },
|
|
38
|
+
...over
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
/** 부채꼴 점들이 도는 각. 0 도에서 시작해 어디까지 벌어졌나. */
|
|
42
|
+
function angles(path: { x: number; y: number }[]) {
|
|
43
|
+
const arc = path.filter(point => Math.hypot(point.x, point.y) > 1)
|
|
44
|
+
return arc.map(point => Math.round((Math.atan2(point.y, point.x) * 180) / Math.PI))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('관절 부채꼴', () => {
|
|
48
|
+
it('축 벡터를 이름으로 옮긴다', () => {
|
|
49
|
+
assert.equal(axisName({ x: 0, y: 1, z: 0 }), '+y')
|
|
50
|
+
assert.equal(axisName({ x: -1, y: 0, z: 0 }), '-x')
|
|
51
|
+
assert.equal(axisName({ x: 0, y: 0, z: 1 }), '+z')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('비스듬한 축은 이름이 없고, 그리지도 않는다 — 아무 쪽이나 고르면 틀린 쪽을 가리킨다', () => {
|
|
55
|
+
const slanted = { x: 0.7, y: 0.7, z: 0 }
|
|
56
|
+
assert.equal(axisName(slanted), undefined)
|
|
57
|
+
assert.equal(gizmoPart(joint({ axis: slanted }), ARM, DRAFT), undefined)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('가동 범위만큼 벌어지고, 가운데 점이 하나 있다', () => {
|
|
61
|
+
const part = gizmoPart(joint(), ARM, DRAFT)!
|
|
62
|
+
const path = part.path!
|
|
63
|
+
|
|
64
|
+
assert.equal(part.name, JOINT_GIZMO)
|
|
65
|
+
assert.deepEqual(path[0], { x: 0, y: 0 }, '부채꼴은 가운데에서 시작한다')
|
|
66
|
+
|
|
67
|
+
const turned = angles(path)
|
|
68
|
+
const span = Math.abs(turned[turned.length - 1]! - turned[0]!)
|
|
69
|
+
assert.equal(span, 180, '−90 에서 90 까지면 180 도 벌어진다')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('무한 회전은 온전한 원이라 가운데 점이 없다', () => {
|
|
73
|
+
const part = gizmoPart(joint({ type: 'continuous', limits: undefined }), ARM, DRAFT)!
|
|
74
|
+
const path = part.path!
|
|
75
|
+
|
|
76
|
+
assert.ok(path.every(point => Math.hypot(point.x, point.y) > 1), '가운데 점이 없다')
|
|
77
|
+
assert.ok(path.length >= 60, '한 바퀴를 6도씩 그린다')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('반지름은 도는 부품의 크기를 따라간다 — 부품보다 작으면 무엇이 도는지 안 읽힌다', () => {
|
|
81
|
+
const big = gizmoPart(joint(), ARM, DRAFT)!
|
|
82
|
+
const small = gizmoPart(joint(), { ...ARM, width: 40, height: 40, depth: 40 }, DRAFT)!
|
|
83
|
+
|
|
84
|
+
assert.equal(big.width, 2 * Math.round(500 * 0.6))
|
|
85
|
+
assert.equal(small.width, 2 * 60, '작은 부품에도 눈에 보이는 최소 크기가 있다')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('관절의 기준점에 선다', () => {
|
|
89
|
+
const part = gizmoPart(joint({ origin: { x: 50, y: 300, z: -20 } }), ARM, DRAFT)!
|
|
90
|
+
/* 저작면은 왼쪽 위 기준이고 z 는 밑면이다. 기준점이 가운데로 가야 맞다. */
|
|
91
|
+
assert.equal(part.left + part.width / 2, DRAFT.width / 2 + 50)
|
|
92
|
+
assert.equal(part.top + part.height / 2, DRAFT.height / 2 - 20)
|
|
93
|
+
assert.equal(part.zPos + part.depth / 2, 300)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('저작면에만 있고, 씬이 집지 않는다', () => {
|
|
97
|
+
const part = gizmoPart(joint(), ARM, DRAFT)!
|
|
98
|
+
assert.equal(part.locked, true)
|
|
99
|
+
assert.equal(part.transparent, true)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* 가리키는 쪽이 맞나 — **이것이 이 부채꼴의 전부다.**
|
|
104
|
+
*
|
|
105
|
+
* 반대로 벌어진 부채꼴은 없느니만 못하다. 저작자가 그것을 보고 축을 고르기 때문이다. 축마다
|
|
106
|
+
* 씬의 회전과 단면의 각이 도는 쪽이 갈려서, 여섯 축을 하나씩 런타임이 도는 식(로드리게스)과
|
|
107
|
+
* 맞대 본다.
|
|
108
|
+
*/
|
|
109
|
+
describe('벌어진 쪽이 실제로 도는 쪽과 같다', () => {
|
|
110
|
+
const AXES: [string, { x: number; y: number; z: number }][] = [
|
|
111
|
+
['+x', { x: 1, y: 0, z: 0 }],
|
|
112
|
+
['-x', { x: -1, y: 0, z: 0 }],
|
|
113
|
+
['+y', { x: 0, y: 1, z: 0 }],
|
|
114
|
+
['-y', { x: 0, y: -1, z: 0 }],
|
|
115
|
+
['+z', { x: 0, y: 0, z: 1 }],
|
|
116
|
+
['-z', { x: 0, y: 0, z: -1 }]
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
/** 로드리게스 — 런타임이 관절을 돌리는 식(`setFromAxisAngle`)과 같은 회전이다. */
|
|
120
|
+
function turned(v: { x: number; y: number; z: number }, axis: { x: number; y: number; z: number }, degrees: number) {
|
|
121
|
+
const t = (degrees * Math.PI) / 180
|
|
122
|
+
const c = Math.cos(t)
|
|
123
|
+
const s = Math.sin(t)
|
|
124
|
+
const dot = axis.x * v.x + axis.y * v.y + axis.z * v.z
|
|
125
|
+
const cross = {
|
|
126
|
+
x: axis.y * v.z - axis.z * v.y,
|
|
127
|
+
y: axis.z * v.x - axis.x * v.z,
|
|
128
|
+
z: axis.x * v.y - axis.y * v.x
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
x: v.x * c + cross.x * s + axis.x * dot * (1 - c),
|
|
132
|
+
y: v.y * c + cross.y * s + axis.y * dot * (1 - c),
|
|
133
|
+
z: v.z * c + cross.z * s + axis.z * dot * (1 - c)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 단면의 점을 부품 회전까지 태워 figure 좌표로. 단면은 부품의 x–z 평면에 눕는다. */
|
|
138
|
+
function inFigure(part: PartModel, point: { x: number; y: number }) {
|
|
139
|
+
const v = { x: point.x, y: 0, z: point.y }
|
|
140
|
+
const rx = part.rotationX ?? 0
|
|
141
|
+
const rz = part.rotationY ?? 0
|
|
142
|
+
const afterZ = { x: v.x * Math.cos(rz) - v.y * Math.sin(rz), y: v.x * Math.sin(rz) + v.y * Math.cos(rz), z: v.z }
|
|
143
|
+
return { x: afterZ.x, y: afterZ.y * Math.cos(rx) - afterZ.z * Math.sin(rx), z: afterZ.y * Math.sin(rx) + afterZ.z * Math.cos(rx) }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const [name, axis] of AXES) {
|
|
147
|
+
it(`${name} 축`, () => {
|
|
148
|
+
const limits = { min: -90, max: 90 }
|
|
149
|
+
const part = gizmoPart(joint({ axis, limits }), ARM, DRAFT)!
|
|
150
|
+
/* 가운데 점 하나에 호가 이어진다. 한 바퀴가 아니면 늘 그렇다. */
|
|
151
|
+
const arc = part.path!.slice(1)
|
|
152
|
+
const at = (degrees: number) =>
|
|
153
|
+
inFigure(part, arc[Math.round(((degrees - limits.min) / (limits.max - limits.min)) * (arc.length - 1))]!)
|
|
154
|
+
|
|
155
|
+
/* 조각이 6 도씩이라 48 도가 점 하나에 정확히 떨어진다 — 사이 값을 쓰면 반올림이 섞인다. */
|
|
156
|
+
const zero = at(0)
|
|
157
|
+
const swung = at(48)
|
|
158
|
+
const runtime = turned(zero, axis, 48)
|
|
159
|
+
const flipped = turned(zero, axis, -48)
|
|
160
|
+
|
|
161
|
+
const off = Math.hypot(swung.x - runtime.x, swung.y - runtime.y, swung.z - runtime.z)
|
|
162
|
+
const offFlipped = Math.hypot(swung.x - flipped.x, swung.y - flipped.y, swung.z - flipped.z)
|
|
163
|
+
|
|
164
|
+
assert.ok(off < 0.5, `${name}: 부채꼴이 ${off.toFixed(1)}mm 어긋난다 — 반대로 재면 ${offFlipped.toFixed(1)}mm`)
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('수평축 관절은 눕는다 — 도는 평면이 세로다', () => {
|
|
170
|
+
const part = gizmoPart(joint({ axis: { x: 1, y: 0, z: 0 } }), ARM, DRAFT)!
|
|
171
|
+
/* 씬은 라디안을 쓴다. z 90 도가 `rotationY` 로 간다(`figure-source.ts`). */
|
|
172
|
+
assert.equal(Math.round(((part.rotationY ?? 0) * 180) / Math.PI), 90)
|
|
173
|
+
})
|
|
174
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { poseAsDefaults, poseChanges } from '../client/modeller/pose-defaults.ts'
|
|
5
|
+
import type { FigureDraft } from '../client/modeller/figure-source.ts'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* 미리보기에서 세운 자세를 기본값으로 앉힌다.
|
|
9
|
+
*
|
|
10
|
+
* 관절 도형의 휴지 자세(값 0)는 곧게 선 기둥이라, 기본값이 없으면 목록에서도 도면에서도 그
|
|
11
|
+
* 모양으로 선다. 옮겨 적는 일을 손으로 하면 여섯 축에 여섯 번이고, 옮기는 사이 화면의 자세와
|
|
12
|
+
* 적은 수가 갈린다.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const DRAFT: FigureDraft = {
|
|
16
|
+
version: 2,
|
|
17
|
+
figureType: 'ARM',
|
|
18
|
+
width: 900,
|
|
19
|
+
height: 900,
|
|
20
|
+
depth: 1900,
|
|
21
|
+
detailLevel: 'L',
|
|
22
|
+
parameters: [
|
|
23
|
+
{ name: 'shoulder', range: { unit: 'deg', min: -120, max: 120 }, default: 0, clip: { channels: [] } },
|
|
24
|
+
{ name: 'elbow', range: { unit: 'deg', min: -150, max: 150 }, default: 0, clip: { channels: [] } },
|
|
25
|
+
/* 기본값을 안 적은 파라미터. 형식은 그때 범위의 처음에 선다. */
|
|
26
|
+
{ name: 'hoist', range: { unit: 'mm', min: 0, max: 1800 }, clip: { channels: [] } }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('이 자세를 기본으로', () => {
|
|
31
|
+
it('민 것만 앉히고 나머지는 건드리지 않는다', () => {
|
|
32
|
+
const next = poseAsDefaults(DRAFT, { shoulder: -35 })
|
|
33
|
+
|
|
34
|
+
assert.equal(next.parameters![0]!.default, -35)
|
|
35
|
+
assert.equal(next.parameters![1], DRAFT.parameters![1], '안 민 파라미터는 같은 객체 그대로다')
|
|
36
|
+
assert.equal(next.parameters![2], DRAFT.parameters![2])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('이미 그 값이면 초안을 그대로 돌려준다 — 고친 것이 없는데 「저장 안 됨」이 붙지 않게', () => {
|
|
40
|
+
assert.equal(poseAsDefaults(DRAFT, { shoulder: 0 }), DRAFT)
|
|
41
|
+
assert.equal(poseAsDefaults(DRAFT, {}), DRAFT)
|
|
42
|
+
/* 기본값을 안 적은 파라미터는 범위의 처음에 서 있다. 그 값을 다시 적지 않는다. */
|
|
43
|
+
assert.equal(poseAsDefaults(DRAFT, { hoist: 0 }), DRAFT)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('기본값을 안 적었던 파라미터에도 앉는다', () => {
|
|
47
|
+
const next = poseAsDefaults(DRAFT, { hoist: 1200 })
|
|
48
|
+
assert.equal(next.parameters![2]!.default, 1200)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('범위 밖은 끝에서 문다 — 다른 길로 들어온 값도 형식을 지나야 한다', () => {
|
|
52
|
+
const next = poseAsDefaults(DRAFT, { elbow: 400 })
|
|
53
|
+
assert.equal(next.parameters![1]!.default, 150)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('수가 아닌 값은 안 말한 것으로 친다', () => {
|
|
57
|
+
for (const bad of [NaN, Infinity, undefined as unknown as number]) {
|
|
58
|
+
assert.equal(poseAsDefaults(DRAFT, { shoulder: bad }), DRAFT, String(bad))
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('무엇이 달라지는지 먼저 말할 수 있다 — 단추를 띄울지 이것으로 정한다', () => {
|
|
63
|
+
assert.deepEqual(poseChanges(DRAFT, { shoulder: -35, elbow: 0 }), [{ name: 'shoulder', from: 0, to: -35 }])
|
|
64
|
+
assert.deepEqual(poseChanges(DRAFT, { shoulder: 0 }), [])
|
|
65
|
+
assert.deepEqual(poseChanges(undefined, { shoulder: -35 }), [])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('없는 파라미터 이름은 무시한다 — 후보를 보다 남은 값이 섞여도 초안을 늘리지 않는다', () => {
|
|
69
|
+
assert.equal(poseAsDefaults(DRAFT, { 'no-such-thing': 42 }), DRAFT)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { capacityFrom, MEASURED, SCENE_BUDGET } from '../client/modeller/scene-budget.ts'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* 「몇 개까지 편한가」의 분모.
|
|
8
|
+
*
|
|
9
|
+
* 이 수는 **잰 값이다**(2026-09-18, Apple M5). 시험이 지키는 것은 셈이 아니라 그 사실이다 —
|
|
10
|
+
* 분모가 조용히 목표치로 돌아가거나, 잰 기계와 날짜가 화면에서 빠지면 사람이 어디서나 참인
|
|
11
|
+
* 수로 읽는다.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
describe('한 화면이 감당하는 양', () => {
|
|
15
|
+
it('분모는 화면에 보이는 부품 묶음이고, 잰 기계와 날짜를 달고 있다', () => {
|
|
16
|
+
assert.equal(SCENE_BUDGET.groupsOnScreen, 4000)
|
|
17
|
+
assert.equal(MEASURED.gpu, 'Apple M5')
|
|
18
|
+
assert.equal(MEASURED.on, '2026-09-18')
|
|
19
|
+
assert.equal(MEASURED.fps, 60)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('묶음으로 갈리면 묶음 수로 나눈다 — 그림자를 다시 곱하지 않는다', () => {
|
|
23
|
+
/*
|
|
24
|
+
실측이 그림자를 켠 채 잰 값이라 배수가 이미 안에 있다. 전에는 여기서 2 를 곱했고,
|
|
25
|
+
그 곱을 남겨 두면 같은 값을 두 번 세어 개수가 절반으로 나온다.
|
|
26
|
+
*/
|
|
27
|
+
const robot = capacityFrom(9, 200)!
|
|
28
|
+
assert.equal(robot.bound, 'groups')
|
|
29
|
+
assert.equal(robot.per, 9)
|
|
30
|
+
assert.equal(robot.instances, Math.floor(4000 / 9))
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('삼각형이 먼저 차면 그쪽으로 갈린다', () => {
|
|
34
|
+
const heavy = capacityFrom(2, 100_000)!
|
|
35
|
+
assert.equal(heavy.bound, 'triangles')
|
|
36
|
+
assert.equal(heavy.instances, 5)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('잴 것이 없으면 답하지 않는다 — 「무한히 놓을 수 있다」가 아니다', () => {
|
|
40
|
+
assert.equal(capacityFrom(0, 0), undefined)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
실측한 두 도형이 이 분모로 되돌아오는지 본다. 분모를 누가 고치면 이 둘이 먼저 어긋난다.
|
|
45
|
+
*/
|
|
46
|
+
it('잰 두 도형이 잰 수와 맞다', () => {
|
|
47
|
+
/* 묶음 9 짜리 6 축 로봇: 600 대에서 59.5fps, 700 대에서 56.7fps 였다. */
|
|
48
|
+
assert.ok(capacityFrom(9, 200)!.instances <= 600, '잰 값보다 많이 놓으라고 하면 안 된다')
|
|
49
|
+
/* 묶음 2 짜리 컨베이어: 1,600 대에서 59.3fps, 2,500 대에서 36.8fps 였다. */
|
|
50
|
+
const conveyor = capacityFrom(2, 100)!
|
|
51
|
+
assert.ok(conveyor.instances >= 1600 && conveyor.instances <= 2500)
|
|
52
|
+
})
|
|
53
|
+
})
|
package/translations/en.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"figure.button.delete-figure": "delete",
|
|
13
13
|
"figure.button.discard": "discard",
|
|
14
14
|
"figure.button.duplicate-part": "duplicate",
|
|
15
|
+
"figure.button.fit-base-to-parts": "fit to parts",
|
|
15
16
|
"figure.button.mirror-x": "mirror X",
|
|
16
17
|
"figure.button.mirror-z": "mirror Z",
|
|
17
18
|
"figure.button.move-part-down": "move down",
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
"figure.button.new-figure": "new figure",
|
|
20
21
|
"figure.button.pivot-clear": "part centre",
|
|
21
22
|
"figure.button.pivot-set": "set",
|
|
23
|
+
"figure.button.pose-as-default": "make this the default pose",
|
|
22
24
|
"figure.button.preview-fix": "preview fix · {part} {axis} face: {from} → {to}",
|
|
23
25
|
"figure.button.redo": "redo",
|
|
24
26
|
"figure.button.release-figure": "release",
|
|
@@ -82,6 +84,7 @@
|
|
|
82
84
|
"figure.label.axis-scale": "scale",
|
|
83
85
|
"figure.label.axis-scale-short": "scale",
|
|
84
86
|
"figure.label.axis-width": "width",
|
|
87
|
+
"figure.label.base-box": "base box (mm)",
|
|
85
88
|
"figure.label.budget-animations": "moving parts",
|
|
86
89
|
"figure.label.budget-groups": "material groups",
|
|
87
90
|
"figure.label.budget-parts": "part count",
|
|
@@ -262,6 +265,8 @@
|
|
|
262
265
|
"figure.label.triangles": "triangles",
|
|
263
266
|
"figure.label.triangles-at-100": "triangles for 100",
|
|
264
267
|
"figure.label.triangles-at-10k": "triangles at 10k instances",
|
|
268
|
+
"figure.label.unit-none": "no unit",
|
|
269
|
+
"figure.label.unit-type-it": "type it…",
|
|
265
270
|
"figure.label.weight": "drawing cost",
|
|
266
271
|
"figure.label.what-this-does": "what this figure does",
|
|
267
272
|
"figure.text.accepts-anything": "empty means anything",
|
|
@@ -278,8 +283,10 @@
|
|
|
278
283
|
"figure.text.animation-needs-a-named-part": "add a part first.",
|
|
279
284
|
"figure.text.assistant-proposed-a-figure": "the proposal is shown on the right.",
|
|
280
285
|
"figure.text.axes-not-measurable-yet": "left off the chart because they cannot be measured: {axes}. the reasons are below.",
|
|
281
|
-
"figure.text.
|
|
286
|
+
"figure.text.base-box-explained": "The box that sets the scale when the figure is placed on a drawing. A part reaching outside it blocks release. Changing the size leaves the parts where they are.",
|
|
287
|
+
"figure.text.bound-by-groups": "Part groups fill up first — this figure is {per} groups, and one screen draws {budget} groups at 60fps.",
|
|
282
288
|
"figure.text.bound-by-triangles": "triangles reach the limit first ({per} per instance, {budget} per drawing).",
|
|
289
|
+
"figure.text.budget-measured-on": "Measured on {gpu}, {on}. A figure standing still costs nothing — only what is on screen and redrawn counts.",
|
|
283
290
|
"figure.text.budget-not-measured-yet": "this target has not been measured yet.",
|
|
284
291
|
"figure.text.cannot-release-now": "cannot be released yet",
|
|
285
292
|
"figure.text.canvas-appears-once-a-part-exists": "pick a shape on the left to add the first part.",
|
|
@@ -296,6 +303,7 @@
|
|
|
296
303
|
"figure.text.change-capabilities": "capabilities",
|
|
297
304
|
"figure.text.change-changed": "a changed part",
|
|
298
305
|
"figure.text.change-detail": "the detail level",
|
|
306
|
+
"figure.text.change-joints": "joints",
|
|
299
307
|
"figure.text.change-placement": "placement",
|
|
300
308
|
"figure.text.change-removed": "a part being removed",
|
|
301
309
|
"figure.text.change-styleKit": "the style kit",
|
|
@@ -307,6 +315,7 @@
|
|
|
307
315
|
"figure.text.clip-needs-a-name": "enter a name.",
|
|
308
316
|
"figure.text.collapse-parts-pane": "collapse the part list",
|
|
309
317
|
"figure.text.collapse-properties-pane": "collapse the property pane",
|
|
318
|
+
"figure.text.contract-not-compared": "Could not check whether this change breaks what is already placed — {why}",
|
|
310
319
|
"figure.text.coordinate-ground-explained": "draws the origin and a grid on the floor",
|
|
311
320
|
"figure.text.delete-figure-confirm": "delete {name}? drawings already using this type will no longer find it.",
|
|
312
321
|
"figure.text.designate-in-properties": "nothing designated yet. select a part and set its role in the properties tab.",
|
|
@@ -323,6 +332,7 @@
|
|
|
323
332
|
"figure.text.figure-type-example": "e.g. OHT, AGV_LIFTER",
|
|
324
333
|
"figure.text.figure-type-placeholder": "Figure type identifier (e.g. OHT)",
|
|
325
334
|
"figure.text.find-by-this-tag": "find by this tag",
|
|
335
|
+
"figure.text.fit-base-explained": "Shrinks the box to the smallest one holding every part. The centre stays put, so a part sitting off-centre leaves room on the other side.",
|
|
326
336
|
"figure.text.fits-n-instances": "up to {n}",
|
|
327
337
|
"figure.text.floor-explained": "overlaps the coordinate ground, so it is usually off",
|
|
328
338
|
"figure.text.follow-camera-explained": "turn off to fix the shadow direction",
|
|
@@ -333,7 +343,7 @@
|
|
|
333
343
|
"figure.text.hemisphere-light-explained": "even light from sky and ground. it brightens shaded faces.",
|
|
334
344
|
"figure.text.hide-all-parts": "hide all",
|
|
335
345
|
"figure.text.hide-this-part": "hide this part while you work. not saved",
|
|
336
|
-
"figure.text.how-many-fit": "how many
|
|
346
|
+
"figure.text.how-many-fit": "how many can be on screen before it slows down",
|
|
337
347
|
"figure.text.instances-unit": "instances",
|
|
338
348
|
"figure.text.interpolation-explained": "how the gap between keys is filled.",
|
|
339
349
|
"figure.text.joint-channel-value-deg": "key values are joint angles in degrees.",
|
|
@@ -392,6 +402,7 @@
|
|
|
392
402
|
"figure.text.picked-changes-do-not-validate": "the selected changes do not make a valid figure",
|
|
393
403
|
"figure.text.pivot-defaults-to-the-part-centre": "left unset, the part turns about its own centre. coordinates are local to the part.",
|
|
394
404
|
"figure.text.placement-explained": "sets the anchor plane in 3D scene (floor: AGV/conveyor, ceiling: OHT/hoist, center: sensor/drone).",
|
|
405
|
+
"figure.text.pose-as-default-explained": "Writes the pose you have set as the parameters’ defaults, so the figure stands like this when nobody gives it a value.",
|
|
395
406
|
"figure.text.preview-blocked-by-errors": "cannot show the preview because of format errors. see the maturity tab for the reason.",
|
|
396
407
|
"figure.text.preview-needs-a-part": "add a part to see how it looks placed on a drawing.",
|
|
397
408
|
"figure.text.proposal-feedback-note": "optional note for the next suggestion (this session only)",
|