@chocodrop/mcp 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/bin/chocodrop-mcp.js +13 -0
- package/package.json +42 -0
- package/runtime/.chocodrop-mcp-runtime +1 -0
- package/runtime/asset-bridge.js +460 -0
- package/runtime/mcp-server.js +107 -0
- package/runtime/site/examples/basic/README.md +38 -0
- package/runtime/site/examples/basic/index.html +1730 -0
- package/runtime/site/examples/bookmarklet-v2.html +141 -0
- package/runtime/site/examples/bookmarklet.html +126 -0
- package/runtime/site/examples/lofi-room/index.html +2570 -0
- package/runtime/site/examples/lofi-room/sandbox-lite.html +124 -0
- package/runtime/site/examples/music-garden/index.html +2150 -0
- package/runtime/site/examples/pixel-ocean/index.html +1780 -0
- package/runtime/site/examples/react-three-fiber/AdvancedExample.jsx +338 -0
- package/runtime/site/examples/react-three-fiber/App.jsx +97 -0
- package/runtime/site/examples/react-three-fiber/README.md +294 -0
- package/runtime/site/examples/react-three-fiber/package.json +25 -0
- package/runtime/site/examples/sdk-test/README.md +70 -0
- package/runtime/site/examples/space/index.html +1975 -0
- package/runtime/site/examples/toy-city/index.html +2288 -0
- package/runtime/site/examples/wabi-sabi/index.html +2515 -0
- package/runtime/site/getting-started.html +84 -0
- package/runtime/site/index.html +1370 -0
- package/runtime/site/public/CommandUI.js +6341 -0
- package/runtime/site/public/LiveCommandClient.js +432 -0
- package/runtime/site/public/SceneManager.js +5493 -0
- package/runtime/site/public/bookmarklet.js +206 -0
- package/runtime/site/public/bootstrap.js +50 -0
- package/runtime/site/public/chocodrop-demo.umd.js +24020 -0
- package/runtime/site/public/chocodrop-demo.umd.min.js +24020 -0
- package/runtime/site/public/chocodrop.esm.js +2 -0
- package/runtime/site/public/html-sandbox/frame.js +865 -0
- package/runtime/site/public/icons/icon-192.png +0 -0
- package/runtime/site/public/icons/icon-512.png +0 -0
- package/runtime/site/public/icons/icon-maskable-512.png +0 -0
- package/runtime/site/public/immersive-launcher.js +175 -0
- package/runtime/site/public/immersive.html +165 -0
- package/runtime/site/public/index.html +1011 -0
- package/runtime/site/public/index.js +9 -0
- package/runtime/site/public/load-chocodrop.js +25 -0
- package/runtime/site/public/load-three.js +26 -0
- package/runtime/site/public/manifest.webmanifest +59 -0
- package/runtime/site/public/pwa-bootstrap.js +128 -0
- package/runtime/site/public/sample-card.svg +15 -0
- package/runtime/site/public/service-worker.js +117 -0
- package/runtime/site/public/translation-dictionary.js +257 -0
- package/runtime/site/public/xr/xr-ui.css +338 -0
- package/runtime/site/public/xr-viewer.html +263 -0
- package/runtime/site/src/client/local-bridge.js +83 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
font-family: "Inter", "SF Pro Display", "Noto Sans JP", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
4
|
+
--panel-bg: rgba(15, 23, 42, 0.72);
|
|
5
|
+
--panel-border: rgba(148, 163, 184, 0.3);
|
|
6
|
+
--pill-bg: rgba(37, 99, 235, 0.2);
|
|
7
|
+
--pill-border: rgba(37, 99, 235, 0.4);
|
|
8
|
+
--gradient-start: #38bdf8;
|
|
9
|
+
--gradient-end: #6366f1;
|
|
10
|
+
--success: #4ade80;
|
|
11
|
+
--error: #f87171;
|
|
12
|
+
--pending: #facc15;
|
|
13
|
+
--warn: #f97316;
|
|
14
|
+
--text-muted: #94a3b8;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
* {
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
body {
|
|
22
|
+
margin: 0;
|
|
23
|
+
min-height: 100vh;
|
|
24
|
+
background: radial-gradient(circle at 20% 30%, rgba(139, 92, 246, 0.15), transparent 40%),
|
|
25
|
+
radial-gradient(circle at 80% 70%, rgba(56, 189, 248, 0.12), transparent 50%),
|
|
26
|
+
#020817;
|
|
27
|
+
color: #e2e8f0;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
canvas {
|
|
32
|
+
width: 100vw;
|
|
33
|
+
height: 100vh;
|
|
34
|
+
display: block;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[data-overlay] {
|
|
38
|
+
position: fixed;
|
|
39
|
+
inset: clamp(1rem, 2vw, 2rem);
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
gap: 0.75rem;
|
|
43
|
+
pointer-events: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.panel {
|
|
47
|
+
display: inline-flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 0.75rem;
|
|
50
|
+
background: linear-gradient(135deg,
|
|
51
|
+
rgba(15, 23, 42, 0.85) 0%,
|
|
52
|
+
rgba(30, 41, 59, 0.75) 100%);
|
|
53
|
+
backdrop-filter: blur(20px) saturate(150%);
|
|
54
|
+
-webkit-backdrop-filter: blur(20px) saturate(150%);
|
|
55
|
+
border: 1px solid var(--panel-border);
|
|
56
|
+
border-radius: 999px;
|
|
57
|
+
padding: 0.75rem 1.25rem;
|
|
58
|
+
color: #e2e8f0;
|
|
59
|
+
pointer-events: auto;
|
|
60
|
+
flex-wrap: wrap;
|
|
61
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3),
|
|
62
|
+
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
|
|
63
|
+
animation: panel-fade 320ms ease;
|
|
64
|
+
transition: all 0.3s ease;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.panel:hover {
|
|
68
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4),
|
|
69
|
+
0 0 0 1px rgba(255, 255, 255, 0.08) inset;
|
|
70
|
+
transform: translateY(-2px);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@keyframes panel-fade {
|
|
74
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
75
|
+
to { opacity: 1; transform: translateY(0); }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.pill {
|
|
79
|
+
font-size: 0.85rem;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
padding: 0.35rem 0.75rem;
|
|
82
|
+
border-radius: 999px;
|
|
83
|
+
background: linear-gradient(135deg,
|
|
84
|
+
rgba(56, 189, 248, 0.9) 0%,
|
|
85
|
+
rgba(59, 130, 246, 0.7) 100%);
|
|
86
|
+
border: 1px solid rgba(56, 189, 248, 0.6);
|
|
87
|
+
box-shadow: 0 2px 8px rgba(56, 189, 248, 0.3);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
[data-status][data-state="ok"] { color: var(--success); }
|
|
91
|
+
[data-status][data-state="error"] { color: var(--error); }
|
|
92
|
+
[data-status][data-state="pending"] { color: var(--pending); }
|
|
93
|
+
[data-latency][data-state="warn"] { color: var(--warn); }
|
|
94
|
+
|
|
95
|
+
button {
|
|
96
|
+
pointer-events: auto;
|
|
97
|
+
padding: 0.65rem 1.5rem;
|
|
98
|
+
border-radius: 999px;
|
|
99
|
+
border: 1px solid rgba(168, 85, 247, 0.5);
|
|
100
|
+
font-weight: 600;
|
|
101
|
+
background: linear-gradient(135deg,
|
|
102
|
+
rgba(139, 92, 246, 0.85) 0%,
|
|
103
|
+
rgba(168, 85, 247, 0.7) 100%);
|
|
104
|
+
color: #f8fafc;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
transition: transform 0.3s ease, filter 0.3s ease, box-shadow 0.3s ease;
|
|
107
|
+
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
button:disabled {
|
|
111
|
+
background: rgba(30, 41, 59, 0.92);
|
|
112
|
+
color: var(--text-muted);
|
|
113
|
+
cursor: not-allowed;
|
|
114
|
+
box-shadow: none;
|
|
115
|
+
border-color: rgba(148, 163, 184, 0.2);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
button:not(:disabled):hover {
|
|
119
|
+
filter: brightness(1.15);
|
|
120
|
+
transform: translateY(-2px);
|
|
121
|
+
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.5);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
button:not(:disabled):active {
|
|
125
|
+
transform: scale(0.97) translateY(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.panel .button-group {
|
|
129
|
+
display: inline-flex;
|
|
130
|
+
gap: 0.5rem;
|
|
131
|
+
flex-wrap: wrap;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.panel .button-group button {
|
|
135
|
+
background: linear-gradient(135deg,
|
|
136
|
+
rgba(139, 92, 246, 0.85) 0%,
|
|
137
|
+
rgba(168, 85, 247, 0.7) 100%);
|
|
138
|
+
padding: 0.5rem 1.1rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@media (max-width: 640px) {
|
|
142
|
+
[data-overlay] {
|
|
143
|
+
inset: 1rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.panel {
|
|
147
|
+
border-radius: 24px;
|
|
148
|
+
width: 100%;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
align-items: flex-start;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.remote-panel {
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
align-items: stretch;
|
|
157
|
+
gap: 0.6rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.remote-panel .remote-headline {
|
|
161
|
+
display: flex;
|
|
162
|
+
justify-content: space-between;
|
|
163
|
+
align-items: center;
|
|
164
|
+
width: 100%;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
[data-remote-audit] {
|
|
168
|
+
font-size: 0.75rem;
|
|
169
|
+
text-transform: uppercase;
|
|
170
|
+
letter-spacing: 0.08em;
|
|
171
|
+
padding: 0.2rem 0.6rem;
|
|
172
|
+
border-radius: 999px;
|
|
173
|
+
background: rgba(59, 130, 246, 0.15);
|
|
174
|
+
border: 1px solid rgba(59, 130, 246, 0.35);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.remote-input {
|
|
178
|
+
display: flex;
|
|
179
|
+
gap: 0.5rem;
|
|
180
|
+
width: 100%;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.remote-input input {
|
|
184
|
+
flex: 1;
|
|
185
|
+
border-radius: 999px;
|
|
186
|
+
border: 1px solid rgba(148, 163, 184, 0.35);
|
|
187
|
+
background: rgba(15, 23, 42, 0.6);
|
|
188
|
+
color: #e2e8f0;
|
|
189
|
+
padding: 0.55rem 1rem;
|
|
190
|
+
font-size: 0.95rem;
|
|
191
|
+
outline: none;
|
|
192
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.remote-input input:focus {
|
|
196
|
+
border-color: #38bdf8;
|
|
197
|
+
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.25);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.remote-input button {
|
|
201
|
+
border-radius: 999px;
|
|
202
|
+
padding: 0.55rem 1.2rem;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
[data-remote-status] {
|
|
206
|
+
font-size: 0.85rem;
|
|
207
|
+
color: #cbd5f5;
|
|
208
|
+
transition: color 0.2s ease;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
[data-remote-status][data-state="pending"] { color: #facc15; }
|
|
212
|
+
[data-remote-status][data-state="ok"] { color: #4ade80; }
|
|
213
|
+
[data-remote-status][data-state="error"] { color: #fb7185; }
|
|
214
|
+
|
|
215
|
+
[data-remote-detail] {
|
|
216
|
+
font-size: 0.75rem;
|
|
217
|
+
color: #94a3b8;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
[data-remote-actions] {
|
|
221
|
+
display: flex;
|
|
222
|
+
gap: 0.5rem;
|
|
223
|
+
opacity: 0;
|
|
224
|
+
transform: translateY(-4px);
|
|
225
|
+
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
226
|
+
pointer-events: none;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
[data-remote-actions][data-active="true"] {
|
|
230
|
+
opacity: 1;
|
|
231
|
+
transform: translateY(0);
|
|
232
|
+
pointer-events: auto;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
[data-remote-actions] button {
|
|
236
|
+
background: rgba(15, 23, 42, 0.8);
|
|
237
|
+
border: 1px solid rgba(148, 163, 184, 0.4);
|
|
238
|
+
padding: 0.4rem 0.9rem;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.remote-dock {
|
|
242
|
+
position: fixed;
|
|
243
|
+
bottom: 1.5rem;
|
|
244
|
+
right: 1.5rem;
|
|
245
|
+
width: clamp(260px, 28vw, 420px);
|
|
246
|
+
backdrop-filter: blur(20px) saturate(150%);
|
|
247
|
+
-webkit-backdrop-filter: blur(20px) saturate(150%);
|
|
248
|
+
background: linear-gradient(135deg,
|
|
249
|
+
rgba(15, 23, 42, 0.95) 0%,
|
|
250
|
+
rgba(30, 41, 59, 0.9) 100%);
|
|
251
|
+
border: 1px solid rgba(148, 163, 184, 0.3);
|
|
252
|
+
border-radius: 1.25rem;
|
|
253
|
+
padding: 1rem;
|
|
254
|
+
color: #e2e8f0;
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
gap: 0.75rem;
|
|
258
|
+
pointer-events: auto;
|
|
259
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
|
|
260
|
+
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
|
|
261
|
+
transition: all 0.3s ease;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.remote-dock:hover {
|
|
265
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5),
|
|
266
|
+
0 0 0 1px rgba(255, 255, 255, 0.08) inset;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.remote-dock header {
|
|
270
|
+
display: flex;
|
|
271
|
+
justify-content: space-between;
|
|
272
|
+
align-items: center;
|
|
273
|
+
font-weight: 600;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
[data-remote-pulse] {
|
|
277
|
+
font-size: 0.8rem;
|
|
278
|
+
color: #cbd5f5;
|
|
279
|
+
padding: 0.2rem 0.6rem;
|
|
280
|
+
border-radius: 999px;
|
|
281
|
+
border: 1px solid rgba(148, 163, 184, 0.4);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
[data-remote-pulse][data-state="proxy"] {
|
|
285
|
+
border-color: rgba(249, 115, 22, 0.6);
|
|
286
|
+
color: #fb923c;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.remote-stage {
|
|
290
|
+
position: relative;
|
|
291
|
+
width: 100%;
|
|
292
|
+
aspect-ratio: 16 / 9;
|
|
293
|
+
border-radius: 1rem;
|
|
294
|
+
overflow: hidden;
|
|
295
|
+
border: 1px solid rgba(148, 163, 184, 0.3);
|
|
296
|
+
background: rgba(2, 6, 23, 0.9);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.remote-stage::before {
|
|
300
|
+
content: 'XR対応サイトをここでプレビュー';
|
|
301
|
+
position: absolute;
|
|
302
|
+
inset: 0;
|
|
303
|
+
display: flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
justify-content: center;
|
|
306
|
+
color: #94a3b8;
|
|
307
|
+
font-size: 0.9rem;
|
|
308
|
+
text-align: center;
|
|
309
|
+
padding: 1rem;
|
|
310
|
+
transition: opacity 0.2s ease;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.remote-stage[data-state="loaded"]::before,
|
|
314
|
+
.remote-stage[data-state="pending"]::before {
|
|
315
|
+
opacity: 0;
|
|
316
|
+
pointer-events: none;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.remote-stage iframe {
|
|
320
|
+
width: 100%;
|
|
321
|
+
height: 100%;
|
|
322
|
+
border: 0;
|
|
323
|
+
background: #000;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
[data-remote-container] [data-remotescenecontainer] {
|
|
327
|
+
width: 100%;
|
|
328
|
+
height: 100%;
|
|
329
|
+
pointer-events: auto;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
@media (max-width: 960px) {
|
|
333
|
+
.remote-dock {
|
|
334
|
+
position: static;
|
|
335
|
+
width: auto;
|
|
336
|
+
margin: 1rem;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ja">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
|
+
<title>ChocoDrop XR GLB Viewer</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
font-family: 'Inter', 'Noto Sans JP', system-ui, -apple-system, sans-serif;
|
|
12
|
+
background: #01060f;
|
|
13
|
+
color: #e2e8f0;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
}
|
|
16
|
+
#ui {
|
|
17
|
+
position: fixed;
|
|
18
|
+
top: 12px;
|
|
19
|
+
left: 12px;
|
|
20
|
+
right: 12px;
|
|
21
|
+
display: grid;
|
|
22
|
+
grid-template-columns: 1fr auto;
|
|
23
|
+
gap: 10px;
|
|
24
|
+
align-items: center;
|
|
25
|
+
background: rgba(15, 23, 42, 0.72);
|
|
26
|
+
border: 1px solid rgba(148, 163, 184, 0.25);
|
|
27
|
+
border-radius: 12px;
|
|
28
|
+
padding: 10px 12px;
|
|
29
|
+
backdrop-filter: blur(10px);
|
|
30
|
+
z-index: 10;
|
|
31
|
+
}
|
|
32
|
+
#right-controls { display: flex; gap: 8px; align-items: center; }
|
|
33
|
+
input[type="file"], input[type="url"] {
|
|
34
|
+
width: 100%;
|
|
35
|
+
padding: 8px 10px;
|
|
36
|
+
border-radius: 10px;
|
|
37
|
+
border: 1px solid rgba(148, 163, 184, 0.4);
|
|
38
|
+
background: rgba(15, 23, 42, 0.7);
|
|
39
|
+
color: #e2e8f0;
|
|
40
|
+
}
|
|
41
|
+
button {
|
|
42
|
+
border-radius: 10px;
|
|
43
|
+
border: 1px solid rgba(56, 189, 248, 0.8);
|
|
44
|
+
background: linear-gradient(120deg, rgba(56, 189, 248, 0.85), rgba(56, 189, 248, 0.45));
|
|
45
|
+
color: #0f172a;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
padding: 8px 12px;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
}
|
|
50
|
+
#drop-hint {
|
|
51
|
+
position: fixed;
|
|
52
|
+
inset: 0;
|
|
53
|
+
display: none;
|
|
54
|
+
place-items: center;
|
|
55
|
+
background: rgba(14, 165, 233, 0.16);
|
|
56
|
+
border: 2px dashed rgba(56, 189, 248, 0.9);
|
|
57
|
+
color: #e0f2fe;
|
|
58
|
+
font-size: 1.2rem;
|
|
59
|
+
z-index: 9;
|
|
60
|
+
}
|
|
61
|
+
#drop-hint[data-active="true"] { display: grid; }
|
|
62
|
+
#status { font-size: 0.9rem; color: #cbd5e1; }
|
|
63
|
+
canvas { display: block; }
|
|
64
|
+
</style>
|
|
65
|
+
<script type="importmap">
|
|
66
|
+
{
|
|
67
|
+
"imports": {
|
|
68
|
+
"three": "https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.module.js",
|
|
69
|
+
"three/examples/jsm/": "https://cdn.jsdelivr.net/npm/three@0.158.0/examples/jsm/"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<div id="ui">
|
|
76
|
+
<div>
|
|
77
|
+
<input id="url-input" type="url" placeholder="GLB/GLTF の URL を貼り付け (同一オリジン推奨)" spellcheck="false" />
|
|
78
|
+
<div id="status">待機中</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div id="right-controls">
|
|
81
|
+
<label style="display:inline-block">
|
|
82
|
+
<input id="file-input" type="file" accept=".glb,.gltf" style="max-width:180px" />
|
|
83
|
+
</label>
|
|
84
|
+
<button id="load-url">URL読込</button>
|
|
85
|
+
<button id="reset">リセット</button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div id="drop-hint">ここに GLB/GLTF をドロップ</div>
|
|
89
|
+
<canvas id="xr-canvas"></canvas>
|
|
90
|
+
<script type="module">
|
|
91
|
+
import * as THREE from 'three';
|
|
92
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
93
|
+
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
|
94
|
+
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
|
|
95
|
+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
96
|
+
import { VRButton } from 'three/examples/jsm/webxr/VRButton.js';
|
|
97
|
+
import { ARButton } from 'three/examples/jsm/webxr/ARButton.js';
|
|
98
|
+
|
|
99
|
+
const canvas = document.querySelector('#xr-canvas');
|
|
100
|
+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
|
101
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
102
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
103
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
104
|
+
renderer.xr.enabled = true;
|
|
105
|
+
document.body.appendChild(VRButton.createButton(renderer));
|
|
106
|
+
// ARはQuestブラウザがAR対応なら利用可
|
|
107
|
+
document.body.appendChild(ARButton.createButton(renderer, { requiredFeatures: [] }));
|
|
108
|
+
|
|
109
|
+
const scene = new THREE.Scene();
|
|
110
|
+
scene.background = new THREE.Color(0x020617);
|
|
111
|
+
|
|
112
|
+
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100);
|
|
113
|
+
camera.position.set(0, 1.6, 3);
|
|
114
|
+
|
|
115
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
116
|
+
controls.target.set(0, 1.2, 0);
|
|
117
|
+
controls.update();
|
|
118
|
+
|
|
119
|
+
// 環境
|
|
120
|
+
scene.add(new THREE.HemisphereLight(0xffffff, 0x1e293b, 1.0));
|
|
121
|
+
const dir = new THREE.DirectionalLight(0xffffff, 1.2);
|
|
122
|
+
dir.position.set(3, 5, 2);
|
|
123
|
+
dir.castShadow = true;
|
|
124
|
+
scene.add(dir);
|
|
125
|
+
const ground = new THREE.Mesh(
|
|
126
|
+
new THREE.CircleGeometry(6, 64),
|
|
127
|
+
new THREE.MeshStandardMaterial({ color: 0x0b1221, roughness: 0.9, metalness: 0.05 })
|
|
128
|
+
);
|
|
129
|
+
ground.rotation.x = -Math.PI / 2;
|
|
130
|
+
ground.receiveShadow = true;
|
|
131
|
+
scene.add(ground);
|
|
132
|
+
|
|
133
|
+
const loader = new GLTFLoader();
|
|
134
|
+
const draco = new DRACOLoader();
|
|
135
|
+
draco.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.158.0/examples/jsm/libs/draco/');
|
|
136
|
+
loader.setDRACOLoader(draco);
|
|
137
|
+
|
|
138
|
+
let current = null;
|
|
139
|
+
|
|
140
|
+
function setStatus(text, tone = 'info') {
|
|
141
|
+
const el = document.querySelector('#status');
|
|
142
|
+
el.textContent = text;
|
|
143
|
+
el.style.color = tone === 'error' ? '#f87171' : tone === 'warn' ? '#fbbf24' : '#cbd5e1';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function clearScene() {
|
|
147
|
+
if (current) {
|
|
148
|
+
scene.remove(current);
|
|
149
|
+
current.traverse?.(obj => {
|
|
150
|
+
if (obj.isMesh) {
|
|
151
|
+
obj.geometry?.dispose?.();
|
|
152
|
+
if (Array.isArray(obj.material)) {
|
|
153
|
+
obj.material.forEach(m => m?.dispose?.());
|
|
154
|
+
} else {
|
|
155
|
+
obj.material?.dispose?.();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
current = null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function centerModel(object) {
|
|
164
|
+
const box = new THREE.Box3().setFromObject(object);
|
|
165
|
+
const size = box.getSize(new THREE.Vector3());
|
|
166
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
167
|
+
object.position.sub(center); // 原点に合わせる
|
|
168
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
169
|
+
const scale = maxDim > 4 ? 4 / maxDim : 1;
|
|
170
|
+
object.scale.multiplyScalar(scale);
|
|
171
|
+
controls.reset();
|
|
172
|
+
controls.target.set(0, size.y * 0.5, 0);
|
|
173
|
+
camera.position.set(0, size.y * 0.6 + 1.2, Math.max(3, maxDim * 1.2));
|
|
174
|
+
controls.update();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function loadFromArrayBuffer(buffer, name = 'model') {
|
|
178
|
+
setStatus(`${name} を読み込み中…`);
|
|
179
|
+
try {
|
|
180
|
+
const glb = await loader.parseAsync(buffer, '');
|
|
181
|
+
clearScene();
|
|
182
|
+
current = glb.scene;
|
|
183
|
+
scene.add(current);
|
|
184
|
+
centerModel(current);
|
|
185
|
+
setStatus(`${name} を読み込みました`, 'info');
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(error);
|
|
188
|
+
setStatus(`読み込み失敗: ${error?.message || error}`, 'error');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function loadFromURL(url) {
|
|
193
|
+
setStatus('URL 取得中…');
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetch(url);
|
|
196
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
197
|
+
const buffer = await response.arrayBuffer();
|
|
198
|
+
const name = url.split('/').pop() || 'model';
|
|
199
|
+
await loadFromArrayBuffer(buffer, name);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error(error);
|
|
202
|
+
setStatus(`URL 読み込み失敗: ${error?.message || error}`, 'error');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// UI
|
|
207
|
+
const fileInput = document.querySelector('#file-input');
|
|
208
|
+
const urlInput = document.querySelector('#url-input');
|
|
209
|
+
const loadUrlBtn = document.querySelector('#load-url');
|
|
210
|
+
const resetBtn = document.querySelector('#reset');
|
|
211
|
+
const dropHint = document.querySelector('#drop-hint');
|
|
212
|
+
|
|
213
|
+
fileInput.addEventListener('change', async e => {
|
|
214
|
+
const file = e.target.files?.[0];
|
|
215
|
+
e.target.value = '';
|
|
216
|
+
if (!file) return;
|
|
217
|
+
const buffer = await file.arrayBuffer();
|
|
218
|
+
loadFromArrayBuffer(buffer, file.name);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
loadUrlBtn.addEventListener('click', () => {
|
|
222
|
+
const url = urlInput.value.trim();
|
|
223
|
+
if (!url) return setStatus('URL を入力してください', 'warn');
|
|
224
|
+
loadFromURL(url);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
resetBtn.addEventListener('click', () => {
|
|
228
|
+
clearScene();
|
|
229
|
+
setStatus('待機中');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
window.addEventListener('dragover', e => {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
dropHint.dataset.active = 'true';
|
|
235
|
+
});
|
|
236
|
+
window.addEventListener('dragleave', e => {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
dropHint.dataset.active = 'false';
|
|
239
|
+
});
|
|
240
|
+
window.addEventListener('drop', async e => {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
dropHint.dataset.active = 'false';
|
|
243
|
+
const file = e.dataTransfer?.files?.[0];
|
|
244
|
+
if (!file) return;
|
|
245
|
+
const buffer = await file.arrayBuffer();
|
|
246
|
+
loadFromArrayBuffer(buffer, file.name);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
function onResize() {
|
|
250
|
+
const w = window.innerWidth;
|
|
251
|
+
const h = window.innerHeight;
|
|
252
|
+
camera.aspect = w / h;
|
|
253
|
+
camera.updateProjectionMatrix();
|
|
254
|
+
renderer.setSize(w, h);
|
|
255
|
+
}
|
|
256
|
+
window.addEventListener('resize', onResize);
|
|
257
|
+
|
|
258
|
+
renderer.setAnimationLoop(() => {
|
|
259
|
+
renderer.render(scene, camera);
|
|
260
|
+
});
|
|
261
|
+
</script>
|
|
262
|
+
</body>
|
|
263
|
+
</html>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Connect a local Basic scene to the authenticated, same-origin MCP bridge. */
|
|
2
|
+
export function connectLocalBridge(sceneManager, { statusElement } = {}) {
|
|
3
|
+
const setStatus = (message) => {
|
|
4
|
+
if (statusElement) statusElement.textContent = message;
|
|
5
|
+
};
|
|
6
|
+
if (location.hostname !== '127.0.0.1' || location.protocol !== 'http:') {
|
|
7
|
+
setStatus('ツール連携はローカル起動時のURLから利用してください');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const tokenKey = 'chocodrop:local-token';
|
|
11
|
+
const token = new URLSearchParams(location.hash.slice(1)).get('token') || sessionStorage.getItem(tokenKey);
|
|
12
|
+
if (!token) {
|
|
13
|
+
setStatus('接続情報がありません。起動時に表示されたURLを開いてください');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
sessionStorage.setItem(tokenKey, token);
|
|
17
|
+
history.replaceState(null, '', location.pathname + location.search);
|
|
18
|
+
const socket = new WebSocket(`ws://${location.host}/local-api/live?token=${encodeURIComponent(token)}`);
|
|
19
|
+
const requests = new Map();
|
|
20
|
+
socket.addEventListener('open', () =>
|
|
21
|
+
setStatus('ツール連携中 · Codex・Claude Code・Antigravityなどから素材を配置できます')
|
|
22
|
+
);
|
|
23
|
+
socket.addEventListener('close', () => {
|
|
24
|
+
for (const request of requests.values()) request.cancelled = true;
|
|
25
|
+
setStatus('ツール連携が切れました · サーバーを確認して再読み込みしてください');
|
|
26
|
+
});
|
|
27
|
+
socket.addEventListener('error', () => setStatus('ツールへ接続できません。起動時のURLを開き直してください'));
|
|
28
|
+
socket.addEventListener('message', async ({ data }) => {
|
|
29
|
+
let message;
|
|
30
|
+
try { message = JSON.parse(data); } catch { return; }
|
|
31
|
+
if (message.type === 'import-cancel') {
|
|
32
|
+
const request = requests.get(message.requestId);
|
|
33
|
+
if (request) request.cancelled = true;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (message.type !== 'import' || typeof message.requestId !== 'string') return;
|
|
37
|
+
const reply = (value) => {
|
|
38
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
39
|
+
socket.send(JSON.stringify({ type: 'import-result', requestId: message.requestId, ...value }));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
if (requests.size) {
|
|
43
|
+
reply({ ok: false, error: 'Another asset is still loading; retry after it completes' });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const request = { cancelled: false };
|
|
47
|
+
requests.set(message.requestId, request);
|
|
48
|
+
setStatus(`読み込み中 · ${message.asset?.name || '素材'}`);
|
|
49
|
+
try {
|
|
50
|
+
const result = await importBridgeAsset(sceneManager, message, location.origin);
|
|
51
|
+
if (request.cancelled || socket.readyState !== WebSocket.OPEN) {
|
|
52
|
+
sceneManager.removeObject(result.objectId);
|
|
53
|
+
setStatus('読み込みを中断しました。接続を確認して再実行してください');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
setStatus(`配置完了 · ${message.asset.name} · シーン内 ${sceneManager.spawnedObjects.size} 個`);
|
|
57
|
+
reply({ ok: true, objectId: result.objectId });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
setStatus(`配置できませんでした: ${error.message}`);
|
|
60
|
+
reply({ ok: false, error: error.message });
|
|
61
|
+
} finally {
|
|
62
|
+
requests.delete(message.requestId);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
window.addEventListener('pagehide', () => socket.close(), { once: true });
|
|
66
|
+
return socket;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Import only URLs served by this local bridge; success means an object exists. */
|
|
70
|
+
export async function importBridgeAsset(sceneManager, { asset, position }, origin) {
|
|
71
|
+
if (!asset || typeof asset.url !== 'string') throw new Error('Missing asset URL');
|
|
72
|
+
const url = new URL(asset.url, origin);
|
|
73
|
+
if (url.origin !== origin || !/^\/local-api\/assets\/[0-9a-f-]+$/i.test(url.pathname)) {
|
|
74
|
+
throw new Error('Asset URL must belong to the local bridge');
|
|
75
|
+
}
|
|
76
|
+
const method = { image: 'loadImageFile', video: 'loadVideoFile', model: 'load3DModel' }[asset.mediaType];
|
|
77
|
+
if (!method) throw new Error('Unsupported media type');
|
|
78
|
+
const result = await sceneManager[method](url.href, { fileName: asset.name, position });
|
|
79
|
+
if (!result?.success || !sceneManager.spawnedObjects.has(result.objectId)) {
|
|
80
|
+
throw new Error('The asset was not added to the scene');
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|