toto10 commited on
Commit
fea727b
1 Parent(s): d5dede2

fb1ffd4d2aea1b996230b8c8528d58e19308ce05ffb2f5c4d89d42fa73455684

Browse files
Files changed (50) hide show
  1. sd-webui-3d-open-pose-editor/public/icons/icon-144x144.png +0 -0
  2. sd-webui-3d-open-pose-editor/public/icons/icon-152x152.png +0 -0
  3. sd-webui-3d-open-pose-editor/public/icons/icon-192x192.png +0 -0
  4. sd-webui-3d-open-pose-editor/public/icons/icon-384x384.png +0 -0
  5. sd-webui-3d-open-pose-editor/public/icons/icon-48x48.png +0 -0
  6. sd-webui-3d-open-pose-editor/public/icons/icon-512x512.png +0 -0
  7. sd-webui-3d-open-pose-editor/public/icons/icon-72x72.png +0 -0
  8. sd-webui-3d-open-pose-editor/public/icons/icon-96x96.png +0 -0
  9. sd-webui-3d-open-pose-editor/public/mstile-150x150.png +0 -0
  10. sd-webui-3d-open-pose-editor/public/safari-pinned-tab.svg +61 -0
  11. sd-webui-3d-open-pose-editor/scripts/__pycache__/openpose_editor.cpython-310.pyc +0 -0
  12. sd-webui-3d-open-pose-editor/scripts/openpose_editor.py +276 -0
  13. sd-webui-3d-open-pose-editor/src/assets.ts +9 -0
  14. sd-webui-3d-open-pose-editor/src/body.ts +1293 -0
  15. sd-webui-3d-open-pose-editor/src/components/ContextMenu/index.tsx +145 -0
  16. sd-webui-3d-open-pose-editor/src/components/ContextMenu/styles.module.css +132 -0
  17. sd-webui-3d-open-pose-editor/src/components/Dialog/index.tsx +103 -0
  18. sd-webui-3d-open-pose-editor/src/components/Dialog/styles.module.css +124 -0
  19. sd-webui-3d-open-pose-editor/src/components/Loading/index.tsx +83 -0
  20. sd-webui-3d-open-pose-editor/src/components/Loading/styles.module.css +147 -0
  21. sd-webui-3d-open-pose-editor/src/components/Menu/index.tsx +416 -0
  22. sd-webui-3d-open-pose-editor/src/components/Menu/styles.module.css +131 -0
  23. sd-webui-3d-open-pose-editor/src/components/Oops.tsx +19 -0
  24. sd-webui-3d-open-pose-editor/src/components/PopupOver/index.tsx +348 -0
  25. sd-webui-3d-open-pose-editor/src/components/PopupOver/styles.module.css +115 -0
  26. sd-webui-3d-open-pose-editor/src/components/Slider/index.tsx +38 -0
  27. sd-webui-3d-open-pose-editor/src/components/Slider/styles.module.css +26 -0
  28. sd-webui-3d-open-pose-editor/src/components/Toast/index.tsx +91 -0
  29. sd-webui-3d-open-pose-editor/src/components/Toast/styles.module.css +142 -0
  30. sd-webui-3d-open-pose-editor/src/defines.ts +209 -0
  31. sd-webui-3d-open-pose-editor/src/editor.ts +1875 -0
  32. sd-webui-3d-open-pose-editor/src/entry.ts +3 -0
  33. sd-webui-3d-open-pose-editor/src/env.d.ts +3 -0
  34. sd-webui-3d-open-pose-editor/src/environments/extension/@types/webui.d.ts +6 -0
  35. sd-webui-3d-open-pose-editor/src/environments/extension/@types/window.d.ts +25 -0
  36. sd-webui-3d-open-pose-editor/src/environments/extension/assets.ts +19 -0
  37. sd-webui-3d-open-pose-editor/src/environments/extension/entry.ts +217 -0
  38. sd-webui-3d-open-pose-editor/src/environments/extension/internal/gradio.ts +78 -0
  39. sd-webui-3d-open-pose-editor/src/environments/extension/internal/message.ts +62 -0
  40. sd-webui-3d-open-pose-editor/src/environments/online/App.module.css +42 -0
  41. sd-webui-3d-open-pose-editor/src/environments/online/App.tsx +284 -0
  42. sd-webui-3d-open-pose-editor/src/environments/online/helper.ts +208 -0
  43. sd-webui-3d-open-pose-editor/src/environments/online/index.css +21 -0
  44. sd-webui-3d-open-pose-editor/src/environments/online/init.ts +8 -0
  45. sd-webui-3d-open-pose-editor/src/environments/online/main.tsx +18 -0
  46. sd-webui-3d-open-pose-editor/src/environments/online/update.ts +31 -0
  47. sd-webui-3d-open-pose-editor/src/hooks/index.ts +90 -0
  48. sd-webui-3d-open-pose-editor/src/hooks/useEventCall.ts +22 -0
  49. sd-webui-3d-open-pose-editor/src/hooks/useFoceUpdate.ts +11 -0
  50. sd-webui-3d-open-pose-editor/src/hooks/useMessageDispatch.ts +124 -0
sd-webui-3d-open-pose-editor/public/icons/icon-144x144.png ADDED
sd-webui-3d-open-pose-editor/public/icons/icon-152x152.png ADDED
sd-webui-3d-open-pose-editor/public/icons/icon-192x192.png ADDED
sd-webui-3d-open-pose-editor/public/icons/icon-384x384.png ADDED
sd-webui-3d-open-pose-editor/public/icons/icon-48x48.png ADDED
sd-webui-3d-open-pose-editor/public/icons/icon-512x512.png ADDED
sd-webui-3d-open-pose-editor/public/icons/icon-72x72.png ADDED
sd-webui-3d-open-pose-editor/public/icons/icon-96x96.png ADDED
sd-webui-3d-open-pose-editor/public/mstile-150x150.png ADDED
sd-webui-3d-open-pose-editor/public/safari-pinned-tab.svg ADDED
sd-webui-3d-open-pose-editor/scripts/__pycache__/openpose_editor.cpython-310.pyc ADDED
Binary file (7.69 kB). View file
 
