@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/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! @firetable/project-xiaochun v0.1.14 | MIT | https://github.com/FireTable/project-xiaochun */
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;
@@ -21,23 +21,39 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var src_exports = {};
23
23
  __export(src_exports, {
24
+ XC_CAMERA_RANGES: () => XC_CAMERA_RANGES,
24
25
  XC_DEFAULT_ORIGIN: () => XC_DEFAULT_ORIGIN,
25
26
  XC_DEFAULT_SRC: () => XC_DEFAULT_SRC,
26
27
  XC_FRAME_TO_HOST: () => XC_FRAME_TO_HOST,
27
28
  XC_HOST_TO_FRAME: () => XC_HOST_TO_FRAME,
29
+ XC_ID_RE: () => XC_ID_RE,
28
30
  XC_IMPLEMENTED_COMMANDS: () => XC_IMPLEMENTED_COMMANDS,
31
+ XC_LANGS: () => XC_LANGS,
29
32
  XC_PROTOCOL_MAPPING: () => XC_PROTOCOL_MAPPING,
30
33
  XC_PROTOCOL_VERSION: () => XC_PROTOCOL_VERSION,
34
+ XC_UI_AUTOHIDE_DEFAULT: () => XC_UI_AUTOHIDE_DEFAULT,
35
+ XC_UI_LEGACY_PARTS: () => XC_UI_LEGACY_PARTS,
36
+ XC_UI_PARTS: () => XC_UI_PARTS,
31
37
  XC_UNSUPPORTED_COMMANDS: () => XC_UNSUPPORTED_COMMANDS,
38
+ XC_WINDOW_CORNER_RADIUS: () => XC_WINDOW_CORNER_RADIUS,
32
39
  XIAOCHUN_ELEMENT_TAG: () => XIAOCHUN_ELEMENT_TAG,
33
40
  XIAOCHUN_PROTOCOL_SCHEME: () => XIAOCHUN_PROTOCOL_SCHEME,
34
41
  createXiaochun: () => createXiaochun,
35
42
  defineXiaochunElement: () => defineXiaochunElement,
36
43
  isXcEnvelope: () => isXcEnvelope,
44
+ isXcId: () => isXcId,
37
45
  normalizeOrigin: () => normalizeOrigin,
46
+ normalizeXcCamera: () => normalizeXcCamera,
47
+ normalizeXcUiOption: () => normalizeXcUiOption,
38
48
  parseProtocolUrl: () => parseProtocolUrl,
49
+ parseXcCameraParams: () => parseXcCameraParams,
50
+ parseXcUiAutoHide: () => parseXcUiAutoHide,
51
+ parseXcUiList: () => parseXcUiList,
52
+ parseXcUiParam: () => parseXcUiParam,
39
53
  toProtocolUrl: () => toProtocolUrl,
40
- xcMessage: () => xcMessage
54
+ xcHasOwn: () => xcHasOwn,
55
+ xcMessage: () => xcMessage,
56
+ xcUiAutoHideActive: () => xcUiAutoHideActive
41
57
  });
42
58
  module.exports = __toCommonJS(src_exports);
43
59
 
