@things-factory/figure-ui 10.1.47 → 10.1.49
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/figure-v3-parts.ts +86 -11
- package/client/modeller/side-panel-styles.ts +8 -0
- package/client/modeller/v3-canvas.ts +62 -8
- package/client/modeller/v3-editor.ts +196 -24
- package/client/modeller/v3-fields.ts +36 -0
- package/client/modeller/v3-joint-fan.ts +292 -0
- package/client/modeller/v3-links.ts +8 -0
- package/client/modeller/v3-stage-model.ts +24 -0
- package/client/modeller/v3-thumbnail.ts +8 -4
- package/dist-client/modeller/figure-v3-capability.d.ts +1 -1
- package/dist-client/modeller/figure-v3-clips.d.ts +1 -1
- package/dist-client/modeller/figure-v3-controls.d.ts +1 -1
- package/dist-client/modeller/figure-v3-links.d.ts +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +13 -0
- package/dist-client/modeller/figure-v3-parts.js +81 -11
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-properties.d.ts +1 -1
- package/dist-client/modeller/side-panel-styles.js +8 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -1
- package/dist-client/modeller/v3-canvas.d.ts +17 -0
- package/dist-client/modeller/v3-canvas.js +72 -8
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.d.ts +34 -0
- package/dist-client/modeller/v3-editor.js +200 -22
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +15 -0
- package/dist-client/modeller/v3-fields.js +35 -0
- package/dist-client/modeller/v3-fields.js.map +1 -1
- package/dist-client/modeller/v3-joint-fan.d.ts +9 -0
- package/dist-client/modeller/v3-joint-fan.js +264 -0
- package/dist-client/modeller/v3-joint-fan.js.map +1 -0
- package/dist-client/modeller/v3-links.d.ts +5 -0
- package/dist-client/modeller/v3-links.js +7 -0
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-stage-model.d.ts +8 -0
- package/dist-client/modeller/v3-stage-model.js +26 -0
- package/dist-client/modeller/v3-stage-model.js.map +1 -1
- package/dist-client/modeller/v3-thumbnail.js +6 -3
- package/dist-client/modeller/v3-thumbnail.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/v3-fields.test.ts +46 -1
- package/test/v3-gizmo.test.ts +3 -1
- package/test/v3-joint-fan.test.ts +210 -0
- package/test/v3-links.test.ts +13 -1
- package/test/v3-maturity.test.ts +1 -1
- package/test/v3-palette-icons.test.ts +22 -0
- package/test/v3-thumbnail.test.ts +29 -0
- package/translations/en.json +17 -2
- package/translations/ja.json +17 -2
- package/translations/ko.json +17 -2
- package/translations/ms.json +17 -2
- package/translations/zh.json +17 -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.49",
|
|
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.55",
|
|
30
|
+
"@hatiolab/things-scene": "^10.1.33",
|
|
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.49",
|
|
41
41
|
"three": "^0.185.1"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "f9b29dcbdfecbfdd72e33bbcf74aea05331da75f"
|
|
44
44
|
}
|
package/test/v3-fields.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ import assert from 'node:assert/strict'
|
|
|
7
7
|
import { readFileSync } from 'node:fs'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
|
|
10
|
-
import { groupFields, partLabelOf } from '../client/modeller/v3-fields.ts'
|
|
10
|
+
import { groupFields, ownerOf, partLabelOf, repeatOf } from '../client/modeller/v3-fields.ts'
|
|
11
11
|
|
|
12
12
|
const f = (id: string, value = 1, unit = 'mm') => ({ id, unit, value, role: 'design' })
|
|
13
13
|
|
|
@@ -60,3 +60,48 @@ test('되풀이한 부품은 되풀이의 이름과 몇 번째인지로 읽는
|
|
|
60
60
|
assert.ok(parts.includes('>${partLabelOf(part.id)}'), '목록이 이 이름으로 그린다')
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
+
test('되풀이한 부품의 속성 이름 칸도 사람의 이름이고, 몇 개 중 몇 번째인지 말한다 — 날 id 를 보이지 않는다', () => {
|
|
64
|
+
const geometry = Array.from({ length: 16 }, (_, i) => ({ id: JSON.stringify(['roller.repeat', i, 'roller.value']) })).concat([{ id: 'deck' }])
|
|
65
|
+
assert.deepEqual(repeatOf('["roller.repeat",14,"roller.value"]', geometry), { name: 'roller', count: 16, index: 15 })
|
|
66
|
+
assert.equal(repeatOf('deck', geometry), null)
|
|
67
|
+
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/v3-editor.ts', import.meta.url)), 'utf8')
|
|
68
|
+
assert.ok(editor.includes('.value=${partLabelOf(this.selected)}'), '사용자 2026-09-24: 「부품 이름 ???」')
|
|
69
|
+
assert.ok(editor.includes('?disabled=${unavailable || !!repeatOf(this.selected, view.geometry)}'), '복사본에는 바꿀 제 이름이 없다')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('편집기는 저작한 부품 단위로 고르고 보인다 — 되풀이는 한 부품, 캔버스에서는 복사본 모두가 골라진다', () => {
|
|
73
|
+
assert.equal(ownerOf('["roller.repeat",14,"roller.value"]'), 'roller')
|
|
74
|
+
assert.equal(ownerOf('deck'), 'deck')
|
|
75
|
+
assert.deepEqual(repeatOf('roller', [{ id: '["roller.repeat",0,"roller.value"]' }, { id: '["roller.repeat",1,"roller.value"]' }, { id: 'deck' }]), { name: 'roller', count: 2, index: 0 })
|
|
76
|
+
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
77
|
+
const canvas = read('../client/modeller/v3-canvas.ts')
|
|
78
|
+
assert.ok(canvas.includes('.filter(([name]) => ownerOf(name) === this.selected)'), '사용자 2026-09-24: 「why repeatable parts are not selected?」')
|
|
79
|
+
const editor = read('../client/modeller/v3-editor.ts')
|
|
80
|
+
assert.ok(editor.includes('this.selected = id ? ownerOf(id) : id'), '캔버스에서 복사본을 누르면 그 되풀이를 고른다')
|
|
81
|
+
assert.ok(editor.includes('.parts=${this.partRows(view.geometry)}'), '목록은 저작한 부품 한 줄씩')
|
|
82
|
+
assert.ok(editor.includes("this.session!.command({ kind: 'set-repeat', part: this.selected, pitch })"), 'pitch 는 명령으로 정한다')
|
|
83
|
+
const parts = read('../client/modeller/figure-v3-parts.ts')
|
|
84
|
+
assert.ok(parts.includes('?disabled=${this.locked || full}'), '등급 한도에 이르면 팔레트가 잠긴다(V2)')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('pitch 는 한 곳에서 하나의 값이고, 늘 set-repeat 로 바꿔 최대 길이가 따라온다', () => {
|
|
88
|
+
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/v3-editor.ts', import.meta.url)), 'utf8')
|
|
89
|
+
assert.ok(editor.includes("groupFields(own, this.selected)"), '사용자 2026-09-24: 되풀이의 간격과 형상 치수의 repeat · pitch 가 같은 값으로 두 번 떴다')
|
|
90
|
+
assert.ok(editor.includes("!i.id.startsWith(this.selected + '.repeat.')"))
|
|
91
|
+
const pitch = editor.slice(editor.indexOf(' private setPitch('), editor.indexOf(' /** Choose a part,'))
|
|
92
|
+
assert.ok(pitch.includes("this.session!.command({ kind: 'set-repeat', part: this.selected, pitch })"), '늘 명령으로 — 최대 길이가 따라온다')
|
|
93
|
+
assert.ok(!pitch.includes('changeField'))
|
|
94
|
+
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
|
|
95
|
+
const table = JSON.parse(readFileSync(fileURLToPath(new URL(`../translations/${lang}.json`, import.meta.url)), 'utf8'))
|
|
96
|
+
assert.equal(table['figure.label.repeat-gap'], 'pitch', lang)
|
|
97
|
+
assert.ok(['{length}', '{count}', '{longest}', '{limit}'].every(k => table['figure.text.repeat-what-pitch-gives']?.includes(k)), lang)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('기즈모로 끈 변환 부품은 규칙을 지킨 채 옮겨지고, 관절·붙이기가 정하는 부품은 까닭을 말한다 — 튕겨 돌아가지 않는다', () => {
|
|
102
|
+
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/v3-editor.ts', import.meta.url)), 'utf8')
|
|
103
|
+
const moved = editor.slice(editor.indexOf(' private transformed('), editor.indexOf(' /** 맞추기'))
|
|
104
|
+
assert.ok(moved.includes("this.session!.command({ kind: 'nudge-part', part: id,"), '사용자 2026-09-24: 「기즈모 컨트롤도 전혀 작동이 안되는군」')
|
|
105
|
+
assert.ok(moved.includes("'figure.text.gizmo-joint-places'") && moved.includes("'figure.text.gizmo-fastening-places'") && moved.includes("'figure.text.gizmo-size-worked-out'"))
|
|
106
|
+
})
|
|
107
|
+
|
package/test/v3-gizmo.test.ts
CHANGED
|
@@ -78,7 +78,9 @@ test('끈 것은 세션의 한 걸음이다 — 한 번의 되돌리기로 돌
|
|
|
78
78
|
const workspace = createV3EditorWorkspace() as unknown as Record<string, Function>
|
|
79
79
|
assert.ok(typeof workspace.apply === 'function')
|
|
80
80
|
const editor = read('../client/modeller/v3-editor.ts')
|
|
81
|
-
|
|
81
|
+
const moved = editor.slice(editor.indexOf(' private transformed('), editor.indexOf(' /** 맞추기'))
|
|
82
|
+
assert.ok(moved.includes('this.run(() => this.session!.apply(changes))'), 'one apply, one step')
|
|
83
|
+
assert.ok(moved.includes("this.session!.command({ kind: 'nudge-part'"), 'a nudge is one command, one step')
|
|
82
84
|
})
|
|
83
85
|
|
|
84
86
|
test('캔버스 — 기즈모 · W E R · 스냅은 씬의 편집 모드가 한다, V2 처럼', () => {
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **고른 부품이 어디서 얼마나 도는지** (사용자 2026-09-24: 「로봇암은 편집기에서 원래는 부품을 선택했을 때, 가변 각도가
|
|
3
|
+
* 표현이 되었었는데」). V2 는 관절의 가동 범위를 부채꼴로 저작면에 그렸다. 부채꼴의 두 끝이 제어값의 최솟값과 최댓값에서
|
|
4
|
+
* 부품이 실제로 가는 쪽을 가리키는지를 — 평가해서 — 본다.
|
|
5
|
+
*/
|
|
6
|
+
import { test } from 'node:test'
|
|
7
|
+
import assert from 'node:assert/strict'
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
import { addV3Box, applyV3Authoring, compileV3Asset, convertV2ToV3, createV3Asset } from '@hatiolab/figure-model'
|
|
12
|
+
import type { V3Asset } from '@hatiolab/figure-model'
|
|
13
|
+
|
|
14
|
+
import { JOINT_FAN, jointFanOf } from '../client/modeller/v3-joint-fan.ts'
|
|
15
|
+
|
|
16
|
+
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
17
|
+
|
|
18
|
+
function arm(): V3Asset {
|
|
19
|
+
let asset = createV3Asset('Probe')
|
|
20
|
+
asset = addV3Box(asset, { id: 'base', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
|
|
21
|
+
asset = addV3Box(asset, { id: 'arm', size: { x: 400, y: 50, z: 50 }, position: { x: 200, y: 125, z: 0 } })
|
|
22
|
+
return applyV3Authoring(asset, { kind: 'add-motion', part: 'arm', motion: { kind: 'turn', axis: 'y' }, state: { id: 'arm.turn', unit: 'deg', min: -45, max: 90 } })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The fan's profile point in the figure's frame. The polygon lies in its local X–Z: profile (x, y) is local (x, 0, y). */
|
|
26
|
+
const world = (fan: { pose: { t: number[]; r: number[][] } }, p: { x: number; y: number }) =>
|
|
27
|
+
[0, 1, 2].map(i => fan.pose.t[i]! + fan.pose.r[i]![0]! * p.x + fan.pose.r[i]![2]! * p.y)
|
|
28
|
+
|
|
29
|
+
const direction = (v: number[]) => {
|
|
30
|
+
const l = Math.hypot(v[0]!, v[2]!)
|
|
31
|
+
return [v[0]! / l, v[2]! / l]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('a part that turns gets a fan from the lowest value to the highest, where the part really goes', () => {
|
|
35
|
+
const asset = arm()
|
|
36
|
+
const fan = jointFanOf(asset, 'arm') as unknown as { id: string; shape: { provider: string; profile: { x: number; y: number }[] }; pose: { t: number[]; r: number[][] } }
|
|
37
|
+
assert.ok(fan)
|
|
38
|
+
assert.equal(fan.id, JOINT_FAN)
|
|
39
|
+
assert.equal(fan.shape.provider, 'polygon')
|
|
40
|
+
const profile = fan.shape.profile
|
|
41
|
+
assert.deepEqual(profile[0], { x: 0, y: 0 }, 'not a full turn: the fan starts at the pivot')
|
|
42
|
+
|
|
43
|
+
const armAt = (deg: number) => {
|
|
44
|
+
const body = compileV3Asset({ ...asset, stateDefaults: { ...asset.stateDefaults, 'arm.turn': deg } }).evaluate().geometry.find(g => g.id === 'arm')!
|
|
45
|
+
return direction((body as unknown as { pose: { t: number[] } }).pose.t)
|
|
46
|
+
}
|
|
47
|
+
const first = direction(world(fan, profile[1]!))
|
|
48
|
+
const last = direction(world(fan, profile[profile.length - 1]!))
|
|
49
|
+
for (const [got, want] of [
|
|
50
|
+
[first, armAt(-45)],
|
|
51
|
+
[last, armAt(90)]
|
|
52
|
+
] as const) {
|
|
53
|
+
assert.ok(Math.abs(got[0]! - want[0]!) < 1e-6 && Math.abs(got[1]! - want[1]!) < 1e-6, `${got} ≈ ${want}`)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('a fan is as large as what turns, and never smaller than 60 mm', () => {
|
|
58
|
+
const big = jointFanOf(arm(), 'arm') as unknown as { shape: { profile: { x: number; y: number }[] } }
|
|
59
|
+
const radius = Math.max(...big.shape.profile.map(p => Math.hypot(p.x, p.y)))
|
|
60
|
+
assert.ok(Math.abs(radius - 240) < 1e-3, `0.6 × 400 — ${radius}`)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('no fan for a part that does not turn, nor for a slide -- a fan in a guessed plane would point the wrong way', () => {
|
|
64
|
+
assert.equal(jointFanOf(arm(), 'base'), null)
|
|
65
|
+
assert.equal(jointFanOf(arm(), ''), null)
|
|
66
|
+
let slider = createV3Asset('Probe')
|
|
67
|
+
slider = addV3Box(slider, { id: 'block', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
68
|
+
slider = applyV3Authoring(slider, { kind: 'add-motion', part: 'block', motion: { kind: 'slide', axis: 'x' }, state: { id: 'shift', unit: 'mm', min: 0, max: 60 } })
|
|
69
|
+
assert.equal(jointFanOf(slider, 'block'), null)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('the editor draws it for the selected part, with the pose in hand; the canvas never lets it be picked', () => {
|
|
73
|
+
const editor = read('../client/modeller/v3-editor.ts')
|
|
74
|
+
assert.ok(editor.includes('.fan=${this.fanOf(asset as V3Asset | undefined)}'))
|
|
75
|
+
assert.ok(editor.includes('return jointFanOf(asset, this.selected, this.live.values)'))
|
|
76
|
+
const canvas = read('../client/modeller/v3-canvas.ts')
|
|
77
|
+
assert.ok(canvas.includes('{ ...fan, capturable: false, locked: true }'))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
/* ── Joints the converter wrote (user 2026-09-24: 「로봇암 axis와 관절의 각도 표현을 다시 검증해보라」) ───────── */
|
|
81
|
+
|
|
82
|
+
/** A V2 column: a link on a hinge at the root, a tip on a wrist inside it -- a first joint and a nested one. */
|
|
83
|
+
function column(): V3Asset {
|
|
84
|
+
const source = {
|
|
85
|
+
version: 2,
|
|
86
|
+
type: 'COLUMN',
|
|
87
|
+
base: { x: 200, y: 700, z: 200 },
|
|
88
|
+
parts: [
|
|
89
|
+
{ name: 'base', primitive: 'cube', transform: { position: { x: 0, y: 50, z: 0 }, size: { x: 200, y: 100, z: 200 } }, material: { token: 'palette.dark' } },
|
|
90
|
+
{ name: 'link', primitive: 'cube', transform: { position: { x: 0, y: 300, z: 0 }, size: { x: 80, y: 400, z: 80 } }, material: { token: 'palette.primary' }, parent: 'base' },
|
|
91
|
+
{ name: 'tip', primitive: 'cube', transform: { position: { x: 0, y: 600, z: 0 }, size: { x: 60, y: 200, z: 60 } }, material: { token: 'palette.primary' }, parent: 'link' },
|
|
92
|
+
/* Rides the hinge, centred on the wrist's axis -- the wrist's housing, as ROBOT_ARM's elbow-housing is the elbow's. */
|
|
93
|
+
{ name: 'housing', primitive: 'cube', transform: { position: { x: 0, y: 500, z: 0 }, size: { x: 100, y: 100, z: 100 } }, material: { token: 'palette.dark' }, parent: 'link' },
|
|
94
|
+
/* Rides the hinge and holds no pivot. */
|
|
95
|
+
{ name: 'badge', primitive: 'cube', transform: { position: { x: 60, y: 300, z: 0 }, size: { x: 10, y: 40, z: 40 } }, material: { token: 'palette.dark' }, parent: 'link' }
|
|
96
|
+
],
|
|
97
|
+
joints: [
|
|
98
|
+
{ name: 'hinge', child: 'link', type: 'revolute', origin: { x: 0, y: 100, z: 0 }, axis: { x: 1, y: 0, z: 0 }, limits: { min: -90, max: 60 } },
|
|
99
|
+
{ name: 'wrist', child: 'tip', type: 'revolute', origin: { x: 0, y: 500, z: 0 }, axis: { x: 0, y: 0, z: 1 }, limits: { min: -120, max: 120 } }
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
const result = convertV2ToV3(source, { palette: { 'palette.dark': '#333333', 'palette.primary': '#3366cc' } } as never)
|
|
103
|
+
assert.equal(result.status, 'converted', JSON.stringify((result as { refusals?: unknown }).refusals))
|
|
104
|
+
return (result as unknown as { asset: V3Asset }).asset
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const sub = (a: number[], b: number[]) => a.map((v, i) => v - b[i]!)
|
|
108
|
+
const dot = (a: number[], b: number[]) => a.reduce((s, v, i) => s + v * b[i]!, 0)
|
|
109
|
+
const angle = (a: number[], b: number[]) => (Math.acos(Math.max(-1, Math.min(1, dot(a, b) / (Math.hypot(...a) * Math.hypot(...b))))) * 180) / Math.PI
|
|
110
|
+
const turnOf = (r1: number[][], r2: number[][]) => {
|
|
111
|
+
let trace = 0
|
|
112
|
+
for (let i = 0; i < 3; i++) for (let k = 0; k < 3; k++) trace += r1[k]![i]! * r2[k]![i]!
|
|
113
|
+
return (Math.acos(Math.max(-1, Math.min(1, (trace - 1) / 2))) * 180) / Math.PI
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
Checked against the motion, not against the fan's own sampling: where the part's centre really is at the control's
|
|
118
|
+
ends, and how far its orientation really turns, summed in small steps.
|
|
119
|
+
*/
|
|
120
|
+
for (const [part, joint] of [
|
|
121
|
+
['link', 'hinge'],
|
|
122
|
+
['tip', 'wrist']
|
|
123
|
+
] as const) {
|
|
124
|
+
test(`${part} on ${joint}: the axis holds the part's plane, the fan's ends are where it goes, the fan opens as far as it turns`, () => {
|
|
125
|
+
const asset = column()
|
|
126
|
+
const fan = jointFanOf(asset, part) as unknown as { pose: { t: number[]; r: number[][] }; shape: { profile: { x: number; y: number }[] } }
|
|
127
|
+
assert.ok(fan, 'a fan -- the first joint of a chain included')
|
|
128
|
+
const input = asset.document.model.inputs.find(i => i.id === joint)!
|
|
129
|
+
const pivot = fan.pose.t
|
|
130
|
+
const axis = [0, 1, 2].map(i => fan.pose.r[i]![1]!)
|
|
131
|
+
const at = (s: number) =>
|
|
132
|
+
compileV3Asset({ ...asset, stateDefaults: { ...asset.stateDefaults, [joint]: s } }).evaluate().geometry.find(g => g.id === part) as unknown as { pose: { t: number[]; r: number[][] } }
|
|
133
|
+
const onFan = (p: { x: number; y: number }) => [0, 1, 2].map(i => pivot[i]! + fan.pose.r[i]![0]! * p.x + fan.pose.r[i]![2]! * p.y)
|
|
134
|
+
const flat = (v: number[]) => sub(v, axis.map(a => a * dot(v, axis)))
|
|
135
|
+
|
|
136
|
+
const axial = [input.min!, 0, input.max!].map(s => dot(sub(at(s).pose.t, pivot), axis))
|
|
137
|
+
assert.ok(Math.max(...axial) - Math.min(...axial) < 1e-6, `the part stays in the fan's plane: ${axial}`)
|
|
138
|
+
|
|
139
|
+
const edges = fan.shape.profile.filter(p => p.x !== 0 || p.y !== 0)
|
|
140
|
+
assert.ok(angle(flat(sub(onFan(edges[0]!), pivot)), flat(sub(at(input.min!).pose.t, pivot))) < 0.01, 'the first edge is where the part is at the lowest value (the profile is kept to a micrometre)')
|
|
141
|
+
assert.ok(angle(flat(sub(onFan(edges[edges.length - 1]!), pivot)), flat(sub(at(input.max!).pose.t, pivot))) < 0.01, 'the last edge, at the highest')
|
|
142
|
+
|
|
143
|
+
let opens = 0,
|
|
144
|
+
turns = 0
|
|
145
|
+
for (let i = 1; i < edges.length; i++) opens += angle(sub(onFan(edges[i - 1]!), pivot), sub(onFan(edges[i]!), pivot))
|
|
146
|
+
for (let i = 1; i <= 60; i++) {
|
|
147
|
+
const s = (k: number) => input.min! + ((input.max! - input.min!) * k) / 60
|
|
148
|
+
turns += turnOf(at(s(i - 1)).pose.r, at(s(i)).pose.r)
|
|
149
|
+
}
|
|
150
|
+
assert.ok(Math.abs(opens - turns) < 0.01 && Math.abs(turns - (input.max! - input.min!)) < 0.01, `opens ${opens}, turns ${turns}`)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
test('a housing shows the joint it houses, not the one that carries it; a part that holds no pivot shows none', () => {
|
|
155
|
+
const asset = column()
|
|
156
|
+
const turns = (part: string) => (jointFanOf(asset, part) as unknown as { turns?: string } | null)?.turns ?? null
|
|
157
|
+
assert.equal(turns('housing'), 'wrist', 'user 2026-09-24: 「elbow-housing itself is a axis」 -- it rides the hinge, it houses the wrist')
|
|
158
|
+
assert.equal(turns('link'), 'hinge', 'a link with pivots only at its ends shows the joint that turns it')
|
|
159
|
+
assert.equal(turns('tip'), 'wrist')
|
|
160
|
+
assert.equal(turns('badge'), null)
|
|
161
|
+
assert.equal(turns('base'), null)
|
|
162
|
+
|
|
163
|
+
/* The housing's fan is the wrist's: its ends are where the tip goes. */
|
|
164
|
+
const fan = jointFanOf(asset, 'housing') as unknown as { pose: { t: number[]; r: number[][] }; shape: { profile: { x: number; y: number }[] } }
|
|
165
|
+
const axis = [0, 1, 2].map(i => fan.pose.r[i]![1]!)
|
|
166
|
+
const flat = (v: number[]) => sub(v, axis.map(a => a * dot(v, axis)))
|
|
167
|
+
const onFan = (p: { x: number; y: number }) => [0, 1, 2].map(i => fan.pose.t[i]! + fan.pose.r[i]![0]! * p.x + fan.pose.r[i]![2]! * p.y)
|
|
168
|
+
const tipAt = (s: number) => (compileV3Asset({ ...asset, stateDefaults: { ...asset.stateDefaults, wrist: s } }).evaluate().geometry.find(g => g.id === 'tip') as unknown as { pose: { t: number[] } }).pose.t
|
|
169
|
+
const edges = fan.shape.profile.filter(p => p.x !== 0 || p.y !== 0)
|
|
170
|
+
assert.ok(angle(flat(sub(onFan(edges[0]!), fan.pose.t)), flat(sub(tipAt(-120), fan.pose.t))) < 0.01)
|
|
171
|
+
assert.ok(angle(flat(sub(onFan(edges[edges.length - 1]!), fan.pose.t)), flat(sub(tipAt(120), fan.pose.t))) < 0.01)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('one joint, one fan: the housing and the part it turns show the same fan, sized from what turns', () => {
|
|
175
|
+
const asset = column()
|
|
176
|
+
const size = (part: string) => {
|
|
177
|
+
const fan = jointFanOf(asset, part) as unknown as { shape: { profile: { x: number; y: number }[] }; pose: { t: number[] } }
|
|
178
|
+
return { radius: Math.max(...fan.shape.profile.map(p => Math.hypot(p.x, p.y))), pivot: fan.pose.t }
|
|
179
|
+
}
|
|
180
|
+
const housing = size('housing'),
|
|
181
|
+
tip = size('tip')
|
|
182
|
+
assert.ok(Math.abs(housing.radius - tip.radius) < 1e-6, `user 2026-09-24: the elbow housing's fan was smaller than the forearm's -- ${housing.radius} vs ${tip.radius}`)
|
|
183
|
+
assert.deepEqual(housing.pivot, tip.pivot)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('an axis running through a part does not make it that joint’s: a roll along a link shows on the roller, not the link', () => {
|
|
187
|
+
/* ROBOT_ARM's forearm showed the wrist roll, whose axis runs its length (2026-09-24 audit). */
|
|
188
|
+
const source = {
|
|
189
|
+
version: 2,
|
|
190
|
+
type: 'ROLLER',
|
|
191
|
+
base: { x: 200, y: 800, z: 200 },
|
|
192
|
+
parts: [
|
|
193
|
+
{ name: 'base', primitive: 'cube', transform: { position: { x: 0, y: 50, z: 0 }, size: { x: 200, y: 100, z: 200 } }, material: { token: 'palette.dark' } },
|
|
194
|
+
{ name: 'link', primitive: 'cube', transform: { position: { x: 0, y: 300, z: 0 }, size: { x: 80, y: 400, z: 80 } }, material: { token: 'palette.primary' }, parent: 'base' },
|
|
195
|
+
{ name: 'roller', primitive: 'cube', transform: { position: { x: 30, y: 600, z: 0 }, size: { x: 120, y: 200, z: 60 } }, material: { token: 'palette.primary' }, parent: 'link' }
|
|
196
|
+
],
|
|
197
|
+
joints: [
|
|
198
|
+
{ name: 'hinge', child: 'link', type: 'revolute', origin: { x: 0, y: 100, z: 0 }, axis: { x: 1, y: 0, z: 0 }, limits: { min: -90, max: 60 } },
|
|
199
|
+
{ name: 'twist', child: 'roller', type: 'revolute', origin: { x: 0, y: 500, z: 0 }, axis: { x: 0, y: 1, z: 0 }, limits: { min: -170, max: 170 } }
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
const result = convertV2ToV3(source, { palette: { 'palette.dark': '#333333', 'palette.primary': '#3366cc' } } as never)
|
|
203
|
+
assert.equal(result.status, 'converted')
|
|
204
|
+
const asset = (result as unknown as { asset: V3Asset }).asset
|
|
205
|
+
const turns = (part: string) => (jointFanOf(asset, part) as unknown as { turns?: string } | null)?.turns ?? null
|
|
206
|
+
assert.equal(turns('link'), 'hinge', 'the twist axis runs through the link, and the link does not twist')
|
|
207
|
+
assert.equal(turns('roller'), 'twist')
|
|
208
|
+
assert.equal(turns('base'), null)
|
|
209
|
+
})
|
|
210
|
+
|
package/test/v3-links.test.ts
CHANGED
|
@@ -10,7 +10,7 @@ import assert from 'node:assert/strict'
|
|
|
10
10
|
import { addV3Box, addV3Primitive, applyV3Authoring, createV3Asset } from '@hatiolab/figure-model'
|
|
11
11
|
import type { V3Asset } from '@hatiolab/figure-model'
|
|
12
12
|
|
|
13
|
-
import { acceptsOf, canFasten, canFollow, dimensionNow, instanceRangeOf, rangeDiffers, fastenedTo, followableAxes, followOf, hasInstanceSize, positionIsShare, holdsThings, motionOf, placeFacesOf, placeIdFor, placesOn, positionsWorkedOut } from '../client/modeller/v3-links.ts'
|
|
13
|
+
import { acceptsOf, canFasten, canFollow, dimensionNow, instanceRangeOf, rangeDiffers, turnsAboutAJoint, fastenedTo, followableAxes, followOf, hasInstanceSize, positionIsShare, holdsThings, motionOf, placeFacesOf, placeIdFor, placesOn, positionsWorkedOut } from '../client/modeller/v3-links.ts'
|
|
14
14
|
|
|
15
15
|
const pair = (): V3Asset => {
|
|
16
16
|
let asset = createV3Asset('Pair')
|
|
@@ -232,3 +232,15 @@ test('놓일 수 있는 크기 — 남은 만큼 채우던 부품을 비례로
|
|
|
232
232
|
assert.ok(editor.includes("this.linkCommand({ kind: 'declare-instance-size', range: range.measured, replace: true })"))
|
|
233
233
|
})
|
|
234
234
|
|
|
235
|
+
test('관절이 있는 도형에는 한 비율로 키우기를 권하고, 선언하면 규칙으로 보인다(수석 아키텍트 판정 09-24)', () => {
|
|
236
|
+
let asset = sizedPair()
|
|
237
|
+
assert.equal(turnsAboutAJoint(asset), false)
|
|
238
|
+
asset = applyV3Authoring(asset, { kind: 'add-motion', part: 'flange', motion: { kind: 'turn', axis: 'y' }, state: { id: 'flange.turn', unit: 'deg', min: 0, max: 90 } })
|
|
239
|
+
assert.equal(turnsAboutAJoint(asset), true)
|
|
240
|
+
const editor = read('../client/modeller/v3-editor.ts')
|
|
241
|
+
const rule = editor.slice(editor.indexOf(' private renderSizingRule('), editor.indexOf(' /** Choose a part,'))
|
|
242
|
+
assert.ok(rule.includes("asset.sizing === 'uniform'") && rule.includes("'figure.label.sizing-uniform'"), '선언하면 규칙을 말한다')
|
|
243
|
+
assert.ok(rule.includes("turnsAboutAJoint(asset) ? html`<p hint>${i18next.t('figure.text.sizing-uniform-proposed')}"), '관절이 있으면 권한다')
|
|
244
|
+
assert.ok(rule.includes("this.linkCommand({ kind: 'scale-uniformly' })"), '누르면 선언한다 — 사람이 정한다')
|
|
245
|
+
})
|
|
246
|
+
|
package/test/v3-maturity.test.ts
CHANGED
|
@@ -66,7 +66,7 @@ test('등급은 figure-model 의 v3ScoreOf 그대로다 — 목록과 이 탭이
|
|
|
66
66
|
asset = addV3Box(asset, cube('a', 0))
|
|
67
67
|
const one = v3ReportOf(asset)!
|
|
68
68
|
assert.deepEqual(one.score, v3ScoreOf(asset))
|
|
69
|
-
assert.deepEqual(V3_BUDGET_KEYS, one.score.budgets.map(b => b.key), '화면이 말을 찾는 키와 셈의 키가 같다')
|
|
69
|
+
assert.deepEqual(V3_BUDGET_KEYS.filter(k => k !== 'parts'), one.score.budgets.map(b => b.key), '화면이 말을 찾는 키와 셈의 키가 같다 — 등급을 안 정하면 부품 수 한도는 없다')
|
|
70
70
|
})
|
|
71
71
|
|
|
72
72
|
test('draw call 한도 3 은 없다 — 한 화면 166 대 목표에 견준다(수석 아키텍트 판정 09-24)', () => {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **팔레트 아이콘은 글꼴에 있는 이름만** (사용자 2026-09-24: 「is this icon proper?」 — 벽 칸에 「AL」이 떴다).
|
|
3
|
+
*
|
|
4
|
+
* `md-icon` 은 이름을 합자로 그린다. 글꼴에 없는 이름은 글자 그대로 나오고, 칸에 잘려 「wall」이 「AL」로 보였다.
|
|
5
|
+
* node 에서는 글꼴을 볼 수 없어, :3300 의 글꼴에서 한 칸 폭(24px)으로 그려지는 것을 잰 이름만 여기에 둔다. 새 아이콘은
|
|
6
|
+
* 화면에서 재어 보고 이 목록에 더한다.
|
|
7
|
+
*/
|
|
8
|
+
import { test } from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
/** Measured in the app's icon font on :3300, 2026-09-24: each draws one cell wide. `wall` drew 52px -- its letters. */
|
|
14
|
+
const RENDERS = new Set(['deployed_code', 'database', 'crop_square', 'circle', 'change_history', 'hexagon', 'check_box_outline_blank', 'crop_portrait', 'door_front'])
|
|
15
|
+
|
|
16
|
+
test('every shape in the palette has an icon the font draws', () => {
|
|
17
|
+
const parts = readFileSync(fileURLToPath(new URL('../client/modeller/figure-v3-parts.ts', import.meta.url)), 'utf8')
|
|
18
|
+
const recipes = parts.slice(parts.indexOf('const RECIPES'), parts.indexOf(']', parts.indexOf('const RECIPES')))
|
|
19
|
+
const icons = [...recipes.matchAll(/icon: '([a-z_]+)'/g)].map(m => m[1]!)
|
|
20
|
+
assert.ok(icons.length >= 9)
|
|
21
|
+
assert.deepEqual(icons.filter(icon => !RENDERS.has(icon)), [])
|
|
22
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **저장하면 카드 그림이 남는다** (사용자 2026-09-24: 「why you don't save thumbnail when I save the figure?」).
|
|
3
|
+
*
|
|
4
|
+
* 캔버스가 씬 위로 옮겨 갈 때 찍는 길(`snapshot`)이 빠졌고, 찍는 쪽은 없는 메서드를 `?.()` 로 불러 아무 말 없이
|
|
5
|
+
* 그림 없이 저장했다. 브라우저에서만 찍히므로 여기서는 길이 이어져 있는지를 본다: 찍는 쪽이 보기 전용 캔버스를 세워
|
|
6
|
+
* `snapshot` 을 부르고, 캔버스는 씬의 3D 그림(`snapshot3d`)을 내며, 보기 전용이면 편집기에 아무것도 알리지 않는다.
|
|
7
|
+
*/
|
|
8
|
+
import { test } from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
14
|
+
|
|
15
|
+
test('the save takes the card picture from a view-only canvas, through the scene’s 3D snapshot', () => {
|
|
16
|
+
const shooter = read('../client/modeller/v3-thumbnail.ts')
|
|
17
|
+
assert.ok(shooter.includes('shooter.viewOnly = true'))
|
|
18
|
+
assert.ok(shooter.includes('return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined'))
|
|
19
|
+
assert.ok(!shooter.includes('thumbnail?.()'), 'no call to a method the canvas does not have')
|
|
20
|
+
|
|
21
|
+
const canvas = read('../client/modeller/v3-canvas.ts')
|
|
22
|
+
assert.ok(canvas.includes('async snapshot(width = 480, height = 360)'))
|
|
23
|
+
assert.ok(canvas.includes("await layer.snapshot3d(width, height, 'image/webp', 0.85)"), 'the 3D view, not the 2D sheet')
|
|
24
|
+
const watch = canvas.slice(canvas.indexOf(' private watchScene()'), canvas.indexOf(' private showSelected()'))
|
|
25
|
+
assert.ok(watch.includes('|| this.viewOnly) return'), 'a view-only canvas tells the editor nothing')
|
|
26
|
+
|
|
27
|
+
const editor = read('../client/modeller/v3-editor.ts')
|
|
28
|
+
assert.ok(editor.includes('const thumbnail = await captureV3Thumbnail('), 'the save asks for it')
|
|
29
|
+
})
|
package/translations/en.json
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"figure.button.fix-position": "fix here",
|
|
18
18
|
"figure.button.scale-position": "in proportion",
|
|
19
19
|
"figure.button.remeasure-range": "measure again",
|
|
20
|
+
"figure.label.sizing-rule": "when the size changes",
|
|
21
|
+
"figure.label.sizing-uniform": "one ratio (geometric mean of the three axes)",
|
|
22
|
+
"figure.button.scale-uniformly": "grow by one ratio",
|
|
23
|
+
"figure.text.sizing-uniform-proposed": "this figure has joints. growing it by one ratio keeps the joints together at any size.",
|
|
24
|
+
"figure.text.sizing-uniform-explained": "grows the whole figure by the geometric mean of the three sizes a board gives it. the feet stay on the floor; room left above the figure stays empty.",
|
|
20
25
|
"figure.button.mirror-x": "mirror X",
|
|
21
26
|
"figure.button.mirror-z": "mirror Z",
|
|
22
27
|
"figure.button.move-part-down": "move down",
|
|
@@ -53,6 +58,16 @@
|
|
|
53
58
|
"figure.text.follow-would-loop": "that dimension already follows this part, so the two cannot follow each other.",
|
|
54
59
|
"figure.text.links-need-a-box": "size following works on box-shaped parts, fastening on boxes and cylinders.",
|
|
55
60
|
"figure.text.scale-position-explained": "keeps this place as a share of the figure size — at twice the size it is twice as far.",
|
|
61
|
+
"figure.text.repeat-member": "copy {index} of {count} made by repeating {name}.",
|
|
62
|
+
"figure.label.detail-none": "none",
|
|
63
|
+
"figure.label.repeat": "repeat",
|
|
64
|
+
"figure.text.repeat-fits-length": "as many copies as fit the figure’s length at this pitch.",
|
|
65
|
+
"figure.text.repeat-fixed-count": "a repeat of a fixed count; its pitch cannot be set yet.",
|
|
66
|
+
"figure.text.repeat-what-pitch-gives": "{count} at a length of {length} mm · placed up to {longest} mm long ({limit} in a row).",
|
|
67
|
+
"figure.text.repeat-pitch-too-small": "the pitch must be at least {min} mm: at the authored length of {length} mm it would make more than {limit}.",
|
|
68
|
+
"figure.text.gizmo-joint-places": "a joint places {part}. move it with its control in the motion tab.",
|
|
69
|
+
"figure.text.gizmo-fastening-places": "{part} is fastened to a face of another part, which places it. detach it first to move it.",
|
|
70
|
+
"figure.text.gizmo-size-worked-out": "the size of {part} is worked out, so the gizmo cannot change it. set it to fixed under dimensions.",
|
|
56
71
|
"figure.text.instance-range-can-widen": "measured again after the parts changed: {ranges} mm.",
|
|
57
72
|
"figure.text.mounting-face-needs-space": "declare the space first to choose one.",
|
|
58
73
|
"figure.text.part-is-referenced": "'{part}' cannot be deleted: another part is fastened to it or follows its size. Undo that link first.",
|
|
@@ -257,6 +272,7 @@
|
|
|
257
272
|
"figure.label.released-versions": "released versions",
|
|
258
273
|
"figure.label.repeat-axis": "axis it repeats along",
|
|
259
274
|
"figure.label.repeat-count": "count",
|
|
275
|
+
"figure.label.repeat-gap": "pitch",
|
|
260
276
|
"figure.label.repeat-pitch": "pitch",
|
|
261
277
|
"figure.label.reuse-trend": "reuse trend",
|
|
262
278
|
"figure.label.instance-count": "instance count",
|
|
@@ -280,7 +296,6 @@
|
|
|
280
296
|
"figure.label.shapes": "shapes",
|
|
281
297
|
"figure.label.sizing-fixed": "keeps its size",
|
|
282
298
|
"figure.label.sizing-repeat": "repeats",
|
|
283
|
-
"figure.label.sizing-rule": "when the size changes",
|
|
284
299
|
"figure.label.sizing-scale": "scales proportionally",
|
|
285
300
|
"figure.label.sizing-stretch": "stretches on one axis",
|
|
286
301
|
"figure.label.sky-color": "sky color",
|
|
@@ -315,7 +330,7 @@
|
|
|
315
330
|
"figure.text.bound-by-triangles": "triangles reach the limit first ({per} per instance, {budget} per drawing).",
|
|
316
331
|
"figure.text.budget-measured-on": "Measured on {gpu}, {on}. A figure standing still costs nothing — only what is on screen and redrawn counts.",
|
|
317
332
|
"figure.text.budget-not-measured-yet": "this target has not been measured yet.",
|
|
318
|
-
"figure.text.budgets-not-in-v3": "off-palette surfaces
|
|
333
|
+
"figure.text.budgets-not-in-v3": "off-palette surfaces are not in the V3 format, so they are not shown. the part-count limit shows once a detail level is set.",
|
|
319
334
|
"figure.text.trend-chart-title": "drawing cost by instance count",
|
|
320
335
|
"figure.text.trend-chart-lead": "flat lines stay the same as instances grow. instances of the same size share them.",
|
|
321
336
|
"figure.text.trend-at-count": "at {n}",
|
package/translations/ja.json
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"figure.button.fix-position": "ここに固定",
|
|
18
18
|
"figure.button.scale-position": "サイズに比例",
|
|
19
19
|
"figure.button.remeasure-range": "測り直す",
|
|
20
|
+
"figure.label.sizing-rule": "when the size changes",
|
|
21
|
+
"figure.label.sizing-uniform": "一つの比率 (3 軸の幾何平均)",
|
|
22
|
+
"figure.button.scale-uniformly": "一つの比率で拡大",
|
|
23
|
+
"figure.text.sizing-uniform-proposed": "関節のある図形です。一つの比率で拡大すると、どのサイズでも関節が離れません。",
|
|
24
|
+
"figure.text.sizing-uniform-explained": "ボードが与えた 3 軸サイズの幾何平均の比率で図形全体を拡大します。足は床に置き、箱の上の余りは空けたままにします。",
|
|
20
25
|
"figure.button.mirror-x": "mirror X",
|
|
21
26
|
"figure.button.mirror-z": "mirror Z",
|
|
22
27
|
"figure.button.move-part-down": "move down",
|
|
@@ -51,6 +56,16 @@
|
|
|
51
56
|
"figure.text.follow-would-loop": "その寸法はすでにこの部品に追従しているため、互いに追従させられません。",
|
|
52
57
|
"figure.text.links-need-a-box": "サイズ追従は箱形、取り付けは箱形と円柱の部品で使えます。",
|
|
53
58
|
"figure.text.scale-position-explained": "今の位置を図形サイズの割合として保ちます。2 倍のサイズなら 2 倍の距離に置かれます。",
|
|
59
|
+
"figure.text.repeat-member": "{name} を {count} 個繰り返したうちの {index} 番目です。",
|
|
60
|
+
"figure.label.detail-none": "なし",
|
|
61
|
+
"figure.label.repeat": "繰り返し",
|
|
62
|
+
"figure.text.repeat-fits-length": "図形の長さに合わせ、この間隔で入るだけ置かれます。",
|
|
63
|
+
"figure.text.repeat-fixed-count": "個数が固定された繰り返しのため、間隔はまだ変更できません。",
|
|
64
|
+
"figure.text.repeat-what-pitch-gives": "長さ {length} mm で {count} 個 · 最大 {longest} mm まで置けます (1 列 {limit} 個)。",
|
|
65
|
+
"figure.text.repeat-pitch-too-small": "pitch は {min} mm 以上にしてください。作成した長さ {length} mm で {limit} 個を超えるためです。",
|
|
66
|
+
"figure.text.gizmo-joint-places": "{part} は関節に付いていて、関節が位置を決めます。動きタブの操作値で動かしてください。",
|
|
67
|
+
"figure.text.gizmo-fastening-places": "{part} は他の部品の面に付いていて、その面が位置を決めます。動かすには先に外してください。",
|
|
68
|
+
"figure.text.gizmo-size-worked-out": "{part} のサイズは式で決まっているため、ギズモでは変えられません。寸法欄で固定にしてください。",
|
|
54
69
|
"figure.text.instance-range-can-widen": "部品が変わったので測り直すと {ranges} mm です。",
|
|
55
70
|
"figure.text.mounting-face-needs-space": "先に空間を宣言すると選べます。",
|
|
56
71
|
"figure.text.part-is-referenced": "'{part}' には他の部品が取り付けられているか、そのサイズに従っているため削除できません。先にその連結を外してください。",
|
|
@@ -255,6 +270,7 @@
|
|
|
255
270
|
"figure.label.released-versions": "発行済みバージョン",
|
|
256
271
|
"figure.label.repeat-axis": "axis it repeats along",
|
|
257
272
|
"figure.label.repeat-count": "個数",
|
|
273
|
+
"figure.label.repeat-gap": "pitch",
|
|
258
274
|
"figure.label.repeat-pitch": "pitch",
|
|
259
275
|
"figure.label.reuse-trend": "reuse trend",
|
|
260
276
|
"figure.label.instance-count": "インスタンス数",
|
|
@@ -278,7 +294,6 @@
|
|
|
278
294
|
"figure.label.shapes": "shapes",
|
|
279
295
|
"figure.label.sizing-fixed": "keeps its size",
|
|
280
296
|
"figure.label.sizing-repeat": "repeats",
|
|
281
|
-
"figure.label.sizing-rule": "when the size changes",
|
|
282
297
|
"figure.label.sizing-scale": "scales proportionally",
|
|
283
298
|
"figure.label.sizing-stretch": "stretches on one axis",
|
|
284
299
|
"figure.label.sky-color": "sky color",
|
|
@@ -313,7 +328,7 @@
|
|
|
313
328
|
"figure.text.bound-by-triangles": "三角形が先に上限に達します (1 個あたり {per}、図面あたり {budget})。",
|
|
314
329
|
"figure.text.budget-measured-on": "{gpu} で {on} に測定した値です。静止している図形は負荷になりません — 画面に見えて再描画されるものだけを数えます。",
|
|
315
330
|
"figure.text.budget-not-measured-yet": "this target has not been measured yet.",
|
|
316
|
-
"figure.text.budgets-not-in-v3": "
|
|
331
|
+
"figure.text.budgets-not-in-v3": "パレット外の表面は V3 形式にない項目のため表示しません。部品数の上限は詳細度を決めると表示されます。",
|
|
317
332
|
"figure.text.trend-chart-title": "インスタンス数による描画コスト",
|
|
318
333
|
"figure.text.trend-chart-lead": "平らな線はインスタンスが増えても変わらない値です。同じサイズのインスタンスが共有します。",
|
|
319
334
|
"figure.text.trend-at-count": "{n}個のとき",
|
package/translations/ko.json
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"figure.button.fix-position": "여기에 고정",
|
|
18
18
|
"figure.button.scale-position": "크기에 비례",
|
|
19
19
|
"figure.button.remeasure-range": "다시 재기",
|
|
20
|
+
"figure.label.sizing-rule": "크기 변경 시 동작",
|
|
21
|
+
"figure.label.sizing-uniform": "한 비율로 (세 축의 기하평균)",
|
|
22
|
+
"figure.button.scale-uniformly": "한 비율로 키우기",
|
|
23
|
+
"figure.text.sizing-uniform-proposed": "관절이 있는 도형입니다. 한 비율로 키우면 어떤 크기에서도 관절이 벌어지지 않습니다.",
|
|
24
|
+
"figure.text.sizing-uniform-explained": "보드가 준 세 축 크기의 기하평균 비율로 도형 전체를 키웁니다. 발은 바닥에 두고, 상자 위가 남으면 그대로 비워 둡니다.",
|
|
20
25
|
"figure.button.mirror-x": "X 대칭",
|
|
21
26
|
"figure.button.mirror-z": "Z 대칭",
|
|
22
27
|
"figure.button.move-part-down": "아래로",
|
|
@@ -53,6 +58,16 @@
|
|
|
53
58
|
"figure.text.follow-would-loop": "그 치수는 이미 이 부품을 따르고 있어서 서로 따르게 할 수 없습니다.",
|
|
54
59
|
"figure.text.links-need-a-box": "크기 따르기는 상자 모양, 붙이기는 상자·원기둥 부품에서 씁니다.",
|
|
55
60
|
"figure.text.scale-position-explained": "지금 자리를 도형 크기의 몫으로 둡니다. 두 배 크기면 두 배 거리에 놓입니다.",
|
|
61
|
+
"figure.text.repeat-member": "{name} 되풀이 {count}개 중 {index}번째입니다.",
|
|
62
|
+
"figure.label.detail-none": "없음",
|
|
63
|
+
"figure.label.repeat": "되풀이",
|
|
64
|
+
"figure.text.repeat-fits-length": "도형 길이에 맞춰 이 간격으로 들어가는 만큼 놓입니다.",
|
|
65
|
+
"figure.text.repeat-fixed-count": "개수가 고정된 되풀이라 간격은 아직 바꿀 수 없습니다.",
|
|
66
|
+
"figure.text.repeat-what-pitch-gives": "길이 {length} mm 에서 {count}개 · 최대 길이 {longest} mm 까지 놓입니다 (한 줄 {limit}개).",
|
|
67
|
+
"figure.text.repeat-pitch-too-small": "pitch 는 {min} mm 이상이어야 합니다. 저작한 길이 {length} mm 에서 {limit}개를 넘기 때문입니다.",
|
|
68
|
+
"figure.text.gizmo-joint-places": "{part} 부품은 관절에 달려 있어 관절이 자리를 정합니다. 움직임 탭의 조작값으로 움직이세요.",
|
|
69
|
+
"figure.text.gizmo-fastening-places": "{part} 부품은 다른 부품의 면에 붙어 있어 그 면이 자리를 정합니다. 옮기려면 먼저 떼세요.",
|
|
70
|
+
"figure.text.gizmo-size-worked-out": "{part} 부품의 크기는 식으로 정해져 있어 기즈모로 바꿀 수 없습니다. 치수 칸에서 고정으로 바꾸면 됩니다.",
|
|
56
71
|
"figure.text.instance-range-can-widen": "부품이 바뀌어 다시 재면 {ranges} mm 입니다.",
|
|
57
72
|
"figure.text.mounting-face-needs-space": "공간을 먼저 선언해야 고를 수 있습니다.",
|
|
58
73
|
"figure.text.part-is-referenced": "'{part}' 에 다른 부품이 붙어 있거나 그 크기를 따르고 있어 지울 수 없습니다. 그 연결을 먼저 푸세요.",
|
|
@@ -257,6 +272,7 @@
|
|
|
257
272
|
"figure.label.released-versions": "발행된 버전",
|
|
258
273
|
"figure.label.repeat-axis": "늘어나는 축",
|
|
259
274
|
"figure.label.repeat-count": "개수",
|
|
275
|
+
"figure.label.repeat-gap": "pitch",
|
|
260
276
|
"figure.label.repeat-pitch": "한 칸 (피치)",
|
|
261
277
|
"figure.label.reuse-trend": "재사용 추이",
|
|
262
278
|
"figure.label.instance-count": "인스턴스 수",
|
|
@@ -280,7 +296,6 @@
|
|
|
280
296
|
"figure.label.shapes": "도형",
|
|
281
297
|
"figure.label.sizing-fixed": "크기 고정",
|
|
282
298
|
"figure.label.sizing-repeat": "개수 증가",
|
|
283
|
-
"figure.label.sizing-rule": "크기 변경 시 동작",
|
|
284
299
|
"figure.label.sizing-scale": "비례 확대",
|
|
285
300
|
"figure.label.sizing-stretch": "한 축만 늘임",
|
|
286
301
|
"figure.label.sky-color": "배경색",
|
|
@@ -315,7 +330,7 @@
|
|
|
315
330
|
"figure.text.bound-by-triangles": "삼각형이 먼저 한도에 도달합니다 (1개당 {per}, 도면당 {budget}).",
|
|
316
331
|
"figure.text.budget-measured-on": "{gpu} 에서 {on} 에 잰 값입니다. 가만히 서 있는 도형은 값을 치르지 않습니다 — 화면에 보이고 다시 그려지는 것만 셉니다.",
|
|
317
332
|
"figure.text.budget-not-measured-yet": "아직 실측 전 목표치입니다.",
|
|
318
|
-
"figure.text.budgets-not-in-v3": "팔레트 외
|
|
333
|
+
"figure.text.budgets-not-in-v3": "팔레트 외 표면 한도는 V3 형식에 없는 항목이라 표시하지 않습니다. 부품 수 한도는 디테일 등급을 정하면 보입니다.",
|
|
319
334
|
"figure.text.trend-chart-title": "인스턴스 수에 따른 그리는 비용",
|
|
320
335
|
"figure.text.trend-chart-lead": "평평한 선은 인스턴스가 늘어도 그대로인 값입니다. 같은 크기의 인스턴스가 공유합니다.",
|
|
321
336
|
"figure.text.trend-at-count": "{n}개일 때",
|
package/translations/ms.json
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"figure.button.fix-position": "tetapkan di sini",
|
|
18
18
|
"figure.button.scale-position": "berkadar",
|
|
19
19
|
"figure.button.remeasure-range": "ukur semula",
|
|
20
|
+
"figure.label.sizing-rule": "when the size changes",
|
|
21
|
+
"figure.label.sizing-uniform": "satu nisbah (min geometri tiga paksi)",
|
|
22
|
+
"figure.button.scale-uniformly": "besarkan dengan satu nisbah",
|
|
23
|
+
"figure.text.sizing-uniform-proposed": "figura ini mempunyai sendi. membesarkannya dengan satu nisbah memastikan sendi tidak terpisah pada sebarang saiz.",
|
|
24
|
+
"figure.text.sizing-uniform-explained": "membesarkan seluruh figura dengan min geometri tiga saiz yang diberi papan. kaki kekal di lantai; ruang di atas figura dibiarkan kosong.",
|
|
20
25
|
"figure.button.mirror-x": "mirror X",
|
|
21
26
|
"figure.button.mirror-z": "mirror Z",
|
|
22
27
|
"figure.button.move-part-down": "move down",
|
|
@@ -51,6 +56,16 @@
|
|
|
51
56
|
"figure.text.follow-would-loop": "dimensi itu sudah mengikut bahagian ini, jadi kedua-duanya tidak boleh saling mengikut.",
|
|
52
57
|
"figure.text.links-need-a-box": "ikut saiz untuk bahagian berbentuk kotak, pasangan untuk kotak dan silinder.",
|
|
53
58
|
"figure.text.scale-position-explained": "menyimpan kedudukan ini sebagai bahagian saiz figura — pada saiz dua kali, jaraknya dua kali.",
|
|
59
|
+
"figure.text.repeat-member": "salinan ke-{index} daripada {count} yang dibuat dengan mengulang {name}.",
|
|
60
|
+
"figure.label.detail-none": "tiada",
|
|
61
|
+
"figure.label.repeat": "ulangan",
|
|
62
|
+
"figure.text.repeat-fits-length": "seberapa banyak salinan yang muat pada panjang figura dengan jarak ini.",
|
|
63
|
+
"figure.text.repeat-fixed-count": "ulangan dengan bilangan tetap; jaraknya belum boleh ditetapkan.",
|
|
64
|
+
"figure.text.repeat-what-pitch-gives": "{count} pada panjang {length} mm · boleh diletakkan sehingga {longest} mm ({limit} sebaris).",
|
|
65
|
+
"figure.text.repeat-pitch-too-small": "pitch mesti sekurang-kurangnya {min} mm: pada panjang asal {length} mm ia akan melebihi {limit}.",
|
|
66
|
+
"figure.text.gizmo-joint-places": "sendi menentukan kedudukan {part}. gerakkannya dengan kawalannya di tab gerakan.",
|
|
67
|
+
"figure.text.gizmo-fastening-places": "{part} dilekatkan pada muka bahagian lain, yang menentukan kedudukannya. tanggalkan dahulu untuk menggerakkannya.",
|
|
68
|
+
"figure.text.gizmo-size-worked-out": "saiz {part} dikira melalui formula, jadi gizmo tidak dapat mengubahnya. tetapkan kepada tetap di bawah dimensi.",
|
|
54
69
|
"figure.text.instance-range-can-widen": "diukur semula selepas bahagian berubah: {ranges} mm.",
|
|
55
70
|
"figure.text.mounting-face-needs-space": "isytiharkan ruang dahulu untuk memilih.",
|
|
56
71
|
"figure.text.part-is-referenced": "'{part}' tidak boleh dipadam: bahagian lain dipasang padanya atau mengikut saiznya. Leraikan pautan itu dahulu.",
|
|
@@ -255,6 +270,7 @@
|
|
|
255
270
|
"figure.label.released-versions": "versi yang diterbitkan",
|
|
256
271
|
"figure.label.repeat-axis": "axis it repeats along",
|
|
257
272
|
"figure.label.repeat-count": "bilangan",
|
|
273
|
+
"figure.label.repeat-gap": "pitch",
|
|
258
274
|
"figure.label.repeat-pitch": "pitch",
|
|
259
275
|
"figure.label.reuse-trend": "reuse trend",
|
|
260
276
|
"figure.label.instance-count": "bilangan instans",
|
|
@@ -278,7 +294,6 @@
|
|
|
278
294
|
"figure.label.shapes": "shapes",
|
|
279
295
|
"figure.label.sizing-fixed": "keeps its size",
|
|
280
296
|
"figure.label.sizing-repeat": "repeats",
|
|
281
|
-
"figure.label.sizing-rule": "when the size changes",
|
|
282
297
|
"figure.label.sizing-scale": "scales proportionally",
|
|
283
298
|
"figure.label.sizing-stretch": "stretches on one axis",
|
|
284
299
|
"figure.label.sky-color": "sky color",
|
|
@@ -313,7 +328,7 @@
|
|
|
313
328
|
"figure.text.bound-by-triangles": "segi tiga mencapai had dahulu ({per} setiap satu, {budget} setiap lukisan).",
|
|
314
329
|
"figure.text.budget-measured-on": "Diukur pada {gpu}, {on}. Figura yang diam tidak menelan kos — hanya yang kelihatan dan dilukis semula dikira.",
|
|
315
330
|
"figure.text.budget-not-measured-yet": "this target has not been measured yet.",
|
|
316
|
-
"figure.text.budgets-not-in-v3": "permukaan luar palet
|
|
331
|
+
"figure.text.budgets-not-in-v3": "permukaan luar palet tiada dalam format V3, jadi tidak dipaparkan. had bilangan bahagian dipaparkan apabila tahap perincian ditetapkan.",
|
|
317
332
|
"figure.text.trend-chart-title": "kos lukisan mengikut bilangan instans",
|
|
318
333
|
"figure.text.trend-chart-lead": "garis mendatar kekal sama apabila instans bertambah. instans yang sama saiz berkongsinya.",
|
|
319
334
|
"figure.text.trend-at-count": "pada {n}",
|