@firetable/project-xiaochun 0.1.13 → 0.1.15
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 +144 -18
- package/README.md +144 -18
- package/dist/avatar-element.d.ts +23 -6
- package/dist/chunks/{chunk-UB2HFUBS.js → chunk-KKRZ3SUD.js} +50 -2
- package/dist/chunks/chunk-KKRZ3SUD.js.map +7 -0
- package/dist/chunks/chunk-KQSKHPVF.js +957 -0
- package/dist/chunks/chunk-KQSKHPVF.js.map +7 -0
- package/dist/chunks/{chunk-NKR4JG3C.js → chunk-T6EHFEEZ.js} +83 -9
- package/dist/chunks/chunk-T6EHFEEZ.js.map +7 -0
- package/dist/client.d.ts +134 -6
- package/dist/element.cjs +572 -71
- 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 +599 -71
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +22 -4
- 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 +49 -1
- package/dist/protocol.cjs.map +2 -2
- package/dist/protocol.d.ts +180 -9
- package/dist/protocol.js +20 -2
- package/dist/react.cjs +540 -77
- package/dist/react.cjs.map +4 -4
- package/dist/react.d.ts +17 -1
- package/dist/react.js +50 -15
- package/dist/react.js.map +2 -2
- package/package.json +5 -3
- package/dist/chunks/chunk-NKR4JG3C.js.map +0 -7
- package/dist/chunks/chunk-UB2HFUBS.js.map +0 -7
- package/dist/chunks/chunk-XE2YKXRA.js +0 -544
- package/dist/chunks/chunk-XE2YKXRA.js.map +0 -7
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.15 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -25,18 +25,27 @@ __export(src_exports, {
|
|
|
25
25
|
XC_DEFAULT_SRC: () => XC_DEFAULT_SRC,
|
|
26
26
|
XC_FRAME_TO_HOST: () => XC_FRAME_TO_HOST,
|
|
27
27
|
XC_HOST_TO_FRAME: () => XC_HOST_TO_FRAME,
|
|
28
|
+
XC_ID_RE: () => XC_ID_RE,
|
|
28
29
|
XC_IMPLEMENTED_COMMANDS: () => XC_IMPLEMENTED_COMMANDS,
|
|
29
30
|
XC_PROTOCOL_MAPPING: () => XC_PROTOCOL_MAPPING,
|
|
30
31
|
XC_PROTOCOL_VERSION: () => XC_PROTOCOL_VERSION,
|
|
32
|
+
XC_UI_LEGACY_PARTS: () => XC_UI_LEGACY_PARTS,
|
|
33
|
+
XC_UI_PARTS: () => XC_UI_PARTS,
|
|
31
34
|
XC_UNSUPPORTED_COMMANDS: () => XC_UNSUPPORTED_COMMANDS,
|
|
35
|
+
XC_WINDOW_CORNER_RADIUS: () => XC_WINDOW_CORNER_RADIUS,
|
|
32
36
|
XIAOCHUN_ELEMENT_TAG: () => XIAOCHUN_ELEMENT_TAG,
|
|
33
37
|
XIAOCHUN_PROTOCOL_SCHEME: () => XIAOCHUN_PROTOCOL_SCHEME,
|
|
34
38
|
createXiaochun: () => createXiaochun,
|
|
35
39
|
defineXiaochunElement: () => defineXiaochunElement,
|
|
36
40
|
isXcEnvelope: () => isXcEnvelope,
|
|
41
|
+
isXcId: () => isXcId,
|
|
37
42
|
normalizeOrigin: () => normalizeOrigin,
|
|
43
|
+
normalizeXcUiOption: () => normalizeXcUiOption,
|
|
38
44
|
parseProtocolUrl: () => parseProtocolUrl,
|
|
45
|
+
parseXcUiList: () => parseXcUiList,
|
|
46
|
+
parseXcUiParam: () => parseXcUiParam,
|
|
39
47
|
toProtocolUrl: () => toProtocolUrl,
|
|
48
|
+
xcHasOwn: () => xcHasOwn,
|
|
40
49
|
xcMessage: () => xcMessage
|
|
41
50
|
});
|
|
42
51
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -56,6 +65,9 @@ var XC_HOST_TO_FRAME = [
|
|
|
56
65
|
"xc.lookAt",
|
|
57
66
|
"xc.pointer",
|
|
58
67
|
"xc.setModel",
|
|
68
|
+
"xc.setOutfit",
|
|
69
|
+
"xc.setScene",
|
|
70
|
+
"xc.prefetch",
|
|
59
71
|
"xc.setConfig",
|
|
60
72
|
"xc.mic",
|
|
61
73
|
"xc.pause",
|
|
@@ -66,10 +78,15 @@ var XC_FRAME_TO_HOST = [
|
|
|
66
78
|
"xc.ready",
|
|
67
79
|
"xc.load.progress",
|
|
68
80
|
"xc.loaded",
|
|
81
|
+
"xc.outfit-changed",
|
|
82
|
+
"xc.scene-changed",
|
|
83
|
+
"xc.prefetched",
|
|
69
84
|
"xc.state",
|
|
70
85
|
"xc.stt",
|
|
71
86
|
"xc.utterance",
|
|
72
87
|
"xc.hit-region",
|
|
88
|
+
"xc.gesture-move",
|
|
89
|
+
"xc.gesture-resize",
|
|
73
90
|
"xc.error"
|
|
74
91
|
];
|
|
75
92
|
var XC_IMPLEMENTED_COMMANDS = [
|
|
@@ -81,6 +98,9 @@ var XC_IMPLEMENTED_COMMANDS = [
|
|
|
81
98
|
"xc.expression",
|
|
82
99
|
"xc.pointer",
|
|
83
100
|
"xc.setModel",
|
|
101
|
+
"xc.setOutfit",
|
|
102
|
+
"xc.setScene",
|
|
103
|
+
"xc.prefetch",
|
|
84
104
|
"xc.setConfig",
|
|
85
105
|
"xc.mic",
|
|
86
106
|
"xc.pause",
|
|
@@ -95,6 +115,34 @@ var XC_PROTOCOL_MAPPING = [
|
|
|
95
115
|
{ xc: "xc.audio", note: "source \u4E3A ArrayBuffer / Blob", action: "audio", url: null },
|
|
96
116
|
{ xc: "xc.audio.chunk / xc.audio.end", note: "\u6D41\u5F0F PCM", action: "audio", url: null }
|
|
97
117
|
];
|
|
118
|
+
var XC_WINDOW_CORNER_RADIUS = 20;
|
|
119
|
+
var XC_UI_PARTS = ["chat", "bubble", "outfit", "scene"];
|
|
120
|
+
var XC_UI_PART_SET = new Set(XC_UI_PARTS);
|
|
121
|
+
var XC_UI_LEGACY_PARTS = ["chat", "bubble"];
|
|
122
|
+
function parseXcUiList(items) {
|
|
123
|
+
const seen = /* @__PURE__ */ new Set();
|
|
124
|
+
const unknown = [];
|
|
125
|
+
for (const it of items) {
|
|
126
|
+
const name = typeof it === "string" ? it.trim().toLowerCase() : "";
|
|
127
|
+
if (XC_UI_PART_SET.has(name)) seen.add(name);
|
|
128
|
+
else unknown.push(typeof it === "string" ? it.slice(0, 40) : String(typeof it));
|
|
129
|
+
}
|
|
130
|
+
return { parts: XC_UI_PARTS.filter((p) => seen.has(p)), unknown, legacy: false };
|
|
131
|
+
}
|
|
132
|
+
function parseXcUiParam(raw) {
|
|
133
|
+
const v = (raw ?? "").trim().toLowerCase();
|
|
134
|
+
if (v === "" || v === "0" || v === "false") return { parts: [], unknown: [], legacy: false };
|
|
135
|
+
if (v === "1" || v === "true") return { parts: [...XC_UI_LEGACY_PARTS], unknown: [], legacy: true };
|
|
136
|
+
return parseXcUiList(v.split(",").filter((x) => x.trim() !== ""));
|
|
137
|
+
}
|
|
138
|
+
function normalizeXcUiOption(v) {
|
|
139
|
+
if (v === true) return { parts: [...XC_UI_LEGACY_PARTS], unknown: [], legacy: true };
|
|
140
|
+
if (Array.isArray(v)) return parseXcUiList(v);
|
|
141
|
+
return { parts: [], unknown: [], legacy: false };
|
|
142
|
+
}
|
|
143
|
+
var XC_ID_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
144
|
+
var isXcId = (v) => typeof v === "string" && XC_ID_RE.test(v);
|
|
145
|
+
var xcHasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
98
146
|
function xcMessage(type, payload, id) {
|
|
99
147
|
const m = { type, v: XC_PROTOCOL_VERSION, payload };
|
|
100
148
|
if (id !== void 0) m.id = id;
|
|
@@ -116,6 +164,95 @@ function normalizeOrigin(input) {
|
|
|
116
164
|
}
|
|
117
165
|
}
|
|
118
166
|
|
|
167
|
+
// src/gesture-box.ts
|
|
168
|
+
var DEFAULT_RESIZE_LIMITS = Object.freeze({
|
|
169
|
+
minWidth: 120,
|
|
170
|
+
minHeight: 180,
|
|
171
|
+
maxWidth: Infinity,
|
|
172
|
+
maxHeight: Infinity
|
|
173
|
+
});
|
|
174
|
+
var finitePositive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
175
|
+
function normalizeResizable(v) {
|
|
176
|
+
if (!v) return null;
|
|
177
|
+
const o = v === true ? {} : v;
|
|
178
|
+
const minWidth = finitePositive(o.minWidth) ? o.minWidth : DEFAULT_RESIZE_LIMITS.minWidth;
|
|
179
|
+
const minHeight = finitePositive(o.minHeight) ? o.minHeight : DEFAULT_RESIZE_LIMITS.minHeight;
|
|
180
|
+
const maxWidth = finitePositive(o.maxWidth) ? Math.max(o.maxWidth, minWidth) : Infinity;
|
|
181
|
+
const maxHeight = finitePositive(o.maxHeight) ? Math.max(o.maxHeight, minHeight) : Infinity;
|
|
182
|
+
return { minWidth, minHeight, maxWidth, maxHeight };
|
|
183
|
+
}
|
|
184
|
+
var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
|
|
185
|
+
function moveBox(start, totalDx, totalDy, vp) {
|
|
186
|
+
return {
|
|
187
|
+
left: clamp(start.left + totalDx, 0, vp.width - start.width),
|
|
188
|
+
top: clamp(start.top + totalDy, 0, vp.height - start.height),
|
|
189
|
+
width: start.width,
|
|
190
|
+
height: start.height
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function resizeBox(start, corner, totalDx, totalDy, limits, vp) {
|
|
194
|
+
const west = corner === "NW" || corner === "SW";
|
|
195
|
+
const north = corner === "NW" || corner === "NE";
|
|
196
|
+
const right = start.left + start.width, bottom = start.top + start.height;
|
|
197
|
+
const wantW = west ? start.width - totalDx : start.width + totalDx;
|
|
198
|
+
const wantH = north ? start.height - totalDy : start.height + totalDy;
|
|
199
|
+
const roomW = west ? right : vp.width - start.left;
|
|
200
|
+
const roomH = north ? bottom : vp.height - start.top;
|
|
201
|
+
const width = clamp(wantW, limits.minWidth, Math.min(limits.maxWidth, roomW));
|
|
202
|
+
const height = clamp(wantH, limits.minHeight, Math.min(limits.maxHeight, roomH));
|
|
203
|
+
return { left: west ? right - width : start.left, top: north ? bottom - height : start.top, width, height };
|
|
204
|
+
}
|
|
205
|
+
function fitBox(b, vp) {
|
|
206
|
+
const width = Math.min(b.width, Math.max(1, vp.width)), height = Math.min(b.height, Math.max(1, vp.height));
|
|
207
|
+
return { left: clamp(b.left, 0, vp.width - width), top: clamp(b.top, 0, vp.height - height), width, height };
|
|
208
|
+
}
|
|
209
|
+
var CORNERS = /* @__PURE__ */ new Set(["NW", "NE", "SW", "SE"]);
|
|
210
|
+
var PHASES = /* @__PURE__ */ new Set(["start", "move", "end"]);
|
|
211
|
+
var REASONS = /* @__PURE__ */ new Set(["up", "cancel", "blur"]);
|
|
212
|
+
var MAX_GESTURE_DELTA = 2e4;
|
|
213
|
+
var delta = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= MAX_GESTURE_DELTA;
|
|
214
|
+
var counter = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
215
|
+
function parseGesturePayload(kind, raw) {
|
|
216
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
217
|
+
const p = raw;
|
|
218
|
+
if (!counter(p.gesture) || !counter(p.seq) || typeof p.phase !== "string" || !PHASES.has(p.phase)) return null;
|
|
219
|
+
if (!delta(p.dx) || !delta(p.dy) || !delta(p.totalDx) || !delta(p.totalDy)) return null;
|
|
220
|
+
if (p.reason !== void 0 && (typeof p.reason !== "string" || !REASONS.has(p.reason))) return null;
|
|
221
|
+
const out = { gesture: p.gesture, seq: p.seq, phase: p.phase, totalDx: p.totalDx, totalDy: p.totalDy };
|
|
222
|
+
if (kind === "resize") {
|
|
223
|
+
if (typeof p.corner !== "string" || !CORNERS.has(p.corner)) return null;
|
|
224
|
+
out.corner = p.corner;
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
var GestureGate = class {
|
|
229
|
+
constructor() {
|
|
230
|
+
this.lastGesture = 0;
|
|
231
|
+
this.lastSeq = -1;
|
|
232
|
+
this.open = false;
|
|
233
|
+
}
|
|
234
|
+
get active() {
|
|
235
|
+
return this.open;
|
|
236
|
+
}
|
|
237
|
+
accept(p) {
|
|
238
|
+
if (p.phase === "start") {
|
|
239
|
+
if (p.gesture <= this.lastGesture || p.seq !== 0) return false;
|
|
240
|
+
this.lastGesture = p.gesture;
|
|
241
|
+
this.lastSeq = 0;
|
|
242
|
+
this.open = true;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
if (!this.open || p.gesture !== this.lastGesture || p.seq <= this.lastSeq) return false;
|
|
246
|
+
this.lastSeq = p.seq;
|
|
247
|
+
if (p.phase === "end") this.open = false;
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
/** 宿主主动结束 (关闭手势 / 销毁): 之后同一手势的迟到消息被拒绝。 */
|
|
251
|
+
close() {
|
|
252
|
+
this.open = false;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
119
256
|
// src/client.ts
|
|
120
257
|
var FALLBACK_PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
121
258
|
'<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>'
|
|
@@ -142,8 +279,46 @@ function createXiaochun(options) {
|
|
|
142
279
|
if (!n) throw new Error(`[project-xiaochun] invalid allowedOrigins entry: ${o} (wildcards are not allowed)`);
|
|
143
280
|
trusted.add(n);
|
|
144
281
|
}
|
|
145
|
-
const
|
|
146
|
-
const
|
|
282
|
+
const prefsKey = options.persist === "host" ? "xiaochun:prefs" : typeof options.persist === "string" && options.persist ? options.persist : null;
|
|
283
|
+
const readPrefs = () => {
|
|
284
|
+
if (!prefsKey) return {};
|
|
285
|
+
try {
|
|
286
|
+
const raw = JSON.parse(window.localStorage.getItem(prefsKey) ?? "null");
|
|
287
|
+
const out = {};
|
|
288
|
+
if (raw && typeof raw === "object") {
|
|
289
|
+
if (isXcId(raw.outfit)) out.outfit = raw.outfit;
|
|
290
|
+
if (isXcId(raw.scene)) out.scene = raw.scene;
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
} catch {
|
|
294
|
+
return {};
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const writePrefs = (patch) => {
|
|
298
|
+
if (!prefsKey) return;
|
|
299
|
+
try {
|
|
300
|
+
const next = { ...readPrefs() };
|
|
301
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
302
|
+
if (v) next[k] = v;
|
|
303
|
+
else delete next[k];
|
|
304
|
+
}
|
|
305
|
+
window.localStorage.setItem(prefsKey, JSON.stringify(next));
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const prefs = readPrefs();
|
|
310
|
+
if (options.model !== void 0 && options.outfit === void 0) {
|
|
311
|
+
console.warn('[project-xiaochun] option "model" is deprecated, use "outfit" (built-in outfit id).');
|
|
312
|
+
}
|
|
313
|
+
const uiParse = normalizeXcUiOption(options.ui);
|
|
314
|
+
const uiParts = uiParse.parts;
|
|
315
|
+
if (uiParse.legacy) console.warn(`[project-xiaochun] option "ui: true" is deprecated, use an array of parts, e.g. ui: ['chat', 'bubble', 'outfit', 'scene'].`);
|
|
316
|
+
if (uiParse.unknown.length) console.warn(`[project-xiaochun] ignoring unknown ui part(s): ${uiParse.unknown.join(", ")} (known: ${XC_UI_PARTS.join(", ")})`);
|
|
317
|
+
const outfitOpt = options.outfit ?? (isXcId(options.model) ? options.model : void 0);
|
|
318
|
+
const initialOutfit = outfitOpt ?? prefs.outfit;
|
|
319
|
+
const initialScene = options.scene ?? prefs.scene ?? (options.transparent ? "transparent" : void 0);
|
|
320
|
+
let transparent = initialScene === "transparent" || initialScene !== "light" && initialScene !== "dark" && (options.transparent ?? false);
|
|
321
|
+
let passthrough = options.passthrough ?? transparent;
|
|
147
322
|
const lazy = options.lazy ?? true;
|
|
148
323
|
const position = options.position ?? "inline";
|
|
149
324
|
const handshakeTimeout = options.handshakeTimeout ?? 2e4;
|
|
@@ -156,9 +331,20 @@ function createXiaochun(options) {
|
|
|
156
331
|
ws.maxWidth = "100%";
|
|
157
332
|
ws.overflow = "visible";
|
|
158
333
|
ws.contain = "layout style";
|
|
159
|
-
ws.borderRadius = "var(--xc-radius, 0)";
|
|
160
334
|
ws.boxShadow = "var(--xc-shadow, none)";
|
|
161
|
-
|
|
335
|
+
const applyWrapperBg = () => {
|
|
336
|
+
ws.background = transparent ? "" : "var(--xc-bg, transparent)";
|
|
337
|
+
};
|
|
338
|
+
applyWrapperBg();
|
|
339
|
+
let radiusOpt = options.borderRadius;
|
|
340
|
+
const radiusValue = () => radiusOpt !== void 0 ? css(radiusOpt, "0px") : transparent ? "0px" : `${XC_WINDOW_CORNER_RADIUS}px`;
|
|
341
|
+
const applyRadius = () => {
|
|
342
|
+
const v = `var(--xc-radius, ${radiusValue()})`;
|
|
343
|
+
ws.borderRadius = v;
|
|
344
|
+
if (placeholderEl) placeholderEl.style.borderRadius = v;
|
|
345
|
+
if (iframe) iframe.style.borderRadius = v;
|
|
346
|
+
};
|
|
347
|
+
ws.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
162
348
|
if (position === "bottom-right") {
|
|
163
349
|
ws.right = "var(--xc-offset-x, 16px)";
|
|
164
350
|
ws.bottom = "var(--xc-offset-y, 16px)";
|
|
@@ -185,7 +371,7 @@ function createXiaochun(options) {
|
|
|
185
371
|
}
|
|
186
372
|
const ps = placeholderEl.style;
|
|
187
373
|
placeholderEl.setAttribute("part", "placeholder");
|
|
188
|
-
ps.borderRadius =
|
|
374
|
+
ps.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
189
375
|
ps.position = "absolute";
|
|
190
376
|
ps.inset = "0";
|
|
191
377
|
ps.width = "100%";
|
|
@@ -222,25 +408,45 @@ function createXiaochun(options) {
|
|
|
222
408
|
let autoPaused = false;
|
|
223
409
|
let loaded = false;
|
|
224
410
|
let lastHit = false;
|
|
411
|
+
let caps = null;
|
|
412
|
+
let currentOutfit = null;
|
|
413
|
+
let currentScene = null;
|
|
225
414
|
const queue = [];
|
|
226
415
|
const pending = /* @__PURE__ */ new Map();
|
|
227
416
|
let idSeq = 0;
|
|
228
417
|
let handshakeTimer = null;
|
|
229
418
|
let io = null;
|
|
230
419
|
let idleHandle = null;
|
|
231
|
-
|
|
420
|
+
let resolveHandshake;
|
|
421
|
+
const handshaked = new Promise((res) => {
|
|
422
|
+
resolveHandshake = res;
|
|
423
|
+
});
|
|
424
|
+
const failLocal = (type, id, code, message) => {
|
|
425
|
+
emit("error", { code, message, command: type });
|
|
426
|
+
const p = id ? pending.get(id) : void 0;
|
|
427
|
+
if (p && id) {
|
|
428
|
+
pending.delete(id);
|
|
429
|
+
p.reject(new Error(`[${code}] ${message}`));
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
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)`);
|
|
433
|
+
const postToFrame = (type, payload, id, transfer, requires) => {
|
|
232
434
|
if (!port) {
|
|
233
|
-
queue.push({ type, payload, id, transfer });
|
|
435
|
+
queue.push({ type, payload, id, transfer, requires });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (requires && !caps?.[requires]) {
|
|
439
|
+
unsupportedByFrame(type, id);
|
|
234
440
|
return;
|
|
235
441
|
}
|
|
236
442
|
port.postMessage(xcMessage(type, payload, id), transfer ?? []);
|
|
237
443
|
};
|
|
238
|
-
const send = (type, payload, wait = false, transfer, fixedId) => {
|
|
444
|
+
const send = (type, payload, wait = false, transfer, fixedId, requires) => {
|
|
239
445
|
if (destroyed) return Promise.reject(new Error("[project-xiaochun] instance destroyed"));
|
|
240
446
|
if (!iframe) activate();
|
|
241
447
|
const id = fixedId ?? `c${++idSeq}`;
|
|
242
448
|
const p = wait ? new Promise((resolve, reject) => pending.set(id, { resolve, reject, sawEnd: false })) : Promise.resolve();
|
|
243
|
-
postToFrame(type, payload, id, transfer);
|
|
449
|
+
postToFrame(type, payload, id, transfer, requires);
|
|
244
450
|
return p;
|
|
245
451
|
};
|
|
246
452
|
const toBuffer = (d) => d instanceof ArrayBuffer ? d : d.buffer.slice(d.byteOffset, d.byteOffset + d.byteLength);
|
|
@@ -290,7 +496,7 @@ function createXiaochun(options) {
|
|
|
290
496
|
let ended = false;
|
|
291
497
|
let fmt = rest.format === "pcm16" || rest.format === "float32" ? rest.format : null;
|
|
292
498
|
let done = Promise.resolve();
|
|
293
|
-
const
|
|
499
|
+
const handle = {
|
|
294
500
|
write(data) {
|
|
295
501
|
if (ended) throw new Error("[project-xiaochun] audio stream already ended");
|
|
296
502
|
if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
|
|
@@ -324,17 +530,18 @@ function createXiaochun(options) {
|
|
|
324
530
|
return done;
|
|
325
531
|
}
|
|
326
532
|
};
|
|
327
|
-
return
|
|
533
|
+
return handle;
|
|
328
534
|
};
|
|
329
535
|
function buildUrl() {
|
|
330
536
|
const u = new URL(srcUrl.href);
|
|
331
537
|
u.searchParams.set("host", window.location.origin);
|
|
332
538
|
if (transparent) u.searchParams.set("transparent", "1");
|
|
333
|
-
if (
|
|
539
|
+
if (uiParts.length) u.searchParams.set("ui", uiParts.join(","));
|
|
334
540
|
if (options.lang) u.searchParams.set("lang", options.lang);
|
|
335
541
|
if (options.heavy) u.searchParams.set("heavy", options.heavy);
|
|
336
|
-
if (options.controls) u.searchParams.set("controls", "
|
|
337
|
-
if (
|
|
542
|
+
if (options.controls === false) u.searchParams.set("controls", "0");
|
|
543
|
+
if (initialScene && isXcId(initialScene)) u.searchParams.set("scene", initialScene);
|
|
544
|
+
if (initialOutfit) u.searchParams.set("outfit", initialOutfit);
|
|
338
545
|
return u.href;
|
|
339
546
|
}
|
|
340
547
|
function activate() {
|
|
@@ -345,7 +552,7 @@ function createXiaochun(options) {
|
|
|
345
552
|
const f = document.createElement("iframe");
|
|
346
553
|
f.title = "Project XiaoChun";
|
|
347
554
|
f.loading = "lazy";
|
|
348
|
-
f.allow = "microphone; autoplay";
|
|
555
|
+
f.allow = options.crossOriginIsolated === true ? "microphone; autoplay; cross-origin-isolated" : "microphone; autoplay";
|
|
349
556
|
f.referrerPolicy = "strict-origin-when-cross-origin";
|
|
350
557
|
if (options.sandbox !== false) {
|
|
351
558
|
f.setAttribute("sandbox", options.sandbox ?? "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox");
|
|
@@ -360,7 +567,7 @@ function createXiaochun(options) {
|
|
|
360
567
|
s.border = "0";
|
|
361
568
|
s.background = "transparent";
|
|
362
569
|
s.colorScheme = "normal";
|
|
363
|
-
s.borderRadius =
|
|
570
|
+
s.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
364
571
|
f.setAttribute("part", "iframe");
|
|
365
572
|
s.opacity = "0";
|
|
366
573
|
s.transition = "opacity .25s ease";
|
|
@@ -373,7 +580,7 @@ function createXiaochun(options) {
|
|
|
373
580
|
if (!port) emit("error", { code: "timeout", message: `no xc.ready from ${frameOrigin} within ${handshakeTimeout}ms` });
|
|
374
581
|
}, handshakeTimeout);
|
|
375
582
|
setupAutoPause();
|
|
376
|
-
|
|
583
|
+
if (passthrough) enablePassthrough();
|
|
377
584
|
}
|
|
378
585
|
const onWindowMessage = (ev) => {
|
|
379
586
|
if (destroyed || !iframe || ev.source !== iframe.contentWindow) return;
|
|
@@ -386,20 +593,39 @@ function createXiaochun(options) {
|
|
|
386
593
|
const ch = new MessageChannel();
|
|
387
594
|
port = ch.port1;
|
|
388
595
|
port.onmessage = onPortMessage;
|
|
596
|
+
caps = data.payload?.capabilities ?? null;
|
|
597
|
+
const gWant = wantGestures();
|
|
598
|
+
sentGestures = gWant;
|
|
599
|
+
const initConfig = {
|
|
600
|
+
...options.allowCustomModel ? { allowCustomModel: true } : {},
|
|
601
|
+
...gWant.move || gWant.resize ? { gestures: gWant } : {}
|
|
602
|
+
};
|
|
389
603
|
iframe.contentWindow.postMessage(
|
|
390
|
-
xcMessage("xc.init", { hostOrigin: window.location.origin }),
|
|
604
|
+
xcMessage("xc.init", { hostOrigin: window.location.origin, ...Object.keys(initConfig).length ? { config: initConfig } : {} }),
|
|
391
605
|
ev.origin,
|
|
392
606
|
[ch.port2]
|
|
393
607
|
);
|
|
608
|
+
warnGestureUnsupported();
|
|
394
609
|
if (handshakeTimer) {
|
|
395
610
|
clearTimeout(handshakeTimer);
|
|
396
611
|
handshakeTimer = null;
|
|
397
612
|
}
|
|
398
613
|
emit("handshake", data.payload);
|
|
399
|
-
|
|
614
|
+
resolveHandshake();
|
|
615
|
+
for (const m of queue.splice(0)) {
|
|
616
|
+
if (m.requires && !caps?.[m.requires]) unsupportedByFrame(m.type, m.id);
|
|
617
|
+
else port.postMessage(xcMessage(m.type, m.payload, m.id), m.transfer ?? []);
|
|
618
|
+
}
|
|
400
619
|
syncPause();
|
|
401
620
|
};
|
|
402
621
|
window.addEventListener("message", onWindowMessage);
|
|
622
|
+
function ack(id, value) {
|
|
623
|
+
const p = id ? pending.get(id) : void 0;
|
|
624
|
+
if (p && id) {
|
|
625
|
+
pending.delete(id);
|
|
626
|
+
p.resolve(value);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
403
629
|
function onPortMessage(ev) {
|
|
404
630
|
const data = ev.data;
|
|
405
631
|
if (!isXcEnvelope(data)) return;
|
|
@@ -410,6 +636,11 @@ function createXiaochun(options) {
|
|
|
410
636
|
case "xc.loaded":
|
|
411
637
|
if (!loaded) {
|
|
412
638
|
loaded = true;
|
|
639
|
+
if (options.prefetch && options.heavy === "eager" && caps?.prefetch) {
|
|
640
|
+
const ids = Array.isArray(options.prefetch) ? options.prefetch : void 0;
|
|
641
|
+
void api.prefetch(ids).catch(() => {
|
|
642
|
+
});
|
|
643
|
+
}
|
|
413
644
|
if (iframe) iframe.style.opacity = "1";
|
|
414
645
|
if (placeholderEl) {
|
|
415
646
|
const el = placeholderEl;
|
|
@@ -422,6 +653,30 @@ function createXiaochun(options) {
|
|
|
422
653
|
}
|
|
423
654
|
emit("ready", data.payload);
|
|
424
655
|
break;
|
|
656
|
+
case "xc.outfit-changed": {
|
|
657
|
+
const pl = data.payload;
|
|
658
|
+
if (!pl.noop) {
|
|
659
|
+
currentOutfit = pl.id;
|
|
660
|
+
emit("outfit-changed", pl);
|
|
661
|
+
if (!pl.initial && pl.id) writePrefs({ outfit: pl.id });
|
|
662
|
+
}
|
|
663
|
+
ack(data.id);
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
case "xc.scene-changed": {
|
|
667
|
+
const pl = data.payload;
|
|
668
|
+
currentScene = pl.id;
|
|
669
|
+
if (pl.transparent !== transparent) applyTransparency(pl.transparent);
|
|
670
|
+
if (!pl.noop) {
|
|
671
|
+
emit("scene-changed", pl);
|
|
672
|
+
if (!pl.initial) writePrefs({ scene: pl.id });
|
|
673
|
+
}
|
|
674
|
+
ack(data.id);
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
case "xc.prefetched":
|
|
678
|
+
ack(data.id, data.payload);
|
|
679
|
+
break;
|
|
425
680
|
case "xc.state":
|
|
426
681
|
emit("state", data.payload);
|
|
427
682
|
break;
|
|
@@ -446,9 +701,16 @@ function createXiaochun(options) {
|
|
|
446
701
|
}
|
|
447
702
|
break;
|
|
448
703
|
}
|
|
704
|
+
case "xc.gesture-move":
|
|
705
|
+
onGesture("move", data.payload);
|
|
706
|
+
break;
|
|
707
|
+
case "xc.gesture-resize":
|
|
708
|
+
onGesture("resize", data.payload);
|
|
709
|
+
break;
|
|
449
710
|
case "xc.error": {
|
|
450
711
|
const { code, message, command } = data.payload;
|
|
451
712
|
emit("error", { code, message, command });
|
|
713
|
+
if (code === "unknown_id" && (command === "outfit" || command === "scene")) writePrefs({ [command]: null });
|
|
452
714
|
const p = data.id ? pending.get(data.id) : void 0;
|
|
453
715
|
if (p && data.id) {
|
|
454
716
|
pending.delete(data.id);
|
|
@@ -460,8 +722,26 @@ function createXiaochun(options) {
|
|
|
460
722
|
break;
|
|
461
723
|
}
|
|
462
724
|
}
|
|
725
|
+
const POINTER_MIN_INTERVAL_MS = 32;
|
|
726
|
+
const POINTER_MIN_MOVE_PX = 2;
|
|
463
727
|
let pointerRaf = 0;
|
|
464
728
|
let lastPtr = null;
|
|
729
|
+
let lastPosted = null;
|
|
730
|
+
let rectCache = null;
|
|
731
|
+
let rectRo = null;
|
|
732
|
+
let rectAt = 0;
|
|
733
|
+
const invalidateRect = () => {
|
|
734
|
+
rectCache = null;
|
|
735
|
+
};
|
|
736
|
+
const getRect = () => {
|
|
737
|
+
if (!iframe) return null;
|
|
738
|
+
const now = performance.now();
|
|
739
|
+
if (!rectCache || now - rectAt > 400) {
|
|
740
|
+
rectCache = iframe.getBoundingClientRect();
|
|
741
|
+
rectAt = now;
|
|
742
|
+
}
|
|
743
|
+
return rectCache;
|
|
744
|
+
};
|
|
465
745
|
const onHostPointer = (e) => {
|
|
466
746
|
if (!iframe || !port || iframe.style.pointerEvents === "auto") return;
|
|
467
747
|
lastPtr = { x: e.clientX, y: e.clientY };
|
|
@@ -469,20 +749,63 @@ function createXiaochun(options) {
|
|
|
469
749
|
pointerRaf = requestAnimationFrame(() => {
|
|
470
750
|
pointerRaf = 0;
|
|
471
751
|
if (!iframe || !port || !lastPtr) return;
|
|
472
|
-
const r =
|
|
752
|
+
const r = getRect();
|
|
753
|
+
if (!r) return;
|
|
473
754
|
const x = lastPtr.x - r.left, y = lastPtr.y - r.top;
|
|
474
755
|
if (x < 0 || y < 0 || x > r.width || y > r.height) {
|
|
756
|
+
lastPosted = null;
|
|
475
757
|
if (lastHit) {
|
|
476
758
|
lastHit = false;
|
|
477
759
|
iframe.style.pointerEvents = "none";
|
|
478
760
|
}
|
|
479
761
|
return;
|
|
480
762
|
}
|
|
763
|
+
const now = performance.now();
|
|
764
|
+
if (lastPosted && now - lastPosted.t < POINTER_MIN_INTERVAL_MS) return;
|
|
765
|
+
if (lastPosted && Math.abs(x - lastPosted.x) < POINTER_MIN_MOVE_PX && Math.abs(y - lastPosted.y) < POINTER_MIN_MOVE_PX) return;
|
|
766
|
+
lastPosted = { x, y, t: now };
|
|
481
767
|
port.postMessage(xcMessage("xc.pointer", { x, y }));
|
|
482
768
|
});
|
|
483
769
|
};
|
|
484
|
-
|
|
485
|
-
|
|
770
|
+
let passthroughActive = false;
|
|
771
|
+
function enablePassthrough() {
|
|
772
|
+
if (passthroughActive || !iframe) return;
|
|
773
|
+
passthroughActive = true;
|
|
774
|
+
window.addEventListener("pointermove", onHostPointer, { passive: true });
|
|
775
|
+
window.addEventListener("pointerdown", onHostPointer, { passive: true });
|
|
776
|
+
window.addEventListener("scroll", invalidateRect, { passive: true, capture: true });
|
|
777
|
+
window.addEventListener("resize", invalidateRect, { passive: true });
|
|
778
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
779
|
+
rectRo = new ResizeObserver(invalidateRect);
|
|
780
|
+
rectRo.observe(iframe);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function disablePassthrough() {
|
|
784
|
+
if (!passthroughActive) return;
|
|
785
|
+
passthroughActive = false;
|
|
786
|
+
window.removeEventListener("pointermove", onHostPointer);
|
|
787
|
+
window.removeEventListener("pointerdown", onHostPointer);
|
|
788
|
+
window.removeEventListener("scroll", invalidateRect, true);
|
|
789
|
+
window.removeEventListener("resize", invalidateRect);
|
|
790
|
+
rectRo?.disconnect();
|
|
791
|
+
rectRo = null;
|
|
792
|
+
if (pointerRaf) {
|
|
793
|
+
cancelAnimationFrame(pointerRaf);
|
|
794
|
+
pointerRaf = 0;
|
|
795
|
+
}
|
|
796
|
+
lastPtr = null;
|
|
797
|
+
}
|
|
798
|
+
function applyTransparency(next) {
|
|
799
|
+
transparent = next;
|
|
800
|
+
applyWrapperBg();
|
|
801
|
+
applyRadius();
|
|
802
|
+
passthrough = next ? options.passthrough ?? true : false;
|
|
803
|
+
lastHit = false;
|
|
804
|
+
lastPosted = null;
|
|
805
|
+
rectCache = null;
|
|
806
|
+
if (passthrough) enablePassthrough();
|
|
807
|
+
else disablePassthrough();
|
|
808
|
+
if (iframe) iframe.style.pointerEvents = passthrough ? "none" : "auto";
|
|
486
809
|
}
|
|
487
810
|
let pauseIo = null;
|
|
488
811
|
function setupAutoPause() {
|
|
@@ -535,49 +858,123 @@ function createXiaochun(options) {
|
|
|
535
858
|
}, { rootMargin: `${options.lazyMargin ?? 200}px` });
|
|
536
859
|
io.observe(wrapper);
|
|
537
860
|
}
|
|
538
|
-
let
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (!drag) return;
|
|
569
|
-
ws.left = `${Math.min(Math.max(0, e.clientX - drag.dx), window.innerWidth - 40)}px`;
|
|
570
|
-
ws.top = `${Math.min(Math.max(0, e.clientY - drag.dy), window.innerHeight - 40)}px`;
|
|
861
|
+
let draggableOn = options.draggable === true;
|
|
862
|
+
let resizeLimits = normalizeResizable(options.resizable);
|
|
863
|
+
const gates = { move: new GestureGate(), resize: new GestureGate() };
|
|
864
|
+
let activeGesture = null;
|
|
865
|
+
let inlineShift = { x: 0, y: 0 };
|
|
866
|
+
let userMoved = false;
|
|
867
|
+
let sentGestures = null;
|
|
868
|
+
let warnedGesture = false;
|
|
869
|
+
const viewport = () => ({
|
|
870
|
+
width: document.documentElement.clientWidth || window.innerWidth,
|
|
871
|
+
height: document.documentElement.clientHeight || window.innerHeight
|
|
872
|
+
});
|
|
873
|
+
const readBox = () => {
|
|
874
|
+
const r = wrapper.getBoundingClientRect();
|
|
875
|
+
return { left: r.left, top: r.top, width: r.width, height: r.height };
|
|
876
|
+
};
|
|
877
|
+
const wantGestures = () => ({ move: draggableOn && caps?.gestures?.move === true, resize: resizeLimits !== null && caps?.gestures?.resize === true });
|
|
878
|
+
function applyBox(b, inlineBase, withSize) {
|
|
879
|
+
if (withSize) {
|
|
880
|
+
ws.width = `${b.width}px`;
|
|
881
|
+
ws.height = `${b.height}px`;
|
|
882
|
+
ws.maxWidth = "none";
|
|
883
|
+
}
|
|
884
|
+
if (position === "inline") {
|
|
885
|
+
const base = inlineBase ?? { left: b.left - inlineShift.x, top: b.top - inlineShift.y };
|
|
886
|
+
inlineShift = { x: b.left - base.left, y: b.top - base.top };
|
|
887
|
+
ws.setProperty("translate", `${inlineShift.x}px ${inlineShift.y}px`);
|
|
888
|
+
} else {
|
|
889
|
+
ws.left = `${b.left}px`;
|
|
890
|
+
ws.top = `${b.top}px`;
|
|
571
891
|
ws.right = "auto";
|
|
572
892
|
ws.bottom = "auto";
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
893
|
+
}
|
|
894
|
+
userMoved = true;
|
|
895
|
+
invalidateRect();
|
|
896
|
+
}
|
|
897
|
+
let selectionLock = null;
|
|
898
|
+
function lockSelection(on) {
|
|
899
|
+
const de = document.documentElement;
|
|
900
|
+
if (on && !selectionLock) {
|
|
901
|
+
selectionLock = { us: de.style.getPropertyValue("user-select"), wus: de.style.getPropertyValue("-webkit-user-select") };
|
|
902
|
+
de.style.setProperty("user-select", "none");
|
|
903
|
+
de.style.setProperty("-webkit-user-select", "none");
|
|
904
|
+
try {
|
|
905
|
+
window.getSelection()?.removeAllRanges();
|
|
906
|
+
} catch {
|
|
907
|
+
}
|
|
908
|
+
} else if (!on && selectionLock) {
|
|
909
|
+
de.style.setProperty("user-select", selectionLock.us);
|
|
910
|
+
de.style.setProperty("-webkit-user-select", selectionLock.wus);
|
|
911
|
+
selectionLock = null;
|
|
912
|
+
}
|
|
580
913
|
}
|
|
914
|
+
function endActiveGesture(kind) {
|
|
915
|
+
if (!activeGesture || kind && activeGesture.kind !== kind) return;
|
|
916
|
+
gates[activeGesture.kind].close();
|
|
917
|
+
const b = readBox();
|
|
918
|
+
emit(activeGesture.kind, { phase: "end", ...b });
|
|
919
|
+
activeGesture = null;
|
|
920
|
+
lockSelection(false);
|
|
921
|
+
}
|
|
922
|
+
function onGesture(kind, raw) {
|
|
923
|
+
if (destroyed || !caps?.gestures) return;
|
|
924
|
+
if (kind === "move" ? !draggableOn : resizeLimits === null) return;
|
|
925
|
+
const p = parseGesturePayload(kind, raw);
|
|
926
|
+
if (!p) return;
|
|
927
|
+
if (p.phase === "start" ? activeGesture !== null : activeGesture?.kind !== kind) return;
|
|
928
|
+
if (!gates[kind].accept(p)) return;
|
|
929
|
+
if (p.phase === "start") {
|
|
930
|
+
const start = readBox();
|
|
931
|
+
activeGesture = {
|
|
932
|
+
kind,
|
|
933
|
+
start,
|
|
934
|
+
corner: p.corner,
|
|
935
|
+
inlineBase: position === "inline" ? { left: start.left - inlineShift.x, top: start.top - inlineShift.y } : null
|
|
936
|
+
};
|
|
937
|
+
lockSelection(true);
|
|
938
|
+
emit(kind, { phase: "start", ...start });
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const a = activeGesture;
|
|
942
|
+
const vp = viewport();
|
|
943
|
+
const next = kind === "move" ? moveBox(a.start, p.totalDx, p.totalDy, vp) : resizeBox(a.start, a.corner, p.totalDx, p.totalDy, resizeLimits, vp);
|
|
944
|
+
applyBox(next, a.inlineBase, kind === "resize");
|
|
945
|
+
emit(kind, { phase: p.phase, ...next });
|
|
946
|
+
if (p.phase === "end") {
|
|
947
|
+
activeGesture = null;
|
|
948
|
+
lockSelection(false);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function syncGestures() {
|
|
952
|
+
if (destroyed) return;
|
|
953
|
+
const want = wantGestures();
|
|
954
|
+
if (!want.move) endActiveGesture("move");
|
|
955
|
+
if (!want.resize) endActiveGesture("resize");
|
|
956
|
+
if (!port || !caps) return;
|
|
957
|
+
warnGestureUnsupported();
|
|
958
|
+
if (sentGestures && sentGestures.move === want.move && sentGestures.resize === want.resize) return;
|
|
959
|
+
sentGestures = want;
|
|
960
|
+
if (caps.gestures) port.postMessage(xcMessage("xc.setConfig", { gestures: want }));
|
|
961
|
+
}
|
|
962
|
+
function warnGestureUnsupported() {
|
|
963
|
+
if (warnedGesture || !caps || caps.gestures) return;
|
|
964
|
+
const needMove = draggableOn;
|
|
965
|
+
if (!needMove && resizeLimits === null) return;
|
|
966
|
+
warnedGesture = true;
|
|
967
|
+
const what = [needMove ? "draggable" : "", resizeLimits !== null ? "resizable" : ""].filter(Boolean).join(" / ");
|
|
968
|
+
emit("error", { code: "unsupported", message: `${what} needs gesture support (capabilities.gestures), which this /embed deployment does not advertise (older version)`, command: "gestures" });
|
|
969
|
+
}
|
|
970
|
+
const onWindowResize = () => {
|
|
971
|
+
if (!userMoved || activeGesture || destroyed) return;
|
|
972
|
+
const cur = readBox();
|
|
973
|
+
const fit = fitBox(cur, viewport());
|
|
974
|
+
if (fit.left === cur.left && fit.top === cur.top && fit.width === cur.width && fit.height === cur.height) return;
|
|
975
|
+
applyBox(fit, position === "inline" ? { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y } : null, fit.width !== cur.width || fit.height !== cur.height);
|
|
976
|
+
};
|
|
977
|
+
window.addEventListener("resize", onWindowResize, { passive: true });
|
|
581
978
|
scheduleLazy();
|
|
582
979
|
const api = {
|
|
583
980
|
element: wrapper,
|
|
@@ -592,8 +989,65 @@ function createXiaochun(options) {
|
|
|
592
989
|
motion: (m) => send("xc.motion", typeof m === "string" ? m.startsWith("http") || m.startsWith("/") ? { url: m } : { name: m } : m),
|
|
593
990
|
expression: (name) => send("xc.expression", { name }),
|
|
594
991
|
lookAt: (x, y) => send("xc.lookAt", { x, y }),
|
|
595
|
-
|
|
992
|
+
setOutfit: (id) => {
|
|
993
|
+
if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid outfit id"));
|
|
994
|
+
return send("xc.setOutfit", { id }, true, void 0, void 0, "outfits");
|
|
995
|
+
},
|
|
996
|
+
setScene: (id) => {
|
|
997
|
+
if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid scene id"));
|
|
998
|
+
return send("xc.setScene", { id }, true, void 0, void 0, "scenes");
|
|
999
|
+
},
|
|
1000
|
+
prefetch: (ids) => {
|
|
1001
|
+
if (ids !== void 0 && (!Array.isArray(ids) || ids.some((x) => !isXcId(x)))) {
|
|
1002
|
+
return Promise.reject(new Error("[bad_request] prefetch ids must be an array of outfit ids"));
|
|
1003
|
+
}
|
|
1004
|
+
return send("xc.prefetch", ids ? { ids } : {}, true, void 0, void 0, "prefetch");
|
|
1005
|
+
},
|
|
1006
|
+
getOutfits: async () => {
|
|
1007
|
+
if (!iframe) activate();
|
|
1008
|
+
await handshaked;
|
|
1009
|
+
return caps?.outfits ? caps.outfits.slice() : [];
|
|
1010
|
+
},
|
|
1011
|
+
getScenes: async () => {
|
|
1012
|
+
if (!iframe) activate();
|
|
1013
|
+
await handshaked;
|
|
1014
|
+
return caps?.scenes ? caps.scenes.slice() : [];
|
|
1015
|
+
},
|
|
1016
|
+
get outfit() {
|
|
1017
|
+
return currentOutfit;
|
|
1018
|
+
},
|
|
1019
|
+
get scene() {
|
|
1020
|
+
return currentScene;
|
|
1021
|
+
},
|
|
1022
|
+
setModel: (m) => {
|
|
1023
|
+
const msg = typeof m === "string" ? /^(https?:)?\/|\.vrm/i.test(m) ? { url: m } : { outfit: m } : m;
|
|
1024
|
+
if (msg.url !== void 0 && msg.outfit === void 0 && !options.allowCustomModel) {
|
|
1025
|
+
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";
|
|
1026
|
+
emit("error", { code: "unsupported", message, command: "xc.setModel" });
|
|
1027
|
+
return Promise.reject(new Error(`[unsupported] ${message}`));
|
|
1028
|
+
}
|
|
1029
|
+
return send("xc.setModel", msg);
|
|
1030
|
+
},
|
|
596
1031
|
setConfig: (cfg) => send("xc.setConfig", cfg),
|
|
1032
|
+
setSize(width, height) {
|
|
1033
|
+
if (destroyed) return;
|
|
1034
|
+
ws.width = css(width, "320px");
|
|
1035
|
+
ws.height = css(height, "480px");
|
|
1036
|
+
invalidateRect();
|
|
1037
|
+
},
|
|
1038
|
+
getBox: () => readBox(),
|
|
1039
|
+
setBorderRadius(radius) {
|
|
1040
|
+
radiusOpt = radius;
|
|
1041
|
+
applyRadius();
|
|
1042
|
+
},
|
|
1043
|
+
setDraggable(on) {
|
|
1044
|
+
draggableOn = on === true;
|
|
1045
|
+
syncGestures();
|
|
1046
|
+
},
|
|
1047
|
+
setResizable(on) {
|
|
1048
|
+
resizeLimits = normalizeResizable(on);
|
|
1049
|
+
syncGestures();
|
|
1050
|
+
},
|
|
597
1051
|
startListening: () => send("xc.mic", { enabled: true }),
|
|
598
1052
|
stopListening: () => send("xc.mic", { enabled: false }),
|
|
599
1053
|
mic: (enabled) => send("xc.mic", { enabled }),
|
|
@@ -618,7 +1072,10 @@ function createXiaochun(options) {
|
|
|
618
1072
|
if (handshakeTimer) clearTimeout(handshakeTimer);
|
|
619
1073
|
if (pointerRaf) cancelAnimationFrame(pointerRaf);
|
|
620
1074
|
window.removeEventListener("message", onWindowMessage);
|
|
621
|
-
window.removeEventListener("
|
|
1075
|
+
window.removeEventListener("resize", onWindowResize);
|
|
1076
|
+
lockSelection(false);
|
|
1077
|
+
disablePassthrough();
|
|
1078
|
+
resolveHandshake();
|
|
622
1079
|
try {
|
|
623
1080
|
port?.close();
|
|
624
1081
|
} catch {
|
|
@@ -651,11 +1108,17 @@ function createXiaochun(options) {
|
|
|
651
1108
|
// src/avatar-element.ts
|
|
652
1109
|
var OBSERVED = [
|
|
653
1110
|
"src",
|
|
1111
|
+
"outfit",
|
|
1112
|
+
"scene",
|
|
654
1113
|
"model",
|
|
655
1114
|
"lang",
|
|
656
1115
|
"mic",
|
|
657
1116
|
"transparent",
|
|
658
1117
|
"draggable",
|
|
1118
|
+
"resizable",
|
|
1119
|
+
"min-size",
|
|
1120
|
+
"max-size",
|
|
1121
|
+
"border-radius",
|
|
659
1122
|
"position",
|
|
660
1123
|
"size",
|
|
661
1124
|
"lazy",
|
|
@@ -664,14 +1127,37 @@ var OBSERVED = [
|
|
|
664
1127
|
"heavy",
|
|
665
1128
|
"ui",
|
|
666
1129
|
"controls",
|
|
667
|
-
"allowed-origins"
|
|
1130
|
+
"allowed-origins",
|
|
1131
|
+
"cross-origin-isolated",
|
|
1132
|
+
"persist",
|
|
1133
|
+
"allow-custom-model",
|
|
1134
|
+
"prefetch"
|
|
668
1135
|
];
|
|
669
|
-
var REBUILD = /* @__PURE__ */ new Set(["src", "
|
|
1136
|
+
var REBUILD = /* @__PURE__ */ new Set(["src", "position", "lazy", "placeholder", "heavy", "ui", "controls", "allowed-origins", "transparent", "cross-origin-isolated", "persist", "allow-custom-model", "prefetch"]);
|
|
1137
|
+
function radiusAttr(v) {
|
|
1138
|
+
if (v === null || v.trim() === "") return void 0;
|
|
1139
|
+
const t = v.trim();
|
|
1140
|
+
return /^\d+(\.\d+)?$/.test(t) ? Number(t) : t;
|
|
1141
|
+
}
|
|
670
1142
|
function boolAttr(el, name, dflt) {
|
|
671
1143
|
const v = el.getAttribute(name);
|
|
672
1144
|
if (v === null) return dflt;
|
|
673
1145
|
return v !== "false";
|
|
674
1146
|
}
|
|
1147
|
+
function prefetchAttr(v) {
|
|
1148
|
+
if (v === null || v === "false") return false;
|
|
1149
|
+
if (v === "" || v === "true") return true;
|
|
1150
|
+
return v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1151
|
+
}
|
|
1152
|
+
function uiAttr(v) {
|
|
1153
|
+
if (v === null || v === "false" || v === "0") return void 0;
|
|
1154
|
+
if (v === "" || v === "true" || v === "1") return true;
|
|
1155
|
+
return v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1156
|
+
}
|
|
1157
|
+
function parsePx(v) {
|
|
1158
|
+
const m = v?.trim().match(/^(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)$/i);
|
|
1159
|
+
return m ? { w: Number(m[1]), h: Number(m[2]) } : void 0;
|
|
1160
|
+
}
|
|
675
1161
|
function parseSize(v) {
|
|
676
1162
|
if (!v) return { width: "320px", height: "480px" };
|
|
677
1163
|
const m = v.trim().split(/x/i);
|
|
@@ -719,10 +1205,20 @@ function createElementClass() {
|
|
|
719
1205
|
return;
|
|
720
1206
|
}
|
|
721
1207
|
const c = this.client;
|
|
722
|
-
if (name === "
|
|
1208
|
+
if ((name === "outfit" || name === "model") && newV) {
|
|
1209
|
+
const want = name === "model" && this.hasAttribute("outfit") ? null : newV;
|
|
1210
|
+
if (want) void (isXcId(want) ? c.setOutfit(want) : c.setModel(want)).catch(() => {
|
|
1211
|
+
});
|
|
1212
|
+
} else if (name === "scene" && newV) void c.setScene(newV).catch(() => {
|
|
723
1213
|
});
|
|
724
1214
|
else if (name === "lang" && newV) void c.setConfig({ lang: newV }).catch(() => {
|
|
725
1215
|
});
|
|
1216
|
+
else if (name === "size") {
|
|
1217
|
+
const { width, height } = parseSize(newV);
|
|
1218
|
+
c.setSize(width, height);
|
|
1219
|
+
} else if (name === "border-radius") c.setBorderRadius(radiusAttr(newV));
|
|
1220
|
+
else if (name === "draggable") c.setDraggable(boolAttr(this, "draggable", false));
|
|
1221
|
+
else if (name === "resizable" || name === "min-size" || name === "max-size") c.setResizable(this.resizableOption());
|
|
726
1222
|
else if (name === "mic") void c.mic(boolAttr(this, "mic", false)).catch(() => {
|
|
727
1223
|
});
|
|
728
1224
|
else if (name === "paused") {
|
|
@@ -749,11 +1245,18 @@ function createElementClass() {
|
|
|
749
1245
|
height,
|
|
750
1246
|
position: this.getAttribute("position") ?? "inline",
|
|
751
1247
|
draggable: boolAttr(this, "draggable", false),
|
|
1248
|
+
resizable: this.resizableOption(),
|
|
1249
|
+
borderRadius: radiusAttr(this.getAttribute("border-radius")),
|
|
752
1250
|
lang: this.getAttribute("lang") ?? void 0,
|
|
753
|
-
|
|
1251
|
+
outfit: this.getAttribute("outfit") ?? this.getAttribute("model") ?? void 0,
|
|
1252
|
+
scene: this.getAttribute("scene") ?? void 0,
|
|
1253
|
+
persist: this.getAttribute("persist") || void 0,
|
|
1254
|
+
allowCustomModel: boolAttr(this, "allow-custom-model", false),
|
|
1255
|
+
prefetch: prefetchAttr(this.getAttribute("prefetch")),
|
|
754
1256
|
heavy: this.getAttribute("heavy") ?? void 0,
|
|
755
|
-
ui:
|
|
756
|
-
controls: boolAttr(this, "controls",
|
|
1257
|
+
ui: uiAttr(this.getAttribute("ui")),
|
|
1258
|
+
controls: boolAttr(this, "controls", true),
|
|
1259
|
+
crossOriginIsolated: boolAttr(this, "cross-origin-isolated", false)
|
|
757
1260
|
});
|
|
758
1261
|
this.client = c;
|
|
759
1262
|
const fwd = (ev, name) => c.on(ev, (detail) => this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })));
|
|
@@ -763,10 +1266,20 @@ function createElementClass() {
|
|
|
763
1266
|
fwd("stt", "xc-stt");
|
|
764
1267
|
fwd("utterance", "xc-utterance");
|
|
765
1268
|
fwd("error", "xc-error");
|
|
1269
|
+
fwd("outfit-changed", "xc-outfit-changed");
|
|
1270
|
+
fwd("scene-changed", "xc-scene-changed");
|
|
1271
|
+
fwd("move", "xc-move");
|
|
1272
|
+
fwd("resize", "xc-resize");
|
|
766
1273
|
if (boolAttr(this, "paused", false)) c.pause();
|
|
767
1274
|
if (boolAttr(this, "mic", false)) void c.ready.then(() => c.mic(true)).catch(() => {
|
|
768
1275
|
});
|
|
769
1276
|
}
|
|
1277
|
+
/** resizable / min-size / max-size 属性 → SDK 的 resizable 选项 (缺省 / "false" = 关)。 */
|
|
1278
|
+
resizableOption() {
|
|
1279
|
+
if (!boolAttr(this, "resizable", false)) return false;
|
|
1280
|
+
const min = parsePx(this.getAttribute("min-size")), max = parsePx(this.getAttribute("max-size"));
|
|
1281
|
+
return { minWidth: min?.w, minHeight: min?.h, maxWidth: max?.w, maxHeight: max?.h };
|
|
1282
|
+
}
|
|
770
1283
|
say(text, opts) {
|
|
771
1284
|
return this.requireClient().say(text, opts);
|
|
772
1285
|
}
|
|
@@ -782,6 +1295,21 @@ function createElementClass() {
|
|
|
782
1295
|
expression(name) {
|
|
783
1296
|
return this.requireClient().expression(name);
|
|
784
1297
|
}
|
|
1298
|
+
setOutfit(id) {
|
|
1299
|
+
return this.requireClient().setOutfit(id);
|
|
1300
|
+
}
|
|
1301
|
+
setScene(id) {
|
|
1302
|
+
return this.requireClient().setScene(id);
|
|
1303
|
+
}
|
|
1304
|
+
getOutfits() {
|
|
1305
|
+
return this.requireClient().getOutfits();
|
|
1306
|
+
}
|
|
1307
|
+
getScenes() {
|
|
1308
|
+
return this.requireClient().getScenes();
|
|
1309
|
+
}
|
|
1310
|
+
prefetch(ids) {
|
|
1311
|
+
return this.requireClient().prefetch(ids);
|
|
1312
|
+
}
|
|
785
1313
|
destroy() {
|
|
786
1314
|
this.client?.destroy();
|
|
787
1315
|
this.client = null;
|