@@ -56,6 +72,9 @@ var XC_HOST_TO_FRAME = [
56
72
  "xc.lookAt",
57
73
  "xc.pointer",
58
74
  "xc.setModel",
75
+ "xc.setOutfit",
76
+ "xc.setScene",
77
+ "xc.prefetch",
59
78
  "xc.setConfig",
60
79
  "xc.mic",
61
80
  "xc.pause",
@@ -66,10 +85,16 @@ var XC_FRAME_TO_HOST = [
66
85
  "xc.ready",
67
86
  "xc.load.progress",
68
87
  "xc.loaded",
88
+ "xc.outfit-changed",
89
+ "xc.scene-changed",
90
+ "xc.lang-changed",
91
+ "xc.prefetched",
69
92
  "xc.state",
70
93
  "xc.stt",
71
94
  "xc.utterance",
72
95
  "xc.hit-region",
96
+ "xc.gesture-move",
97
+ "xc.gesture-resize",
73
98
  "xc.error"
74
99
  ];
75
100
  var XC_IMPLEMENTED_COMMANDS = [
@@ -81,6 +106,9 @@ var XC_IMPLEMENTED_COMMANDS = [
81
106
  "xc.expression",
82
107
  "xc.pointer",
83
108
  "xc.setModel",
109
+ "xc.setOutfit",
110
+ "xc.setScene",
111
+ "xc.prefetch",
84
112
  "xc.setConfig",
85
113
  "xc.mic",
86
114
  "xc.pause",
@@ -95,6 +123,109 @@ var XC_PROTOCOL_MAPPING = [
95
123
  { xc: "xc.audio", note: "source \u4E3A ArrayBuffer / Blob", action: "audio", url: null },
96
124
  { xc: "xc.audio.chunk / xc.audio.end", note: "\u6D41\u5F0F PCM", action: "audio", url: null }
97
125
  ];
126
+ var XC_WINDOW_CORNER_RADIUS = 20;
127
+ var XC_UI_PARTS = ["chat", "bubble", "outfit", "scene", "lang", "github"];
128
+ var XC_UI_PART_SET = new Set(XC_UI_PARTS);
129
+ var XC_UI_LEGACY_PARTS = ["chat", "bubble"];
130
+ function parseXcUiList(items) {
131
+ const seen = /* @__PURE__ */ new Set();
132
+ const unknown = [];
133
+ for (const it of items) {
134
+ const name = typeof it === "string" ? it.trim().toLowerCase() : "";
135
+ if (XC_UI_PART_SET.has(name)) seen.add(name);
136
+ else unknown.push(typeof it === "string" ? it.slice(0, 40) : String(typeof it));
137
+ }
138
+ return { parts: XC_UI_PARTS.filter((p) => seen.has(p)), unknown, legacy: false };
139
+ }
140
+ function parseXcUiParam(raw) {
141
+ const v = (raw ?? "").trim().toLowerCase();
142
+ if (v === "" || v === "0" || v === "false") return { parts: [], unknown: [], legacy: false };
143
+ if (v === "1" || v === "true") return { parts: [...XC_UI_LEGACY_PARTS], unknown: [], legacy: true };
144
+ return parseXcUiList(v.split(",").filter((x) => x.trim() !== ""));
145
+ }
146
+ function normalizeXcUiOption(v) {
147
+ if (v === true) return { parts: [...XC_UI_LEGACY_PARTS], unknown: [], legacy: true };
148
+ if (Array.isArray(v)) return parseXcUiList(v);
149
+ return { parts: [], unknown: [], legacy: false };
150
+ }
151
+ var XC_UI_AUTOHIDE_DEFAULT = "transparent";
152
+ function parseXcUiAutoHide(raw) {
153
+ if (raw === true || raw === false || raw === "transparent") return raw;
154
+ if (typeof raw !== "string") return void 0;
155
+ const v = raw.trim().toLowerCase();
156
+ if (v === "1" || v === "true") return true;
157
+ if (v === "0" || v === "false") return false;
158
+ if (v === "transparent") return "transparent";
159
+ return void 0;
160
+ }
161
+ function xcUiAutoHideActive(mode, sceneTransparent) {
162
+ return mode === "transparent" ? sceneTransparent : mode;
163
+ }
164
+ var XC_CAMERA_RANGES = {
165
+ fov: [15, 60],
166
+ distance: [1, 15],
167
+ height: [-1, 1]
168
+ };
169
+ var XC_CAMERA_NUM_KEYS = ["fov", "distance", "height"];
170
+ var XC_CAMERA_KEYS = ["fov", "distance", "height", "intro"];
171
+ function normalizeXcCamera(raw) {
172
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return { ok: false, error: "camera must be an object" };
173
+ const src = raw;
174
+ for (const k of Object.keys(src)) {
175
+ if (!XC_CAMERA_KEYS.includes(k)) return { ok: false, error: `unknown camera key: ${k} (expected ${XC_CAMERA_KEYS.join(" | ")})` };
176
+ }
177
+ const out = {};
178
+ const clamped = [];
179
+ for (const k of XC_CAMERA_NUM_KEYS) {
180
+ const v = src[k];
181
+ if (v === void 0) continue;
182
+ if (v === null) {
183
+ out[k] = null;
184
+ continue;
185
+ }
186
+ if (typeof v !== "number" || !Number.isFinite(v)) return { ok: false, error: `camera.${k} must be a finite number or null` };
187
+ const [lo, hi] = XC_CAMERA_RANGES[k];
188
+ const c = Math.min(hi, Math.max(lo, v));
189
+ if (c !== v) clamped.push(k);
190
+ out[k] = c;
191
+ }
192
+ if (src.intro !== void 0) {
193
+ if (src.intro !== null && typeof src.intro !== "boolean") return { ok: false, error: "camera.intro must be a boolean or null" };
194
+ out.intro = src.intro;
195
+ }
196
+ return { ok: true, camera: out, clamped };
197
+ }
198
+ function parseXcCameraParams(get) {
199
+ const camera = {};
200
+ const warnings = [];
201
+ const names = { fov: "cameraFov", distance: "cameraDistance", height: "cameraHeight" };
202
+ for (const k of XC_CAMERA_NUM_KEYS) {
203
+ const raw = get(names[k]);
204
+ if (raw === null) continue;
205
+ const t = raw.trim();
206
+ const n = t === "" ? NaN : Number(t);
207
+ if (!Number.isFinite(n)) {
208
+ warnings.push(`ignoring invalid ?${names[k]}=${raw}`);
209
+ continue;
210
+ }
211
+ const [lo, hi] = XC_CAMERA_RANGES[k];
212
+ const c = Math.min(hi, Math.max(lo, n));
213
+ if (c !== n) warnings.push(`?${names[k]}=${raw} is out of range [${lo}, ${hi}], clamped to ${c}`);
214
+ camera[k] = c;
215
+ }
216
+ const intro = get("cameraIntro");
217
+ if (intro !== null) {
218
+ const v = intro.trim().toLowerCase();
219
+ if (v === "1" || v === "true") camera.intro = true;
220
+ else if (v === "0" || v === "false") camera.intro = false;
221
+ else warnings.push(`ignoring invalid ?cameraIntro=${intro}`);
222
+ }
223
+ return { camera, warnings };
224
+ }
225
+ var XC_ID_RE = /^[a-z][a-z0-9_]{0,63}$/;
226
+ var isXcId = (v) => typeof v === "string" && XC_ID_RE.test(v);
227
+ var xcHasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
228
+ var XC_LANGS = ["zh-CN", "en", "ja"];
98
229
  function xcMessage(type, payload, id) {
99
230
  const m = { type, v: XC_PROTOCOL_VERSION, payload };
100
231
  if (id !== void 0) m.id = id;
@@ -116,11 +247,113 @@ function normalizeOrigin(input) {
116
247
  }
117
248
  }
118
249
 
250
+ // src/gesture-box.ts
251
+ var DEFAULT_RESIZE_LIMITS = Object.freeze({
252
+ minWidth: 120,
253
+ minHeight: 180,
254
+ maxWidth: Infinity,
255
+ maxHeight: Infinity
256
+ });
257
+ var finitePositive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
258
+ function normalizeResizable(v) {
259
+ if (!v) return null;
260
+ const o = v === true ? {} : v;
261
+ const minWidth = finitePositive(o.minWidth) ? o.minWidth : DEFAULT_RESIZE_LIMITS.minWidth;
262
+ const minHeight = finitePositive(o.minHeight) ? o.minHeight : DEFAULT_RESIZE_LIMITS.minHeight;
263
+ const maxWidth = finitePositive(o.maxWidth) ? Math.max(o.maxWidth, minWidth) : Infinity;
264
+ const maxHeight = finitePositive(o.maxHeight) ? Math.max(o.maxHeight, minHeight) : Infinity;
265
+ return { minWidth, minHeight, maxWidth, maxHeight };
266
+ }
267
+ var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
268
+ function moveBox(start, totalDx, totalDy, vp) {
269
+ return {
270
+ left: clamp(start.left + totalDx, 0, vp.width - start.width),
271
+ top: clamp(start.top + totalDy, 0, vp.height - start.height),
272
+ width: start.width,
273
+ height: start.height
274
+ };
275
+ }
276
+ function resizeBox(start, corner, totalDx, totalDy, limits, vp) {
277
+ const west = corner === "NW" || corner === "SW";
278
+ const north = corner === "NW" || corner === "NE";
279
+ const right = start.left + start.width, bottom = start.top + start.height;
280
+ const wantW = west ? start.width - totalDx : start.width + totalDx;
281
+ const wantH = north ? start.height - totalDy : start.height + totalDy;
282
+ const roomW = west ? right : vp.width - start.left;
283
+ const roomH = north ? bottom : vp.height - start.top;
284
+ const width = clamp(wantW, limits.minWidth, Math.min(limits.maxWidth, roomW));
285
+ const height = clamp(wantH, limits.minHeight, Math.min(limits.maxHeight, roomH));
286
+ return { left: west ? right - width : start.left, top: north ? bottom - height : start.top, width, height };
287
+ }
288
+ function fitBox(b, vp) {
289
+ const width = Math.min(b.width, Math.max(1, vp.width)), height = Math.min(b.height, Math.max(1, vp.height));
290
+ return { left: clamp(b.left, 0, vp.width - width), top: clamp(b.top, 0, vp.height - height), width, height };
291
+ }
292
+ var CORNERS = /* @__PURE__ */ new Set(["NW", "NE", "SW", "SE"]);
293
+ var PHASES = /* @__PURE__ */ new Set(["start", "move", "end"]);
294
+ var REASONS = /* @__PURE__ */ new Set(["up", "cancel", "blur"]);
295
+ var MAX_GESTURE_DELTA = 2e4;
296
+ var delta = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= MAX_GESTURE_DELTA;
297
+ var counter = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
298
+ function parseGesturePayload(kind, raw) {
299
+ if (raw === null || typeof raw !== "object") return null;
300
+ const p = raw;
301
+ if (!counter(p.gesture) || !counter(p.seq) || typeof p.phase !== "string" || !PHASES.has(p.phase)) return null;
302
+ if (!delta(p.dx) || !delta(p.dy) || !delta(p.totalDx) || !delta(p.totalDy)) return null;
303
+ if (p.reason !== void 0 && (typeof p.reason !== "string" || !REASONS.has(p.reason))) return null;
304
+ const out = { gesture: p.gesture, seq: p.seq, phase: p.phase, totalDx: p.totalDx, totalDy: p.totalDy };
305
+ if (kind === "resize") {
306
+ if (typeof p.corner !== "string" || !CORNERS.has(p.corner)) return null;
307
+ out.corner = p.corner;
308
+ }
309
+ return out;
310
+ }
311
+ var GestureGate = class {
312
+ constructor() {
313
+ this.lastGesture = 0;
314
+ this.lastSeq = -1;
315
+ this.open = false;
316
+ }
317
+ get active() {
318
+ return this.open;
319
+ }
320
+ accept(p) {
321
+ if (p.phase === "start") {
322
+ if (p.gesture <= this.lastGesture || p.seq !== 0) return false;
323
+ this.lastGesture = p.gesture;
324
+ this.lastSeq = 0;
325
+ this.open = true;
326
+ return true;
327
+ }
328
+ if (!this.open || p.gesture !== this.lastGesture || p.seq <= this.lastSeq) return false;
329
+ this.lastSeq = p.seq;
330
+ if (p.phase === "end") this.open = false;
331
+ return true;
332
+ }
333
+ /** 宿主主动结束 (关闭手势 / 销毁): 之后同一手势的迟到消息被拒绝。 */
334
+ close() {
335
+ this.open = false;
336
+ }
337
+ };
338
+
119
339
  // src/client.ts
120
340
  var FALLBACK_PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
121
341
  '<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>'
122
342
  );
343
+ var noSelect = (st) => {
344
+ st.setProperty("user-select", "none");
345
+ st.setProperty("-webkit-user-select", "none");
346
+ st.setProperty("-webkit-touch-callout", "none");
347
+ st.setProperty("-webkit-tap-highlight-color", "transparent");
348
+ };
123
349
  var css = (v, d) => v === void 0 ? d : typeof v === "number" ? `${v}px` : v;
350
+ var XC_DEFAULT_WIDTH = 600;
351
+ var XC_DEFAULT_HEIGHT = 1080;
352
+ var defaultWidthCss = (floating = false) => floating ? `min(${XC_DEFAULT_WIDTH}px, calc(100vw - 2 * var(--xc-offset-x, 16px)))` : `min(${XC_DEFAULT_WIDTH}px, 100vw)`;
353
+ var defaultHeightCss = (floating = false) => {
354
+ const unit = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("height", "1svh") ? "svh" : "vh";
355
+ return floating ? `min(${XC_DEFAULT_HEIGHT}px, calc(100${unit} - 2 * var(--xc-offset-y, 16px)))` : `min(${XC_DEFAULT_HEIGHT}px, 100${unit})`;
356
+ };
124
357
  function createXiaochun(options) {
125
358
  if (typeof window === "undefined" || typeof document === "undefined") {
126
359
  throw new Error("[project-xiaochun] createXiaochun must run in a browser");
@@ -142,8 +375,56 @@ function createXiaochun(options) {
142
375
  if (!n) throw new Error(`[project-xiaochun] invalid allowedOrigins entry: ${o} (wildcards are not allowed)`);
143
376
  trusted.add(n);
144
377
  }
145
- const transparent = options.transparent ?? false;
146
- const passthrough = options.passthrough ?? transparent;
378
+ const prefsKey = options.persist === "host" ? "xiaochun:prefs" : typeof options.persist === "string" && options.persist ? options.persist : null;
379
+ const readPrefs = () => {
380
+ if (!prefsKey) return {};
381
+ try {
382
+ const raw = JSON.parse(window.localStorage.getItem(prefsKey) ?? "null");
383
+ const out = {};
384
+ if (raw && typeof raw === "object") {
385
+ if (isXcId(raw.outfit)) out.outfit = raw.outfit;
386
+ if (isXcId(raw.scene)) out.scene = raw.scene;
387
+ }
388
+ return out;
389
+ } catch {
390
+ return {};
391
+ }
392
+ };
393
+ const writePrefs = (patch) => {
394
+ if (!prefsKey) return;
395
+ try {
396
+ const next = { ...readPrefs() };
397
+ for (const [k, v] of Object.entries(patch)) {
398
+ if (v) next[k] = v;
399
+ else delete next[k];
400
+ }
401
+ window.localStorage.setItem(prefsKey, JSON.stringify(next));
402
+ } catch {
403
+ }
404
+ };
405
+ const prefs = readPrefs();
406
+ if (options.model !== void 0 && options.outfit === void 0) {
407
+ console.warn('[project-xiaochun] option "model" is deprecated, use "outfit" (built-in outfit id).');
408
+ }
409
+ const uiParse = normalizeXcUiOption(options.ui);
410
+ const uiParts = uiParse.parts;
411
+ const uiAutoHide = parseXcUiAutoHide(options.uiAutoHide);
412
+ if (options.uiAutoHide !== void 0 && uiAutoHide === void 0) console.warn(`[project-xiaochun] ignoring invalid uiAutoHide: ${String(options.uiAutoHide)} (use true | false | 'transparent')`);
413
+ let camera = {};
414
+ if (options.camera !== void 0) {
415
+ const r = normalizeXcCamera(options.camera);
416
+ if (r.ok) {
417
+ camera = r.camera;
418
+ 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")})`);
419
+ } else console.warn(`[project-xiaochun] ignoring invalid camera option: ${r.error}`);
420
+ }
421
+ if (uiParse.legacy) console.warn(`[project-xiaochun] option "ui: true" is deprecated, use an array of parts, e.g. ui: ['chat', 'bubble', 'outfit', 'scene'].`);
422
+ if (uiParse.unknown.length) console.warn(`[project-xiaochun] ignoring unknown ui part(s): ${uiParse.unknown.join(", ")} (known: ${XC_UI_PARTS.join(", ")})`);
423
+ const outfitOpt = options.outfit ?? (isXcId(options.model) ? options.model : void 0);
424
+ const initialOutfit = outfitOpt ?? prefs.outfit;
425
+ const initialScene = options.scene ?? prefs.scene ?? (options.transparent ? "transparent" : void 0);
426
+ let transparent = initialScene === "transparent" || initialScene !== "light" && initialScene !== "dark" && (options.transparent ?? false);
427
+ let passthrough = options.passthrough ?? transparent;
147
428
  const lazy = options.lazy ?? true;
148
429
  const position = options.position ?? "inline";
149
430
  const handshakeTimeout = options.handshakeTimeout ?? 2e4;
@@ -151,14 +432,26 @@ function createXiaochun(options) {
151
432
  wrapper.setAttribute("data-xiaochun", "");
152
433
  const ws = wrapper.style;
153
434
  ws.position = position === "inline" ? "relative" : "fixed";
154
- ws.width = css(options.width, "320px");
155
- ws.height = css(options.height, "480px");
435
+ ws.width = css(options.width, defaultWidthCss(position !== "inline"));
436
+ ws.height = css(options.height, defaultHeightCss(position !== "inline"));
156
437
  ws.maxWidth = "100%";
157
438
  ws.overflow = "visible";
158
439
  ws.contain = "layout style";
159
- ws.borderRadius = "var(--xc-radius, 0)";
440
+ noSelect(ws);
160
441
  ws.boxShadow = "var(--xc-shadow, none)";
161
- if (!transparent) ws.background = "var(--xc-bg, transparent)";
442
+ const applyWrapperBg = () => {
443
+ ws.background = transparent ? "" : "var(--xc-bg, transparent)";
444
+ };
445
+ applyWrapperBg();
446
+ let radiusOpt = options.borderRadius;
447
+ const radiusValue = () => radiusOpt !== void 0 ? css(radiusOpt, "0px") : transparent ? "0px" : `${XC_WINDOW_CORNER_RADIUS}px`;
448
+ const applyRadius = () => {
449
+ const v = `var(--xc-radius, ${radiusValue()})`;
450
+ ws.borderRadius = v;
451
+ if (placeholderEl) placeholderEl.style.borderRadius = v;
452
+ if (iframe) iframe.style.borderRadius = v;
453
+ };
454
+ ws.borderRadius = `var(--xc-radius, ${radiusValue()})`;
162
455
  if (position === "bottom-right") {
163
456
  ws.right = "var(--xc-offset-x, 16px)";
164
457
  ws.bottom = "var(--xc-offset-y, 16px)";
@@ -179,13 +472,13 @@ function createXiaochun(options) {
179
472
  img.src = options.placeholder || FALLBACK_PLACEHOLDER;
180
473
  img.alt = "Project XiaoChun";
181
474
  img.decoding = "async";
182
- img.width = 320;
183
- img.height = 480;
475
+ img.width = XC_DEFAULT_WIDTH;
476
+ img.height = XC_DEFAULT_HEIGHT;
184
477
  placeholderEl = img;
185
478
  }
186
479
  const ps = placeholderEl.style;
187
480
  placeholderEl.setAttribute("part", "placeholder");
188
- ps.borderRadius = "var(--xc-radius, 0)";
481
+ ps.borderRadius = `var(--xc-radius, ${radiusValue()})`;
189
482
  ps.position = "absolute";
190
483
  ps.inset = "0";
191
484
  ps.width = "100%";
@@ -194,6 +487,7 @@ function createXiaochun(options) {
194
487
  ps.transition = "opacity .25s ease";
195
488
  ps.pointerEvents = "auto";
196
489
  ps.cursor = lazy === false ? "default" : "pointer";
490
+ noSelect(ps);
197
491
  placeholderEl.addEventListener("click", () => activate());
198
492
  wrapper.appendChild(placeholderEl);
199
493
  }
@@ -222,25 +516,45 @@ function createXiaochun(options) {
222
516
  let autoPaused = false;
223
517
  let loaded = false;
224
518
  let lastHit = false;
519
+ let caps = null;
520
+ let currentOutfit = null;
521
+ let currentScene = null;
225
522
  const queue = [];
226
523
  const pending = /* @__PURE__ */ new Map();
227
524
  let idSeq = 0;
228
525
  let handshakeTimer = null;
229
526
  let io = null;
230
527
  let idleHandle = null;
231
- const postToFrame = (type, payload, id, transfer) => {
528
+ let resolveHandshake;
529
+ const handshaked = new Promise((res) => {
530
+ resolveHandshake = res;
531
+ });
532
+ const failLocal = (type, id, code, message) => {
533
+ emit("error", { code, message, command: type });
534
+ const p = id ? pending.get(id) : void 0;
535
+ if (p && id) {
536
+ pending.delete(id);
537
+ p.reject(new Error(`[${code}] ${message}`));
538
+ }
539
+ };
540
+ 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)`);
541
+ const postToFrame = (type, payload, id, transfer, requires) => {
232
542
  if (!port) {
233
- queue.push({ type, payload, id, transfer });
543
+ queue.push({ type, payload, id, transfer, requires });
544
+ return;
545
+ }
546
+ if (requires && !caps?.[requires]) {
547
+ unsupportedByFrame(type, id);
234
548
  return;
235
549
  }
236
550
  port.postMessage(xcMessage(type, payload, id), transfer ?? []);
237
551
  };
238
- const send = (type, payload, wait = false, transfer, fixedId) => {
552
+ const send = (type, payload, wait = false, transfer, fixedId, requires) => {
239
553
  if (destroyed) return Promise.reject(new Error("[project-xiaochun] instance destroyed"));
240
554
  if (!iframe) activate();
241
555
  const id = fixedId ?? `c${++idSeq}`;
242
556
  const p = wait ? new Promise((resolve, reject) => pending.set(id, { resolve, reject, sawEnd: false })) : Promise.resolve();
243
- postToFrame(type, payload, id, transfer);
557
+ postToFrame(type, payload, id, transfer, requires);
244
558
  return p;
245
559
  };
246
560
  const toBuffer = (d) => d instanceof ArrayBuffer ? d : d.buffer.slice(d.byteOffset, d.byteOffset + d.byteLength);
@@ -290,7 +604,7 @@ function createXiaochun(options) {
290
604
  let ended = false;
291
605
  let fmt = rest.format === "pcm16" || rest.format === "float32" ? rest.format : null;
292
606
  let done = Promise.resolve();
293
- const handle2 = {
607
+ const handle = {
294
608
  write(data) {
295
609
  if (ended) throw new Error("[project-xiaochun] audio stream already ended");
296
610
  if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
@@ -324,17 +638,23 @@ function createXiaochun(options) {
324
638
  return done;
325
639
  }
326
640
  };
327
- return handle2;
641
+ return handle;
328
642
  };
329
643
  function buildUrl() {
330
644
  const u = new URL(srcUrl.href);
331
645
  u.searchParams.set("host", window.location.origin);
332
646
  if (transparent) u.searchParams.set("transparent", "1");
333
- if (options.ui) u.searchParams.set("ui", "1");
647
+ if (uiParts.length) u.searchParams.set("ui", uiParts.join(","));
334
648
  if (options.lang) u.searchParams.set("lang", options.lang);
649
+ if (uiAutoHide !== void 0) u.searchParams.set("uiAutoHide", String(uiAutoHide));
650
+ if (typeof camera.fov === "number") u.searchParams.set("cameraFov", String(camera.fov));
651
+ if (typeof camera.distance === "number") u.searchParams.set("cameraDistance", String(camera.distance));
652
+ if (typeof camera.height === "number") u.searchParams.set("cameraHeight", String(camera.height));
653
+ if (typeof camera.intro === "boolean") u.searchParams.set("cameraIntro", camera.intro ? "1" : "0");
335
654
  if (options.heavy) u.searchParams.set("heavy", options.heavy);
336
- if (options.controls) u.searchParams.set("controls", "1");
337
- if (options.model) u.searchParams.set("outfit", options.model);
655
+ if (options.controls === false) u.searchParams.set("controls", "0");
656
+ if (initialScene && isXcId(initialScene)) u.searchParams.set("scene", initialScene);
657
+ if (initialOutfit) u.searchParams.set("outfit", initialOutfit);
338
658
  return u.href;
339
659
  }
340
660
  function activate() {
@@ -360,8 +680,9 @@ function createXiaochun(options) {
360
680
  s.border = "0";
361
681
  s.background = "transparent";
362
682
  s.colorScheme = "normal";
363
- s.borderRadius = "var(--xc-radius, 0)";
683
+ s.borderRadius = `var(--xc-radius, ${radiusValue()})`;
364
684
  f.setAttribute("part", "iframe");
685
+ noSelect(s);
365
686
  s.opacity = "0";
366
687
  s.transition = "opacity .25s ease";
367
688
  s.pointerEvents = passthrough ? "none" : "auto";
@@ -373,7 +694,7 @@ function createXiaochun(options) {
373
694
  if (!port) emit("error", { code: "timeout", message: `no xc.ready from ${frameOrigin} within ${handshakeTimeout}ms` });
374
695
  }, handshakeTimeout);
375
696
  setupAutoPause();
376
- setupPassthrough();
697
+ if (passthrough) enablePassthrough();
377
698
  }
378
699
  const onWindowMessage = (ev) => {
379
700
  if (destroyed || !iframe || ev.source !== iframe.contentWindow) return;
@@ -386,20 +707,39 @@ function createXiaochun(options) {
386
707
  const ch = new MessageChannel();
387
708
  port = ch.port1;
388
709
  port.onmessage = onPortMessage;
710
+ caps = data.payload?.capabilities ?? null;
711
+ const gWant = wantGestures();
712
+ sentGestures = gWant;
713
+ const initConfig = {
714
+ ...options.allowCustomModel ? { allowCustomModel: true } : {},
715
+ ...gWant.move || gWant.resize ? { gestures: gWant } : {}
716
+ };
389
717
  iframe.contentWindow.postMessage(
390
- xcMessage("xc.init", { hostOrigin: window.location.origin }),
718
+ xcMessage("xc.init", { hostOrigin: window.location.origin, ...Object.keys(initConfig).length ? { config: initConfig } : {} }),
391
719
  ev.origin,
392
720
  [ch.port2]
393
721
  );
722
+ warnGestureUnsupported();
394
723
  if (handshakeTimer) {
395
724
  clearTimeout(handshakeTimer);
396
725
  handshakeTimer = null;
397
726
  }
398
727
  emit("handshake", data.payload);
399
- for (const m of queue.splice(0)) port.postMessage(xcMessage(m.type, m.payload, m.id), m.transfer ?? []);
728
+ resolveHandshake();
729
+ for (const m of queue.splice(0)) {
730
+ if (m.requires && !caps?.[m.requires]) unsupportedByFrame(m.type, m.id);
731
+ else port.postMessage(xcMessage(m.type, m.payload, m.id), m.transfer ?? []);
732
+ }
400
733
  syncPause();
401
734
  };
402
735
  window.addEventListener("message", onWindowMessage);
736
+ function ack(id, value) {
737
+ const p = id ? pending.get(id) : void 0;
738
+ if (p && id) {
739
+ pending.delete(id);
740
+ p.resolve(value);
741
+ }
742
+ }
403
743
  function onPortMessage(ev) {
404
744
  const data = ev.data;
405
745
  if (!isXcEnvelope(data)) return;
@@ -410,6 +750,11 @@ function createXiaochun(options) {
410
750
  case "xc.loaded":
411
751
  if (!loaded) {
412
752
  loaded = true;
753
+ if (options.prefetch && options.heavy === "eager" && caps?.prefetch) {
754
+ const ids = Array.isArray(options.prefetch) ? options.prefetch : void 0;
755
+ void api.prefetch(ids).catch(() => {
756
+ });
757
+ }
413
758
  if (iframe) iframe.style.opacity = "1";
414
759
  if (placeholderEl) {
415
760
  const el = placeholderEl;
@@ -422,6 +767,33 @@ function createXiaochun(options) {
422
767
  }
423
768
  emit("ready", data.payload);
424
769
  break;
770
+ case "xc.outfit-changed": {
771
+ const pl = data.payload;
772
+ if (!pl.noop) {
773
+ currentOutfit = pl.id;
774
+ emit("outfit-changed", pl);
775
+ if (!pl.initial && pl.id) writePrefs({ outfit: pl.id });
776
+ }
777
+ ack(data.id);
778
+ break;
779
+ }
780
+ case "xc.scene-changed": {
781
+ const pl = data.payload;
782
+ currentScene = pl.id;
783
+ if (pl.transparent !== transparent) applyTransparency(pl.transparent);
784
+ if (!pl.noop) {
785
+ emit("scene-changed", pl);
786
+ if (!pl.initial) writePrefs({ scene: pl.id });
787
+ }
788
+ ack(data.id);
789
+ break;
790
+ }
791
+ case "xc.lang-changed":
792
+ emit("lang-changed", data.payload);
793
+ break;
794
+ case "xc.prefetched":
795
+ ack(data.id, data.payload);
796
+ break;
425
797
  case "xc.state":
426
798
  emit("state", data.payload);
427
799
  break;
@@ -446,9 +818,16 @@ function createXiaochun(options) {
446
818
  }
447
819
  break;
448
820
  }
821
+ case "xc.gesture-move":
822
+ onGesture("move", data.payload);
823
+ break;
824
+ case "xc.gesture-resize":
825
+ onGesture("resize", data.payload);
826
+ break;
449
827
  case "xc.error": {
450
828
  const { code, message, command } = data.payload;
451
829
  emit("error", { code, message, command });
830
+ if (code === "unknown_id" && (command === "outfit" || command === "scene")) writePrefs({ [command]: null });
452
831
  const p = data.id ? pending.get(data.id) : void 0;
453
832
  if (p && data.id) {
454
833
  pending.delete(data.id);
@@ -505,16 +884,46 @@ function createXiaochun(options) {
505
884
  port.postMessage(xcMessage("xc.pointer", { x, y }));
506
885
  });
507
886
  };
508
- function setupPassthrough() {
509
- if (!passthrough) return;
887
+ let passthroughActive = false;
888
+ function enablePassthrough() {
889
+ if (passthroughActive || !iframe) return;
890
+ passthroughActive = true;
510
891
  window.addEventListener("pointermove", onHostPointer, { passive: true });
892
+ window.addEventListener("pointerdown", onHostPointer, { passive: true });
511
893
  window.addEventListener("scroll", invalidateRect, { passive: true, capture: true });
512
894
  window.addEventListener("resize", invalidateRect, { passive: true });
513
- if (typeof ResizeObserver !== "undefined" && iframe) {
895
+ if (typeof ResizeObserver !== "undefined") {
514
896
  rectRo = new ResizeObserver(invalidateRect);
515
897
  rectRo.observe(iframe);
516
898
  }
517
899
  }
900
+ function disablePassthrough() {
901
+ if (!passthroughActive) return;
902
+ passthroughActive = false;
903
+ window.removeEventListener("pointermove", onHostPointer);
904
+ window.removeEventListener("pointerdown", onHostPointer);
905
+ window.removeEventListener("scroll", invalidateRect, true);
906
+ window.removeEventListener("resize", invalidateRect);
907
+ rectRo?.disconnect();
908
+ rectRo = null;
909
+ if (pointerRaf) {
910
+ cancelAnimationFrame(pointerRaf);
911
+ pointerRaf = 0;
912
+ }
913
+ lastPtr = null;
914
+ }
915
+ function applyTransparency(next) {
916
+ transparent = next;
917
+ applyWrapperBg();
918
+ applyRadius();
919
+ passthrough = next ? options.passthrough ?? true : false;
920
+ lastHit = false;
921
+ lastPosted = null;
922
+ rectCache = null;
923
+ if (passthrough) enablePassthrough();
924
+ else disablePassthrough();
925
+ if (iframe) iframe.style.pointerEvents = passthrough ? "none" : "auto";
926
+ }
518
927
  let pauseIo = null;
519
928
  function setupAutoPause() {
520
929
  if (options.autoPause === false || typeof IntersectionObserver === "undefined") return;
@@ -566,50 +975,177 @@ function createXiaochun(options) {
566
975
  }, { rootMargin: `${options.lazyMargin ?? 200}px` });
567
976
  io.observe(wrapper);
568
977
  }
569
- let handle = null;
570
- if (options.draggable && position !== "inline") {
571
- handle = document.createElement("div");
572
- handle.setAttribute("aria-label", "drag");
573
- Object.assign(handle.style, {
574
- position: "absolute",
575
- left: "0",
576
- top: "0",
577
- width: "28px",
578
- height: "28px",
579
- cursor: "grab",
580
- pointerEvents: "auto",
581
- borderRadius: "8px",
582
- background: "rgba(0,0,0,.25)",
583
- color: "#fff",
584
- font: "14px/28px system-ui",
585
- textAlign: "center",
586
- userSelect: "none",
587
- touchAction: "none",
588
- zIndex: "2",
589
- opacity: "0.5"
590
- });
591
- handle.textContent = "\u283F";
592
- let drag = null;
593
- handle.addEventListener("pointerdown", (e) => {
594
- const r = wrapper.getBoundingClientRect();
595
- drag = { dx: e.clientX - r.left, dy: e.clientY - r.top };
596
- handle.setPointerCapture(e.pointerId);
597
- });
598
- handle.addEventListener("pointermove", (e) => {
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`;
978
+ let draggableOn = options.draggable === true;
979
+ let resizeLimits = normalizeResizable(options.resizable);
980
+ const gates = { move: new GestureGate(), resize: new GestureGate() };
981
+ let activeGesture = null;
982
+ let inlineShift = { x: 0, y: 0 };
983
+ let userMoved = false;
984
+ let sentGestures = null;
985
+ let warnedGesture = false;
986
+ const viewport = () => ({
987
+ width: document.documentElement.clientWidth || window.innerWidth,
988
+ height: document.documentElement.clientHeight || window.innerHeight
989
+ });
990
+ const readBox = () => {
991
+ const r = wrapper.getBoundingClientRect();
992
+ return { left: r.left, top: r.top, width: r.width, height: r.height };
993
+ };
994
+ const wantGestures = () => ({ move: draggableOn && caps?.gestures?.move === true, resize: resizeLimits !== null && caps?.gestures?.resize === true });
995
+ function applyBox(b, inlineBase, withSize) {
996
+ if (withSize) {
997
+ ws.width = `${b.width}px`;
998
+ ws.height = `${b.height}px`;
999
+ ws.maxWidth = "none";
1000
+ }
1001
+ if (position === "inline") {
1002
+ const base = inlineBase ?? { left: b.left - inlineShift.x, top: b.top - inlineShift.y };
1003
+ inlineShift = { x: b.left - base.left, y: b.top - base.top };
1004
+ ws.setProperty("translate", `${inlineShift.x}px ${inlineShift.y}px`);
1005
+ } else {
1006
+ ws.left = `${b.left}px`;
1007
+ ws.top = `${b.top}px`;
602
1008
  ws.right = "auto";
603
1009
  ws.bottom = "auto";
604
- invalidateRect();
605
- });
606
- const end = () => {
607
- drag = null;
608
- };
609
- handle.addEventListener("pointerup", end);
610
- handle.addEventListener("pointercancel", end);
611
- wrapper.appendChild(handle);
1010
+ }
1011
+ userMoved = true;
1012
+ invalidateRect();
1013
+ }
1014
+ let selectionLock = null;
1015
+ function lockSelection(on) {
1016
+ const de = document.documentElement;
1017
+ if (on && !selectionLock) {
1018
+ selectionLock = { us: de.style.getPropertyValue("user-select"), wus: de.style.getPropertyValue("-webkit-user-select") };
1019
+ de.style.setProperty("user-select", "none");
1020
+ de.style.setProperty("-webkit-user-select", "none");
1021
+ try {
1022
+ window.getSelection()?.removeAllRanges();
1023
+ } catch {
1024
+ }
1025
+ } else if (!on && selectionLock) {
1026
+ de.style.setProperty("user-select", selectionLock.us);
1027
+ de.style.setProperty("-webkit-user-select", selectionLock.wus);
1028
+ selectionLock = null;
1029
+ }
1030
+ }
1031
+ function endActiveGesture(kind) {
1032
+ if (!activeGesture || kind && activeGesture.kind !== kind) return;
1033
+ gates[activeGesture.kind].close();
1034
+ const b = readBox();
1035
+ emit(activeGesture.kind, { phase: "end", ...b });
1036
+ activeGesture = null;
1037
+ lockSelection(false);
1038
+ saveBox();
1039
+ }
1040
+ function onGesture(kind, raw) {
1041
+ if (destroyed || !caps?.gestures) return;
1042
+ if (kind === "move" ? !draggableOn : resizeLimits === null) return;
1043
+ const p = parseGesturePayload(kind, raw);
1044
+ if (!p) return;
1045
+ if (p.phase === "start" ? activeGesture !== null : activeGesture?.kind !== kind) return;
1046
+ if (!gates[kind].accept(p)) return;
1047
+ if (p.phase === "start") {
1048
+ const start = readBox();
1049
+ activeGesture = {
1050
+ kind,
1051
+ start,
1052
+ corner: p.corner,
1053
+ inlineBase: position === "inline" ? { left: start.left - inlineShift.x, top: start.top - inlineShift.y } : null
1054
+ };
1055
+ lockSelection(true);
1056
+ emit(kind, { phase: "start", ...start });
1057
+ return;
1058
+ }
1059
+ const a = activeGesture;
1060
+ const vp = viewport();
1061
+ const next = kind === "move" ? moveBox(a.start, p.totalDx, p.totalDy, vp) : resizeBox(a.start, a.corner, p.totalDx, p.totalDy, resizeLimits, vp);
1062
+ applyBox(next, a.inlineBase, kind === "resize");
1063
+ emit(kind, { phase: p.phase, ...next });
1064
+ if (p.phase === "end") {
1065
+ activeGesture = null;
1066
+ lockSelection(false);
1067
+ saveBox();
1068
+ }
612
1069
  }
1070
+ function syncGestures() {
1071
+ if (destroyed) return;
1072
+ const want = wantGestures();
1073
+ if (!want.move) endActiveGesture("move");
1074
+ if (!want.resize) endActiveGesture("resize");
1075
+ if (!port || !caps) return;
1076
+ warnGestureUnsupported();
1077
+ if (sentGestures && sentGestures.move === want.move && sentGestures.resize === want.resize) return;
1078
+ sentGestures = want;
1079
+ if (caps.gestures) port.postMessage(xcMessage("xc.setConfig", { gestures: want }));
1080
+ }
1081
+ function warnGestureUnsupported() {
1082
+ if (warnedGesture || !caps || caps.gestures) return;
1083
+ const needMove = draggableOn;
1084
+ if (!needMove && resizeLimits === null) return;
1085
+ warnedGesture = true;
1086
+ const what = [needMove ? "draggable" : "", resizeLimits !== null ? "resizable" : ""].filter(Boolean).join(" / ");
1087
+ emit("error", { code: "unsupported", message: `${what} needs gesture support (capabilities.gestures), which this /embed deployment does not advertise (older version)`, command: "gestures" });
1088
+ }
1089
+ const onWindowResize = () => {
1090
+ if (!userMoved || activeGesture || destroyed) return;
1091
+ const cur = readBox();
1092
+ const vp = viewport();
1093
+ 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);
1094
+ if (fit.left === cur.left && fit.top === cur.top && fit.width === cur.width && fit.height === cur.height) return;
1095
+ applyBox(fit, position === "inline" ? { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y } : null, fit.width !== cur.width || fit.height !== cur.height);
1096
+ };
1097
+ window.addEventListener("resize", onWindowResize, { passive: true });
1098
+ const boxKey = options.persistBox === true ? "xiaochun:box" : typeof options.persistBox === "string" && options.persistBox ? `xiaochun:box:${options.persistBox}` : null;
1099
+ const saveBox = () => {
1100
+ if (!boxKey) return;
1101
+ try {
1102
+ const b = readBox();
1103
+ const at = position === "inline" ? inlineShift : { x: b.left, y: b.top };
1104
+ 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) }));
1105
+ } catch {
1106
+ }
1107
+ };
1108
+ const loadBox = () => {
1109
+ if (!boxKey) return null;
1110
+ try {
1111
+ const raw = window.localStorage.getItem(boxKey);
1112
+ if (raw === null) return null;
1113
+ let o = null;
1114
+ try {
1115
+ o = JSON.parse(raw);
1116
+ } catch {
1117
+ }
1118
+ const ok = (v, lo, hi) => typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi;
1119
+ 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)) {
1120
+ return { x: o.x, y: o.y, width: o.width, height: o.height };
1121
+ }
1122
+ window.localStorage.removeItem(boxKey);
1123
+ } catch {
1124
+ }
1125
+ return null;
1126
+ };
1127
+ function restoreBox() {
1128
+ if (!boxKey || !draggableOn && resizeLimits === null) return;
1129
+ const st = loadBox();
1130
+ if (!st) return;
1131
+ const vp = viewport();
1132
+ const lim = resizeLimits ?? DEFAULT_RESIZE_LIMITS;
1133
+ const cur = readBox();
1134
+ const useW = resizeLimits !== null && options.width === void 0;
1135
+ const useH = resizeLimits !== null && options.height === void 0;
1136
+ const clampN = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
1137
+ const width = useW ? clampN(st.width, lim.minWidth, Math.min(lim.maxWidth, vp.width)) : cur.width;
1138
+ const height = useH ? clampN(st.height, lim.minHeight, Math.min(lim.maxHeight, vp.height)) : cur.height;
1139
+ if (position === "inline") {
1140
+ const base = { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y };
1141
+ const left = clampN(base.left + st.x, 0, vp.width - width);
1142
+ const top = Math.max(base.top + st.y, -window.scrollY);
1143
+ applyBox({ left, top, width, height }, base, useW || useH);
1144
+ } else {
1145
+ applyBox(fitBox({ left: st.x, top: st.y, width, height }, vp), null, useW || useH);
1146
+ }
1147
+ }
1148
+ restoreBox();
613
1149
  scheduleLazy();
