fb1ffd4d2aea1b996230b8c8528d58e19308ce05ffb2f5c4d89d42fa73455684
Browse files- sd-webui-3d-open-pose-editor/public/icons/icon-144x144.png +0 -0
- sd-webui-3d-open-pose-editor/public/icons/icon-152x152.png +0 -0
- sd-webui-3d-open-pose-editor/public/icons/icon-192x192.png +0 -0
- sd-webui-3d-open-pose-editor/public/icons/icon-384x384.png +0 -0
- sd-webui-3d-open-pose-editor/public/icons/icon-48x48.png +0 -0
- sd-webui-3d-open-pose-editor/public/icons/icon-512x512.png +0 -0
- sd-webui-3d-open-pose-editor/public/icons/icon-72x72.png +0 -0
- sd-webui-3d-open-pose-editor/public/icons/icon-96x96.png +0 -0
- sd-webui-3d-open-pose-editor/public/mstile-150x150.png +0 -0
- sd-webui-3d-open-pose-editor/public/safari-pinned-tab.svg +61 -0
- sd-webui-3d-open-pose-editor/scripts/__pycache__/openpose_editor.cpython-310.pyc +0 -0
- sd-webui-3d-open-pose-editor/scripts/openpose_editor.py +276 -0
- sd-webui-3d-open-pose-editor/src/assets.ts +9 -0
- sd-webui-3d-open-pose-editor/src/body.ts +1293 -0
- sd-webui-3d-open-pose-editor/src/components/ContextMenu/index.tsx +145 -0
- sd-webui-3d-open-pose-editor/src/components/ContextMenu/styles.module.css +132 -0
- sd-webui-3d-open-pose-editor/src/components/Dialog/index.tsx +103 -0
- sd-webui-3d-open-pose-editor/src/components/Dialog/styles.module.css +124 -0
- sd-webui-3d-open-pose-editor/src/components/Loading/index.tsx +83 -0
- sd-webui-3d-open-pose-editor/src/components/Loading/styles.module.css +147 -0
- sd-webui-3d-open-pose-editor/src/components/Menu/index.tsx +416 -0
- sd-webui-3d-open-pose-editor/src/components/Menu/styles.module.css +131 -0
- sd-webui-3d-open-pose-editor/src/components/Oops.tsx +19 -0
- sd-webui-3d-open-pose-editor/src/components/PopupOver/index.tsx +348 -0
- sd-webui-3d-open-pose-editor/src/components/PopupOver/styles.module.css +115 -0
- sd-webui-3d-open-pose-editor/src/components/Slider/index.tsx +38 -0
- sd-webui-3d-open-pose-editor/src/components/Slider/styles.module.css +26 -0
- sd-webui-3d-open-pose-editor/src/components/Toast/index.tsx +91 -0
- sd-webui-3d-open-pose-editor/src/components/Toast/styles.module.css +142 -0
- sd-webui-3d-open-pose-editor/src/defines.ts +209 -0
- sd-webui-3d-open-pose-editor/src/editor.ts +1875 -0
- sd-webui-3d-open-pose-editor/src/entry.ts +3 -0
- sd-webui-3d-open-pose-editor/src/env.d.ts +3 -0
- sd-webui-3d-open-pose-editor/src/environments/extension/@types/webui.d.ts +6 -0
- sd-webui-3d-open-pose-editor/src/environments/extension/@types/window.d.ts +25 -0
- sd-webui-3d-open-pose-editor/src/environments/extension/assets.ts +19 -0
- sd-webui-3d-open-pose-editor/src/environments/extension/entry.ts +217 -0
- sd-webui-3d-open-pose-editor/src/environments/extension/internal/gradio.ts +78 -0
- sd-webui-3d-open-pose-editor/src/environments/extension/internal/message.ts +62 -0
- sd-webui-3d-open-pose-editor/src/environments/online/App.module.css +42 -0
- sd-webui-3d-open-pose-editor/src/environments/online/App.tsx +284 -0
- sd-webui-3d-open-pose-editor/src/environments/online/helper.ts +208 -0
- sd-webui-3d-open-pose-editor/src/environments/online/index.css +21 -0
- sd-webui-3d-open-pose-editor/src/environments/online/init.ts +8 -0
- sd-webui-3d-open-pose-editor/src/environments/online/main.tsx +18 -0
- sd-webui-3d-open-pose-editor/src/environments/online/update.ts +31 -0
- sd-webui-3d-open-pose-editor/src/hooks/index.ts +90 -0
- sd-webui-3d-open-pose-editor/src/hooks/useEventCall.ts +22 -0
- sd-webui-3d-open-pose-editor/src/hooks/useFoceUpdate.ts +11 -0
- 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 |
+
}
|