@firetable/project-xiaochun 0.1.14 → 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 +105 -17
- package/README.md +105 -17
- package/dist/avatar-element.d.ts +23 -6
- package/dist/chunks/{chunk-DUU5PTKT.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-HLVD6HQM.js → chunk-T6EHFEEZ.js} +81 -9
- package/dist/chunks/chunk-T6EHFEEZ.js.map +7 -0
- package/dist/client.d.ts +127 -6
- package/dist/element.cjs +538 -74
- 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 +565 -74
- 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 +178 -9
- package/dist/protocol.js +20 -2
- package/dist/react.cjs +507 -80
- package/dist/react.cjs.map +4 -4
- package/dist/react.d.ts +17 -1
- package/dist/react.js +49 -15
- package/dist/react.js.map +2 -2
- package/package.json +5 -3
- 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/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() {
|
|
@@ -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);
|
|
@@ -505,16 +767,46 @@ function createXiaochun(options) {
|
|
|
505
767
|
port.postMessage(xcMessage("xc.pointer", { x, y }));
|
|
506
768
|
});
|
|
507
769
|
};
|
|
508
|
-
|
|
509
|
-
|
|
770
|
+
let passthroughActive = false;
|
|
771
|
+
function enablePassthrough() {
|
|
772
|
+
if (passthroughActive || !iframe) return;
|
|
773
|
+
passthroughActive = true;
|
|
510
774
|
window.addEventListener("pointermove", onHostPointer, { passive: true });
|
|
775
|
+
window.addEventListener("pointerdown", onHostPointer, { passive: true });
|
|
511
776
|
window.addEventListener("scroll", invalidateRect, { passive: true, capture: true });
|
|
512
777
|
window.addEventListener("resize", invalidateRect, { passive: true });
|
|
513
|
-
if (typeof ResizeObserver !== "undefined"
|
|
778
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
514
779
|
rectRo = new ResizeObserver(invalidateRect);
|
|
515
780
|
rectRo.observe(iframe);
|
|
516
781
|
}
|
|
517
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";
|
|
809
|
+
}
|
|
518
810
|
let pauseIo = null;
|
|
519
811
|
function setupAutoPause() {
|
|
520
812
|
if (options.autoPause === false || typeof IntersectionObserver === "undefined") return;
|
|
@@ -566,50 +858,123 @@ function createXiaochun(options) {
|
|
|
566
858
|
}, { rootMargin: `${options.lazyMargin ?? 200}px` });
|
|
567
859
|
io.observe(wrapper);
|
|
568
860
|
}
|
|
569
|
-
let
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if (!drag) return;
|
|
600
|
-
ws.left = `${Math.min(Math.max(0, e.clientX - drag.dx), window.innerWidth - 40)}px`;
|
|
601
|
-
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`;
|
|
602
891
|
ws.right = "auto";
|
|
603
892
|
ws.bottom = "auto";
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
+
}
|
|
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);
|
|
612
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 });
|
|
613
978
|
scheduleLazy();
|
|
614
979
|
const api = {
|
|
615
980
|
element: wrapper,
|
|
@@ -624,8 +989,65 @@ function createXiaochun(options) {
|
|
|
624
989
|
motion: (m) => send("xc.motion", typeof m === "string" ? m.startsWith("http") || m.startsWith("/") ? { url: m } : { name: m } : m),
|
|
625
990
|
expression: (name) => send("xc.expression", { name }),
|
|
626
991
|
lookAt: (x, y) => send("xc.lookAt", { x, y }),
|
|
627
|
-
|
|
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
|
+
},
|
|
628
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
|
+
},
|
|
629
1051
|
startListening: () => send("xc.mic", { enabled: true }),
|
|
630
1052
|
stopListening: () => send("xc.mic", { enabled: false }),
|
|
631
1053
|
mic: (enabled) => send("xc.mic", { enabled }),
|
|
@@ -650,10 +1072,10 @@ function createXiaochun(options) {
|
|
|
650
1072
|
if (handshakeTimer) clearTimeout(handshakeTimer);
|
|
651
1073
|
if (pointerRaf) cancelAnimationFrame(pointerRaf);
|
|
652
1074
|
window.removeEventListener("message", onWindowMessage);
|
|
653
|
-
window.removeEventListener("
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1075
|
+
window.removeEventListener("resize", onWindowResize);
|
|
1076
|
+
lockSelection(false);
|
|
1077
|
+
disablePassthrough();
|
|
1078
|
+
resolveHandshake();
|
|
657
1079
|
try {
|
|
658
1080
|
port?.close();
|
|
659
1081
|
} catch {
|
|
@@ -686,11 +1108,17 @@ function createXiaochun(options) {
|
|
|
686
1108
|
// src/avatar-element.ts
|
|
687
1109
|
var OBSERVED = [
|
|
688
1110
|
"src",
|
|
1111
|
+
"outfit",
|
|
1112
|
+
"scene",
|
|
689
1113
|
"model",
|
|
690
1114
|
"lang",
|
|
691
1115
|
"mic",
|
|
692
1116
|
"transparent",
|
|
693
1117
|
"draggable",
|
|
1118
|
+
"resizable",
|
|
1119
|
+
"min-size",
|
|
1120
|
+
"max-size",
|
|
1121
|
+
"border-radius",
|
|
694
1122
|
"position",
|
|
695
1123
|
"size",
|
|
696
1124
|
"lazy",
|
|
@@ -700,14 +1128,36 @@ var OBSERVED = [
|
|
|
700
1128
|
"ui",
|
|
701
1129
|
"controls",
|
|
702
1130
|
"allowed-origins",
|
|
703
|
-
"cross-origin-isolated"
|
|
1131
|
+
"cross-origin-isolated",
|
|
1132
|
+
"persist",
|
|
1133
|
+
"allow-custom-model",
|
|
1134
|
+
"prefetch"
|
|
704
1135
|
];
|
|
705
|
-
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
|
+
}
|
|
706
1142
|
function boolAttr(el, name, dflt) {
|
|
707
1143
|
const v = el.getAttribute(name);
|
|
708
1144
|
if (v === null) return dflt;
|
|
709
1145
|
return v !== "false";
|
|
710
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
|
+
}
|
|
711
1161
|
function parseSize(v) {
|
|
712
1162
|
if (!v) return { width: "320px", height: "480px" };
|
|
713
1163
|
const m = v.trim().split(/x/i);
|
|
@@ -755,10 +1205,20 @@ function createElementClass() {
|
|
|
755
1205
|
return;
|
|
756
1206
|
}
|
|
757
1207
|
const c = this.client;
|
|
758
|
-
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(() => {
|
|
759
1213
|
});
|
|
760
1214
|
else if (name === "lang" && newV) void c.setConfig({ lang: newV }).catch(() => {
|
|
761
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());
|
|
762
1222
|
else if (name === "mic") void c.mic(boolAttr(this, "mic", false)).catch(() => {
|
|
763
1223
|
});
|
|
764
1224
|
else if (name === "paused") {
|
|
@@ -785,11 +1245,17 @@ function createElementClass() {
|
|
|
785
1245
|
height,
|
|
786
1246
|
position: this.getAttribute("position") ?? "inline",
|
|
787
1247
|
draggable: boolAttr(this, "draggable", false),
|
|
1248
|
+
resizable: this.resizableOption(),
|
|
1249
|
+
borderRadius: radiusAttr(this.getAttribute("border-radius")),
|
|
788
1250
|
lang: this.getAttribute("lang") ?? void 0,
|
|
789
|
-
|
|
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")),
|
|
790
1256
|
heavy: this.getAttribute("heavy") ?? void 0,
|
|
791
|
-
ui:
|
|
792
|
-
controls: boolAttr(this, "controls",
|
|
1257
|
+
ui: uiAttr(this.getAttribute("ui")),
|
|
1258
|
+
controls: boolAttr(this, "controls", true),
|
|
793
1259
|
crossOriginIsolated: boolAttr(this, "cross-origin-isolated", false)
|
|
794
1260
|
});
|
|
795
1261
|
this.client = c;
|
|
@@ -800,10 +1266,20 @@ function createElementClass() {
|
|
|
800
1266
|
fwd("stt", "xc-stt");
|
|
801
1267
|
fwd("utterance", "xc-utterance");
|
|
802
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");
|
|
803
1273
|
if (boolAttr(this, "paused", false)) c.pause();
|
|
804
1274
|
if (boolAttr(this, "mic", false)) void c.ready.then(() => c.mic(true)).catch(() => {
|
|
805
1275
|
});
|
|
806
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
|
+
}
|
|
807
1283
|
say(text, opts) {
|
|
808
1284
|
return this.requireClient().say(text, opts);
|
|
809
1285
|
}
|
|
@@ -819,6 +1295,21 @@ function createElementClass() {
|
|
|
819
1295
|
expression(name) {
|
|
820
1296
|
return this.requireClient().expression(name);
|
|
821
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
|
+
}
|
|
822
1313
|
destroy() {
|
|
823
1314
|
this.client?.destroy();
|
|
824
1315
|
this.client = null;
|