614
1150
  const api = {
615
1151
  element: wrapper,
@@ -624,8 +1160,98 @@ function createXiaochun(options) {
624
1160
  motion: (m) => send("xc.motion", typeof m === "string" ? m.startsWith("http") || m.startsWith("/") ? { url: m } : { name: m } : m),
625
1161
  expression: (name) => send("xc.expression", { name }),
626
1162
  lookAt: (x, y) => send("xc.lookAt", { x, y }),
627
- setModel: (m) => send("xc.setModel", typeof m === "string" ? /^(https?:)?\/|\.vrm/i.test(m) ? { url: m } : { outfit: m } : m),
1163
+ setOutfit: (id) => {
1164
+ if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid outfit id"));
1165
+ return send("xc.setOutfit", { id }, true, void 0, void 0, "outfits");
1166
+ },
1167
+ setScene: (id) => {
1168
+ if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid scene id"));
1169
+ return send("xc.setScene", { id }, true, void 0, void 0, "scenes");
1170
+ },
1171
+ prefetch: (ids) => {
1172
+ if (ids !== void 0 && (!Array.isArray(ids) || ids.some((x) => !isXcId(x)))) {
1173
+ return Promise.reject(new Error("[bad_request] prefetch ids must be an array of outfit ids"));
1174
+ }
1175
+ return send("xc.prefetch", ids ? { ids } : {}, true, void 0, void 0, "prefetch");
1176
+ },
1177
+ getOutfits: async () => {
1178
+ if (!iframe) activate();
1179
+ await handshaked;
1180
+ return caps?.outfits ? caps.outfits.slice() : [];
1181
+ },
1182
+ getScenes: async () => {
1183
+ if (!iframe) activate();
1184
+ await handshaked;
1185
+ return caps?.scenes ? caps.scenes.slice() : [];
1186
+ },
1187
+ get outfit() {
1188
+ return currentOutfit;
1189
+ },
1190
+ get scene() {
1191
+ return currentScene;
1192
+ },
1193
+ setModel: (m) => {
1194
+ const msg = typeof m === "string" ? /^(https?:)?\/|\.vrm/i.test(m) ? { url: m } : { outfit: m } : m;
1195
+ if (msg.url !== void 0 && msg.outfit === void 0 && !options.allowCustomModel) {
1196
+ 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";
1197
+ emit("error", { code: "unsupported", message, command: "xc.setModel" });
1198
+ return Promise.reject(new Error(`[unsupported] ${message}`));
1199
+ }
1200
+ return send("xc.setModel", msg);
1201
+ },
628
1202
  setConfig: (cfg) => send("xc.setConfig", cfg),
1203
+ setSize(width, height) {
1204
+ if (destroyed) return;
1205
+ ws.width = css(width, defaultWidthCss(position !== "inline"));
1206
+ ws.height = css(height, defaultHeightCss(position !== "inline"));
1207
+ if (ws.maxWidth === "none") ws.maxWidth = "100%";
1208
+ invalidateRect();
1209
+ },
1210
+ getBox: () => readBox(),
1211
+ clearPersistedBox(opts) {
1212
+ if (boxKey) {
1213
+ try {
1214
+ window.localStorage.removeItem(boxKey);
1215
+ } catch {
1216
+ }
1217
+ }
1218
+ if (destroyed || opts?.reset !== true) return;
1219
+ endActiveGesture();
1220
+ ws.width = css(options.width, defaultWidthCss(position !== "inline"));
1221
+ ws.height = css(options.height, defaultHeightCss(position !== "inline"));
1222
+ ws.maxWidth = "100%";
1223
+ if (position === "inline") {
1224
+ inlineShift = { x: 0, y: 0 };
1225
+ ws.removeProperty("translate");
1226
+ } else {
1227
+ ws.left = "";
1228
+ ws.top = "";
1229
+ ws.right = "";
1230
+ ws.bottom = "";
1231
+ if (position === "bottom-right") {
1232
+ ws.right = "var(--xc-offset-x, 16px)";
1233
+ ws.bottom = "var(--xc-offset-y, 16px)";
1234
+ }
1235
+ if (position === "bottom-left") {
1236
+ ws.left = "var(--xc-offset-x, 16px)";
1237
+ ws.bottom = "var(--xc-offset-y, 16px)";
1238
+ }
1239
+ }
1240
+ userMoved = false;
1241
+ invalidateRect();
1242
+ },
1243
+ setBorderRadius(radius) {
1244
+ radiusOpt = radius;
1245
+ applyRadius();
1246
+ },
1247
+ setDraggable(on) {
1248
+ draggableOn = on === true;
1249
+ syncGestures();
1250
+ },
1251
+ setResizable(on) {
1252
+ resizeLimits = normalizeResizable(on);
1253
+ syncGestures();
1254
+ },
629
1255
  startListening: () => send("xc.mic", { enabled: true }),
630
1256
  stopListening: () => send("xc.mic", { enabled: false }),
631
1257
  mic: (enabled) => send("xc.mic", { enabled }),
@@ -650,10 +1276,10 @@ function createXiaochun(options) {
650
1276
  if (handshakeTimer) clearTimeout(handshakeTimer);
651
1277
  if (pointerRaf) cancelAnimationFrame(pointerRaf);
652
1278
  window.removeEventListener("message", onWindowMessage);
653
- window.removeEventListener("pointermove", onHostPointer);
654
- window.removeEventListener("scroll", invalidateRect, true);
655
- window.removeEventListener("resize", invalidateRect);
656
- rectRo?.disconnect();
1279
+ window.removeEventListener("resize", onWindowResize);
1280
+ lockSelection(false);
1281
+ disablePassthrough();
1282
+ resolveHandshake();
657
1283
  try {
658
1284
  port?.close();
659
1285
  } catch {
@@ -686,11 +1312,17 @@ function createXiaochun(options) {
686
1312
  // src/avatar-element.ts
687
1313
  var OBSERVED = [
688
1314
  "src",
1315
+ "outfit",
1316
+ "scene",
689
1317
  "model",
690
1318
  "lang",
691
1319
  "mic",
692
1320
  "transparent",
693
1321
  "draggable",
1322
+ "resizable",
1323
+ "min-size",
1324
+ "max-size",
1325
+ "border-radius",
694
1326
  "position",
695
1327
  "size",
696
1328
  "lazy",
@@ -698,23 +1330,66 @@ var OBSERVED = [
698
1330
  "placeholder",
699
1331
  "heavy",
700
1332
  "ui",
1333
+ "ui-autohide",
1334
+ "camera-fov",
1335
+ "camera-distance",
1336
+ "camera-height",
1337
+ "camera-intro",
701
1338
  "controls",
702
1339
  "allowed-origins",
703
- "cross-origin-isolated"
1340
+ "cross-origin-isolated",
1341
+ "persist",
1342
+ "persist-box",
1343
+ "allow-custom-model",
1344
+ "prefetch"
704
1345
  ];
705
- var REBUILD = /* @__PURE__ */ new Set(["src", "draggable", "position", "size", "lazy", "placeholder", "heavy", "ui", "controls", "allowed-origins", "transparent", "cross-origin-isolated"]);
1346
+ 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"]);
1347
+ function radiusAttr(v) {
1348
+ if (v === null || v.trim() === "") return void 0;
1349
+ const t = v.trim();
1350
+ return /^\d+(\.\d+)?$/.test(t) ? Number(t) : t;
1351
+ }
706
1352
  function boolAttr(el, name, dflt) {
707
1353
  const v = el.getAttribute(name);
708
1354
  if (v === null) return dflt;
709
1355
  return v !== "false";
710
1356
  }
1357
+ function prefetchAttr(v) {
1358
+ if (v === null || v === "false") return false;
1359
+ if (v === "" || v === "true") return true;
1360
+ return v.split(",").map((s) => s.trim()).filter(Boolean);
1361
+ }
1362
+ function persistBoxAttr(v) {
1363
+ if (v === null || v === "false" || v === "0") return void 0;
1364
+ if (v === "" || v === "true" || v === "1") return true;
1365
+ return v;
1366
+ }
1367
+ function uiAttr(v) {
1368
+ if (v === null || v === "false" || v === "0") return void 0;
1369
+ if (v === "" || v === "true" || v === "1") return true;
1370
+ return v.split(",").map((s) => s.trim()).filter(Boolean);
1371
+ }
1372
+ function cameraAttrs(el) {
1373
+ const num = (name) => {
1374
+ const v = el.getAttribute(name);
1375
+ if (v === null || v.trim() === "") return null;
1376
+ const n = Number(v);
1377
+ return Number.isFinite(n) ? n : null;
1378
+ };
1379
+ const i = el.getAttribute("camera-intro");
1380
+ return { fov: num("camera-fov"), distance: num("camera-distance"), height: num("camera-height"), intro: i === null ? null : i !== "false" && i !== "0" };
1381
+ }
1382
+ function parsePx(v) {
1383
+ const m = v?.trim().match(/^(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)$/i);
1384
+ return m ? { w: Number(m[1]), h: Number(m[2]) } : void 0;
1385
+ }
711
1386
  function parseSize(v) {
712
- if (!v) return { width: "320px", height: "480px" };
1387
+ if (!v) return { width: void 0, height: void 0 };
713
1388
  const m = v.trim().split(/x/i);
714
1389
  const px = (s) => /^\d+(\.\d+)?$/.test(s) ? `${s}px` : s;
715
1390
  if (m.length >= 2) return { width: px(m[0]), height: px(m[1]) };
716
1391
  const w = parseFloat(m[0]);
717
- return Number.isFinite(w) && /^\d+(\.\d+)?$/.test(m[0]) ? { width: `${w}px`, height: `${Math.round(w * 1.5)}px` } : { width: m[0], height: "480px" };
1392
+ 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 };
718
1393
  }
719
1394
  function createElementClass() {
720
1395
  class XiaochunAvatarElementImpl extends HTMLElement {
@@ -755,10 +1430,22 @@ function createElementClass() {
755
1430
  return;
756
1431
  }
757
1432
  const c = this.client;
758
- if (name === "model" && newV) void c.setModel(newV).catch(() => {
1433
+ if ((name === "outfit" || name === "model") && newV) {
1434
+ const want = name === "model" && this.hasAttribute("outfit") ? null : newV;
1435
+ if (want) void (isXcId(want) ? c.setOutfit(want) : c.setModel(want)).catch(() => {
1436
+ });
1437
+ } else if (name === "scene" && newV) void c.setScene(newV).catch(() => {
759
1438
  });
760
1439
  else if (name === "lang" && newV) void c.setConfig({ lang: newV }).catch(() => {
761
1440
  });
1441
+ else if (name === "size") {
1442
+ const { width, height } = parseSize(newV);
1443
+ c.setSize(width, height);
1444
+ } else if (name === "border-radius") c.setBorderRadius(radiusAttr(newV));
1445
+ else if (name === "draggable") c.setDraggable(boolAttr(this, "draggable", false));
1446
+ else if (name === "resizable" || name === "min-size" || name === "max-size") c.setResizable(this.resizableOption());
1447
+ else if (name.startsWith("camera-")) void c.setConfig({ camera: cameraAttrs(this) }).catch(() => {
1448
+ });
762
1449
  else if (name === "mic") void c.mic(boolAttr(this, "mic", false)).catch(() => {
763
1450
  });
764
1451
  else if (name === "paused") {
@@ -785,11 +1472,20 @@ function createElementClass() {
785
1472
  height,
786
1473
  position: this.getAttribute("position") ?? "inline",
787
1474
  draggable: boolAttr(this, "draggable", false),
1475
+ resizable: this.resizableOption(),
1476
+ borderRadius: radiusAttr(this.getAttribute("border-radius")),
788
1477
  lang: this.getAttribute("lang") ?? void 0,
789
- model: this.getAttribute("model") ?? void 0,
1478
+ outfit: this.getAttribute("outfit") ?? this.getAttribute("model") ?? void 0,
1479
+ scene: this.getAttribute("scene") ?? void 0,
1480
+ persist: this.getAttribute("persist") || void 0,
1481
+ persistBox: persistBoxAttr(this.getAttribute("persist-box")),
1482
+ allowCustomModel: boolAttr(this, "allow-custom-model", false),
1483
+ prefetch: prefetchAttr(this.getAttribute("prefetch")),
790
1484
  heavy: this.getAttribute("heavy") ?? void 0,
791
- ui: boolAttr(this, "ui", false),
792
- controls: boolAttr(this, "controls", false),
1485
+ ui: uiAttr(this.getAttribute("ui")),
1486
+ uiAutoHide: parseXcUiAutoHide(this.getAttribute("ui-autohide")),
1487
+ camera: cameraAttrs(this),
1488
+ controls: boolAttr(this, "controls", true),
793
1489
  crossOriginIsolated: boolAttr(this, "cross-origin-isolated", false)
794
1490
  });
795
1491
  this.client = c;
@@ -800,10 +1496,21 @@ function createElementClass() {
800
1496
  fwd("stt", "xc-stt");
801
1497
  fwd("utterance", "xc-utterance");
802
1498
  fwd("error", "xc-error");
1499
+ fwd("outfit-changed", "xc-outfit-changed");
1500
+ fwd("scene-changed", "xc-scene-changed");
1501
+ fwd("lang-changed", "xc-lang-changed");
1502
+ fwd("move", "xc-move");
1503
+ fwd("resize", "xc-resize");
803
1504
  if (boolAttr(this, "paused", false)) c.pause();
804
1505
  if (boolAttr(this, "mic", false)) void c.ready.then(() => c.mic(true)).catch(() => {
805
1506
  });
806
1507
  }
1508
+ /** resizable / min-size / max-size 属性 → SDK 的 resizable 选项 (缺省 / "false" = 关)。 */
1509
+ resizableOption() {
1510
+ if (!boolAttr(this, "resizable", false)) return false;
1511
+ const min = parsePx(this.getAttribute("min-size")), max = parsePx(this.getAttribute("max-size"));
1512
+ return { minWidth: min?.w, minHeight: min?.h, maxWidth: max?.w, maxHeight: max?.h };
1513
+ }
807
1514
  say(text, opts) {
808
1515
  return this.requireClient().say(text, opts);
809
1516
  }
@@ -819,6 +1526,24 @@ function createElementClass() {
819
1526
  expression(name) {
820
1527
  return this.requireClient().expression(name);
821
1528
  }
1529
+ setOutfit(id) {
1530
+ return this.requireClient().setOutfit(id);
1531
+ }
1532
+ setScene(id) {
1533
+ return this.requireClient().setScene(id);
1534
+ }
1535
+ getOutfits() {
1536
+ return this.requireClient().getOutfits();
1537
+ }
1538
+ getScenes() {
1539
+ return this.requireClient().getScenes();
1540
+ }
1541
+ clearPersistedBox(opts) {
1542
+ this.client?.clearPersistedBox(opts);
1543
+ }
1544
+ prefetch(ids) {
1545
+ return this.requireClient().prefetch(ids);
1546
+ }
822
1547
  destroy() {
823
1548
  this.client?.destroy();
824
1549
  this.client = null;