@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/element.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
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -30,6 +30,70 @@ module.exports = __toCommonJS(element_exports);
|
|
|
30
30
|
var XC_PROTOCOL_VERSION = 1;
|
|
31
31
|
var XC_DEFAULT_ORIGIN = "https://xiaochun.firetable.tech";
|
|
32
32
|
var XC_DEFAULT_SRC = `${XC_DEFAULT_ORIGIN}/embed`;
|
|
33
|
+
var XC_WINDOW_CORNER_RADIUS = 20;
|
|
34
|
+
var XC_UI_PARTS = ["chat", "bubble", "outfit", "scene", "lang", "github"];
|
|
35
|
+
var XC_UI_PART_SET = new Set(XC_UI_PARTS);
|
|
36
|
+
var XC_UI_LEGACY_PARTS = ["chat", "bubble"];
|
|
37
|
+
function parseXcUiList(items) {
|
|
38
|
+
const seen = /* @__PURE__ */ new Set();
|
|
39
|
+
const unknown = [];
|
|
40
|
+
for (const it of items) {
|
|
41
|
+
const name = typeof it === "string" ? it.trim().toLowerCase() : "";
|
|
42
|
+
if (XC_UI_PART_SET.has(name)) seen.add(name);
|
|
43
|
+
else unknown.push(typeof it === "string" ? it.slice(0, 40) : String(typeof it));
|
|
44
|
+
}
|
|
45
|
+
return { parts: XC_UI_PARTS.filter((p) => seen.has(p)), unknown, legacy: false };
|
|
46
|
+
}
|
|
47
|
+
function normalizeXcUiOption(v) {
|
|
48
|
+
if (v === true) return { parts: [...XC_UI_LEGACY_PARTS], unknown: [], legacy: true };
|
|
49
|
+
if (Array.isArray(v)) return parseXcUiList(v);
|
|
50
|
+
return { parts: [], unknown: [], legacy: false };
|
|
51
|
+
}
|
|
52
|
+
function parseXcUiAutoHide(raw) {
|
|
53
|
+
if (raw === true || raw === false || raw === "transparent") return raw;
|
|
54
|
+
if (typeof raw !== "string") return void 0;
|
|
55
|
+
const v = raw.trim().toLowerCase();
|
|
56
|
+
if (v === "1" || v === "true") return true;
|
|
57
|
+
if (v === "0" || v === "false") return false;
|
|
58
|
+
if (v === "transparent") return "transparent";
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
var XC_CAMERA_RANGES = {
|
|
62
|
+
fov: [15, 60],
|
|
63
|
+
distance: [1, 15],
|
|
64
|
+
height: [-1, 1]
|
|
65
|
+
};
|
|
66
|
+
var XC_CAMERA_NUM_KEYS = ["fov", "distance", "height"];
|
|
67
|
+
var XC_CAMERA_KEYS = ["fov", "distance", "height", "intro"];
|
|
68
|
+
function normalizeXcCamera(raw) {
|
|
69
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return { ok: false, error: "camera must be an object" };
|
|
70
|
+
const src = raw;
|
|
71
|
+
for (const k of Object.keys(src)) {
|
|
72
|
+
if (!XC_CAMERA_KEYS.includes(k)) return { ok: false, error: `unknown camera key: ${k} (expected ${XC_CAMERA_KEYS.join(" | ")})` };
|
|
73
|
+
}
|
|
74
|
+
const out = {};
|
|
75
|
+
const clamped = [];
|
|
76
|
+
for (const k of XC_CAMERA_NUM_KEYS) {
|
|
77
|
+
const v = src[k];
|
|
78
|
+
if (v === void 0) continue;
|
|
79
|
+
if (v === null) {
|
|
80
|
+
out[k] = null;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return { ok: false, error: `camera.${k} must be a finite number or null` };
|
|
84
|
+
const [lo, hi] = XC_CAMERA_RANGES[k];
|
|
85
|
+
const c = Math.min(hi, Math.max(lo, v));
|
|
86
|
+
if (c !== v) clamped.push(k);
|
|
87
|
+
out[k] = c;
|
|
88
|
+
}
|
|
89
|
+
if (src.intro !== void 0) {
|
|
90
|
+
if (src.intro !== null && typeof src.intro !== "boolean") return { ok: false, error: "camera.intro must be a boolean or null" };
|
|
91
|
+
out.intro = src.intro;
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, camera: out, clamped };
|
|
94
|
+
}
|
|
95
|
+
var XC_ID_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
96
|
+
var isXcId = (v) => typeof v === "string" && XC_ID_RE.test(v);
|
|
33
97
|
function xcMessage(type, payload, id) {
|
|
34
98
|
const m = { type, v: XC_PROTOCOL_VERSION, payload };
|
|
35
99
|
if (id !== void 0) m.id = id;
|
|
@@ -51,11 +115,113 @@ function normalizeOrigin(input) {
|
|
|
51
115
|
}
|
|
52
116
|
}
|
|
53
117
|
|
|
118
|
+
// src/gesture-box.ts
|
|
119
|
+
var DEFAULT_RESIZE_LIMITS = Object.freeze({
|
|
120
|
+
minWidth: 120,
|
|
121
|
+
minHeight: 180,
|
|
122
|
+
maxWidth: Infinity,
|
|
123
|
+
maxHeight: Infinity
|
|
124
|
+
});
|
|
125
|
+
var finitePositive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
126
|
+
function normalizeResizable(v) {
|
|
127
|
+
if (!v) return null;
|
|
128
|
+
const o = v === true ? {} : v;
|
|
129
|
+
const minWidth = finitePositive(o.minWidth) ? o.minWidth : DEFAULT_RESIZE_LIMITS.minWidth;
|
|
130
|
+
const minHeight = finitePositive(o.minHeight) ? o.minHeight : DEFAULT_RESIZE_LIMITS.minHeight;
|
|
131
|
+
const maxWidth = finitePositive(o.maxWidth) ? Math.max(o.maxWidth, minWidth) : Infinity;
|
|
132
|
+
const maxHeight = finitePositive(o.maxHeight) ? Math.max(o.maxHeight, minHeight) : Infinity;
|
|
133
|
+
return { minWidth, minHeight, maxWidth, maxHeight };
|
|
134
|
+
}
|
|
135
|
+
var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
|
|
136
|
+
function moveBox(start, totalDx, totalDy, vp) {
|
|
137
|
+
return {
|
|
138
|
+
left: clamp(start.left + totalDx, 0, vp.width - start.width),
|
|
139
|
+
top: clamp(start.top + totalDy, 0, vp.height - start.height),
|
|
140
|
+
width: start.width,
|
|
141
|
+
height: start.height
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function resizeBox(start, corner, totalDx, totalDy, limits, vp) {
|
|
145
|
+
const west = corner === "NW" || corner === "SW";
|
|
146
|
+
const north = corner === "NW" || corner === "NE";
|
|
147
|
+
const right = start.left + start.width, bottom = start.top + start.height;
|
|
148
|
+
const wantW = west ? start.width - totalDx : start.width + totalDx;
|
|
149
|
+
const wantH = north ? start.height - totalDy : start.height + totalDy;
|
|
150
|
+
const roomW = west ? right : vp.width - start.left;
|
|
151
|
+
const roomH = north ? bottom : vp.height - start.top;
|
|
152
|
+
const width = clamp(wantW, limits.minWidth, Math.min(limits.maxWidth, roomW));
|
|
153
|
+
const height = clamp(wantH, limits.minHeight, Math.min(limits.maxHeight, roomH));
|
|
154
|
+
return { left: west ? right - width : start.left, top: north ? bottom - height : start.top, width, height };
|
|
155
|
+
}
|
|
156
|
+
function fitBox(b, vp) {
|
|
157
|
+
const width = Math.min(b.width, Math.max(1, vp.width)), height = Math.min(b.height, Math.max(1, vp.height));
|
|
158
|
+
return { left: clamp(b.left, 0, vp.width - width), top: clamp(b.top, 0, vp.height - height), width, height };
|
|
159
|
+
}
|
|
160
|
+
var CORNERS = /* @__PURE__ */ new Set(["NW", "NE", "SW", "SE"]);
|
|
161
|
+
var PHASES = /* @__PURE__ */ new Set(["start", "move", "end"]);
|
|
162
|
+
var REASONS = /* @__PURE__ */ new Set(["up", "cancel", "blur"]);
|
|
163
|
+
var MAX_GESTURE_DELTA = 2e4;
|
|
164
|
+
var delta = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= MAX_GESTURE_DELTA;
|
|
165
|
+
var counter = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
166
|
+
function parseGesturePayload(kind, raw) {
|
|
167
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
168
|
+
const p = raw;
|
|
169
|
+
if (!counter(p.gesture) || !counter(p.seq) || typeof p.phase !== "string" || !PHASES.has(p.phase)) return null;
|
|
170
|
+
if (!delta(p.dx) || !delta(p.dy) || !delta(p.totalDx) || !delta(p.totalDy)) return null;
|
|
171
|
+
if (p.reason !== void 0 && (typeof p.reason !== "string" || !REASONS.has(p.reason))) return null;
|
|
172
|
+
const out = { gesture: p.gesture, seq: p.seq, phase: p.phase, totalDx: p.totalDx, totalDy: p.totalDy };
|
|
173
|
+
if (kind === "resize") {
|
|
174
|
+
if (typeof p.corner !== "string" || !CORNERS.has(p.corner)) return null;
|
|
175
|
+
out.corner = p.corner;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
var GestureGate = class {
|
|
180
|
+
constructor() {
|
|
181
|
+
this.lastGesture = 0;
|
|
182
|
+
this.lastSeq = -1;
|
|
183
|
+
this.open = false;
|
|
184
|
+
}
|
|
185
|
+
get active() {
|
|
186
|
+
return this.open;
|
|
187
|
+
}
|
|
188
|
+
accept(p) {
|
|
189
|
+
if (p.phase === "start") {
|
|
190
|
+
if (p.gesture <= this.lastGesture || p.seq !== 0) return false;
|
|
191
|
+
this.lastGesture = p.gesture;
|
|
192
|
+
this.lastSeq = 0;
|
|
193
|
+
this.open = true;
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (!this.open || p.gesture !== this.lastGesture || p.seq <= this.lastSeq) return false;
|
|
197
|
+
this.lastSeq = p.seq;
|
|
198
|
+
if (p.phase === "end") this.open = false;
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
/** 宿主主动结束 (关闭手势 / 销毁): 之后同一手势的迟到消息被拒绝。 */
|
|
202
|
+
close() {
|
|
203
|
+
this.open = false;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
54
207
|
// src/client.ts
|
|
55
208
|
var FALLBACK_PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
56
209
|
'<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>'
|
|
57
210
|
);
|
|
211
|
+
var noSelect = (st) => {
|
|
212
|
+
st.setProperty("user-select", "none");
|
|
213
|
+
st.setProperty("-webkit-user-select", "none");
|
|
214
|
+
st.setProperty("-webkit-touch-callout", "none");
|
|
215
|
+
st.setProperty("-webkit-tap-highlight-color", "transparent");
|
|
216
|
+
};
|
|
58
217
|
var css = (v, d) => v === void 0 ? d : typeof v === "number" ? `${v}px` : v;
|
|
218
|
+
var XC_DEFAULT_WIDTH = 600;
|
|
219
|
+
var XC_DEFAULT_HEIGHT = 1080;
|
|
220
|
+
var defaultWidthCss = (floating = false) => floating ? `min(${XC_DEFAULT_WIDTH}px, calc(100vw - 2 * var(--xc-offset-x, 16px)))` : `min(${XC_DEFAULT_WIDTH}px, 100vw)`;
|
|
221
|
+
var defaultHeightCss = (floating = false) => {
|
|
222
|
+
const unit = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("height", "1svh") ? "svh" : "vh";
|
|
223
|
+
return floating ? `min(${XC_DEFAULT_HEIGHT}px, calc(100${unit} - 2 * var(--xc-offset-y, 16px)))` : `min(${XC_DEFAULT_HEIGHT}px, 100${unit})`;
|
|
224
|
+
};
|
|
59
225
|
function createXiaochun(options) {
|
|
60
226
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
61
227
|
throw new Error("[project-xiaochun] createXiaochun must run in a browser");
|
|
@@ -77,8 +243,56 @@ function createXiaochun(options) {
|
|
|
77
243
|
if (!n) throw new Error(`[project-xiaochun] invalid allowedOrigins entry: ${o} (wildcards are not allowed)`);
|
|
78
244
|
trusted.add(n);
|
|
79
245
|
}
|
|
80
|
-
const
|
|
81
|
-
const
|
|
246
|
+
const prefsKey = options.persist === "host" ? "xiaochun:prefs" : typeof options.persist === "string" && options.persist ? options.persist : null;
|
|
247
|
+
const readPrefs = () => {
|
|
248
|
+
if (!prefsKey) return {};
|
|
249
|
+
try {
|
|
250
|
+
const raw = JSON.parse(window.localStorage.getItem(prefsKey) ?? "null");
|
|
251
|
+
const out = {};
|
|
252
|
+
if (raw && typeof raw === "object") {
|
|
253
|
+
if (isXcId(raw.outfit)) out.outfit = raw.outfit;
|
|
254
|
+
if (isXcId(raw.scene)) out.scene = raw.scene;
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
} catch {
|
|
258
|
+
return {};
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const writePrefs = (patch) => {
|
|
262
|
+
if (!prefsKey) return;
|
|
263
|
+
try {
|
|
264
|
+
const next = { ...readPrefs() };
|
|
265
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
266
|
+
if (v) next[k] = v;
|
|
267
|
+
else delete next[k];
|
|
268
|
+
}
|
|
269
|
+
window.localStorage.setItem(prefsKey, JSON.stringify(next));
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const prefs = readPrefs();
|
|
274
|
+
if (options.model !== void 0 && options.outfit === void 0) {
|
|
275
|
+
console.warn('[project-xiaochun] option "model" is deprecated, use "outfit" (built-in outfit id).');
|
|
276
|
+
}
|
|
277
|
+
const uiParse = normalizeXcUiOption(options.ui);
|
|
278
|
+
const uiParts = uiParse.parts;
|
|
279
|
+
const uiAutoHide = parseXcUiAutoHide(options.uiAutoHide);
|
|
280
|
+
if (options.uiAutoHide !== void 0 && uiAutoHide === void 0) console.warn(`[project-xiaochun] ignoring invalid uiAutoHide: ${String(options.uiAutoHide)} (use true | false | 'transparent')`);
|
|
281
|
+
let camera = {};
|
|
282
|
+
if (options.camera !== void 0) {
|
|
283
|
+
const r = normalizeXcCamera(options.camera);
|
|
284
|
+
if (r.ok) {
|
|
285
|
+
camera = r.camera;
|
|
286
|
+
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")})`);
|
|
287
|
+
} else console.warn(`[project-xiaochun] ignoring invalid camera option: ${r.error}`);
|
|
288
|
+
}
|
|
289
|
+
if (uiParse.legacy) console.warn(`[project-xiaochun] option "ui: true" is deprecated, use an array of parts, e.g. ui: ['chat', 'bubble', 'outfit', 'scene'].`);
|
|
290
|
+
if (uiParse.unknown.length) console.warn(`[project-xiaochun] ignoring unknown ui part(s): ${uiParse.unknown.join(", ")} (known: ${XC_UI_PARTS.join(", ")})`);
|
|
291
|
+
const outfitOpt = options.outfit ?? (isXcId(options.model) ? options.model : void 0);
|
|
292
|
+
const initialOutfit = outfitOpt ?? prefs.outfit;
|
|
293
|
+
const initialScene = options.scene ?? prefs.scene ?? (options.transparent ? "transparent" : void 0);
|
|
294
|
+
let transparent = initialScene === "transparent" || initialScene !== "light" && initialScene !== "dark" && (options.transparent ?? false);
|
|
295
|
+
let passthrough = options.passthrough ?? transparent;
|
|
82
296
|
const lazy = options.lazy ?? true;
|
|
83
297
|
const position = options.position ?? "inline";
|
|
84
298
|
const handshakeTimeout = options.handshakeTimeout ?? 2e4;
|
|
@@ -86,14 +300,26 @@ function createXiaochun(options) {
|
|
|
86
300
|
wrapper.setAttribute("data-xiaochun", "");
|
|
87
301
|
const ws = wrapper.style;
|
|
88
302
|
ws.position = position === "inline" ? "relative" : "fixed";
|
|
89
|
-
ws.width = css(options.width, "
|
|
90
|
-
ws.height = css(options.height, "
|
|
303
|
+
ws.width = css(options.width, defaultWidthCss(position !== "inline"));
|
|
304
|
+
ws.height = css(options.height, defaultHeightCss(position !== "inline"));
|
|
91
305
|
ws.maxWidth = "100%";
|
|
92
306
|
ws.overflow = "visible";
|
|
93
307
|
ws.contain = "layout style";
|
|
94
|
-
ws
|
|
308
|
+
noSelect(ws);
|
|
95
309
|
ws.boxShadow = "var(--xc-shadow, none)";
|
|
96
|
-
|
|
310
|
+
const applyWrapperBg = () => {
|
|
311
|
+
ws.background = transparent ? "" : "var(--xc-bg, transparent)";
|
|
312
|
+
};
|
|
313
|
+
applyWrapperBg();
|
|
314
|
+
let radiusOpt = options.borderRadius;
|
|
315
|
+
const radiusValue = () => radiusOpt !== void 0 ? css(radiusOpt, "0px") : transparent ? "0px" : `${XC_WINDOW_CORNER_RADIUS}px`;
|
|
316
|
+
const applyRadius = () => {
|
|
317
|
+
const v = `var(--xc-radius, ${radiusValue()})`;
|
|
318
|
+
ws.borderRadius = v;
|
|
319
|
+
if (placeholderEl) placeholderEl.style.borderRadius = v;
|
|
320
|
+
if (iframe) iframe.style.borderRadius = v;
|
|
321
|
+
};
|
|
322
|
+
ws.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
97
323
|
if (position === "bottom-right") {
|
|
98
324
|
ws.right = "var(--xc-offset-x, 16px)";
|
|
99
325
|
ws.bottom = "var(--xc-offset-y, 16px)";
|
|
@@ -114,13 +340,13 @@ function createXiaochun(options) {
|
|
|
114
340
|
img.src = options.placeholder || FALLBACK_PLACEHOLDER;
|
|
115
341
|
img.alt = "Project XiaoChun";
|
|
116
342
|
img.decoding = "async";
|
|
117
|
-
img.width =
|
|
118
|
-
img.height =
|
|
343
|
+
img.width = XC_DEFAULT_WIDTH;
|
|
344
|
+
img.height = XC_DEFAULT_HEIGHT;
|
|
119
345
|
placeholderEl = img;
|
|
120
346
|
}
|
|
121
347
|
const ps = placeholderEl.style;
|
|
122
348
|
placeholderEl.setAttribute("part", "placeholder");
|
|
123
|
-
ps.borderRadius =
|
|
349
|
+
ps.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
124
350
|
ps.position = "absolute";
|
|
125
351
|
ps.inset = "0";
|
|
126
352
|
ps.width = "100%";
|
|
@@ -129,6 +355,7 @@ function createXiaochun(options) {
|
|
|
129
355
|
ps.transition = "opacity .25s ease";
|
|
130
356
|
ps.pointerEvents = "auto";
|
|
131
357
|
ps.cursor = lazy === false ? "default" : "pointer";
|
|
358
|
+
noSelect(ps);
|
|
132
359
|
placeholderEl.addEventListener("click", () => activate());
|
|
133
360
|
wrapper.appendChild(placeholderEl);
|
|
134
361
|
}
|
|
@@ -157,25 +384,45 @@ function createXiaochun(options) {
|
|
|
157
384
|
let autoPaused = false;
|
|
158
385
|
let loaded = false;
|
|
159
386
|
let lastHit = false;
|
|
387
|
+
let caps = null;
|
|
388
|
+
let currentOutfit = null;
|
|
389
|
+
let currentScene = null;
|
|
160
390
|
const queue = [];
|
|
161
391
|
const pending = /* @__PURE__ */ new Map();
|
|
162
392
|
let idSeq = 0;
|
|
163
393
|
let handshakeTimer = null;
|
|
164
394
|
let io = null;
|
|
165
395
|
let idleHandle = null;
|
|
166
|
-
|
|
396
|
+
let resolveHandshake;
|
|
397
|
+
const handshaked = new Promise((res) => {
|
|
398
|
+
resolveHandshake = res;
|
|
399
|
+
});
|
|
400
|
+
const failLocal = (type, id, code, message) => {
|
|
401
|
+
emit("error", { code, message, command: type });
|
|
402
|
+
const p = id ? pending.get(id) : void 0;
|
|
403
|
+
if (p && id) {
|
|
404
|
+
pending.delete(id);
|
|
405
|
+
p.reject(new Error(`[${code}] ${message}`));
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
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)`);
|
|
409
|
+
const postToFrame = (type, payload, id, transfer, requires) => {
|
|
167
410
|
if (!port) {
|
|
168
|
-
queue.push({ type, payload, id, transfer });
|
|
411
|
+
queue.push({ type, payload, id, transfer, requires });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (requires && !caps?.[requires]) {
|
|
415
|
+
unsupportedByFrame(type, id);
|
|
169
416
|
return;
|
|
170
417
|
}
|
|
171
418
|
port.postMessage(xcMessage(type, payload, id), transfer ?? []);
|
|
172
419
|
};
|
|
173
|
-
const send = (type, payload, wait = false, transfer, fixedId) => {
|
|
420
|
+
const send = (type, payload, wait = false, transfer, fixedId, requires) => {
|
|
174
421
|
if (destroyed) return Promise.reject(new Error("[project-xiaochun] instance destroyed"));
|
|
175
422
|
if (!iframe) activate();
|
|
176
423
|
const id = fixedId ?? `c${++idSeq}`;
|
|
177
424
|
const p = wait ? new Promise((resolve, reject) => pending.set(id, { resolve, reject, sawEnd: false })) : Promise.resolve();
|
|
178
|
-
postToFrame(type, payload, id, transfer);
|
|
425
|
+
postToFrame(type, payload, id, transfer, requires);
|
|
179
426
|
return p;
|
|
180
427
|
};
|
|
181
428
|
const toBuffer = (d) => d instanceof ArrayBuffer ? d : d.buffer.slice(d.byteOffset, d.byteOffset + d.byteLength);
|
|
@@ -225,7 +472,7 @@ function createXiaochun(options) {
|
|
|
225
472
|
let ended = false;
|
|
226
473
|
let fmt = rest.format === "pcm16" || rest.format === "float32" ? rest.format : null;
|
|
227
474
|
let done = Promise.resolve();
|
|
228
|
-
const
|
|
475
|
+
const handle = {
|
|
229
476
|
write(data) {
|
|
230
477
|
if (ended) throw new Error("[project-xiaochun] audio stream already ended");
|
|
231
478
|
if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
|
|
@@ -259,17 +506,23 @@ function createXiaochun(options) {
|
|
|
259
506
|
return done;
|
|
260
507
|
}
|
|
261
508
|
};
|
|
262
|
-
return
|
|
509
|
+
return handle;
|
|
263
510
|
};
|
|
264
511
|
function buildUrl() {
|
|
265
512
|
const u = new URL(srcUrl.href);
|
|
266
513
|
u.searchParams.set("host", window.location.origin);
|
|
267
514
|
if (transparent) u.searchParams.set("transparent", "1");
|
|
268
|
-
if (
|
|
515
|
+
if (uiParts.length) u.searchParams.set("ui", uiParts.join(","));
|
|
269
516
|
if (options.lang) u.searchParams.set("lang", options.lang);
|
|
517
|
+
if (uiAutoHide !== void 0) u.searchParams.set("uiAutoHide", String(uiAutoHide));
|
|
518
|
+
if (typeof camera.fov === "number") u.searchParams.set("cameraFov", String(camera.fov));
|
|
519
|
+
if (typeof camera.distance === "number") u.searchParams.set("cameraDistance", String(camera.distance));
|
|
520
|
+
if (typeof camera.height === "number") u.searchParams.set("cameraHeight", String(camera.height));
|
|
521
|
+
if (typeof camera.intro === "boolean") u.searchParams.set("cameraIntro", camera.intro ? "1" : "0");
|
|
270
522
|
if (options.heavy) u.searchParams.set("heavy", options.heavy);
|
|
271
|
-
if (options.controls) u.searchParams.set("controls", "
|
|
272
|
-
if (
|
|
523
|
+
if (options.controls === false) u.searchParams.set("controls", "0");
|
|
524
|
+
if (initialScene && isXcId(initialScene)) u.searchParams.set("scene", initialScene);
|
|
525
|
+
if (initialOutfit) u.searchParams.set("outfit", initialOutfit);
|
|
273
526
|
return u.href;
|
|
274
527
|
}
|
|
275
528
|
function activate() {
|
|
@@ -295,8 +548,9 @@ function createXiaochun(options) {
|
|
|
295
548
|
s.border = "0";
|
|
296
549
|
s.background = "transparent";
|
|
297
550
|
s.colorScheme = "normal";
|
|
298
|
-
s.borderRadius =
|
|
551
|
+
s.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
299
552
|
f.setAttribute("part", "iframe");
|
|
553
|
+
noSelect(s);
|
|
300
554
|
s.opacity = "0";
|
|
301
555
|
s.transition = "opacity .25s ease";
|
|
302
556
|
s.pointerEvents = passthrough ? "none" : "auto";
|
|
@@ -308,7 +562,7 @@ function createXiaochun(options) {
|
|
|
308
562
|
if (!port) emit("error", { code: "timeout", message: `no xc.ready from ${frameOrigin} within ${handshakeTimeout}ms` });
|
|
309
563
|
}, handshakeTimeout);
|
|
310
564
|
setupAutoPause();
|
|
311
|
-
|
|
565
|
+
if (passthrough) enablePassthrough();
|
|
312
566
|
}
|
|
313
567
|
const onWindowMessage = (ev) => {
|
|
314
568
|
if (destroyed || !iframe || ev.source !== iframe.contentWindow) return;
|
|
@@ -321,20 +575,39 @@ function createXiaochun(options) {
|
|
|
321
575
|
const ch = new MessageChannel();
|
|
322
576
|
port = ch.port1;
|
|
323
577
|
port.onmessage = onPortMessage;
|
|
578
|
+
caps = data.payload?.capabilities ?? null;
|
|
579
|
+
const gWant = wantGestures();
|
|
580
|
+
sentGestures = gWant;
|
|
581
|
+
const initConfig = {
|
|
582
|
+
...options.allowCustomModel ? { allowCustomModel: true } : {},
|
|
583
|
+
...gWant.move || gWant.resize ? { gestures: gWant } : {}
|
|
584
|
+
};
|
|
324
585
|
iframe.contentWindow.postMessage(
|
|
325
|
-
xcMessage("xc.init", { hostOrigin: window.location.origin }),
|
|
586
|
+
xcMessage("xc.init", { hostOrigin: window.location.origin, ...Object.keys(initConfig).length ? { config: initConfig } : {} }),
|
|
326
587
|
ev.origin,
|
|
327
588
|
[ch.port2]
|
|
328
589
|
);
|
|
590
|
+
warnGestureUnsupported();
|
|
329
591
|
if (handshakeTimer) {
|
|
330
592
|
clearTimeout(handshakeTimer);
|
|
331
593
|
handshakeTimer = null;
|
|
332
594
|
}
|
|
333
595
|
emit("handshake", data.payload);
|
|
334
|
-
|
|
596
|
+
resolveHandshake();
|
|
597
|
+
for (const m of queue.splice(0)) {
|
|
598
|
+
if (m.requires && !caps?.[m.requires]) unsupportedByFrame(m.type, m.id);
|
|
599
|
+
else port.postMessage(xcMessage(m.type, m.payload, m.id), m.transfer ?? []);
|
|
600
|
+
}
|
|
335
601
|
syncPause();
|
|
336
602
|
};
|
|
337
603
|
window.addEventListener("message", onWindowMessage);
|
|
604
|
+
function ack(id, value) {
|
|
605
|
+
const p = id ? pending.get(id) : void 0;
|
|
606
|
+
if (p && id) {
|
|
607
|
+
pending.delete(id);
|
|
608
|
+
p.resolve(value);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
338
611
|
function onPortMessage(ev) {
|
|
339
612
|
const data = ev.data;
|
|
340
613
|
if (!isXcEnvelope(data)) return;
|
|
@@ -345,6 +618,11 @@ function createXiaochun(options) {
|
|
|
345
618
|
case "xc.loaded":
|
|
346
619
|
if (!loaded) {
|
|
347
620
|
loaded = true;
|
|
621
|
+
if (options.prefetch && options.heavy === "eager" && caps?.prefetch) {
|
|
622
|
+
const ids = Array.isArray(options.prefetch) ? options.prefetch : void 0;
|
|
623
|
+
void api.prefetch(ids).catch(() => {
|
|
624
|
+
});
|
|
625
|
+
}
|
|
348
626
|
if (iframe) iframe.style.opacity = "1";
|
|
349
627
|
if (placeholderEl) {
|
|
350
628
|
const el = placeholderEl;
|
|
@@ -357,6 +635,33 @@ function createXiaochun(options) {
|
|
|
357
635
|
}
|
|
358
636
|
emit("ready", data.payload);
|
|
359
637
|
break;
|
|
638
|
+
case "xc.outfit-changed": {
|
|
639
|
+
const pl = data.payload;
|
|
640
|
+
if (!pl.noop) {
|
|
641
|
+
currentOutfit = pl.id;
|
|
642
|
+
emit("outfit-changed", pl);
|
|
643
|
+
if (!pl.initial && pl.id) writePrefs({ outfit: pl.id });
|
|
644
|
+
}
|
|
645
|
+
ack(data.id);
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
case "xc.scene-changed": {
|
|
649
|
+
const pl = data.payload;
|
|
650
|
+
currentScene = pl.id;
|
|
651
|
+
if (pl.transparent !== transparent) applyTransparency(pl.transparent);
|
|
652
|
+
if (!pl.noop) {
|
|
653
|
+
emit("scene-changed", pl);
|
|
654
|
+
if (!pl.initial) writePrefs({ scene: pl.id });
|
|
655
|
+
}
|
|
656
|
+
ack(data.id);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
case "xc.lang-changed":
|
|
660
|
+
emit("lang-changed", data.payload);
|
|
661
|
+
break;
|
|
662
|
+
case "xc.prefetched":
|
|
663
|
+
ack(data.id, data.payload);
|
|
664
|
+
break;
|
|
360
665
|
case "xc.state":
|
|
361
666
|
emit("state", data.payload);
|
|
362
667
|
break;
|
|
@@ -381,9 +686,16 @@ function createXiaochun(options) {
|
|
|
381
686
|
}
|
|
382
687
|
break;
|
|
383
688
|
}
|
|
689
|
+
case "xc.gesture-move":
|
|
690
|
+
onGesture("move", data.payload);
|
|
691
|
+
break;
|
|
692
|
+
case "xc.gesture-resize":
|
|
693
|
+
onGesture("resize", data.payload);
|
|
694
|
+
break;
|
|
384
695
|
case "xc.error": {
|
|
385
696
|
const { code, message, command } = data.payload;
|
|
386
697
|
emit("error", { code, message, command });
|
|
698
|
+
if (code === "unknown_id" && (command === "outfit" || command === "scene")) writePrefs({ [command]: null });
|
|
387
699
|
const p = data.id ? pending.get(data.id) : void 0;
|
|
388
700
|
if (p && data.id) {
|
|
389
701
|
pending.delete(data.id);
|
|
@@ -440,16 +752,46 @@ function createXiaochun(options) {
|
|
|
440
752
|
port.postMessage(xcMessage("xc.pointer", { x, y }));
|
|
441
753
|
});
|
|
442
754
|
};
|
|
443
|
-
|
|
444
|
-
|
|
755
|
+
let passthroughActive = false;
|
|
756
|
+
function enablePassthrough() {
|
|
757
|
+
if (passthroughActive || !iframe) return;
|
|
758
|
+
passthroughActive = true;
|
|
445
759
|
window.addEventListener("pointermove", onHostPointer, { passive: true });
|
|
760
|
+
window.addEventListener("pointerdown", onHostPointer, { passive: true });
|
|
446
761
|
window.addEventListener("scroll", invalidateRect, { passive: true, capture: true });
|
|
447
762
|
window.addEventListener("resize", invalidateRect, { passive: true });
|
|
448
|
-
if (typeof ResizeObserver !== "undefined"
|
|
763
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
449
764
|
rectRo = new ResizeObserver(invalidateRect);
|
|
450
765
|
rectRo.observe(iframe);
|
|
451
766
|
}
|
|
452
767
|
}
|
|
768
|
+
function disablePassthrough() {
|
|
769
|
+
if (!passthroughActive) return;
|
|
770
|
+
passthroughActive = false;
|
|
771
|
+
window.removeEventListener("pointermove", onHostPointer);
|
|
772
|
+
window.removeEventListener("pointerdown", onHostPointer);
|
|
773
|
+
window.removeEventListener("scroll", invalidateRect, true);
|
|
774
|
+
window.removeEventListener("resize", invalidateRect);
|
|
775
|
+
rectRo?.disconnect();
|
|
776
|
+
rectRo = null;
|
|
777
|
+
if (pointerRaf) {
|
|
778
|
+
cancelAnimationFrame(pointerRaf);
|
|
779
|
+
pointerRaf = 0;
|
|
780
|
+
}
|
|
781
|
+
lastPtr = null;
|
|
782
|
+
}
|
|
783
|
+
function applyTransparency(next) {
|
|
784
|
+
transparent = next;
|
|
785
|
+
applyWrapperBg();
|
|
786
|
+
applyRadius();
|
|
787
|
+
passthrough = next ? options.passthrough ?? true : false;
|
|
788
|
+
lastHit = false;
|
|
789
|
+
lastPosted = null;
|
|
790
|
+
rectCache = null;
|
|
791
|
+
if (passthrough) enablePassthrough();
|
|
792
|
+
else disablePassthrough();
|
|
793
|
+
if (iframe) iframe.style.pointerEvents = passthrough ? "none" : "auto";
|
|
794
|
+
}
|
|
453
795
|
let pauseIo = null;
|
|
454
796
|
function setupAutoPause() {
|
|
455
797
|
if (options.autoPause === false || typeof IntersectionObserver === "undefined") return;
|
|
@@ -501,50 +843,177 @@ function createXiaochun(options) {
|
|
|
501
843
|
}, { rootMargin: `${options.lazyMargin ?? 200}px` });
|
|
502
844
|
io.observe(wrapper);
|
|
503
845
|
}
|
|
504
|
-
let
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
if (!drag) return;
|
|
535
|
-
ws.left = `${Math.min(Math.max(0, e.clientX - drag.dx), window.innerWidth - 40)}px`;
|
|
536
|
-
ws.top = `${Math.min(Math.max(0, e.clientY - drag.dy), window.innerHeight - 40)}px`;
|
|
846
|
+
let draggableOn = options.draggable === true;
|
|
847
|
+
let resizeLimits = normalizeResizable(options.resizable);
|
|
848
|
+
const gates = { move: new GestureGate(), resize: new GestureGate() };
|
|
849
|
+
let activeGesture = null;
|
|
850
|
+
let inlineShift = { x: 0, y: 0 };
|
|
851
|
+
let userMoved = false;
|
|
852
|
+
let sentGestures = null;
|
|
853
|
+
let warnedGesture = false;
|
|
854
|
+
const viewport = () => ({
|
|
855
|
+
width: document.documentElement.clientWidth || window.innerWidth,
|
|
856
|
+
height: document.documentElement.clientHeight || window.innerHeight
|
|
857
|
+
});
|
|
858
|
+
const readBox = () => {
|
|
859
|
+
const r = wrapper.getBoundingClientRect();
|
|
860
|
+
return { left: r.left, top: r.top, width: r.width, height: r.height };
|
|
861
|
+
};
|
|
862
|
+
const wantGestures = () => ({ move: draggableOn && caps?.gestures?.move === true, resize: resizeLimits !== null && caps?.gestures?.resize === true });
|
|
863
|
+
function applyBox(b, inlineBase, withSize) {
|
|
864
|
+
if (withSize) {
|
|
865
|
+
ws.width = `${b.width}px`;
|
|
866
|
+
ws.height = `${b.height}px`;
|
|
867
|
+
ws.maxWidth = "none";
|
|
868
|
+
}
|
|
869
|
+
if (position === "inline") {
|
|
870
|
+
const base = inlineBase ?? { left: b.left - inlineShift.x, top: b.top - inlineShift.y };
|
|
871
|
+
inlineShift = { x: b.left - base.left, y: b.top - base.top };
|
|
872
|
+
ws.setProperty("translate", `${inlineShift.x}px ${inlineShift.y}px`);
|
|
873
|
+
} else {
|
|
874
|
+
ws.left = `${b.left}px`;
|
|
875
|
+
ws.top = `${b.top}px`;
|
|
537
876
|
ws.right = "auto";
|
|
538
877
|
ws.bottom = "auto";
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
878
|
+
}
|
|
879
|
+
userMoved = true;
|
|
880
|
+
invalidateRect();
|
|
881
|
+
}
|
|
882
|
+
let selectionLock = null;
|
|
883
|
+
function lockSelection(on) {
|
|
884
|
+
const de = document.documentElement;
|
|
885
|
+
if (on && !selectionLock) {
|
|
886
|
+
selectionLock = { us: de.style.getPropertyValue("user-select"), wus: de.style.getPropertyValue("-webkit-user-select") };
|
|
887
|
+
de.style.setProperty("user-select", "none");
|
|
888
|
+
de.style.setProperty("-webkit-user-select", "none");
|
|
889
|
+
try {
|
|
890
|
+
window.getSelection()?.removeAllRanges();
|
|
891
|
+
} catch {
|
|
892
|
+
}
|
|
893
|
+
} else if (!on && selectionLock) {
|
|
894
|
+
de.style.setProperty("user-select", selectionLock.us);
|
|
895
|
+
de.style.setProperty("-webkit-user-select", selectionLock.wus);
|
|
896
|
+
selectionLock = null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function endActiveGesture(kind) {
|
|
900
|
+
if (!activeGesture || kind && activeGesture.kind !== kind) return;
|
|
901
|
+
gates[activeGesture.kind].close();
|
|
902
|
+
const b = readBox();
|
|
903
|
+
emit(activeGesture.kind, { phase: "end", ...b });
|
|
904
|
+
activeGesture = null;
|
|
905
|
+
lockSelection(false);
|
|
906
|
+
saveBox();
|
|
907
|
+
}
|
|
908
|
+
function onGesture(kind, raw) {
|
|
909
|
+
if (destroyed || !caps?.gestures) return;
|
|
910
|
+
if (kind === "move" ? !draggableOn : resizeLimits === null) return;
|
|
911
|
+
const p = parseGesturePayload(kind, raw);
|
|
912
|
+
if (!p) return;
|
|
913
|
+
if (p.phase === "start" ? activeGesture !== null : activeGesture?.kind !== kind) return;
|
|
914
|
+
if (!gates[kind].accept(p)) return;
|
|
915
|
+
if (p.phase === "start") {
|
|
916
|
+
const start = readBox();
|
|
917
|
+
activeGesture = {
|
|
918
|
+
kind,
|
|
919
|
+
start,
|
|
920
|
+
corner: p.corner,
|
|
921
|
+
inlineBase: position === "inline" ? { left: start.left - inlineShift.x, top: start.top - inlineShift.y } : null
|
|
922
|
+
};
|
|
923
|
+
lockSelection(true);
|
|
924
|
+
emit(kind, { phase: "start", ...start });
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const a = activeGesture;
|
|
928
|
+
const vp = viewport();
|
|
929
|
+
const next = kind === "move" ? moveBox(a.start, p.totalDx, p.totalDy, vp) : resizeBox(a.start, a.corner, p.totalDx, p.totalDy, resizeLimits, vp);
|
|
930
|
+
applyBox(next, a.inlineBase, kind === "resize");
|
|
931
|
+
emit(kind, { phase: p.phase, ...next });
|
|
932
|
+
if (p.phase === "end") {
|
|
933
|
+
activeGesture = null;
|
|
934
|
+
lockSelection(false);
|
|
935
|
+
saveBox();
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function syncGestures() {
|
|
939
|
+
if (destroyed) return;
|
|
940
|
+
const want = wantGestures();
|
|
941
|
+
if (!want.move) endActiveGesture("move");
|
|
942
|
+
if (!want.resize) endActiveGesture("resize");
|
|
943
|
+
if (!port || !caps) return;
|
|
944
|
+
warnGestureUnsupported();
|
|
945
|
+
if (sentGestures && sentGestures.move === want.move && sentGestures.resize === want.resize) return;
|
|
946
|
+
sentGestures = want;
|
|
947
|
+
if (caps.gestures) port.postMessage(xcMessage("xc.setConfig", { gestures: want }));
|
|
948
|
+
}
|
|
949
|
+
function warnGestureUnsupported() {
|
|
950
|
+
if (warnedGesture || !caps || caps.gestures) return;
|
|
951
|
+
const needMove = draggableOn;
|
|
952
|
+
if (!needMove && resizeLimits === null) return;
|
|
953
|
+
warnedGesture = true;
|
|
954
|
+
const what = [needMove ? "draggable" : "", resizeLimits !== null ? "resizable" : ""].filter(Boolean).join(" / ");
|
|
955
|
+
emit("error", { code: "unsupported", message: `${what} needs gesture support (capabilities.gestures), which this /embed deployment does not advertise (older version)`, command: "gestures" });
|
|
547
956
|
}
|
|
957
|
+
const onWindowResize = () => {
|
|
958
|
+
if (!userMoved || activeGesture || destroyed) return;
|
|
959
|
+
const cur = readBox();
|
|
960
|
+
const vp = viewport();
|
|
961
|
+
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);
|
|
962
|
+
if (fit.left === cur.left && fit.top === cur.top && fit.width === cur.width && fit.height === cur.height) return;
|
|
963
|
+
applyBox(fit, position === "inline" ? { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y } : null, fit.width !== cur.width || fit.height !== cur.height);
|
|
964
|
+
};
|
|
965
|
+
window.addEventListener("resize", onWindowResize, { passive: true });
|
|
966
|
+
const boxKey = options.persistBox === true ? "xiaochun:box" : typeof options.persistBox === "string" && options.persistBox ? `xiaochun:box:${options.persistBox}` : null;
|
|
967
|
+
const saveBox = () => {
|
|
968
|
+
if (!boxKey) return;
|
|
969
|
+
try {
|
|
970
|
+
const b = readBox();
|
|
971
|
+
const at = position === "inline" ? inlineShift : { x: b.left, y: b.top };
|
|
972
|
+
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) }));
|
|
973
|
+
} catch {
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
const loadBox = () => {
|
|
977
|
+
if (!boxKey) return null;
|
|
978
|
+
try {
|
|
979
|
+
const raw = window.localStorage.getItem(boxKey);
|
|
980
|
+
if (raw === null) return null;
|
|
981
|
+
let o = null;
|
|
982
|
+
try {
|
|
983
|
+
o = JSON.parse(raw);
|
|
984
|
+
} catch {
|
|
985
|
+
}
|
|
986
|
+
const ok = (v, lo, hi) => typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi;
|
|
987
|
+
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)) {
|
|
988
|
+
return { x: o.x, y: o.y, width: o.width, height: o.height };
|
|
989
|
+
}
|
|
990
|
+
window.localStorage.removeItem(boxKey);
|
|
991
|
+
} catch {
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
};
|
|
995
|
+
function restoreBox() {
|
|
996
|
+
if (!boxKey || !draggableOn && resizeLimits === null) return;
|
|
997
|
+
const st = loadBox();
|
|
998
|
+
if (!st) return;
|
|
999
|
+
const vp = viewport();
|
|
1000
|
+
const lim = resizeLimits ?? DEFAULT_RESIZE_LIMITS;
|
|
1001
|
+
const cur = readBox();
|
|
1002
|
+
const useW = resizeLimits !== null && options.width === void 0;
|
|
1003
|
+
const useH = resizeLimits !== null && options.height === void 0;
|
|
1004
|
+
const clampN = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
|
|
1005
|
+
const width = useW ? clampN(st.width, lim.minWidth, Math.min(lim.maxWidth, vp.width)) : cur.width;
|
|
1006
|
+
const height = useH ? clampN(st.height, lim.minHeight, Math.min(lim.maxHeight, vp.height)) : cur.height;
|
|
1007
|
+
if (position === "inline") {
|
|
1008
|
+
const base = { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y };
|
|
1009
|
+
const left = clampN(base.left + st.x, 0, vp.width - width);
|
|
1010
|
+
const top = Math.max(base.top + st.y, -window.scrollY);
|
|
1011
|
+
applyBox({ left, top, width, height }, base, useW || useH);
|
|
1012
|
+
} else {
|
|
1013
|
+
applyBox(fitBox({ left: st.x, top: st.y, width, height }, vp), null, useW || useH);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
restoreBox();
|
|
548
1017
|
scheduleLazy();
|
|
549
1018
|
const api = {
|
|
550
1019
|
element: wrapper,
|
|
@@ -559,8 +1028,98 @@ function createXiaochun(options) {
|
|
|
559
1028
|
motion: (m) => send("xc.motion", typeof m === "string" ? m.startsWith("http") || m.startsWith("/") ? { url: m } : { name: m } : m),
|
|
560
1029
|
expression: (name) => send("xc.expression", { name }),
|
|
561
1030
|
lookAt: (x, y) => send("xc.lookAt", { x, y }),
|
|
562
|
-
|
|
1031
|
+
setOutfit: (id) => {
|
|
1032
|
+
if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid outfit id"));
|
|
1033
|
+
return send("xc.setOutfit", { id }, true, void 0, void 0, "outfits");
|
|
1034
|
+
},
|
|
1035
|
+
setScene: (id) => {
|
|
1036
|
+
if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid scene id"));
|
|
1037
|
+
return send("xc.setScene", { id }, true, void 0, void 0, "scenes");
|
|
1038
|
+
},
|
|
1039
|
+
prefetch: (ids) => {
|
|
1040
|
+
if (ids !== void 0 && (!Array.isArray(ids) || ids.some((x) => !isXcId(x)))) {
|
|
1041
|
+
return Promise.reject(new Error("[bad_request] prefetch ids must be an array of outfit ids"));
|
|
1042
|
+
}
|
|
1043
|
+
return send("xc.prefetch", ids ? { ids } : {}, true, void 0, void 0, "prefetch");
|
|
1044
|
+
},
|
|
1045
|
+
getOutfits: async () => {
|
|
1046
|
+
if (!iframe) activate();
|
|
1047
|
+
await handshaked;
|
|
1048
|
+
return caps?.outfits ? caps.outfits.slice() : [];
|
|
1049
|
+
},
|
|
1050
|
+
getScenes: async () => {
|
|
1051
|
+
if (!iframe) activate();
|
|
1052
|
+
await handshaked;
|
|
1053
|
+
return caps?.scenes ? caps.scenes.slice() : [];
|
|
1054
|
+
},
|
|
1055
|
+
get outfit() {
|
|
1056
|
+
return currentOutfit;
|
|
1057
|
+
},
|
|
1058
|
+
get scene() {
|
|
1059
|
+
return currentScene;
|
|
1060
|
+
},
|
|
1061
|
+
setModel: (m) => {
|
|
1062
|
+
const msg = typeof m === "string" ? /^(https?:)?\/|\.vrm/i.test(m) ? { url: m } : { outfit: m } : m;
|
|
1063
|
+
if (msg.url !== void 0 && msg.outfit === void 0 && !options.allowCustomModel) {
|
|
1064
|
+
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";
|
|
1065
|
+
emit("error", { code: "unsupported", message, command: "xc.setModel" });
|
|
1066
|
+
return Promise.reject(new Error(`[unsupported] ${message}`));
|
|
1067
|
+
}
|
|
1068
|
+
return send("xc.setModel", msg);
|
|
1069
|
+
},
|
|
563
1070
|
setConfig: (cfg) => send("xc.setConfig", cfg),
|
|
1071
|
+
setSize(width, height) {
|
|
1072
|
+
if (destroyed) return;
|
|
1073
|
+
ws.width = css(width, defaultWidthCss(position !== "inline"));
|
|
1074
|
+
ws.height = css(height, defaultHeightCss(position !== "inline"));
|
|
1075
|
+
if (ws.maxWidth === "none") ws.maxWidth = "100%";
|
|
1076
|
+
invalidateRect();
|
|
1077
|
+
},
|
|
1078
|
+
getBox: () => readBox(),
|
|
1079
|
+
clearPersistedBox(opts) {
|
|
1080
|
+
if (boxKey) {
|
|
1081
|
+
try {
|
|
1082
|
+
window.localStorage.removeItem(boxKey);
|
|
1083
|
+
} catch {
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (destroyed || opts?.reset !== true) return;
|
|
1087
|
+
endActiveGesture();
|
|
1088
|
+
ws.width = css(options.width, defaultWidthCss(position !== "inline"));
|
|
1089
|
+
ws.height = css(options.height, defaultHeightCss(position !== "inline"));
|
|
1090
|
+
ws.maxWidth = "100%";
|
|
1091
|
+
if (position === "inline") {
|
|
1092
|
+
inlineShift = { x: 0, y: 0 };
|
|
1093
|
+
ws.removeProperty("translate");
|
|
1094
|
+
} else {
|
|
1095
|
+
ws.left = "";
|
|
1096
|
+
ws.top = "";
|
|
1097
|
+
ws.right = "";
|
|
1098
|
+
ws.bottom = "";
|
|
1099
|
+
if (position === "bottom-right") {
|
|
1100
|
+
ws.right = "var(--xc-offset-x, 16px)";
|
|
1101
|
+
ws.bottom = "var(--xc-offset-y, 16px)";
|
|
1102
|
+
}
|
|
1103
|
+
if (position === "bottom-left") {
|
|
1104
|
+
ws.left = "var(--xc-offset-x, 16px)";
|
|
1105
|
+
ws.bottom = "var(--xc-offset-y, 16px)";
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
userMoved = false;
|
|
1109
|
+
invalidateRect();
|
|
1110
|
+
},
|
|
1111
|
+
setBorderRadius(radius) {
|
|
1112
|
+
radiusOpt = radius;
|
|
1113
|
+
applyRadius();
|
|
1114
|
+
},
|
|
1115
|
+
setDraggable(on) {
|
|
1116
|
+
draggableOn = on === true;
|
|
1117
|
+
syncGestures();
|
|
1118
|
+
},
|
|
1119
|
+
setResizable(on) {
|
|
1120
|
+
resizeLimits = normalizeResizable(on);
|
|
1121
|
+
syncGestures();
|
|
1122
|
+
},
|
|
564
1123
|
startListening: () => send("xc.mic", { enabled: true }),
|
|
565
1124
|
stopListening: () => send("xc.mic", { enabled: false }),
|
|
566
1125
|
mic: (enabled) => send("xc.mic", { enabled }),
|
|
@@ -585,10 +1144,10 @@ function createXiaochun(options) {
|
|
|
585
1144
|
if (handshakeTimer) clearTimeout(handshakeTimer);
|
|
586
1145
|
if (pointerRaf) cancelAnimationFrame(pointerRaf);
|
|
587
1146
|
window.removeEventListener("message", onWindowMessage);
|
|
588
|
-
window.removeEventListener("
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1147
|
+
window.removeEventListener("resize", onWindowResize);
|
|
1148
|
+
lockSelection(false);
|
|
1149
|
+
disablePassthrough();
|
|
1150
|
+
resolveHandshake();
|
|
592
1151
|
try {
|
|
593
1152
|
port?.close();
|
|
594
1153
|
} catch {
|
|
@@ -621,11 +1180,17 @@ function createXiaochun(options) {
|
|
|
621
1180
|
// src/avatar-element.ts
|
|
622
1181
|
var OBSERVED = [
|
|
623
1182
|
"src",
|
|
1183
|
+
"outfit",
|
|
1184
|
+
"scene",
|
|
624
1185
|
"model",
|
|
625
1186
|
"lang",
|
|
626
1187
|
"mic",
|
|
627
1188
|
"transparent",
|
|
628
1189
|
"draggable",
|
|
1190
|
+
"resizable",
|
|
1191
|
+
"min-size",
|
|
1192
|
+
"max-size",
|
|
1193
|
+
"border-radius",
|
|
629
1194
|
"position",
|
|
630
1195
|
"size",
|
|
631
1196
|
"lazy",
|
|
@@ -633,23 +1198,66 @@ var OBSERVED = [
|
|
|
633
1198
|
"placeholder",
|
|
634
1199
|
"heavy",
|
|
635
1200
|
"ui",
|
|
1201
|
+
"ui-autohide",
|
|
1202
|
+
"camera-fov",
|
|
1203
|
+
"camera-distance",
|
|
1204
|
+
"camera-height",
|
|
1205
|
+
"camera-intro",
|
|
636
1206
|
"controls",
|
|
637
1207
|
"allowed-origins",
|
|
638
|
-
"cross-origin-isolated"
|
|
1208
|
+
"cross-origin-isolated",
|
|
1209
|
+
"persist",
|
|
1210
|
+
"persist-box",
|
|
1211
|
+
"allow-custom-model",
|
|
1212
|
+
"prefetch"
|
|
639
1213
|
];
|
|
640
|
-
var REBUILD = /* @__PURE__ */ new Set(["src", "
|
|
1214
|
+
var REBUILD = /* @__PURE__ */ new Set(["src", "position", "lazy", "placeholder", "heavy", "ui", "ui-autohide", "controls", "allowed-origins", "transparent", "cross-origin-isolated", "persist", "persist-box", "allow-custom-model", "prefetch"]);
|
|
1215
|
+
function radiusAttr(v) {
|
|
1216
|
+
if (v === null || v.trim() === "") return void 0;
|
|
1217
|
+
const t = v.trim();
|
|
1218
|
+
return /^\d+(\.\d+)?$/.test(t) ? Number(t) : t;
|
|
1219
|
+
}
|
|
641
1220
|
function boolAttr(el, name, dflt) {
|
|
642
1221
|
const v = el.getAttribute(name);
|
|
643
1222
|
if (v === null) return dflt;
|
|
644
1223
|
return v !== "false";
|
|
645
1224
|
}
|
|
1225
|
+
function prefetchAttr(v) {
|
|
1226
|
+
if (v === null || v === "false") return false;
|
|
1227
|
+
if (v === "" || v === "true") return true;
|
|
1228
|
+
return v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1229
|
+
}
|
|
1230
|
+
function persistBoxAttr(v) {
|
|
1231
|
+
if (v === null || v === "false" || v === "0") return void 0;
|
|
1232
|
+
if (v === "" || v === "true" || v === "1") return true;
|
|
1233
|
+
return v;
|
|
1234
|
+
}
|
|
1235
|
+
function uiAttr(v) {
|
|
1236
|
+
if (v === null || v === "false" || v === "0") return void 0;
|
|
1237
|
+
if (v === "" || v === "true" || v === "1") return true;
|
|
1238
|
+
return v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1239
|
+
}
|
|
1240
|
+
function cameraAttrs(el) {
|
|
1241
|
+
const num = (name) => {
|
|
1242
|
+
const v = el.getAttribute(name);
|
|
1243
|
+
if (v === null || v.trim() === "") return null;
|
|
1244
|
+
const n = Number(v);
|
|
1245
|
+
return Number.isFinite(n) ? n : null;
|
|
1246
|
+
};
|
|
1247
|
+
const i = el.getAttribute("camera-intro");
|
|
1248
|
+
return { fov: num("camera-fov"), distance: num("camera-distance"), height: num("camera-height"), intro: i === null ? null : i !== "false" && i !== "0" };
|
|
1249
|
+
}
|
|
1250
|
+
function parsePx(v) {
|
|
1251
|
+
const m = v?.trim().match(/^(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)$/i);
|
|
1252
|
+
return m ? { w: Number(m[1]), h: Number(m[2]) } : void 0;
|
|
1253
|
+
}
|
|
646
1254
|
function parseSize(v) {
|
|
647
|
-
if (!v) return { width:
|
|
1255
|
+
if (!v) return { width: void 0, height: void 0 };
|
|
648
1256
|
const m = v.trim().split(/x/i);
|
|
649
1257
|
const px = (s) => /^\d+(\.\d+)?$/.test(s) ? `${s}px` : s;
|
|
650
1258
|
if (m.length >= 2) return { width: px(m[0]), height: px(m[1]) };
|
|
651
1259
|
const w = parseFloat(m[0]);
|
|
652
|
-
return Number.isFinite(w) && /^\d+(\.\d+)?$/.test(m[0]) ? { width: `${w}px`, height: `${Math.round(w * 1.5)}px` } : { width: m[0], height:
|
|
1260
|
+
return Number.isFinite(w) && /^\d+(\.\d+)?$/.test(m[0]) ? { width: `${w}px`, height: `${Math.round(w * 1.5)}px` } : { width: m[0], height: void 0 };
|
|
653
1261
|
}
|
|
654
1262
|
function createElementClass() {
|
|
655
1263
|
class XiaochunAvatarElementImpl extends HTMLElement {
|
|
@@ -690,10 +1298,22 @@ function createElementClass() {
|
|
|
690
1298
|
return;
|
|
691
1299
|
}
|
|
692
1300
|
const c = this.client;
|
|
693
|
-
if (name === "
|
|
1301
|
+
if ((name === "outfit" || name === "model") && newV) {
|
|
1302
|
+
const want = name === "model" && this.hasAttribute("outfit") ? null : newV;
|
|
1303
|
+
if (want) void (isXcId(want) ? c.setOutfit(want) : c.setModel(want)).catch(() => {
|
|
1304
|
+
});
|
|
1305
|
+
} else if (name === "scene" && newV) void c.setScene(newV).catch(() => {
|
|
694
1306
|
});
|
|
695
1307
|
else if (name === "lang" && newV) void c.setConfig({ lang: newV }).catch(() => {
|
|
696
1308
|
});
|
|
1309
|
+
else if (name === "size") {
|
|
1310
|
+
const { width, height } = parseSize(newV);
|
|
1311
|
+
c.setSize(width, height);
|
|
1312
|
+
} else if (name === "border-radius") c.setBorderRadius(radiusAttr(newV));
|
|
1313
|
+
else if (name === "draggable") c.setDraggable(boolAttr(this, "draggable", false));
|
|
1314
|
+
else if (name === "resizable" || name === "min-size" || name === "max-size") c.setResizable(this.resizableOption());
|
|
1315
|
+
else if (name.startsWith("camera-")) void c.setConfig({ camera: cameraAttrs(this) }).catch(() => {
|
|
1316
|
+
});
|
|
697
1317
|
else if (name === "mic") void c.mic(boolAttr(this, "mic", false)).catch(() => {
|
|
698
1318
|
});
|
|
699
1319
|
else if (name === "paused") {
|
|
@@ -720,11 +1340,20 @@ function createElementClass() {
|
|
|
720
1340
|
height,
|
|
721
1341
|
position: this.getAttribute("position") ?? "inline",
|
|
722
1342
|
draggable: boolAttr(this, "draggable", false),
|
|
1343
|
+
resizable: this.resizableOption(),
|
|
1344
|
+
borderRadius: radiusAttr(this.getAttribute("border-radius")),
|
|
723
1345
|
lang: this.getAttribute("lang") ?? void 0,
|
|
724
|
-
|
|
1346
|
+
outfit: this.getAttribute("outfit") ?? this.getAttribute("model") ?? void 0,
|
|
1347
|
+
scene: this.getAttribute("scene") ?? void 0,
|
|
1348
|
+
persist: this.getAttribute("persist") || void 0,
|
|
1349
|
+
persistBox: persistBoxAttr(this.getAttribute("persist-box")),
|
|
1350
|
+
allowCustomModel: boolAttr(this, "allow-custom-model", false),
|
|
1351
|
+
prefetch: prefetchAttr(this.getAttribute("prefetch")),
|
|
725
1352
|
heavy: this.getAttribute("heavy") ?? void 0,
|
|
726
|
-
ui:
|
|
727
|
-
|
|
1353
|
+
ui: uiAttr(this.getAttribute("ui")),
|
|
1354
|
+
uiAutoHide: parseXcUiAutoHide(this.getAttribute("ui-autohide")),
|
|
1355
|
+
camera: cameraAttrs(this),
|
|
1356
|
+
controls: boolAttr(this, "controls", true),
|
|
728
1357
|
crossOriginIsolated: boolAttr(this, "cross-origin-isolated", false)
|
|
729
1358
|
});
|
|
730
1359
|
this.client = c;
|
|
@@ -735,10 +1364,21 @@ function createElementClass() {
|
|
|
735
1364
|
fwd("stt", "xc-stt");
|
|
736
1365
|
fwd("utterance", "xc-utterance");
|
|
737
1366
|
fwd("error", "xc-error");
|
|
1367
|
+
fwd("outfit-changed", "xc-outfit-changed");
|
|
1368
|
+
fwd("scene-changed", "xc-scene-changed");
|
|
1369
|
+
fwd("lang-changed", "xc-lang-changed");
|
|
1370
|
+
fwd("move", "xc-move");
|
|
1371
|
+
fwd("resize", "xc-resize");
|
|
738
1372
|
if (boolAttr(this, "paused", false)) c.pause();
|
|
739
1373
|
if (boolAttr(this, "mic", false)) void c.ready.then(() => c.mic(true)).catch(() => {
|
|
740
1374
|
});
|
|
741
1375
|
}
|
|
1376
|
+
/** resizable / min-size / max-size 属性 → SDK 的 resizable 选项 (缺省 / "false" = 关)。 */
|
|
1377
|
+
resizableOption() {
|
|
1378
|
+
if (!boolAttr(this, "resizable", false)) return false;
|
|
1379
|
+
const min = parsePx(this.getAttribute("min-size")), max = parsePx(this.getAttribute("max-size"));
|
|
1380
|
+
return { minWidth: min?.w, minHeight: min?.h, maxWidth: max?.w, maxHeight: max?.h };
|
|
1381
|
+
}
|
|
742
1382
|
say(text, opts) {
|
|
743
1383
|
return this.requireClient().say(text, opts);
|
|
744
1384
|
}
|
|
@@ -754,6 +1394,24 @@ function createElementClass() {
|
|
|
754
1394
|
expression(name) {
|
|
755
1395
|
return this.requireClient().expression(name);
|
|
756
1396
|
}
|
|
1397
|
+
setOutfit(id) {
|
|
1398
|
+
return this.requireClient().setOutfit(id);
|
|
1399
|
+
}
|
|
1400
|
+
setScene(id) {
|
|
1401
|
+
return this.requireClient().setScene(id);
|
|
1402
|
+
}
|
|
1403
|
+
getOutfits() {
|
|
1404
|
+
return this.requireClient().getOutfits();
|
|
1405
|
+
}
|
|
1406
|
+
getScenes() {
|
|
1407
|
+
return this.requireClient().getScenes();
|
|
1408
|
+
}
|
|
1409
|
+
clearPersistedBox(opts) {
|
|
1410
|
+
this.client?.clearPersistedBox(opts);
|
|
1411
|
+
}
|
|
1412
|
+
prefetch(ids) {
|
|
1413
|
+
return this.requireClient().prefetch(ids);
|
|
1414
|
+
}
|
|
757
1415
|
destroy() {
|
|
758
1416
|
this.client?.destroy();
|
|
759
1417
|
this.client = null;
|