@things-factory/figure-ui 10.1.43 → 10.1.45

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.
Files changed (128) hide show
  1. package/client/modeller/figure-v3-links.ts +303 -0
  2. package/client/modeller/modeller-route.ts +6 -21
  3. package/client/modeller/v3-editor.ts +44 -3
  4. package/client/modeller/v3-gizmo.ts +13 -2
  5. package/client/modeller/v3-links.ts +138 -0
  6. package/client/pages/figure-v3-modeller-page.ts +2 -5
  7. package/client/route.ts +2 -6
  8. package/dist-client/modeller/figure-v3-links.d.ts +42 -0
  9. package/dist-client/modeller/figure-v3-links.js +306 -0
  10. package/dist-client/modeller/figure-v3-links.js.map +1 -0
  11. package/dist-client/modeller/modeller-route.d.ts +4 -11
  12. package/dist-client/modeller/modeller-route.js +5 -22
  13. package/dist-client/modeller/modeller-route.js.map +1 -1
  14. package/dist-client/modeller/v3-editor.d.ts +5 -0
  15. package/dist-client/modeller/v3-editor.js +46 -2
  16. package/dist-client/modeller/v3-editor.js.map +1 -1
  17. package/dist-client/modeller/v3-gizmo.js +11 -1
  18. package/dist-client/modeller/v3-gizmo.js.map +1 -1
  19. package/dist-client/modeller/v3-links.d.ts +54 -0
  20. package/dist-client/modeller/v3-links.js +110 -0
  21. package/dist-client/modeller/v3-links.js.map +1 -0
  22. package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -5
  23. package/dist-client/pages/figure-v3-modeller-page.js +2 -5
  24. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  25. package/dist-client/route.d.ts +1 -1
  26. package/dist-client/route.js +2 -5
  27. package/dist-client/route.js.map +1 -1
  28. package/dist-client/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +4 -4
  30. package/test/ai-proposal-contract.test.ts +0 -88
  31. package/test/figure-source.test.ts +0 -102
  32. package/test/i18n-prefix-guard.test.ts +0 -24
  33. package/test/modeller-route.test.ts +14 -25
  34. package/test/part-edits.test.ts +0 -7
  35. package/test/scene-budget.test.ts +0 -7
  36. package/test/v3-gizmo.test.ts +10 -0
  37. package/test/v3-links.test.ts +123 -0
  38. package/things-factory.config.js +2 -5
  39. package/translations/en.json +6 -0
  40. package/translations/ja.json +11 -1
  41. package/translations/ko.json +6 -0
  42. package/translations/ms.json +11 -1
  43. package/translations/zh.json +11 -1
  44. package/client/modeller/angle-dial.ts +0 -70
  45. package/client/modeller/base-box.ts +0 -175
  46. package/client/modeller/figure-animations.ts +0 -1249
  47. package/client/modeller/figure-canvas.ts +0 -567
  48. package/client/modeller/figure-capabilities.ts +0 -379
  49. package/client/modeller/figure-inspector.ts +0 -2208
  50. package/client/modeller/figure-parts.ts +0 -479
  51. package/client/modeller/figure-preview.ts +0 -598
  52. package/client/modeller/figure-report.ts +0 -966
  53. package/client/modeller/figure-settings.ts +0 -328
  54. package/client/modeller/figure-side.ts +0 -272
  55. package/client/modeller/figure-thumbnail.ts +0 -123
  56. package/client/modeller/figure-view.ts +0 -152
  57. package/client/modeller/joint-edits.ts +0 -262
  58. package/client/modeller/joint-gizmo.ts +0 -180
  59. package/client/modeller/maturity.ts +0 -288
  60. package/client/modeller/new-figure-entry.ts +0 -4
  61. package/client/modeller/pose-defaults.ts +0 -67
  62. package/client/pages/figure-modeller-page.ts +0 -2082
  63. package/dist-client/modeller/angle-dial.d.ts +0 -32
  64. package/dist-client/modeller/angle-dial.js +0 -41
  65. package/dist-client/modeller/angle-dial.js.map +0 -1
  66. package/dist-client/modeller/base-box.d.ts +0 -71
  67. package/dist-client/modeller/base-box.js +0 -110
  68. package/dist-client/modeller/base-box.js.map +0 -1
  69. package/dist-client/modeller/figure-animations.d.ts +0 -159
  70. package/dist-client/modeller/figure-animations.js +0 -1132
  71. package/dist-client/modeller/figure-animations.js.map +0 -1
  72. package/dist-client/modeller/figure-canvas.d.ts +0 -150
  73. package/dist-client/modeller/figure-canvas.js +0 -506
  74. package/dist-client/modeller/figure-canvas.js.map +0 -1
  75. package/dist-client/modeller/figure-capabilities.d.ts +0 -42
  76. package/dist-client/modeller/figure-capabilities.js +0 -349
  77. package/dist-client/modeller/figure-capabilities.js.map +0 -1
  78. package/dist-client/modeller/figure-inspector.d.ts +0 -329
  79. package/dist-client/modeller/figure-inspector.js +0 -2095
  80. package/dist-client/modeller/figure-inspector.js.map +0 -1
  81. package/dist-client/modeller/figure-parts.d.ts +0 -64
  82. package/dist-client/modeller/figure-parts.js +0 -478
  83. package/dist-client/modeller/figure-parts.js.map +0 -1
  84. package/dist-client/modeller/figure-preview.d.ts +0 -138
  85. package/dist-client/modeller/figure-preview.js +0 -549
  86. package/dist-client/modeller/figure-preview.js.map +0 -1
  87. package/dist-client/modeller/figure-report.d.ts +0 -165
  88. package/dist-client/modeller/figure-report.js +0 -952
  89. package/dist-client/modeller/figure-report.js.map +0 -1
  90. package/dist-client/modeller/figure-settings.d.ts +0 -39
  91. package/dist-client/modeller/figure-settings.js +0 -317
  92. package/dist-client/modeller/figure-settings.js.map +0 -1
  93. package/dist-client/modeller/figure-side.d.ts +0 -78
  94. package/dist-client/modeller/figure-side.js +0 -317
  95. package/dist-client/modeller/figure-side.js.map +0 -1
  96. package/dist-client/modeller/figure-thumbnail.d.ts +0 -3
  97. package/dist-client/modeller/figure-thumbnail.js +0 -95
  98. package/dist-client/modeller/figure-thumbnail.js.map +0 -1
  99. package/dist-client/modeller/figure-view.d.ts +0 -118
  100. package/dist-client/modeller/figure-view.js +0 -78
  101. package/dist-client/modeller/figure-view.js.map +0 -1
  102. package/dist-client/modeller/joint-edits.d.ts +0 -49
  103. package/dist-client/modeller/joint-edits.js +0 -223
  104. package/dist-client/modeller/joint-edits.js.map +0 -1
  105. package/dist-client/modeller/joint-gizmo.d.ts +0 -34
  106. package/dist-client/modeller/joint-gizmo.js +0 -158
  107. package/dist-client/modeller/joint-gizmo.js.map +0 -1
  108. package/dist-client/modeller/maturity.d.ts +0 -55
  109. package/dist-client/modeller/maturity.js +0 -212
  110. package/dist-client/modeller/maturity.js.map +0 -1
  111. package/dist-client/modeller/new-figure-entry.d.ts +0 -2
  112. package/dist-client/modeller/new-figure-entry.js +0 -5
  113. package/dist-client/modeller/new-figure-entry.js.map +0 -1
  114. package/dist-client/modeller/pose-defaults.d.ts +0 -9
  115. package/dist-client/modeller/pose-defaults.js +0 -35
  116. package/dist-client/modeller/pose-defaults.js.map +0 -1
  117. package/dist-client/pages/figure-modeller-page.d.ts +0 -419
  118. package/dist-client/pages/figure-modeller-page.js +0 -2060
  119. package/dist-client/pages/figure-modeller-page.js.map +0 -1
  120. package/test/angle-dial.test.ts +0 -78
  121. package/test/base-box.test.ts +0 -235
  122. package/test/joint-edits.test.ts +0 -165
  123. package/test/joint-gizmo.test.ts +0 -174
  124. package/test/joint-needle.test.ts +0 -162
  125. package/test/modeller-undo-redo.test.ts +0 -33
  126. package/test/new-figure-entry.test.ts +0 -15
  127. package/test/pose-defaults.test.ts +0 -71
  128. package/test/preview-board-depth.test.ts +0 -27