sd-webui-3d-open-pose-editor/scripts/openpose_editor.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import html
2
+ import json
3
+ import os.path
4
+ import pathlib
5
+ import typing
6
+ import urllib.parse
7
+
8
+ import gradio as gr
9
+
10
+ try:
11
+ root_path = pathlib.Path(__file__).resolve().parents[1]
12
+ except NameError:
13
+ import inspect
14
+
15
+ root_path = pathlib.Path(inspect.getfile(lambda: None)).resolve().parents[1]
16
+
17
+
18
+ def get_asset_url(
19
+ file_path: pathlib.Path, append: typing.Optional[dict[str, str]] = None
20
+ ) -> str:
21
+ if append is None:
22
+ append = {"v": str(os.path.getmtime(file_path))}
23
+ else:
24
+ append = append.copy()
25
+ append["v"] = str(os.path.getmtime(file_path))
26
+ return f"/file={file_path.absolute()}?{urllib.parse.urlencode(append)}"
27
+
28
+
29
+ def write_config_file() -> pathlib.Path:
30
+ assets = {
31
+ "models/hand.fbx": get_asset_url(root_path / "models" / "hand.fbx"),
32
+ "models/foot.fbx": get_asset_url(root_path / "models" / "foot.fbx"),
33
+ "src/poses/data.bin": get_asset_url(root_path / "src" / "poses" / "data.bin"),
34
+ }
35
+
36
+ MEDIAPIPE_POSE_VERSION = "0.5.1675469404"
37
+ mediapipe_dir = root_path / "downloads" / "pose" / MEDIAPIPE_POSE_VERSION
38
+ for file_name in [
39
+ "pose_landmark_full.tflite",
40
+ "pose_web.binarypb",
41
+ "pose_solution_packed_assets.data",
42
+ "pose_solution_simd_wasm_bin.wasm",
43
+ "pose_solution_packed_assets_loader.js",
44
+ "pose_solution_simd_wasm_bin.js",
45
+ ]:
46
+ file_path = mediapipe_dir / file_name
47
+ if not file_path.exists():
48
+ continue
49
+ assets[file_name] = get_asset_url(file_path.absolute())
50
+
51
+ consts = {"assets": assets}
52
+
53
+ config_dir = root_path / "downloads"
54
+ config_dir.mkdir(mode=0o755, parents=True, exist_ok=True)
55
+ config_path = config_dir / "config.json"
56
+ config_path.write_text(json.dumps(consts))
57
+ return config_path
58
+
59
+
60
+ def on_ui_tabs():
61
+ with gr.Blocks(analytics_enabled=False) as blocks:
62
+ create_ui()
63
+ return [(blocks, "3D Openpose", "threedopenpose")]
64
+
65
+
66
+ def create_ui():
67
+ try:
68
+ from modules.shared import opts
69
+
70
+ cn_max: int = opts.control_net_max_models_num
71
+ use_online: bool = opts.openpose3d_use_online_version
72
+ except (ImportError, AttributeError):
73
+ cn_max = 0
74
+ use_online = False
75
+
76
+ if use_online:
77
+ html_url = "https://zhuyu1997.github.io/open-pose-editor/"
78
+ else:
79
+ config = {"config": get_asset_url(write_config_file()) or ""}
80
+ html_url = get_asset_url(root_path / "pages" / "index.html", config)
81
+
82
+ with gr.Tabs(elem_id="openpose3d_main"):
83
+ with gr.Tab(label="Edit Openpose"):
84
+ gr.HTML(
85
+ f"""
86
+ <iframe id="openpose3d_iframe" src="{html.escape(html_url)}"></iframe>
87
+ """
88
+ )
89
+ gr.Markdown(
90
+ "Original: [Online 3D Openpose Editor](https://zhuyu1997.github.io/open-pose-editor/)"
91
+ )
92
+ with gr.Tab(label="Send to ControlNet"):
93
+ with gr.Row():
94
+ send_t2i = gr.Button(value="Send to txt2img", variant="primary")
95
+ send_i2i = gr.Button(value="Send to img2img", variant="primary")
96
+ with gr.Row():
97
+ cn_dropdown_list = [str(i) for i in range(cn_max)]
98
+ cn_dropdown_list.insert(0, "-")
99
+ with gr.Column(variant="panel"):
100
+ pose_image = gr.Image(
101
+ label="Pose",
102
+ elem_id="openpose3d_pose_image",
103
+ )
104
+ with gr.Row():
105
+ pose_target = gr.Dropdown(
106
+ label="Control Model number",
107
+ choices=cn_dropdown_list,
108
+ value="0" if cn_max >= 1 else "-",
109
+ )
110
+ pose_download = gr.Button(value="Download")
111
+ with gr.Column(variant="panel"):
112
+ depth_image = gr.Image(
113
+ label="Depth",
114
+ elem_id="openpose3d_depth_image",
115
+ )
116
+ with gr.Row():
117
+ depth_target = gr.Dropdown(
118
+ label="Control Model number",
119
+ choices=cn_dropdown_list,
120
+ value="1" if cn_max >= 2 else "-",
121
+ )
122
+ depth_download = gr.Button(value="Download")
123
+ with gr.Column(variant="panel"):
124
+ normal_image = gr.Image(
125
+ label="Normal",
126
+ elem_id="openpose3d_normal_image",
127
+ )
128
+ with gr.Row():
129
+ normal_target = gr.Dropdown(
130
+ label="Control Model number",
131
+ choices=cn_dropdown_list,
132
+ value="2" if cn_max >= 3 else "-",
133
+ )
134
+ normal_download = gr.Button(value="Download")
135
+ with gr.Column(variant="panel"):
136
+ canny_image = gr.Image(
137
+ label="Canny",
138
+ elem_id="openpose3d_canny_image",
139
+ )
140
+ with gr.Row():
141
+ canny_target = gr.Dropdown(
142
+ label="Control Model number",
143
+ choices=cn_dropdown_list,
144
+ value="3" if cn_max >= 4 else "-",
145
+ )
146
+ canny_download = gr.Button(value="Download")
147
+
148
+ send_cn_inputs = [
149
+ pose_image,
150
+ pose_target,
151
+ depth_image,
152
+ depth_target,
153
+ normal_image,
154
+ normal_target,
155
+ canny_image,
156
+ canny_target,
157
+ ]
158
+ send_t2i.click(
159
+ None,
160
+ send_cn_inputs,
161
+ None,
162
+ _js="window.openpose3d.sendTxt2img",
163
+ )
164
+ send_i2i.click(
165
+ None,
166
+ send_cn_inputs,
167
+ None,
168
+ _js="window.openpose3d.sendImg2img",
169
+ )
170
+ pose_download.click(
171
+ None,
172
+ pose_image,
173
+ None,
174
+ _js="(v) => window.openpose3d.downloadImage(v, 'pose')",
175
+ )
176
+ depth_download.click(
177
+ None,
178
+ depth_image,
179
+ None,
180
+ _js="(v) => window.openpose3d.downloadImage(v, 'depth')",
181
+ )
182
+ normal_download.click(
183
+ None,
184
+ normal_image,
185
+ None,
186
+ _js="(v) => window.openpose3d.downloadImage(v, 'normal')",
187
+ )
188
+ canny_download.click(
189
+ None,
190
+ canny_image,
191
+ None,
192
+ _js="(v) => window.openpose3d.downloadImage(v, 'canny')",
193
+ )
194
+
195
+
196
+ def on_ui_settings():
197
+ from modules.shared import OptionInfo, opts
198
+
199
+ section = ("openpose3d", "3D Openpose Editor")
200
+
201
+ opts.add_option(
202
+ "openpose3d_use_online_version",
203
+ OptionInfo(False, "Use online version", section=section),
204
+ )
205
+
206
+
207
+ def main():
208
+ js_path = root_path / "javascript" / "index.js"
209
+ css_path = root_path / "style.css"
210
+
211
+ original_template_response = gr.routes.templates.TemplateResponse
212
+ head = """
213
+ <script>
214
+ function waitForElement(parent, selector) {
215
+ return new Promise((resolve) => {
216
+ const observer = new MutationObserver(() => {
217
+ if (!parent.querySelector(selector)) {
218
+ return
219
+ }
220
+ observer.disconnect()
221
+ resolve(undefined)
222
+ })
223
+
224
+ observer.observe(parent, {
225
+ childList: true,
226
+ subtree: true,
227
+ })
228
+
229
+ if (parent.querySelector(selector)) {
230
+ resolve(undefined)
231
+ }
232
+ })
233
+ }
234
+ let onTabChangedCallback
235
+ function gradioApp() {
236
+ const elems = document.getElementsByTagName('gradio-app')
237
+ const gradioShadowRoot = elems.length == 0 ? null : elems[0].shadowRoot
238
+ return gradioShadowRoot ? gradioShadowRoot : document
239
+ }
240
+ async function onUiLoaded(callback){
241
+ await waitForElement(gradioApp(), '#openpose3d_main')
242
+ await callback()
243
+ await onTabChangedCallback?.()
244
+ }
245
+ function onUiUpdate(callback){
246
+ onTabChangedCallback = callback
247
+ }
248
+ </script>
249
+ """
250
+ head += f"""
251
+ <script type="module">
252
+ document.addEventListener("DOMContentLoaded", function() {{import("{get_asset_url(js_path)}")}})
253
+ </script>
254
+ """
255
+
256
+ def template_response(*args, **kwargs):
257
+ res = original_template_response(*args, **kwargs)
258
+ res.body = res.body.replace(b"</head>", f"{head}</head>".encode("utf8"))
259
+ res.init_headers()
260
+ return res
261
+
262
+ gr.routes.templates.TemplateResponse = template_response
263
+
264
+ with gr.Blocks(analytics_enabled=False, css=css_path.read_text()) as blocks:
265
+ with gr.Tab(label="3D Openpose", elem_id="tab_threedopenpose"):
266
+ create_ui()
267
+ blocks.launch()
268
+
269
+
270
+ try:
271
+ from modules import script_callbacks
272
+
273
+ script_callbacks.on_ui_tabs(on_ui_tabs)
274
+ script_callbacks.on_ui_settings(on_ui_settings)
275
+ except ImportError:
276
+ main()
sd-webui-3d-open-pose-editor/src/assets.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import handFBXFileUrl from '../models/hand.fbx?url'
2
+ import footFBXFileUrl from '../models/foot.fbx?url'
3
+ import posesLibraryUrl from './poses/data.bin?url'
4
+
5
+ export default {
6
+ 'models/hand.fbx': handFBXFileUrl,
7
+ 'models/foot.fbx': footFBXFileUrl,
8
+ 'src/poses/data.bin': posesLibraryUrl,
9
+ }
sd-webui-3d-open-pose-editor/src/body.ts ADDED
@@ -0,0 +1,1293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from 'three'
2
+ import { Bone, Object3D } from 'three'
3
+ import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils'
4
+ import type { TupleToUnion } from 'type-fest'
5
+ import {
6
+ FindObjectItem,
7
+ GetLocalPosition,
8
+ GetWorldPosition,
9
+ } from './utils/three-utils'
10
+ import { CCDIKSolver } from './utils/CCDIKSolver'
11
+ import {
12
+ BoneThickness,
13
+ ConnectColor,
14
+ ToHexColor,
15
+ GetColorOfLinkByName,
16
+ OpenposeKeypoints,
17
+ OpenposeyKeypointsConst,
18
+ PartIndexMappingOfBlazePoseModel,
19
+ PartIndexMappingOfPoseModel,
20
+ } from './defines'
21
+ import {
22
+ ExtremitiesMapping,
23
+ FootObject,
24
+ HandObject,
25
+ IsMatchBonePrefix,
26
+ } from './models'
27
+
28
+ function GetPresetColorOfJoint(name: string) {
29
+ const index = OpenposeKeypoints.indexOf(name)
30
+ return index !== -1 ? ToHexColor(ConnectColor[index]) : 0x0
31
+ }
32
+
33
+ function CreateLink(
34
+ startObject: THREE.Object3D,
35
+ startName: string,
36
+ endName: string
37
+ ) {
38
+ const presetColor = GetColorOfLinkByName(startName, endName)
39
+ const material = new THREE.MeshBasicMaterial({
40
+ color: presetColor ?? 0x0,
41
+ opacity: 0.6,
42
+ transparent: true,
43
+ })
44
+ const mesh = new THREE.Mesh(
45
+ new THREE.SphereGeometry(BoneThickness),
46
+ material
47
+ )
48
+ mesh.name = startName + '_link_' + endName
49
+ startObject.add(mesh)
50
+ return mesh
51
+ }
52
+
53
+ function UpdateJointSphere(obj: Object3D, thickness = BoneThickness) {
54
+ const name = obj.name + '_joint_sphere'
55
+ obj.getObjectByName(name)?.scale.setScalar(thickness)
56
+ }
57
+
58
+ function UpdateLink4(
59
+ startObject: Object3D,
60
+ endObject: Object3D,
61
+ startName: string,
62
+ endName: string,
63
+ thickness = BoneThickness,
64
+ create = false
65
+ ) {
66
+ const startPosition = new THREE.Vector3(0, 0, 0)
67
+ const endPostion = endObject.position
68
+ const distance = startPosition.distanceTo(endPostion)
69
+ // 将拉伸后的球体放在中点,并计算旋转轴和角度
70
+ const origin = startPosition.clone().add(endPostion).multiplyScalar(0.5)
71
+
72
+ // Another method
73
+ // new THREE.Quaternion().setFromUnitVectors(...)
74
+ const v = endPostion.clone().sub(startPosition)
75
+ const unit = new THREE.Vector3(1, 0, 0)
76
+ const axis = unit.clone().cross(v)
77
+ const angle = unit.clone().angleTo(v)
78
+ const mesh = create
79
+ ? CreateLink(startObject, startName, endName)
80
+ : startObject.getObjectByName(startName + '_link_' + endName)!
81
+ mesh.scale.copy(
82
+ new THREE.Vector3(
83
+ distance / 2,
84
+ thickness / BoneThickness,
85
+ thickness / BoneThickness
86
+ )
87
+ )
88
+ mesh.position.copy(origin)
89
+ mesh.setRotationFromAxisAngle(axis.normalize(), angle)
90
+
91
+ UpdateJointSphere(startObject, thickness)
92
+ UpdateJointSphere(endObject, thickness)
93
+ }
94
+
95
+ function UpdateLink2(
96
+ startObject: Object3D,
97
+ endObject: Object3D,
98
+ thickness = BoneThickness,
99
+ create = false
100
+ ) {
101
+ UpdateLink4(
102
+ startObject,
103
+ endObject,
104
+ startObject.name,
105
+ endObject.name,
106
+ thickness,
107
+ create
108
+ )
109
+ }
110
+
111
+ function CreateSphere(name: string, thickness: number, color: number) {
112
+ const sphere = new THREE.Mesh(
113
+ new THREE.SphereGeometry(thickness),
114
+ new THREE.MeshBasicMaterial({ color: color })
115
+ )
116
+ sphere.name = name
117
+ return sphere
118
+ }
119
+
120
+ function CreateGroup(name: string, x = 0, y = 0, z = 0) {
121
+ const object = new THREE.Group()
122
+ object.name = name
123
+ object.translateX(x)
124
+ object.translateY(y)
125
+ object.translateZ(z)
126
+ return object
127
+ }
128
+
129
+ function CreateJoint(
130
+ name: string,
131
+ x = 0,
132
+ y = 0,
133
+ z = 0,
134
+ thickness: number = BoneThickness
135
+ ) {
136
+ return CreateGroup(name, x, y, z).add(
137
+ CreateSphere(
138
+ name + '_joint_sphere',
139
+ thickness,
140
+ GetPresetColorOfJoint(name)
141
+ )
142
+ )
143
+ }
144
+
145
+ let templateBody: THREE.Group | null = null
146
+
147
+ export function CloneBody() {
148
+ if (templateBody) {
149
+ return SkeletonUtils.clone(templateBody)
150
+ }
151
+ return null
152
+ }
153
+
154
+ const HandScale = 2.2
155
+ const FootScale = 0.25
156
+
157
+ function CreateHand(name: 'right_hand' | 'left_hand') {
158
+ if (!HandObject || !FootObject) {
159
+ throw new Error('Failed to create body')
160
+ }
161
+
162
+ if (name === 'right_hand') {
163
+ const right_hand = SkeletonUtils.clone(HandObject)
164
+
165
+ right_hand.name = 'right_hand'
166
+ right_hand.translateX(-0.4)
167
+ right_hand.translateY(3)
168
+ right_hand.rotateY(Math.PI)
169
+ right_hand.rotateZ(-Math.PI / 2)
170
+
171
+ right_hand.scale.multiplyScalar(HandScale)
172
+
173
+ right_hand.traverse((o) => {
174
+ if (IsBone(o.name)) {
175
+ o.name = o.name + '_R'
176
+ }
177
+ })
178
+ return right_hand
179
+ } else {
180
+ const left_hand = SkeletonUtils.clone(HandObject)
181
+
182
+ left_hand.name = 'left_hand'
183
+ left_hand.scale.x = -1
184
+ left_hand.translateX(0.4)
185
+ left_hand.translateY(3)
186
+ left_hand.rotateY(Math.PI)
187
+ left_hand.rotateZ(Math.PI / 2)
188
+ left_hand.scale.multiplyScalar(HandScale)
189
+
190
+ left_hand.traverse((o) => {
191
+ if (IsBone(o.name)) {
192
+ o.name = o.name + '_L'
193
+ }
194
+ })
195
+ return left_hand
196
+ }
197
+ }
198
+
199
+ function CreateFoot(name: 'right_foot' | 'left_foot') {
200
+ if (!HandObject || !FootObject) {
201
+ throw new Error('Failed to create body')
202
+ }
203
+
204
+ if (name === 'right_foot') {
205
+ const right_foot = SkeletonUtils.clone(FootObject)
206
+
207
+ right_foot.name = 'right_foot'
208
+ right_foot.scale.setX(-1)
209
+ right_foot.scale.multiplyScalar(FootScale)
210
+
211
+ right_foot.traverse((o) => {
212
+ if (IsBone(o.name)) {
213
+ o.name = o.name + '_R'
214
+ }
215
+ })
216
+
217
+ return right_foot
218
+ } else {
219
+ const left_foot = SkeletonUtils.clone(FootObject)
220
+ left_foot.name = 'left_foot'
221
+ left_foot.scale.multiplyScalar(FootScale)
222
+
223
+ left_foot.traverse((o) => {
224
+ if (IsBone(o.name)) {
225
+ o.name = o.name + '_L'
226
+ }
227
+ })
228
+ return left_foot
229
+ }
230
+ }
231
+
232
+ export function CreateTemplateBody() {
233
+ if (!HandObject || !FootObject) {
234
+ throw new Error('Failed to create body')
235
+ }
236
+ const width = 34
237
+ const height = 46
238
+
239
+ const torso = CreateGroup('torso', 0, 115, 0).add(
240
+ CreateSphere('center', BoneThickness, 0x888888),
241
+ CreateGroup('five', 0, height / 2, 0).add(
242
+ CreateJoint('neck').add(
243
+ CreateJoint('nose', 0, 20, 14).add(
244
+ CreateJoint('right_eye', -3, 3, -3).add(
245
+ CreateJoint('right_ear', -4, -3, -8)
246
+ ),
247
+ CreateJoint('left_eye', 3, 3, -3).add(
248
+ CreateJoint('left_ear', 4, -3, -8)
249
+ )
250
+ )
251
+ ),
252
+ CreateGroup('left_shoulder_inner').add(
253
+ CreateJoint('right_shoulder', -width / 2, 0, 0).add(
254
+ CreateJoint('right_elbow', 0, -25, 0).add(
255
+ CreateJoint('right_wrist', 0, -25, 0).add(
256
+ CreateHand('right_hand')
257
+ )
258
+ )
259
+ )
260
+ ),
261
+ CreateGroup('right_shoulder_inner').add(
262
+ CreateJoint('left_shoulder', width / 2, 0, 0).add(
263
+ CreateJoint('left_elbow', 0, -25, 0).add(
264
+ CreateJoint('left_wrist', 0, -25, 0).add(
265
+ CreateHand('left_hand')
266
+ )
267
+ )
268
+ )
269
+ ),
270
+ CreateGroup('right_hip_inner').add(
271
+ CreateJoint('right_hip', -width / 2 + 7, -height, 0).add(
272
+ CreateJoint('right_knee', 0, -40, 0).add(
273
+ CreateJoint('right_ankle', 0, -36, 0).add(
274
+ CreateFoot('right_foot')
275
+ )
276
+ )
277
+ )
278
+ ),
279
+ CreateGroup('left_hip_inner').add(
280
+ CreateJoint('left_hip', width / 2 - 7, -height, 0).add(
281
+ CreateJoint('left_knee', 0, -40, 0).add(
282
+ CreateJoint('left_ankle', 0, -36, 0).add(
283
+ CreateFoot('left_foot')
284
+ )
285
+ )
286
+ )
287
+ )
288
+ )
289
+ )
290
+
291
+ new BodyControlor(torso).Create()
292
+ templateBody = torso
293
+ }
294
+
295
+ function CreateIKTarget(body: Object3D, effector: Object3D, name: string) {
296
+ const target = new THREE.Mesh(
297
+ new THREE.BoxGeometry(
298
+ BoneThickness * 5,
299
+ BoneThickness * 5,
300
+ BoneThickness * 5
301
+ ),
302
+ new THREE.MeshBasicMaterial({
303
+ color: 0x0088ff,
304
+ transparent: true,
305
+ opacity: 0.5,
306
+ })
307
+ )
308
+
309
+ const effector_pos = GetWorldPosition(effector)
310
+ target.position.copy(GetLocalPosition(body, effector_pos))
311
+ target.name = name
312
+
313
+ return target
314
+ }
315
+
316
+ export function GetExtremityMesh(o: Object3D) {
317
+ if (!(o.name in ExtremitiesMapping)) {
318
+ return null
319
+ }
320
+ return FindObjectItem<THREE.SkinnedMesh>(
321
+ o,
322
+ ExtremitiesMapping[o.name].meshName
323
+ )
324
+ }
325
+
326
+ export function IsVirtualPoint(name: string) {
327
+ return [
328
+ 'right_shoulder_inner',
329
+ 'left_shoulder_inner',
330
+ 'right_hip_inner',
331
+ 'left_hip_inner',
332
+ 'five',
333
+ ].includes(name)
334
+ }
335
+
336
+ export function IsNeedSaveObject(name: string) {
337
+ if (OpenposeKeypoints.includes(name)) return true
338
+ if (name === 'right_hand' || name === 'left_hand') return true
339
+ if (name === 'right_foot' || name === 'left_foot') return true
340
+ if (IsVirtualPoint(name)) return true // virtual point
341
+ if (IsBone(name)) return true
342
+ if (name.includes('_joint_sphere')) return true
343
+ if (name.includes('_link_')) return true
344
+ return false
345
+ }
346
+
347
+ export function IsBone(name: string) {
348
+ return IsMatchBonePrefix(name)
349
+ }
350
+
351
+ const pickableObjectNames: string[] = [
352
+ 'torso',
353
+ 'nose',
354
+ 'neck',
355
+ 'right_shoulder',
356
+ 'left_shoulder',
357
+ 'right_elbow',
358
+ 'left_elbow',
359
+ 'right_hip',
360
+ 'left_hip',
361
+ 'right_knee',
362
+ 'left_knee',
363
+ // virtual point for better control
364
+ 'right_shoulder_inner',
365
+ 'left_shoulder_inner',
366
+ 'right_hip_inner',
367
+ 'left_hip_inner',
368
+ 'left_wrist_target',
369
+ 'right_wrist_target',
370
+ 'left_ankle_target',
371
+ 'right_ankle_target',
372
+ ]
373
+
374
+ export function IsPickable(name: string, isFreeMode = false) {
375
+ if (isFreeMode && OpenposeKeypoints.includes(name)) return true
376
+ if (pickableObjectNames.includes(name)) return true
377
+ if (IsBone(name)) return true
378
+ return false
379
+ }
380
+
381
+ export function IsTranslate(name: string, isFreeMode = false) {
382
+ if (isFreeMode)
383
+ return (
384
+ [
385
+ 'right_shoulder_inner',
386
+ 'left_shoulder_inner',
387
+ 'right_hip_inner',
388
+ 'left_hip_inner',
389
+ 'neck',
390
+ ].includes(name) == false
391
+ )
392
+
393
+ if (name.endsWith('_target')) return true
394
+ return false
395
+ }
396
+
397
+ export function IsTarget(name: string) {
398
+ if (name.endsWith('_target')) return true
399
+ return false
400
+ }
401
+
402
+ export function IsHand(name: string) {
403
+ return ['left_hand', 'right_hand'].includes(name)
404
+ }
405
+
406
+ export function IsFoot(name: string) {
407
+ return ['left_foot', 'right_foot'].includes(name)
408
+ }
409
+
410
+ export function IsMask(name: string) {
411
+ return ['foot_mask', 'hand_mask'].includes(name)
412
+ }
413
+
414
+ export function IsSkeleton(name: string) {
415
+ if (name == 'torso') return true
416
+ if (OpenposeKeypoints.includes(name)) return true
417
+ if (IsVirtualPoint(name)) return true // virtual point
418
+ if (name.includes('_joint_sphere')) return true
419
+ if (name.includes('_link_')) return true
420
+ return false
421
+ }
422
+
423
+ export function IsExtremities(name: string) {
424
+ return ['left_hand', 'right_hand', 'left_foot', 'right_foot'].includes(name)
425
+ }
426
+
427
+ const ControlablePart = [
428
+ ...OpenposeyKeypointsConst,
429
+ 'left_shoulder_inner',
430
+ 'right_shoulder_inner',
431
+ 'left_hip_inner',
432
+ 'right_hip_inner',
433
+ 'five',
434
+ 'right_hand',
435
+ 'left_hand',
436
+ 'left_foot',
437
+ 'right_foot',
438
+ 'torso',
439
+
440
+ 'left_wrist_target',
441
+ 'right_wrist_target',
442
+ 'left_ankle_target',
443
+ 'right_ankle_target',
444
+ ] as const
445
+
446
+ type ControlPartName = TupleToUnion<typeof ControlablePart>
447
+
448
+ export interface BodyData {
449
+ position?: ReturnType<THREE.Vector3['toArray']>
450
+ rotation?: ReturnType<THREE.Euler['toArray']>
451
+ scale?: ReturnType<THREE.Vector3['toArray']>
452
+
453
+ child: Record<
454
+ string,
455
+ {
456
+ position?: ReturnType<THREE.Vector3['toArray']>
457
+ rotation?: ReturnType<THREE.Euler['toArray']>
458
+ scale?: ReturnType<THREE.Vector3['toArray']>
459
+ }
460
+ >
461
+ }
462
+
463
+ export interface HandData {
464
+ child: Record<
465
+ string,
466
+ {
467
+ position?: ReturnType<THREE.Vector3['toArray']>
468
+ rotation?: ReturnType<THREE.Euler['toArray']>
469
+ scale?: ReturnType<THREE.Vector3['toArray']>
470
+ }
471
+ >
472
+ }
473
+
474
+ export class BodyControlor {
475
+ body: Object3D
476
+ part: Record<ControlPartName, Object3D> = {} as any
477
+ constructor(o: Object3D) {
478
+ this.body = o
479
+ this.body.traverse((o) => {
480
+ if (ControlablePart.includes(o.name as ControlPartName)) {
481
+ this.part[o.name as ControlPartName] = o
482
+ }
483
+ })
484
+
485
+ this.part['left_shoulder_inner'] = this.getObjectByName(
486
+ 'left_shoulder_inner'
487
+ )
488
+ this.part['right_shoulder_inner'] = this.getObjectByName(
489
+ 'right_shoulder_inner'
490
+ )
491
+ this.part['left_hip_inner'] = this.getObjectByName('left_hip_inner')
492
+ this.part['right_hip_inner'] = this.getObjectByName('right_hip_inner')
493
+ this.part['five'] = this.getObjectByName('five')
494
+ this.part['right_hand'] = this.getObjectByName('right_hand')
495
+ this.part['left_hand'] = this.getObjectByName('left_hand')
496
+ this.part['right_foot'] = this.getObjectByName('right_foot')
497
+ this.part['left_foot'] = this.getObjectByName('left_foot')
498
+ this.part['torso'] = this.body
499
+ }
500
+
501
+ getObjectByName(name: string) {
502
+ const part = this.body.getObjectByName(name)
503
+
504
+ if (!part) throw new Error(`Not found part: ${name}`)
505
+
506
+ return part
507
+ }
508
+ getWorldPosition(o: Object3D) {
509
+ const pos = new THREE.Vector3()
510
+ o.getWorldPosition(pos)
511
+ return pos
512
+ }
513
+
514
+ UpdateLink(
515
+ name: ControlPartName,
516
+ thickness = this.BoneThickness,
517
+ create = false
518
+ ) {
519
+ if (
520
+ [
521
+ 'left_hip',
522
+ 'right_hip',
523
+ 'right_shoulder',
524
+ 'left_shoulder',
525
+ ].includes(name)
526
+ )
527
+ UpdateLink4(
528
+ this.part[`${name}_inner` as ControlPartName],
529
+ this.part[name],
530
+ 'neck',
531
+ name,
532
+ thickness,
533
+ create
534
+ )
535
+ else if (name !== 'neck' && OpenposeKeypoints.includes(name)) {
536
+ UpdateLink2(
537
+ this.part[name].parent!,
538
+ this.part[name],
539
+ thickness,
540
+ create
541
+ )
542
+ }
543
+ }
544
+
545
+ get HeadSize() {
546
+ const size = this.getWorldPosition(this.part['right_ear']).distanceTo(
547
+ this.getWorldPosition(this.part['left_ear'])
548
+ )
549
+ return size
550
+ }
551
+
552
+ set HeadSize(value: number) {
553
+ const scale = value / this.HeadSize
554
+
555
+ const earLength = this.part['left_ear'].position.length() * scale
556
+ const eyeLength = this.part['left_eye'].position.length() * scale
557
+
558
+ this.part['left_ear'].position.normalize().multiplyScalar(earLength)
559
+ this.part['right_ear'].position.normalize().multiplyScalar(earLength)
560
+ this.part['left_eye'].position.normalize().multiplyScalar(eyeLength)
561
+ this.part['right_eye'].position.normalize().multiplyScalar(eyeLength)
562
+
563
+ this.UpdateLink('left_eye')
564
+ this.UpdateLink('right_eye')
565
+ this.UpdateLink('left_ear')
566
+ this.UpdateLink('right_ear')
567
+ }
568
+ get NoseToNeck() {
569
+ return this.part['nose'].position.length()
570
+ }
571
+ set NoseToNeck(value: number) {
572
+ this.part['nose'].position.normalize().multiplyScalar(value)
573
+ this.UpdateLink('nose')
574
+ }
575
+ get ShoulderToHip() {
576
+ return this.getDistanceOf(
577
+ this.getWorldPosition(this.part['five']),
578
+ this.getMidpoint(
579
+ this.getWorldPosition(this.part['left_hip']),
580
+ this.getWorldPosition(this.part['right_hip'])
581
+ )
582
+ )
583
+ }
584
+ set ShoulderToHip(value: number) {
585
+ const origin = this.ShoulderToHip
586
+
587
+ this.part['five'].position.normalize().multiplyScalar(value / 2)
588
+ this.part['left_hip'].position.multiplyScalar(value / origin)
589
+ this.part['right_hip'].position.multiplyScalar(value / origin)
590
+
591
+ this.UpdateLink('left_hip')
592
+ this.UpdateLink('right_hip')
593
+ }
594
+ get ShoulderWidth() {
595
+ return this.part['left_shoulder'].position.distanceTo(
596
+ this.part['right_shoulder'].position
597
+ )
598
+ }
599
+ set ShoulderWidth(width: number) {
600
+ const right_shoulder = this.part['right_shoulder']
601
+ right_shoulder.position.x = -width / 2
602
+ const left_shoulder = this.part['left_shoulder']
603
+ left_shoulder.position.x = width / 2
604
+
605
+ this.UpdateLink('right_shoulder')
606
+ this.UpdateLink('left_shoulder')
607
+ }
608
+
609
+ get UpperArm() {
610
+ return this.part['left_elbow'].position.length()
611
+ }
612
+ set UpperArm(length: number) {
613
+ this.part['left_elbow'].position.normalize().multiplyScalar(length)
614
+ this.part['right_elbow'].position.normalize().multiplyScalar(length)
615
+ this.UpdateLink('left_elbow')
616
+ this.UpdateLink('right_elbow')
617
+ }
618
+ get Forearm() {
619
+ return this.part['left_wrist'].position.length()
620
+ }
621
+ set Forearm(length: number) {
622
+ this.part['left_wrist'].position.normalize().multiplyScalar(length)
623
+ this.part['right_wrist'].position.normalize().multiplyScalar(length)
624
+
625
+ this.UpdateLink('left_wrist')
626
+ this.UpdateLink('right_wrist')
627
+ }
628
+
629
+ get ArmLength() {
630
+ return this.UpperArm + this.Forearm
631
+ }
632
+ set ArmLength(length: number) {
633
+ const origin = this.ArmLength
634
+ this.UpperArm = (length * this.UpperArm) / origin
635
+ this.Forearm = (length * this.Forearm) / origin
636
+ }
637
+
638
+ get Thigh() {
639
+ return this.part['left_knee'].position.length()
640
+ }
641
+ set Thigh(length: number) {
642
+ this.part['left_knee'].position.normalize().multiplyScalar(length)
643
+ this.part['right_knee'].position.normalize().multiplyScalar(length)
644
+
645
+ this.UpdateLink('left_knee')
646
+ this.UpdateLink('right_knee')
647
+ }
648
+
649
+ get HandSize() {
650
+ return Math.abs(this.part['left_hand'].scale.x) / HandScale
651
+ }
652
+ set HandSize(size: number) {
653
+ const origin = this.HandSize
654
+ this.part['left_hand'].scale
655
+ .divideScalar(origin * HandScale)
656
+ .multiplyScalar(size * HandScale)
657
+ this.part['right_hand'].scale
658
+ .divideScalar(origin * HandScale)
659
+ .multiplyScalar(size * HandScale)
660
+ }
661
+
662
+ get Hips() {
663
+ return this.getDistanceOf(
664
+ this.getWorldPosition(this.part['left_hip']),
665
+ this.getWorldPosition(this.part['right_hip'])
666
+ )
667
+ }
668
+ set Hips(width: number) {
669
+ const left = this.getWorldPosition(this.part['left_hip'])
670
+ const right = this.getWorldPosition(this.part['right_hip'])
671
+
672
+ const mid = this.getMidpoint(left, right)
673
+
674
+ // newLLeft = normalize(left - mid) * width + mid
675
+
676
+ const newLeft = left
677
+ .sub(mid)
678
+ .normalize()
679
+ .multiplyScalar(width / 2.0)
680
+ .add(mid)
681
+ const newRight = right
682
+ .sub(mid)
683
+ .normalize()
684
+ .multiplyScalar(width / 2.0)
685
+ .add(mid)
686
+
687
+ this.setPositionFromWorld(this.part['left_hip'], newLeft)
688
+ this.setPositionFromWorld(this.part['right_hip'], newRight)
689
+
690
+ this.UpdateLink('left_hip')
691
+ this.UpdateLink('right_hip')
692
+ }
693
+ get LowerLeg() {
694
+ return this.part['left_ankle'].position.length()
695
+ }
696
+ set LowerLeg(length: number) {
697
+ this.part['left_ankle'].position.normalize().multiplyScalar(length)
698
+ this.part['right_ankle'].position.normalize().multiplyScalar(length)
699
+
700
+ this.UpdateLink('left_ankle')
701
+ this.UpdateLink('right_ankle')
702
+ }
703
+
704
+ get LegLength() {
705
+ return this.Thigh + this.LowerLeg
706
+ }
707
+ set LegLength(length: number) {
708
+ const origin = this.LegLength
709
+ this.Thigh = (length * this.Thigh) / origin
710
+ this.LowerLeg = (length * this.LowerLeg) / origin
711
+ }
712
+
713
+ get FootSize() {
714
+ return Math.abs(this.part['left_foot'].scale.x) / FootScale
715
+ }
716
+ set FootSize(size: number) {
717
+ const origin = this.FootSize
718
+ this.part['left_foot'].scale
719
+ .divideScalar(origin * FootScale)
720
+ .multiplyScalar(size * FootScale)
721
+ this.part['right_foot'].scale
722
+ .divideScalar(origin * FootScale)
723
+ .multiplyScalar(size * FootScale)
724
+ }
725
+ getLocalPosition(obj: Object3D, postion: THREE.Vector3) {
726
+ return obj.worldToLocal(postion.clone())
727
+ }
728
+
729
+ setPositionFromWorld(obj: Object3D, postion: THREE.Vector3) {
730
+ return obj.position.copy(this.getLocalPosition(obj.parent!, postion))
731
+ }
732
+
733
+ getDirectionVectorByParentOf(
734
+ name: ControlPartName,
735
+ from: THREE.Vector3,
736
+ to: THREE.Vector3
737
+ ) {
738
+ const parent = this.part[name].parent!
739
+ const localFrom = this.getLocalPosition(parent, from)
740
+ const localTo = this.getLocalPosition(parent, to)
741
+
742
+ return localTo.clone().sub(localFrom).normalize()
743
+ }
744
+
745
+ rotateTo(name: ControlPartName, dir: THREE.Vector3) {
746
+ const obj = this.part[name]
747
+ const unit = obj.position.clone().normalize()
748
+ const axis = unit.clone().cross(dir)
749
+ const angle = unit.clone().angleTo(dir)
750
+ obj.parent?.rotateOnAxis(axis.normalize(), angle)
751
+ }
752
+
753
+ rotateTo3(name: ControlPartName, from: THREE.Vector3, to: THREE.Vector3) {
754
+ this.rotateTo(name, this.getDirectionVectorByParentOf(name, from, to))
755
+ }
756
+
757
+ setPositionByDistance(
758
+ name: ControlPartName,
759
+ from: THREE.Vector3,
760
+ to: THREE.Vector3
761
+ ) {
762
+ const dis = this.getDistanceOf(from, to)
763
+ this.part[name].position.normalize().multiplyScalar(dis)
764
+ }
765
+
766
+ setDirectionVector(name: ControlPartName, v: THREE.Vector3) {
767
+ const len = this.part[name].position.length()
768
+ this.part[name].position.copy(v).multiplyScalar(len)
769
+ this.UpdateLink(name)
770
+ }
771
+
772
+ getDistanceOf(from: THREE.Vector3, to: THREE.Vector3) {
773
+ return from.distanceTo(to)
774
+ }
775
+
776
+ getMidpoint(from: THREE.Vector3, to: THREE.Vector3) {
777
+ return from.clone().add(to).multiplyScalar(0.5)
778
+ }
779
+ ResetPose() {
780
+ templateBody?.traverse((o) => {
781
+ if (o.name in this.part) {
782
+ const name = o.name as ControlPartName
783
+
784
+ if (name == 'torso') this.part[name].position.setY(o.position.y)
785
+ else this.part[name].position.copy(o.position)
786
+
787
+ this.part[name].rotation.copy(o.rotation)
788
+ this.part[name].scale.copy(o.scale)
789
+ this.UpdateLink(name)
790
+ }
791
+ })
792
+ }
793
+
794
+ SetBlazePose(rawData: [number, number, number][]) {
795
+ this.ResetPose()
796
+
797
+ const data = Object.fromEntries(
798
+ Object.entries(PartIndexMappingOfBlazePoseModel).map(
799
+ ([name, index]) => {
800
+ return [
801
+ name,
802
+ new THREE.Vector3().fromArray(
803
+ rawData[index] ?? [0, 0, 0]
804
+ ),
805
+ ]
806
+ }
807
+ )
808
+ ) as Record<
809
+ keyof typeof PartIndexMappingOfBlazePoseModel,
810
+ THREE.Vector3
811
+ >
812
+
813
+ // this.Hips = this.getDistanceOf(data['right_hip'], data['left_hip'])
814
+ // this.Thigh = this.getDistanceOf(data['left_knee'], data['left_hip'])
815
+ // this.LowerLeg = this.getDistanceOf(
816
+ // data['left_ankle'],
817
+ // data['left_knee']
818
+ // )
819
+ // this.UpperArm = this.getDistanceOf(
820
+ // data['left_shoulder'],
821
+ // data['left_elbow']
822
+ // )
823
+ // this.Forearm = this.getDistanceOf(
824
+ // data['left_elbow'],
825
+ // data['left_wrist']
826
+ // )
827
+ // this.ShoulderWidth = this.getDistanceOf(
828
+ // data['left_shoulder'],
829
+ // data['right_shoulder']
830
+ // )
831
+
832
+ // this.NoseToNeck = this.getDistanceOf(
833
+ // data['nose'],
834
+ // this.getMidpoint(data['left_shoulder'], data['right_shoulder'])
835
+ // )
836
+
837
+ const map: [
838
+ ControlPartName,
839
+ [
840
+ THREE.Vector3 | keyof typeof PartIndexMappingOfBlazePoseModel,
841
+ THREE.Vector3 | keyof typeof PartIndexMappingOfBlazePoseModel
842
+ ]
843
+ ][] = [
844
+ [
845
+ 'five',
846
+ [
847
+ this.getMidpoint(
848
+ this.getMidpoint(data['left_hip'], data['right_hip']),
849
+ this.getMidpoint(
850
+ data['left_shoulder'],
851
+ data['right_shoulder']
852
+ )
853
+ ),
854
+ this.getMidpoint(
855
+ data['left_shoulder'],
856
+ data['right_shoulder']
857
+ ),
858
+ ],
859
+ ],
860
+
861
+ [
862
+ 'left_shoulder',
863
+ [
864
+ this.getMidpoint(
865
+ data['left_shoulder'],
866
+ data['right_shoulder']
867
+ ),
868
+ 'left_shoulder',
869
+ ],
870
+ ],
871
+ ['left_elbow', ['left_shoulder', 'left_elbow']],
872
+ ['left_wrist', ['left_elbow', 'left_wrist']],
873
+ [
874
+ 'left_hip',
875
+ [
876
+ this.getMidpoint(
877
+ data['left_shoulder'],
878
+ data['right_shoulder']
879
+ ),
880
+ 'left_hip',
881
+ ],
882
+ ],
883
+ ['left_knee', ['left_hip', 'left_knee']],
884
+ ['left_ankle', ['left_knee', 'left_ankle']],
885
+
886
+ [
887
+ 'right_shoulder',
888
+ [
889
+ this.getMidpoint(
890
+ data['left_shoulder'],
891
+ data['right_shoulder']
892
+ ),
893
+ 'right_shoulder',
894
+ ],
895
+ ],
896
+ ['right_elbow', ['right_shoulder', 'right_elbow']],
897
+ ['right_wrist', ['right_elbow', 'right_wrist']],
898
+
899
+ [
900
+ 'right_hip',
901
+ [
902
+ this.getMidpoint(
903
+ data['left_shoulder'],
904
+ data['right_shoulder']
905
+ ),
906
+ 'right_hip',
907
+ ],
908
+ ],
909
+ ['right_knee', ['right_hip', 'right_knee']],
910
+ ['right_ankle', ['right_knee', 'right_ankle']],
911
+
912
+ [
913
+ 'nose',
914
+ [
915
+ this.getMidpoint(
916
+ data['left_shoulder'],
917
+ data['right_shoulder']
918
+ ),
919
+ 'nose',
920
+ ],
921
+ ],
922
+ ['left_eye', ['nose', 'left_eye']],
923
+ ['right_eye', ['nose', 'right_eye']],
924
+ ['left_ear', ['left_eye', 'left_ear']],
925
+ ['right_ear', ['right_eye', 'right_ear']],
926
+ ]
927
+
928
+ for (const [name, [from, to]] of map) {
929
+ this.rotateTo3(
930
+ name,
931
+ from instanceof THREE.Vector3 ? from : data[from],
932
+ to instanceof THREE.Vector3 ? to : data[to]
933
+ )
934
+
935
+ this.setPositionByDistance(
936
+ name,
937
+ from instanceof THREE.Vector3 ? from : data[from],
938
+ to instanceof THREE.Vector3 ? to : data[to]
939
+ )
940
+
941
+ this.UpdateLink(name)
942
+ }
943
+ this.Update()
944
+ }
945
+ SetPose(rawData: [number, number, number][]) {
946
+ this.ResetPose()
947
+
948
+ const data = Object.fromEntries(
949
+ Object.entries(PartIndexMappingOfPoseModel).map(([name, index]) => {
950
+ return [
951
+ name,
952
+ new THREE.Vector3().fromArray(rawData[index] ?? [0, 0, 0]),
953
+ ]
954
+ })
955
+ ) as Record<keyof typeof PartIndexMappingOfPoseModel, THREE.Vector3>
956
+
957
+ this.part['torso'].position.setY(
958
+ this.getMidpoint(data['Hips'], data['Chest']).y
959
+ )
960
+ this.Hips = this.getDistanceOf(data['Hips'], data['UpLeg_L']) * 2
961
+ this.Thigh = this.getDistanceOf(data['UpLeg_L'], data['Leg_L'])
962
+ this.LowerLeg = this.getDistanceOf(data['Leg_L'], data['Foot_L'])
963
+ this.UpperArm = this.getDistanceOf(data['Arm_L'], data['ForeArm_L'])
964
+ this.Forearm = this.getDistanceOf(data['ForeArm_L'], data['Hand_L'])
965
+ this.ShoulderWidth =
966
+ 2 *
967
+ (this.getDistanceOf(data['Shoulder_L'], data['Arm_L']) +
968
+ this.getDistanceOf(data['Chest'], data['Shoulder_L']) /
969
+ Math.SQRT2)
970
+
971
+ const map: [
972
+ ControlPartName,
973
+ [
974
+ keyof typeof PartIndexMappingOfPoseModel,
975
+ keyof typeof PartIndexMappingOfPoseModel
976
+ ]
977
+ ][] = [
978
+ ['five', ['Hips', 'Chest']],
979
+ ['left_elbow', ['Arm_L', 'ForeArm_L']],
980
+ ['left_wrist', ['ForeArm_L', 'Hand_L']],
981
+ ['left_knee', ['UpLeg_L', 'Leg_L']],
982
+ ['left_ankle', ['Leg_L', 'Foot_L']],
983
+ ['right_elbow', ['Arm_R', 'ForeArm_R']],
984
+ ['right_wrist', ['ForeArm_R', 'Hand_R']],
985
+ ['right_knee', ['UpLeg_R', 'Leg_R']],
986
+ ['right_ankle', ['Leg_R', 'Foot_R']],
987
+ ]
988
+
989
+ for (const [name, [from, to]] of map)
990
+ this.rotateTo(
991
+ name,
992
+ this.getDirectionVectorByParentOf(name, data[from], data[to])
993
+ )
994
+ this.Update()
995
+ }
996
+
997
+ GetHandData(hand: 'left_hand' | 'right_hand'): HandData {
998
+ const o = this.part[hand]
999
+ const result: HandData = {
1000
+ child: {},
1001
+ }
1002
+ o.traverse((child) => {
1003
+ if (child.name && IsBone(child.name)) {
1004
+ if (child.name in result.child)
1005
+ console.log('Duplicate name', child.name, child)
1006
+ const data: Pick<BodyData, 'position' | 'rotation' | 'scale'> =
1007
+ {}
1008
+
1009
+ if (
1010
+ this.getDistanceOf(
1011
+ child.position,
1012
+ new THREE.Vector3(0, 0, 0)
1013
+ ) != 0
1014
+ ) {
1015
+ data.position = child.position.toArray()
1016
+ }
1017
+
1018
+ if (
1019
+ this.getDistanceOf(
1020
+ child.scale,
1021
+ new THREE.Vector3(1, 1, 1)
1022
+ ) != 0
1023
+ ) {
1024
+ data.scale = child.scale.toArray()
1025
+ }
1026
+
1027
+ if (
1028
+ child.rotation.x !== 0 ||
1029
+ child.rotation.y !== 0 ||
1030
+ child.rotation.z !== 0
1031
+ ) {
1032
+ data.rotation = child.rotation.toArray()
1033
+ }
1034
+ if (data) result.child[child.name] = data
1035
+ }
1036
+ })
1037
+
1038
+ return result
1039
+ }
1040
+
1041
+ RestoreHand(hand: 'left_hand' | 'right_hand', data: HandData) {
1042
+ data.child = Object.fromEntries(
1043
+ Object.entries(data.child).map(([k, v]) => {
1044
+ if (hand == 'left_hand') return [k.replace('_R', '_L'), v]
1045
+ if (hand == 'right_hand') return [k.replace('_L', '_R'), v]
1046
+ return [k, v]
1047
+ })
1048
+ )
1049
+ this.part[hand]?.traverse((o) => {
1050
+ if (o.name && o.name in data.child) {
1051
+ const child = data.child[o.name]
1052
+ if (child.position) o.position.fromArray(child.position)
1053
+ if (child.rotation) o.rotation.fromArray(child.rotation as any)
1054
+ if (child.scale) o.scale.fromArray(child.scale)
1055
+ }
1056
+ })
1057
+ }
1058
+
1059
+ GetBodyData(): BodyData {
1060
+ const o = this.part['torso']
1061
+ const result: BodyData = {
1062
+ position: o.position.toArray(),
1063
+ rotation: o.rotation.toArray(),
1064
+ scale: o.scale.toArray(),
1065
+ child: {},
1066
+ }
1067
+ o.traverse((child) => {
1068
+ if (child.name && IsNeedSaveObject(child.name)) {
1069
+ if (child.name in result.child)
1070
+ console.log('Duplicate name', child.name, child)
1071
+ const data: Pick<BodyData, 'position' | 'rotation' | 'scale'> =
1072
+ {}
1073
+
1074
+ if (
1075
+ this.getDistanceOf(
1076
+ child.position,
1077
+ new THREE.Vector3(0, 0, 0)
1078
+ ) != 0
1079
+ ) {
1080
+ data.position = child.position.toArray()
1081
+ }
1082
+
1083
+ if (
1084
+ this.getDistanceOf(
1085
+ child.scale,
1086
+ new THREE.Vector3(1, 1, 1)
1087
+ ) != 0
1088
+ ) {
1089
+ data.scale = child.scale.toArray()
1090
+ }
1091
+
1092
+ if (
1093
+ child.rotation.x !== 0 ||
1094
+ child.rotation.y !== 0 ||
1095
+ child.rotation.z !== 0
1096
+ ) {
1097
+ data.rotation = child.rotation.toArray()
1098
+ }
1099
+ if (data) result.child[child.name] = data
1100
+ }
1101
+ })
1102
+
1103
+ return result
1104
+ }
1105
+
1106
+ RestoreBody(data: BodyData) {
1107
+ const body = this.part['torso']
1108
+
1109
+ body?.traverse((o) => {
1110
+ if (o.name && o.name in data.child) {
1111
+ const child = data.child[o.name]
1112
+ if (child.position) o.position.fromArray(child.position)
1113
+ if (child.rotation) o.rotation.fromArray(child.rotation as any)
1114
+ if (child.scale) o.scale.fromArray(child.scale)
1115
+ }
1116
+ })
1117
+ if (data.position) body.position.fromArray(data.position)
1118
+ if (data.rotation) body.rotation.fromArray(data.rotation as any)
1119
+ if (data.scale) body.scale.fromArray(data.scale)
1120
+ }
1121
+
1122
+ UpdateBones(thickness = this.BoneThickness) {
1123
+ this.part['torso'].traverse((o) => {
1124
+ if (o.name in this.part) {
1125
+ const name = o.name as ControlPartName
1126
+ this.UpdateLink(name, thickness)
1127
+ }
1128
+ })
1129
+ }
1130
+
1131
+ CreateBones(thickness = this.BoneThickness) {
1132
+ this.part['torso'].traverse((o) => {
1133
+ if (o.name in this.part) {
1134
+ const name = o.name as ControlPartName
1135
+ this.UpdateLink(name, thickness, true)
1136
+ }
1137
+ })
1138
+ }
1139
+
1140
+ Create() {
1141
+ this.CreateBones()
1142
+
1143
+ this.part['torso'].add(
1144
+ CreateIKTarget(
1145
+ this.part['torso'],
1146
+ this.part['left_wrist'],
1147
+ 'left_wrist_target'
1148
+ )
1149
+ )
1150
+ this.part['torso'].add(
1151
+ CreateIKTarget(
1152
+ this.part['torso'],
1153
+ this.part['right_wrist'],
1154
+ 'right_wrist_target'
1155
+ )
1156
+ )
1157
+ this.part['torso'].add(
1158
+ CreateIKTarget(
1159
+ this.part['torso'],
1160
+ this.part['left_ankle'],
1161
+ 'left_ankle_target'
1162
+ )
1163
+ )
1164
+ this.part['torso'].add(
1165
+ CreateIKTarget(
1166
+ this.part['torso'],
1167
+ this.part['right_ankle'],
1168
+ 'right_ankle_target'
1169
+ )
1170
+ )
1171
+ }
1172
+
1173
+ Get18keyPointsData(): Array<[number, number, number]> {
1174
+ return OpenposeKeypoints.map((name) => {
1175
+ if (name in this.part) {
1176
+ return this.getWorldPosition(
1177
+ this.part[name as ControlPartName]
1178
+ ).toArray()
1179
+ } else {
1180
+ return [0, 0, 0]
1181
+ }
1182
+ })
1183
+ }
1184
+
1185
+ get BoneThickness() {
1186
+ return Math.abs(
1187
+ this.part['neck'].getObjectByName('neck_joint_sphere')?.scale.x ??
1188
+ BoneThickness
1189
+ )
1190
+ }
1191
+
1192
+ set BoneThickness(thickness: number) {
1193
+ this.UpdateBones(thickness)
1194
+ }
1195
+
1196
+ GetIKSolver() {
1197
+ return new CCDIKSolver([
1198
+ {
1199
+ target: this.part['left_wrist_target'],
1200
+ effector: this.part['left_wrist'],
1201
+ links: [
1202
+ {
1203
+ index: this.part['left_elbow'],
1204
+ enabled: true,
1205
+ },
1206
+ {
1207
+ index: this.part['left_shoulder'],
1208
+ enabled: true,
1209
+ },
1210
+ ],
1211
+ iteration: 10,
1212
+ minAngle: 0.0,
1213
+ maxAngle: 1.0,
1214
+ },
1215
+ {
1216
+ target: this.part['right_wrist_target'],
1217
+ effector: this.part['right_wrist'],
1218
+ links: [
1219
+ {
1220
+ index: this.part['right_elbow'],
1221
+ enabled: true,
1222
+ },
1223
+ {
1224
+ index: this.part['right_shoulder'],
1225
+ enabled: true,
1226
+ },
1227
+ ],
1228
+ iteration: 10,
1229
+ minAngle: 0.0,
1230
+ maxAngle: 1.0,
1231
+ },
1232
+ {
1233
+ target: this.part['left_ankle_target'],
1234
+ effector: this.part['left_ankle'],
1235
+ links: [
1236
+ {
1237
+ index: this.part['left_knee'],
1238
+ enabled: true,
1239
+ },
1240
+ {
1241
+ index: this.part['left_hip'],
1242
+ enabled: true,
1243
+ },
1244
+ ],
1245
+ iteration: 10,
1246
+ minAngle: 0.0,
1247
+ maxAngle: 1.0,
1248
+ },
1249
+ {
1250
+ target: this.part['right_ankle_target'],
1251
+ effector: this.part['right_ankle'],
1252
+ links: [
1253
+ {
1254
+ index: this.part['right_knee'],
1255
+ enabled: true,
1256
+ },
1257
+ {
1258
+ index: this.part['right_hip'],
1259
+ enabled: true,
1260
+ },
1261
+ ],
1262
+ iteration: 10,
1263
+ minAngle: 0.0,
1264
+ maxAngle: 1.0,
1265
+ },
1266
+ ])
1267
+ }
1268
+
1269
+ ResetTargetPosition(
1270
+ effectorName: ControlPartName,
1271
+ targetName: ControlPartName
1272
+ ) {
1273
+ const body = this.part['torso']
1274
+ const effector = this.part[effectorName]
1275
+ const target = this.part[targetName]
1276
+
1277
+ const effector_pos = GetWorldPosition(effector)
1278
+ target.position.copy(this.getLocalPosition(body, effector_pos))
1279
+ }
1280
+
1281
+ ResetAllTargetsPosition() {
1282
+ this.ResetTargetPosition('left_wrist', 'left_wrist_target')
1283
+ this.ResetTargetPosition('right_wrist', 'right_wrist_target')
1284
+ this.ResetTargetPosition('left_ankle', 'left_ankle_target')
1285
+ this.ResetTargetPosition('right_ankle', 'right_ankle_target')
1286
+ }
1287
+
1288
+ Update() {
1289
+ this.ResetAllTargetsPosition()
1290
+ this.UpdateBones()
1291
+ this.part['torso'].updateMatrixWorld(true)
1292
+ }
1293
+ }
sd-webui-3d-open-pose-editor/src/components/ContextMenu/index.tsx ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useMemo } from 'react'
2
+ import classes from './styles.module.css'
3
+ import NiceModal, { useModal } from '@ebay/nice-modal-react'
4
+ import { debounce } from 'lodash-es'
5
+ import { BodyEditor } from '../../editor'
6
+ import i18n, { IsChina } from '../../i18n'
7
+ import { Helper } from '../../environments/online/helper'
8
+ import { SetCDNBase } from '../../utils/detect'
9
+
10
+ const { Root, ContextMenuContent, ContextMenuItem, RightSlot } = classes
11
+
12
+ const MyContextMenu = NiceModal.create<{
13
+ editor: BodyEditor
14
+ mouseX: number
15
+ mouseY: number
16
+ onChangeBackground: (url: string) => void
17
+ }>(({ editor, mouseX, mouseY, onChangeBackground }) => {
18
+ const helper = useMemo(() => new Helper(editor), [editor])
19
+
20
+ const modal = useModal()
21
+ return (
22
+ <div
23
+ className={Root}
24
+ style={{
25
+ display: modal.visible ? undefined : 'none',
26
+ }}
27
+ onClick={() => {
28
+ modal.hide()
29
+ }}
30
+ onContextMenu={(e) => {
31
+ e.preventDefault()
32
+ }}
33
+ >
34
+ <div
35
+ className={ContextMenuContent}
36
+ style={{
37
+ top: mouseY,
38
+ left: mouseX,
39
+ }}
40
+ >
41
+ <div
42
+ className={ContextMenuItem}
43
+ onClick={() => {
44
+ editor.Undo()
45
+ }}
46
+ >
47
+ {i18n.t('Undo')}
48
+ <div className={RightSlot}>⌘ Z</div>
49
+ </div>
50
+ <div
51
+ className={ContextMenuItem}
52
+ onClick={() => {
53
+ editor.Redo()
54
+ }}
55
+ >
56
+ {i18n.t('Redo')}
57
+ <div className={RightSlot}>⇧ ⌘ Z</div>
58
+ </div>
59
+ <div
60
+ className={ContextMenuItem}
61
+ onClick={() => {
62
+ helper.CopySkeleton()
63
+ }}
64
+ >
65
+ {i18n.t('Duplicate Skeleton')}
66
+ <div className={RightSlot}>⇧ D</div>
67
+ </div>
68
+ <div
69
+ className={ContextMenuItem}
70
+ onClick={() => {
71
+ helper.RemoveSkeleton()
72
+ }}
73
+ >
74
+ {i18n.t('Delete Skeleton')}
75
+ <div className={RightSlot}>{i18n.t('Del')}</div>
76
+ </div>
77
+ <div
78
+ className={ContextMenuItem}
79
+ onClick={() => {
80
+ helper.CopyKeypointToClipboard()
81
+ }}
82
+ >
83
+ {i18n.t('Copy Keypoint Data')}
84
+ </div>
85
+ <div
86
+ className={ContextMenuItem}
87
+ onClick={() => {
88
+ helper.SetRandomPose()
89
+ }}
90
+ >
91
+ {i18n.t('Set Random Pose')}
92
+ </div>
93
+ <div
94
+ className={ContextMenuItem}
95
+ onClick={() => helper.DetectFromImage(onChangeBackground)}
96
+ >
97
+ {i18n.t('Detect From Image')}
98
+ </div>
99
+ {IsChina() ? (
100
+ <div
101
+ className={ContextMenuItem}
102
+ onClick={() => {
103
+ SetCDNBase(false)
104
+ helper.DetectFromImage(onChangeBackground)
105
+ }}
106
+ >
107
+ {i18n.t('Detect From Image') + ' [中国]'}
108
+ </div>
109
+ ) : undefined}
110
+ </div>
111
+ </div>
112
+ )
113
+ })
114
+
115
+ declare type NiceModalArgs<T> = T extends
116
+ | keyof JSX.IntrinsicElements
117
+ | React.JSXElementConstructor<any>
118
+ ? Omit<React.ComponentProps<T>, 'id'>
119
+ : Record<string, unknown>
120
+
121
+ export type ShowProps = NiceModalArgs<typeof MyContextMenu>
122
+
123
+ export function GetContextMenu(wait = 0) {
124
+ const show = debounce((props: ShowProps) => {
125
+ NiceModal.show(MyContextMenu, props)
126
+ }, wait)
127
+
128
+ return {
129
+ show: (props: ShowProps) => {
130
+ show(props)
131
+ },
132
+ hide: () => {
133
+ show.cancel()
134
+ HideContextMenu()
135
+ },
136
+ }
137
+ }
138
+
139
+ export async function ShowContextMenu(props: ShowProps): Promise<string> {
140
+ return await NiceModal.show(MyContextMenu, props)
141
+ }
142
+
143
+ export function HideContextMenu() {
144
+ return NiceModal.hide(MyContextMenu)
145
+ }
sd-webui-3d-open-pose-editor/src/components/ContextMenu/styles.module.css ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import '@radix-ui/colors/blackA.css';
2
+ @import '@radix-ui/colors/mauve.css';
3
+ @import '@radix-ui/colors/violet.css';
4
+
5
+ .Root {
6
+ position: fixed;
7
+ top: 0px;
8
+ left: 0px;
9
+ width: 100%;
10
+ height: 100%;
11
+ }
12
+
13
+ .ContextMenuContent {
14
+ animation-name: slideDownAndFade;
15
+ position: fixed;
16
+ top: 0px;
17
+ left: 0px;
18
+ }
19
+
20
+ .ContextMenuContent {
21
+ min-width: 220px;
22
+ background-color: white;
23
+ border-radius: 6px;
24
+ padding: 5px;
25
+ box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35),
26
+ 0px 10px 20px -15px rgba(22, 23, 24, 0.2);
27
+ animation-duration: 400ms;
28
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
29
+ will-change: transform, opacity;
30
+ }
31
+
32
+ .ContextMenuItem {
33
+ font-size: 13px;
34
+ line-height: 1;
35
+ color: var(--violet11);
36
+ border-radius: 3px;
37
+ display: flex;
38
+ align-items: center;
39
+ height: 25px;
40
+ padding: 0 5px;
41
+ position: relative;
42
+ padding-left: 25px;
43
+ user-select: none;
44
+ outline: none;
45
+ }
46
+
47
+ .ContextMenuItem:hover {
48
+ background-color: var(--violet9);
49
+ color: var(--violet1);
50
+ }
51
+
52
+ .ContextMenuSeparator {
53
+ height: 1px;
54
+ background-color: var(--violet6);
55
+ margin: 5px;
56
+ }
57
+
58
+ .IconButton {
59
+ all: unset;
60
+ font-family: inherit;
61
+ border-radius: 100%;
62
+ height: 35px;
63
+ width: 35px;
64
+ display: inline-flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ color: var(--violet11);
68
+ background-color: white;
69
+ box-shadow: 0 2px 10px var(--blackA7);
70
+ width: 100%;
71
+ height: 100%;
72
+ }
73
+ .IconButton:hover {
74
+ background-color: var(--violet3);
75
+ }
76
+ .IconButton:focus {
77
+ box-shadow: 0 0 0 2px black;
78
+ }
79
+
80
+ .RightSlot {
81
+ margin-left: auto;
82
+ padding-left: 20px;
83
+ color: var(--mauve11);
84
+ }
85
+
86
+ .ContextMenuItem:hover > .RightSlot {
87
+ color: white;
88
+ }
89
+
90
+ @keyframes slideUpAndFade {
91
+ from {
92
+ opacity: 0;
93
+ transform: translateY(2px);
94
+ }
95
+ to {
96
+ opacity: 1;
97
+ transform: translateY(0);
98
+ }
99
+ }
100
+
101
+ @keyframes slideRightAndFade {
102
+ from {
103
+ opacity: 0;
104
+ transform: translateX(-2px);
105
+ }
106
+ to {
107
+ opacity: 1;
108
+ transform: translateX(0);
109
+ }
110
+ }
111
+
112
+ @keyframes slideDownAndFade {
113
+ from {
114
+ opacity: 0;
115
+ transform: translateY(-2px);
116
+ }
117
+ to {
118
+ opacity: 1;
119
+ transform: translateY(0);
120
+ }
121
+ }
122
+
123
+ @keyframes slideLeftAndFade {
124
+ from {
125
+ opacity: 0;
126
+ transform: translateX(2px);
127
+ }
128
+ to {
129
+ opacity: 1;
130
+ transform: translateX(0);
131
+ }
132
+ }
sd-webui-3d-open-pose-editor/src/components/Dialog/index.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import * as Dialog from '@radix-ui/react-dialog'
3
+ import { Cross2Icon } from '@radix-ui/react-icons'
4
+ import classes from './styles.module.css'
5
+ import NiceModal, { useModal } from '@ebay/nice-modal-react'
6
+ import { debounce } from 'lodash-es'
7
+ import classNames from 'classnames'
8
+
9
+ const {
10
+ DialogOverlay,
11
+ DialogContent,
12
+ DialogTitle,
13
+ DialogDescription,
14
+ Button,
15
+ IconButton,
16
+ green,
17
+ } = classes
18
+
19
+ const MyDialog = NiceModal.create<{
20
+ title?: string
21
+ description?: string
22
+ children?: React.ReactNode
23
+ button?: string
24
+ }>(({ title, description, children, button }) => {
25
+ const modal = useModal()
26
+ return (
27
+ <Dialog.Root
28
+ defaultOpen={true}
29
+ open={modal.visible}
30
+ onOpenChange={(value) => {
31
+ if (value) modal.show()
32
+ else modal.hide()
33
+ }}
34
+ >
35
+ <Dialog.Portal>
36
+ <Dialog.Overlay className={DialogOverlay} />
37
+ <Dialog.Content className={DialogContent}>
38
+ <Dialog.Title className={DialogTitle}>{title}</Dialog.Title>
39
+ <Dialog.Description className={DialogDescription}>
40
+ {description}
41
+ </Dialog.Description>
42
+ <div> {children}</div>
43
+ <div
44
+ style={{
45
+ display: 'flex',
46
+ marginTop: 25,
47
+ justifyContent: 'flex-end',
48
+ }}
49
+ >
50
+ <Dialog.Close asChild>
51
+ <button
52
+ className={classNames(Button, green)}
53
+ onClick={() => {
54
+ modal.resolve('action')
55
+ }}
56
+ >
57
+ {button}
58
+ </button>
59
+ </Dialog.Close>
60
+ </div>
61
+ <Dialog.Close asChild>
62
+ <button className={IconButton}>
63
+ <Cross2Icon />
64
+ </button>
65
+ </Dialog.Close>
66
+ </Dialog.Content>
67
+ </Dialog.Portal>
68
+ </Dialog.Root>
69
+ )
70
+ })
71
+
72
+ declare type NiceModalArgs<T> = T extends
73
+ | keyof JSX.IntrinsicElements
74
+ | React.JSXElementConstructor<any>
75
+ ? Omit<React.ComponentProps<T>, 'id'>
76
+ : Record<string, unknown>
77
+
78
+ export type ShowProps = NiceModalArgs<typeof MyDialog>
79
+
80
+ export function GetDialog(wait = 0) {
81
+ const show = debounce((props: ShowProps) => {
82
+ NiceModal.show(MyDialog, props)
83
+ }, wait)
84
+
85
+ return {
86
+ show: (props: ShowProps) => {
87
+ show(props)
88
+ },
89
+ hide: () => {
90
+ show.cancel()
91
+ HideDialog()
92
+ },
93
+ }
94
+ }
95
+
96
+ export async function ShowDialog(props: ShowProps): Promise<string> {
97
+ console.log(props)
98
+ return await NiceModal.show(MyDialog, props)
99
+ }
100
+
101
+ export function HideDialog() {
102
+ return NiceModal.hide(MyDialog)
103
+ }
sd-webui-3d-open-pose-editor/src/components/Dialog/styles.module.css ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import '@radix-ui/colors/blackA.css';
2
+ @import '@radix-ui/colors/green.css';
3
+ @import '@radix-ui/colors/mauve.css';
4
+ @import '@radix-ui/colors/violet.css';
5
+
6
+ .DialogOverlay {
7
+ background-color: var(--blackA9);
8
+ position: fixed;
9
+ inset: 0;
10
+ animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
11
+ font-family: var(--openpose-editor-font-family);
12
+ }
13
+
14
+ .DialogContent {
15
+ font-family: var(--openpose-editor-font-family);
16
+ background-color: white;
17
+ border-radius: 6px;
18
+ box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
19
+ hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
20
+ position: fixed;
21
+ top: 50%;
22
+ left: 50%;
23
+ transform: translate(-50%, -50%);
24
+ width: 90vw;
25
+ max-width: 450px;
26
+ max-height: 85vh;
27
+ padding: 25px;
28
+ animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
29
+ }
30
+ .DialogContent:focus {
31
+ outline: none;
32
+ }
33
+
34
+ .DialogTitle {
35
+ margin: 0;
36
+ font-weight: 500;
37
+ color: var(--mauve12);
38
+ font-size: 32px;
39
+ }
40
+
41
+ .DialogDescription {
42
+ margin: 10px 0 20px;
43
+ color: var(--mauve11);
44
+ font-size: 15px;
45
+ line-height: 1.5;
46
+ overflow-y: auto;
47
+ word-break: break-all;
48
+ max-height: 60vh;
49
+ }
50
+
51
+ .Button {
52
+ all: unset;
53
+ display: inline-flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ border-radius: 4px;
57
+ padding: 0 15px;
58
+ font-size: 15px;
59
+ line-height: 1;
60
+ font-weight: 500;
61
+ height: 35px;
62
+ }
63
+ .Button.violet {
64
+ background-color: white;
65
+ color: var(--violet11);
66
+ box-shadow: 0 2px 10px var(--blackA7);
67
+ }
68
+ .Button.violet:hover {
69
+ background-color: var(--mauve3);
70
+ }
71
+ .Button.violet:focus {
72
+ box-shadow: 0 0 0 2px black;
73
+ }
74
+ .Button.green {
75
+ background-color: var(--green4);
76
+ color: var(--green11);
77
+ }
78
+ .Button.green:hover {
79
+ background-color: var(--green5);
80
+ }
81
+ .Button.green:focus {
82
+ box-shadow: 0 0 0 2px var(--green7);
83
+ }
84
+
85
+ .IconButton {
86
+ all: unset;
87
+ font-family: inherit;
88
+ border-radius: 100%;
89
+ height: 25px;
90
+ width: 25px;
91
+ display: inline-flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ color: var(--violet11);
95
+ position: absolute;
96
+ top: 10px;
97
+ right: 10px;
98
+ }
99
+ .IconButton:hover {
100
+ background-color: var(--violet4);
101
+ }
102
+ .IconButton:focus {
103
+ box-shadow: 0 0 0 2px var(--violet7);
104
+ }
105
+
106
+ @keyframes overlayShow {
107
+ from {
108
+ opacity: 0;
109
+ }
110
+ to {
111
+ opacity: 1;
112
+ }
113
+ }
114
+
115
+ @keyframes contentShow {
116
+ from {
117
+ opacity: 0;
118
+ transform: translate(-50%, -48%) scale(0.96);
119
+ }
120
+ to {
121
+ opacity: 1;
122
+ transform: translate(-50%, -50%) scale(1);
123
+ }
124
+ }
sd-webui-3d-open-pose-editor/src/components/Loading/index.tsx ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import * as AlertDialog from '@radix-ui/react-alert-dialog'
3
+ import NiceModal, { useModal } from '@ebay/nice-modal-react'
4
+ import { debounce } from 'lodash-es'
5
+
6
+ import classes from './styles.module.css'
7
+
8
+ const {
9
+ AlertDialogOverlay,
10
+ AlertDialogContent,
11
+ AlertDialogTitle,
12
+ 'lds-roller': LdsRoller,
13
+ } = classes
14
+
15
+ function CSSLoading() {
16
+ return (
17
+ <div className={LdsRoller}>
18
+ <div></div>
19
+ <div></div>
20
+ <div></div>
21
+ <div></div>
22
+ <div></div>
23
+ <div></div>
24
+ <div></div>
25
+ <div></div>
26
+ </div>
27
+ )
28
+ }
29
+
30
+ const Loading = NiceModal.create<{ title: string }>(
31
+ ({ title }: { title: string }) => {
32
+ const modal = useLoading()
33
+ return (
34
+ <AlertDialog.Root defaultOpen={true} open={modal.visible}>
35
+ <AlertDialog.Portal>
36
+ <AlertDialog.Overlay className={AlertDialogOverlay} />
37
+ <AlertDialog.Content className={AlertDialogContent}>
38
+ <AlertDialog.Title className={AlertDialogTitle}>
39
+ <CSSLoading></CSSLoading>
40
+ {title}
41
+ </AlertDialog.Title>
42
+ </AlertDialog.Content>
43
+ </AlertDialog.Portal>
44
+ </AlertDialog.Root>
45
+ )
46
+ }
47
+ )
48
+
49
+ export function useLoading() {
50
+ return useModal(Loading)
51
+ }
52
+
53
+ declare type NiceModalArgs<T> = T extends
54
+ | keyof JSX.IntrinsicElements
55
+ | React.JSXElementConstructor<any>
56
+ ? Omit<React.ComponentProps<T>, 'id'>
57
+ : Record<string, unknown>
58
+
59
+ export type ShowProps = NiceModalArgs<typeof Loading>
60
+
61
+ export function GetLoading(wait = 0) {
62
+ const show = debounce((props: ShowProps) => {
63
+ NiceModal.show(Loading, props)
64
+ }, wait)
65
+
66
+ return {
67
+ show: (props: ShowProps) => {
68
+ show(props)
69
+ },
70
+ hide: () => {
71
+ show.cancel()
72
+ HideLoading()
73
+ },
74
+ }
75
+ }
76
+
77
+ export function ShowLoading(props: ShowProps) {
78
+ NiceModal.show(Loading, props)
79
+ }
80
+
81
+ export function HideLoading() {
82
+ NiceModal.hide(Loading)
83
+ }
sd-webui-3d-open-pose-editor/src/components/Loading/styles.module.css ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import '@radix-ui/colors/blackA.css';
2
+ @import '@radix-ui/colors/mauve.css';
3
+ @import '@radix-ui/colors/red.css';
4
+ @import '@radix-ui/colors/violet.css';
5
+
6
+ .AlertDialogOverlay {
7
+ font-family: var(--openpose-editor-font-family);
8
+ background-color: var(--blackA9);
9
+ position: fixed;
10
+ inset: 0;
11
+ animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
12
+ }
13
+
14
+ .AlertDialogContent {
15
+ font-family: var(--openpose-editor-font-family);
16
+ background-color: white;
17
+ border-radius: 6px;
18
+ box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
19
+ hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
20
+ position: fixed;
21
+ top: 50%;
22
+ left: 50%;
23
+ transform: translate(-50%, -50%);
24
+ width: 90vw;
25
+ max-width: 500px;
26
+ max-height: 85vh;
27
+ padding: 25px;
28
+ animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
29
+ }
30
+ .AlertDialogContent:focus {
31
+ outline: none;
32
+ }
33
+
34
+ .AlertDialogTitle {
35
+ margin: 0;
36
+ color: var(--mauve12);
37
+ font-size: 27px;
38
+ font-weight: 500;
39
+ display: flex;
40
+ justify-content: flex-start;
41
+ align-items: center;
42
+ }
43
+
44
+ @keyframes overlayShow {
45
+ from {
46
+ opacity: 0;
47
+ }
48
+ to {
49
+ opacity: 1;
50
+ }
51
+ }
52
+
53
+ @keyframes contentShow {
54
+ from {
55
+ opacity: 0;
56
+ transform: translate(-50%, -48%) scale(0.96);
57
+ }
58
+ to {
59
+ opacity: 1;
60
+ transform: translate(-50%, -50%) scale(1);
61
+ }
62
+ }
63
+
64
+ .lds-roller {
65
+ display: inline-block;
66
+ position: relative;
67
+ min-width: 100px;
68
+ min-height: 80px;
69
+ }
70
+ .lds-roller div {
71
+ animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
72
+ transform-origin: 40px 40px;
73
+ }
74
+ .lds-roller div:after {
75
+ content: ' ';
76
+ display: block;
77
+ position: absolute;
78
+ width: 7px;
79
+ height: 7px;
80
+ border-radius: 50%;
81
+ background: gray;
82
+ margin: -4px 0 0 -4px;
83
+ }
84
+ .lds-roller div:nth-child(1) {
85
+ animation-delay: -0.036s;
86
+ }
87
+ .lds-roller div:nth-child(1):after {
88
+ top: 63px;
89
+ left: 63px;
90
+ }
91
+ .lds-roller div:nth-child(2) {
92
+ animation-delay: -0.072s;
93
+ }
94
+ .lds-roller div:nth-child(2):after {
95
+ top: 68px;
96
+ left: 56px;
97
+ }
98
+ .lds-roller div:nth-child(3) {
99
+ animation-delay: -0.108s;
100
+ }
101
+ .lds-roller div:nth-child(3):after {
102
+ top: 71px;
103
+ left: 48px;
104
+ }
105
+ .lds-roller div:nth-child(4) {
106
+ animation-delay: -0.144s;
107
+ }
108
+ .lds-roller div:nth-child(4):after {
109
+ top: 72px;
110
+ left: 40px;
111
+ }
112
+ .lds-roller div:nth-child(5) {
113
+ animation-delay: -0.18s;
114
+ }
115
+ .lds-roller div:nth-child(5):after {
116
+ top: 71px;
117
+ left: 32px;
118
+ }
119
+ .lds-roller div:nth-child(6) {
120
+ animation-delay: -0.216s;
121
+ }
122
+ .lds-roller div:nth-child(6):after {
123
+ top: 68px;
124
+ left: 24px;
125
+ }
126
+ .lds-roller div:nth-child(7) {
127
+ animation-delay: -0.252s;
128
+ }
129
+ .lds-roller div:nth-child(7):after {
130
+ top: 63px;
131
+ left: 17px;
132
+ }
133
+ .lds-roller div:nth-child(8) {
134
+ animation-delay: -0.288s;
135
+ }
136
+ .lds-roller div:nth-child(8):after {
137
+ top: 56px;
138
+ left: 12px;
139
+ }
140
+ @keyframes lds-roller {
141
+ 0% {
142
+ transform: rotate(0deg);
143
+ }
144
+ 100% {
145
+ transform: rotate(360deg);
146
+ }
147
+ }
sd-webui-3d-open-pose-editor/src/components/Menu/index.tsx ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useMemo, useState } from 'react'
2
+ import * as Menubar from '@radix-ui/react-menubar'
3
+ import { CheckIcon, DotFilledIcon } from '@radix-ui/react-icons'
4
+
5
+ import classes from './styles.module.css'
6
+ import { BodyEditor } from '../../editor'
7
+ import i18n, { IsChina } from '../../i18n'
8
+ import { Helper } from '../../environments/online/helper'
9
+ import { uploadImage } from '../../utils/transfer'
10
+ import { getCurrentTime } from '../../utils/time'
11
+ import useForceUpdate from '../../hooks/useFoceUpdate'
12
+ import classNames from 'classnames'
13
+ import { useLanguageSelect } from '../../hooks'
14
+ import { ShowContextMenu } from '../ContextMenu'
15
+ import { ShowDialog } from '../Dialog'
16
+ import { SetCDNBase } from '../../utils/detect'
17
+
18
+ const {
19
+ MenubarRoot,
20
+ MenubarTrigger,
21
+ MenubarContent,
22
+ MenubarItem,
23
+ MenubarCheckboxItem,
24
+ MenubarRadioItem,
25
+ MenubarItemIndicator,
26
+ MenubarSeparator,
27
+ RightSlot,
28
+ Blue,
29
+ inset,
30
+ } = classes
31
+ const MenubarDemo: React.FC<{
32
+ editor: BodyEditor
33
+ onChangeBackground: (url: string) => void
34
+ onScreenShot: (data: Record<string, { src: string; title: string }>) => void
35
+ style?: React.CSSProperties
36
+ }> = ({ editor, onChangeBackground, onScreenShot, style }) => {
37
+ const forceUpdate = useForceUpdate()
38
+ const helper = useMemo(() => new Helper(editor), [editor])
39
+ const { current, changeLanguage, languagList } = useLanguageSelect()
40
+
41
+ useEffect(() => {
42
+ const show = (data: { mouseX: number; mouseY: number }) => {
43
+ ShowContextMenu({ ...data, editor, onChangeBackground })
44
+ }
45
+ editor?.ContextMenuEventManager.AddEventListener(show)
46
+ return () => {
47
+ editor?.ContextMenuEventManager.RemoveEventListener(show)
48
+ }
49
+ }, [editor])
50
+
51
+ return (
52
+ <Menubar.Root className={MenubarRoot} style={style}>
53
+ <Menubar.Menu>
54
+ <Menubar.Trigger className={MenubarTrigger}>
55
+ {i18n.t('File')}
56
+ </Menubar.Trigger>
57
+ <Menubar.Portal>
58
+ <Menubar.Content
59
+ className={MenubarContent}
60
+ align="start"
61
+ sideOffset={5}
62
+ alignOffset={-3}
63
+ >
64
+ <Menubar.Item
65
+ className={MenubarItem}
66
+ onSelect={() => editor.ResetScene()}
67
+ >
68
+ {i18n.t('Reset Scene')}
69
+ </Menubar.Item>
70
+ <Menubar.Item
71
+ className={MenubarItem}
72
+ onSelect={() => editor.LoadScene()}
73
+ >
74
+ {i18n.t('Load Scene')}
75
+ </Menubar.Item>
76
+ <Menubar.Item
77
+ className={MenubarItem}
78
+ onSelect={() => editor.SaveScene()}
79
+ >
80
+ {i18n.t('Save Scene')}
81
+ </Menubar.Item>
82
+ <Menubar.Item
83
+ className={MenubarItem}
84
+ onSelect={() => helper.LoadGesture()}
85
+ >
86
+ {i18n.t('Load Gesture')}
87
+ </Menubar.Item>
88
+ <Menubar.Item
89
+ className={MenubarItem}
90
+ onSelect={() => helper.SaveGesture()}
91
+ >
92
+ {i18n.t('Save Gesture')}
93
+ </Menubar.Item>
94
+ <Menubar.Item
95
+ className={MenubarItem}
96
+ onSelect={() => {
97
+ helper.GenerateSceneURL()
98
+ }}
99
+ >
100
+ {i18n.t('Generate Scene URL')}
101
+ </Menubar.Item>
102
+ <Menubar.Separator className={MenubarSeparator} />
103
+ <Menubar.Item
104
+ className={MenubarItem}
105
+ onSelect={() => editor.RestoreLastSavedScene()}
106
+ >
107
+ {i18n.t('Restore Last Scene')}
108
+ </Menubar.Item>
109
+ <Menubar.Separator className={MenubarSeparator} />
110
+ <Menubar.Item
111
+ className={MenubarItem}
112
+ onSelect={() =>
113
+ helper.DetectFromImage(onChangeBackground)
114
+ }
115
+ >
116
+ {i18n.t('Detect From Image')}
117
+ </Menubar.Item>
118
+ {IsChina() ? (
119
+ <Menubar.Item
120
+ className={MenubarItem}
121
+ onSelect={() => {
122
+ SetCDNBase(false)
123
+ helper.DetectFromImage(onChangeBackground)
124
+ }}
125
+ >
126
+ {i18n.t('Detect From Image') + ' [中国]'}
127
+ </Menubar.Item>
128
+ ) : undefined}
129
+ <Menubar.Item
130
+ className={MenubarItem}
131
+ onSelect={() => helper.SetRandomPose()}
132
+ >
133
+ {i18n.t('Set Random Pose')}
134
+ </Menubar.Item>
135
+ <Menubar.Item
136
+ className={MenubarItem}
137
+ onSelect={async () => {
138
+ const image = await uploadImage()
139
+ if (image) onChangeBackground(image)
140
+ }}
141
+ >
142
+ {i18n.t('Set Background Image')}
143
+ </Menubar.Item>
144
+ <Menubar.Item className={MenubarItem}>
145
+ v{__APP_VERSION__}
146
+ </Menubar.Item>
147
+ </Menubar.Content>
148
+ </Menubar.Portal>
149
+ </Menubar.Menu>
150
+
151
+ <Menubar.Menu>
152
+ <Menubar.Trigger className={MenubarTrigger}>
153
+ {i18n.t('Edit')}
154
+ </Menubar.Trigger>
155
+ <Menubar.Portal>
156
+ <Menubar.Content
157
+ className={MenubarContent}
158
+ align="start"
159
+ sideOffset={5}
160
+ alignOffset={-3}
161
+ >
162
+ <Menubar.Item
163
+ className={MenubarItem}
164
+ onSelect={() => editor.Undo()}
165
+ >
166
+ {i18n.t('Undo')}
167
+ <div className={RightSlot}>⌘ Z</div>
168
+ </Menubar.Item>
169
+ <Menubar.Item
170
+ className={MenubarItem}
171
+ onSelect={() => editor.Redo()}
172
+ >
173
+ {i18n.t('Redo')}
174
+ <div className={RightSlot}>⇧ ⌘ Z</div>
175
+ </Menubar.Item>
176
+ <Menubar.Separator className={MenubarSeparator} />
177
+ <Menubar.Item
178
+ className={MenubarItem}
179
+ onSelect={() => editor.CopySelectedBody()}
180
+ >
181
+ {i18n.t('Duplicate Skeleton')}
182
+ <div className={RightSlot}>⇧ D</div>
183
+ </Menubar.Item>
184
+ <Menubar.Item
185
+ className={MenubarItem}
186
+ onSelect={() => editor.RemoveBody()}
187
+ >
188
+ {i18n.t('Delete Skeleton')}
189
+ <div className={RightSlot}>{i18n.t('Del')}</div>
190
+ </Menubar.Item>
191
+ </Menubar.Content>
192
+ </Menubar.Portal>
193
+ </Menubar.Menu>
194
+
195
+ <Menubar.Menu>
196
+ <Menubar.Trigger className={MenubarTrigger}>
197
+ {i18n.t('View')}
198
+ </Menubar.Trigger>
199
+ <Menubar.Portal>
200
+ <Menubar.Content
201
+ className={MenubarContent}
202
+ align="start"
203
+ sideOffset={5}
204
+ alignOffset={-14}
205
+ >
206
+ <Menubar.Item
207
+ className={classNames(MenubarItem, inset)}
208
+ onSelect={() => {
209
+ editor.LockView()
210
+ }}
211
+ >
212
+ {i18n.t('Lock View')}
213
+ </Menubar.Item>
214
+ <Menubar.Item
215
+ className={classNames(MenubarItem, inset)}
216
+ onSelect={() => {
217
+ editor.UnlockView()
218
+ }}
219
+ >
220
+ {i18n.t('Unlock View')}
221
+ </Menubar.Item>
222
+ <Menubar.Item
223
+ className={classNames(MenubarItem, inset)}
224
+ onSelect={() => {
225
+ editor.RestoreView()
226
+ }}
227
+ >
228
+ {i18n.t('Restore View')}
229
+ </Menubar.Item>
230
+ </Menubar.Content>
231
+ </Menubar.Portal>
232
+ </Menubar.Menu>
233
+
234
+ <Menubar.Menu>
235
+ <Menubar.Trigger className={MenubarTrigger}>
236
+ {i18n.t('Setting')}
237
+ </Menubar.Trigger>
238
+ <Menubar.Portal>
239
+ <Menubar.Content
240
+ className={MenubarContent}
241
+ align="start"
242
+ sideOffset={5}
243
+ alignOffset={-14}
244
+ >
245
+ <Menubar.CheckboxItem
246
+ className={classNames(MenubarCheckboxItem, inset)}
247
+ checked={editor.MoveMode}
248
+ onCheckedChange={() => {
249
+ editor.MoveMode = !editor.MoveMode
250
+ forceUpdate()
251
+ }}
252
+ >
253
+ <Menubar.ItemIndicator
254
+ className={MenubarItemIndicator}
255
+ >
256
+ <CheckIcon />
257
+ </Menubar.ItemIndicator>
258
+ {i18n.t('Move Mode')}
259
+ </Menubar.CheckboxItem>
260
+ <Menubar.CheckboxItem
261
+ className={classNames(MenubarCheckboxItem, inset)}
262
+ checked={editor.FreeMode}
263
+ onCheckedChange={() => {
264
+ editor.FreeMode = !editor.FreeMode
265
+ forceUpdate()
266
+ }}
267
+ >
268
+ <Menubar.ItemIndicator
269
+ className={MenubarItemIndicator}
270
+ >
271
+ <CheckIcon />
272
+ </Menubar.ItemIndicator>
273
+ {i18n.t('Free Mode')}
274
+ </Menubar.CheckboxItem>
275
+ <Menubar.CheckboxItem
276
+ className={classNames(MenubarCheckboxItem, inset)}
277
+ checked={editor.OnlyHand}
278
+ onCheckedChange={() => {
279
+ editor.OnlyHand = !editor.OnlyHand
280
+ forceUpdate()
281
+ }}
282
+ >
283
+ <Menubar.ItemIndicator
284
+ className={MenubarItemIndicator}
285
+ >
286
+ <CheckIcon />
287
+ </Menubar.ItemIndicator>
288
+ {i18n.t('Only Hand')}
289
+ </Menubar.CheckboxItem>
290
+ <Menubar.CheckboxItem
291
+ className={classNames(MenubarCheckboxItem, inset)}
292
+ checked={editor.enablePreview}
293
+ onCheckedChange={() => {
294
+ editor.enablePreview = !editor.enablePreview
295
+ forceUpdate()
296
+ }}
297
+ >
298
+ <Menubar.ItemIndicator
299
+ className={MenubarItemIndicator}
300
+ >
301
+ <CheckIcon />
302
+ </Menubar.ItemIndicator>
303
+ {i18n.t('Show Preview')}
304
+ </Menubar.CheckboxItem>
305
+ <Menubar.CheckboxItem
306
+ className={classNames(MenubarCheckboxItem, inset)}
307
+ checked={editor.EnableHelper}
308
+ onCheckedChange={() => {
309
+ editor.EnableHelper = !editor.EnableHelper
310
+ forceUpdate()
311
+ }}
312
+ >
313
+ <Menubar.ItemIndicator
314
+ className={MenubarItemIndicator}
315
+ >
316
+ <CheckIcon />
317
+ </Menubar.ItemIndicator>
318
+ {i18n.t('Show Grid')}
319
+ </Menubar.CheckboxItem>
320
+ </Menubar.Content>
321
+ </Menubar.Portal>
322
+ </Menubar.Menu>
323
+
324
+ <Menubar.Menu>
325
+ <Menubar.Trigger className={MenubarTrigger}>
326
+ {i18n.t('Feedback')}
327
+ </Menubar.Trigger>
328
+ <Menubar.Portal>
329
+ <Menubar.Content
330
+ className={MenubarContent}
331
+ align="start"
332
+ sideOffset={5}
333
+ alignOffset={-14}
334
+ >
335
+ <Menubar.Item
336
+ className={classNames(MenubarItem, inset)}
337
+ onSelect={() => {
338
+ helper.FeedbackByGithub()
339
+ }}
340
+ >
341
+ Github
342
+ </Menubar.Item>
343
+ <Menubar.Item
344
+ className={classNames(MenubarItem, inset)}
345
+ onSelect={() => {
346
+ helper.FeedbackByQQ()
347
+ }}
348
+ >
349
+ QQ
350
+ </Menubar.Item>
351
+ </Menubar.Content>
352
+ </Menubar.Portal>
353
+ </Menubar.Menu>
354
+ <Menubar.Menu>
355
+ <Menubar.Trigger className={MenubarTrigger}>
356
+ Language
357
+ </Menubar.Trigger>
358
+ <Menubar.Portal>
359
+ <Menubar.Content
360
+ className={MenubarContent}
361
+ align="start"
362
+ sideOffset={5}
363
+ alignOffset={-14}
364
+ >
365
+ <Menubar.RadioGroup
366
+ value={current}
367
+ onValueChange={(v) => {
368
+ changeLanguage(v)
369
+ }}
370
+ >
371
+ {languagList.map((item) => (
372
+ <Menubar.RadioItem
373
+ className={classNames(
374
+ MenubarRadioItem,
375
+ inset
376
+ )}
377
+ key={item}
378
+ value={item}
379
+ >
380
+ <Menubar.ItemIndicator
381
+ className={MenubarItemIndicator}
382
+ >
383
+ <DotFilledIcon />
384
+ </Menubar.ItemIndicator>
385
+ {item}
386
+ </Menubar.RadioItem>
387
+ ))}
388
+ </Menubar.RadioGroup>
389
+ </Menubar.Content>
390
+ </Menubar.Portal>
391
+ </Menubar.Menu>
392
+ <Menubar.Menu>
393
+ <Menubar.Trigger
394
+ className={classNames(MenubarTrigger, Blue)}
395
+ onClick={async () => {
396
+ const image = editor.MakeImages()
397
+ const result = Object.fromEntries(
398
+ Object.entries(image).map(([name, imgData]) => [
399
+ name,
400
+ {
401
+ src: imgData,
402
+ title: name + '_' + getCurrentTime(),
403
+ },
404
+ ])
405
+ )
406
+ onScreenShot(result)
407
+ }}
408
+ >
409
+ {i18n.t('Generate')}
410
+ </Menubar.Trigger>
411
+ </Menubar.Menu>
412
+ </Menubar.Root>
413
+ )
414
+ }
415
+
416
+ export default MenubarDemo
sd-webui-3d-open-pose-editor/src/components/Menu/styles.module.css ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import '@radix-ui/colors/blackA.css';
2
+ @import '@radix-ui/colors/mauve.css';
3
+ @import '@radix-ui/colors/violet.css';
4
+
5
+ .MenubarRoot {
6
+ display: flex;
7
+ background-color: white;
8
+ padding: 3px;
9
+ border-radius: 6px;
10
+ box-shadow: 0 2px 10px var(--blackA7);
11
+ font-family: var(--openpose-editor-font-family);
12
+ }
13
+
14
+ .MenubarTrigger {
15
+ all: unset;
16
+ padding: 8px 12px;
17
+ outline: none;
18
+ user-select: none;
19
+ font-weight: 500;
20
+ line-height: 1;
21
+ border-radius: 4px;
22
+ color: var(--violet11);
23
+ font-size: 13px;
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: space-between;
27
+ gap: 2px;
28
+ }
29
+
30
+ .MenubarTrigger[data-highlighted],
31
+ .MenubarTrigger[data-state='open'] {
32
+ background-color: var(--violet4);
33
+ }
34
+
35
+ .MenubarTrigger.Blue {
36
+ background-color: var(--violet9);
37
+ color: white;
38
+ }
39
+
40
+ .MenubarContent,
41
+ .MenubarSubContent {
42
+ min-width: 220px;
43
+ background-color: white;
44
+ border-radius: 6px;
45
+ padding: 5px;
46
+ box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35),
47
+ 0px 10px 20px -15px rgba(22, 23, 24, 0.2);
48
+ animation-duration: 400ms;
49
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
50
+ will-change: transform, opacity;
51
+ }
52
+
53
+ .MenubarItem,
54
+ .MenubarSubTrigger,
55
+ .MenubarCheckboxItem,
56
+ .MenubarRadioItem {
57
+ all: unset;
58
+ font-size: 13px;
59
+ line-height: 1;
60
+ color: var(--violet11);
61
+ border-radius: 4px;
62
+ display: flex;
63
+ align-items: center;
64
+ height: 25px;
65
+ padding: 0 10px;
66
+ position: relative;
67
+ user-select: none;
68
+ font-family: var(--openpose-editor-font-family);
69
+ }
70
+
71
+ .MenubarItem.inset,
72
+ .MenubarSubTrigger.inset,
73
+ .MenubarCheckboxItem.inset,
74
+ .MenubarRadioItem.inset {
75
+ padding-left: 20px;
76
+ }
77
+
78
+ .MenubarItem[data-state='open'],
79
+ .MenubarSubTrigger[data-state='open'] {
80
+ background-color: var(--violet4);
81
+ color: var(--violet11);
82
+ }
83
+
84
+ .MenubarItem[data-highlighted],
85
+ .MenubarSubTrigger[data-highlighted],
86
+ .MenubarCheckboxItem[data-highlighted],
87
+ .MenubarRadioItem[data-highlighted] {
88
+ background-image: linear-gradient(
89
+ 135deg,
90
+ var(--violet9) 0%,
91
+ var(--violet10) 100%
92
+ );
93
+ color: var(--violet1);
94
+ }
95
+
96
+ .MenubarItem[data-disabled],
97
+ .MenubarSubTrigger[data-disabled],
98
+ .MenubarCheckboxItem[data-disabled],
99
+ .MenubarRadioItem[data-disabled] {
100
+ color: var(--mauve8);
101
+ pointer-events: none;
102
+ }
103
+
104
+ .MenubarItemIndicator {
105
+ position: absolute;
106
+ left: 0;
107
+ width: 20px;
108
+ display: inline-flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ }
112
+
113
+ .MenubarSeparator {
114
+ height: 1px;
115
+ background-color: var(--violet6);
116
+ margin: 5px;
117
+ }
118
+
119
+ .RightSlot {
120
+ margin-left: auto;
121
+ padding-left: 20px;
122
+ color: var(--mauve9);
123
+ }
124
+
125
+ [data-highlighted] > .RightSlot {
126
+ color: white;
127
+ }
128
+
129
+ [data-disabled] > .RightSlot {
130
+ color: var(--mauve8);
131
+ }
sd-webui-3d-open-pose-editor/src/components/Oops.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import i18n from '../i18n'
2
+ import { ShowDialog } from './Dialog'
3
+
4
+ export function Oops(error: any) {
5
+ const stack = error?.stack
6
+ const text = `${i18n.t('Something went wrong!')}\n${stack || error}`
7
+ ShowDialog({
8
+ title: i18n.t('Oops...') ?? '',
9
+ description: text,
10
+ children: (
11
+ <a href="https://github.com/nonnonstop/sd-webui-3d-open-pose-editor/issues/new/choose">
12
+ {i18n.t(
13
+ 'If the problem persists, please click here to ask a question.'
14
+ )}
15
+ </a>
16
+ ),
17
+ button: i18n.t('Close') ?? '',
18
+ })
19
+ }
sd-webui-3d-open-pose-editor/src/components/PopupOver/index.tsx ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useMemo, useState } from 'react'
2
+ import * as Popover from '@radix-ui/react-popover'
3
+ import { PinLeftIcon } from '@radix-ui/react-icons'
4
+ import classes from './styles.module.css'
5
+ import Slider from '../Slider'
6
+ import { BodyEditor } from '../../editor'
7
+ import useForceUpdate from '../../hooks/useFoceUpdate'
8
+ import i18n from '../../i18n'
9
+ import { BodyControlor } from '../../body'
10
+
11
+ const { PopoverContent, IconButton, PopoverArrow, Input } = classes
12
+
13
+ const Slider2: React.FC<{
14
+ type: 'int' | 'float' | undefined
15
+ name: string
16
+ range: [number, number]
17
+ getValue(): number
18
+ onChange?: (value: number) => void
19
+ onValueCommit?: (value: number) => void
20
+ forceUpdate: () => any
21
+ }> = ({
22
+ type,
23
+ name,
24
+ range,
25
+ getValue,
26
+ onChange,
27
+ onValueCommit,
28
+ forceUpdate,
29
+ }) => {
30
+ const value = getValue()
31
+ const [inputValue, setInputValue] = useState(() =>
32
+ type == 'int' ? value.toString() : getValue().toFixed(2)
33
+ )
34
+
35
+ useEffect(() => {
36
+ setInputValue(type == 'int' ? value.toString() : getValue().toFixed(2))
37
+ }, [value])
38
+
39
+ return (
40
+ <div
41
+ style={{
42
+ display: 'flex',
43
+ justifyContent: 'flex-end',
44
+ }}
45
+ >
46
+ <div
47
+ style={{
48
+ minWidth: 60,
49
+ maxWidth: 120,
50
+ // width:"max-content",
51
+ overflow: 'hidden',
52
+ color: 'gray',
53
+ fontSize: '70%',
54
+ marginInlineEnd: 10,
55
+ // whiteSpace: 'nowrap',
56
+ textAlign: 'end',
57
+ textOverflow: 'ellipsis',
58
+ }}
59
+ >
60
+ {name}
61
+ </div>
62
+ <Slider
63
+ key={name}
64
+ range={range}
65
+ value={getValue()}
66
+ onValueChange={(value: number) => {
67
+ if (type == 'int') {
68
+ onChange?.(Math.round(value))
69
+ } else onChange?.(value)
70
+ forceUpdate()
71
+ }}
72
+ onValueCommit={onValueCommit}
73
+ style={{
74
+ width: 150,
75
+ }}
76
+ ></Slider>
77
+ <input
78
+ className={Input}
79
+ style={{
80
+ marginInlineStart: 10,
81
+ width: 60,
82
+ height: 20,
83
+ color: 'gray',
84
+ fontSize: '70%',
85
+ }}
86
+ value={inputValue}
87
+ onChange={(event) => {
88
+ const value = event.target.value
89
+
90
+ setInputValue(value)
91
+ }}
92
+ onBlur={() => {
93
+ try {
94
+ let v = parseFloat(inputValue)
95
+ if (isNaN(v)) throw 'Is NaN'
96
+ v = Math.max(Math.min(v, range[1]), range[0])
97
+ console.log(v)
98
+ onChange?.(v)
99
+ } catch (error) {
100
+ console.log('invalid input')
101
+ setInputValue(value.toString())
102
+ }
103
+ }}
104
+ ></input>
105
+ </div>
106
+ )
107
+ }
108
+
109
+ function GetCameraParamControlor(editor: BodyEditor) {
110
+ const CameraParamsInit = {
111
+ OutputWidth: { type: 'int', range: [128, 3000], name: i18n.t('Width') },
112
+ OutputHeight: {
113
+ type: 'int',
114
+ range: [128, 3000],
115
+ name: i18n.t('Height'),
116
+ },
117
+ CameraNear: { range: [0.1, 2000], name: i18n.t('Camera Near') },
118
+ CameraFar: { range: [0.1, 20000], name: i18n.t('Camera Far') },
119
+ CameraFocalLength: {
120
+ range: [0.1, 100],
121
+ name: i18n.t('Camera Focal Length'),
122
+ },
123
+ } as const
124
+
125
+ return Object.entries(
126
+ CameraParamsInit as Record<
127
+ keyof typeof CameraParamsInit,
128
+ {
129
+ type: 'int' | 'float' | undefined
130
+ range: [number, number]
131
+ name: string
132
+ }
133
+ >
134
+ ).map(([paramName, { type, range, name }]) => {
135
+ return {
136
+ type,
137
+ name,
138
+ range,
139
+ getValue() {
140
+ const value = editor[paramName as keyof typeof CameraParamsInit]
141
+ // webui exception in launch
142
+ return isNaN(value) ? range[0] : value
143
+ },
144
+ onChange(value: number) {
145
+ editor[paramName as keyof typeof CameraParamsInit] = value
146
+ },
147
+ }
148
+ })
149
+ }
150
+
151
+ function GetBodyParamControlor(editor: BodyEditor) {
152
+ const BodyParamsInit = {
153
+ BoneThickness: { range: [0.1, 3], name: i18n.t('Bone Thickness') },
154
+ HeadSize: { range: [0.1, 100], name: i18n.t('Head Size') },
155
+ NoseToNeck: { range: [0.1, 100], name: i18n.t('Nose To Neck') },
156
+ ShoulderWidth: { range: [0.1, 100], name: i18n.t('Shoulder Width') },
157
+ ShoulderToHip: { range: [0.1, 100], name: i18n.t('Shoulder To Hip') },
158
+ ArmLength: { range: [0.1, 100], name: i18n.t('Arm Length') },
159
+ Forearm: { range: [0.1, 100], name: i18n.t('Forearm') },
160
+ UpperArm: { range: [0.1, 100], name: i18n.t('Upper Arm') },
161
+ HandSize: { range: [0.1, 10], name: i18n.t('Hand Size') },
162
+ Hips: { range: [0.1, 100], name: i18n.t('Hips') },
163
+ LegLength: { range: [0.1, 100], name: i18n.t('Leg Length') },
164
+ Thigh: { range: [0.1, 100], name: i18n.t('Thigh') },
165
+ LowerLeg: { range: [0.1, 100], name: i18n.t('Lower Leg') },
166
+ FootSize: { range: [0.1, 10], name: i18n.t('Foot Size') },
167
+ } as const
168
+
169
+ function PushExecuteBodyParamsCommand(
170
+ editor: BodyEditor,
171
+ controlor: BodyControlor,
172
+ name: keyof typeof BodyParamsInit,
173
+ oldValue: number,
174
+ value: number
175
+ ) {
176
+ console.log(oldValue, value)
177
+ const cmd = {
178
+ execute: () => {
179
+ controlor[name] = value
180
+ controlor.Update()
181
+ },
182
+ undo: () => {
183
+ controlor[name] = oldValue
184
+ controlor.Update()
185
+ },
186
+ }
187
+ cmd.execute()
188
+ editor.pushCommand(cmd)
189
+ }
190
+
191
+ let currentBody = editor.getSelectedBody()
192
+ let currentControlor: BodyControlor | null = currentBody
193
+ ? new BodyControlor(currentBody)
194
+ : null
195
+
196
+ const getCurrentControlor = () => {
197
+ const body = editor.getSelectedBody()
198
+
199
+ if (body !== currentBody) {
200
+ currentBody = body
201
+ currentControlor = body ? new BodyControlor(body) : null
202
+ }
203
+
204
+ return currentControlor
205
+ }
206
+
207
+ let oldValue = 0
208
+ let changing = false
209
+
210
+ return Object.entries(
211
+ BodyParamsInit as Record<
212
+ keyof typeof BodyParamsInit,
213
+ {
214
+ type: 'int' | 'float' | undefined
215
+ range: [number, number]
216
+ name: string
217
+ }
218
+ >
219
+ ).map(([_paramName, { type, range, name }]) => {
220
+ return {
221
+ type,
222
+ name,
223
+ range,
224
+ getValue: () => {
225
+ const paramName = _paramName as keyof typeof BodyParamsInit
226
+ const controlor = getCurrentControlor()
227
+ if (controlor) {
228
+ return controlor[paramName]
229
+ }
230
+ return -1
231
+ },
232
+ onChange(value: number) {
233
+ const paramName = _paramName as keyof typeof BodyParamsInit
234
+ const controlor = getCurrentControlor()
235
+
236
+ if (controlor) {
237
+ // the first time
238
+ if (changing == false) oldValue = controlor[paramName]
239
+ changing = true
240
+ controlor[paramName] = value
241
+ }
242
+ },
243
+ onValueCommit(value: number) {
244
+ const paramName = _paramName as keyof typeof BodyParamsInit
245
+ const controlor = getCurrentControlor()
246
+
247
+ if (controlor) {
248
+ changing = false
249
+ PushExecuteBodyParamsCommand(
250
+ editor,
251
+ controlor,
252
+ paramName,
253
+ oldValue,
254
+ value
255
+ )
256
+ controlor[paramName] = value
257
+ }
258
+ },
259
+ }
260
+ })
261
+ }
262
+
263
+ const ControlorPopover: React.FC<{
264
+ editor: BodyEditor
265
+ style?: React.CSSProperties
266
+ }> = ({ editor, style }) => {
267
+ const forceUpdate = useForceUpdate()
268
+ const [open, setOpen] = useState(true)
269
+
270
+ const cameraParamControlor = useMemo(() => {
271
+ return GetCameraParamControlor(editor)
272
+ }, [editor])
273
+ const bodyParamControlor = useMemo(() => {
274
+ return GetBodyParamControlor(editor)
275
+ }, [editor])
276
+
277
+ const [bodySelected, setBodySelected] = useState(false)
278
+ useEffect(() => {
279
+ const select = () => {
280
+ setBodySelected(true)
281
+ }
282
+ const unselect = () => {
283
+ setBodySelected(false)
284
+ }
285
+ editor.SelectEventManager.AddEventListener(select)
286
+ editor.UnselectEventManager.AddEventListener(unselect)
287
+
288
+ return () => {
289
+ editor.SelectEventManager.RemoveEventListener(select)
290
+ editor.UnselectEventManager.RemoveEventListener(unselect)
291
+ }
292
+ }, [editor])
293
+ return (
294
+ <Popover.Root open={open}>
295
+ <Popover.Trigger asChild>
296
+ <button
297
+ className={IconButton}
298
+ style={style}
299
+ onClick={() => setOpen((v) => !v)}
300
+ >
301
+ <PinLeftIcon />
302
+ </button>
303
+ </Popover.Trigger>
304
+ <Popover.Portal>
305
+ <Popover.Content className={PopoverContent} sideOffset={5}>
306
+ <div
307
+ style={{
308
+ display: 'flex',
309
+ flexDirection: 'column',
310
+ gap: 10,
311
+ }}
312
+ >
313
+ {cameraParamControlor.map((props, index) => (
314
+ <Slider2
315
+ key={index}
316
+ {...props}
317
+ forceUpdate={forceUpdate}
318
+ ></Slider2>
319
+ ))}
320
+ {bodySelected ? (
321
+ <>
322
+ <div
323
+ style={{
324
+ fontSize: 15,
325
+ marginTop: 10,
326
+ marginBottom: 8,
327
+ }}
328
+ >
329
+ {i18n.t('Body Parameters')}
330
+ </div>
331
+ {bodyParamControlor.map((props, index) => (
332
+ <Slider2
333
+ key={index}
334
+ {...props}
335
+ forceUpdate={forceUpdate}
336
+ ></Slider2>
337
+ ))}
338
+ </>
339
+ ) : undefined}
340
+ </div>
341
+ <Popover.Arrow className={PopoverArrow} />
342
+ </Popover.Content>
343
+ </Popover.Portal>
344
+ </Popover.Root>
345
+ )
346
+ }
347
+
348
+ export default ControlorPopover
sd-webui-3d-open-pose-editor/src/components/PopupOver/styles.module.css ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import '@radix-ui/colors/blackA.css';
2
+ @import '@radix-ui/colors/mauve.css';
3
+ @import '@radix-ui/colors/violet.css';
4
+
5
+ .PopoverContent {
6
+ border-radius: 4px;
7
+ padding: 20px;
8
+ /* width: 260px; */
9
+ background-color: white;
10
+ box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
11
+ hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
12
+ animation-duration: 400ms;
13
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
14
+ will-change: transform, opacity;
15
+ font-family: var(--openpose-editor-font-family);
16
+ }
17
+
18
+ .PopoverContent:focus {
19
+ box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
20
+ hsl(206 22% 7% / 20%) 0px 10px 20px -15px, 0 0 0 2px var(--violet7);
21
+ }
22
+ .PopoverContent[data-state='open'][data-side='top'] {
23
+ animation-name: slideDownAndFade;
24
+ }
25
+ .PopoverContent[data-state='open'][data-side='right'] {
26
+ animation-name: slideLeftAndFade;
27
+ }
28
+ .PopoverContent[data-state='open'][data-side='bottom'] {
29
+ animation-name: slideUpAndFade;
30
+ }
31
+ .PopoverContent[data-state='open'][data-side='left'] {
32
+ animation-name: slideRightAndFade;
33
+ }
34
+
35
+ .PopoverArrow {
36
+ fill: white;
37
+ }
38
+
39
+ .IconButton {
40
+ all: unset;
41
+ font-family: inherit;
42
+ border-radius: 100%;
43
+ height: 35px;
44
+ width: 35px;
45
+ display: inline-flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ color: var(--violet11);
49
+ background-color: white;
50
+ box-shadow: 0 2px 10px var(--blackA7);
51
+ }
52
+ .IconButton:hover {
53
+ background-color: var(--violet3);
54
+ }
55
+ .IconButton:focus {
56
+ box-shadow: 0 0 0 2px var(--violet6);
57
+ }
58
+
59
+ @keyframes slideUpAndFade {
60
+ from {
61
+ opacity: 0;
62
+ transform: translateY(2px);
63
+ }
64
+ to {
65
+ opacity: 1;
66
+ transform: translateY(0);
67
+ }
68
+ }
69
+
70
+ @keyframes slideRightAndFade {
71
+ from {
72
+ opacity: 0;
73
+ transform: translateX(-2px);
74
+ }
75
+ to {
76
+ opacity: 1;
77
+ transform: translateX(0);
78
+ }
79
+ }
80
+
81
+ @keyframes slideDownAndFade {
82
+ from {
83
+ opacity: 0;
84
+ transform: translateY(-2px);
85
+ }
86
+ to {
87
+ opacity: 1;
88
+ transform: translateY(0);
89
+ }
90
+ }
91
+
92
+ @keyframes slideLeftAndFade {
93
+ from {
94
+ opacity: 0;
95
+ transform: translateX(2px);
96
+ }
97
+ to {
98
+ opacity: 1;
99
+ transform: translateX(0);
100
+ }
101
+ }
102
+
103
+ Input {
104
+ background-color: var(--violet5);
105
+ border: none;
106
+ box-sizing: border-box;
107
+ padding-left: 5px;
108
+ padding-right: 5px;
109
+ border-radius: 7px;
110
+ }
111
+
112
+ Input:focus {
113
+ border: 2px var(--violet11) solid;
114
+ border-radius: 5;
115
+ }
sd-webui-3d-open-pose-editor/src/components/Slider/index.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import * as Slider from '@radix-ui/react-slider'
3
+ import classes from './styles.module.css'
4
+
5
+ const { SliderRoot, SliderTrack, SliderRange } = classes
6
+ const SliderDemo: React.FC<{
7
+ range: [number, number]
8
+ value: number
9
+ onValueChange?: (value: number) => void
10
+ onValueCommit?: (value: number) => void
11
+ style?: React.CSSProperties
12
+ }> = ({ range, value, onValueChange, onValueCommit, style }) => (
13
+ <form
14
+ style={{
15
+ ...style,
16
+ }}
17
+ >
18
+ <Slider.Root
19
+ className={SliderRoot}
20
+ value={[value]}
21
+ min={range[0]}
22
+ max={range[1]}
23
+ step={(range[1] - range[0]) / 150.0}
24
+ onValueChange={([value]: number[]) => {
25
+ onValueChange?.(value)
26
+ }}
27
+ onValueCommit={([value]: number[]) => {
28
+ onValueCommit?.(value)
29
+ }}
30
+ >
31
+ <Slider.Track className={SliderTrack}>
32
+ <Slider.Range className={SliderRange} />
33
+ </Slider.Track>
34
+ </Slider.Root>
35
+ </form>
36
+ )
37
+
38
+ export default SliderDemo
sd-webui-3d-open-pose-editor/src/components/Slider/styles.module.css ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import '@radix-ui/colors/blackA.css';
2
+ @import '@radix-ui/colors/violet.css';
3
+
4
+ .SliderRoot {
5
+ position: relative;
6
+ display: flex;
7
+ align-items: center;
8
+ user-select: none;
9
+ touch-action: none;
10
+ height: 20px;
11
+ }
12
+
13
+ .SliderTrack {
14
+ background-color: var(--violet5);
15
+ position: relative;
16
+ flex-grow: 1;
17
+ border-radius: 9999px;
18
+ height: 10px;
19
+ }
20
+
21
+ .SliderRange {
22
+ position: absolute;
23
+ background-color: var(--violet9);
24
+ border-radius: 9999px;
25
+ height: 100%;
26
+ }
sd-webui-3d-open-pose-editor/src/components/Toast/index.tsx ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import * as Toast from '@radix-ui/react-toast'
3
+ import NiceModal, { useModal } from '@ebay/nice-modal-react'
4
+ import { debounce } from 'lodash-es'
5
+
6
+ import classes from './styles.module.css'
7
+ import classNames from 'classnames'
8
+
9
+ const {
10
+ ToastViewport,
11
+ ToastRoot,
12
+ ToastTitle,
13
+ ToastAction,
14
+ Button,
15
+ small,
16
+ green,
17
+ } = classes
18
+
19
+ const MyToast = NiceModal.create<{
20
+ title: string
21
+ button?: string
22
+ duration?: number
23
+ }>(({ title, button, duration = 3000 }) => {
24
+ const modal = useToast()
25
+ return (
26
+ <Toast.Provider swipeDirection="right" duration={duration}>
27
+ <Toast.Root
28
+ className={ToastRoot}
29
+ defaultOpen={true}
30
+ open={modal.visible}
31
+ onOpenChange={(open) => {
32
+ if (open == false) {
33
+ modal.hide()
34
+ }
35
+ }}
36
+ >
37
+ <Toast.Title className={ToastTitle}>{title}</Toast.Title>
38
+ {button ? (
39
+ <Toast.Action className={ToastAction} asChild altText="">
40
+ <button
41
+ className={classNames(Button, small, green)}
42
+ onClick={() => {
43
+ modal.resolve('action')
44
+ modal.hide()
45
+ }}
46
+ >
47
+ {button}
48
+ </button>
49
+ </Toast.Action>
50
+ ) : undefined}
51
+ </Toast.Root>
52
+ <Toast.Viewport className={ToastViewport} />
53
+ </Toast.Provider>
54
+ )
55
+ })
56
+
57
+ export function useToast() {
58
+ return useModal(MyToast)
59
+ }
60
+
61
+ declare type NiceModalArgs<T> = T extends
62
+ | keyof JSX.IntrinsicElements
63
+ | React.JSXElementConstructor<any>
64
+ ? Omit<React.ComponentProps<T>, 'id'>
65
+ : Record<string, unknown>
66
+
67
+ export type ShowProps = NiceModalArgs<typeof MyToast>
68
+
69
+ export function GetToast(wait = 0) {
70
+ const show = debounce((props: ShowProps) => {
71
+ NiceModal.show(MyToast, props)
72
+ }, wait)
73
+
74
+ return {
75
+ show: (props: ShowProps) => {
76
+ show(props)
77
+ },
78
+ hide: () => {
79
+ show.cancel()
80
+ HideToast()
81
+ },
82
+ }
83
+ }
84
+
85
+ export async function ShowToast(props: ShowProps): Promise<string> {
86
+ return await NiceModal.show(MyToast, props)
87
+ }
88
+
89
+ export function HideToast() {
90
+ return NiceModal.hide(MyToast)
91
+ }
sd-webui-3d-open-pose-editor/src/components/Toast/styles.module.css ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import '@radix-ui/colors/blackA.css';
2
+ @import '@radix-ui/colors/green.css';
3
+ @import '@radix-ui/colors/mauve.css';
4
+ @import '@radix-ui/colors/slate.css';
5
+ @import '@radix-ui/colors/violet.css';
6
+
7
+ .ToastViewport {
8
+ font-family: var(--openpose-editor-font-family);
9
+ --viewport-padding: 25px;
10
+ position: fixed;
11
+ bottom: 0;
12
+ right: 0;
13
+ display: flex;
14
+ flex-direction: column;
15
+ padding: var(--viewport-padding);
16
+ gap: 10px;
17
+ width: 390px;
18
+ max-width: 100vw;
19
+ margin: 0;
20
+ list-style: none;
21
+ z-index: 2147483647;
22
+ outline: none;
23
+ }
24
+
25
+ .ToastRoot {
26
+ background-color: white;
27
+ border-radius: 6px;
28
+ box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
29
+ hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
30
+ padding: 15px;
31
+ display: grid;
32
+ grid-template-areas: 'title action' 'description action';
33
+ grid-template-columns: auto max-content;
34
+ column-gap: 15px;
35
+ align-items: center;
36
+ }
37
+ .ToastRoot[data-state='open'] {
38
+ animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
39
+ }
40
+ .ToastRoot[data-state='closed'] {
41
+ animation: hide 100ms ease-in;
42
+ }
43
+ .ToastRoot[data-swipe='move'] {
44
+ transform: translateX(var(--radix-toast-swipe-move-x));
45
+ }
46
+ .ToastRoot[data-swipe='cancel'] {
47
+ transform: translateX(0);
48
+ transition: transform 200ms ease-out;
49
+ }
50
+ .ToastRoot[data-swipe='end'] {
51
+ animation: swipeOut 100ms ease-out;
52
+ }
53
+
54
+ @keyframes hide {
55
+ from {
56
+ opacity: 1;
57
+ }
58
+ to {
59
+ opacity: 0;
60
+ }
61
+ }
62
+
63
+ @keyframes slideIn {
64
+ from {
65
+ transform: translateX(calc(100% + var(--viewport-padding)));
66
+ }
67
+ to {
68
+ transform: translateX(0);
69
+ }
70
+ }
71
+
72
+ @keyframes swipeOut {
73
+ from {
74
+ transform: translateX(var(--radix-toast-swipe-end-x));
75
+ }
76
+ to {
77
+ transform: translateX(calc(100% + var(--viewport-padding)));
78
+ }
79
+ }
80
+
81
+ .ToastTitle {
82
+ grid-area: title;
83
+ margin-bottom: 5px;
84
+ font-weight: 500;
85
+ color: var(--slate12);
86
+ font-size: 15px;
87
+ }
88
+
89
+ .ToastDescription {
90
+ grid-area: description;
91
+ margin: 0;
92
+ color: var(--slate11);
93
+ font-size: 13px;
94
+ line-height: 1.3;
95
+ }
96
+
97
+ .ToastAction {
98
+ grid-area: action;
99
+ }
100
+
101
+ .Button {
102
+ all: unset;
103
+ display: inline-flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ border-radius: 4px;
107
+ font-weight: 500;
108
+ }
109
+ .Button.small {
110
+ font-size: 12px;
111
+ padding: 0 10px;
112
+ line-height: 25px;
113
+ height: 25px;
114
+ }
115
+ .Button.large {
116
+ font-size: 15px;
117
+ padding: 0 15px;
118
+ line-height: 35px;
119
+ height: 35px;
120
+ }
121
+ .Button.violet {
122
+ background-color: white;
123
+ color: var(--violet11);
124
+ box-shadow: 0 2px 10px var(--blackA7);
125
+ }
126
+ .Button.violet:hover {
127
+ background-color: var(--mauve3);
128
+ }
129
+ .Button.violet:focus {
130
+ box-shadow: 0 0 0 2px black;
131
+ }
132
+ .Button.green {
133
+ background-color: var(--green2);
134
+ color: var(--green11);
135
+ box-shadow: inset 0 0 0 1px var(--green7);
136
+ }
137
+ .Button.green:hover {
138
+ box-shadow: inset 0 0 0 1px var(--green8);
139
+ }
140
+ .Button.green:focus {
141
+ box-shadow: 0 0 0 2px var(--green8);
142
+ }
sd-webui-3d-open-pose-editor/src/defines.ts ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const OpenposeyKeypointsConst = [
2
+ 'nose',
3
+ 'neck',
4
+ 'right_shoulder',
5
+ 'right_elbow',
6
+ 'right_wrist',
7
+ 'left_shoulder',
8
+ 'left_elbow',
9
+ 'left_wrist',
10
+ 'right_hip',
11
+ 'right_knee',
12
+ 'right_ankle',
13
+ 'left_hip',
14
+ 'left_knee',
15
+ 'left_ankle',
16
+ 'right_eye',
17
+ 'left_eye',
18
+ 'right_ear',
19
+ 'left_ear',
20
+ ] as const
21
+
22
+ export const OpenposeKeypoints = OpenposeyKeypointsConst as unknown as string[]
23
+
24
+ export const ConnectKeypoints = [
25
+ [1, 2],
26
+ [1, 5],
27
+ [2, 3],
28
+ [3, 4],
29
+ [5, 6],
30
+ [6, 7],
31
+ [1, 8],
32
+ [8, 9],
33
+ [9, 10],
34
+ [1, 11],
35
+ [11, 12],
36
+ [12, 13],
37
+ [0, 1],
38
+ [0, 14],
39
+ [14, 16],
40
+ [0, 15],
41
+ [15, 17],
42
+ ] as const
43
+
44
+ export const ConnectColor = [
45
+ [255, 0, 0], // [1, 2], 0
46
+ [255, 85, 0], // [1, 5], 1
47
+ [255, 170, 0], // [2, 3], 2
48
+ [255, 255, 0], // [3, 4], 3
49
+ [170, 255, 0], // [5, 6], 4
50
+ [85, 255, 0], // [6, 7], 5
51
+ [0, 255, 0], // [1, 8], 6
52
+ [0, 255, 85], // [8, 9], 7
53
+ [0, 255, 170], // [9, 10], 8
54
+ [0, 255, 255], // [1, 11], 9
55
+ [0, 170, 255], // [11, 12], 10
56
+ [0, 85, 255], // [12, 13], 11
57
+ [0, 0, 255], // [0, 1], 12
58
+ [85, 0, 255], // [0, 14], 13
59
+ [170, 0, 255], // [14, 16], 14
60
+ [255, 0, 255], // [0, 15], 15
61
+ [255, 0, 170], // [15, 17], 16
62
+ [255, 0, 85], // 17
63
+ ] as const
64
+
65
+ export function ToHexColor([r, g, b]: readonly [number, number, number]) {
66
+ return (r << 16) + (g << 8) + b
67
+ }
68
+ function SearchColor(start: number, end: number) {
69
+ const index = ConnectKeypoints.findIndex(
70
+ ([s, e]) => s === start && e === end
71
+ )
72
+
73
+ if (typeof index !== 'undefined') {
74
+ const [r, g, b] = ConnectColor[index]
75
+
76
+ return (r << 16) + (g << 8) + b
77
+ }
78
+ return null
79
+ }
80
+
81
+ export function GetColorOfLinkByName(startName: string, endName: string) {
82
+ if (!startName || !endName) return null
83
+
84
+ const indexStart = OpenposeKeypoints.indexOf(startName)
85
+ const indexEnd = OpenposeKeypoints.indexOf(endName)
86
+
87
+ if (indexStart === -1 || indexEnd === -1) return null
88
+
89
+ if (indexStart > indexEnd) return SearchColor(indexEnd, indexStart)
90
+ else return SearchColor(indexStart, indexEnd)
91
+ }
92
+
93
+ export const BoneThickness = 1
94
+
95
+ export const PartIndexMappingOfPoseModel = {
96
+ Root: 0,
97
+ Hips: 1,
98
+ Spine: 2,
99
+ Spine1: 3,
100
+ Spine2: 4,
101
+ Chest: 5,
102
+ Neck: 6,
103
+ Head: 7,
104
+ Eye_R: 8,
105
+ Eye_L: 9,
106
+ Head_Null: 10, // maybe null
107
+ Shoulder_L: 11,
108
+ Arm_L: 12,
109
+ ForeArm_L: 13,
110
+ Hand_L: 14,
111
+ HandPinky1_L: 15,
112
+ HandPinky2_L: 16,
113
+ HandPinky3_L: 17,
114
+ HandRing1_L: 18,
115
+ HandRing2_L: 19,
116
+ HandRing3_L: 20,
117
+ HandMiddle1_L: 21,
118
+ HandMiddle2_L: 22,
119
+ HandMiddle3_L: 23,
120
+ HandIndex1_L: 24,
121
+ HandIndex2_L: 25,
122
+ HandIndex3_L: 26,
123
+ HandThumb1_L: 27,
124
+ HandThumb2_L: 28,
125
+ HandThumb3_L: 29,
126
+ Elbow_L: 30,
127
+ ForeArmTwist_L: 31,
128
+ ArmTwist_L: 32,
129
+ Shoulder_R: 33,
130
+ Arm_R: 34,
131
+ ForeArm_R: 35,
132
+ Hand_R: 36,
133
+ HandPinky1_R: 37,
134
+ HandPinky2_R: 38,
135
+ HandPinky3_R: 39,
136
+ HandRing1_R: 40,
137
+ HandRing2_R: 41,
138
+ HandRing3_R: 42,
139
+ HandMiddle1_R: 43,
140
+ HandMiddle2_R: 44,
141
+ HandMiddle3_R: 45,
142
+ HandIndex1_R: 46,
143
+ HandIndex2_R: 47,
144
+ HandIndex3_R: 48,
145
+ HandThumb1_R: 49,
146
+ HandThumb2_R: 50,
147
+ HandThumb3_R: 51,
148
+ Elbow_R: 52,
149
+ ForeArmTwist_R: 53,
150
+ ArmTwist_R: 54,
151
+ UpLeg_L: 55,
152
+ Leg_L: 56,
153
+ Knee_L: 57,
154
+ Foot_L: 58,
155
+ FootPinky1_L: 59,
156
+ FootRing_L: 60,
157
+ FootMiddle_L: 61,
158
+ FootIndex_L: 62,
159
+ FootThumb_L: 63,
160
+ UpLegTwist_L: 64,
161
+ ThighFront_L: 65,
162
+ UpLeg_R: 66,
163
+ Leg_R: 67,
164
+ Knee_R: 68,
165
+ Foot_R: 69,
166
+ FootPinky1_R: 70,
167
+ FootRing_R: 71,
168
+ FootMiddle_R: 72,
169
+ FootIndex_R: 73,
170
+ FootThumb_R: 74,
171
+ UpLegTwist_R: 75,
172
+ ThighFront_R: 76,
173
+ }
174
+
175
+ export const PartIndexMappingOfBlazePoseModel = {
176
+ nose: 0,
177
+ left_eye_inner: 1,
178
+ left_eye: 2,
179
+ left_eye_outer: 3,
180
+ right_eye_inner: 4,
181
+ right_eye: 5,
182
+ right_eye_outer: 6,
183
+ left_ear: 7,
184
+ right_ear: 8,
185
+ mouth_left: 9,
186
+ mouth_right: 10,
187
+ left_shoulder: 11,
188
+ right_shoulder: 12,
189
+ left_elbow: 13,
190
+ right_elbow: 14,
191
+ left_wrist: 15,
192
+ right_wrist: 16,
193
+ left_pinky: 17,
194
+ right_pinky: 18,
195
+ left_index: 19,
196
+ right_index: 20,
197
+ left_thumb: 21,
198
+ right_thumb: 22,
199
+ left_hip: 23,
200
+ right_hip: 24,
201
+ left_knee: 25,
202
+ right_knee: 26,
203
+ left_ankle: 27,
204
+ right_ankle: 28,
205
+ left_heel: 29,
206
+ right_heel: 30,
207
+ left_foot_index: 31,
208
+ right_foot_index: 32,
209
+ }
sd-webui-3d-open-pose-editor/src/editor.ts ADDED
@@ -0,0 +1,1875 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from 'three'
2
+ import {
3
+ Bone,
4
+ Material,
5
+ Mesh,
6
+ MeshBasicMaterial,
7
+ MeshDepthMaterial,
8
+ MeshNormalMaterial,
9
+ MeshPhongMaterial,
10
+ Object3D,
11
+ Skeleton,
12
+ SkinnedMesh,
13
+ } from 'three'
14
+ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
15
+ import { TransformControls } from 'three/examples/jsm/controls/TransformControls'
16
+
17
+ // @ts-ignore
18
+ // import {
19
+ // CCDIKHelper,
20
+ // CCDIKSolver,
21
+ // IKS,
22
+ // } from 'three/examples/jsm/animate/CCDIKSolver'
23
+ import { CCDIKSolver } from './utils/CCDIKSolver'
24
+ import Stats from 'three/examples/jsm/libs/stats.module'
25
+ import {
26
+ BodyControlor,
27
+ BodyData,
28
+ CloneBody,
29
+ GetExtremityMesh,
30
+ IsBone,
31
+ IsExtremities,
32
+ IsFoot,
33
+ IsHand,
34
+ IsMask,
35
+ IsNeedSaveObject,
36
+ IsPickable,
37
+ IsSkeleton,
38
+ IsTarget,
39
+ IsTranslate,
40
+ } from './body'
41
+
42
+ import { downloadJson, uploadJson } from './utils/transfer'
43
+
44
+ import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
45
+ import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
46
+ import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
47
+
48
+ import { LuminosityShader } from 'three/examples/jsm/shaders/LuminosityShader.js'
49
+ import { SobelOperatorShader } from 'three/examples/jsm/shaders/SobelOperatorShader.js'
50
+ import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils'
51
+ import { Oops } from './components/Oops'
52
+ import { getCurrentTime } from './utils/time'
53
+ import { sendToAll } from './hooks/useMessageDispatch'
54
+
55
+ type EditorEventHandler<T> = (args: T) => void
56
+
57
+ class EditorEventManager<T> {
58
+ private eventHandlers: EditorEventHandler<T>[] = []
59
+
60
+ AddEventListener(handler: EditorEventHandler<T>): void {
61
+ this.eventHandlers.push(handler)
62
+ }
63
+
64
+ RemoveEventListener(handler: EditorEventHandler<T>): void {
65
+ this.eventHandlers = this.eventHandlers.filter((h) => h !== handler)
66
+ }
67
+
68
+ TriggerEvent(args: T): void {
69
+ this.eventHandlers.forEach((h) => h(args))
70
+ }
71
+ }
72
+
73
+ interface CameraData {
74
+ position: ReturnType<THREE.Vector3['toArray']>
75
+ rotation: ReturnType<THREE.Euler['toArray']>
76
+ target: ReturnType<THREE.Vector3['toArray']>
77
+ near: number
78
+ far: number
79
+ zoom: number
80
+ }
81
+
82
+ interface TransformValue {
83
+ scale: Object3D['scale']
84
+ rotation: Object3D['rotation']
85
+ position: Object3D['position']
86
+ }
87
+
88
+ function GetTransformValue(obj: Object3D): TransformValue {
89
+ return {
90
+ scale: obj.scale.clone(),
91
+ rotation: obj.rotation.clone(),
92
+ position: obj.position.clone(),
93
+ }
94
+ }
95
+
96
+ export interface Command {
97
+ execute: () => void
98
+ undo: () => void
99
+ }
100
+
101
+ export interface ParentElement {
102
+ addEventListener(
103
+ type: 'keydown',
104
+ listener: (this: any, ev: KeyboardEvent) => any,
105
+ options?: boolean | AddEventListenerOptions | undefined
106
+ ): void
107
+ addEventListener(
108
+ type: 'keyup',
109
+ listener: (this: any, ev: KeyboardEvent) => any,
110
+ options?: boolean | AddEventListenerOptions | undefined
111
+ ): void
112
+ removeEventListener(
113
+ type: 'keydown',
114
+ listener: (this: Document, ev: KeyboardEvent) => any,
115
+ options?: boolean | EventListenerOptions | undefined
116
+ ): void
117
+ removeEventListener(
118
+ type: 'keyup',
119
+ listener: (this: Document, ev: KeyboardEvent) => any,
120
+ options?: boolean | EventListenerOptions | undefined
121
+ ): void
122
+ }
123
+
124
+ class PreviewRenderer {
125
+ scene: THREE.Scene
126
+ camera: THREE.PerspectiveCamera
127
+ canvas?: HTMLCanvasElement
128
+ renderer: THREE.WebGLRenderer
129
+ orbitControls: OrbitControls
130
+
131
+ constructor(setting: {
132
+ scene: THREE.Scene
133
+ camera: THREE.PerspectiveCamera
134
+ orbitControls: OrbitControls
135
+
136
+ canvas?: HTMLCanvasElement
137
+ renderer?: THREE.WebGLRenderer
138
+ }) {
139
+ this.scene = setting.scene
140
+ this.camera = setting.camera
141
+ this.canvas = setting.canvas
142
+ this.orbitControls = setting.orbitControls
143
+ if (setting.renderer) {
144
+ this.renderer = setting.renderer
145
+ } else {
146
+ this.renderer = new THREE.WebGLRenderer({
147
+ antialias: true,
148
+ canvas: setting.canvas,
149
+ // logarithmicDepthBuffer: true
150
+ })
151
+ }
152
+ }
153
+
154
+ renderBySize(
155
+ outputWidth: number,
156
+ outputHeight: number,
157
+ render: (outputWidth: number, outputHeight: number) => void
158
+ ) {
159
+ const save = {
160
+ aspect: this.camera.aspect,
161
+ }
162
+ this.camera.aspect = outputWidth / outputHeight
163
+ this.camera.updateProjectionMatrix()
164
+ this.renderer.setSize(outputWidth, outputHeight, true)
165
+
166
+ render(outputWidth, outputHeight)
167
+
168
+ this.camera.aspect = save.aspect
169
+ this.camera.updateProjectionMatrix()
170
+ }
171
+
172
+ GetCameraData() {
173
+ const result = {
174
+ position: this.camera.position.toArray(),
175
+ rotation: this.camera.rotation.toArray(),
176
+ target: this.orbitControls.target.toArray(),
177
+ near: this.camera.near,
178
+ far: this.camera.far,
179
+ zoom: this.camera.zoom,
180
+ }
181
+
182
+ return result
183
+ }
184
+
185
+ RestoreCamera(data: CameraData, updateOrbitControl = true) {
186
+ this.camera.position.fromArray(data.position)
187
+ this.camera.rotation.fromArray(data.rotation as any)
188
+ this.camera.near = data.near
189
+ this.camera.far = data.far
190
+ this.camera.zoom = data.zoom
191
+ this.camera.updateProjectionMatrix()
192
+
193
+ if (data.target) this.orbitControls.target.fromArray(data.target)
194
+ if (updateOrbitControl) this.orbitControls.update() // fix position change
195
+ }
196
+
197
+ changeView(cameraDataOfView?: CameraData) {
198
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
199
+ if (!cameraDataOfView) return () => {}
200
+
201
+ const old = this.GetCameraData()
202
+ this.RestoreCamera(cameraDataOfView, false)
203
+ return () => {
204
+ this.RestoreCamera(old)
205
+ }
206
+ }
207
+
208
+ render(
209
+ outputWidth: number,
210
+ outputHeight: number,
211
+ cameraDataOfView?: CameraData,
212
+ custom?: (outputWidth: number, outputHeight: number) => void
213
+ ) {
214
+ const render = () => {
215
+ this.renderer.render(this.scene, this.camera)
216
+ }
217
+ const restoreView = this.changeView(cameraDataOfView)
218
+ this.renderBySize(outputWidth, outputHeight, custom ?? render)
219
+ restoreView()
220
+ }
221
+ }
222
+
223
+ export class BodyEditor {
224
+ renderer: THREE.WebGLRenderer
225
+ outputRenderer: THREE.WebGLRenderer
226
+ previewRenderer: PreviewRenderer
227
+ scene: THREE.Scene
228
+ gridHelper: THREE.GridHelper
229
+ axesHelper: THREE.AxesHelper
230
+ camera: THREE.PerspectiveCamera
231
+ orbitControls: OrbitControls
232
+ transformControl: TransformControls
233
+
234
+ dlight: THREE.DirectionalLight
235
+ alight: THREE.AmbientLight
236
+ raycaster = new THREE.Raycaster()
237
+ IsClick = false
238
+ stats: Stats | undefined
239
+
240
+ // ikSolver?: CCDIKSolver
241
+ composer?: EffectComposer
242
+ finalComposer?: EffectComposer
243
+ effectSobel?: ShaderPass
244
+ enableComposer = false
245
+ enablePreview = true
246
+ enableHelper = true
247
+
248
+ paused = false
249
+
250
+ parentElem: ParentElement
251
+
252
+ clearColor = 0xaaaaaa
253
+ constructor({
254
+ canvas,
255
+ previewCanvas,
256
+ parentElem = document,
257
+ statsElem,
258
+ }: {
259
+ canvas: HTMLCanvasElement
260
+ previewCanvas: HTMLCanvasElement
261
+ parentElem?: ParentElement
262
+ statsElem?: Element
263
+ }) {
264
+ this.parentElem = parentElem
265
+ this.renderer = new THREE.WebGLRenderer({
266
+ canvas,
267
+ antialias: true,
268
+ // logarithmicDepthBuffer: true
269
+ })
270
+ this.outputRenderer = new THREE.WebGLRenderer({
271
+ antialias: true,
272
+ // logarithmicDepthBuffer: true
273
+ })
274
+ this.outputRenderer.domElement.style.display = 'none'
275
+ document.body.appendChild(this.outputRenderer.domElement)
276
+
277
+ this.renderer.setClearColor(this.clearColor, 0.0)
278
+ this.scene = new THREE.Scene()
279
+
280
+ this.gridHelper = new THREE.GridHelper(8000, 200)
281
+ this.axesHelper = new THREE.AxesHelper(1000)
282
+ this.scene.add(this.gridHelper)
283
+ this.scene.add(this.axesHelper)
284
+
285
+ const aspect = window.innerWidth / window.innerHeight
286
+
287
+ this.camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 10000)
288
+
289
+ this.camera.position.set(0, 100, 200)
290
+ this.camera.lookAt(0, 100, 0)
291
+ // this.camera.near = 130
292
+ // this.camera.far = 600
293
+ this.camera.updateProjectionMatrix()
294
+
295
+ this.orbitControls = new OrbitControls(
296
+ this.camera,
297
+ this.renderer.domElement
298
+ )
299
+ this.orbitControls.target = new THREE.Vector3(0, 100, 0)
300
+ this.orbitControls.update()
301
+
302
+ this.transformControl = new TransformControls(
303
+ this.camera,
304
+ this.renderer.domElement
305
+ )
306
+
307
+ this.transformControl.setMode('rotate') //旋转
308
+ this.transformControl.setSize(0.4)
309
+ this.transformControl.setSpace('local')
310
+ this.registerTranformControlEvent()
311
+ this.scene.add(this.transformControl)
312
+
313
+ this.previewRenderer = new PreviewRenderer({
314
+ scene: this.scene,
315
+ camera: this.camera,
316
+ orbitControls: this.orbitControls,
317
+ canvas: previewCanvas,
318
+ })
319
+
320
+ // Light
321
+ this.dlight = new THREE.DirectionalLight(0xffffff, 1.0)
322
+ this.dlight.position.set(0, 160, 1000)
323
+ this.scene.add(this.dlight)
324
+ this.alight = new THREE.AmbientLight(0xffffff, 0.5)
325
+ this.scene.add(this.alight)
326
+
327
+ this.onMouseDown = this.onMouseDown.bind(this)
328
+ this.onMouseMove = this.onMouseMove.bind(this)
329
+ this.onMouseUp = this.onMouseUp.bind(this)
330
+ this.handleResize = this.handleResize.bind(this)
331
+ this.handleKeyDown = this.handleKeyDown.bind(this)
332
+ this.handleKeyUp = this.handleKeyUp.bind(this)
333
+
334
+ this.addEvent()
335
+
336
+ this.initEdgeComposer()
337
+
338
+ // // Create a render target with depth texture
339
+ // this.setupRenderTarget();
340
+
341
+ // // Setup post-processing step
342
+ // this.setupPost();
343
+
344
+ if (statsElem) {
345
+ this.stats = Stats()
346
+ statsElem.appendChild(this.stats.dom)
347
+ }
348
+
349
+ this.animate = this.animate.bind(this)
350
+ this.animate()
351
+ this.handleResize()
352
+ this.AutoSaveScene()
353
+ }
354
+
355
+ disponse() {
356
+ this.pause()
357
+ this.removeEvent()
358
+ this.renderer.dispose()
359
+ this.outputRenderer.dispose()
360
+
361
+ console.log('BodyEditor disponse')
362
+ }
363
+
364
+ commandHistory: Command[] = []
365
+ historyIndex = -1
366
+ pushCommand(cmd: Command) {
367
+ console.log('pushCommand')
368
+ if (this.historyIndex != this.commandHistory.length - 1)
369
+ this.commandHistory = this.commandHistory.slice(
370
+ 0,
371
+ this.historyIndex + 1
372
+ )
373
+ this.commandHistory.push(cmd)
374
+ this.historyIndex = this.commandHistory.length - 1
375
+ }
376
+
377
+ CreateTransformCommand(obj: Object3D, _old: TransformValue): Command {
378
+ const oldValue = _old
379
+ const newValue = GetTransformValue(obj)
380
+ const controlor = new BodyControlor(this.getBodyByPart(obj)!)
381
+ return {
382
+ execute: () => {
383
+ obj.position.copy(newValue.position)
384
+ obj.rotation.copy(newValue.rotation)
385
+ obj.scale.copy(newValue.scale)
386
+ controlor.Update()
387
+ },
388
+ undo: () => {
389
+ obj.position.copy(oldValue.position)
390
+ obj.rotation.copy(oldValue.rotation)
391
+ obj.scale.copy(oldValue.scale)
392
+ controlor.Update()
393
+ },
394
+ }
395
+ }
396
+
397
+ CreateAllTransformCommand(obj: Object3D, _old: BodyData): Command {
398
+ const oldValue = _old
399
+ const body = this.getBodyByPart(obj)!
400
+ const controlor = new BodyControlor(body)
401
+ const newValue = controlor.GetBodyData()
402
+
403
+ return {
404
+ execute: () => {
405
+ controlor.RestoreBody(newValue)
406
+ controlor.Update()
407
+ },
408
+ undo: () => {
409
+ controlor.RestoreBody(oldValue)
410
+ controlor.Update()
411
+ },
412
+ }
413
+ }
414
+
415
+ CreateAddBodyCommand(obj: Object3D): Command {
416
+ return {
417
+ execute: () => {
418
+ this.scene.add(obj)
419
+ },
420
+ undo: () => {
421
+ obj.removeFromParent()
422
+ this.DetachTransfromControl()
423
+ },
424
+ }
425
+ }
426
+
427
+ CreateRemoveBodyCommand(obj: Object3D): Command {
428
+ return {
429
+ execute: () => {
430
+ obj.removeFromParent()
431
+ this.DetachTransfromControl()
432
+ },
433
+ undo: () => {
434
+ this.scene.add(obj)
435
+ },
436
+ }
437
+ }
438
+
439
+ Undo() {
440
+ console.log('Undo', this.historyIndex)
441
+
442
+ if (this.historyIndex >= 0) {
443
+ const cmd = this.commandHistory[this.historyIndex]
444
+ cmd.undo()
445
+ this.historyIndex--
446
+ }
447
+ }
448
+
449
+ Redo() {
450
+ console.log('Redo', this.historyIndex)
451
+
452
+ if (this.historyIndex < this.commandHistory.length - 1) {
453
+ const cmd = this.commandHistory[this.historyIndex + 1]
454
+ cmd.execute()
455
+ this.historyIndex++
456
+ }
457
+ }
458
+
459
+ handleKeyDown(e: KeyboardEvent) {
460
+ if (this.paused) {
461
+ return
462
+ }
463
+ if (e.code === 'KeyZ' && (e.ctrlKey || e.metaKey) && e.shiftKey) {
464
+ this.Redo()
465
+ } else if (e.code === 'KeyY' && (e.ctrlKey || e.metaKey)) {
466
+ this.Redo()
467
+ // prevent brower refresh
468
+ e.preventDefault()
469
+ } else if (e.code === 'KeyZ' && (e.ctrlKey || e.metaKey)) {
470
+ this.Undo()
471
+ } else if (e.code === 'KeyD' && e.shiftKey) {
472
+ this.CopySelectedBody()
473
+ } else if (e.key === 'Delete') {
474
+ this.RemoveBody()
475
+ } else if (e.code === 'KeyX') {
476
+ this.MoveMode = true
477
+ }
478
+ }
479
+
480
+ handleKeyUp(e: KeyboardEvent) {
481
+ if (this.paused) {
482
+ return
483
+ }
484
+
485
+ if (e.code === 'KeyX') {
486
+ this.MoveMode = false
487
+ }
488
+ }
489
+
490
+ registerTranformControlEvent() {
491
+ let oldTransformValue: TransformValue = {
492
+ scale: new THREE.Vector3(),
493
+ rotation: new THREE.Euler(),
494
+ position: new THREE.Vector3(),
495
+ }
496
+ let oldBodyData: BodyData = {} as BodyData
497
+ this.transformControl.addEventListener('change', () => {
498
+ const body = this.getSelectedBody()
499
+
500
+ if (body) {
501
+ new BodyControlor(body).UpdateBones()
502
+ }
503
+ // console.log('change')
504
+ // this.renderer.render(this.scene, this.camera)
505
+ })
506
+
507
+ this.transformControl.addEventListener('objectChange', () => {
508
+ // console.log('objectChange')
509
+ // this.renderer.render(this.scene, this.camera)
510
+ })
511
+
512
+ this.transformControl.addEventListener('mouseDown', () => {
513
+ const part = this.getSelectedPart()
514
+ if (part) {
515
+ oldTransformValue = GetTransformValue(part)
516
+ const body = this.getBodyByPart(part)!
517
+ oldBodyData = new BodyControlor(body).GetBodyData()
518
+ }
519
+ this.orbitControls.enabled = false
520
+ })
521
+ this.transformControl.addEventListener('mouseUp', () => {
522
+ const part = this.getSelectedPart()
523
+ if (part) {
524
+ if (IsTarget(part.name))
525
+ this.pushCommand(
526
+ this.CreateAllTransformCommand(part, oldBodyData)
527
+ )
528
+ else
529
+ this.pushCommand(
530
+ this.CreateTransformCommand(part, oldTransformValue)
531
+ )
532
+ }
533
+ this.orbitControls.enabled = true
534
+
535
+ this.saveSelectedBodyControlor?.Update()
536
+ })
537
+ }
538
+
539
+ ikSolver?: CCDIKSolver
540
+ saveSelectedBodyControlor?: BodyControlor
541
+
542
+ updateSelectedBodyIKSolver() {
543
+ const body = this.getSelectedBody() ?? undefined
544
+
545
+ if (body !== this.saveSelectedBodyControlor) {
546
+ this.saveSelectedBodyControlor = body
547
+ ? new BodyControlor(body!)
548
+ : undefined
549
+ this.ikSolver = body
550
+ ? this.saveSelectedBodyControlor?.GetIKSolver()
551
+ : undefined
552
+ }
553
+
554
+ if (IsTranslate(this.getSelectedPart()?.name ?? ''))
555
+ this.ikSolver?.update()
556
+ else this.saveSelectedBodyControlor?.ResetAllTargetsPosition()
557
+ }
558
+
559
+ render(width: number = this.Width, height: number = this.Height) {
560
+ this.updateSelectedBodyIKSolver()
561
+
562
+ this.renderer.setViewport(0, 0, width, height)
563
+ this.renderer.setScissor(0, 0, width, height)
564
+ this.renderer.setScissorTest(true)
565
+
566
+ this.renderer.render(this.scene, this.camera)
567
+ }
568
+
569
+ autoSize = true
570
+ outputWidth = 0
571
+ outputHeight = 0
572
+ get OutputWidth() {
573
+ return this.autoSize
574
+ ? this.Width
575
+ : this.outputWidth === 0
576
+ ? this.Height
577
+ : this.outputWidth
578
+ }
579
+ set OutputWidth(value: number) {
580
+ this.autoSize = false
581
+ this.outputWidth = value
582
+ }
583
+ get OutputHeight() {
584
+ return this.autoSize
585
+ ? this.Height
586
+ : this.outputHeight === 0
587
+ ? this.Height
588
+ : this.outputHeight
589
+ }
590
+
591
+ set OutputHeight(value: number) {
592
+ this.autoSize = false
593
+ this.outputHeight = value
594
+ }
595
+
596
+ renderPreview() {
597
+ const outputWidth = this.OutputWidth
598
+ const outputHeight = this.OutputHeight
599
+
600
+ const outputAspect = outputWidth / outputHeight
601
+ const maxOutoutAspect = 2
602
+ const [left, bottom, width, height] =
603
+ outputAspect > maxOutoutAspect
604
+ ? [
605
+ this.Width - 50 - 150 * maxOutoutAspect,
606
+ 220,
607
+ 150 * maxOutoutAspect,
608
+ (150 * maxOutoutAspect * outputHeight) / outputWidth,
609
+ ]
610
+ : [
611
+ this.Width - 50 - (150 * outputWidth) / outputHeight,
612
+ 220,
613
+ (150 * outputWidth) / outputHeight,
614
+ 150,
615
+ ]
616
+ const save = {
617
+ viewport: new THREE.Vector4(),
618
+ scissor: new THREE.Vector4(),
619
+ scissorTest: this.renderer.getScissorTest(),
620
+ aspect: this.camera.aspect,
621
+ }
622
+
623
+ this.renderer.getViewport(save.viewport)
624
+ this.renderer.getScissor(save.viewport)
625
+
626
+ this.renderer.setViewport(left, bottom, width, height)
627
+ this.renderer.setScissor(left, bottom, width, height)
628
+ this.renderer.setScissorTest(true)
629
+ this.camera.aspect = width / height
630
+ this.camera.updateProjectionMatrix()
631
+
632
+ const restoreView = this.changeView()
633
+ this.renderer.render(this.scene, this.camera)
634
+ restoreView()
635
+ // restore
636
+ this.renderer.setViewport(save.viewport)
637
+ this.renderer.setScissor(save.scissor)
638
+ this.renderer.setScissorTest(save.scissorTest)
639
+ this.camera.aspect = save.aspect
640
+ this.camera.updateProjectionMatrix()
641
+ }
642
+
643
+ renderOutputBySize(
644
+ outputWidth: number,
645
+ outputHeight: number,
646
+ render: (outputWidth: number, outputHeight: number) => void
647
+ ) {
648
+ const save = {
649
+ aspect: this.camera.aspect,
650
+ }
651
+ this.camera.aspect = outputWidth / outputHeight
652
+ this.camera.updateProjectionMatrix()
653
+ this.outputRenderer.setSize(outputWidth, outputHeight, true)
654
+
655
+ render(outputWidth, outputHeight)
656
+
657
+ this.camera.aspect = save.aspect
658
+ this.camera.updateProjectionMatrix()
659
+ }
660
+
661
+ renderOutput(
662
+ scale = 1,
663
+ custom?: (outputWidth: number, outputHeight: number) => void
664
+ ) {
665
+ const outputWidth = this.OutputWidth * scale
666
+ const outputHeight = this.OutputHeight * scale
667
+
668
+ const render = () => {
669
+ this.outputRenderer.render(this.scene, this.camera)
670
+ }
671
+ this.renderOutputBySize(outputWidth, outputHeight, custom ?? render)
672
+ }
673
+ getOutputPNG() {
674
+ return this.outputRenderer.domElement.toDataURL('image/png')
675
+ }
676
+ animate() {
677
+ if (this.paused) {
678
+ return
679
+ }
680
+ requestAnimationFrame(this.animate)
681
+ this.handleResize()
682
+ this.render()
683
+ this.outputPreview()
684
+ this.stats?.update()
685
+ }
686
+
687
+ outputPreview() {
688
+ if (this.enablePreview) this.CapturePreview()
689
+ this.PreviewEventManager.TriggerEvent(this.enablePreview)
690
+ }
691
+ pause() {
692
+ this.paused = true
693
+ }
694
+
695
+ resume() {
696
+ this.paused = false
697
+ this.animate()
698
+ }
699
+
700
+ getAncestors(o: Object3D) {
701
+ const ancestors: Object3D[] = []
702
+ o.traverseAncestors((ancestor) => ancestors.push(ancestor))
703
+ return ancestors
704
+ }
705
+ getBodyByPart(o: Object3D) {
706
+ if (o?.name === 'torso') return o
707
+
708
+ const body =
709
+ this.getAncestors(o).find((o) => o?.name === 'torso') ?? null
710
+ return body
711
+ }
712
+
713
+ SelectEventManager = new EditorEventManager<BodyControlor>()
714
+ UnselectEventManager = new EditorEventManager<void>()
715
+ ContextMenuEventManager = new EditorEventManager<{
716
+ mouseX: number
717
+ mouseY: number
718
+ }>()
719
+ PreviewEventManager = new EditorEventManager<boolean>()
720
+ LockViewEventManager = new EditorEventManager<boolean>()
721
+
722
+ triggerSelectEvent(body: Object3D) {
723
+ const c = new BodyControlor(body)
724
+ this.SelectEventManager.TriggerEvent(c)
725
+ this.UpdateBones()
726
+ }
727
+ triggerUnselectEvent() {
728
+ this.UnselectEventManager.TriggerEvent()
729
+ this.UpdateBones()
730
+ }
731
+
732
+ addEvent() {
733
+ this.renderer.domElement.addEventListener(
734
+ 'mousedown',
735
+ this.onMouseDown,
736
+ false
737
+ )
738
+ this.renderer.domElement.addEventListener(
739
+ 'mousemove',
740
+ this.onMouseMove,
741
+ false
742
+ )
743
+ this.renderer.domElement.addEventListener(
744
+ 'mouseup',
745
+ this.onMouseUp,
746
+ false
747
+ )
748
+
749
+ this.renderer.domElement.addEventListener('resize', this.handleResize)
750
+
751
+ this.parentElem.addEventListener('keydown', this.handleKeyDown)
752
+ this.parentElem.addEventListener('keyup', this.handleKeyUp)
753
+ }
754
+
755
+ removeEvent() {
756
+ this.renderer.domElement.removeEventListener(
757
+ 'mousedown',
758
+ this.onMouseDown,
759
+ false
760
+ )
761
+ this.renderer.domElement.removeEventListener(
762
+ 'mousemove',
763
+ this.onMouseMove,
764
+ false
765
+ )
766
+ this.renderer.domElement.removeEventListener(
767
+ 'mouseup',
768
+ this.onMouseUp,
769
+ false
770
+ )
771
+
772
+ this.renderer.domElement.removeEventListener(
773
+ 'resize',
774
+ this.handleResize
775
+ )
776
+
777
+ this.parentElem.removeEventListener('keydown', this.handleKeyDown)
778
+ this.parentElem.removeEventListener('keyup', this.handleKeyUp)
779
+ }
780
+
781
+ onMouseDown(event: MouseEvent) {
782
+ event.preventDefault()
783
+ this.IsClick = true
784
+ }
785
+ onMouseMove(event: MouseEvent) {
786
+ // some devices still send movemove event, filter it.
787
+ if (event.movementX == 0 && event.movementY == 0) return
788
+ this.IsClick = false
789
+ }
790
+
791
+ onMouseUp(event: MouseEvent) {
792
+ const x = event.offsetX - this.renderer.domElement.offsetLeft
793
+ const y = event.offsetY - this.renderer.domElement.offsetTop
794
+ this.raycaster.setFromCamera(
795
+ {
796
+ x: (x / this.renderer.domElement.clientWidth) * 2 - 1,
797
+ y: -(y / this.renderer.domElement.clientHeight) * 2 + 1,
798
+ },
799
+ this.camera
800
+ )
801
+ const intersects: THREE.Intersection[] =
802
+ this.raycaster.intersectObjects(this.GetBodies(), true)
803
+ // If read_point is found, choose it first
804
+ const point = intersects.find((o) => o.object.name === 'red_point')
805
+ const intersectedObject: THREE.Object3D | null = point
806
+ ? point.object
807
+ : intersects.length > 0
808
+ ? intersects[0].object
809
+ : null
810
+ const name = intersectedObject ? intersectedObject.name : ''
811
+ let obj: Object3D | null = intersectedObject
812
+
813
+ console.log(obj?.name)
814
+
815
+ if (this.IsClick) {
816
+ if (event.button === 2 || event.which === 3) {
817
+ console.log('Right mouse button released')
818
+ this.ContextMenuEventManager.TriggerEvent({
819
+ mouseX: x,
820
+ mouseY: y,
821
+ })
822
+ return
823
+ }
824
+
825
+ if (!obj) {
826
+ this.DetachTransfromControl()
827
+ this.triggerUnselectEvent()
828
+ return
829
+ }
830
+
831
+ if (this.MoveMode) {
832
+ const isOk = IsPickable(name, this.FreeMode)
833
+
834
+ if (!isOk) {
835
+ obj =
836
+ this.getAncestors(obj).find((o) =>
837
+ IsPickable(o.name, this.FreeMode)
838
+ ) ?? null
839
+ }
840
+
841
+ if (obj) {
842
+ if (IsTranslate(obj.name, this.FreeMode) === false)
843
+ obj = this.getBodyByPart(obj)
844
+ }
845
+
846
+ if (obj) {
847
+ console.log(obj.name)
848
+ this.transformControl.setMode('translate')
849
+ this.transformControl.setSpace('world')
850
+ this.transformControl.attach(obj)
851
+ const body = this.getBodyByPart(obj)
852
+ if (body) this.triggerSelectEvent(body)
853
+ }
854
+ } else {
855
+ const isOk = IsPickable(name)
856
+
857
+ if (!isOk) {
858
+ obj =
859
+ this.getAncestors(obj).find((o) =>
860
+ IsPickable(o.name)
861
+ ) ?? null
862
+ }
863
+
864
+ if (obj) {
865
+ console.log(obj.name)
866
+
867
+ if (IsTranslate(obj.name)) {
868
+ this.transformControl.setMode('translate')
869
+ this.transformControl.setSpace('world')
870
+ } else {
871
+ this.transformControl.setMode('rotate')
872
+ this.transformControl.setSpace('local')
873
+ }
874
+
875
+ this.transformControl.attach(obj)
876
+
877
+ const body = this.getBodyByPart(obj)
878
+ if (body) this.triggerSelectEvent(body)
879
+ }
880
+ }
881
+ }
882
+ }
883
+
884
+ traverseHandObjecct(handle: (o: THREE.Mesh) => void) {
885
+ this.GetBodies().forEach((o) => {
886
+ o.traverse((child) => {
887
+ if (IsHand(child?.name)) {
888
+ handle(child as THREE.Mesh)
889
+ }
890
+ })
891
+ })
892
+ }
893
+
894
+ traverseBodies(handle: (o: Object3D) => void) {
895
+ this.GetBodies().forEach((o) => {
896
+ o.traverse((child) => {
897
+ handle(child)
898
+ })
899
+ })
900
+ }
901
+
902
+ traverseBones(handle: (o: Bone) => void) {
903
+ this.GetBodies().forEach((o) => {
904
+ o.traverse((child) => {
905
+ if (child instanceof Bone && IsBone(child.name)) handle(child)
906
+ })
907
+ })
908
+ }
909
+
910
+ traverseExtremities(handle: (o: THREE.Mesh) => void) {
911
+ this.GetBodies().forEach((o) => {
912
+ o.traverse((child) => {
913
+ if (IsExtremities(child.name)) {
914
+ handle(child as THREE.Mesh)
915
+ }
916
+ })
917
+ })
918
+ }
919
+
920
+ onlyShowSkeleton() {
921
+ const recoveryArr: Object3D[] = []
922
+ this.traverseBodies((o) => {
923
+ if (IsSkeleton(o.name) === false) {
924
+ if (o.visible == true) {
925
+ o.visible = false
926
+ recoveryArr.push(o)
927
+ }
928
+ }
929
+ })
930
+
931
+ return () => {
932
+ recoveryArr.forEach((o) => (o.visible = true))
933
+ }
934
+ }
935
+
936
+ showMask() {
937
+ const recoveryArr: Object3D[] = []
938
+ this.scene.traverse((o) => {
939
+ if (IsMask(o.name)) {
940
+ console.log(o.name)
941
+ o.visible = true
942
+ recoveryArr.push(o)
943
+ }
944
+ })
945
+
946
+ return () => {
947
+ recoveryArr.forEach((o) => (o.visible = false))
948
+ }
949
+ }
950
+
951
+ hideSkeleten() {
952
+ const map = new Map<Object3D, Object3D | null>()
953
+
954
+ this.GetBodies().forEach((o) => {
955
+ o.traverse((child) => {
956
+ if (IsExtremities(child?.name)) {
957
+ map.set(child, child.parent)
958
+ this.scene.attach(child)
959
+ } else if (child?.name === 'red_point') {
960
+ child.visible = false
961
+ }
962
+ })
963
+ o.visible = false
964
+ })
965
+ return map
966
+ }
967
+
968
+ GetBodies() {
969
+ return this.scene.children.filter((o) => o?.name === 'torso')
970
+ }
971
+ showSkeleten(map: Map<Object3D, Object3D | null>) {
972
+ for (const [k, v] of map.entries()) {
973
+ v?.attach(k)
974
+ }
975
+
976
+ map.clear()
977
+
978
+ this.GetBodies().forEach((o) => {
979
+ o.traverse((child) => {
980
+ if (child?.name === 'red_point') {
981
+ child.visible = true
982
+ }
983
+ })
984
+ o.visible = true
985
+ })
986
+ }
987
+
988
+ changeComposer(enable: boolean) {
989
+ const save = this.enableComposer
990
+ this.enableComposer = enable
991
+
992
+ return () => (this.enableComposer = save)
993
+ }
994
+
995
+ changeHandMaterialTraverse(type: 'depth' | 'normal' | 'phone') {
996
+ const map = new Map<THREE.Mesh, Material | Material[]>()
997
+ this.scene.traverse((child) => {
998
+ if (!IsExtremities(child.name)) return
999
+ const o = GetExtremityMesh(child) as THREE.Mesh
1000
+ map.set(o, o.material)
1001
+ if (type == 'depth') o.material = new MeshDepthMaterial()
1002
+ else if (type == 'normal') o.material = new MeshNormalMaterial()
1003
+ else if (type == 'phone') o.material = new MeshPhongMaterial()
1004
+ })
1005
+
1006
+ return () => {
1007
+ for (const [k, v] of map.entries()) {
1008
+ k.material = v
1009
+ }
1010
+
1011
+ map.clear()
1012
+ }
1013
+ }
1014
+
1015
+ changeHandMaterial(type: 'depth' | 'normal' | 'phone') {
1016
+ if (type == 'depth')
1017
+ this.scene.overrideMaterial = new MeshDepthMaterial()
1018
+ else if (type == 'normal')
1019
+ this.scene.overrideMaterial = new MeshNormalMaterial()
1020
+ else if (type == 'phone')
1021
+ this.scene.overrideMaterial = new MeshPhongMaterial()
1022
+
1023
+ return () => {
1024
+ this.scene.overrideMaterial = null
1025
+ }
1026
+ }
1027
+
1028
+ // https://stackoverflow.com/questions/15696963/three-js-set-and-read-camera-look-vector?noredirect=1&lq=1
1029
+ getCameraLookAtVector() {
1030
+ const lookAtVector = new THREE.Vector3(0, 0, -1)
1031
+ lookAtVector.applyQuaternion(this.camera.quaternion)
1032
+ return lookAtVector
1033
+ }
1034
+
1035
+ getZDistanceFromCamera(p: THREE.Vector3) {
1036
+ const lookAt = this.getCameraLookAtVector().normalize()
1037
+ const v = p.clone().sub(this.camera.position)
1038
+ return v.dot(lookAt)
1039
+ }
1040
+ changeCamera() {
1041
+ let hands: THREE.Mesh[] = []
1042
+ this.scene.traverse((o) => {
1043
+ if (this.OnlyHand) {
1044
+ if (IsHand(o?.name)) hands.push(o as THREE.Mesh)
1045
+ } else {
1046
+ if (IsExtremities(o?.name)) hands.push(o as THREE.Mesh)
1047
+ }
1048
+ })
1049
+
1050
+ // filter object in frustum
1051
+ hands = this.objectInView(hands)
1052
+
1053
+ const cameraPos = new THREE.Vector3()
1054
+ this.camera.getWorldPosition(cameraPos)
1055
+
1056
+ const handsPos = hands.map((o) => {
1057
+ const cameraPos = new THREE.Vector3()
1058
+ o.getWorldPosition(cameraPos)
1059
+ return cameraPos
1060
+ })
1061
+
1062
+ const handsDis = handsPos.map((pos) => {
1063
+ return this.getZDistanceFromCamera(pos)
1064
+ })
1065
+
1066
+ const minDis = Math.min(...handsDis)
1067
+ const maxDis = Math.max(...handsDis)
1068
+
1069
+ const saveNear = this.camera.near
1070
+ const saveFar = this.camera.far
1071
+
1072
+ this.camera.near = Math.max(minDis - 20, 0)
1073
+ this.camera.far = Math.max(maxDis + 20, 20)
1074
+ console.log('camera', this.camera.near, this.camera.far)
1075
+
1076
+ this.camera.updateProjectionMatrix()
1077
+ return () => {
1078
+ this.camera.near = saveNear
1079
+ this.camera.far = saveFar
1080
+ this.camera.updateProjectionMatrix()
1081
+ }
1082
+ }
1083
+
1084
+ Capture() {
1085
+ const restore = this.onlyShowSkeleton()
1086
+
1087
+ this.renderOutput()
1088
+ const imgData = this.getOutputPNG()
1089
+
1090
+ restore()
1091
+
1092
+ return imgData
1093
+ }
1094
+
1095
+ CapturePreview() {
1096
+ const scale = (window.devicePixelRatio * 140.0) / this.OutputHeight
1097
+
1098
+ const outputWidth = this.OutputWidth * scale
1099
+ const outputHeight = this.OutputHeight * scale
1100
+
1101
+ this.previewRenderer.render(
1102
+ outputWidth,
1103
+ outputHeight,
1104
+ this.cameraDataOfView
1105
+ )
1106
+ }
1107
+
1108
+ CaptureCanny() {
1109
+ this.renderOutput(1, (outputWidth, outputHeight) => {
1110
+ this.changeComposerResoultion(outputWidth, outputHeight)
1111
+ const restoreMaterialTraverse =
1112
+ this.changeHandMaterialTraverse('normal')
1113
+ // step 1: get mask image
1114
+ const restoreMask = this.showMask()
1115
+ this.composer?.render()
1116
+ restoreMask()
1117
+
1118
+ // step 2:
1119
+ // get sobel image
1120
+ // filer out pixels not in mask
1121
+ // get binarized pixels
1122
+ this.finalComposer?.render()
1123
+ restoreMaterialTraverse()
1124
+ })
1125
+
1126
+ return this.getOutputPNG()
1127
+ }
1128
+
1129
+ CaptureNormal() {
1130
+ const restoreHand = this.changeHandMaterial('normal')
1131
+ this.renderOutput()
1132
+ restoreHand()
1133
+ return this.getOutputPNG()
1134
+ }
1135
+
1136
+ CaptureDepth() {
1137
+ const restoreHand = this.changeHandMaterial('depth')
1138
+ const restoreCamera = this.changeCamera()
1139
+ this.renderOutput()
1140
+ restoreCamera()
1141
+ restoreHand()
1142
+ return this.getOutputPNG()
1143
+ }
1144
+
1145
+ changeTransformControl() {
1146
+ const part = this.getSelectedPart()
1147
+
1148
+ if (part) {
1149
+ this.DetachTransfromControl()
1150
+ return () => {
1151
+ this.transformControl.attach(part)
1152
+ }
1153
+ }
1154
+
1155
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
1156
+ return () => {}
1157
+ }
1158
+ changeHelper() {
1159
+ const old = {
1160
+ axesHelper: this.axesHelper.visible,
1161
+ gridHelper: this.gridHelper.visible,
1162
+ }
1163
+ this.axesHelper.visible = false
1164
+ this.gridHelper.visible = false
1165
+
1166
+ return () => {
1167
+ this.axesHelper.visible = old.axesHelper
1168
+ this.gridHelper.visible = old.gridHelper
1169
+ }
1170
+ }
1171
+ MakeImages() {
1172
+ this.renderer.setClearColor(0x000000)
1173
+
1174
+ const restoreHelper = this.changeHelper()
1175
+
1176
+ const restoreTransfromControl = this.changeTransformControl()
1177
+ const restoreView = this.changeView()
1178
+
1179
+ const poseImage = this.Capture()
1180
+
1181
+ /// begin
1182
+ const map = this.hideSkeleten()
1183
+ const depthImage = this.CaptureDepth()
1184
+ const normalImage = this.CaptureNormal()
1185
+ const cannyImage = this.CaptureCanny()
1186
+ this.showSkeleten(map)
1187
+ /// end
1188
+
1189
+ this.renderer.setClearColor(0x000000, 0)
1190
+ restoreHelper()
1191
+
1192
+ restoreTransfromControl()
1193
+ restoreView()
1194
+
1195
+ const result = {
1196
+ pose: poseImage,
1197
+ depth: depthImage,
1198
+ normal: normalImage,
1199
+ canny: cannyImage,
1200
+ }
1201
+
1202
+ sendToAll({
1203
+ method: 'MakeImages',
1204
+ type: 'event',
1205
+ payload: result,
1206
+ })
1207
+ return result
1208
+ }
1209
+
1210
+ CopySelectedBody() {
1211
+ const list = this.GetBodies()
1212
+
1213
+ const selectedBody = this.getSelectedBody()
1214
+
1215
+ if (!selectedBody && list.length !== 0) return
1216
+
1217
+ const body =
1218
+ list.length === 0 ? CloneBody() : SkeletonUtils.clone(selectedBody!)
1219
+
1220
+ if (!body) return
1221
+
1222
+ this.pushCommand(this.CreateAddBodyCommand(body))
1223
+
1224
+ if (list.length !== 0) body.position.x += 10
1225
+ this.scene.add(body)
1226
+ this.fixFootVisible()
1227
+ this.transformControl.setMode('translate')
1228
+ this.transformControl.setSpace('world')
1229
+
1230
+ this.transformControl.attach(body)
1231
+ }
1232
+
1233
+ CopyBodyZ() {
1234
+ const body = CloneBody()
1235
+ if (!body) return
1236
+
1237
+ const list = this.GetBodies()
1238
+ .filter((o) => o.position.x === 0)
1239
+ .map((o) => Math.ceil(o.position.z / 30))
1240
+
1241
+ if (list.length > 0) body.translateZ((Math.min(...list) - 1) * 30)
1242
+
1243
+ this.pushCommand(this.CreateAddBodyCommand(body))
1244
+
1245
+ this.scene.add(body)
1246
+ this.fixFootVisible()
1247
+ }
1248
+
1249
+ CopyBodyX() {
1250
+ const body = CloneBody()
1251
+ if (!body) return
1252
+
1253
+ const list = this.GetBodies()
1254
+ .filter((o) => o.position.z === 0)
1255
+ .map((o) => Math.ceil(o.position.x / 50))
1256
+
1257
+ if (list.length > 0) body.translateX((Math.min(...list) - 1) * 50)
1258
+
1259
+ this.pushCommand(this.CreateAddBodyCommand(body))
1260
+
1261
+ this.scene.add(body)
1262
+ this.fixFootVisible()
1263
+ }
1264
+
1265
+ getSelectedBody() {
1266
+ let obj: Object3D | null = this.getSelectedPart() ?? null
1267
+ obj = obj ? this.getBodyByPart(obj) : null
1268
+
1269
+ return obj
1270
+ }
1271
+ getSelectedPart() {
1272
+ return this.transformControl.object
1273
+ }
1274
+
1275
+ getHandByPart(o: Object3D) {
1276
+ if (IsHand(o?.name)) return o
1277
+
1278
+ const body = this.getAncestors(o).find((o) => IsHand(o?.name)) ?? null
1279
+ return body
1280
+ }
1281
+
1282
+ getSelectedHand() {
1283
+ let obj: Object3D | null = this.getSelectedPart() ?? null
1284
+ obj = obj ? this.getHandByPart(obj) : null
1285
+ return obj
1286
+ }
1287
+ RemoveBody() {
1288
+ const obj = this.getSelectedBody()
1289
+
1290
+ if (obj) {
1291
+ this.pushCommand(this.CreateRemoveBodyCommand(obj))
1292
+ console.log(obj.name)
1293
+ obj.removeFromParent()
1294
+ this.DetachTransfromControl()
1295
+ }
1296
+ }
1297
+
1298
+ pointsInView(points: THREE.Vector3[]) {
1299
+ this.camera.updateMatrix() // make sure camera's local matrix is updated
1300
+ this.camera.updateMatrixWorld() // make sure camera's world matrix is updated
1301
+
1302
+ const frustum = new THREE.Frustum().setFromProjectionMatrix(
1303
+ new THREE.Matrix4().multiplyMatrices(
1304
+ this.camera.projectionMatrix,
1305
+ this.camera.matrixWorldInverse
1306
+ )
1307
+ )
1308
+
1309
+ //console.log(points);
1310
+ return points.filter((p) => frustum.containsPoint(p))
1311
+ }
1312
+
1313
+ getBouningSphere(o: Object3D) {
1314
+ const bbox = new THREE.Box3().setFromObject(o, true)
1315
+ // const helper = new THREE.Box3Helper(bbox, new THREE.Color(0, 255, 0))
1316
+ // this.scene.add(helper)
1317
+
1318
+ const center = new THREE.Vector3()
1319
+ bbox.getCenter(center)
1320
+
1321
+ const bsphere = bbox.getBoundingSphere(new THREE.Sphere(center))
1322
+
1323
+ return bsphere
1324
+ }
1325
+ objectInView<T extends Object3D>(objs: T[]) {
1326
+ this.camera.updateMatrix() // make sure camera's local matrix is updated
1327
+ this.camera.updateMatrixWorld() // make sure camera's world matrix is updated
1328
+
1329
+ const frustum = new THREE.Frustum().setFromProjectionMatrix(
1330
+ new THREE.Matrix4().multiplyMatrices(
1331
+ this.camera.projectionMatrix,
1332
+ this.camera.matrixWorldInverse
1333
+ )
1334
+ )
1335
+
1336
+ //console.log(points);
1337
+ return objs.filter((obj) => {
1338
+ const sphere = this.getBouningSphere(obj)
1339
+ return frustum.intersectsSphere(sphere)
1340
+ })
1341
+ }
1342
+ isMoveMode = false
1343
+ get MoveMode() {
1344
+ return this.isMoveMode
1345
+ }
1346
+ set MoveMode(move: boolean) {
1347
+ let IsTranslateMode = move
1348
+ this.isMoveMode = move
1349
+
1350
+ const name = this.getSelectedPart()?.name ?? ''
1351
+
1352
+ if (move) {
1353
+ if (IsTranslate(name, this.FreeMode)) {
1354
+ IsTranslateMode = true
1355
+ } else {
1356
+ const obj = this.getSelectedBody()
1357
+ if (obj) this.transformControl.attach(obj)
1358
+ }
1359
+ } else {
1360
+ if (IsTarget(name)) {
1361
+ IsTranslateMode = true
1362
+ }
1363
+ }
1364
+
1365
+ if (IsTranslateMode) {
1366
+ this.transformControl.setMode('translate')
1367
+ this.transformControl.setSpace('world')
1368
+ } else {
1369
+ this.transformControl.setMode('rotate')
1370
+ this.transformControl.setSpace('local')
1371
+ }
1372
+ }
1373
+
1374
+ FreeMode = true
1375
+
1376
+ get Width() {
1377
+ return this.renderer.domElement.clientWidth
1378
+ }
1379
+
1380
+ get Height() {
1381
+ return this.renderer.domElement.clientHeight
1382
+ }
1383
+
1384
+ onlyHand = false
1385
+ get OnlyHand() {
1386
+ return this.onlyHand
1387
+ }
1388
+
1389
+ set OnlyHand(value: boolean) {
1390
+ this.onlyHand = value
1391
+ this.setFootVisible(!this.onlyHand)
1392
+ }
1393
+
1394
+ get EnableHelper() {
1395
+ return this.enableHelper
1396
+ }
1397
+ set EnableHelper(value: boolean) {
1398
+ this.enableHelper = value
1399
+ this.gridHelper.visible = value
1400
+ this.axesHelper.visible = value
1401
+ }
1402
+ setFootVisible(value: boolean) {
1403
+ this.traverseExtremities((o) => {
1404
+ if (IsFoot(o.name)) {
1405
+ o.visible = value
1406
+ }
1407
+ })
1408
+ }
1409
+
1410
+ fixFootVisible() {
1411
+ this.setFootVisible(!this.OnlyHand)
1412
+ }
1413
+
1414
+ handleResize() {
1415
+ const size = new THREE.Vector2()
1416
+ this.renderer.getSize(size)
1417
+
1418
+ if (size.width == this.Width && size.height === this.Height) return
1419
+
1420
+ const canvas = this.renderer.domElement
1421
+ if (canvas.clientWidth == 0 || canvas.clientHeight == 0) return
1422
+ this.camera.aspect = canvas.clientWidth / canvas.clientHeight
1423
+
1424
+ this.camera.updateProjectionMatrix()
1425
+
1426
+ // console.log(canvas.clientWidth, canvas.clientHeight)
1427
+ this.renderer.setSize(canvas.clientWidth, canvas.clientHeight, false)
1428
+ // console.log(this.Width, this.Height)
1429
+ }
1430
+
1431
+ initEdgeComposer() {
1432
+ this.composer = new EffectComposer(this.outputRenderer)
1433
+ const renderPass = new RenderPass(this.scene, this.camera)
1434
+ this.composer.addPass(renderPass)
1435
+ this.composer.renderToScreen = false
1436
+
1437
+ const finalPass = new ShaderPass(
1438
+ new THREE.ShaderMaterial({
1439
+ uniforms: {
1440
+ baseTexture: { value: null },
1441
+ bloomTexture: {
1442
+ value: this.composer.renderTarget2.texture,
1443
+ },
1444
+ },
1445
+ vertexShader: `
1446
+ varying vec2 vUv;
1447
+ void main() {
1448
+ vUv = uv;
1449
+ gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
1450
+
1451
+ }`,
1452
+ fragmentShader: `
1453
+ uniform sampler2D baseTexture;
1454
+ uniform sampler2D bloomTexture;
1455
+
1456
+ varying vec2 vUv;
1457
+
1458
+ void main() {
1459
+ vec4 bloomColor = texture2D(bloomTexture, vUv);
1460
+ float grayValue = dot(bloomColor.rgb, vec3(0.299, 0.587, 0.114));
1461
+ vec4 baseColor = texture2D(baseTexture, vUv);
1462
+ vec4 masked = vec4(baseColor.rgb * step(0.001, grayValue), 1.0);
1463
+ gl_FragColor = step(0.5, masked) * vec4(1.0); // Binarization
1464
+ // gl_FragColor = bloomColor;
1465
+ }
1466
+
1467
+ `,
1468
+ defines: {},
1469
+ }),
1470
+ 'baseTexture'
1471
+ )
1472
+ finalPass.needsSwap = true
1473
+
1474
+ this.finalComposer = new EffectComposer(this.outputRenderer)
1475
+ this.finalComposer.addPass(renderPass)
1476
+
1477
+ // color to grayscale conversion
1478
+ const effectGrayScale = new ShaderPass(LuminosityShader)
1479
+ this.finalComposer.addPass(effectGrayScale)
1480
+
1481
+ // Sobel operator
1482
+ const effectSobel = new ShaderPass(SobelOperatorShader)
1483
+ effectSobel.uniforms['resolution'].value.x =
1484
+ this.Width * window.devicePixelRatio
1485
+ effectSobel.uniforms['resolution'].value.y =
1486
+ this.Height * window.devicePixelRatio
1487
+ this.finalComposer.addPass(effectSobel)
1488
+
1489
+ this.effectSobel = effectSobel
1490
+
1491
+ this.finalComposer.addPass(finalPass)
1492
+ }
1493
+
1494
+ changeComposerResoultion(width: number, height: number) {
1495
+ this.composer?.setSize(width, height)
1496
+ this.finalComposer?.setSize(width, height)
1497
+
1498
+ if (this.effectSobel) {
1499
+ this.effectSobel.uniforms['resolution'].value.x =
1500
+ width * window.devicePixelRatio
1501
+ this.effectSobel.uniforms['resolution'].value.y =
1502
+ height * window.devicePixelRatio
1503
+ }
1504
+ }
1505
+
1506
+ get CameraNear() {
1507
+ return this.camera.near
1508
+ }
1509
+ set CameraNear(value: number) {
1510
+ this.camera.near = value
1511
+ this.camera.updateProjectionMatrix()
1512
+ }
1513
+
1514
+ get CameraFar() {
1515
+ return this.camera.far
1516
+ }
1517
+ set CameraFar(value: number) {
1518
+ this.camera.far = value
1519
+ this.camera.updateProjectionMatrix()
1520
+ }
1521
+
1522
+ get CameraFocalLength() {
1523
+ return this.camera.getFocalLength()
1524
+ }
1525
+ set CameraFocalLength(value) {
1526
+ this.camera.setFocalLength(value)
1527
+ }
1528
+
1529
+ GetCameraData() {
1530
+ const result = {
1531
+ position: this.camera.position.toArray(),
1532
+ rotation: this.camera.rotation.toArray(),
1533
+ target: this.orbitControls.target.toArray(),
1534
+ near: this.camera.near,
1535
+ far: this.camera.far,
1536
+ zoom: this.camera.zoom,
1537
+ }
1538
+
1539
+ return result
1540
+ }
1541
+ GetSceneData() {
1542
+ const bodies = this.GetBodies().map((o) =>
1543
+ new BodyControlor(o).GetBodyData()
1544
+ )
1545
+
1546
+ const data = {
1547
+ header: 'Openpose Editor by Yu Zhu',
1548
+ version: __APP_VERSION__,
1549
+ object: {
1550
+ bodies: bodies,
1551
+ camera: this.GetCameraData(),
1552
+ },
1553
+ setting: {},
1554
+ }
1555
+
1556
+ return data
1557
+ }
1558
+ GetGesture() {
1559
+ const hand = this.getSelectedHand()
1560
+ const body = this.getSelectedBody()
1561
+
1562
+ if (!hand || !body) return null
1563
+ const data = {
1564
+ header: 'Openpose Editor by Yu Zhu',
1565
+ version: __APP_VERSION__,
1566
+ object: {
1567
+ hand: new BodyControlor(body).GetHandData(
1568
+ hand.name === 'left_hand' ? 'left_hand' : 'right_hand'
1569
+ ),
1570
+ },
1571
+ setting: {},
1572
+ }
1573
+
1574
+ return data
1575
+ }
1576
+
1577
+ AutoSaveScene() {
1578
+ try {
1579
+ const rawData = localStorage.getItem('AutoSaveSceneData')
1580
+ if (rawData) {
1581
+ localStorage.setItem('LastSceneData', rawData)
1582
+ }
1583
+ setInterval(() => {
1584
+ localStorage.setItem(
1585
+ 'AutoSaveSceneData',
1586
+ JSON.stringify(this.GetSceneData())
1587
+ )
1588
+ }, 5000)
1589
+ } catch (error) {
1590
+ console.error(error)
1591
+ }
1592
+ }
1593
+
1594
+ SaveScene() {
1595
+ try {
1596
+ downloadJson(
1597
+ JSON.stringify(this.GetSceneData()),
1598
+ `scene_${getCurrentTime()}.json`
1599
+ )
1600
+ } catch (error) {
1601
+ console.error(error)
1602
+ }
1603
+ }
1604
+ RestoreGesture(rawData: string) {
1605
+ const data = JSON.parse(rawData)
1606
+
1607
+ const {
1608
+ version,
1609
+ object: { hand: handData },
1610
+ setting,
1611
+ } = data
1612
+
1613
+ if (!handData) throw new Error('Invalid json')
1614
+ const hand = this.getSelectedHand()
1615
+ const body = this.getSelectedBody()
1616
+
1617
+ if (!hand || !body) throw new Error('!hand || !body')
1618
+
1619
+ new BodyControlor(body).RestoreHand(
1620
+ hand.name == 'left_hand' ? 'left_hand' : 'right_hand',
1621
+ handData
1622
+ )
1623
+ }
1624
+ SaveGesture() {
1625
+ const data = this.GetGesture()
1626
+ if (!data) throw new Error('Failed to get gesture')
1627
+ downloadJson(JSON.stringify(data), `gesture_${getCurrentTime()}.json`)
1628
+ }
1629
+
1630
+ ClearScene() {
1631
+ this.GetBodies().forEach((o) => o.removeFromParent())
1632
+ }
1633
+
1634
+ CreateBodiesFromData(bodies: BodyData[]) {
1635
+ return bodies.map((data) => {
1636
+ const body = CloneBody()!
1637
+ new BodyControlor(body).RestoreBody(data)
1638
+ return body
1639
+ })
1640
+ }
1641
+ RestoreCamera(data: CameraData, updateOrbitControl = true) {
1642
+ this.camera.position.fromArray(data.position)
1643
+ this.camera.rotation.fromArray(data.rotation as any)
1644
+ this.camera.near = data.near
1645
+ this.camera.far = data.far
1646
+ this.camera.zoom = data.zoom
1647
+ this.camera.updateProjectionMatrix()
1648
+
1649
+ if (data.target) this.orbitControls.target.fromArray(data.target)
1650
+ if (updateOrbitControl) this.orbitControls.update() // fix position change
1651
+ }
1652
+ RestoreScene(rawData: string) {
1653
+ try {
1654
+ if (!rawData) return
1655
+ const data = JSON.parse(rawData)
1656
+
1657
+ const {
1658
+ version,
1659
+ object: { bodies, camera },
1660
+ setting,
1661
+ } = data
1662
+
1663
+ const bodiesObject = this.CreateBodiesFromData(bodies)
1664
+ this.ClearScene()
1665
+
1666
+ if (bodiesObject.length > 0) this.scene.add(...bodiesObject)
1667
+ for (const body of bodiesObject) {
1668
+ new BodyControlor(body).ResetAllTargetsPosition()
1669
+ }
1670
+ this.RestoreCamera(camera)
1671
+ } catch (error: any) {
1672
+ Oops(error)
1673
+ console.error(error)
1674
+ }
1675
+ }
1676
+ ResetScene() {
1677
+ try {
1678
+ this.ClearScene()
1679
+ this.CopySelectedBody()
1680
+ const body = this.getSelectedBody()
1681
+ if (body) {
1682
+ this.scene.add(body)
1683
+ this.dlight.target = body
1684
+ }
1685
+ } catch (error: any) {
1686
+ Oops(error)
1687
+ console.error(error)
1688
+ }
1689
+ }
1690
+ RestoreLastSavedScene() {
1691
+ const rawData = localStorage.getItem('LastSceneData')
1692
+ if (rawData) this.RestoreScene(rawData)
1693
+ }
1694
+ async LoadScene() {
1695
+ const rawData = await uploadJson()
1696
+ if (rawData) this.RestoreScene(rawData)
1697
+ }
1698
+
1699
+ // drawPoseData(positions: THREE.Vector3[]) {
1700
+ // const objects: Record<
1701
+ // keyof typeof PartIndexMappingOfBlazePoseModel,
1702
+ // Object3D
1703
+ // > = Object.fromEntries(
1704
+ // Object.keys(PartIndexMappingOfBlazePoseModel).map((name) => {
1705
+ // const p = positions[PartIndexMappingOfBlazePoseModel[name]]
1706
+ // const material = new THREE.MeshBasicMaterial({
1707
+ // color: 0xff0000,
1708
+ // })
1709
+ // const mesh = new THREE.Mesh(
1710
+ // new THREE.SphereGeometry(1),
1711
+ // material
1712
+ // )
1713
+ // mesh.position.copy(p)
1714
+ // this.scene.add(mesh)
1715
+ // return [name, mesh]
1716
+ // })
1717
+ // )
1718
+
1719
+ // const CreateLink2 = (
1720
+ // startObject: THREE.Object3D,
1721
+ // endObject: THREE.Object3D
1722
+ // ) => {
1723
+ // const startPosition = startObject.position
1724
+ // const endPostion = endObject.position
1725
+ // const distance = startPosition.distanceTo(endPostion)
1726
+
1727
+ // const material = new THREE.MeshBasicMaterial({
1728
+ // color: 0x666666,
1729
+ // opacity: 0.6,
1730
+ // transparent: true,
1731
+ // })
1732
+ // const mesh = new THREE.Mesh(new THREE.SphereGeometry(1), material)
1733
+
1734
+ // // 将拉伸后的球体放在中点,并计算旋转轴和角度
1735
+ // const origin = startPosition
1736
+ // .clone()
1737
+ // .add(endPostion)
1738
+ // .multiplyScalar(0.5)
1739
+ // const v = endPostion.clone().sub(startPosition)
1740
+ // const unit = new THREE.Vector3(1, 0, 0)
1741
+ // const axis = unit.clone().cross(v)
1742
+ // const angle = unit.clone().angleTo(v)
1743
+
1744
+ // mesh.scale.copy(new THREE.Vector3(distance / 2, 1, 1))
1745
+ // mesh.position.copy(origin)
1746
+ // mesh.setRotationFromAxisAngle(axis.normalize(), angle)
1747
+ // this.scene.add(mesh)
1748
+ // }
1749
+
1750
+ // CreateLink2(objects['left_shoulder'], objects['left_elbow'])
1751
+ // CreateLink2(objects['left_elbow'], objects['left_wrist'])
1752
+ // CreateLink2(objects['left_hip'], objects['left_knee'])
1753
+ // CreateLink2(objects['left_knee'], objects['left_ankle'])
1754
+ // CreateLink2(objects['right_shoulder'], objects['right_elbow'])
1755
+ // CreateLink2(objects['right_elbow'], objects['right_wrist'])
1756
+ // CreateLink2(objects['right_hip'], objects['right_knee'])
1757
+ // CreateLink2(objects['right_knee'], objects['right_ankle'])
1758
+
1759
+ // CreateLink2(objects['left_shoulder'], objects['right_shoulder'])
1760
+ // CreateLink2(objects['nose'], objects['right_eye'])
1761
+ // CreateLink2(objects['nose'], objects['left_eye'])
1762
+ // CreateLink2(objects['left_eye'], objects['left_ear'])
1763
+
1764
+ // CreateLink2(objects['right_eye'], objects['right_ear'])
1765
+ // }
1766
+ async GetBodyToSetPose() {
1767
+ const bodies = this.GetBodies()
1768
+ const body = bodies.length == 1 ? bodies[0] : this.getSelectedBody()
1769
+ return body
1770
+ }
1771
+ async SetPose(poseData: [number, number, number][]) {
1772
+ const body = await this.GetBodyToSetPose()
1773
+
1774
+ if (!body) return
1775
+
1776
+ const controlor = new BodyControlor(body)
1777
+ const old: BodyData = controlor.GetBodyData()
1778
+ controlor.SetPose(poseData)
1779
+ this.pushCommand(this.CreateAllTransformCommand(body, old))
1780
+ }
1781
+ async SetBlazePose(positions: [number, number, number][]) {
1782
+ const body = await this.GetBodyToSetPose()
1783
+ if (!body) return
1784
+
1785
+ const controlor = new BodyControlor(body)
1786
+ const old: BodyData = controlor.GetBodyData()
1787
+ controlor.SetBlazePose(positions)
1788
+ this.pushCommand(this.CreateAllTransformCommand(body, old))
1789
+ }
1790
+
1791
+ DetachTransfromControl() {
1792
+ this.transformControl.detach()
1793
+ this.triggerUnselectEvent()
1794
+ }
1795
+
1796
+ cameraDataOfView?: CameraData
1797
+ LockView() {
1798
+ this.cameraDataOfView = this.GetCameraData()
1799
+ this.LockViewEventManager.TriggerEvent(true)
1800
+ }
1801
+ UnlockView() {
1802
+ this.cameraDataOfView = undefined
1803
+ this.LockViewEventManager.TriggerEvent(false)
1804
+ }
1805
+ RestoreView() {
1806
+ if (this.cameraDataOfView) this.RestoreCamera(this.cameraDataOfView)
1807
+ }
1808
+
1809
+ changeView() {
1810
+ if (this.cameraDataOfView) {
1811
+ const old = this.GetCameraData()
1812
+ this.RestoreCamera(this.cameraDataOfView, false)
1813
+ return () => {
1814
+ this.RestoreCamera(old)
1815
+ }
1816
+ }
1817
+
1818
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
1819
+ return () => {}
1820
+ }
1821
+ getSelectedBone() {
1822
+ const part = this.getSelectedPart()
1823
+ const isSelectBone = part && IsBone(part.name)
1824
+ return isSelectBone ? (part as Bone) : null
1825
+ }
1826
+ UpdateBones() {
1827
+ const DEFAULT_COLOR = 0xff0000
1828
+ const DEFAULT = 1
1829
+ const SELECTED = 1
1830
+ const SELECTED_COLOR = 0xeeee00
1831
+ const ACTIVE = 1
1832
+ const INACTIVE = 0.2
1833
+
1834
+ const setColor = (
1835
+ bone: Bone,
1836
+ opacity: number,
1837
+ color = DEFAULT_COLOR
1838
+ ) => {
1839
+ const point = bone.children.find(
1840
+ (o) => o instanceof THREE.Mesh && !IsMask(o.name)
1841
+ ) as THREE.Mesh
1842
+ if (point) {
1843
+ const material = point.material as MeshBasicMaterial
1844
+
1845
+ material.color.set(color)
1846
+ material.opacity = opacity
1847
+ material.needsUpdate = true
1848
+ }
1849
+ }
1850
+ const selectedBone = this.getSelectedBone()
1851
+
1852
+ this.traverseBones((bone) => {
1853
+ setColor(bone, selectedBone ? INACTIVE : DEFAULT)
1854
+ })
1855
+
1856
+ if (selectedBone) {
1857
+ let bone = selectedBone
1858
+
1859
+ setColor(bone, SELECTED, SELECTED_COLOR)
1860
+
1861
+ bone.traverseAncestors((ancestor) => {
1862
+ if (IsBone(ancestor.name)) {
1863
+ setColor(ancestor as Bone, ACTIVE)
1864
+ }
1865
+ })
1866
+
1867
+ for (;;) {
1868
+ const child = bone.children.filter((o) => o instanceof Bone)
1869
+ if (child.length !== 1) break
1870
+ setColor(child[0] as Bone, ACTIVE)
1871
+ bone = child[0] as Bone
1872
+ }
1873
+ }
1874
+ }
1875
+ }
sd-webui-3d-open-pose-editor/src/entry.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import { Main } from './environments/online/main'
2
+
3
+ Main()
sd-webui-3d-open-pose-editor/src/env.d.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ interface ImportMetaEnv {
2
+ readonly VITE_IS_ONLINE?: boolean
3
+ }
sd-webui-3d-open-pose-editor/src/environments/extension/@types/webui.d.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ declare const gradioApp: () => Document | ShadowRoot
2
+ declare const switch_to_txt2img: () => void
3
+ declare const switch_to_img2img: () => void
4
+ declare const onUiLoaded: (callback: () => void) => void
5
+ declare const onUiUpdate: (callback: () => void) => void
6
+ declare const onUiTabChange: (callback: () => void) => void
sd-webui-3d-open-pose-editor/src/environments/extension/@types/window.d.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface Window {
2
+ openpose3d?: {
3
+ sendTxt2img: (
4
+ pose_image: string | null,
5
+ pose_target: string,
6
+ depth_image: string | null,
7
+ depth_target: string,
8
+ normal_image: string | null,
9
+ normal_target: string,
10
+ canny_image: string | null,
11
+ canny_target: string
12
+ ) => void
13
+ sendImg2img: (
14
+ pose_image: string,
15
+ pose_target: string,
16
+ depth_image: string,
17
+ depth_target: string,
18
+ normal_image: string,
19
+ normal_target: string,
20
+ canny_image: string,
21
+ canny_target: string
22
+ ) => void
23
+ downloadImage: (image: string | null, name: string) => void
24
+ }
25
+ }
sd-webui-3d-open-pose-editor/src/environments/extension/assets.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const files: Record<string, string> = {
2
+ 'models/hand.fbx': '../models/hand.fbx',
3
+ 'models/foot.fbx': '../models/foot.fbx',
4
+ 'src/poses/data.bin': '../src/poses/data.bin',
5
+ }
6
+
7
+ try {
8
+ const params = new URLSearchParams(window.location.search)
9
+ const url = params.get('config')
10
+ if (url?.startsWith('/')) {
11
+ const response = await fetch(url)
12
+ const config = await response.json()
13
+ Object.assign(files, config['assets'])
14
+ }
15
+ } catch (error) {
16
+ console.error(error)
17
+ }
18
+
19
+ export default files
sd-webui-3d-open-pose-editor/src/environments/extension/entry.ts ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getCurrentTime } from '../../utils/time'
2
+ import { download } from '../../utils/transfer'
3
+ import {
4
+ openGradioAccordion,
5
+ switchGradioTab,
6
+ updateGradioImage,
7
+ waitForElementToBeInDocument,
8
+ } from './internal/gradio'
9
+ import {
10
+ AddMessageEventListener,
11
+ InitMessageListener,
12
+ InvokeCommand,
13
+ } from './internal/message'
14
+
15
+ const isTabActive = () => {
16
+ const tab = gradioApp().querySelector<HTMLElement>('#tab_threedopenpose')
17
+ return tab && tab.style.display != 'none'
18
+ }
19
+
20
+ let isInitialized = false
21
+ let isPaused = false
22
+
23
+ onUiLoaded(async () => {
24
+ console.log('sd-webui-3d-open-pose-editor: onUiLoaded')
25
+
26
+ const sendToControlNet = async (
27
+ container: Element,
28
+ poseImage: string | null,
29
+ poseTarget: string,
30
+ depthImage: string | null,
31
+ depthTarget: string,
32
+ normalImage: string | null,
33
+ normalTarget: string,
34
+ cannyImage: string | null,
35
+ cannyTarget: string
36
+ ) => {
37
+ let element: Element | null | undefined =
38
+ container.querySelector('#controlnet')
39
+ if (!element) {
40
+ for (const spans of container.querySelectorAll<HTMLSpanElement>(
41
+ '.cursor-pointer > span'
42
+ )) {
43
+ if (!spans.textContent?.includes('ControlNet')) {
44
+ continue
45
+ }
46
+ if (spans.textContent?.includes('M2M')) {
47
+ continue
48
+ }
49
+ element = spans.parentElement?.parentElement
50
+ }
51
+ if (!element) {
52
+ console.error('ControlNet element not found')
53
+ return
54
+ }
55
+ } else {
56
+ openGradioAccordion(element)
57
+ }
58
+ await waitForElementToBeInDocument(element, 'div[data-testid="image"]')
59
+ const imageElems = element.querySelectorAll('div[data-testid="image"]')
60
+ const tabsElem = element.querySelector('.tab-nav')
61
+ if (poseImage && poseTarget != '' && poseTarget != '-') {
62
+ const tabIndex = Number(poseTarget)
63
+ if (tabsElem) {
64
+ switchGradioTab(tabsElem, tabIndex)
65
+ }
66
+ await updateGradioImage(imageElems[tabIndex], poseImage, 'pose.png')
67
+ }
68
+ if (depthImage && depthTarget != '' && depthTarget != '-') {
69
+ const tabIndex = Number(depthTarget)
70
+ if (tabsElem) {
71
+ switchGradioTab(tabsElem, tabIndex)
72
+ }
73
+ await updateGradioImage(
74
+ imageElems[tabIndex],
75
+ depthImage,
76
+ 'depth.png'
77
+ )
78
+ }
79
+ if (normalImage && normalTarget != '' && normalTarget != '-') {
80
+ const tabIndex = Number(normalTarget)
81
+ if (tabsElem) {
82
+ switchGradioTab(tabsElem, tabIndex)
83
+ }
84
+ await updateGradioImage(
85
+ imageElems[tabIndex],
86
+ normalImage,
87
+ 'normal.png'
88
+ )
89
+ }
90
+ if (cannyImage && cannyTarget != '' && cannyTarget != '-') {
91
+ const tabIndex = Number(cannyTarget)
92
+ if (tabsElem) {
93
+ switchGradioTab(tabsElem, tabIndex)
94
+ }
95
+ await updateGradioImage(
96
+ imageElems[tabIndex],
97
+ cannyImage,
98
+ 'canny.png'
99
+ )
100
+ }
101
+ }
102
+
103
+ window.openpose3d = {
104
+ sendTxt2img: async (
105
+ poseImage: string | null,
106
+ poseTarget: string,
107
+ depthImage: string | null,
108
+ depthTarget: string,
109
+ normalImage: string | null,
110
+ normalTarget: string,
111
+ cannyImage: string | null,
112
+ cannyTarget: string
113
+ ) => {
114
+ const container = gradioApp().querySelector(
115
+ '#txt2img_script_container'
116
+ )!
117
+ switch_to_txt2img()
118
+ await sendToControlNet(
119
+ container,
120
+ poseImage,
121
+ poseTarget,
122
+ depthImage,
123
+ depthTarget,
124
+ normalImage,
125
+ normalTarget,
126
+ cannyImage,
127
+ cannyTarget
128
+ )
129
+ },
130
+ sendImg2img: async (
131
+ poseImage: string,
132
+ poseTarget: string,
133
+ depthImage: string,
134
+ depthTarget: string,
135
+ normalImage: string,
136
+ normalTarget: string,
137
+ cannyImage: string,
138
+ cannyTarget: string
139
+ ) => {
140
+ const container = gradioApp().querySelector(
141
+ '#img2img_script_container'
142
+ )!
143
+ switch_to_img2img()
144
+ await sendToControlNet(
145
+ container,
146
+ poseImage,
147
+ poseTarget,
148
+ depthImage,
149
+ depthTarget,
150
+ normalImage,
151
+ normalTarget,
152
+ cannyImage,
153
+ cannyTarget
154
+ )
155
+ },
156
+ downloadImage: (image: string | null, name: string) => {
157
+ if (!image) {
158
+ return
159
+ }
160
+ const fileName = name + '_' + getCurrentTime() + '.png'
161
+ download(image, fileName)
162
+ },
163
+ }
164
+
165
+ InitMessageListener()
166
+ AddMessageEventListener({
167
+ MakeImages: async (args: Record<string, string>) => {
168
+ for (const [name, url] of Object.entries(args)) {
169
+ const element = gradioApp().querySelector(
170
+ `#openpose3d_${name}_image`
171
+ )!
172
+ await updateGradioImage(element, url, name + '.png')
173
+ }
174
+ const tabs = gradioApp().querySelector('#openpose3d_main')!
175
+ switchGradioTab(tabs, 1)
176
+ },
177
+ })
178
+
179
+ for (let i = 0; i < 30; ++i) {
180
+ try {
181
+ await InvokeCommand('GetAppVersion')
182
+ isInitialized = true
183
+ break
184
+ } catch (error: any) {
185
+ if (error.status != 'Timeout') {
186
+ throw error
187
+ }
188
+ }
189
+ }
190
+ if (!isInitialized) {
191
+ console.error('sd-webui-3d-open-pose-editor: Timeout')
192
+ return
193
+ }
194
+
195
+ // await InvokeCommand('OutputWidth', 512)
196
+ // await InvokeCommand('OutputHeight', 512)
197
+ if (!isTabActive()) {
198
+ await InvokeCommand('Pause')
199
+ }
200
+ })
201
+
202
+ onUiUpdate(async () => {
203
+ if (!isInitialized) {
204
+ return
205
+ }
206
+ if (isTabActive()) {
207
+ if (isPaused) {
208
+ isPaused = false
209
+ await InvokeCommand('Resume')
210
+ }
211
+ } else {
212
+ if (!isPaused) {
213
+ isPaused = true
214
+ await InvokeCommand('Pause')
215
+ }
216
+ }
217
+ })
sd-webui-3d-open-pose-editor/src/environments/extension/internal/gradio.ts ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const waitForElement = async (
2
+ parent: Element,
3
+ selector: string,
4
+ exist: boolean
5
+ ) => {
6
+ return new Promise((resolve) => {
7
+ const observer = new MutationObserver(() => {
8
+ if (!!parent.querySelector(selector) != exist) {
9
+ return
10
+ }
11
+ observer.disconnect()
12
+ resolve(undefined)
13
+ })
14
+
15
+ observer.observe(parent, {
16
+ childList: true,
17
+ subtree: true,
18
+ })
19
+
20
+ if (!!parent.querySelector(selector) == exist) {
21
+ resolve(undefined)
22
+ }
23
+ })
24
+ }
25
+
26
+ const timeout = (ms: number) => {
27
+ return new Promise(function (resolve, reject) {
28
+ setTimeout(() => reject('Timeout'), ms)
29
+ })
30
+ }
31
+
32
+ export const waitForElementToBeInDocument = (
33
+ parent: Element,
34
+ selector: string
35
+ ) => Promise.race([waitForElement(parent, selector, true), timeout(10000)])
36
+ export const waitForElementToBeRemoved = (parent: Element, selector: string) =>
37
+ Promise.race([waitForElement(parent, selector, false), timeout(10000)])
38
+
39
+ export const updateGradioImage = async (
40
+ element: Element,
41
+ url: string,
42
+ name: string
43
+ ) => {
44
+ const blob = await (await fetch(url)).blob()
45
+ const file = new File([blob], name)
46
+ const dt = new DataTransfer()
47
+ dt.items.add(file)
48
+
49
+ element
50
+ .querySelector<HTMLButtonElement>("button[aria-label='Clear']")
51
+ ?.click()
52
+ await waitForElementToBeRemoved(element, "button[aria-label='Clear']")
53
+ const input = element.querySelector<HTMLInputElement>("input[type='file']")!
54
+ input.value = ''
55
+ input.files = dt.files
56
+ input.dispatchEvent(
57
+ new Event('change', {
58
+ bubbles: true,
59
+ composed: true,
60
+ })
61
+ )
62
+ await waitForElementToBeInDocument(element, "button[aria-label='Clear']")
63
+ }
64
+
65
+ export const switchGradioTab = (element: Element, index: number) => {
66
+ element.querySelectorAll('button')[index].click()
67
+ }
68
+
69
+ export const openGradioAccordion = (element: Element) => {
70
+ const labelElem = element.querySelector<HTMLElement>(':scope > .label-wrap')
71
+ if (!labelElem) {
72
+ return
73
+ }
74
+ if (labelElem.classList.contains('open')) {
75
+ return
76
+ }
77
+ labelElem.click()
78
+ }
sd-webui-3d-open-pose-editor/src/environments/extension/internal/message.ts ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IPostMessage as IPostMessageBase } from '../../../hooks/useMessageDispatch'
2
+
3
+ export type IPostMessage = IPostMessageBase & {
4
+ cmd: string
5
+ }
6
+
7
+ const poseMessage = (message: IPostMessage) => {
8
+ const iframe =
9
+ gradioApp().querySelector<HTMLIFrameElement>('#openpose3d_iframe')!
10
+ iframe.contentWindow!.postMessage(message, '*')
11
+ }
12
+
13
+ const MessageReturnHandler: Record<string, (arg: any) => void> = {}
14
+ const MessageEventHandler: Record<string, (arg: any) => void> = {}
15
+
16
+ export const InitMessageListener = () => {
17
+ window.addEventListener('message', (event: MessageEvent<IPostMessage>) => {
18
+ const { data } = event
19
+ if (data && data.cmd && data.cmd == 'openpose-3d' && data.method) {
20
+ const method = data.method
21
+ console.log('Method', method, event)
22
+ if (data.type == 'return') {
23
+ MessageReturnHandler[method]?.(data.payload)
24
+ } else if (data.type == 'event') {
25
+ console.log(MessageEventHandler)
26
+ MessageEventHandler[method]?.(data.payload)
27
+ }
28
+ }
29
+ })
30
+ }
31
+
32
+ export const AddMessageEventListener = (
33
+ listeners: Record<string, (arg: any) => void>
34
+ ) => {
35
+ Object.assign(MessageEventHandler, listeners)
36
+ }
37
+
38
+ export function InvokeCommand(method: string, ...args: any[]) {
39
+ return new Promise((resolve, reject) => {
40
+ const id = setTimeout(() => {
41
+ delete MessageReturnHandler[method]
42
+
43
+ reject({
44
+ method,
45
+ status: 'Timeout',
46
+ })
47
+ }, 1000)
48
+
49
+ const onReutrn = (arg: any) => {
50
+ clearTimeout(id)
51
+ resolve(arg)
52
+ }
53
+ MessageReturnHandler[method] = onReutrn
54
+
55
+ poseMessage({
56
+ cmd: 'openpose-3d',
57
+ method,
58
+ type: 'call',
59
+ payload: args,
60
+ })
61
+ })
62
+ }
sd-webui-3d-open-pose-editor/src/environments/online/App.module.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .app {
2
+ width: 100vw;
3
+ height: 100vh;
4
+ position: absolute;
5
+ top: 0px;
6
+ left: 0px;
7
+ }
8
+
9
+ .threejsCanvas {
10
+ display: block;
11
+ width: 100vw;
12
+ height: 100vh;
13
+ }
14
+
15
+ .gallery {
16
+ user-select: none;
17
+ height: 140px;
18
+ position: fixed;
19
+ bottom: 50px;
20
+ right: 50px;
21
+ display: flex;
22
+ justify-content: flex-end;
23
+ }
24
+
25
+ .gallery img {
26
+ width: 100px;
27
+ height: 140px;
28
+ object-fit: contain;
29
+ background-color: gray;
30
+ user-select: none;
31
+ margin-right: 2px;
32
+ margin-left: 2px;
33
+ }
34
+
35
+ .background {
36
+ width: 100vw;
37
+ height: 100vh;
38
+ background-color: rgb(214, 214, 214);
39
+ background-position: center;
40
+ background-repeat: no-repeat;
41
+ background-size: contain;
42
+ }
sd-webui-3d-open-pose-editor/src/environments/online/App.tsx ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
2
+ import { download } from '../../utils/transfer'
3
+ import classes from './App.module.css'
4
+ import Menu from '../../components/Menu'
5
+ import PopupOver from '../../components/PopupOver'
6
+ import { useBodyEditor } from '../../hooks'
7
+ import {
8
+ LockClosedIcon,
9
+ LockOpen2Icon,
10
+ ResetIcon,
11
+ ResumeIcon,
12
+ } from '@radix-ui/react-icons'
13
+ import { getCurrentTime } from '../../utils/time'
14
+ import useMessageDispatch from '../../hooks/useMessageDispatch'
15
+
16
+ const { app, threejsCanvas, gallery, background } = classes
17
+
18
+ const PreviewCanvas = React.forwardRef<
19
+ HTMLCanvasElement,
20
+ {
21
+ isLock: boolean
22
+ enable: boolean
23
+ onChange: (isLock: boolean) => void
24
+ onRestore: () => void
25
+ onRun: () => void
26
+ }
27
+ >(({ isLock, enable, onChange, onRestore, onRun }, ref) => {
28
+ const Icon = isLock ? LockClosedIcon : LockOpen2Icon
29
+ return (
30
+ <div
31
+ style={{
32
+ position: 'relative',
33
+ display: enable ? 'flex' : 'none',
34
+ justifyContent: 'center',
35
+ backgroundColor: 'gray',
36
+ }}
37
+ >
38
+ <canvas
39
+ ref={ref}
40
+ style={{
41
+ objectFit: 'contain',
42
+ width: 'unset',
43
+ // height:"unset",
44
+ maxHeight: '100%',
45
+ maxWidth: 300,
46
+ }}
47
+ ></canvas>
48
+ <ResumeIcon
49
+ style={{
50
+ position: 'absolute',
51
+ top: -10,
52
+ right: 5,
53
+ backgroundColor: 'white',
54
+ borderRadius: 10,
55
+ padding: 5,
56
+ }}
57
+ onClick={() => {
58
+ onRun()
59
+ }}
60
+ ></ResumeIcon>
61
+ <Icon
62
+ style={{
63
+ position: 'absolute',
64
+ top: 20,
65
+ right: 5,
66
+ backgroundColor: 'white',
67
+ borderRadius: 10,
68
+ padding: 5,
69
+ }}
70
+ onClick={() => {
71
+ onChange(!isLock)
72
+ }}
73
+ ></Icon>
74
+
75
+ <ResetIcon
76
+ style={{
77
+ position: 'absolute',
78
+ top: 50,
79
+ right: 5,
80
+ backgroundColor: !isLock ? 'gray' : 'white',
81
+ borderRadius: 10,
82
+ padding: 5,
83
+ }}
84
+ onClick={() => {
85
+ if (isLock) onRestore()
86
+ }}
87
+ ></ResetIcon>
88
+ </div>
89
+ )
90
+ })
91
+
92
+ function App() {
93
+ const canvasRef = useRef(null)
94
+ const previewCanvasRef = useRef(null)
95
+
96
+ const backgroundRef = useRef<HTMLDivElement>(null)
97
+ const editor = useBodyEditor(canvasRef, previewCanvasRef, backgroundRef)
98
+ const [imageData, setImageData] = useState<
99
+ Record<string, { title: string; src: string }>
100
+ >(() => ({
101
+ pose: {
102
+ title: '',
103
+ src: '',
104
+ },
105
+ depth: {
106
+ title: '',
107
+ src: '',
108
+ },
109
+ normal: {
110
+ title: '',
111
+ src: '',
112
+ },
113
+ canny: {
114
+ title: '',
115
+ src: '',
116
+ },
117
+ }))
118
+
119
+ const onChangeBackground = useCallback((url: string) => {
120
+ const div = backgroundRef.current
121
+ if (div) {
122
+ div.style.backgroundImage = url ? `url(${url})` : 'none'
123
+ }
124
+ }, [])
125
+
126
+ const onScreenShot = useCallback(
127
+ (data: Record<string, { src: string; title: string }>) => {
128
+ setImageData(data)
129
+ },
130
+ []
131
+ )
132
+
133
+ const [preview, setPreivew] = useState(false)
134
+ const [lockView, setLockView] = useState(false)
135
+
136
+ useEffect(() => {
137
+ const preview = (enable: boolean) => {
138
+ setPreivew(enable)
139
+ }
140
+
141
+ const lcokView = (value: boolean) => {
142
+ setLockView(value)
143
+ }
144
+ editor?.PreviewEventManager.AddEventListener(preview)
145
+ editor?.LockViewEventManager.AddEventListener(lcokView)
146
+
147
+ return () => {
148
+ editor?.PreviewEventManager.RemoveEventListener(preview)
149
+ editor?.LockViewEventManager.RemoveEventListener(lcokView)
150
+ }
151
+ }, [editor])
152
+
153
+ useMessageDispatch({
154
+ GetAppVersion: () => __APP_VERSION__,
155
+ MakeImages: () => editor?.MakeImages(),
156
+ Pause: () => editor?.pause(),
157
+ Resume: () => editor?.resume(),
158
+ OutputWidth: (value: number) => {
159
+ if (editor && typeof value === 'number') {
160
+ editor.OutputWidth = value
161
+ return true
162
+ } else return false
163
+ },
164
+ OutputHeight: (value: number) => {
165
+ if (editor && typeof value === 'number') {
166
+ editor.OutputHeight = value
167
+ return true
168
+ } else return false
169
+ },
170
+ OnlyHand(value: boolean) {
171
+ if (editor && typeof value === 'boolean') {
172
+ editor.OnlyHand = value
173
+ return true
174
+ } else return false
175
+ },
176
+ MoveMode(value: boolean) {
177
+ if (editor && typeof value === 'boolean') {
178
+ editor.MoveMode = value
179
+ return true
180
+ } else return false
181
+ },
182
+ GetWidth: () => editor?.Width,
183
+ GetHeight: () => editor?.Height,
184
+ GetSceneData: () => editor?.GetSceneData(),
185
+ LockView: () => editor?.LockView(),
186
+ UnlockView: () => editor?.UnlockView(),
187
+ RestoreView: () => editor?.RestoreView(),
188
+ })
189
+
190
+ return (
191
+ <div ref={backgroundRef} className={background}>
192
+ <canvas
193
+ className={threejsCanvas}
194
+ tabIndex={-1}
195
+ ref={canvasRef}
196
+ onContextMenu={(e) => {
197
+ e.preventDefault()
198
+ }}
199
+ ></canvas>
200
+ <div className={gallery}>
201
+ <PreviewCanvas
202
+ enable={preview}
203
+ ref={previewCanvasRef}
204
+ isLock={lockView}
205
+ onChange={(isLock) => {
206
+ if (isLock) {
207
+ editor?.LockView()
208
+ } else {
209
+ editor?.UnlockView()
210
+ }
211
+ }}
212
+ onRestore={() => {
213
+ editor?.RestoreView()
214
+ }}
215
+ onRun={async () => {
216
+ if (!editor) return
217
+ const image = editor.MakeImages()
218
+ const result = Object.fromEntries(
219
+ Object.entries(image).map(([name, imgData]) => [
220
+ name,
221
+ {
222
+ src: imgData,
223
+ title: name + '_' + getCurrentTime(),
224
+ },
225
+ ])
226
+ )
227
+ onScreenShot(result)
228
+ }}
229
+ ></PreviewCanvas>
230
+
231
+ {Object.entries(imageData).map(([name, { src, title }]) => (
232
+ <img
233
+ key={name}
234
+ // avoid show error image
235
+ {...(src ? { src } : {})}
236
+ title={title}
237
+ onClick={(e) => {
238
+ const image = e.target as HTMLImageElement
239
+ const title = image?.getAttribute('title') ?? ''
240
+ const url = image?.getAttribute('src') ?? ''
241
+ download(url, title)
242
+ }}
243
+ ></img>
244
+ ))}
245
+ </div>
246
+ <div
247
+ className={app}
248
+ style={{
249
+ pointerEvents: 'none',
250
+ }}
251
+ >
252
+ <div
253
+ style={{
254
+ pointerEvents: 'initial',
255
+ marginTop: 10,
256
+ display: 'flex',
257
+ justifyContent: 'center',
258
+ }}
259
+ >
260
+ {editor ? (
261
+ <Menu
262
+ editor={editor}
263
+ onChangeBackground={onChangeBackground}
264
+ onScreenShot={onScreenShot}
265
+ />
266
+ ) : undefined}
267
+ </div>
268
+ </div>
269
+ {editor ? (
270
+ <PopupOver
271
+ editor={editor}
272
+ style={{
273
+ pointerEvents: 'initial',
274
+ position: 'fixed',
275
+ top: 10,
276
+ right: 10,
277
+ }}
278
+ ></PopupOver>
279
+ ) : undefined}
280
+ </div>
281
+ )
282
+ }
283
+
284
+ export default App
sd-webui-3d-open-pose-editor/src/environments/online/helper.ts ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getImage } from '../../utils/image'
2
+ import {
3
+ CopyTextToClipboard,
4
+ uploadImage,
5
+ uploadJson,
6
+ } from '../../utils/transfer'
7
+ import { DetectPosefromImage } from '../../utils/detect'
8
+
9
+ import { BodyControlor } from '../../body'
10
+
11
+ import { GetLoading } from '../../components/Loading'
12
+ import { BodyEditor } from '../../editor'
13
+ import i18n from '../../i18n'
14
+ import { Oops } from '../../components/Oops'
15
+ import assets from '../../assets'
16
+ import { ShowToast } from '../../components/Toast'
17
+ import { GetRandomPose, LoadPosesLibrary } from '../../pose-library'
18
+
19
+ export class Helper {
20
+ editor: BodyEditor
21
+ constructor(editor: BodyEditor) {
22
+ this.editor = editor
23
+ }
24
+
25
+ async DetectFromImage(onChangeBackground: (url: string) => void) {
26
+ const body = await this.editor.GetBodyToSetPose()
27
+ if (!body) {
28
+ ShowToast({ title: i18n.t('Please select a skeleton!!') })
29
+ return
30
+ }
31
+
32
+ const loading = GetLoading(500)
33
+
34
+ try {
35
+ const dataUrl = await uploadImage()
36
+
37
+ if (!dataUrl) return
38
+
39
+ const image = await getImage(dataUrl)
40
+ onChangeBackground(dataUrl)
41
+
42
+ loading.show({ title: i18n.t('Downloading MediaPipe Pose Model') })
43
+ const result = await DetectPosefromImage(image)
44
+ loading.hide()
45
+
46
+ if (result) {
47
+ if (!result.poseWorldLandmarks)
48
+ throw new Error(JSON.stringify(result))
49
+
50
+ const positions: [number, number, number][] =
51
+ result.poseWorldLandmarks.map(({ x, y, z }) => [
52
+ x * 100,
53
+ -y * 100,
54
+ -z * 100,
55
+ ])
56
+
57
+ // this.drawPoseData(
58
+ // result.poseWorldLandmarks.map(({ x, y, z }) =>
59
+ // new THREE.Vector3().fromArray([x * 100, -y * 100, -z * 100])
60
+ // )
61
+ // )
62
+
63
+ await this.editor.SetBlazePose(positions)
64
+ return
65
+ }
66
+ } catch (error) {
67
+ loading.hide()
68
+
69
+ Oops(
70
+ i18n.t(
71
+ 'If you try to detect anime characters, you may get an error. Please try again with photos.'
72
+ ) +
73
+ '\n' +
74
+ error
75
+ )
76
+ console.error(error)
77
+ return null
78
+ }
79
+ }
80
+
81
+ async CopyKeypointToClipboard() {
82
+ const body = await this.editor.GetBodyToSetPose()
83
+ if (!body) {
84
+ ShowToast({ title: i18n.t('Please select a skeleton!!') })
85
+ return
86
+ }
87
+ try {
88
+ const data = new BodyControlor(body).Get18keyPointsData()
89
+ await CopyTextToClipboard(JSON.stringify(data, null, 4))
90
+ ShowToast({ title: i18n.t('Copied to Clipboard') })
91
+ } catch (error) {
92
+ Oops(error)
93
+ console.error(error)
94
+ return null
95
+ }
96
+ }
97
+
98
+ async SaveGesture() {
99
+ const hand = await this.editor.getSelectedHand()
100
+ if (!hand) {
101
+ ShowToast({ title: i18n.t('Please select a hand!!') })
102
+ return
103
+ }
104
+ try {
105
+ this.editor.SaveGesture()
106
+ } catch (error) {
107
+ Oops(error)
108
+ console.error(error)
109
+ return null
110
+ }
111
+ }
112
+
113
+ async LoadGesture() {
114
+ const hand = await this.editor.getSelectedHand()
115
+
116
+ if (!hand) {
117
+ ShowToast({ title: i18n.t('Please select a hand!!') })
118
+ return
119
+ }
120
+
121
+ const rawData = await uploadJson()
122
+ if (!rawData) return
123
+
124
+ try {
125
+ this.editor.RestoreGesture(rawData)
126
+ } catch (error) {
127
+ Oops(error)
128
+ console.error(error)
129
+ return null
130
+ }
131
+ }
132
+
133
+ async GenerateSceneURL() {
134
+ try {
135
+ const d = encodeURIComponent(
136
+ JSON.stringify(this.editor.GetSceneData())
137
+ )
138
+ const url_base = location.href.replace(/#$/, '')
139
+ const url = `${url_base}#${d}`
140
+ await CopyTextToClipboard(url)
141
+ ShowToast({ title: i18n.t('Copied to Clipboard') })
142
+ } catch (error) {
143
+ Oops(error)
144
+ console.error(error)
145
+ }
146
+ }
147
+
148
+ async SetRandomPose() {
149
+ const body = await this.editor.GetBodyToSetPose()
150
+ if (!body) {
151
+ ShowToast({ title: i18n.t('Please select a skeleton!!') })
152
+ return
153
+ }
154
+
155
+ const loading = GetLoading(500)
156
+
157
+ try {
158
+ let poseData = GetRandomPose()
159
+ if (poseData) {
160
+ await this.editor.SetPose(poseData)
161
+ return
162
+ }
163
+
164
+ loading.show({ title: i18n.t('Downloading Poses Library') })
165
+
166
+ await LoadPosesLibrary(assets['src/poses/data.bin'])
167
+ loading.hide()
168
+
169
+ poseData = GetRandomPose()
170
+ if (poseData) {
171
+ await this.editor.SetPose(poseData)
172
+ return
173
+ }
174
+ } catch (error) {
175
+ loading.hide()
176
+
177
+ Oops(error)
178
+ console.error(error)
179
+ return
180
+ }
181
+ }
182
+ async CopySkeleton() {
183
+ const body = this.editor.getSelectedBody()
184
+ if (!body) {
185
+ ShowToast({ title: i18n.t('Please select a skeleton!!') })
186
+ return
187
+ }
188
+
189
+ this.editor.CopySelectedBody()
190
+ }
191
+ async RemoveSkeleton() {
192
+ const body = this.editor.getSelectedBody()
193
+ if (!body) {
194
+ ShowToast({ title: i18n.t('Please select a skeleton!!') })
195
+ return
196
+ }
197
+
198
+ this.editor.RemoveBody()
199
+ }
200
+ FeedbackByQQ() {
201
+ window.open('https://jq.qq.com/?_wv=1027&k=N6j4nigd')
202
+ }
203
+ FeedbackByGithub() {
204
+ window.open(
205
+ 'https://github.com/nonnonstop/sd-webui-3d-open-pose-editor/issues/new/choose'
206
+ )
207
+ }
208
+ }
sd-webui-3d-open-pose-editor/src/environments/online/index.css ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --openpose-editor-font-family: -apple-system, BlinkMacSystemFont, Tahoma,
3
+ Arial, 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
4
+ }
5
+
6
+ * {
7
+ margin: 0;
8
+ padding: 0;
9
+ outline: none;
10
+ }
11
+
12
+ body,
13
+ #root {
14
+ width: 100vw;
15
+ height: 100vh;
16
+ position: relative;
17
+ }
18
+
19
+ #root * {
20
+ font-family: var(--openpose-editor-font-family);
21
+ }
sd-webui-3d-open-pose-editor/src/environments/online/init.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import dayjs from 'dayjs'
2
+ console.log(
3
+ __APP_VERSION__ +
4
+ ' ' +
5
+ dayjs(__APP_BUILD_TIME__).format('YYYY-MM-DD HH:mm:ss')
6
+ )
7
+ import { PWACheck } from './update'
8
+ PWACheck()
sd-webui-3d-open-pose-editor/src/environments/online/main.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import './init'
2
+ import './index.css'
3
+
4
+ import React from 'react'
5
+ import ReactDOM from 'react-dom/client'
6
+ import NiceModal from '@ebay/nice-modal-react'
7
+
8
+ import App from './App'
9
+
10
+ export function Main() {
11
+ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
12
+ <React.StrictMode>
13
+ <NiceModal.Provider>
14
+ <App />
15
+ </NiceModal.Provider>
16
+ </React.StrictMode>
17
+ )
18
+ }
sd-webui-3d-open-pose-editor/src/environments/online/update.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="vite-plugin-pwa/client" />
2
+
3
+ // #v-ifdef VITE_IS_ONLINE
4
+ import { registerSW } from 'virtual:pwa-register'
5
+ // #v-endif
6
+
7
+ import { ShowDialog } from '../../components/Dialog'
8
+ import i18n from '../../i18n'
9
+
10
+ async function PWAPopup(update: (reloadPage?: boolean) => Promise<void>) {
11
+ const result = await ShowDialog({
12
+ title: i18n.t('Updates are available, please confirm!!') ?? '',
13
+ button: i18n.t('Update') ?? '',
14
+ })
15
+
16
+ if (result === 'action') {
17
+ update(true)
18
+ }
19
+ }
20
+ export function PWACheck() {
21
+ if (import.meta.env.MODE !== 'online') return
22
+ const updateSW = registerSW({
23
+ onNeedRefresh() {
24
+ console.log('有更新,需要刷新!!')
25
+ PWAPopup(updateSW)
26
+ },
27
+ onOfflineReady() {
28
+ console.log('已经入离线模式!!')
29
+ },
30
+ })
31
+ }
sd-webui-3d-open-pose-editor/src/hooks/index.ts ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RefObject, useEffect, useState } from 'react'
2
+ import assets from '../assets'
3
+ import { CreateTemplateBody } from '../body'
4
+ import { GetLoading } from '../components/Loading'
5
+ import { BodyEditor } from '../editor'
6
+ import i18n, { LanguageMapping } from '../i18n'
7
+ import { LoadFoot, LoadHand } from '../models'
8
+
9
+ export async function LoadBodyData() {
10
+ const loading = GetLoading(500)
11
+ loading.show({ title: i18n.t('Downloading Hand Model') })
12
+ await LoadHand(assets['models/hand.fbx'])
13
+ loading.show({ title: i18n.t('Downloading Foot Model') })
14
+ await LoadFoot(assets['models/foot.fbx'])
15
+ loading.hide()
16
+ CreateTemplateBody()
17
+ }
18
+
19
+ export function useBodyEditor(
20
+ canvasRef: RefObject<HTMLCanvasElement>,
21
+ previewCanvasRef: RefObject<HTMLCanvasElement>,
22
+ parent?: RefObject<HTMLDivElement>
23
+ ) {
24
+ const [editor, setEditor] = useState<BodyEditor>()
25
+
26
+ useEffect(() => {
27
+ const canvas = canvasRef.current
28
+ if (!canvas) return
29
+ const previewCanvas = previewCanvasRef.current
30
+ if (!previewCanvas) return
31
+ console.warn('create editor')
32
+
33
+ let editor: BodyEditor | null = new BodyEditor({
34
+ canvas,
35
+ previewCanvas,
36
+ parentElem: parent?.current ?? (document as any),
37
+ statsElem: import.meta.env.DEV ? document.body : undefined,
38
+ })
39
+
40
+ setEditor(editor)
41
+
42
+ const init = async () => {
43
+ // StrictMode will render twice
44
+ // we have to check if the editor is null to avoid meaningless operations
45
+ if (editor) {
46
+ await LoadBodyData()
47
+ editor?.ResetScene()
48
+ if (editor?.RestoreScene && location.hash) {
49
+ const rawData = decodeURIComponent(
50
+ location.hash.replace(/^#/, '')
51
+ )
52
+ editor?.RestoreScene(rawData)
53
+ location.hash = ''
54
+ }
55
+ }
56
+ }
57
+ init()
58
+
59
+ return () => {
60
+ console.warn('disponse')
61
+ editor?.disponse()
62
+ editor = null
63
+ }
64
+ }, [])
65
+
66
+ return editor
67
+ }
68
+
69
+ export function useLanguageSelect() {
70
+ const [current] = useState(
71
+ () => LanguageMapping[i18n.language] ?? 'English'
72
+ )
73
+ const [R_LanguageMapping] = useState(() =>
74
+ Object.fromEntries(
75
+ Object.entries(LanguageMapping).map(([k, v]) => [v, k])
76
+ )
77
+ )
78
+
79
+ return {
80
+ current,
81
+ languagList: [...Object.values(LanguageMapping)],
82
+ changeLanguage: (value: string) => {
83
+ const lng = R_LanguageMapping[value]
84
+ const url = new URL(window.location.href)
85
+
86
+ url.searchParams.set('lng', lng)
87
+ window.location.assign(url)
88
+ },
89
+ }
90
+ }
sd-webui-3d-open-pose-editor/src/hooks/useEventCall.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // https://github.com/Volune/use-event-callback/blob/master/src/index.ts
2
+
3
+ import { useLayoutEffect, useMemo, useRef } from 'react'
4
+
5
+ type Fn<ARGS extends any[], R> = (...args: ARGS) => R
6
+
7
+ const useEventCallback = <A extends any[], R>(fn: Fn<A, R>): Fn<A, R> => {
8
+ const ref = useRef<Fn<A, R>>(fn)
9
+ useLayoutEffect(() => {
10
+ ref.current = fn
11
+ })
12
+ return useMemo(
13
+ () =>
14
+ (...args: A): R => {
15
+ const { current } = ref
16
+ return current(...args)
17
+ },
18
+ []
19
+ )
20
+ }
21
+
22
+ export default useEventCallback
sd-webui-3d-open-pose-editor/src/hooks/useFoceUpdate.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useState } from 'react'
2
+
3
+ const createNewObject = (): Record<string, never> => ({})
4
+
5
+ export default function useForceUpdate(): VoidFunction {
6
+ const [, setValue] = useState<Record<string, never>>(createNewObject)
7
+
8
+ return useCallback((): void => {
9
+ setValue(createNewObject())
10
+ }, [])
11
+ }
sd-webui-3d-open-pose-editor/src/hooks/useMessageDispatch.ts ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // https://github.com/rottitime/react-hook-window-message-event/blob/main/src/useMessage.ts
2
+
3
+ import { useEffect, useRef } from 'react'
4
+ import useEventCallback from './useEventCall'
5
+
6
+ let SEND_TO_SENDER = false // Avoid duplicate event
7
+
8
+ export type IPostMessage = {
9
+ method: string
10
+ type: 'call' | 'return' | 'event'
11
+ payload: any
12
+ }
13
+
14
+ export type EventHandler = (
15
+ callback: (data: IPostMessage) => unknown,
16
+ payload: IPostMessage['payload']
17
+ ) => void
18
+
19
+ const postMessage = (
20
+ data: IPostMessage & {
21
+ cmd?: string // Just avoid webui exception
22
+ },
23
+ target: MessageEvent['source'],
24
+ origin = '*'
25
+ ) => {
26
+ console.log('return', { target, origin, data })
27
+ target?.postMessage(
28
+ { cmd: 'openpose-3d', ...data },
29
+ { targetOrigin: origin }
30
+ )
31
+ }
32
+
33
+ export const sendToParent = (data: IPostMessage) => {
34
+ if (SEND_TO_SENDER) return
35
+ const { parent } = window
36
+ if (!parent) throw new Error('Parent window has closed')
37
+ postMessage(data, parent)
38
+ }
39
+
40
+ export const sendToParentDirectly = (data: IPostMessage) => {
41
+ const { parent } = window
42
+ if (!parent) throw new Error('Parent window has closed')
43
+ postMessage(data, parent)
44
+ }
45
+
46
+ export const sendToOpener = (data: IPostMessage) => {
47
+ const { opener } = window
48
+ if (!opener) throw new Error('Opener window has closed')
49
+ postMessage(data, opener)
50
+ }
51
+
52
+ export const sendToAll = (data: IPostMessage) => {
53
+ if (SEND_TO_SENDER) return
54
+ const { opener, parent } = window
55
+ if (!opener && !parent) {
56
+ throw new Error('window has closed')
57
+ }
58
+ if (parent) sendToParent(data)
59
+ if (opener) sendToOpener(data)
60
+ }
61
+
62
+ export default function useMessageDispatch(
63
+ dispatch: Record<string, (...args: any[]) => any>
64
+ ) {
65
+ const originRef = useRef<string>()
66
+ const sourceRef = useRef<MessageEvent['source']>(null)
67
+
68
+ originRef.current = ''
69
+ sourceRef.current = null as MessageEvent['source']
70
+
71
+ const sendToSender = (data: IPostMessage) =>
72
+ postMessage(data, sourceRef.current, originRef.current)
73
+
74
+ const onWatchEventHandler = useEventCallback(
75
+ // tslint:disable-next-line: no-shadowed-variable
76
+ async ({ origin, source, data }: MessageEvent) => {
77
+ if (!data) return
78
+
79
+ const { method, payload, type } = data as IPostMessage
80
+ // It is invalid message, not from webui extension.
81
+ if (type != 'call') return
82
+
83
+ console.log('method', method, payload)
84
+
85
+ if (payload && Array.isArray(payload) === false) {
86
+ console.error('payload is not array')
87
+ return
88
+ }
89
+
90
+ sourceRef.current = source
91
+ originRef.current = origin
92
+
93
+ if (method in dispatch) {
94
+ const eventHandler = dispatch[method]
95
+ if (typeof eventHandler === 'function') {
96
+ SEND_TO_SENDER = true
97
+ const ret = eventHandler(...(payload ?? []))
98
+ const value = ret instanceof Promise ? await ret : ret
99
+ try {
100
+ sendToSender({
101
+ method,
102
+ type: 'return',
103
+ payload: value,
104
+ })
105
+ } catch (error) {
106
+ console.log(error)
107
+ }
108
+ SEND_TO_SENDER = false
109
+ }
110
+ } else if (method === 'GetAPIs') {
111
+ sendToSender({
112
+ method,
113
+ type: 'return',
114
+ payload: Object.keys(dispatch),
115
+ })
116
+ }
117
+ }
118
+ )
119
+
120
+ useEffect(() => {
121
+ window.addEventListener('message', onWatchEventHandler)
122
+ return () => window.removeEventListener('message', onWatchEventHandler)
123
+ }, [onWatchEventHandler])
124
+ }