@virtualdisplay.io/client 3.4.1 → 3.5.1

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;
@@ -56,10 +57,15 @@ const EVENT_NAMES = {
56
57
  MUTATION_MESSAGE: "mutation:message",
57
58
  // Config flow (Client → Server)
58
59
  CONFIG_MESSAGE: "config:message",
60
+ // Camera flow (Client → Server)
61
+ CAMERA_MESSAGE: "camera:message",
59
62
  // State flow (Server → Client)
60
63
  MESSAGE_RECEIVED: "message:received",
61
64
  STATE_CHANGED: "state:changed",
62
- 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"
63
69
  };
64
70
  const g = Object.freeze({
65
71
  debug: 0,
@@ -6928,7 +6934,9 @@ class MappingValidator {
6928
6934
  const MESSAGE_TYPES = {
6929
6935
  MUTATION: "mutation",
6930
6936
  STATE: "state",
6931
- CONFIG: "config"
6937
+ CONFIG: "config",
6938
+ CAMERA: "camera",
6939
+ SNAPSHOT: "snapshot"
6932
6940
  };
6933
6941
  function createMutationMessage(mutations) {
6934
6942
  if (!Array.isArray(mutations)) {
@@ -6940,14 +6948,7 @@ function createMutationMessage(mutations) {
6940
6948
  };
6941
6949
  }
6942
6950
  function isValidMessage(data) {
6943
- return isMutationMessage(data) || isStateMessage(data) || isConfigMessage(data);
6944
- }
6945
- function isMutationMessage(data) {
6946
- if (typeof data !== "object" || data === null) {
6947
- return false;
6948
- }
6949
- const obj = data;
6950
- return obj.type === MESSAGE_TYPES.MUTATION && Array.isArray(obj.mutations);
6951
+ return isStateMessage(data) || isSnapshotDevelopedMessage(data);
6951
6952
  }
6952
6953
  function isStateMessage(data) {
6953
6954
  if (typeof data !== "object" || data === null) {
@@ -6956,12 +6957,12 @@ function isStateMessage(data) {
6956
6957
  const obj = data;
6957
6958
  return obj.type === MESSAGE_TYPES.STATE && Array.isArray(obj.nodes);
6958
6959
  }
6959
- function isConfigMessage(data) {
6960
+ function isSnapshotDevelopedMessage(data) {
6960
6961
  if (typeof data !== "object" || data === null) {
6961
6962
  return false;
6962
6963
  }
6963
6964
  const obj = data;
6964
- 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";
6965
6966
  }
6966
6967
  class AttributeService {
6967
6968
  attributes = /* @__PURE__ */ new Map();
@@ -7052,6 +7053,61 @@ class AttributeService {
7052
7053
  this.isInitialized = false;
7053
7054
  }
7054
7055
  }
7056
+ class Camera {
7057
+ constructor(eventBus) {
7058
+ this.eventBus = eventBus;
7059
+ }
7060
+ commands = [];
7061
+ /**
7062
+ * Rotate camera horizontally
7063
+ * @param degrees - Absolute rotation in degrees (0 = front view)
7064
+ */
7065
+ rotate(degrees) {
7066
+ this.commands.push({ action: "rotate", value: degrees });
7067
+ return this;
7068
+ }
7069
+ /**
7070
+ * Tilt camera vertically
7071
+ * @param degrees - Absolute tilt in degrees (0 = top view, 90 = horizontal)
7072
+ */
7073
+ tilt(degrees) {
7074
+ this.commands.push({ action: "tilt", value: degrees });
7075
+ return this;
7076
+ }
7077
+ /**
7078
+ * Zoom camera
7079
+ * @param percentage - Zoom percentage (100 = normal, >100 = zoom in, <100 = zoom out)
7080
+ */
7081
+ zoom(percentage) {
7082
+ this.commands.push({ action: "zoom", value: percentage });
7083
+ return this;
7084
+ }
7085
+ /**
7086
+ * Execute accumulated camera commands
7087
+ */
7088
+ set() {
7089
+ if (this.commands.length === 0) {
7090
+ return;
7091
+ }
7092
+ const message = {
7093
+ type: MESSAGE_TYPES.CAMERA,
7094
+ commands: this.commands
7095
+ };
7096
+ this.eventBus.emit(EVENT_NAMES.CAMERA_MESSAGE, { message });
7097
+ this.commands = [];
7098
+ }
7099
+ /**
7100
+ * Reset camera to initial position
7101
+ * This is not chainable and executes immediately
7102
+ */
7103
+ reset() {
7104
+ const message = {
7105
+ type: MESSAGE_TYPES.CAMERA,
7106
+ commands: [{ action: "reset" }]
7107
+ };
7108
+ this.eventBus.emit(EVENT_NAMES.CAMERA_MESSAGE, { message });
7109
+ }
7110
+ }
7055
7111
  class EventBus {
7056
7112
  listeners = /* @__PURE__ */ new Map();
7057
7113
  on(event, listener) {
@@ -7168,11 +7224,14 @@ class MessageHandler {
7168
7224
  setupOutgoingMessages() {
7169
7225
  const outgoingEvents = [
7170
7226
  EVENT_NAMES.MUTATION_MESSAGE,
7171
- EVENT_NAMES.CONFIG_MESSAGE
7227
+ EVENT_NAMES.CONFIG_MESSAGE,
7228
+ EVENT_NAMES.CAMERA_MESSAGE,
7229
+ EVENT_NAMES.SNAPSHOT_MESSAGE
7172
7230
  ];
7173
7231
  outgoingEvents.forEach((eventName) => {
7174
7232
  this.eventBus.on(eventName, (event) => {
7175
- this.queueOrSend(event.message);
7233
+ const messageEvent = event;
7234
+ this.queueOrSend(messageEvent.message);
7176
7235
  });
7177
7236
  });
7178
7237
  }
@@ -7360,6 +7419,156 @@ class StateService {
7360
7419
  this.hasReceivedInitialSync = false;
7361
7420
  }
7362
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
+ }
7363
7572
  class VirtualdisplayViewerService {
7364
7573
  eventBus;
7365
7574
  constructor(eventBus) {
@@ -7452,6 +7661,8 @@ class VirtualdisplayClient {
7452
7661
  stateService;
7453
7662
  messageHandler;
7454
7663
  viewerService;
7664
+ cameraApi;
7665
+ snapshotApi;
7455
7666
  options;
7456
7667
  iframe;
7457
7668
  constructor(options) {
@@ -7461,6 +7672,8 @@ class VirtualdisplayClient {
7461
7672
  this.stateService = new StateService(this.eventBus);
7462
7673
  this.messageHandler = new MessageHandler(this.eventBus);
7463
7674
  this.viewerService = new VirtualdisplayViewerService(this.eventBus);
7675
+ this.cameraApi = new Camera(this.eventBus);
7676
+ this.snapshotApi = new Snapshot(this.eventBus);
7464
7677
  this.initializeLogging();
7465
7678
  this.createAndConnectIframe();
7466
7679
  this.setupInitialConfig();
@@ -7554,6 +7767,18 @@ class VirtualdisplayClient {
7554
7767
  get viewer() {
7555
7768
  return this.viewerService;
7556
7769
  }
7770
+ /**
7771
+ * Get the camera control API
7772
+ */
7773
+ get camera() {
7774
+ return this.cameraApi;
7775
+ }
7776
+ /**
7777
+ * Get the snapshot API for taking screenshots
7778
+ */
7779
+ get snapshot() {
7780
+ return this.snapshotApi;
7781
+ }
7557
7782
  destroy() {
7558
7783
  logger.info("Destroying Virtualdisplay client");
7559
7784
  this.attributeService.clear();
@@ -7568,6 +7793,7 @@ class VirtualdisplayClient {
7568
7793
  }
7569
7794
  export {
7570
7795
  AttributeSelector,
7796
+ Camera,
7571
7797
  ERROR_CODES,
7572
7798
  EVENT_NAMES,
7573
7799
  MESSAGE_TYPES,
@@ -7577,6 +7803,8 @@ export {
7577
7803
  Mutation,
7578
7804
  NODE_TYPES,
7579
7805
  NodeSelector,
7806
+ Photo,
7807
+ Snapshot,
7580
7808
  VirtualdisplayClient,
7581
7809
  VirtualdisplayError,
7582
7810
  VirtualdisplayViewerService,