@firetable/project-xiaochun 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! @firetable/project-xiaochun v0.1.13 | MIT | https://github.com/FireTable/project-xiaochun */
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 transparent = options.transparent ?? false;
146
- const passthrough = options.passthrough ?? transparent;
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
- if (!transparent) ws.background = "var(--xc-bg, transparent)";
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 = "var(--xc-radius, 0)";
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
- const postToFrame = (type, payload, id, transfer) => {
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 handle2 = {
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 handle2;
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 (options.ui) u.searchParams.set("ui", "1");
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", "1");
337
- if (options.model) u.searchParams.set("outfit", options.model);
542
+ if (options.controls === false) u.searchParams.set("controls", "0");
543
+ if (initialScene && isXcId(initialScene)) u.searchParams.set("scene", initialScene);
544
+ if (initialOutfit) u.searchParams.set("outfit", initialOutfit);
338
545
  return u.href;
339
546
  }
340
547
  function activate() {
@@ -345,7 +552,7 @@ function createXiaochun(options) {
345
552
  const f = document.createElement("iframe");
346
553
  f.title = "Project XiaoChun";
347
554
  f.loading = "lazy";
348
- f.allow = "microphone; autoplay";
555
+ f.allow = options.crossOriginIsolated === true ? "microphone; autoplay; cross-origin-isolated" : "microphone; autoplay";
349
556
  f.referrerPolicy = "strict-origin-when-cross-origin";
350
557
  if (options.sandbox !== false) {
351
558
  f.setAttribute("sandbox", options.sandbox ?? "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox");
@@ -360,7 +567,7 @@ function createXiaochun(options) {
360
567
  s.border = "0";
361
568
  s.background = "transparent";
362
569
  s.colorScheme = "normal";
363
- s.borderRadius = "var(--xc-radius, 0)";
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
- setupPassthrough();
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
- for (const m of queue.splice(0)) port.postMessage(xcMessage(m.type, m.payload, m.id), m.transfer ?? []);
614
+ resolveHandshake();
615
+ for (const m of queue.splice(0)) {
616
+ if (m.requires && !caps?.[m.requires]) unsupportedByFrame(m.type, m.id);
617
+ else port.postMessage(xcMessage(m.type, m.payload, m.id), m.transfer ?? []);
618
+ }
400
619
  syncPause();
401
620
  };
402
621
  window.addEventListener("message", onWindowMessage);
622
+ function ack(id, value) {
623
+ const p = id ? pending.get(id) : void 0;
624
+ if (p && id) {
625
+ pending.delete(id);
626
+ p.resolve(value);
627
+ }
628
+ }
403
629
  function onPortMessage(ev) {
404
630
  const data = ev.data;
405
631
  if (!isXcEnvelope(data)) return;
@@ -410,6 +636,11 @@ function createXiaochun(options) {
410
636
  case "xc.loaded":
411
637
  if (!loaded) {
412
638
  loaded = true;
639
+ if (options.prefetch && options.heavy === "eager" && caps?.prefetch) {
640
+ const ids = Array.isArray(options.prefetch) ? options.prefetch : void 0;
641
+ void api.prefetch(ids).catch(() => {
642
+ });
643
+ }
413
644
  if (iframe) iframe.style.opacity = "1";
414
645
  if (placeholderEl) {
415
646
  const el = placeholderEl;
@@ -422,6 +653,30 @@ function createXiaochun(options) {
422
653
  }
423
654
  emit("ready", data.payload);
424
655
  break;
656
+ case "xc.outfit-changed": {
657
+ const pl = data.payload;
658
+ if (!pl.noop) {
659
+ currentOutfit = pl.id;
660
+ emit("outfit-changed", pl);
661
+ if (!pl.initial && pl.id) writePrefs({ outfit: pl.id });
662
+ }
663
+ ack(data.id);
664
+ break;
665
+ }
666
+ case "xc.scene-changed": {
667
+ const pl = data.payload;
668
+ currentScene = pl.id;
669
+ if (pl.transparent !== transparent) applyTransparency(pl.transparent);
670
+ if (!pl.noop) {
671
+ emit("scene-changed", pl);
672
+ if (!pl.initial) writePrefs({ scene: pl.id });
673
+ }
674
+ ack(data.id);
675
+ break;
676
+ }
677
+ case "xc.prefetched":
678
+ ack(data.id, data.payload);
679
+ break;
425
680
  case "xc.state":
426
681
  emit("state", data.payload);
427
682
  break;
@@ -446,9 +701,16 @@ function createXiaochun(options) {
446
701
  }
447
702
  break;
448
703
  }
704
+ case "xc.gesture-move":
705
+ onGesture("move", data.payload);
706
+ break;
707
+ case "xc.gesture-resize":
708
+ onGesture("resize", data.payload);
709
+ break;
449
710
  case "xc.error": {
450
711
  const { code, message, command } = data.payload;
451
712
  emit("error", { code, message, command });
713
+ if (code === "unknown_id" && (command === "outfit" || command === "scene")) writePrefs({ [command]: null });
452
714
  const p = data.id ? pending.get(data.id) : void 0;
453
715
  if (p && data.id) {
454
716
  pending.delete(data.id);
@@ -460,8 +722,26 @@ function createXiaochun(options) {
460
722
  break;
461
723
  }
462
724
  }
725
+ const POINTER_MIN_INTERVAL_MS = 32;
726
+ const POINTER_MIN_MOVE_PX = 2;
463
727
  let pointerRaf = 0;
464
728
  let lastPtr = null;
729
+ let lastPosted = null;
730
+ let rectCache = null;
731
+ let rectRo = null;
732
+ let rectAt = 0;
733
+ const invalidateRect = () => {
734
+ rectCache = null;
735
+ };
736
+ const getRect = () => {
737
+ if (!iframe) return null;
738
+ const now = performance.now();
739
+ if (!rectCache || now - rectAt > 400) {
740
+ rectCache = iframe.getBoundingClientRect();
741
+ rectAt = now;
742
+ }
743
+ return rectCache;
744
+ };
465
745
  const onHostPointer = (e) => {
466
746
  if (!iframe || !port || iframe.style.pointerEvents === "auto") return;
467
747
  lastPtr = { x: e.clientX, y: e.clientY };
@@ -469,20 +749,63 @@ function createXiaochun(options) {
469
749
  pointerRaf = requestAnimationFrame(() => {
470
750
  pointerRaf = 0;
471
751
  if (!iframe || !port || !lastPtr) return;
472
- const r = iframe.getBoundingClientRect();
752
+ const r = getRect();
753
+ if (!r) return;
473
754
  const x = lastPtr.x - r.left, y = lastPtr.y - r.top;
474
755
  if (x < 0 || y < 0 || x > r.width || y > r.height) {
756
+ lastPosted = null;
475
757
  if (lastHit) {
476
758
  lastHit = false;
477
759
  iframe.style.pointerEvents = "none";
478
760
  }
479
761
  return;
480
762
  }
763
+ const now = performance.now();
764
+ if (lastPosted && now - lastPosted.t < POINTER_MIN_INTERVAL_MS) return;
765
+ if (lastPosted && Math.abs(x - lastPosted.x) < POINTER_MIN_MOVE_PX && Math.abs(y - lastPosted.y) < POINTER_MIN_MOVE_PX) return;
766
+ lastPosted = { x, y, t: now };
481
767
  port.postMessage(xcMessage("xc.pointer", { x, y }));
482
768
  });
483
769
  };
484
- function setupPassthrough() {
485
- if (passthrough) window.addEventListener("pointermove", onHostPointer, { passive: true });
770
+ let passthroughActive = false;
771
+ function enablePassthrough() {
772
+ if (passthroughActive || !iframe) return;
773
+ passthroughActive = true;
774
+ window.addEventListener("pointermove", onHostPointer, { passive: true });
775
+ window.addEventListener("pointerdown", onHostPointer, { passive: true });
776
+ window.addEventListener("scroll", invalidateRect, { passive: true, capture: true });
777
+ window.addEventListener("resize", invalidateRect, { passive: true });
778
+ if (typeof ResizeObserver !== "undefined") {
779
+ rectRo = new ResizeObserver(invalidateRect);
780
+ rectRo.observe(iframe);
781
+ }
782
+ }
783
+ function disablePassthrough() {
784
+ if (!passthroughActive) return;
785
+ passthroughActive = false;
786
+ window.removeEventListener("pointermove", onHostPointer);
787
+ window.removeEventListener("pointerdown", onHostPointer);
788
+ window.removeEventListener("scroll", invalidateRect, true);
789
+ window.removeEventListener("resize", invalidateRect);
790
+ rectRo?.disconnect();
791
+ rectRo = null;
792
+ if (pointerRaf) {
793
+ cancelAnimationFrame(pointerRaf);
794
+ pointerRaf = 0;
795
+ }
796
+ lastPtr = null;
797
+ }
798
+ function applyTransparency(next) {
799
+ transparent = next;
800
+ applyWrapperBg();
801
+ applyRadius();
802
+ passthrough = next ? options.passthrough ?? true : false;
803
+ lastHit = false;
804
+ lastPosted = null;
805
+ rectCache = null;
806
+ if (passthrough) enablePassthrough();
807
+ else disablePassthrough();
808
+ if (iframe) iframe.style.pointerEvents = passthrough ? "none" : "auto";
486
809
  }
487
810
  let pauseIo = null;
488
811
  function setupAutoPause() {
@@ -535,49 +858,123 @@ function createXiaochun(options) {
535
858
  }, { rootMargin: `${options.lazyMargin ?? 200}px` });
536
859
  io.observe(wrapper);
537
860
  }
538
- let handle = null;
539
- if (options.draggable && position !== "inline") {
540
- handle = document.createElement("div");
541
- handle.setAttribute("aria-label", "drag");
542
- Object.assign(handle.style, {
543
- position: "absolute",
544
- left: "0",
545
- top: "0",
546
- width: "28px",
547
- height: "28px",
548
- cursor: "grab",
549
- pointerEvents: "auto",
550
- borderRadius: "8px",
551
- background: "rgba(0,0,0,.25)",
552
- color: "#fff",
553
- font: "14px/28px system-ui",
554
- textAlign: "center",
555
- userSelect: "none",
556
- touchAction: "none",
557
- zIndex: "2",
558
- opacity: "0.5"
559
- });
560
- handle.textContent = "\u283F";
561
- let drag = null;
562
- handle.addEventListener("pointerdown", (e) => {
563
- const r = wrapper.getBoundingClientRect();
564
- drag = { dx: e.clientX - r.left, dy: e.clientY - r.top };
565
- handle.setPointerCapture(e.pointerId);
566
- });
567
- handle.addEventListener("pointermove", (e) => {
568
- if (!drag) return;
569
- ws.left = `${Math.min(Math.max(0, e.clientX - drag.dx), window.innerWidth - 40)}px`;
570
- ws.top = `${Math.min(Math.max(0, e.clientY - drag.dy), window.innerHeight - 40)}px`;
861
+ let draggableOn = options.draggable === true;
862
+ let resizeLimits = normalizeResizable(options.resizable);
863
+ const gates = { move: new GestureGate(), resize: new GestureGate() };
864
+ let activeGesture = null;
865
+ let inlineShift = { x: 0, y: 0 };
866
+ let userMoved = false;
867
+ let sentGestures = null;
868
+ let warnedGesture = false;
869
+ const viewport = () => ({
870
+ width: document.documentElement.clientWidth || window.innerWidth,
871
+ height: document.documentElement.clientHeight || window.innerHeight
872
+ });
873
+ const readBox = () => {
874
+ const r = wrapper.getBoundingClientRect();
875
+ return { left: r.left, top: r.top, width: r.width, height: r.height };
876
+ };
877
+ const wantGestures = () => ({ move: draggableOn && caps?.gestures?.move === true, resize: resizeLimits !== null && caps?.gestures?.resize === true });
878
+ function applyBox(b, inlineBase, withSize) {
879
+ if (withSize) {
880
+ ws.width = `${b.width}px`;
881
+ ws.height = `${b.height}px`;
882
+ ws.maxWidth = "none";
883
+ }
884
+ if (position === "inline") {
885
+ const base = inlineBase ?? { left: b.left - inlineShift.x, top: b.top - inlineShift.y };
886
+ inlineShift = { x: b.left - base.left, y: b.top - base.top };
887
+ ws.setProperty("translate", `${inlineShift.x}px ${inlineShift.y}px`);
888
+ } else {
889
+ ws.left = `${b.left}px`;
890
+ ws.top = `${b.top}px`;
571
891
  ws.right = "auto";
572
892
  ws.bottom = "auto";
573
- });
574
- const end = () => {
575
- drag = null;
576
- };
577
- handle.addEventListener("pointerup", end);
578
- handle.addEventListener("pointercancel", end);
579
- wrapper.appendChild(handle);
893
+ }
894
+ userMoved = true;
895
+ invalidateRect();
896
+ }
897
+ let selectionLock = null;
898
+ function lockSelection(on) {
899
+ const de = document.documentElement;
900
+ if (on && !selectionLock) {
901
+ selectionLock = { us: de.style.getPropertyValue("user-select"), wus: de.style.getPropertyValue("-webkit-user-select") };
902
+ de.style.setProperty("user-select", "none");
903
+ de.style.setProperty("-webkit-user-select", "none");
904
+ try {
905
+ window.getSelection()?.removeAllRanges();
906
+ } catch {
907
+ }
908
+ } else if (!on && selectionLock) {
909
+ de.style.setProperty("user-select", selectionLock.us);
910
+ de.style.setProperty("-webkit-user-select", selectionLock.wus);
911
+ selectionLock = null;
912
+ }
580
913
  }
914
+ function endActiveGesture(kind) {
915
+ if (!activeGesture || kind && activeGesture.kind !== kind) return;
916
+ gates[activeGesture.kind].close();
917
+ const b = readBox();
918
+ emit(activeGesture.kind, { phase: "end", ...b });
919
+ activeGesture = null;
920
+ lockSelection(false);
921
+ }
922
+ function onGesture(kind, raw) {
923
+ if (destroyed || !caps?.gestures) return;
924
+ if (kind === "move" ? !draggableOn : resizeLimits === null) return;
925
+ const p = parseGesturePayload(kind, raw);
926
+ if (!p) return;
927
+ if (p.phase === "start" ? activeGesture !== null : activeGesture?.kind !== kind) return;
928
+ if (!gates[kind].accept(p)) return;
929
+ if (p.phase === "start") {
930
+ const start = readBox();
931
+ activeGesture = {
932
+ kind,
933
+ start,
934
+ corner: p.corner,
935
+ inlineBase: position === "inline" ? { left: start.left - inlineShift.x, top: start.top - inlineShift.y } : null
936
+ };
937
+ lockSelection(true);
938
+ emit(kind, { phase: "start", ...start });
939
+ return;
940
+ }
941
+ const a = activeGesture;
942
+ const vp = viewport();
943
+ const next = kind === "move" ? moveBox(a.start, p.totalDx, p.totalDy, vp) : resizeBox(a.start, a.corner, p.totalDx, p.totalDy, resizeLimits, vp);
944
+ applyBox(next, a.inlineBase, kind === "resize");
945
+ emit(kind, { phase: p.phase, ...next });
946
+ if (p.phase === "end") {
947
+ activeGesture = null;
948
+ lockSelection(false);
949
+ }
950
+ }
951
+ function syncGestures() {
952
+ if (destroyed) return;
953
+ const want = wantGestures();
954
+ if (!want.move) endActiveGesture("move");
955
+ if (!want.resize) endActiveGesture("resize");
956
+ if (!port || !caps) return;
957
+ warnGestureUnsupported();
958
+ if (sentGestures && sentGestures.move === want.move && sentGestures.resize === want.resize) return;
959
+ sentGestures = want;
960
+ if (caps.gestures) port.postMessage(xcMessage("xc.setConfig", { gestures: want }));
961
+ }
962
+ function warnGestureUnsupported() {
963
+ if (warnedGesture || !caps || caps.gestures) return;
964
+ const needMove = draggableOn;
965
+ if (!needMove && resizeLimits === null) return;
966
+ warnedGesture = true;
967
+ const what = [needMove ? "draggable" : "", resizeLimits !== null ? "resizable" : ""].filter(Boolean).join(" / ");
968
+ emit("error", { code: "unsupported", message: `${what} needs gesture support (capabilities.gestures), which this /embed deployment does not advertise (older version)`, command: "gestures" });
969
+ }
970
+ const onWindowResize = () => {
971
+ if (!userMoved || activeGesture || destroyed) return;
972
+ const cur = readBox();
973
+ const fit = fitBox(cur, viewport());
974
+ if (fit.left === cur.left && fit.top === cur.top && fit.width === cur.width && fit.height === cur.height) return;
975
+ applyBox(fit, position === "inline" ? { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y } : null, fit.width !== cur.width || fit.height !== cur.height);
976
+ };
977
+ window.addEventListener("resize", onWindowResize, { passive: true });
581
978
  scheduleLazy();
582
979
  const api = {
583
980
  element: wrapper,
@@ -592,8 +989,65 @@ function createXiaochun(options) {
592
989
  motion: (m) => send("xc.motion", typeof m === "string" ? m.startsWith("http") || m.startsWith("/") ? { url: m } : { name: m } : m),
593
990
  expression: (name) => send("xc.expression", { name }),
594
991
  lookAt: (x, y) => send("xc.lookAt", { x, y }),
595
- setModel: (m) => send("xc.setModel", typeof m === "string" ? /^(https?:)?\/|\.vrm/i.test(m) ? { url: m } : { outfit: m } : m),
992
+ setOutfit: (id) => {
993
+ if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid outfit id"));
994
+ return send("xc.setOutfit", { id }, true, void 0, void 0, "outfits");
995
+ },
996
+ setScene: (id) => {
997
+ if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid scene id"));
998
+ return send("xc.setScene", { id }, true, void 0, void 0, "scenes");
999
+ },
1000
+ prefetch: (ids) => {
1001
+ if (ids !== void 0 && (!Array.isArray(ids) || ids.some((x) => !isXcId(x)))) {
1002
+ return Promise.reject(new Error("[bad_request] prefetch ids must be an array of outfit ids"));
1003
+ }
1004
+ return send("xc.prefetch", ids ? { ids } : {}, true, void 0, void 0, "prefetch");
1005
+ },
1006
+ getOutfits: async () => {
1007
+ if (!iframe) activate();
1008
+ await handshaked;
1009
+ return caps?.outfits ? caps.outfits.slice() : [];
1010
+ },
1011
+ getScenes: async () => {
1012
+ if (!iframe) activate();
1013
+ await handshaked;
1014
+ return caps?.scenes ? caps.scenes.slice() : [];
1015
+ },
1016
+ get outfit() {
1017
+ return currentOutfit;
1018
+ },
1019
+ get scene() {
1020
+ return currentScene;
1021
+ },
1022
+ setModel: (m) => {
1023
+ const msg = typeof m === "string" ? /^(https?:)?\/|\.vrm/i.test(m) ? { url: m } : { outfit: m } : m;
1024
+ if (msg.url !== void 0 && msg.outfit === void 0 && !options.allowCustomModel) {
1025
+ const message = "custom model URLs are disabled; pass allowCustomModel: true to createXiaochun() (only for URLs you trust) or use setOutfit() with a built-in id";
1026
+ emit("error", { code: "unsupported", message, command: "xc.setModel" });
1027
+ return Promise.reject(new Error(`[unsupported] ${message}`));
1028
+ }
1029
+ return send("xc.setModel", msg);
1030
+ },
596
1031
  setConfig: (cfg) => send("xc.setConfig", cfg),
1032
+ setSize(width, height) {
1033
+ if (destroyed) return;
1034
+ ws.width = css(width, "320px");
1035
+ ws.height = css(height, "480px");
1036
+ invalidateRect();
1037
+ },
1038
+ getBox: () => readBox(),
1039
+ setBorderRadius(radius) {
1040
+ radiusOpt = radius;
1041
+ applyRadius();
1042
+ },
1043
+ setDraggable(on) {
1044
+ draggableOn = on === true;
1045
+ syncGestures();
1046
+ },
1047
+ setResizable(on) {
1048
+ resizeLimits = normalizeResizable(on);
1049
+ syncGestures();
1050
+ },
597
1051
  startListening: () => send("xc.mic", { enabled: true }),
598
1052
  stopListening: () => send("xc.mic", { enabled: false }),
599
1053
  mic: (enabled) => send("xc.mic", { enabled }),
@@ -618,7 +1072,10 @@ function createXiaochun(options) {
618
1072
  if (handshakeTimer) clearTimeout(handshakeTimer);
619
1073
  if (pointerRaf) cancelAnimationFrame(pointerRaf);
620
1074
  window.removeEventListener("message", onWindowMessage);
621
- window.removeEventListener("pointermove", onHostPointer);
1075
+ window.removeEventListener("resize", onWindowResize);
1076
+ lockSelection(false);
1077
+ disablePassthrough();
1078
+ resolveHandshake();
622
1079
  try {
623
1080
  port?.close();
624
1081
  } catch {
@@ -651,11 +1108,17 @@ function createXiaochun(options) {
651
1108
  // src/avatar-element.ts
652
1109
  var OBSERVED = [
653
1110
  "src",
1111
+ "outfit",
1112
+ "scene",
654
1113
  "model",
655
1114
  "lang",
656
1115
  "mic",
657
1116
  "transparent",
658
1117
  "draggable",
1118
+ "resizable",
1119
+ "min-size",
1120
+ "max-size",
1121
+ "border-radius",
659
1122
  "position",
660
1123
  "size",
661
1124
  "lazy",
@@ -664,14 +1127,37 @@ var OBSERVED = [
664
1127
  "heavy",
665
1128
  "ui",
666
1129
  "controls",
667
- "allowed-origins"
1130
+ "allowed-origins",
1131
+ "cross-origin-isolated",
1132
+ "persist",
1133
+ "allow-custom-model",
1134
+ "prefetch"
668
1135
  ];
669
- var REBUILD = /* @__PURE__ */ new Set(["src", "draggable", "position", "size", "lazy", "placeholder", "heavy", "ui", "controls", "allowed-origins", "transparent"]);
1136
+ var REBUILD = /* @__PURE__ */ new Set(["src", "position", "lazy", "placeholder", "heavy", "ui", "controls", "allowed-origins", "transparent", "cross-origin-isolated", "persist", "allow-custom-model", "prefetch"]);
1137
+ function radiusAttr(v) {
1138
+ if (v === null || v.trim() === "") return void 0;
1139
+ const t = v.trim();
1140
+ return /^\d+(\.\d+)?$/.test(t) ? Number(t) : t;
1141
+ }
670
1142
  function boolAttr(el, name, dflt) {
671
1143
  const v = el.getAttribute(name);
672
1144
  if (v === null) return dflt;
673
1145
  return v !== "false";
674
1146
  }
1147
+ function prefetchAttr(v) {
1148
+ if (v === null || v === "false") return false;
1149
+ if (v === "" || v === "true") return true;
1150
+ return v.split(",").map((s) => s.trim()).filter(Boolean);
1151
+ }
1152
+ function uiAttr(v) {
1153
+ if (v === null || v === "false" || v === "0") return void 0;
1154
+ if (v === "" || v === "true" || v === "1") return true;
1155
+ return v.split(",").map((s) => s.trim()).filter(Boolean);
1156
+ }
1157
+ function parsePx(v) {
1158
+ const m = v?.trim().match(/^(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)$/i);
1159
+ return m ? { w: Number(m[1]), h: Number(m[2]) } : void 0;
1160
+ }
675
1161
  function parseSize(v) {
676
1162
  if (!v) return { width: "320px", height: "480px" };
677
1163
  const m = v.trim().split(/x/i);
@@ -719,10 +1205,20 @@ function createElementClass() {
719
1205
  return;
720
1206
  }
721
1207
  const c = this.client;
722
- if (name === "model" && newV) void c.setModel(newV).catch(() => {
1208
+ if ((name === "outfit" || name === "model") && newV) {
1209
+ const want = name === "model" && this.hasAttribute("outfit") ? null : newV;
1210
+ if (want) void (isXcId(want) ? c.setOutfit(want) : c.setModel(want)).catch(() => {
1211
+ });
1212
+ } else if (name === "scene" && newV) void c.setScene(newV).catch(() => {
723
1213
  });
724
1214
  else if (name === "lang" && newV) void c.setConfig({ lang: newV }).catch(() => {
725
1215
  });
1216
+ else if (name === "size") {
1217
+ const { width, height } = parseSize(newV);
1218
+ c.setSize(width, height);
1219
+ } else if (name === "border-radius") c.setBorderRadius(radiusAttr(newV));
1220
+ else if (name === "draggable") c.setDraggable(boolAttr(this, "draggable", false));
1221
+ else if (name === "resizable" || name === "min-size" || name === "max-size") c.setResizable(this.resizableOption());
726
1222
  else if (name === "mic") void c.mic(boolAttr(this, "mic", false)).catch(() => {
727
1223
  });
728
1224
  else if (name === "paused") {
@@ -749,11 +1245,18 @@ function createElementClass() {
749
1245
  height,
750
1246
  position: this.getAttribute("position") ?? "inline",
751
1247
  draggable: boolAttr(this, "draggable", false),
1248
+ resizable: this.resizableOption(),
1249
+ borderRadius: radiusAttr(this.getAttribute("border-radius")),
752
1250
  lang: this.getAttribute("lang") ?? void 0,
753
- model: this.getAttribute("model") ?? void 0,
1251
+ outfit: this.getAttribute("outfit") ?? this.getAttribute("model") ?? void 0,
1252
+ scene: this.getAttribute("scene") ?? void 0,
1253
+ persist: this.getAttribute("persist") || void 0,
1254
+ allowCustomModel: boolAttr(this, "allow-custom-model", false),
1255
+ prefetch: prefetchAttr(this.getAttribute("prefetch")),
754
1256
  heavy: this.getAttribute("heavy") ?? void 0,
755
- ui: boolAttr(this, "ui", false),
756
- controls: boolAttr(this, "controls", false)
1257
+ ui: uiAttr(this.getAttribute("ui")),
1258
+ controls: boolAttr(this, "controls", true),
1259
+ crossOriginIsolated: boolAttr(this, "cross-origin-isolated", false)
757
1260
  });
758
1261
  this.client = c;
759
1262
  const fwd = (ev, name) => c.on(ev, (detail) => this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })));
@@ -763,10 +1266,20 @@ function createElementClass() {
763
1266
  fwd("stt", "xc-stt");
764
1267
  fwd("utterance", "xc-utterance");
765
1268
  fwd("error", "xc-error");
1269
+ fwd("outfit-changed", "xc-outfit-changed");
1270
+ fwd("scene-changed", "xc-scene-changed");
1271
+ fwd("move", "xc-move");
1272
+ fwd("resize", "xc-resize");
766
1273
  if (boolAttr(this, "paused", false)) c.pause();
767
1274
  if (boolAttr(this, "mic", false)) void c.ready.then(() => c.mic(true)).catch(() => {
768
1275
  });
769
1276
  }
1277
+ /** resizable / min-size / max-size 属性 → SDK 的 resizable 选项 (缺省 / "false" = 关)。 */
1278
+ resizableOption() {
1279
+ if (!boolAttr(this, "resizable", false)) return false;
1280
+ const min = parsePx(this.getAttribute("min-size")), max = parsePx(this.getAttribute("max-size"));
1281
+ return { minWidth: min?.w, minHeight: min?.h, maxWidth: max?.w, maxHeight: max?.h };
1282
+ }
770
1283
  say(text, opts) {
771
1284
  return this.requireClient().say(text, opts);
772
1285
  }
@@ -782,6 +1295,21 @@ function createElementClass() {
782
1295
  expression(name) {
783
1296
  return this.requireClient().expression(name);
784
1297
  }
1298
+ setOutfit(id) {
1299
+ return this.requireClient().setOutfit(id);
1300
+ }
1301
+ setScene(id) {
1302
+ return this.requireClient().setScene(id);
1303
+ }
1304
+ getOutfits() {
1305
+ return this.requireClient().getOutfits();
1306
+ }
1307
+ getScenes() {
1308
+ return this.requireClient().getScenes();
1309
+ }
1310
+ prefetch(ids) {
1311
+ return this.requireClient().prefetch(ids);
1312
+ }
785
1313
  destroy() {
786
1314
  this.client?.destroy();
787
1315
  this.client = null;