@firetable/project-xiaochun 0.1.14 → 0.1.16
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/README-CN.md +117 -19
- package/README.md +117 -19
- package/dist/avatar-element.d.ts +29 -6
- package/dist/chunks/{chunk-HLVD6HQM.js → chunk-HZ2ADKG7.js} +114 -11
- package/dist/chunks/chunk-HZ2ADKG7.js.map +7 -0
- package/dist/chunks/chunk-RHQW4QZH.js +222 -0
- package/dist/chunks/chunk-RHQW4QZH.js.map +7 -0
- package/dist/chunks/chunk-UWL65W7D.js +1083 -0
- package/dist/chunks/chunk-UWL65W7D.js.map +7 -0
- package/dist/client.d.ts +187 -7
- package/dist/element.cjs +738 -80
- package/dist/element.cjs.map +4 -4
- package/dist/element.js +4 -4
- package/dist/gesture-box.d.ts +72 -0
- package/dist/index.cjs +806 -81
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +38 -6
- package/dist/index.js.map +1 -1
- package/dist/loader.global.js +2 -2
- package/dist/loader.global.js.map +4 -4
- package/dist/protocol.cjs +133 -2
- package/dist/protocol.cjs.map +2 -2
- package/dist/protocol.d.ts +279 -9
- package/dist/protocol.js +36 -4
- package/dist/react.cjs +689 -86
- package/dist/react.cjs.map +4 -4
- package/dist/react.d.ts +23 -1
- package/dist/react.js +66 -18
- package/dist/react.js.map +2 -2
- package/package.json +5 -3
- package/dist/chunks/chunk-DUU5PTKT.js +0 -91
- package/dist/chunks/chunk-DUU5PTKT.js.map +0 -7
- package/dist/chunks/chunk-HLVD6HQM.js.map +0 -7
- package/dist/chunks/chunk-OLVSOJML.js +0 -579
- package/dist/chunks/chunk-OLVSOJML.js.map +0 -7
package/dist/react.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.16 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
"use strict";
|
|
3
3
|
"use client";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -32,6 +32,70 @@ var import_react = require("react");
|
|
|
32
32
|
var XC_PROTOCOL_VERSION = 1;
|
|
33
33
|
var XC_DEFAULT_ORIGIN = "https://xiaochun.firetable.tech";
|
|
34
34
|
var XC_DEFAULT_SRC = `${XC_DEFAULT_ORIGIN}/embed`;
|
|
35
|
+
var XC_WINDOW_CORNER_RADIUS = 20;
|
|
36
|
+
var XC_UI_PARTS = ["chat", "bubble", "outfit", "scene", "lang", "github"];
|
|
37
|
+
var XC_UI_PART_SET = new Set(XC_UI_PARTS);
|
|
38
|
+
var XC_UI_LEGACY_PARTS = ["chat", "bubble"];
|
|
39
|
+
function parseXcUiList(items) {
|
|
40
|
+
const seen = /* @__PURE__ */ new Set();
|
|
41
|
+
const unknown = [];
|
|
42
|
+
for (const it of items) {
|
|
43
|
+
const name = typeof it === "string" ? it.trim().toLowerCase() : "";
|
|
44
|
+
if (XC_UI_PART_SET.has(name)) seen.add(name);
|
|
45
|
+
else unknown.push(typeof it === "string" ? it.slice(0, 40) : String(typeof it));
|
|
46
|
+
}
|
|
47
|
+
return { parts: XC_UI_PARTS.filter((p) => seen.has(p)), unknown, legacy: false };
|
|
48
|
+
}
|
|
49
|
+
function normalizeXcUiOption(v) {
|
|
50
|
+
if (v === true) return { parts: [...XC_UI_LEGACY_PARTS], unknown: [], legacy: true };
|
|
51
|
+
if (Array.isArray(v)) return parseXcUiList(v);
|
|
52
|
+
return { parts: [], unknown: [], legacy: false };
|
|
53
|
+
}
|
|
54
|
+
function parseXcUiAutoHide(raw) {
|
|
55
|
+
if (raw === true || raw === false || raw === "transparent") return raw;
|
|
56
|
+
if (typeof raw !== "string") return void 0;
|
|
57
|
+
const v = raw.trim().toLowerCase();
|
|
58
|
+
if (v === "1" || v === "true") return true;
|
|
59
|
+
if (v === "0" || v === "false") return false;
|
|
60
|
+
if (v === "transparent") return "transparent";
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
var XC_CAMERA_RANGES = {
|
|
64
|
+
fov: [15, 60],
|
|
65
|
+
distance: [1, 15],
|
|
66
|
+
height: [-1, 1]
|
|
67
|
+
};
|
|
68
|
+
var XC_CAMERA_NUM_KEYS = ["fov", "distance", "height"];
|
|
69
|
+
var XC_CAMERA_KEYS = ["fov", "distance", "height", "intro"];
|
|
70
|
+
function normalizeXcCamera(raw) {
|
|
71
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return { ok: false, error: "camera must be an object" };
|
|
72
|
+
const src = raw;
|
|
73
|
+
for (const k of Object.keys(src)) {
|
|
74
|
+
if (!XC_CAMERA_KEYS.includes(k)) return { ok: false, error: `unknown camera key: ${k} (expected ${XC_CAMERA_KEYS.join(" | ")})` };
|
|
75
|
+
}
|
|
76
|
+
const out = {};
|
|
77
|
+
const clamped = [];
|
|
78
|
+
for (const k of XC_CAMERA_NUM_KEYS) {
|
|
79
|
+
const v = src[k];
|
|
80
|
+
if (v === void 0) continue;
|
|
81
|
+
if (v === null) {
|
|
82
|
+
out[k] = null;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return { ok: false, error: `camera.${k} must be a finite number or null` };
|
|
86
|
+
const [lo, hi] = XC_CAMERA_RANGES[k];
|
|
87
|
+
const c = Math.min(hi, Math.max(lo, v));
|
|
88
|
+
if (c !== v) clamped.push(k);
|
|
89
|
+
out[k] = c;
|
|
90
|
+
}
|
|
91
|
+
if (src.intro !== void 0) {
|
|
92
|
+
if (src.intro !== null && typeof src.intro !== "boolean") return { ok: false, error: "camera.intro must be a boolean or null" };
|
|
93
|
+
out.intro = src.intro;
|
|
94
|
+
}
|
|
95
|
+
return { ok: true, camera: out, clamped };
|
|
96
|
+
}
|
|
97
|
+
var XC_ID_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
98
|
+
var isXcId = (v) => typeof v === "string" && XC_ID_RE.test(v);
|
|
35
99
|
function xcMessage(type, payload, id) {
|
|
36
100
|
const m = { type, v: XC_PROTOCOL_VERSION, payload };
|
|
37
101
|
if (id !== void 0) m.id = id;
|
|
@@ -53,11 +117,113 @@ function normalizeOrigin(input) {
|
|
|
53
117
|
}
|
|
54
118
|
}
|
|
55
119
|
|
|
120
|
+
// src/gesture-box.ts
|
|
121
|
+
var DEFAULT_RESIZE_LIMITS = Object.freeze({
|
|
122
|
+
minWidth: 120,
|
|
123
|
+
minHeight: 180,
|
|
124
|
+
maxWidth: Infinity,
|
|
125
|
+
maxHeight: Infinity
|
|
126
|
+
});
|
|
127
|
+
var finitePositive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
128
|
+
function normalizeResizable(v) {
|
|
129
|
+
if (!v) return null;
|
|
130
|
+
const o = v === true ? {} : v;
|
|
131
|
+
const minWidth = finitePositive(o.minWidth) ? o.minWidth : DEFAULT_RESIZE_LIMITS.minWidth;
|
|
132
|
+
const minHeight = finitePositive(o.minHeight) ? o.minHeight : DEFAULT_RESIZE_LIMITS.minHeight;
|
|
133
|
+
const maxWidth = finitePositive(o.maxWidth) ? Math.max(o.maxWidth, minWidth) : Infinity;
|
|
134
|
+
const maxHeight = finitePositive(o.maxHeight) ? Math.max(o.maxHeight, minHeight) : Infinity;
|
|
135
|
+
return { minWidth, minHeight, maxWidth, maxHeight };
|
|
136
|
+
}
|
|
137
|
+
var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
|
|
138
|
+
function moveBox(start, totalDx, totalDy, vp) {
|
|
139
|
+
return {
|
|
140
|
+
left: clamp(start.left + totalDx, 0, vp.width - start.width),
|
|
141
|
+
top: clamp(start.top + totalDy, 0, vp.height - start.height),
|
|
142
|
+
width: start.width,
|
|
143
|
+
height: start.height
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function resizeBox(start, corner, totalDx, totalDy, limits, vp) {
|
|
147
|
+
const west = corner === "NW" || corner === "SW";
|
|
148
|
+
const north = corner === "NW" || corner === "NE";
|
|
149
|
+
const right = start.left + start.width, bottom = start.top + start.height;
|
|
150
|
+
const wantW = west ? start.width - totalDx : start.width + totalDx;
|
|
151
|
+
const wantH = north ? start.height - totalDy : start.height + totalDy;
|
|
152
|
+
const roomW = west ? right : vp.width - start.left;
|
|
153
|
+
const roomH = north ? bottom : vp.height - start.top;
|
|
154
|
+
const width = clamp(wantW, limits.minWidth, Math.min(limits.maxWidth, roomW));
|
|
155
|
+
const height = clamp(wantH, limits.minHeight, Math.min(limits.maxHeight, roomH));
|
|
156
|
+
return { left: west ? right - width : start.left, top: north ? bottom - height : start.top, width, height };
|
|
157
|
+
}
|
|
158
|
+
function fitBox(b, vp) {
|
|
159
|
+
const width = Math.min(b.width, Math.max(1, vp.width)), height = Math.min(b.height, Math.max(1, vp.height));
|
|
160
|
+
return { left: clamp(b.left, 0, vp.width - width), top: clamp(b.top, 0, vp.height - height), width, height };
|
|
161
|
+
}
|
|
162
|
+
var CORNERS = /* @__PURE__ */ new Set(["NW", "NE", "SW", "SE"]);
|
|
163
|
+
var PHASES = /* @__PURE__ */ new Set(["start", "move", "end"]);
|
|
164
|
+
var REASONS = /* @__PURE__ */ new Set(["up", "cancel", "blur"]);
|
|
165
|
+
var MAX_GESTURE_DELTA = 2e4;
|
|
166
|
+
var delta = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= MAX_GESTURE_DELTA;
|
|
167
|
+
var counter = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
168
|
+
function parseGesturePayload(kind, raw) {
|
|
169
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
170
|
+
const p = raw;
|
|
171
|
+
if (!counter(p.gesture) || !counter(p.seq) || typeof p.phase !== "string" || !PHASES.has(p.phase)) return null;
|
|
172
|
+
if (!delta(p.dx) || !delta(p.dy) || !delta(p.totalDx) || !delta(p.totalDy)) return null;
|
|
173
|
+
if (p.reason !== void 0 && (typeof p.reason !== "string" || !REASONS.has(p.reason))) return null;
|
|
174
|
+
const out = { gesture: p.gesture, seq: p.seq, phase: p.phase, totalDx: p.totalDx, totalDy: p.totalDy };
|
|
175
|
+
if (kind === "resize") {
|
|
176
|
+
if (typeof p.corner !== "string" || !CORNERS.has(p.corner)) return null;
|
|
177
|
+
out.corner = p.corner;
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
var GestureGate = class {
|
|
182
|
+
constructor() {
|
|
183
|
+
this.lastGesture = 0;
|
|
184
|
+
this.lastSeq = -1;
|
|
185
|
+
this.open = false;
|
|
186
|
+
}
|
|
187
|
+
get active() {
|
|
188
|
+
return this.open;
|
|
189
|
+
}
|
|
190
|
+
accept(p) {
|
|
191
|
+
if (p.phase === "start") {
|
|
192
|
+
if (p.gesture <= this.lastGesture || p.seq !== 0) return false;
|
|
193
|
+
this.lastGesture = p.gesture;
|
|
194
|
+
this.lastSeq = 0;
|
|
195
|
+
this.open = true;
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
if (!this.open || p.gesture !== this.lastGesture || p.seq <= this.lastSeq) return false;
|
|
199
|
+
this.lastSeq = p.seq;
|
|
200
|
+
if (p.phase === "end") this.open = false;
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
/** 宿主主动结束 (关闭手势 / 销毁): 之后同一手势的迟到消息被拒绝。 */
|
|
204
|
+
close() {
|
|
205
|
+
this.open = false;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
56
209
|
// src/client.ts
|
|
57
210
|
var FALLBACK_PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
58
211
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 180"><defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f6b8ae"/><stop offset="1" stop-color="#ea8377"/></linearGradient></defs><ellipse cx="60" cy="170" rx="34" ry="6" fill="#000" opacity=".12"/><circle cx="60" cy="48" r="26" fill="url(#g)" opacity=".75"/><path d="M26 160c2-40 18-62 34-62s32 22 34 62z" fill="url(#g)" opacity=".6"/></svg>'
|
|
59
212
|
);
|
|
213
|
+
var noSelect = (st) => {
|
|
214
|
+
st.setProperty("user-select", "none");
|
|
215
|
+
st.setProperty("-webkit-user-select", "none");
|
|
216
|
+
st.setProperty("-webkit-touch-callout", "none");
|
|
217
|
+
st.setProperty("-webkit-tap-highlight-color", "transparent");
|
|
218
|
+
};
|
|
60
219
|
var css = (v, d) => v === void 0 ? d : typeof v === "number" ? `${v}px` : v;
|
|
220
|
+
var XC_DEFAULT_WIDTH = 600;
|
|
221
|
+
var XC_DEFAULT_HEIGHT = 1080;
|
|
222
|
+
var defaultWidthCss = (floating = false) => floating ? `min(${XC_DEFAULT_WIDTH}px, calc(100vw - 2 * var(--xc-offset-x, 16px)))` : `min(${XC_DEFAULT_WIDTH}px, 100vw)`;
|
|
223
|
+
var defaultHeightCss = (floating = false) => {
|
|
224
|
+
const unit = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("height", "1svh") ? "svh" : "vh";
|
|
225
|
+
return floating ? `min(${XC_DEFAULT_HEIGHT}px, calc(100${unit} - 2 * var(--xc-offset-y, 16px)))` : `min(${XC_DEFAULT_HEIGHT}px, 100${unit})`;
|
|
226
|
+
};
|
|
61
227
|
function createXiaochun(options) {
|
|
62
228
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
63
229
|
throw new Error("[project-xiaochun] createXiaochun must run in a browser");
|
|
@@ -79,8 +245,56 @@ function createXiaochun(options) {
|
|
|
79
245
|
if (!n) throw new Error(`[project-xiaochun] invalid allowedOrigins entry: ${o} (wildcards are not allowed)`);
|
|
80
246
|
trusted.add(n);
|
|
81
247
|
}
|
|
82
|
-
const
|
|
83
|
-
const
|
|
248
|
+
const prefsKey = options.persist === "host" ? "xiaochun:prefs" : typeof options.persist === "string" && options.persist ? options.persist : null;
|
|
249
|
+
const readPrefs = () => {
|
|
250
|
+
if (!prefsKey) return {};
|
|
251
|
+
try {
|
|
252
|
+
const raw = JSON.parse(window.localStorage.getItem(prefsKey) ?? "null");
|
|
253
|
+
const out = {};
|
|
254
|
+
if (raw && typeof raw === "object") {
|
|
255
|
+
if (isXcId(raw.outfit)) out.outfit = raw.outfit;
|
|
256
|
+
if (isXcId(raw.scene)) out.scene = raw.scene;
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
} catch {
|
|
260
|
+
return {};
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const writePrefs = (patch) => {
|
|
264
|
+
if (!prefsKey) return;
|
|
265
|
+
try {
|
|
266
|
+
const next = { ...readPrefs() };
|
|
267
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
268
|
+
if (v) next[k] = v;
|
|
269
|
+
else delete next[k];
|
|
270
|
+
}
|
|
271
|
+
window.localStorage.setItem(prefsKey, JSON.stringify(next));
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const prefs = readPrefs();
|
|
276
|
+
if (options.model !== void 0 && options.outfit === void 0) {
|
|
277
|
+
console.warn('[project-xiaochun] option "model" is deprecated, use "outfit" (built-in outfit id).');
|
|
278
|
+
}
|
|
279
|
+
const uiParse = normalizeXcUiOption(options.ui);
|
|
280
|
+
const uiParts = uiParse.parts;
|
|
281
|
+
const uiAutoHide = parseXcUiAutoHide(options.uiAutoHide);
|
|
282
|
+
if (options.uiAutoHide !== void 0 && uiAutoHide === void 0) console.warn(`[project-xiaochun] ignoring invalid uiAutoHide: ${String(options.uiAutoHide)} (use true | false | 'transparent')`);
|
|
283
|
+
let camera = {};
|
|
284
|
+
if (options.camera !== void 0) {
|
|
285
|
+
const r = normalizeXcCamera(options.camera);
|
|
286
|
+
if (r.ok) {
|
|
287
|
+
camera = r.camera;
|
|
288
|
+
if (r.clamped.length) console.warn(`[project-xiaochun] camera.${r.clamped.join(", camera.")} out of range, clamped (fov ${XC_CAMERA_RANGES.fov.join("\u2013")}, distance ${XC_CAMERA_RANGES.distance.join("\u2013")}, height ${XC_CAMERA_RANGES.height.join("\u2013")})`);
|
|
289
|
+
} else console.warn(`[project-xiaochun] ignoring invalid camera option: ${r.error}`);
|
|
290
|
+
}
|
|
291
|
+
if (uiParse.legacy) console.warn(`[project-xiaochun] option "ui: true" is deprecated, use an array of parts, e.g. ui: ['chat', 'bubble', 'outfit', 'scene'].`);
|
|
292
|
+
if (uiParse.unknown.length) console.warn(`[project-xiaochun] ignoring unknown ui part(s): ${uiParse.unknown.join(", ")} (known: ${XC_UI_PARTS.join(", ")})`);
|
|
293
|
+
const outfitOpt = options.outfit ?? (isXcId(options.model) ? options.model : void 0);
|
|
294
|
+
const initialOutfit = outfitOpt ?? prefs.outfit;
|
|
295
|
+
const initialScene = options.scene ?? prefs.scene ?? (options.transparent ? "transparent" : void 0);
|
|
296
|
+
let transparent = initialScene === "transparent" || initialScene !== "light" && initialScene !== "dark" && (options.transparent ?? false);
|
|
297
|
+
let passthrough = options.passthrough ?? transparent;
|
|
84
298
|
const lazy = options.lazy ?? true;
|
|
85
299
|
const position = options.position ?? "inline";
|
|
86
300
|
const handshakeTimeout = options.handshakeTimeout ?? 2e4;
|
|
@@ -88,14 +302,26 @@ function createXiaochun(options) {
|
|
|
88
302
|
wrapper.setAttribute("data-xiaochun", "");
|
|
89
303
|
const ws = wrapper.style;
|
|
90
304
|
ws.position = position === "inline" ? "relative" : "fixed";
|
|
91
|
-
ws.width = css(options.width, "
|
|
92
|
-
ws.height = css(options.height, "
|
|
305
|
+
ws.width = css(options.width, defaultWidthCss(position !== "inline"));
|
|
306
|
+
ws.height = css(options.height, defaultHeightCss(position !== "inline"));
|
|
93
307
|
ws.maxWidth = "100%";
|
|
94
308
|
ws.overflow = "visible";
|
|
95
309
|
ws.contain = "layout style";
|
|
96
|
-
ws
|
|
310
|
+
noSelect(ws);
|
|
97
311
|
ws.boxShadow = "var(--xc-shadow, none)";
|
|
98
|
-
|
|
312
|
+
const applyWrapperBg = () => {
|
|
313
|
+
ws.background = transparent ? "" : "var(--xc-bg, transparent)";
|
|
314
|
+
};
|
|
315
|
+
applyWrapperBg();
|
|
316
|
+
let radiusOpt = options.borderRadius;
|
|
317
|
+
const radiusValue = () => radiusOpt !== void 0 ? css(radiusOpt, "0px") : transparent ? "0px" : `${XC_WINDOW_CORNER_RADIUS}px`;
|
|
318
|
+
const applyRadius = () => {
|
|
319
|
+
const v = `var(--xc-radius, ${radiusValue()})`;
|
|
320
|
+
ws.borderRadius = v;
|
|
321
|
+
if (placeholderEl) placeholderEl.style.borderRadius = v;
|
|
322
|
+
if (iframe) iframe.style.borderRadius = v;
|
|
323
|
+
};
|
|
324
|
+
ws.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
99
325
|
if (position === "bottom-right") {
|
|
100
326
|
ws.right = "var(--xc-offset-x, 16px)";
|
|
101
327
|
ws.bottom = "var(--xc-offset-y, 16px)";
|
|
@@ -116,13 +342,13 @@ function createXiaochun(options) {
|
|
|
116
342
|
img.src = options.placeholder || FALLBACK_PLACEHOLDER;
|
|
117
343
|
img.alt = "Project XiaoChun";
|
|
118
344
|
img.decoding = "async";
|
|
119
|
-
img.width =
|
|
120
|
-
img.height =
|
|
345
|
+
img.width = XC_DEFAULT_WIDTH;
|
|
346
|
+
img.height = XC_DEFAULT_HEIGHT;
|
|
121
347
|
placeholderEl = img;
|
|
122
348
|
}
|
|
123
349
|
const ps = placeholderEl.style;
|
|
124
350
|
placeholderEl.setAttribute("part", "placeholder");
|
|
125
|
-
ps.borderRadius =
|
|
351
|
+
ps.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
126
352
|
ps.position = "absolute";
|
|
127
353
|
ps.inset = "0";
|
|
128
354
|
ps.width = "100%";
|
|
@@ -131,6 +357,7 @@ function createXiaochun(options) {
|
|
|
131
357
|
ps.transition = "opacity .25s ease";
|
|
132
358
|
ps.pointerEvents = "auto";
|
|
133
359
|
ps.cursor = lazy === false ? "default" : "pointer";
|
|
360
|
+
noSelect(ps);
|
|
134
361
|
placeholderEl.addEventListener("click", () => activate());
|
|
135
362
|
wrapper.appendChild(placeholderEl);
|
|
136
363
|
}
|
|
@@ -159,25 +386,45 @@ function createXiaochun(options) {
|
|
|
159
386
|
let autoPaused = false;
|
|
160
387
|
let loaded = false;
|
|
161
388
|
let lastHit = false;
|
|
389
|
+
let caps = null;
|
|
390
|
+
let currentOutfit = null;
|
|
391
|
+
let currentScene = null;
|
|
162
392
|
const queue = [];
|
|
163
393
|
const pending = /* @__PURE__ */ new Map();
|
|
164
394
|
let idSeq = 0;
|
|
165
395
|
let handshakeTimer = null;
|
|
166
396
|
let io = null;
|
|
167
397
|
let idleHandle = null;
|
|
168
|
-
|
|
398
|
+
let resolveHandshake;
|
|
399
|
+
const handshaked = new Promise((res) => {
|
|
400
|
+
resolveHandshake = res;
|
|
401
|
+
});
|
|
402
|
+
const failLocal = (type, id, code, message) => {
|
|
403
|
+
emit("error", { code, message, command: type });
|
|
404
|
+
const p = id ? pending.get(id) : void 0;
|
|
405
|
+
if (p && id) {
|
|
406
|
+
pending.delete(id);
|
|
407
|
+
p.reject(new Error(`[${code}] ${message}`));
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const unsupportedByFrame = (type, id) => failLocal(type, id, "unsupported", `${type} is not supported by this /embed deployment (older version; its xc.ready has no capabilities for it)`);
|
|
411
|
+
const postToFrame = (type, payload, id, transfer, requires) => {
|
|
169
412
|
if (!port) {
|
|
170
|
-
queue.push({ type, payload, id, transfer });
|
|
413
|
+
queue.push({ type, payload, id, transfer, requires });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (requires && !caps?.[requires]) {
|
|
417
|
+
unsupportedByFrame(type, id);
|
|
171
418
|
return;
|
|
172
419
|
}
|
|
173
420
|
port.postMessage(xcMessage(type, payload, id), transfer ?? []);
|
|
174
421
|
};
|
|
175
|
-
const send = (type, payload, wait = false, transfer, fixedId) => {
|
|
422
|
+
const send = (type, payload, wait = false, transfer, fixedId, requires) => {
|
|
176
423
|
if (destroyed) return Promise.reject(new Error("[project-xiaochun] instance destroyed"));
|
|
177
424
|
if (!iframe) activate();
|
|
178
425
|
const id = fixedId ?? `c${++idSeq}`;
|
|
179
426
|
const p = wait ? new Promise((resolve, reject) => pending.set(id, { resolve, reject, sawEnd: false })) : Promise.resolve();
|
|
180
|
-
postToFrame(type, payload, id, transfer);
|
|
427
|
+
postToFrame(type, payload, id, transfer, requires);
|
|
181
428
|
return p;
|
|
182
429
|
};
|
|
183
430
|
const toBuffer = (d) => d instanceof ArrayBuffer ? d : d.buffer.slice(d.byteOffset, d.byteOffset + d.byteLength);
|
|
@@ -227,7 +474,7 @@ function createXiaochun(options) {
|
|
|
227
474
|
let ended = false;
|
|
228
475
|
let fmt = rest.format === "pcm16" || rest.format === "float32" ? rest.format : null;
|
|
229
476
|
let done = Promise.resolve();
|
|
230
|
-
const
|
|
477
|
+
const handle = {
|
|
231
478
|
write(data) {
|
|
232
479
|
if (ended) throw new Error("[project-xiaochun] audio stream already ended");
|
|
233
480
|
if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
|
|
@@ -261,17 +508,23 @@ function createXiaochun(options) {
|
|
|
261
508
|
return done;
|
|
262
509
|
}
|
|
263
510
|
};
|
|
264
|
-
return
|
|
511
|
+
return handle;
|
|
265
512
|
};
|
|
266
513
|
function buildUrl() {
|
|
267
514
|
const u = new URL(srcUrl.href);
|
|
268
515
|
u.searchParams.set("host", window.location.origin);
|
|
269
516
|
if (transparent) u.searchParams.set("transparent", "1");
|
|
270
|
-
if (
|
|
517
|
+
if (uiParts.length) u.searchParams.set("ui", uiParts.join(","));
|
|
271
518
|
if (options.lang) u.searchParams.set("lang", options.lang);
|
|
519
|
+
if (uiAutoHide !== void 0) u.searchParams.set("uiAutoHide", String(uiAutoHide));
|
|
520
|
+
if (typeof camera.fov === "number") u.searchParams.set("cameraFov", String(camera.fov));
|
|
521
|
+
if (typeof camera.distance === "number") u.searchParams.set("cameraDistance", String(camera.distance));
|
|
522
|
+
if (typeof camera.height === "number") u.searchParams.set("cameraHeight", String(camera.height));
|
|
523
|
+
if (typeof camera.intro === "boolean") u.searchParams.set("cameraIntro", camera.intro ? "1" : "0");
|
|
272
524
|
if (options.heavy) u.searchParams.set("heavy", options.heavy);
|
|
273
|
-
if (options.controls) u.searchParams.set("controls", "
|
|
274
|
-
if (
|
|
525
|
+
if (options.controls === false) u.searchParams.set("controls", "0");
|
|
526
|
+
if (initialScene && isXcId(initialScene)) u.searchParams.set("scene", initialScene);
|
|
527
|
+
if (initialOutfit) u.searchParams.set("outfit", initialOutfit);
|
|
275
528
|
return u.href;
|
|
276
529
|
}
|
|
277
530
|
function activate() {
|
|
@@ -297,8 +550,9 @@ function createXiaochun(options) {
|
|
|
297
550
|
s.border = "0";
|
|
298
551
|
s.background = "transparent";
|
|
299
552
|
s.colorScheme = "normal";
|
|
300
|
-
s.borderRadius =
|
|
553
|
+
s.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
301
554
|
f.setAttribute("part", "iframe");
|
|
555
|
+
noSelect(s);
|
|
302
556
|
s.opacity = "0";
|
|
303
557
|
s.transition = "opacity .25s ease";
|
|
304
558
|
s.pointerEvents = passthrough ? "none" : "auto";
|
|
@@ -310,7 +564,7 @@ function createXiaochun(options) {
|
|
|
310
564
|
if (!port) emit("error", { code: "timeout", message: `no xc.ready from ${frameOrigin} within ${handshakeTimeout}ms` });
|
|
311
565
|
}, handshakeTimeout);
|
|
312
566
|
setupAutoPause();
|
|
313
|
-
|
|
567
|
+
if (passthrough) enablePassthrough();
|
|
314
568
|
}
|
|
315
569
|
const onWindowMessage = (ev) => {
|
|
316
570
|
if (destroyed || !iframe || ev.source !== iframe.contentWindow) return;
|
|
@@ -323,20 +577,39 @@ function createXiaochun(options) {
|
|
|
323
577
|
const ch = new MessageChannel();
|
|
324
578
|
port = ch.port1;
|
|
325
579
|
port.onmessage = onPortMessage;
|
|
580
|
+
caps = data.payload?.capabilities ?? null;
|
|
581
|
+
const gWant = wantGestures();
|
|
582
|
+
sentGestures = gWant;
|
|
583
|
+
const initConfig = {
|
|
584
|
+
...options.allowCustomModel ? { allowCustomModel: true } : {},
|
|
585
|
+
...gWant.move || gWant.resize ? { gestures: gWant } : {}
|
|
586
|
+
};
|
|
326
587
|
iframe.contentWindow.postMessage(
|
|
327
|
-
xcMessage("xc.init", { hostOrigin: window.location.origin }),
|
|
588
|
+
xcMessage("xc.init", { hostOrigin: window.location.origin, ...Object.keys(initConfig).length ? { config: initConfig } : {} }),
|
|
328
589
|
ev.origin,
|
|
329
590
|
[ch.port2]
|
|
330
591
|
);
|
|
592
|
+
warnGestureUnsupported();
|
|
331
593
|
if (handshakeTimer) {
|
|
332
594
|
clearTimeout(handshakeTimer);
|
|
333
595
|
handshakeTimer = null;
|
|
334
596
|
}
|
|
335
597
|
emit("handshake", data.payload);
|
|
336
|
-
|
|
598
|
+
resolveHandshake();
|
|
599
|
+
for (const m of queue.splice(0)) {
|
|
600
|
+
if (m.requires && !caps?.[m.requires]) unsupportedByFrame(m.type, m.id);
|
|
601
|
+
else port.postMessage(xcMessage(m.type, m.payload, m.id), m.transfer ?? []);
|
|
602
|
+
}
|
|
337
603
|
syncPause();
|
|
338
604
|
};
|
|
339
605
|
window.addEventListener("message", onWindowMessage);
|
|
606
|
+
function ack(id, value) {
|
|
607
|
+
const p = id ? pending.get(id) : void 0;
|
|
608
|
+
if (p && id) {
|
|
609
|
+
pending.delete(id);
|
|
610
|
+
p.resolve(value);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
340
613
|
function onPortMessage(ev) {
|
|
341
614
|
const data = ev.data;
|
|
342
615
|
if (!isXcEnvelope(data)) return;
|
|
@@ -347,6 +620,11 @@ function createXiaochun(options) {
|
|
|
347
620
|
case "xc.loaded":
|
|
348
621
|
if (!loaded) {
|
|
349
622
|
loaded = true;
|
|
623
|
+
if (options.prefetch && options.heavy === "eager" && caps?.prefetch) {
|
|
624
|
+
const ids = Array.isArray(options.prefetch) ? options.prefetch : void 0;
|
|
625
|
+
void api.prefetch(ids).catch(() => {
|
|
626
|
+
});
|
|
627
|
+
}
|
|
350
628
|
if (iframe) iframe.style.opacity = "1";
|
|
351
629
|
if (placeholderEl) {
|
|
352
630
|
const el = placeholderEl;
|
|
@@ -359,6 +637,33 @@ function createXiaochun(options) {
|
|
|
359
637
|
}
|
|
360
638
|
emit("ready", data.payload);
|
|
361
639
|
break;
|
|
640
|
+
case "xc.outfit-changed": {
|
|
641
|
+
const pl = data.payload;
|
|
642
|
+
if (!pl.noop) {
|
|
643
|
+
currentOutfit = pl.id;
|
|
644
|
+
emit("outfit-changed", pl);
|
|
645
|
+
if (!pl.initial && pl.id) writePrefs({ outfit: pl.id });
|
|
646
|
+
}
|
|
647
|
+
ack(data.id);
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case "xc.scene-changed": {
|
|
651
|
+
const pl = data.payload;
|
|
652
|
+
currentScene = pl.id;
|
|
653
|
+
if (pl.transparent !== transparent) applyTransparency(pl.transparent);
|
|
654
|
+
if (!pl.noop) {
|
|
655
|
+
emit("scene-changed", pl);
|
|
656
|
+
if (!pl.initial) writePrefs({ scene: pl.id });
|
|
657
|
+
}
|
|
658
|
+
ack(data.id);
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case "xc.lang-changed":
|
|
662
|
+
emit("lang-changed", data.payload);
|
|
663
|
+
break;
|
|
664
|
+
case "xc.prefetched":
|
|
665
|
+
ack(data.id, data.payload);
|
|
666
|
+
break;
|
|
362
667
|
case "xc.state":
|
|
363
668
|
emit("state", data.payload);
|
|
364
669
|
break;
|
|
@@ -383,9 +688,16 @@ function createXiaochun(options) {
|
|
|
383
688
|
}
|
|
384
689
|
break;
|
|
385
690
|
}
|
|
691
|
+
case "xc.gesture-move":
|
|
692
|
+
onGesture("move", data.payload);
|
|
693
|
+
break;
|
|
694
|
+
case "xc.gesture-resize":
|
|
695
|
+
onGesture("resize", data.payload);
|
|
696
|
+
break;
|
|
386
697
|
case "xc.error": {
|
|
387
698
|
const { code, message, command } = data.payload;
|
|
388
699
|
emit("error", { code, message, command });
|
|
700
|
+
if (code === "unknown_id" && (command === "outfit" || command === "scene")) writePrefs({ [command]: null });
|
|
389
701
|
const p = data.id ? pending.get(data.id) : void 0;
|
|
390
702
|
if (p && data.id) {
|
|
391
703
|
pending.delete(data.id);
|
|
@@ -442,16 +754,46 @@ function createXiaochun(options) {
|
|
|
442
754
|
port.postMessage(xcMessage("xc.pointer", { x, y }));
|
|
443
755
|
});
|
|
444
756
|
};
|
|
445
|
-
|
|
446
|
-
|
|
757
|
+
let passthroughActive = false;
|
|
758
|
+
function enablePassthrough() {
|
|
759
|
+
if (passthroughActive || !iframe) return;
|
|
760
|
+
passthroughActive = true;
|
|
447
761
|
window.addEventListener("pointermove", onHostPointer, { passive: true });
|
|
762
|
+
window.addEventListener("pointerdown", onHostPointer, { passive: true });
|
|
448
763
|
window.addEventListener("scroll", invalidateRect, { passive: true, capture: true });
|
|
449
764
|
window.addEventListener("resize", invalidateRect, { passive: true });
|
|
450
|
-
if (typeof ResizeObserver !== "undefined"
|
|
765
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
451
766
|
rectRo = new ResizeObserver(invalidateRect);
|
|
452
767
|
rectRo.observe(iframe);
|
|
453
768
|
}
|
|
454
769
|
}
|
|
770
|
+
function disablePassthrough() {
|
|
771
|
+
if (!passthroughActive) return;
|
|
772
|
+
passthroughActive = false;
|
|
773
|
+
window.removeEventListener("pointermove", onHostPointer);
|
|
774
|
+
window.removeEventListener("pointerdown", onHostPointer);
|
|
775
|
+
window.removeEventListener("scroll", invalidateRect, true);
|
|
776
|
+
window.removeEventListener("resize", invalidateRect);
|
|
777
|
+
rectRo?.disconnect();
|
|
778
|
+
rectRo = null;
|
|
779
|
+
if (pointerRaf) {
|
|
780
|
+
cancelAnimationFrame(pointerRaf);
|
|
781
|
+
pointerRaf = 0;
|
|
782
|
+
}
|
|
783
|
+
lastPtr = null;
|
|
784
|
+
}
|
|
785
|
+
function applyTransparency(next) {
|
|
786
|
+
transparent = next;
|
|
787
|
+
applyWrapperBg();
|
|
788
|
+
applyRadius();
|
|
789
|
+
passthrough = next ? options.passthrough ?? true : false;
|
|
790
|
+
lastHit = false;
|
|
791
|
+
lastPosted = null;
|
|
792
|
+
rectCache = null;
|
|
793
|
+
if (passthrough) enablePassthrough();
|
|
794
|
+
else disablePassthrough();
|
|
795
|
+
if (iframe) iframe.style.pointerEvents = passthrough ? "none" : "auto";
|
|
796
|
+
}
|
|
455
797
|
let pauseIo = null;
|
|
456
798
|
function setupAutoPause() {
|
|
457
799
|
if (options.autoPause === false || typeof IntersectionObserver === "undefined") return;
|
|
@@ -503,50 +845,177 @@ function createXiaochun(options) {
|
|
|
503
845
|
}, { rootMargin: `${options.lazyMargin ?? 200}px` });
|
|
504
846
|
io.observe(wrapper);
|
|
505
847
|
}
|
|
506
|
-
let
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (!drag) return;
|
|
537
|
-
ws.left = `${Math.min(Math.max(0, e.clientX - drag.dx), window.innerWidth - 40)}px`;
|
|
538
|
-
ws.top = `${Math.min(Math.max(0, e.clientY - drag.dy), window.innerHeight - 40)}px`;
|
|
848
|
+
let draggableOn = options.draggable === true;
|
|
849
|
+
let resizeLimits = normalizeResizable(options.resizable);
|
|
850
|
+
const gates = { move: new GestureGate(), resize: new GestureGate() };
|
|
851
|
+
let activeGesture = null;
|
|
852
|
+
let inlineShift = { x: 0, y: 0 };
|
|
853
|
+
let userMoved = false;
|
|
854
|
+
let sentGestures = null;
|
|
855
|
+
let warnedGesture = false;
|
|
856
|
+
const viewport = () => ({
|
|
857
|
+
width: document.documentElement.clientWidth || window.innerWidth,
|
|
858
|
+
height: document.documentElement.clientHeight || window.innerHeight
|
|
859
|
+
});
|
|
860
|
+
const readBox = () => {
|
|
861
|
+
const r = wrapper.getBoundingClientRect();
|
|
862
|
+
return { left: r.left, top: r.top, width: r.width, height: r.height };
|
|
863
|
+
};
|
|
864
|
+
const wantGestures = () => ({ move: draggableOn && caps?.gestures?.move === true, resize: resizeLimits !== null && caps?.gestures?.resize === true });
|
|
865
|
+
function applyBox(b, inlineBase, withSize) {
|
|
866
|
+
if (withSize) {
|
|
867
|
+
ws.width = `${b.width}px`;
|
|
868
|
+
ws.height = `${b.height}px`;
|
|
869
|
+
ws.maxWidth = "none";
|
|
870
|
+
}
|
|
871
|
+
if (position === "inline") {
|
|
872
|
+
const base = inlineBase ?? { left: b.left - inlineShift.x, top: b.top - inlineShift.y };
|
|
873
|
+
inlineShift = { x: b.left - base.left, y: b.top - base.top };
|
|
874
|
+
ws.setProperty("translate", `${inlineShift.x}px ${inlineShift.y}px`);
|
|
875
|
+
} else {
|
|
876
|
+
ws.left = `${b.left}px`;
|
|
877
|
+
ws.top = `${b.top}px`;
|
|
539
878
|
ws.right = "auto";
|
|
540
879
|
ws.bottom = "auto";
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
880
|
+
}
|
|
881
|
+
userMoved = true;
|
|
882
|
+
invalidateRect();
|
|
883
|
+
}
|
|
884
|
+
let selectionLock = null;
|
|
885
|
+
function lockSelection(on) {
|
|
886
|
+
const de = document.documentElement;
|
|
887
|
+
if (on && !selectionLock) {
|
|
888
|
+
selectionLock = { us: de.style.getPropertyValue("user-select"), wus: de.style.getPropertyValue("-webkit-user-select") };
|
|
889
|
+
de.style.setProperty("user-select", "none");
|
|
890
|
+
de.style.setProperty("-webkit-user-select", "none");
|
|
891
|
+
try {
|
|
892
|
+
window.getSelection()?.removeAllRanges();
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
} else if (!on && selectionLock) {
|
|
896
|
+
de.style.setProperty("user-select", selectionLock.us);
|
|
897
|
+
de.style.setProperty("-webkit-user-select", selectionLock.wus);
|
|
898
|
+
selectionLock = null;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function endActiveGesture(kind) {
|
|
902
|
+
if (!activeGesture || kind && activeGesture.kind !== kind) return;
|
|
903
|
+
gates[activeGesture.kind].close();
|
|
904
|
+
const b = readBox();
|
|
905
|
+
emit(activeGesture.kind, { phase: "end", ...b });
|
|
906
|
+
activeGesture = null;
|
|
907
|
+
lockSelection(false);
|
|
908
|
+
saveBox();
|
|
909
|
+
}
|
|
910
|
+
function onGesture(kind, raw) {
|
|
911
|
+
if (destroyed || !caps?.gestures) return;
|
|
912
|
+
if (kind === "move" ? !draggableOn : resizeLimits === null) return;
|
|
913
|
+
const p = parseGesturePayload(kind, raw);
|
|
914
|
+
if (!p) return;
|
|
915
|
+
if (p.phase === "start" ? activeGesture !== null : activeGesture?.kind !== kind) return;
|
|
916
|
+
if (!gates[kind].accept(p)) return;
|
|
917
|
+
if (p.phase === "start") {
|
|
918
|
+
const start = readBox();
|
|
919
|
+
activeGesture = {
|
|
920
|
+
kind,
|
|
921
|
+
start,
|
|
922
|
+
corner: p.corner,
|
|
923
|
+
inlineBase: position === "inline" ? { left: start.left - inlineShift.x, top: start.top - inlineShift.y } : null
|
|
924
|
+
};
|
|
925
|
+
lockSelection(true);
|
|
926
|
+
emit(kind, { phase: "start", ...start });
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const a = activeGesture;
|
|
930
|
+
const vp = viewport();
|
|
931
|
+
const next = kind === "move" ? moveBox(a.start, p.totalDx, p.totalDy, vp) : resizeBox(a.start, a.corner, p.totalDx, p.totalDy, resizeLimits, vp);
|
|
932
|
+
applyBox(next, a.inlineBase, kind === "resize");
|
|
933
|
+
emit(kind, { phase: p.phase, ...next });
|
|
934
|
+
if (p.phase === "end") {
|
|
935
|
+
activeGesture = null;
|
|
936
|
+
lockSelection(false);
|
|
937
|
+
saveBox();
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function syncGestures() {
|
|
941
|
+
if (destroyed) return;
|
|
942
|
+
const want = wantGestures();
|
|
943
|
+
if (!want.move) endActiveGesture("move");
|
|
944
|
+
if (!want.resize) endActiveGesture("resize");
|
|
945
|
+
if (!port || !caps) return;
|
|
946
|
+
warnGestureUnsupported();
|
|
947
|
+
if (sentGestures && sentGestures.move === want.move && sentGestures.resize === want.resize) return;
|
|
948
|
+
sentGestures = want;
|
|
949
|
+
if (caps.gestures) port.postMessage(xcMessage("xc.setConfig", { gestures: want }));
|
|
950
|
+
}
|
|
951
|
+
function warnGestureUnsupported() {
|
|
952
|
+
if (warnedGesture || !caps || caps.gestures) return;
|
|
953
|
+
const needMove = draggableOn;
|
|
954
|
+
if (!needMove && resizeLimits === null) return;
|
|
955
|
+
warnedGesture = true;
|
|
956
|
+
const what = [needMove ? "draggable" : "", resizeLimits !== null ? "resizable" : ""].filter(Boolean).join(" / ");
|
|
957
|
+
emit("error", { code: "unsupported", message: `${what} needs gesture support (capabilities.gestures), which this /embed deployment does not advertise (older version)`, command: "gestures" });
|
|
958
|
+
}
|
|
959
|
+
const onWindowResize = () => {
|
|
960
|
+
if (!userMoved || activeGesture || destroyed) return;
|
|
961
|
+
const cur = readBox();
|
|
962
|
+
const vp = viewport();
|
|
963
|
+
const fit = position === "inline" ? { ...fitBox({ ...cur, top: 0 }, { width: vp.width, height: Number.MAX_SAFE_INTEGER }), top: Math.max(cur.top, -window.scrollY) } : fitBox(cur, vp);
|
|
964
|
+
if (fit.left === cur.left && fit.top === cur.top && fit.width === cur.width && fit.height === cur.height) return;
|
|
965
|
+
applyBox(fit, position === "inline" ? { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y } : null, fit.width !== cur.width || fit.height !== cur.height);
|
|
966
|
+
};
|
|
967
|
+
window.addEventListener("resize", onWindowResize, { passive: true });
|
|
968
|
+
const boxKey = options.persistBox === true ? "xiaochun:box" : typeof options.persistBox === "string" && options.persistBox ? `xiaochun:box:${options.persistBox}` : null;
|
|
969
|
+
const saveBox = () => {
|
|
970
|
+
if (!boxKey) return;
|
|
971
|
+
try {
|
|
972
|
+
const b = readBox();
|
|
973
|
+
const at = position === "inline" ? inlineShift : { x: b.left, y: b.top };
|
|
974
|
+
window.localStorage.setItem(boxKey, JSON.stringify({ v: 1, mode: position, x: Math.round(at.x), y: Math.round(at.y), width: Math.round(b.width), height: Math.round(b.height) }));
|
|
975
|
+
} catch {
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
const loadBox = () => {
|
|
979
|
+
if (!boxKey) return null;
|
|
980
|
+
try {
|
|
981
|
+
const raw = window.localStorage.getItem(boxKey);
|
|
982
|
+
if (raw === null) return null;
|
|
983
|
+
let o = null;
|
|
984
|
+
try {
|
|
985
|
+
o = JSON.parse(raw);
|
|
986
|
+
} catch {
|
|
987
|
+
}
|
|
988
|
+
const ok = (v, lo, hi) => typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi;
|
|
989
|
+
if (o && o.v === 1 && o.mode === position && ok(o.x, -1e5, 1e5) && ok(o.y, -1e5, 1e5) && ok(o.width, 1, 1e5) && ok(o.height, 1, 1e5)) {
|
|
990
|
+
return { x: o.x, y: o.y, width: o.width, height: o.height };
|
|
991
|
+
}
|
|
992
|
+
window.localStorage.removeItem(boxKey);
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
return null;
|
|
996
|
+
};
|
|
997
|
+
function restoreBox() {
|
|
998
|
+
if (!boxKey || !draggableOn && resizeLimits === null) return;
|
|
999
|
+
const st = loadBox();
|
|
1000
|
+
if (!st) return;
|
|
1001
|
+
const vp = viewport();
|
|
1002
|
+
const lim = resizeLimits ?? DEFAULT_RESIZE_LIMITS;
|
|
1003
|
+
const cur = readBox();
|
|
1004
|
+
const useW = resizeLimits !== null && options.width === void 0;
|
|
1005
|
+
const useH = resizeLimits !== null && options.height === void 0;
|
|
1006
|
+
const clampN = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
|
|
1007
|
+
const width = useW ? clampN(st.width, lim.minWidth, Math.min(lim.maxWidth, vp.width)) : cur.width;
|
|
1008
|
+
const height = useH ? clampN(st.height, lim.minHeight, Math.min(lim.maxHeight, vp.height)) : cur.height;
|
|
1009
|
+
if (position === "inline") {
|
|
1010
|
+
const base = { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y };
|
|
1011
|
+
const left = clampN(base.left + st.x, 0, vp.width - width);
|
|
1012
|
+
const top = Math.max(base.top + st.y, -window.scrollY);
|
|
1013
|
+
applyBox({ left, top, width, height }, base, useW || useH);
|
|
1014
|
+
} else {
|
|
1015
|
+
applyBox(fitBox({ left: st.x, top: st.y, width, height }, vp), null, useW || useH);
|
|
1016
|
+
}
|
|
549
1017
|
}
|
|
1018
|
+
restoreBox();
|
|
550
1019
|
scheduleLazy();
|
|
551
1020
|
const api = {
|
|
552
1021
|
element: wrapper,
|
|
@@ -561,8 +1030,98 @@ function createXiaochun(options) {
|
|
|
561
1030
|
motion: (m) => send("xc.motion", typeof m === "string" ? m.startsWith("http") || m.startsWith("/") ? { url: m } : { name: m } : m),
|
|
562
1031
|
expression: (name) => send("xc.expression", { name }),
|
|
563
1032
|
lookAt: (x, y) => send("xc.lookAt", { x, y }),
|
|
564
|
-
|
|
1033
|
+
setOutfit: (id) => {
|
|
1034
|
+
if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid outfit id"));
|
|
1035
|
+
return send("xc.setOutfit", { id }, true, void 0, void 0, "outfits");
|
|
1036
|
+
},
|
|
1037
|
+
setScene: (id) => {
|
|
1038
|
+
if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid scene id"));
|
|
1039
|
+
return send("xc.setScene", { id }, true, void 0, void 0, "scenes");
|
|
1040
|
+
},
|
|
1041
|
+
prefetch: (ids) => {
|
|
1042
|
+
if (ids !== void 0 && (!Array.isArray(ids) || ids.some((x) => !isXcId(x)))) {
|
|
1043
|
+
return Promise.reject(new Error("[bad_request] prefetch ids must be an array of outfit ids"));
|
|
1044
|
+
}
|
|
1045
|
+
return send("xc.prefetch", ids ? { ids } : {}, true, void 0, void 0, "prefetch");
|
|
1046
|
+
},
|
|
1047
|
+
getOutfits: async () => {
|
|
1048
|
+
if (!iframe) activate();
|
|
1049
|
+
await handshaked;
|
|
1050
|
+
return caps?.outfits ? caps.outfits.slice() : [];
|
|
1051
|
+
},
|
|
1052
|
+
getScenes: async () => {
|
|
1053
|
+
if (!iframe) activate();
|
|
1054
|
+
await handshaked;
|
|
1055
|
+
return caps?.scenes ? caps.scenes.slice() : [];
|
|
1056
|
+
},
|
|
1057
|
+
get outfit() {
|
|
1058
|
+
return currentOutfit;
|
|
1059
|
+
},
|
|
1060
|
+
get scene() {
|
|
1061
|
+
return currentScene;
|
|
1062
|
+
},
|
|
1063
|
+
setModel: (m) => {
|
|
1064
|
+
const msg = typeof m === "string" ? /^(https?:)?\/|\.vrm/i.test(m) ? { url: m } : { outfit: m } : m;
|
|
1065
|
+
if (msg.url !== void 0 && msg.outfit === void 0 && !options.allowCustomModel) {
|
|
1066
|
+
const message = "custom model URLs are disabled; pass allowCustomModel: true to createXiaochun() (only for URLs you trust) or use setOutfit() with a built-in id";
|
|
1067
|
+
emit("error", { code: "unsupported", message, command: "xc.setModel" });
|
|
1068
|
+
return Promise.reject(new Error(`[unsupported] ${message}`));
|
|
1069
|
+
}
|
|
1070
|
+
return send("xc.setModel", msg);
|
|
1071
|
+
},
|
|
565
1072
|
setConfig: (cfg) => send("xc.setConfig", cfg),
|
|
1073
|
+
setSize(width, height) {
|
|
1074
|
+
if (destroyed) return;
|
|
1075
|
+
ws.width = css(width, defaultWidthCss(position !== "inline"));
|
|
1076
|
+
ws.height = css(height, defaultHeightCss(position !== "inline"));
|
|
1077
|
+
if (ws.maxWidth === "none") ws.maxWidth = "100%";
|
|
1078
|
+
invalidateRect();
|
|
1079
|
+
},
|
|
1080
|
+
getBox: () => readBox(),
|
|
1081
|
+
clearPersistedBox(opts) {
|
|
1082
|
+
if (boxKey) {
|
|
1083
|
+
try {
|
|
1084
|
+
window.localStorage.removeItem(boxKey);
|
|
1085
|
+
} catch {
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (destroyed || opts?.reset !== true) return;
|
|
1089
|
+
endActiveGesture();
|
|
1090
|
+
ws.width = css(options.width, defaultWidthCss(position !== "inline"));
|
|
1091
|
+
ws.height = css(options.height, defaultHeightCss(position !== "inline"));
|
|
1092
|
+
ws.maxWidth = "100%";
|
|
1093
|
+
if (position === "inline") {
|
|
1094
|
+
inlineShift = { x: 0, y: 0 };
|
|
1095
|
+
ws.removeProperty("translate");
|
|
1096
|
+
} else {
|
|
1097
|
+
ws.left = "";
|
|
1098
|
+
ws.top = "";
|
|
1099
|
+
ws.right = "";
|
|
1100
|
+
ws.bottom = "";
|
|
1101
|
+
if (position === "bottom-right") {
|
|
1102
|
+
ws.right = "var(--xc-offset-x, 16px)";
|
|
1103
|
+
ws.bottom = "var(--xc-offset-y, 16px)";
|
|
1104
|
+
}
|
|
1105
|
+
if (position === "bottom-left") {
|
|
1106
|
+
ws.left = "var(--xc-offset-x, 16px)";
|
|
1107
|
+
ws.bottom = "var(--xc-offset-y, 16px)";
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
userMoved = false;
|
|
1111
|
+
invalidateRect();
|
|
1112
|
+
},
|
|
1113
|
+
setBorderRadius(radius) {
|
|
1114
|
+
radiusOpt = radius;
|
|
1115
|
+
applyRadius();
|
|
1116
|
+
},
|
|
1117
|
+
setDraggable(on) {
|
|
1118
|
+
draggableOn = on === true;
|
|
1119
|
+
syncGestures();
|
|
1120
|
+
},
|
|
1121
|
+
setResizable(on) {
|
|
1122
|
+
resizeLimits = normalizeResizable(on);
|
|
1123
|
+
syncGestures();
|
|
1124
|
+
},
|
|
566
1125
|
startListening: () => send("xc.mic", { enabled: true }),
|
|
567
1126
|
stopListening: () => send("xc.mic", { enabled: false }),
|
|
568
1127
|
mic: (enabled) => send("xc.mic", { enabled }),
|
|
@@ -587,10 +1146,10 @@ function createXiaochun(options) {
|
|
|
587
1146
|
if (handshakeTimer) clearTimeout(handshakeTimer);
|
|
588
1147
|
if (pointerRaf) cancelAnimationFrame(pointerRaf);
|
|
589
1148
|
window.removeEventListener("message", onWindowMessage);
|
|
590
|
-
window.removeEventListener("
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1149
|
+
window.removeEventListener("resize", onWindowResize);
|
|
1150
|
+
lockSelection(false);
|
|
1151
|
+
disablePassthrough();
|
|
1152
|
+
resolveHandshake();
|
|
594
1153
|
try {
|
|
595
1154
|
port?.close();
|
|
596
1155
|
} catch {
|
|
@@ -629,7 +1188,12 @@ var EVENT_MAP = [
|
|
|
629
1188
|
["stt", "onStt"],
|
|
630
1189
|
["utterance", "onUtterance"],
|
|
631
1190
|
["hit-region", "onHitRegion"],
|
|
1191
|
+
["outfit-changed", "onOutfitChanged"],
|
|
1192
|
+
["scene-changed", "onSceneChanged"],
|
|
1193
|
+
["lang-changed", "onLangChanged"],
|
|
632
1194
|
["error", "onError"],
|
|
1195
|
+
["move", "onMove"],
|
|
1196
|
+
["resize", "onResize"],
|
|
633
1197
|
["destroy", "onDestroy"]
|
|
634
1198
|
];
|
|
635
1199
|
function useXiaochun(options = {}) {
|
|
@@ -644,8 +1208,8 @@ function useXiaochun(options = {}) {
|
|
|
644
1208
|
(0, import_react.useEffect)(() => {
|
|
645
1209
|
if (!el) return;
|
|
646
1210
|
const cur = live.current;
|
|
647
|
-
const { onHandshake: _a, onReady: _b, onProgress: _c, onState: _d, onStt: _e, onUtterance: _f, onHitRegion: _g, onError: _h, onDestroy: _i, paused: _p, mic: _m, allowedOrigins, ...create } = cur;
|
|
648
|
-
void [_a, _b, _c, _d, _e, _f, _g, _h, _i, _p, _m];
|
|
1211
|
+
const { onHandshake: _a, onReady: _b, onProgress: _c, onState: _d, onStt: _e, onUtterance: _f, onHitRegion: _g, onError: _h, onDestroy: _i, onOutfitChanged: _j, onSceneChanged: _k, onLangChanged: _l, onMove: _q, onResize: _r, paused: _p, mic: _m, allowedOrigins, ...create } = cur;
|
|
1212
|
+
void [_a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _p, _m, _q, _r];
|
|
649
1213
|
const inst = createXiaochun({ ...create, allowedOrigins: allowedOrigins?.slice(), container: el });
|
|
650
1214
|
const offs = [];
|
|
651
1215
|
for (const [ev, cbName] of EVENT_MAP) {
|
|
@@ -675,11 +1239,9 @@ function useXiaochun(options = {}) {
|
|
|
675
1239
|
o.lazyMargin,
|
|
676
1240
|
o.placeholder,
|
|
677
1241
|
o.transparent,
|
|
678
|
-
o.width,
|
|
679
|
-
o.height,
|
|
680
1242
|
o.position,
|
|
681
|
-
o.
|
|
682
|
-
o.
|
|
1243
|
+
JSON.stringify(o.ui ?? null),
|
|
1244
|
+
o.uiAutoHide,
|
|
683
1245
|
o.heavy,
|
|
684
1246
|
o.controls,
|
|
685
1247
|
o.autoPause,
|
|
@@ -687,9 +1249,16 @@ function useXiaochun(options = {}) {
|
|
|
687
1249
|
o.sandbox,
|
|
688
1250
|
o.handshakeTimeout,
|
|
689
1251
|
o.crossOriginIsolated,
|
|
690
|
-
o.zIndex
|
|
1252
|
+
o.zIndex,
|
|
1253
|
+
o.allowCustomModel,
|
|
1254
|
+
o.persist,
|
|
1255
|
+
o.persistBox,
|
|
1256
|
+
Array.isArray(o.prefetch) ? o.prefetch.join(",") : o.prefetch
|
|
691
1257
|
]);
|
|
692
|
-
const
|
|
1258
|
+
const wantOutfit = o.outfit ?? o.model;
|
|
1259
|
+
const cameraKey = JSON.stringify(o.camera ?? null);
|
|
1260
|
+
const first = (0, import_react.useRef)({ lang: o.lang, outfit: wantOutfit, scene: o.scene, width: o.width, height: o.height, camera: cameraKey });
|
|
1261
|
+
const resizableKey = JSON.stringify(o.resizable ?? false);
|
|
693
1262
|
(0, import_react.useEffect)(() => {
|
|
694
1263
|
if (!client || first.current.lang === o.lang) return;
|
|
695
1264
|
first.current.lang = o.lang;
|
|
@@ -697,11 +1266,39 @@ function useXiaochun(options = {}) {
|
|
|
697
1266
|
});
|
|
698
1267
|
}, [client, o.lang]);
|
|
699
1268
|
(0, import_react.useEffect)(() => {
|
|
700
|
-
if (!client || first.current.
|
|
701
|
-
first.current.
|
|
702
|
-
if (
|
|
1269
|
+
if (!client || first.current.outfit === wantOutfit) return;
|
|
1270
|
+
first.current.outfit = wantOutfit;
|
|
1271
|
+
if (wantOutfit) void (isXcId(wantOutfit) ? client.setOutfit(wantOutfit) : client.setModel(wantOutfit)).catch(() => {
|
|
703
1272
|
});
|
|
704
|
-
}, [client,
|
|
1273
|
+
}, [client, wantOutfit]);
|
|
1274
|
+
(0, import_react.useEffect)(() => {
|
|
1275
|
+
if (!client || first.current.camera === cameraKey) return;
|
|
1276
|
+
first.current.camera = cameraKey;
|
|
1277
|
+
const c = o.camera;
|
|
1278
|
+
void client.setConfig({ camera: { fov: c?.fov ?? null, distance: c?.distance ?? null, height: c?.height ?? null, intro: c?.intro ?? null } }).catch(() => {
|
|
1279
|
+
});
|
|
1280
|
+
}, [client, cameraKey]);
|
|
1281
|
+
(0, import_react.useEffect)(() => {
|
|
1282
|
+
if (!client || first.current.scene === o.scene) return;
|
|
1283
|
+
first.current.scene = o.scene;
|
|
1284
|
+
if (o.scene) void client.setScene(o.scene).catch(() => {
|
|
1285
|
+
});
|
|
1286
|
+
}, [client, o.scene]);
|
|
1287
|
+
(0, import_react.useEffect)(() => {
|
|
1288
|
+
if (!client || first.current.width === o.width && first.current.height === o.height) return;
|
|
1289
|
+
first.current.width = o.width;
|
|
1290
|
+
first.current.height = o.height;
|
|
1291
|
+
client.setSize(o.width, o.height);
|
|
1292
|
+
}, [client, o.width, o.height]);
|
|
1293
|
+
(0, import_react.useEffect)(() => {
|
|
1294
|
+
client?.setDraggable(o.draggable === true);
|
|
1295
|
+
}, [client, o.draggable]);
|
|
1296
|
+
(0, import_react.useEffect)(() => {
|
|
1297
|
+
client?.setBorderRadius(o.borderRadius);
|
|
1298
|
+
}, [client, o.borderRadius]);
|
|
1299
|
+
(0, import_react.useEffect)(() => {
|
|
1300
|
+
client?.setResizable(o.resizable ?? false);
|
|
1301
|
+
}, [client, resizableKey]);
|
|
705
1302
|
(0, import_react.useEffect)(() => {
|
|
706
1303
|
if (!client || o.paused === void 0) return;
|
|
707
1304
|
if (o.paused) client.pause();
|
|
@@ -741,6 +1338,12 @@ function XiaochunInner(props, ref) {
|
|
|
741
1338
|
motion: (m) => c()?.motion(m) ?? reject(),
|
|
742
1339
|
expression: (n) => c()?.expression(n) ?? reject(),
|
|
743
1340
|
lookAt: (x, y) => c()?.lookAt(x, y) ?? reject(),
|
|
1341
|
+
setOutfit: (id) => c()?.setOutfit(id) ?? reject(),
|
|
1342
|
+
setScene: (id) => c()?.setScene(id) ?? reject(),
|
|
1343
|
+
getOutfits: () => c()?.getOutfits() ?? reject(),
|
|
1344
|
+
getScenes: () => c()?.getScenes() ?? reject(),
|
|
1345
|
+
clearPersistedBox: (opts) => c()?.clearPersistedBox(opts),
|
|
1346
|
+
prefetch: (ids) => c()?.prefetch(ids) ?? reject(),
|
|
744
1347
|
setModel: (m) => c()?.setModel(m) ?? reject(),
|
|
745
1348
|
setConfig: (cfg) => c()?.setConfig(cfg) ?? reject(),
|
|
746
1349
|
startListening: () => c()?.startListening() ?? reject(),
|
|
@@ -760,8 +1363,8 @@ function XiaochunInner(props, ref) {
|
|
|
760
1363
|
}, []);
|
|
761
1364
|
const box = {
|
|
762
1365
|
...style,
|
|
763
|
-
width: css2(props.width,
|
|
764
|
-
height: css2(props.height,
|
|
1366
|
+
width: css2(props.width, defaultWidthCss()),
|
|
1367
|
+
height: css2(props.height, defaultHeightCss()),
|
|
765
1368
|
maxWidth: "100%",
|
|
766
1369
|
position: style?.position ?? "relative"
|
|
767
1370
|
};
|