@@ -1,180 +0,0 @@
1
- /*
2
- * Copyright © HatioLab Inc. All rights reserved.
3
- */
4
-
5
- import type { FigureJoint, FigurePart, FigureSource } from '@hatiolab/figure-model'
6
-
7
- import { partFromFigure } from './figure-source.js'
8
- import type { FigureDraft, PartModel } from './figure-source.js'
9
-
10
- /**
11
- * 관절이 **어느 자리에서 어느 쪽으로 얼마나 도는지**를 저작면에 그린다.
12
- *
13
- * 축은 `+x · -x · +y …` 중에서 고른다. 이름만 보고 어느 쪽으로 도는지 아는 사람은 없고, 지금은
14
- * 미리보기로 건너가 막대를 끝까지 밀어 봐야 안다 — 여섯 축짜리 로봇에서는 그 왕복이 여섯 번이다.
15
- * 도는 자리를 그 자리에 그려 두면 고르는 순간 보인다.
16
- *
17
- * ## 부채꼴 하나다
18
- *
19
- * 관절의 기준점에 **가동 범위만큼 벌어진 얇은 부채꼴**을 눕힌다. 부채꼴의 면이 도는 평면이고,
20
- * 두께 방향이 축이며, 벌어진 각이 `limits` 다. 무한 회전(`continuous`)은 온전한 원이다.
21
- *
22
- * 부품 하나로 그린다 — 씬에 겹쳐 그리는 길을 따로 뚫지 않는다. 저작면은 부품을 컴포넌트로
23
- * 세우는 자리이고, 이 부채꼴도 그 규칙을 그대로 따른다. `locked` 라 클릭·마퀴·기즈모가 집지
24
- * 않고, 저장 형식으로는 나가지 않는다(저작면 안에서만 만들어 붙인다).
25
- */
26
-
27
- /** 저작면에만 있는 이름. 부품 목록에는 없다. */
28
- export const JOINT_GIZMO = '__joint-gizmo'
29
-
30
- /** 부채꼴을 몇 조각으로 그리나. 한 조각이 6도쯤이면 원이 원으로 보인다. */
31
- const STEP = 6
32
-
33
- /** 두께. 얇아야 부품을 가리지 않고, 너무 얇으면 옆에서 볼 때 사라진다. */
34
- const THICKNESS = 6
35
-
36
- /**
37
- * 도는 평면과 두께 방향을 축에 맞춘다.
38
- *
39
- * 부채꼴은 단면이 눕고 **제 y 방향으로** 밀려 두꺼워지는 도형이라, 기본 상태의 축이 +y 다.
40
- * 다른 축은 부품 회전으로 눕힌다 — 부품이 제 회전을 지는 것과 같은 방법이고, 형식이 이미
41
- * 아는 길이다.
42
- *
43
- * `turn` 은 단면 안에서 각을 재는 방향이다. 씬의 회전과 단면의 각이 도는 쪽이 반대인 축이
44
- * 있어서, 그것을 여기 한 곳에 적어 둔다 — 부채꼴이 실제로 도는 쪽과 반대로 벌어지면
45
- * 가리키는 뜻이 뒤집힌다.
46
- */
47
- const AXIS_LAY: Record<string, { rotation?: FigurePart['transform']['rotation']; turn: 1 | -1 }> = {
48
- '+y': { turn: -1 },
49
- '-y': { turn: 1 },
50
- '+x': { rotation: { z: 90 }, turn: 1 },
51
- '-x': { rotation: { z: 90 }, turn: -1 },
52
- '+z': { rotation: { x: 90 }, turn: -1 },
53
- '-z': { rotation: { x: 90 }, turn: 1 }
54
- }
55
-
56
- /** 축 벡터를 `+x` 꼴의 이름으로. 형식은 단위 벡터를 쓰고 저작면은 여섯 중 하나를 고른다. */
57
- export function axisName(axis: { x: number; y: number; z: number }): string | undefined {
58
- const named = [
59
- ['x', axis.x],
60
- ['y', axis.y],
61
- ['z', axis.z]
62
- ] as const
63
- const on = named.filter(([, value]) => value !== 0)
64
- if (on.length !== 1) return undefined
65
- const [name, value] = on[0]!
66
- return `${value > 0 ? '+' : '-'}${name}`
67
- }
68
-
69
- /** 이 관절이 도는 범위. 무한 회전은 한 바퀴다. */
70
- function span(joint: FigureJoint): { from: number; to: number } {
71
- if (joint.type === 'continuous' || !joint.limits) return { from: -180, to: 180 }
72
- return { from: joint.limits.min, to: joint.limits.max }
73
- }
74
-
75
- /** 부채꼴의 단면. 점은 부품 중심 기준의 밀리미터다. */
76
- function sector(from: number, to: number, radius: number, turn: 1 | -1): { x: number; y: number }[] {
77
- const path: { x: number; y: number }[] = []
78
- const full = Math.abs(to - from) >= 359.999
79
-
80
- if (!full) path.push({ x: 0, y: 0 })
81
-
82
- const steps = Math.max(2, Math.ceil(Math.abs(to - from) / STEP))
83
- for (let i = 0; i <= steps; i++) {
84
- const degrees = from + ((to - from) * i) / steps
85
- const rad = (degrees * Math.PI * turn) / 180
86
- path.push({ x: +(Math.cos(rad) * radius).toFixed(3), y: +(Math.sin(rad) * radius).toFixed(3) })
87
- }
88
-
89
- return path
90
- }
91
-
92
- /**
93
- * 관절 하나를 그리는 부품. 그릴 수 없으면 `undefined` — 축이 비스듬하면 부채꼴로 그릴 수 없고,
94
- * 그때 아무 쪽이나 골라 그리면 **틀린 방향을 가리킨다.**
95
- */
96
- export function gizmoPart(joint: FigureJoint, child: PartModel | undefined, draft: FigureDraft): PartModel | undefined {
97
- const lay = AXIS_LAY[axisName(joint.axis) ?? '']
98
- if (!lay) return undefined
99
-
100
- /* 반지름은 **도는 부품의 크기**에서 온다. 부채꼴이 그 부품보다 작으면 무엇이 도는지 안 읽힌다. */
101
- const reach = child ? Math.max(child.width, child.height, child.depth) : 0
102
- const radius = Math.max(60, Math.round(reach * 0.6))
103
-
104
- const { from, to } = span(joint)
105
- const part: FigurePart = {
106
- name: JOINT_GIZMO,
107
- primitive: 'polygon',
108
- transform: {
109
- position: { ...joint.origin },
110
- size: { x: radius * 2, y: THICKNESS, z: radius * 2 },
111
- ...(lay.rotation ? { rotation: lay.rotation } : {})
112
- },
113
- shape: { path: sector(from, to, radius, lay.turn) },
114
- material: { token: 'palette.accent', transparent: true }
115
- }
116
-
117
- return { ...partFromFigure(part, draft), locked: true }
118
- }
119
-
120
- /*
121
- * 미리보기에서 **관절이 돌았다는 것**을 보이게 하는 바늘.
122
- *
123
- * 상자 위에 원기둥을 얹고 그 축으로 돌리면 화면이 그대로다 — 원기둥은 제 축에서 대칭이라
124
- * 돌아도 같은 그림이다. 저작자는 막대를 76도까지 밀고도 아무것도 못 보고 「관절이 안 먹는다」로
125
- * 읽는다. 값은 제대로 갔고 부품도 돌았는데 볼 것이 없었을 뿐이다.
126
- *
127
- * 그래서 관절이 모는 부품에 얇은 막대 하나를 **자식으로** 붙인다. 부모가 관절의 자식이라
128
- * 관절이 도는 만큼 같이 돌고, 축에서 비켜나 있어 대칭인 부품에서도 움직임이 보인다. 매 프레임
129
- * 무엇을 다시 그릴 일도, 막대를 밀 때마다 씬을 다시 세울 일도 없다.
130
- *
131
- * **미리보기에만 붙인다.** 저장 형식에도, 저작면에도, 카드 그림에도 안 들어간다 — 미리보기는
132
- * 크기 반응을 보는 작업대이고 거기서만 쓰는 표시다.
133
- */
134
-
135
- /** 미리보기에만 있는 이름. 저장 형식에는 없다. */
136
- export const NEEDLE = '__needle-'
137
-
138
- /**
139
- * 막대의 굵기 — **길이에 견주어** 잡는다.
140
- *
141
- * 8mm 로 고정했더니 미리보기에서 안 보였다. 미리보기는 여덟 대를 한 화면에 세우는 자리라
142
- * 100mm 짜리 부품이 화면에서 20 픽셀쯤이고, 그 위의 8mm 는 한 픽셀이 안 된다. 세어 보면
143
- * 막대는 분명히 서 있는데(부품 둘짜리 표본의 인스턴스에 mesh 셋 · 삼각형 72) 눈에 안 띈다.
144
- */
145
- const needleThick = (radius: number) => Math.max(6, Math.round(radius / 5))
146
-
147
- /** 축에서 비켜날 방향. 그 축과 직각이어야 도는 것이 보인다. */
148
- const ASIDE: Record<string, 'x' | 'z'> = { x: 'z', y: 'x', z: 'x' }
149
-
150
- export function jointNeedles(source: FigureSource): FigurePart[] {
151
- const byName = new Map(source.parts.map(part => [part.name, part]))
152
-
153
- return (source.joints ?? []).flatMap(joint => {
154
- const axis = axisName(joint.axis)
155
- if (!axis) return []
156
-
157
- const child = byName.get(joint.child)
158
- const size = child?.transform.size
159
- const reach = size ? Math.max(size.x, size.y, size.z) : 0
160
- const radius = Math.max(60, Math.round(reach * 0.6))
161
- const aside = ASIDE[axis.slice(1)]!
162
- const thick = needleThick(radius)
163
-
164
- return [
165
- {
166
- name: `${NEEDLE}${joint.name}`,
167
- primitive: 'cube' as const,
168
- transform: {
169
- position: { ...joint.origin, [aside]: joint.origin[aside] + radius / 2 },
170
- size:
171
- aside === 'x'
172
- ? { x: radius, y: thick, z: thick }
173
- : { x: thick, y: thick, z: radius }
174
- },
175
- material: { token: 'palette.accent' },
176
- parent: joint.child
177
- }
178
- ]
179
- })
180
- }
@@ -1,288 +0,0 @@
1
- import { LIMITS, PART_LIMIT, SEGMENT_PRESETS, materialKeyOf } from '@hatiolab/figure-model'
2
- import type { DetailLevel, FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model'
3
-
4
- import { capacityFrom } from './scene-budget.js'
5
-
6
- /**
7
- * 성숙도 — 이 형상이 얼마나 잘 만들어졌나.
8
- *
9
- * ## 축이 여럿이어야 하는 이유
10
- *
11
- * 「몇 개까지 편한가」 하나로는 좋은 형상과 나쁜 형상이 구분되지 않는다. 부품 하나짜리
12
- * 상자도 그 축에서는 만점이다. 잘 만들어졌다는 것은 **여러 물음에 각각 답하는 것**이고,
13
- * 그것을 한 숫자로 뭉개면 무엇을 고쳐야 할지 알 수 없다.
14
- *
15
- * ## 축을 지어내지 않았다
16
- *
17
- * `docs/design/component-authoring-tool.md` 의 「인정받을 수 있는 최소 품질 — 기준선」
18
- * 다섯이 이미 있다. 그것을 잴 수 있는 형태로 옮기고, 이미 셈이 있는 것(규모·되쓰기)을
19
- * 더했다.
20
- *
21
- * 기준선 1 실루엣으로 알아본다 → readable **아직 측정하지 못한다**
22
- * 기준선 2 상태가 색으로 구분된다 → reactive **아직 측정하지 못한다**
23
- * 기준선 3 다른 자산과 한 세트로 보인다 → consistency
24
- * 기준선 4 확대해도 안 깨진다 → consistency (텍스처 0)
25
- * 기준선 5 라벨이 읽힌다 → reactive
26
- *
27
- * ## 못 재는 축을 지어내지 않는다
28
- *
29
- * 두 축은 지금 잴 수 없다. 하나는 그리기가 필요하고(실루엣), 하나는 형식에 길이
30
- * 없다(상태 바인딩 — figure-model#2). **그것을 0점으로도 만점으로도 두지 않는다.**
31
- * 「아직 측정하지 못한다」고 알리고 이유를 붙인다. 측정하지 못한 것을 점수로 만들면 그 점수가 거짓말을
32
- * 한다.
33
- *
34
- * ## 이 파일은 임시다
35
- *
36
- * 셈은 figure-model 것이어야 한다 — 거기가 `costOf` · `scoreOf` 를 갖고 있다.
37
- * 형식 쪽 결정(#2 · #4)이 서면 이 파일을 지우고 그쪽으로 옮긴다.
38
- */
39
-
40
- export type AxisKey = 'scale' | 'consistency' | 'reuse' | 'detail' | 'reactive' | 'readable'
41
-
42
- export interface MaturityAxis {
43
- key: AxisKey
44
- /** 0~1. 못 재면 없다. */
45
- ratio?: number
46
- /** 지금 이 축의 값. 그대로 보여 준다. */
47
- reading?: string
48
- /** 값이 숫자가 아니라 말일 때의 문구 키. `reading` 과 함께 쓰지 않는다. */
49
- readingKey?: string
50
- /** 왜 이 값인가. */
51
- because?: string
52
- /** 못 재면 그 이유. 있으면 `ratio` 가 없다. */
53
- blocked?: string
54
- }
55
-
56
- /** 한 도면에 이만큼 놓는 것을 본다. 설계 문서 §4.15 의 「자산 500 개」. */
57
- const BOARD_ASSETS = 500
58
-
59
- /**
60
- * 여섯 축을 측정한다.
61
- *
62
- * 한 숫자로 합치지 않는다. 합치면 「무엇을 고쳐야 하나」가 사라진다 — 그것이 이
63
- * 리포트의 존재 이유다.
64
- */
65
- export function maturityOf(source: FigureSource, cost: FigureCost, score: FigureScore): MaturityAxis[] {
66
- return [scale(cost), consistency(source, cost), reuse(score), detail(source, cost), reactive(source), readable(source, cost)]
67
- }
68
-
69
- /** 규모 — 한 도면에 얼마나 놓을 수 있나. */
70
- function scale(cost: FigureCost): MaturityAxis {
71
- const capacity = capacityFrom(cost.groups, cost.triangles)
72
- if (!capacity) {
73
- return { key: 'scale', blocked: 'figure.text.maturity-blocked-no-parts' }
74
- }
75
-
76
- return {
77
- key: 'scale',
78
- // 한 도면 목표치(500)를 채우면 1 이다. 그보다 많이 놓을 수 있어도 1 을 넘기지 않는다 —
79
- // 목표를 넘긴 여유는 다른 축을 고치는 데 쓰는 편이 낫다.
80
- ratio: Math.min(1, capacity.instances / BOARD_ASSETS),
81
- reading: String(capacity.instances),
82
- because: 'figure.text.maturity-scale-because'
83
- }
84
- }
85
-
86
- /**
87
- * 한 세트로 보이나. 기준선 3·4.
88
- *
89
- * 셋을 본다. 팔레트 밖 표면이 없나(텍스처 0 — 확대해도 안 깨진다), 분할 수가
90
- * 프리셋인가, 투명 재질을 아끼나. 셋 다 이미 형식이 위반으로 세고 있는 것들이다.
91
- */
92
- function consistency(source: FigureSource, cost: FigureCost): MaturityAxis {
93
- const offPreset = source.parts.filter(
94
- part => part.segments !== undefined && !SEGMENT_PRESETS.includes(part.segments)
95
- ).length
96
-
97
- const faults =
98
- (cost.surfaceTextures > 0 ? 1 : 0) +
99
- (offPreset > 0 ? 1 : 0) +
100
- (cost.distinctMaterials > LIMITS.materialGroups ? 1 : 0)
101
-
102
- return {
103
- key: 'consistency',
104
- ratio: (3 - faults) / 3,
105
- readingKey: faults === 0 ? 'figure.text.maturity-consistency-clean' : 'figure.text.maturity-consistency-faults',
106
- because: 'figure.text.maturity-consistency-because'
107
- }
108
- }
109
-
110
- /**
111
- * 되쓰나 — 같은 형상을 몇 번 다시 쓰나.
112
- *
113
- * `scoreOf` 가 이미 재고 있으나 **점수에는 넣지 않는다**고 선언돼 있다(한도가 아니라
114
- * 덤이다). 축으로는 뜻이 있다 — 되쓰기가 많은 형상이 잘 짜인 형상이다. 다만 부품마다
115
- * 형상이 달라도 잘못이 아니므로 **낮다고 나무라지 않는다.**
116
- */
117
- function reuse(score: FigureScore): MaturityAxis {
118
- const { distinct, parts, saved } = score.reuse
119
- if (parts === 0) {
120
- return { key: 'reuse', blocked: 'figure.text.maturity-blocked-no-parts' }
121
- }
122
-
123
- return {
124
- key: 'reuse',
125
- // ratio 가 낮을수록 많이 되쓴다 — 뒤집어서 높은 쪽이 좋게 만든다
126
- ratio: 1 - distinct / parts,
127
- reading: String(saved),
128
- because: 'figure.text.maturity-reuse-because'
129
- }
130
- }
131
-
132
- /**
133
- * 정밀도 — 밝힌 등급에 걸맞게 만들어졌나.
134
- *
135
- * 등급(S·M·L)은 「어느 거리에서 읽히도록 만든 것인가」를 밝히는 것이다. 그런데 등급만
136
- * 올려 놓고 부품 셋으로 끝내면 그 약속을 안 지킨 것이다. **한도를 얼마나 썼나**로 본다.
137
- *
138
- * 이 축은 「많이 쓸수록 좋다」가 아니다. 등급에 맞게 채웠나를 본다 — 그래서 넘치면
139
- * 다시 내려간다.
140
- */
141
- function detail(source: FigureSource, cost: FigureCost): MaturityAxis {
142
- const level = (source.detailLevel ?? 'M') as DetailLevel
143
- const limit = PART_LIMIT[level]
144
- const used = cost.parts / limit
145
-
146
- return {
147
- key: 'detail',
148
- // 절반쯤 쓴 것을 제일 좋게 본다. 텅 비어도, 넘쳐도 등급과 안 맞는다.
149
- ratio: used > 1 ? Math.max(0, 2 - used) : Math.min(1, used * 2),
150
- reading: `${cost.parts} / ${limit}`,
151
- because: 'figure.text.maturity-detail-because'
152
- }
153
- }
154
-
155
- /**
156
- * 살아 있나 — 기준선 2·5. 상태가 색으로 구분되고 라벨이 읽히나.
157
- *
158
- * **지금 잴 수 없다.** 형식에 `slot` 이 있지만 그리는 쪽이 읽지 않고(선언 한 줄이
159
- * 전부다), 상태를 부품에 잇는 길 자체가 아직 정해지지 않았다(figure-model#2).
160
- *
161
- * 0 점으로 두지 않는다. 0 은 「이 형상이 상태를 안 보여 준다」는 뜻이 되는데, 사실은
162
- * **아무 형상도 보여 줄 수 없는 상태**다. 축 자체가 아직 없는 것이다.
163
- */
164
- function reactive(source: FigureSource): MaturityAxis {
165
- return { key: 'reactive', blocked: 'figure.text.maturity-blocked-no-binding' }
166
- }
167
-
168
- /**
169
- * 식별성 — 기준선 1. 위에서 볼 때 무엇인지 알아보나.
170
- *
171
- * ## 시점 엔트로피를 빌린다
172
- *
173
- * 「어느 시점에서 얼마나 알아볼 수 있나」는 이미 연구된 물음이다. Vázquez 등의
174
- * **viewpoint entropy** 는 시점에서 보이는 각 면의 투영 면적 비율로 섀넌 엔트로피를
175
- * 낸다. 모든 면이 같은 면적으로 보일 때가 최대이고, 한 면이 화면을 다 덮으면 0 이다.
176
- *
177
- * H = -Σ (Ai / At) · log(Ai / At)
178
- *
179
- * ## 다만 면으로 세지 않는다
180
- *
181
- * 그 문헌이 스스로 밝히는 한계가 **폴리곤 분할에 좌우된다**는 것이다 — 같은 형상도
182
- * 잘게 쪼개면 엔트로피가 올라간다. 우리에게는 그 한계가 치명적이다. 분할 수를 8 에서
183
- * 16 으로 올리는 것은 형상을 알아보기 쉽게 만드는 일이 아닌데, 면으로 세면 점수가
184
- * 오른다.
185
- *
186
- * 그래서 **면이 아니라 색 구역으로 센다.** 눈이 구별하는 것은 삼각형이 아니라 색이
187
- * 다른 덩어리다. 같은 토큰을 쓰는 부품 둘이 붙어 있으면 눈에는 한 덩어리다.
188
- * 이렇게 하면 분할 수에 좌우되지 않으면서 원래 물음에 답한다.
189
- *
190
- * ## 기준값도 지어내지 않는다
191
- *
192
- * 정규화 분모를 `log(재질 group 한도 + 1)` 로 둔다. 이 형식이 허용하는 재질 group 이
193
- * 셋이고 거기에 배경을 더하면 넷이므로, **이 형식에서 도달 가능한 최대**가 곧
194
- * 기준이다. 임의로 고른 수가 아니다.
195
- *
196
- * ## 그리지 않고 셈으로 낸다
197
- *
198
- * 저작 중에 매번 렌더링할 수 없다. 대신 부품의 발자국(x·y·너비·높이)을 위에서
199
- * 겹쳐 본다 — 위에 있는 것이 아래를 가린다. 축에 정렬된 상자이므로 격자에 찍어
200
- * 세면 된다.
201
- *
202
- * 이 근사가 실제 렌더링과 맞는지는 하네스가 따로 확인한다
203
- * (`test/browser/check-silhouette.mjs`).
204
- */
205
- function readable(source: FigureSource, cost: FigureCost): MaturityAxis {
206
- const regions = topViewRegions(source)
207
- if (regions.total === 0) {
208
- return { key: 'readable', blocked: 'figure.text.maturity-blocked-no-parts' }
209
- }
210
-
211
- // 배경도 한 구역으로 센다 — 발자국을 얼마나 채우나가 식별성의 일부다.
212
- // Vázquez 의 「열린 장면」 형태와 같다(최대가 log(면 수 + 1)).
213
- const shares = [...regions.byMaterial.values(), regions.background]
214
- .map(area => area / regions.footprint)
215
- .filter(share => share > 0)
216
-
217
- const entropy = -shares.reduce((sum, p) => sum + p * Math.log(p), 0)
218
- const ceiling = Math.log(LIMITS.materialGroups + 1)
219
-
220
- return {
221
- key: 'readable',
222
- ratio: Math.min(1, entropy / ceiling),
223
- reading: `${regions.byMaterial.size}`,
224
- because: 'figure.text.maturity-readable-because'
225
- }
226
- }
227
-
228
- /**
229
- * 위에서 내려다볼 때 어느 재질이 얼마나 보이나.
230
- *
231
- * 부품 발자국을 격자에 찍되 **높은 것이 이긴다.** 축에 정렬된 상자만 다루므로
232
- * 이것으로 충분하다 — 회전한 부품은 발자국을 크게 잡아 보수적으로 본다.
233
- */
234
- function topViewRegions(source: FigureSource) {
235
- const GRID = 64
236
- const cells = new Array<string | null>(GRID * GRID).fill(null)
237
- const height = new Float32Array(GRID * GRID)
238
-
239
- // 톱뷰의 두 축은 x 와 z 다. y 는 높이라 위에서 보면 보이지 않는다.
240
- // 원점은 기준 상자의 중심이므로 격자의 가운데가 0 이다.
241
- const sx = GRID / source.base.x
242
- const sz = GRID / source.base.z
243
-
244
- for (const part of source.parts) {
245
- const { position, size } = part.transform
246
- const top = position.y + size.y / 2
247
- const key = materialKeyOf(part)
248
-
249
- const x0 = Math.max(0, Math.floor((position.x - size.x / 2 + source.base.x / 2) * sx))
250
- const x1 = Math.min(GRID, Math.ceil((position.x + size.x / 2 + source.base.x / 2) * sx))
251
- const y0 = Math.max(0, Math.floor((position.z - size.z / 2 + source.base.z / 2) * sz))
252
- const y1 = Math.min(GRID, Math.ceil((position.z + size.z / 2 + source.base.z / 2) * sz))
253
-
254
- for (let y = y0; y < y1; y++) {
255
- for (let x = x0; x < x1; x++) {
256
- // 원기둥·구는 발자국 상자를 다 채우지 않는다. 모서리를 깎아 근사한다.
257
- if (ROUND_FOOTPRINT.includes(part.primitive)) {
258
- const cxp = (x + 0.5 - (x0 + x1) / 2) / ((x1 - x0) / 2 || 1)
259
- const cyp = (y + 0.5 - (y0 + y1) / 2) / ((y1 - y0) / 2 || 1)
260
- if (cxp * cxp + cyp * cyp > 1) continue
261
- }
262
- const i = y * GRID + x
263
- if (cells[i] === null || top > height[i]) {
264
- cells[i] = key
265
- height[i] = top
266
- }
267
- }
268
- }
269
- }
270
-
271
- const byMaterial = new Map<string, number>()
272
- let covered = 0
273
- for (const key of cells) {
274
- if (key === null) continue
275
- covered++
276
- byMaterial.set(key, (byMaterial.get(key) ?? 0) + 1)
277
- }
278
-
279
- return {
280
- byMaterial,
281
- total: covered,
282
- background: GRID * GRID - covered,
283
- footprint: GRID * GRID
284
- }
285
- }
286
-
287
- /** 위에서 보면 발자국 상자를 다 채우지 않는 도형. */
288
- const ROUND_FOOTPRINT: string[] = ['cylinder', 'sphere']
@@ -1,4 +0,0 @@
1
- /** A route entry starts a new document; ordinary updates must preserve edits. */
2
- export function shouldStartNewFigure(changes: Record<string, unknown>, hasFigure: boolean, savedId?: string): boolean {
3
- return !hasFigure || !!savedId || changes.active === true || Object.prototype.hasOwnProperty.call(changes, 'resourceId')
4
- }
@@ -1,67 +0,0 @@
1
- /*
2
- * Copyright © HatioLab Inc. All rights reserved.
3
- */
4
-
5
- import type { FigureDraft } from './figure-source.js'
6
-
7
- /**
8
- * 지금 세워 둔 자세를 **기본값으로 앉힌다.**
9
- *
10
- * ## 왜 필요한가
11
- *
12
- * 관절의 휴지 자세는 값이 0 인 자세다(ADR-0066). 팔이 여섯 마디면 그 자세는 곧게 선 기둥이라,
13
- * 목록에서도 도면에서도 로봇으로 안 보인다. 로봇다운 자세는 **파라미터의 기본값**이 만든다 —
14
- * 어깨 -35, 팔꿈치 70 을 적어 두면 아무도 값을 주지 않을 때 그 자세로 선다.
15
- *
16
- * 그것을 손으로 적으려면 미리보기에서 막대를 맞춘 뒤 그 수를 읽어 파라미터 칸마다 옮겨야 한다.
17
- * 여섯 축이면 여섯 번이고, 옮기는 동안 화면의 자세와 적은 수가 갈린다. 여기서 하는 일은 그
18
- * 옮겨 적기 하나다.
19
- *
20
- * ## 무엇을 건드리나
21
- *
22
- * **막대를 민 파라미터만** 건드린다. 안 민 것은 이미 제 기본값에 서 있으므로 적을 것이 없다.
23
- * 값이 지금 기본값과 같으면 그 파라미터는 **그대로 둔다** — 새 객체로 바꾸면 고친 것이 없는데도
24
- * 「저장 안 됨」이 붙는다. 하나도 안 바뀌면 받은 초안을 그대로 돌려준다.
25
- */
26
-
27
- type Parameter = NonNullable<FigureDraft['parameters']>[number]
28
-
29
- /** 지금 이 파라미터가 서는 값. 형식의 `valueOf` 와 같은 순서로 읽는다. */
30
- function standing(parameter: Parameter): number {
31
- return parameter.default ?? parameter.range?.min ?? 0
32
- }
33
-
34
- /** 범위 밖은 끝에서 문다. 막대는 범위 안에서만 움직이지만, 값은 다른 길로도 들어온다. */
35
- function settled(value: number, parameter: Parameter): number {
36
- const range = parameter.range
37
- const bound = range ? Math.min(Math.max(value, range.min), range.max) : value
38
- return Math.round(bound * 10000) / 10000
39
- }
40
-
41
- /** 무엇이 달라지나 — 이름과 바뀔 값. 비어 있으면 앉힐 것이 없다. */
42
- export function poseChanges(
43
- draft: FigureDraft | undefined,
44
- values: Record<string, number>
45
- ): { name: string; from: number; to: number }[] {
46
- return (draft?.parameters ?? []).flatMap(parameter => {
47
- const given = values[parameter.name]
48
- if (given === undefined || !Number.isFinite(given)) return []
49
-
50
- const to = settled(given, parameter)
51
- const from = standing(parameter)
52
- return to === from ? [] : [{ name: parameter.name, from, to }]
53
- })
54
- }
55
-
56
- /** 지금 자세를 기본값으로 앉힌 초안. 앉힐 것이 없으면 **받은 초안 그대로**다. */
57
- export function poseAsDefaults(draft: FigureDraft, values: Record<string, number>): FigureDraft {
58
- const changes = new Map(poseChanges(draft, values).map(change => [change.name, change.to]))
59
- if (changes.size === 0) return draft
60
-
61
- return {
62
- ...draft,
63
- parameters: (draft.parameters ?? []).map(parameter =>
64
- changes.has(parameter.name) ? { ...parameter, default: changes.get(parameter.name)! } : parameter
65
- )
66
- }
67
- }