@virtualdisplay.io/client 3.5.0 → 3.6.0

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.
@@ -4,7 +4,8 @@ const ERROR_CODES = {
4
4
  VALUE_NOT_FOUND: "VALUE_NOT_FOUND",
5
5
  INVALID_MAPPING: "INVALID_MAPPING",
6
6
  PARENT_NOT_FOUND: "PARENT_NOT_FOUND",
7
- INVALID_MUTATIONS: "INVALID_MUTATIONS"
7
+ INVALID_MUTATIONS: "INVALID_MUTATIONS",
8
+ INVALID_PARAMETER: "INVALID_PARAMETER"
8
9
  };
9
10
  class VirtualdisplayError extends Error {
10
11
  code;
@@ -61,7 +62,10 @@ const EVENT_NAMES = {
61
62
  // State flow (Server → Client)
62
63
  MESSAGE_RECEIVED: "message:received",
63
64
  STATE_CHANGED: "state:changed",
64
- INITIAL_STATE_RECEIVED: "state:initial-received"
65
+ INITIAL_STATE_RECEIVED: "state:initial-received",
66
+ // Snapshot flow
67
+ SNAPSHOT_MESSAGE: "snapshot:take",
68
+ SNAPSHOT_DEVELOPED: "snapshot:developed"
65
69
  };
66
70
  const g = Object.freeze({
67
71
  debug: 0,
@@ -6931,7 +6935,8 @@ const MESSAGE_TYPES = {
6931
6935
  MUTATION: "mutation",
6932
6936
  STATE: "state",
6933
6937
  CONFIG: "config",
6934
- CAMERA: "camera"
6938
+ CAMERA: "camera",
6939
+ SNAPSHOT: "snapshot"
6935
6940
  };
6936
6941
  function createMutationMessage(mutations) {
6937
6942
  if (!Array.isArray(mutations)) {
@@ -6943,14 +6948,7 @@ function createMutationMessage(mutations) {
6943
6948
  };
6944
6949
  }
6945
6950
  function isValidMessage(data) {
6946
- return isMutationMessage(data) || isStateMessage(data) || isConfigMessage(data);
6947
- }
6948
- function isMutationMessage(data) {
6949
- if (typeof data !== "object" || data === null) {
6950
- return false;
6951
- }
6952
- const obj = data;
6953
- return obj.type === MESSAGE_TYPES.MUTATION && Array.isArray(obj.mutations);
6951
+ return isStateMessage(data) || isSnapshotDevelopedMessage(data);
6954
6952
  }
6955
6953
  function isStateMessage(data) {
6956
6954
  if (typeof data !== "object" || data === null) {
@@ -6959,12 +6957,12 @@ function isStateMessage(data) {
6959
6957
  const obj = data;
6960
6958
  return obj.type === MESSAGE_TYPES.STATE && Array.isArray(obj.nodes);
6961
6959
  }
6962
- function isConfigMessage(data) {
6960
+ function isSnapshotDevelopedMessage(data) {
6963
6961
  if (typeof data !== "object" || data === null) {
6964
6962
  return false;
6965
6963
  }
6966
6964
  const obj = data;
6967
- return obj.type === MESSAGE_TYPES.CONFIG && typeof obj.config === "object" && obj.config !== null;
6965
+ return obj.type === MESSAGE_TYPES.SNAPSHOT && typeof obj.filename === "string" && typeof obj.data === "string";
6968
6966
  }
6969
6967
  class AttributeService {
6970
6968
  attributes = /* @__PURE__ */ new Map();
@@ -7227,11 +7225,13 @@ class MessageHandler {
7227
7225
  const outgoingEvents = [
7228
7226
  EVENT_NAMES.MUTATION_MESSAGE,
7229
7227
  EVENT_NAMES.CONFIG_MESSAGE,
7230
- EVENT_NAMES.CAMERA_MESSAGE
7228
+ EVENT_NAMES.CAMERA_MESSAGE,
7229
+ EVENT_NAMES.SNAPSHOT_MESSAGE
7231
7230
  ];
7232
7231
  outgoingEvents.forEach((eventName) => {
7233
7232
  this.eventBus.on(eventName, (event) => {
7234
- this.queueOrSend(event.message);
7233
+ const messageEvent = event;
7234
+ this.queueOrSend(messageEvent.message);
7235
7235
  });
7236
7236
  });
7237
7237
  }
@@ -7419,6 +7419,156 @@ class StateService {
7419
7419
  this.hasReceivedInitialSync = false;
7420
7420
  }
7421
7421
  }
7422
+ class Photo {
7423
+ constructor(filename) {
7424
+ this.filename = filename;
7425
+ }
7426
+ callbacks = /* @__PURE__ */ new Set();
7427
+ developed = false;
7428
+ photoData = null;
7429
+ /**
7430
+ * Register a callback for when the photo is developed
7431
+ * If the photo is already developed, the callback is called immediately
7432
+ *
7433
+ * @param callback Function to call with photo data
7434
+ * @returns This Photo instance for chaining
7435
+ */
7436
+ onDeveloped(callback) {
7437
+ if (this.developed && this.photoData !== null) {
7438
+ callback(this.photoData);
7439
+ return this;
7440
+ }
7441
+ this.callbacks.add(callback);
7442
+ return this;
7443
+ }
7444
+ /**
7445
+ * Get the filename for this photo
7446
+ */
7447
+ getFilename() {
7448
+ return this.filename;
7449
+ }
7450
+ /**
7451
+ * Mark the photo as developed and notify all callbacks
7452
+ * This method is called by the SnapshotMessageHandler when the photo data arrives
7453
+ */
7454
+ develop(photoData) {
7455
+ if (this.developed) {
7456
+ return;
7457
+ }
7458
+ this.photoData = photoData;
7459
+ this.developed = true;
7460
+ this.callbacks.forEach((callback) => {
7461
+ callback(this.photoData);
7462
+ });
7463
+ this.callbacks.clear();
7464
+ }
7465
+ }
7466
+ class SnapshotMessageHandler {
7467
+ constructor(eventBus) {
7468
+ this.eventBus = eventBus;
7469
+ logger.debug("SnapshotMessageHandler initialized");
7470
+ this.setupMessageListener();
7471
+ }
7472
+ pendingPhotos = /* @__PURE__ */ new Map();
7473
+ /**
7474
+ * Register a photo to be developed when its data arrives
7475
+ */
7476
+ registerPhoto(photo) {
7477
+ logger.debug("Registering photo for development", { filename: photo.getFilename() });
7478
+ this.pendingPhotos.set(photo.getFilename(), photo);
7479
+ logger.debug("Pending photos count", { count: this.pendingPhotos.size });
7480
+ }
7481
+ setupMessageListener() {
7482
+ logger.debug("Setting up message listener for snapshot messages");
7483
+ this.eventBus.on(EVENT_NAMES.MESSAGE_RECEIVED, (event) => {
7484
+ const { message } = event;
7485
+ if (message.type === MESSAGE_TYPES.SNAPSHOT) {
7486
+ logger.debug("Processing snapshot message");
7487
+ this.handleSnapshotDeveloped(message);
7488
+ }
7489
+ });
7490
+ }
7491
+ handleSnapshotDeveloped(message) {
7492
+ logger.debug("Handling snapshot developed message", {
7493
+ filename: message.filename,
7494
+ dataLength: message.data.length,
7495
+ pendingCount: this.pendingPhotos.size
7496
+ });
7497
+ const photo = this.pendingPhotos.get(message.filename);
7498
+ if (photo !== void 0) {
7499
+ logger.debug("Found pending photo, developing...", { filename: message.filename });
7500
+ const photoData = {
7501
+ filename: message.filename,
7502
+ data: message.data
7503
+ };
7504
+ photo.develop(photoData);
7505
+ this.pendingPhotos.delete(message.filename);
7506
+ logger.debug("Photo developed and removed from pending", {
7507
+ filename: message.filename,
7508
+ remainingCount: this.pendingPhotos.size
7509
+ });
7510
+ return;
7511
+ }
7512
+ logger.warn("No pending photo found for filename", {
7513
+ filename: message.filename,
7514
+ pendingFilenames: Array.from(this.pendingPhotos.keys())
7515
+ });
7516
+ }
7517
+ }
7518
+ class Snapshot {
7519
+ constructor(eventBus) {
7520
+ this.eventBus = eventBus;
7521
+ this.messageHandler = new SnapshotMessageHandler(eventBus);
7522
+ }
7523
+ validExtensions = [".png", ".jpg", ".jpeg", ".webp"];
7524
+ messageHandler;
7525
+ /**
7526
+ * Take a snapshot of the current 3D model view
7527
+ *
7528
+ * @param filename The filename for the snapshot (must include valid extension)
7529
+ * @returns Photo object for registering callbacks
7530
+ * @throws VirtualdisplayError if filename is invalid
7531
+ */
7532
+ take(filename) {
7533
+ this.validateFilename(filename);
7534
+ logger.debug("Taking snapshot", { filename });
7535
+ const photo = new Photo(filename);
7536
+ this.messageHandler.registerPhoto(photo);
7537
+ this.eventBus.emit(EVENT_NAMES.SNAPSHOT_MESSAGE, {
7538
+ message: {
7539
+ type: MESSAGE_TYPES.SNAPSHOT,
7540
+ filename
7541
+ }
7542
+ });
7543
+ return photo;
7544
+ }
7545
+ /**
7546
+ * Validate filename has a valid image extension
7547
+ */
7548
+ validateFilename(filename) {
7549
+ if (filename === "" || typeof filename !== "string") {
7550
+ throw new VirtualdisplayError(
7551
+ "Filename must be a non-empty string",
7552
+ ERROR_CODES.INVALID_PARAMETER
7553
+ );
7554
+ }
7555
+ const hasValidExtension = this.validExtensions.some(
7556
+ (ext) => filename.toLowerCase().endsWith(ext)
7557
+ );
7558
+ if (!hasValidExtension) {
7559
+ throw new VirtualdisplayError(
7560
+ `Invalid filename: "${filename}". Filename must end with one of: ${this.validExtensions.join(", ")}`,
7561
+ ERROR_CODES.INVALID_PARAMETER
7562
+ );
7563
+ }
7564
+ if (filename.includes("..") || filename.includes("/") || filename.includes("\\")) {
7565
+ throw new VirtualdisplayError(
7566
+ 'Filename cannot contain path separators or ".."',
7567
+ ERROR_CODES.INVALID_PARAMETER
7568
+ );
7569
+ }
7570
+ }
7571
+ }
7422
7572
  class VirtualdisplayViewerService {
7423
7573
  eventBus;
7424
7574
  constructor(eventBus) {
@@ -7426,50 +7576,88 @@ class VirtualdisplayViewerService {
7426
7576
  }
7427
7577
  /**
7428
7578
  * Send initial configuration based on client options
7429
- * Only sends config when values are false (server defaults to true)
7579
+ * Supports both individual properties and ui object
7430
7580
  */
7431
7581
  sendInitialConfig(options) {
7432
- const config = {};
7433
- if (options.arEnabled === false) {
7434
- config.arEnabled = false;
7435
- }
7436
- if (options.fullscreenEnabled === false) {
7437
- config.fullscreenEnabled = false;
7582
+ const uiConfig = this.buildUIConfig(options);
7583
+ const viewerConfig = this.buildViewerConfig(uiConfig, options.camera);
7584
+ if (viewerConfig !== void 0) {
7585
+ this.sendConfig(viewerConfig);
7438
7586
  }
7439
- if (options.loadingIndicatorEnabled === false) {
7440
- config.loadingIndicatorEnabled = false;
7587
+ }
7588
+ /**
7589
+ * Build UI configuration from client options
7590
+ *
7591
+ * Supports both legacy properties (options.arEnabled) and the newer ui object
7592
+ * for backward compatibility. Legacy properties take precedence when both exist.
7593
+ */
7594
+ buildUIConfig(options) {
7595
+ const uiValues = this.extractUIValues(options);
7596
+ const hasUIConfig = Object.values(uiValues).some((value) => value !== void 0);
7597
+ if (!hasUIConfig) {
7598
+ return void 0;
7441
7599
  }
7442
- if (Object.keys(config).length > 0) {
7443
- this.sendUIConfig(config);
7600
+ return {
7601
+ ...uiValues.arEnabled !== void 0 && { arEnabled: uiValues.arEnabled },
7602
+ ...uiValues.fullscreenEnabled !== void 0 && {
7603
+ fullscreenEnabled: uiValues.fullscreenEnabled
7604
+ },
7605
+ ...uiValues.loadingIndicatorEnabled !== void 0 && {
7606
+ loadingIndicatorEnabled: uiValues.loadingIndicatorEnabled
7607
+ }
7608
+ };
7609
+ }
7610
+ /**
7611
+ * Extract UI values from options, with legacy properties taking precedence
7612
+ */
7613
+ extractUIValues(options) {
7614
+ return {
7615
+ arEnabled: options.arEnabled ?? options.ui?.arEnabled,
7616
+ fullscreenEnabled: options.fullscreenEnabled ?? options.ui?.fullscreenEnabled,
7617
+ loadingIndicatorEnabled: options.loadingIndicatorEnabled ?? options.ui?.loadingIndicatorEnabled
7618
+ };
7619
+ }
7620
+ /**
7621
+ * Build viewer configuration from UI config and camera config
7622
+ */
7623
+ buildViewerConfig(uiConfig, cameraConfig) {
7624
+ if (uiConfig === void 0 && cameraConfig === void 0) {
7625
+ return void 0;
7444
7626
  }
7627
+ return {
7628
+ ...uiConfig !== void 0 && { ui: uiConfig },
7629
+ ...cameraConfig !== void 0 && { camera: cameraConfig }
7630
+ };
7445
7631
  }
7446
7632
  /**
7447
7633
  * Update AR button visibility
7448
7634
  */
7449
7635
  setArEnabled(enabled) {
7450
7636
  logger.debug(`Setting AR button enabled: ${enabled}`);
7451
- this.sendUIConfig({ arEnabled: enabled });
7637
+ this.updateUIConfig({ arEnabled: enabled });
7452
7638
  }
7453
7639
  /**
7454
7640
  * Update fullscreen button visibility
7455
7641
  */
7456
7642
  setFullscreenEnabled(enabled) {
7457
7643
  logger.debug(`Setting fullscreen button enabled: ${enabled}`);
7458
- this.sendUIConfig({ fullscreenEnabled: enabled });
7644
+ this.updateUIConfig({ fullscreenEnabled: enabled });
7459
7645
  }
7460
7646
  /**
7461
7647
  * Update loading indicator visibility
7462
7648
  */
7463
7649
  setLoadingIndicatorEnabled(enabled) {
7464
7650
  logger.debug(`Setting loading indicator enabled: ${enabled}`);
7465
- this.sendUIConfig({ loadingIndicatorEnabled: enabled });
7651
+ this.updateUIConfig({ loadingIndicatorEnabled: enabled });
7466
7652
  }
7467
7653
  /**
7468
7654
  * Update multiple UI settings at once
7469
7655
  */
7470
7656
  updateUIConfig(config) {
7471
7657
  logger.debug("Updating UI configuration", config);
7472
- this.sendUIConfig(config);
7658
+ if (Object.keys(config).length > 0) {
7659
+ this.sendConfig({ ui: config });
7660
+ }
7473
7661
  }
7474
7662
  /**
7475
7663
  * Hide all UI elements
@@ -7497,11 +7685,17 @@ class VirtualdisplayViewerService {
7497
7685
  * Send UI configuration to server
7498
7686
  */
7499
7687
  sendUIConfig(uiConfig) {
7500
- const config = { ui: uiConfig };
7688
+ this.sendConfig({ ui: uiConfig });
7689
+ }
7690
+ /**
7691
+ * Send full viewer configuration to server
7692
+ */
7693
+ sendConfig(config) {
7501
7694
  const message = {
7502
7695
  type: MESSAGE_TYPES.CONFIG,
7503
7696
  config
7504
7697
  };
7698
+ logger.debug("Sending config", message);
7505
7699
  this.eventBus.emit(EVENT_NAMES.CONFIG_MESSAGE, { message });
7506
7700
  }
7507
7701
  }
@@ -7512,6 +7706,7 @@ class VirtualdisplayClient {
7512
7706
  messageHandler;
7513
7707
  viewerService;
7514
7708
  cameraApi;
7709
+ snapshotApi;
7515
7710
  options;
7516
7711
  iframe;
7517
7712
  constructor(options) {
@@ -7522,6 +7717,7 @@ class VirtualdisplayClient {
7522
7717
  this.messageHandler = new MessageHandler(this.eventBus);
7523
7718
  this.viewerService = new VirtualdisplayViewerService(this.eventBus);
7524
7719
  this.cameraApi = new Camera(this.eventBus);
7720
+ this.snapshotApi = new Snapshot(this.eventBus);
7525
7721
  this.initializeLogging();
7526
7722
  this.createAndConnectIframe();
7527
7723
  this.setupInitialConfig();
@@ -7621,6 +7817,12 @@ class VirtualdisplayClient {
7621
7817
  get camera() {
7622
7818
  return this.cameraApi;
7623
7819
  }
7820
+ /**
7821
+ * Get the snapshot API for taking screenshots
7822
+ */
7823
+ get snapshot() {
7824
+ return this.snapshotApi;
7825
+ }
7624
7826
  destroy() {
7625
7827
  logger.info("Destroying Virtualdisplay client");
7626
7828
  this.attributeService.clear();
@@ -7645,6 +7847,8 @@ export {
7645
7847
  Mutation,
7646
7848
  NODE_TYPES,
7647
7849
  NodeSelector,
7850
+ Photo,
7851
+ Snapshot,
7648
7852
  VirtualdisplayClient,
7649
7853
  VirtualdisplayError,
7650
7854
  VirtualdisplayViewerService,