three.scanner

  1# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  2# This file is Auto Generated by generateScannerPy.py. Do not edit this file manually.
  3# Modifications should be made to _scanner.py and then run generateScannerPy.py to update this file.
  4# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  5
  6## @package three
  7# @file scanner.py
  8# @brief Scanner class to wrap websocket connection. This file will be copied and amended with the three methods.
  9# @date 2024-11-27
 10# @copyright © 2024 Matter and Form. All rights reserved.
 11
 12from MF.V3 import Three
 13from MF.V3.Task import Task
 14from MF.V3.Settings.Align import Align
 15from MF.V3.Settings.AutoFocus import AutoFocus
 16from MF.V3.Settings.ScanSelection import ScanSelection
 17from MF.V3.Settings.CaptureImage import CaptureImage
 18from MF.V3.Settings.Camera import Camera
 19from MF.V3.Settings.Projector import Projector
 20from MF.V3.Settings.Turntable import Turntable
 21from MF.V3.Settings.Capture import Capture
 22from MF.V3.Settings.Scan import Scan
 23from MF.V3.Settings.Export import Export
 24from MF.V3.Settings.Import import Import
 25from MF.V3.Settings.Merge import Merge
 26from MF.V3.Settings.ScanData import ScanData
 27from MF.V3.Settings.Smooth import Smooth
 28from MF.V3.Settings.Advanced import Advanced
 29from MF.V3.Settings.I18n import I18n
 30from MF.V3.Settings.Style import Style
 31from MF.V3.Settings.Tutorials import Tutorials
 32from MF.V3.Settings.Viewer import Viewer
 33from MF.V3.Settings.Software import Software
 34
 35from typing import Any, Callable, Optional, List
 36import websocket
 37import json
 38import threading
 39import time
 40from MF.V3 import Task, TaskState, Buffer
 41from three import __version__
 42import three.MF
 43from three.serialization import TO_JSON
 44from three.MF.V3.Buffer import Buffer
 45
 46
 47
 48
 49class Scanner:
 50    """
 51    Main class to manage and communicate with the Matter and Form THREE 3D Scanner via websocket.
 52
 53    Attributes:
 54        * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error.
 55        * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}}
 56        * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks.
 57    """
 58    
 59    __bufferDescriptor = None
 60    __buffer = None
 61    __error = None
 62    __taskIndex:int = 0
 63    __tasks:List[Task] = []
 64
 65
 66    def __init__(self,
 67        OnTask: Optional[Callable[[Task], None]] = None,
 68        OnMessage: Optional[Callable[[str], None]] = None,
 69        OnBuffer: Optional[Callable[[Any, bytes], None]] = None,
 70        ):
 71        """
 72        Initializes the Scanner object.
 73
 74        Args:
 75            * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error.
 76            * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}}
 77            * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks.
 78        """
 79        self.__isConnected = False
 80
 81        self.OnTask = OnTask
 82        self.OnMessage = OnMessage
 83        self.OnBuffer = OnBuffer
 84        
 85        self.__task_return_event = threading.Event()
 86        
 87        # Dynamically add methods from Three to Scanner
 88        # self._add_three_methods()
 89
 90    # def _add_three_methods(self):
 91    #     """
 92    #     Dynamically adds functions from the three_methods module to the Scanner class.
 93    #     """
 94    #     for name, func in inspect.getmembers(Three, predicate=inspect.isfunction):
 95    #         if not name.startswith('_'):
 96    #             setattr(self, name, func.__get__(self, self.__class__))
 97
 98
 99    def Connect(self, URI:str, timeoutSec=5) -> bool:
100        """
101        Attempts to connect to the scanner using the specified URI and timeout.
102
103        Args:
104            * URI (str): The URI of the websocket server.
105            * timeoutSec (int): Timeout in seconds, default is 5.
106
107        Returns:
108            bool: True if connection is successful, raises Exception otherwise.
109
110        Raises:
111            Exception: If connection fails within the timeout or due to an error.
112        """
113        print('Connecting to: ', URI)
114        self.__URI = URI
115        self.__isConnected = False
116        self.__error = None
117
118        self.__serverVersion__= None
119
120        self.websocket = websocket.WebSocketApp(self.__URI,
121                              on_open=self.__OnOpen,
122                              on_close=self.__OnClose,
123                              on_error=self.__OnError,
124                              on_message=self.__OnMessage,
125                              )
126        
127        wst = threading.Thread(target=self.websocket.run_forever)
128        wst.daemon = True
129        wst.start()
130
131        # Wait for connection
132        start = time.time()
133        while time.time() < start + timeoutSec:
134            if self.__isConnected:
135                # Not checking versions => return True
136                    return True
137            elif self.__error:
138                raise Exception(self.__error)
139            time.sleep(0.1)
140        
141        raise Exception('Connection timeout')
142        
143    def Disconnect(self) -> None:
144        """
145        Close the websocket connection.
146        """
147        if self.__isConnected:
148            # Close the connection
149            self.websocket.close()
150            # Wait for the connection to be closed.
151            while self.__isConnected:
152                time.sleep(0.1)
153
154    def IsConnected(self)-> bool:
155        """
156        Checks if the scanner is currently connected.
157
158        Returns:
159            bool: True if connected, False otherwise.
160        """
161        return self.__isConnected
162    
163    def __callback(self, callback, *args) -> None:
164        if callback:
165                callback(self, *args)
166
167    # Called when the connection is opened
168    def __OnOpen(self, ws) -> None:
169        """
170        Callback function for when the websocket connection is successfully opened.
171
172        Prints a success message to the console.
173
174        Args:
175            ws: The websocket object.
176        """
177        self.__isConnected = True
178        print('Connected to: ', self.__URI)
179
180    # Called when the connection is closed
181    def __OnClose(self, ws, close_status_code, close_msg):
182        """
183        Callback function for when the websocket connection is closed.
184
185        Prints a disconnect message to the console.
186
187        Args:
188            ws: The websocket object.
189            close_status_code: The code indicating why the websocket was closed.
190            close_msg: Additional message about why the websocket was closed.
191        """
192        if self.__isConnected:
193            print('Disconnected')
194        self.__isConnected = False
195
196    # Called when an error happens
197    def __OnError(self, ws, error) -> None:
198        """
199        Callback function for when an error occurs in the websocket connection.
200
201        Prints an error message to the console and stores the error for reference.
202
203        Args:
204            ws: The websocket object.
205            error: The error that occurred.
206        """
207        if self.__isConnected:
208            print('Error: ', error)    
209        else:
210            self.__error = error
211        
212    # Called when a message arrives on the connection
213    def __OnMessage(self, ws, message) -> None:
214        """
215        Callback function for handling messages received via the websocket.
216
217        Determines the type of message received (Task, Buffer, or general Message) and
218        triggers the corresponding handler function if one is set.
219
220        Args:
221            ws: The websocket object.
222            message: The raw message received, which can be either a byte string or a JSON string.
223        """
224        # Bytes ?
225        if isinstance(message, bytes):
226            if self.OnBuffer:
227                
228                if self.__buffer:
229                    self.__buffer += message
230                else:
231                    self.__buffer = message
232                if self.__bufferDescriptor.Size == len(self.__buffer):
233                    self.OnBuffer(self.__bufferDescriptor, message)
234                    self.__bufferDescriptor = None 
235                    self.__buffer = None
236        else:
237            obj = json.loads(message)              
238        
239            # Task
240            if 'Task' in obj:
241                # Create the task from the message
242                task = Task(**obj['Task'])
243                
244                if (task.Progress):
245                    # Extract the first (and only) item from the task.Progress dictionary
246                    # TODO Duct tape fix due to schema weirdness for progress
247                    key, process = next(iter(task.Progress.items()))
248                    task.Progress = type('Progress', (object,), {
249                        'current': process["current"],
250                        'step': process["step"],
251                        'total': process["total"]
252                    })()
253
254                # Find the original task for reference
255                inputTask = self.__FindTaskWithIndex(task.Index)
256                if inputTask == None:
257                    raise Exception('Task not found')
258                    
259                if task.Error:
260                    inputTask.Error = task.Error
261                    self.__OnError(self.websocket, task.Error)
262                    self.__task_return_event.set()
263                    
264                # If assigned => Call the handler
265                if self.OnTask:
266                    self.OnTask(task)
267                
268                
269                # If waiting for a response, set the response and notify
270                if (task.State == TaskState.Completed.value):
271                    if task.Output:
272                        inputTask.Output = task.Output
273                    self.__task_return_event.set()
274                elif (task.State == TaskState.Failed.value):
275                    inputTask.Error = task.Error
276                    self.__task_return_event
277                    
278            # Buffer
279            elif 'Buffer' in obj:
280                self.__bufferDescriptor = Buffer(**obj['Buffer'])
281                self.__buffer = None    
282            # Message
283            elif 'Message' in obj:
284                if self.OnMessage:
285                    self.OnMessage(obj)
286
287    def SendTask(self, task, buffer:bytes = None) -> Any:
288        """
289        Sends a task to the scanner.
290        Tasks are general control requests for the scanner. (eg. Camera exposure, or Get Image)
291
292        Creates a task, serializes it, and sends it via the websocket.
293
294        Args:
295            * task (Task): The task to send.
296            * buffer (bytes): The buffer data to send, default is None.
297
298        Returns:
299            Any: The task object that was sent.
300
301        Raises:
302            AssertionError: If the connection is not established.
303        """
304        assert self.__isConnected
305
306        # Update the index
307        task.Index = self.__taskIndex
308        task.Input.Index = self.__taskIndex
309        self.__taskIndex += 1
310
311        # Send the task
312        self.__task_return_event.clear()
313        
314        # Append the task
315        self.__tasks.append(task)
316
317        if buffer == None:
318            self.__SendTask(task)
319        else:
320            self.__SendTaskWithBuffer(task, buffer)
321
322        if task.Output:
323            # Wait for response
324            self.__task_return_event.wait()
325
326        self.__tasks.remove(task)
327
328        return task
329    
330    # Send a task to the scanner
331    def __SendTask(self, task):
332        assert self.__isConnected
333
334        # Serialize the task
335        message = TO_JSON(task.Input)
336        
337        # Build and send the message
338        message = '{"Task":' + message + '}'
339        print('Message: ', message)
340
341        self.websocket.send(message)
342
343    # Send a task with its buffer to the scanner
344    def __SendTaskWithBuffer(self, task:Task, buffer:bytes):
345        assert self.__isConnected
346
347        # Send the task
348        self.__SendTask(task)
349
350        # Build the buffer descriptor
351        bufferSize = len(buffer)
352        bufferDescriptor = Buffer(0, bufferSize, task)
353
354        # Serialize the buffer descriptor
355        bufferMessage = TO_JSON(bufferDescriptor)
356
357        # Send the buffer descriptor
358        bufferMessage = '{"Buffer":' + bufferMessage + '}'
359        self.websocket.send(bufferMessage)
360
361        # The maximum websocket payload size is 32 MB.
362        MAX_SIZE = 32000000
363        sentSize = 0
364
365        # Send all the sub-payloads of the maximum payload size.
366        while sentSize + MAX_SIZE < bufferSize:
367            self.websocket.send(buffer[sentSize:sentSize + MAX_SIZE], websocket.ABNF.OPCODE_BINARY)
368            sentSize += MAX_SIZE
369
370        # Send the remaining data.
371        if sentSize < bufferSize:
372            self.websocket.send(buffer[sentSize:bufferSize], websocket.ABNF.OPCODE_BINARY)
373    
374    def __FindTaskWithIndex(self, index:int) -> Task:
375        # Find the task in the list
376        for i, t in enumerate(self.__tasks):
377            if t.Index == index:
378                return t
379                break
380        return None
381
382    # Dynamically bound functions from three.py
383
384    def add_merge_to_project(self) -> 'Task':
385        """Add a merged scan to the current project."""
386        return Three.add_merge_to_project(self)
387
388    def align(self, source: 'int', target: 'int', rough: 'Align.Rough' = None, fine: 'Align.Fine' = None) -> 'Task':
389        """Align two scan groups."""
390        return Three.align(self, source, target, rough, fine)
391
392    def auto_focus(self, applyAll: 'bool', cameras: 'list[AutoFocus.Camera]' = None) -> 'Task':
393        """Auto focus one or both cameras."""
394        return Three.auto_focus(self, applyAll, cameras)
395
396    def bounding_box(self, selection: 'ScanSelection', axisAligned: 'bool') -> 'Task':
397        """Get the bounding box of a set of scan groups."""
398        return Three.bounding_box(self, selection, axisAligned)
399
400    def calibrate_cameras(self) -> 'Task':
401        """Calibrate the cameras."""
402        return Three.calibrate_cameras(self)
403
404    def calibrate_turntable(self) -> 'Task':
405        """Calibrate the turntable."""
406        return Three.calibrate_turntable(self)
407
408    def calibration_capture_targets(self) -> 'Task':
409        """Get the calibration capture target for each camera calibration capture."""
410        return Three.calibration_capture_targets(self)
411
412    def camera_calibration(self) -> 'Task':
413        """Get the camera calibration descriptor."""
414        return Three.camera_calibration(self)
415
416    def capture_image(self, selection: 'list[int]' = None, codec: 'CaptureImage.Codec' = None, grayscale: 'bool' = None) -> 'Task':
417        """Capture a single Image."""
418        return Three.capture_image(self, selection, codec, grayscale)
419
420    def clear_settings(self) -> 'Task':
421        """Clear scanner settings and restore the default values."""
422        return Three.clear_settings(self)
423
424    def close_project(self) -> 'Task':
425        """Close the current open project."""
426        return Three.close_project(self)
427
428    def connect_wifi(self, ssid: 'str', password: 'str') -> 'Task':
429        """Connect to a wifi network."""
430        return Three.connect_wifi(self, ssid, password)
431
432    def copy_groups(self, sourceIndexes: 'list[int]' = None, targetIndex: 'int' = None, childPosition: 'int' = None, nameSuffix: 'str' = None, enumerate: 'bool' = None) -> 'Task':
433        """Copy a set of scan groups in the current open project."""
434        return Three.copy_groups(self, sourceIndexes, targetIndex, childPosition, nameSuffix, enumerate)
435
436    def depth_map(self, camera: 'Camera' = None, projector: 'Projector' = None, turntable: 'Turntable' = None, capture: 'Capture' = None, processing: 'Scan.Processing' = None, alignWithScanner: 'bool' = None, centerAtOrigin: 'bool' = None) -> 'Task':
437        """Capture a depth map."""
438        return Three.depth_map(self, camera, projector, turntable, capture, processing, alignWithScanner, centerAtOrigin)
439
440    def detect_calibration_card(self, Input: 'int') -> 'Task':
441        """Detect the calibration card on one or both cameras."""
442        return Three.detect_calibration_card(self, Input)
443
444    def download_project(self, Input: 'int') -> 'Task':
445        """Download a project from the scanner."""
446        return Three.download_project(self, Input)
447
448    def export(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task':
449        """Export a group of scans."""
450        return Three.export(self, selection, texture, merge, format, scale, color)
451
452    def export_factory_calibration_logs(self) -> 'Task':
453        """Export factory calibration logs."""
454        return Three.export_factory_calibration_logs(self)
455
456    def export_heat_map(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task':
457        """Export a mesh with vertex colors generated by the 'HeatMap' task."""
458        return Three.export_heat_map(self, selection, texture, merge, format, scale, color)
459
460    def export_logs(self, Input: 'bool' = None) -> 'Task':
461        """Export scanner logs."""
462        return Three.export_logs(self, Input)
463
464    def export_merge(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task':
465        """Export a merged scan."""
466        return Three.export_merge(self, selection, texture, merge, format, scale, color)
467
468    def factory_reset(self) -> 'Task':
469        """Reset the scanner to factory settings."""
470        return Three.factory_reset(self)
471
472    def flatten_group(self, Input: 'int') -> 'Task':
473        """Flatten a scan group such that it only consists of single scans."""
474        return Three.flatten_group(self, Input)
475
476    def forget_wifi(self) -> 'Task':
477        """Forget all wifi connections."""
478        return Three.forget_wifi(self)
479
480    def has_cameras(self) -> 'Task':
481        """Check if the scanner has working cameras."""
482        return Three.has_cameras(self)
483
484    def has_projector(self) -> 'Task':
485        """Check if the scanner has a working projector."""
486        return Three.has_projector(self)
487
488    def has_turntable(self) -> 'Task':
489        """Check if the scanner is connected to a working turntable."""
490        return Three.has_turntable(self)
491
492    def heat_map(self, sources: 'list[int]' = None, targets: 'list[int]' = None, outlierDistance: 'float' = None) -> 'Task':
493        """Compute the point-to-mesh distances of a source mesh to a target mesh and visualize as a heat map."""
494        return Three.heat_map(self, sources, targets, outlierDistance)
495
496    def import_file(self, name: 'str' = None, scale: 'float' = None, unit: 'Import.Unit' = None, center: 'bool' = None, groupIndex: 'int' = None) -> 'Task':
497        """Import a set of 3D meshes to the current open project.  The meshes must be archived in a ZIP file."""
498        return Three.import_file(self, name, scale, unit, center, groupIndex)
499
500    def list_export_formats(self) -> 'Task':
501        """List all export formats."""
502        return Three.list_export_formats(self)
503
504    def list_groups(self) -> 'Task':
505        """List the scan groups in the current open project."""
506        return Three.list_groups(self)
507
508    def list_network_interfaces(self) -> 'Task':
509        """List available wifi networks."""
510        return Three.list_network_interfaces(self)
511
512    def list_projects(self) -> 'Task':
513        """List all projects."""
514        return Three.list_projects(self)
515
516    def list_scans(self) -> 'Task':
517        """List the scans in the current open project."""
518        return Three.list_scans(self)
519
520    def list_settings(self) -> 'Task':
521        """Get scanner settings."""
522        return Three.list_settings(self)
523
524    def list_wifi(self) -> 'Task':
525        """List available wifi networks."""
526        return Three.list_wifi(self)
527
528    def merge(self, selection: 'ScanSelection' = None, remesh: 'Merge.Remesh' = None, simplify: 'Merge.Simplify' = None, texturize: 'bool' = None) -> 'Task':
529        """Merge two or more scan groups."""
530        return Three.merge(self, selection, remesh, simplify, texturize)
531
532    def merge_data(self, index: 'int', mergeStep: 'ScanData.MergeStep' = None, buffers: 'list[ScanData.Buffer]' = None, metadata: 'list[ScanData.Metadata]' = None) -> 'Task':
533        """Download the raw scan data for the current merge process."""
534        return Three.merge_data(self, index, mergeStep, buffers, metadata)
535
536    def move_group(self, Input: 'list[int]' = None) -> 'Task':
537        """Move a scan group."""
538        return Three.move_group(self, Input)
539
540    def new_group(self, parentIndex: 'int' = None, baseName: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task':
541        """Create a new scan group."""
542        return Three.new_group(self, parentIndex, baseName, color, visible, collapsed, rotation, translation)
543
544    def new_project(self, Input: 'str' = None) -> 'Task':
545        """Create a new project."""
546        return Three.new_project(self, Input)
547
548    def new_scan(self, camera: 'Camera' = None, projector: 'Projector' = None, turntable: 'Turntable' = None, capture: 'Capture' = None, processing: 'Scan.Processing' = None, alignWithScanner: 'bool' = None, centerAtOrigin: 'bool' = None) -> 'Task':
549        """Capture a new scan."""
550        return Three.new_scan(self, camera, projector, turntable, capture, processing, alignWithScanner, centerAtOrigin)
551
552    def open_project(self, Input: 'int') -> 'Task':
553        """Open an existing project."""
554        return Three.open_project(self, Input)
555
556    def pop_settings(self, Input: 'bool' = None) -> 'Task':
557        """Pop and restore scanner settings from the settings stack."""
558        return Three.pop_settings(self, Input)
559
560    def push_settings(self) -> 'Task':
561        """Push the current scanner settings to the settings stack."""
562        return Three.push_settings(self)
563
564    def reboot(self) -> 'Task':
565        """Reboot the scanner."""
566        return Three.reboot(self)
567
568    def remove_groups(self, Input: 'list[int]' = None) -> 'Task':
569        """Remove selected scan groups."""
570        return Three.remove_groups(self, Input)
571
572    def remove_projects(self, Input: 'list[int]' = None) -> 'Task':
573        """Remove selected projects."""
574        return Three.remove_projects(self, Input)
575
576    def restore_factory_calibration(self) -> 'Task':
577        """Restore factory calibration."""
578        return Three.restore_factory_calibration(self)
579
580    def rotate_turntable(self, Input: 'int') -> 'Task':
581        """Rotate the turntable."""
582        return Three.rotate_turntable(self, Input)
583
584    def scan_data(self, index: 'int', mergeStep: 'ScanData.MergeStep' = None, buffers: 'list[ScanData.Buffer]' = None, metadata: 'list[ScanData.Metadata]' = None) -> 'Task':
585        """Download the raw scan data for a scan in the current open project."""
586        return Three.scan_data(self, index, mergeStep, buffers, metadata)
587
588    def set_cameras(self, selection: 'list[int]' = None, autoExposure: 'bool' = None, exposure: 'int' = None, analogGain: 'float' = None, digitalGain: 'int' = None, focus: 'int' = None) -> 'Task':
589        """Apply camera settings to one or both cameras."""
590        return Three.set_cameras(self, selection, autoExposure, exposure, analogGain, digitalGain, focus)
591
592    def set_group(self, index: 'int', name: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task':
593        """Set scan group properties."""
594        return Three.set_group(self, index, name, color, visible, collapsed, rotation, translation)
595
596    def set_project(self, index: 'int' = None, name: 'str' = None) -> 'Task':
597        """Apply settings to the current open project."""
598        return Three.set_project(self, index, name)
599
600    def set_projector(self, on: 'bool' = None, brightness: 'float' = None, pattern: 'Projector.Pattern' = None, image: 'Projector.Image' = None, color: 'list[float]' = None, buffer: 'bytes' = None) -> 'Task':
601        """Apply projector settings."""
602        return Three.set_projector(self, on, brightness, pattern, image, color, buffer)
603
604    def shutdown(self) -> 'Task':
605        """Shutdown the scanner."""
606        return Three.shutdown(self)
607
608    def smooth(self, selection: 'ScanSelection' = None, taubin: 'Smooth.Taubin' = None) -> 'Task':
609        """Smooth a set of scans."""
610        return Three.smooth(self, selection, taubin)
611
612    def split_group(self, Input: 'int') -> 'Task':
613        """Split a scan group (ie. move its subgroups to its parent group)."""
614        return Three.split_group(self, Input)
615
616    def start_video(self) -> 'Task':
617        """Start the video stream."""
618        return Three.start_video(self)
619
620    def stop_video(self) -> 'Task':
621        """Stop the video stream."""
622        return Three.stop_video(self)
623
624    def system_info(self, updateMajor: 'bool' = None, updateNightly: 'bool' = None) -> 'Task':
625        """Get system information."""
626        return Three.system_info(self, updateMajor, updateNightly)
627
628    def transform_group(self, index: 'int', name: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task':
629        """Apply a rigid transformation to a group."""
630        return Three.transform_group(self, index, name, color, visible, collapsed, rotation, translation)
631
632    def turntable_calibration(self) -> 'Task':
633        """Get the turntable calibration descriptor."""
634        return Three.turntable_calibration(self)
635
636    def update_settings(self, advanced: 'Advanced' = None, camera: 'Camera' = None, capture: 'Capture' = None, i18n: 'I18n' = None, projector: 'Projector' = None, style: 'Style' = None, turntable: 'Turntable' = None, tutorials: 'Tutorials' = None, viewer: 'Viewer' = None, software: 'Software' = None) -> 'Task':
637        """Update scanner settings."""
638        return Three.update_settings(self, advanced, camera, capture, i18n, projector, style, turntable, tutorials, viewer, software)
639
640    def upload_project(self, buffer: 'bytes') -> 'Task':
641        """Upload a project to the scanner."""
642        return Three.upload_project(self, buffer)
class Scanner:
 50class Scanner:
 51    """
 52    Main class to manage and communicate with the Matter and Form THREE 3D Scanner via websocket.
 53
 54    Attributes:
 55        * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error.
 56        * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}}
 57        * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks.
 58    """
 59    
 60    __bufferDescriptor = None
 61    __buffer = None
 62    __error = None
 63    __taskIndex:int = 0
 64    __tasks:List[Task] = []
 65
 66
 67    def __init__(self,
 68        OnTask: Optional[Callable[[Task], None]] = None,
 69        OnMessage: Optional[Callable[[str], None]] = None,
 70        OnBuffer: Optional[Callable[[Any, bytes], None]] = None,
 71        ):
 72        """
 73        Initializes the Scanner object.
 74
 75        Args:
 76            * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error.
 77            * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}}
 78            * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks.
 79        """
 80        self.__isConnected = False
 81
 82        self.OnTask = OnTask
 83        self.OnMessage = OnMessage
 84        self.OnBuffer = OnBuffer
 85        
 86        self.__task_return_event = threading.Event()
 87        
 88        # Dynamically add methods from Three to Scanner
 89        # self._add_three_methods()
 90
 91    # def _add_three_methods(self):
 92    #     """
 93    #     Dynamically adds functions from the three_methods module to the Scanner class.
 94    #     """
 95    #     for name, func in inspect.getmembers(Three, predicate=inspect.isfunction):
 96    #         if not name.startswith('_'):
 97    #             setattr(self, name, func.__get__(self, self.__class__))
 98
 99
100    def Connect(self, URI:str, timeoutSec=5) -> bool:
101        """
102        Attempts to connect to the scanner using the specified URI and timeout.
103
104        Args:
105            * URI (str): The URI of the websocket server.
106            * timeoutSec (int): Timeout in seconds, default is 5.
107
108        Returns:
109            bool: True if connection is successful, raises Exception otherwise.
110
111        Raises:
112            Exception: If connection fails within the timeout or due to an error.
113        """
114        print('Connecting to: ', URI)
115        self.__URI = URI
116        self.__isConnected = False
117        self.__error = None
118
119        self.__serverVersion__= None
120
121        self.websocket = websocket.WebSocketApp(self.__URI,
122                              on_open=self.__OnOpen,
123                              on_close=self.__OnClose,
124                              on_error=self.__OnError,
125                              on_message=self.__OnMessage,
126                              )
127        
128        wst = threading.Thread(target=self.websocket.run_forever)
129        wst.daemon = True
130        wst.start()
131
132        # Wait for connection
133        start = time.time()
134        while time.time() < start + timeoutSec:
135            if self.__isConnected:
136                # Not checking versions => return True
137                    return True
138            elif self.__error:
139                raise Exception(self.__error)
140            time.sleep(0.1)
141        
142        raise Exception('Connection timeout')
143        
144    def Disconnect(self) -> None:
145        """
146        Close the websocket connection.
147        """
148        if self.__isConnected:
149            # Close the connection
150            self.websocket.close()
151            # Wait for the connection to be closed.
152            while self.__isConnected:
153                time.sleep(0.1)
154
155    def IsConnected(self)-> bool:
156        """
157        Checks if the scanner is currently connected.
158
159        Returns:
160            bool: True if connected, False otherwise.
161        """
162        return self.__isConnected
163    
164    def __callback(self, callback, *args) -> None:
165        if callback:
166                callback(self, *args)
167
168    # Called when the connection is opened
169    def __OnOpen(self, ws) -> None:
170        """
171        Callback function for when the websocket connection is successfully opened.
172
173        Prints a success message to the console.
174
175        Args:
176            ws: The websocket object.
177        """
178        self.__isConnected = True
179        print('Connected to: ', self.__URI)
180
181    # Called when the connection is closed
182    def __OnClose(self, ws, close_status_code, close_msg):
183        """
184        Callback function for when the websocket connection is closed.
185
186        Prints a disconnect message to the console.
187
188        Args:
189            ws: The websocket object.
190            close_status_code: The code indicating why the websocket was closed.
191            close_msg: Additional message about why the websocket was closed.
192        """
193        if self.__isConnected:
194            print('Disconnected')
195        self.__isConnected = False
196
197    # Called when an error happens
198    def __OnError(self, ws, error) -> None:
199        """
200        Callback function for when an error occurs in the websocket connection.
201
202        Prints an error message to the console and stores the error for reference.
203
204        Args:
205            ws: The websocket object.
206            error: The error that occurred.
207        """
208        if self.__isConnected:
209            print('Error: ', error)    
210        else:
211            self.__error = error
212        
213    # Called when a message arrives on the connection
214    def __OnMessage(self, ws, message) -> None:
215        """
216        Callback function for handling messages received via the websocket.
217
218        Determines the type of message received (Task, Buffer, or general Message) and
219        triggers the corresponding handler function if one is set.
220
221        Args:
222            ws: The websocket object.
223            message: The raw message received, which can be either a byte string or a JSON string.
224        """
225        # Bytes ?
226        if isinstance(message, bytes):
227            if self.OnBuffer:
228                
229                if self.__buffer:
230                    self.__buffer += message
231                else:
232                    self.__buffer = message
233                if self.__bufferDescriptor.Size == len(self.__buffer):
234                    self.OnBuffer(self.__bufferDescriptor, message)
235                    self.__bufferDescriptor = None 
236                    self.__buffer = None
237        else:
238            obj = json.loads(message)              
239        
240            # Task
241            if 'Task' in obj:
242                # Create the task from the message
243                task = Task(**obj['Task'])
244                
245                if (task.Progress):
246                    # Extract the first (and only) item from the task.Progress dictionary
247                    # TODO Duct tape fix due to schema weirdness for progress
248                    key, process = next(iter(task.Progress.items()))
249                    task.Progress = type('Progress', (object,), {
250                        'current': process["current"],
251                        'step': process["step"],
252                        'total': process["total"]
253                    })()
254
255                # Find the original task for reference
256                inputTask = self.__FindTaskWithIndex(task.Index)
257                if inputTask == None:
258                    raise Exception('Task not found')
259                    
260                if task.Error:
261                    inputTask.Error = task.Error
262                    self.__OnError(self.websocket, task.Error)
263                    self.__task_return_event.set()
264                    
265                # If assigned => Call the handler
266                if self.OnTask:
267                    self.OnTask(task)
268                
269                
270                # If waiting for a response, set the response and notify
271                if (task.State == TaskState.Completed.value):
272                    if task.Output:
273                        inputTask.Output = task.Output
274                    self.__task_return_event.set()
275                elif (task.State == TaskState.Failed.value):
276                    inputTask.Error = task.Error
277                    self.__task_return_event
278                    
279            # Buffer
280            elif 'Buffer' in obj:
281                self.__bufferDescriptor = Buffer(**obj['Buffer'])
282                self.__buffer = None    
283            # Message
284            elif 'Message' in obj:
285                if self.OnMessage:
286                    self.OnMessage(obj)
287
288    def SendTask(self, task, buffer:bytes = None) -> Any:
289        """
290        Sends a task to the scanner.
291        Tasks are general control requests for the scanner. (eg. Camera exposure, or Get Image)
292
293        Creates a task, serializes it, and sends it via the websocket.
294
295        Args:
296            * task (Task): The task to send.
297            * buffer (bytes): The buffer data to send, default is None.
298
299        Returns:
300            Any: The task object that was sent.
301
302        Raises:
303            AssertionError: If the connection is not established.
304        """
305        assert self.__isConnected
306
307        # Update the index
308        task.Index = self.__taskIndex
309        task.Input.Index = self.__taskIndex
310        self.__taskIndex += 1
311
312        # Send the task
313        self.__task_return_event.clear()
314        
315        # Append the task
316        self.__tasks.append(task)
317
318        if buffer == None:
319            self.__SendTask(task)
320        else:
321            self.__SendTaskWithBuffer(task, buffer)
322
323        if task.Output:
324            # Wait for response
325            self.__task_return_event.wait()
326
327        self.__tasks.remove(task)
328
329        return task
330    
331    # Send a task to the scanner
332    def __SendTask(self, task):
333        assert self.__isConnected
334
335        # Serialize the task
336        message = TO_JSON(task.Input)
337        
338        # Build and send the message
339        message = '{"Task":' + message + '}'
340        print('Message: ', message)
341
342        self.websocket.send(message)
343
344    # Send a task with its buffer to the scanner
345    def __SendTaskWithBuffer(self, task:Task, buffer:bytes):
346        assert self.__isConnected
347
348        # Send the task
349        self.__SendTask(task)
350
351        # Build the buffer descriptor
352        bufferSize = len(buffer)
353        bufferDescriptor = Buffer(0, bufferSize, task)
354
355        # Serialize the buffer descriptor
356        bufferMessage = TO_JSON(bufferDescriptor)
357
358        # Send the buffer descriptor
359        bufferMessage = '{"Buffer":' + bufferMessage + '}'
360        self.websocket.send(bufferMessage)
361
362        # The maximum websocket payload size is 32 MB.
363        MAX_SIZE = 32000000
364        sentSize = 0
365
366        # Send all the sub-payloads of the maximum payload size.
367        while sentSize + MAX_SIZE < bufferSize:
368            self.websocket.send(buffer[sentSize:sentSize + MAX_SIZE], websocket.ABNF.OPCODE_BINARY)
369            sentSize += MAX_SIZE
370
371        # Send the remaining data.
372        if sentSize < bufferSize:
373            self.websocket.send(buffer[sentSize:bufferSize], websocket.ABNF.OPCODE_BINARY)
374    
375    def __FindTaskWithIndex(self, index:int) -> Task:
376        # Find the task in the list
377        for i, t in enumerate(self.__tasks):
378            if t.Index == index:
379                return t
380                break
381        return None
382
383    # Dynamically bound functions from three.py
384
385    def add_merge_to_project(self) -> 'Task':
386        """Add a merged scan to the current project."""
387        return Three.add_merge_to_project(self)
388
389    def align(self, source: 'int', target: 'int', rough: 'Align.Rough' = None, fine: 'Align.Fine' = None) -> 'Task':
390        """Align two scan groups."""
391        return Three.align(self, source, target, rough, fine)
392
393    def auto_focus(self, applyAll: 'bool', cameras: 'list[AutoFocus.Camera]' = None) -> 'Task':
394        """Auto focus one or both cameras."""
395        return Three.auto_focus(self, applyAll, cameras)
396
397    def bounding_box(self, selection: 'ScanSelection', axisAligned: 'bool') -> 'Task':
398        """Get the bounding box of a set of scan groups."""
399        return Three.bounding_box(self, selection, axisAligned)
400
401    def calibrate_cameras(self) -> 'Task':
402        """Calibrate the cameras."""
403        return Three.calibrate_cameras(self)
404
405    def calibrate_turntable(self) -> 'Task':
406        """Calibrate the turntable."""
407        return Three.calibrate_turntable(self)
408
409    def calibration_capture_targets(self) -> 'Task':
410        """Get the calibration capture target for each camera calibration capture."""
411        return Three.calibration_capture_targets(self)
412
413    def camera_calibration(self) -> 'Task':
414        """Get the camera calibration descriptor."""
415        return Three.camera_calibration(self)
416
417    def capture_image(self, selection: 'list[int]' = None, codec: 'CaptureImage.Codec' = None, grayscale: 'bool' = None) -> 'Task':
418        """Capture a single Image."""
419        return Three.capture_image(self, selection, codec, grayscale)
420
421    def clear_settings(self) -> 'Task':
422        """Clear scanner settings and restore the default values."""
423        return Three.clear_settings(self)
424
425    def close_project(self) -> 'Task':
426        """Close the current open project."""
427        return Three.close_project(self)
428
429    def connect_wifi(self, ssid: 'str', password: 'str') -> 'Task':
430        """Connect to a wifi network."""
431        return Three.connect_wifi(self, ssid, password)
432
433    def copy_groups(self, sourceIndexes: 'list[int]' = None, targetIndex: 'int' = None, childPosition: 'int' = None, nameSuffix: 'str' = None, enumerate: 'bool' = None) -> 'Task':
434        """Copy a set of scan groups in the current open project."""
435        return Three.copy_groups(self, sourceIndexes, targetIndex, childPosition, nameSuffix, enumerate)
436
437    def depth_map(self, camera: 'Camera' = None, projector: 'Projector' = None, turntable: 'Turntable' = None, capture: 'Capture' = None, processing: 'Scan.Processing' = None, alignWithScanner: 'bool' = None, centerAtOrigin: 'bool' = None) -> 'Task':
438        """Capture a depth map."""
439        return Three.depth_map(self, camera, projector, turntable, capture, processing, alignWithScanner, centerAtOrigin)
440
441    def detect_calibration_card(self, Input: 'int') -> 'Task':
442        """Detect the calibration card on one or both cameras."""
443        return Three.detect_calibration_card(self, Input)
444
445    def download_project(self, Input: 'int') -> 'Task':
446        """Download a project from the scanner."""
447        return Three.download_project(self, Input)
448
449    def export(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task':
450        """Export a group of scans."""
451        return Three.export(self, selection, texture, merge, format, scale, color)
452
453    def export_factory_calibration_logs(self) -> 'Task':
454        """Export factory calibration logs."""
455        return Three.export_factory_calibration_logs(self)
456
457    def export_heat_map(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task':
458        """Export a mesh with vertex colors generated by the 'HeatMap' task."""
459        return Three.export_heat_map(self, selection, texture, merge, format, scale, color)
460
461    def export_logs(self, Input: 'bool' = None) -> 'Task':
462        """Export scanner logs."""
463        return Three.export_logs(self, Input)
464
465    def export_merge(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task':
466        """Export a merged scan."""
467        return Three.export_merge(self, selection, texture, merge, format, scale, color)
468
469    def factory_reset(self) -> 'Task':
470        """Reset the scanner to factory settings."""
471        return Three.factory_reset(self)
472
473    def flatten_group(self, Input: 'int') -> 'Task':
474        """Flatten a scan group such that it only consists of single scans."""
475        return Three.flatten_group(self, Input)
476
477    def forget_wifi(self) -> 'Task':
478        """Forget all wifi connections."""
479        return Three.forget_wifi(self)
480
481    def has_cameras(self) -> 'Task':
482        """Check if the scanner has working cameras."""
483        return Three.has_cameras(self)
484
485    def has_projector(self) -> 'Task':
486        """Check if the scanner has a working projector."""
487        return Three.has_projector(self)
488
489    def has_turntable(self) -> 'Task':
490        """Check if the scanner is connected to a working turntable."""
491        return Three.has_turntable(self)
492
493    def heat_map(self, sources: 'list[int]' = None, targets: 'list[int]' = None, outlierDistance: 'float' = None) -> 'Task':
494        """Compute the point-to-mesh distances of a source mesh to a target mesh and visualize as a heat map."""
495        return Three.heat_map(self, sources, targets, outlierDistance)
496
497    def import_file(self, name: 'str' = None, scale: 'float' = None, unit: 'Import.Unit' = None, center: 'bool' = None, groupIndex: 'int' = None) -> 'Task':
498        """Import a set of 3D meshes to the current open project.  The meshes must be archived in a ZIP file."""
499        return Three.import_file(self, name, scale, unit, center, groupIndex)
500
501    def list_export_formats(self) -> 'Task':
502        """List all export formats."""
503        return Three.list_export_formats(self)
504
505    def list_groups(self) -> 'Task':
506        """List the scan groups in the current open project."""
507        return Three.list_groups(self)
508
509    def list_network_interfaces(self) -> 'Task':
510        """List available wifi networks."""
511        return Three.list_network_interfaces(self)
512
513    def list_projects(self) -> 'Task':
514        """List all projects."""
515        return Three.list_projects(self)
516
517    def list_scans(self) -> 'Task':
518        """List the scans in the current open project."""
519        return Three.list_scans(self)
520
521    def list_settings(self) -> 'Task':
522        """Get scanner settings."""
523        return Three.list_settings(self)
524
525    def list_wifi(self) -> 'Task':
526        """List available wifi networks."""
527        return Three.list_wifi(self)
528
529    def merge(self, selection: 'ScanSelection' = None, remesh: 'Merge.Remesh' = None, simplify: 'Merge.Simplify' = None, texturize: 'bool' = None) -> 'Task':
530        """Merge two or more scan groups."""
531        return Three.merge(self, selection, remesh, simplify, texturize)
532
533    def merge_data(self, index: 'int', mergeStep: 'ScanData.MergeStep' = None, buffers: 'list[ScanData.Buffer]' = None, metadata: 'list[ScanData.Metadata]' = None) -> 'Task':
534        """Download the raw scan data for the current merge process."""
535        return Three.merge_data(self, index, mergeStep, buffers, metadata)
536
537    def move_group(self, Input: 'list[int]' = None) -> 'Task':
538        """Move a scan group."""
539        return Three.move_group(self, Input)
540
541    def new_group(self, parentIndex: 'int' = None, baseName: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task':
542        """Create a new scan group."""
543        return Three.new_group(self, parentIndex, baseName, color, visible, collapsed, rotation, translation)
544
545    def new_project(self, Input: 'str' = None) -> 'Task':
546        """Create a new project."""
547        return Three.new_project(self, Input)
548
549    def new_scan(self, camera: 'Camera' = None, projector: 'Projector' = None, turntable: 'Turntable' = None, capture: 'Capture' = None, processing: 'Scan.Processing' = None, alignWithScanner: 'bool' = None, centerAtOrigin: 'bool' = None) -> 'Task':
550        """Capture a new scan."""
551        return Three.new_scan(self, camera, projector, turntable, capture, processing, alignWithScanner, centerAtOrigin)
552
553    def open_project(self, Input: 'int') -> 'Task':
554        """Open an existing project."""
555        return Three.open_project(self, Input)
556
557    def pop_settings(self, Input: 'bool' = None) -> 'Task':
558        """Pop and restore scanner settings from the settings stack."""
559        return Three.pop_settings(self, Input)
560
561    def push_settings(self) -> 'Task':
562        """Push the current scanner settings to the settings stack."""
563        return Three.push_settings(self)
564
565    def reboot(self) -> 'Task':
566        """Reboot the scanner."""
567        return Three.reboot(self)
568
569    def remove_groups(self, Input: 'list[int]' = None) -> 'Task':
570        """Remove selected scan groups."""
571        return Three.remove_groups(self, Input)
572
573    def remove_projects(self, Input: 'list[int]' = None) -> 'Task':
574        """Remove selected projects."""
575        return Three.remove_projects(self, Input)
576
577    def restore_factory_calibration(self) -> 'Task':
578        """Restore factory calibration."""
579        return Three.restore_factory_calibration(self)
580
581    def rotate_turntable(self, Input: 'int') -> 'Task':
582        """Rotate the turntable."""
583        return Three.rotate_turntable(self, Input)
584
585    def scan_data(self, index: 'int', mergeStep: 'ScanData.MergeStep' = None, buffers: 'list[ScanData.Buffer]' = None, metadata: 'list[ScanData.Metadata]' = None) -> 'Task':
586        """Download the raw scan data for a scan in the current open project."""
587        return Three.scan_data(self, index, mergeStep, buffers, metadata)
588
589    def set_cameras(self, selection: 'list[int]' = None, autoExposure: 'bool' = None, exposure: 'int' = None, analogGain: 'float' = None, digitalGain: 'int' = None, focus: 'int' = None) -> 'Task':
590        """Apply camera settings to one or both cameras."""
591        return Three.set_cameras(self, selection, autoExposure, exposure, analogGain, digitalGain, focus)
592
593    def set_group(self, index: 'int', name: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task':
594        """Set scan group properties."""
595        return Three.set_group(self, index, name, color, visible, collapsed, rotation, translation)
596
597    def set_project(self, index: 'int' = None, name: 'str' = None) -> 'Task':
598        """Apply settings to the current open project."""
599        return Three.set_project(self, index, name)
600
601    def set_projector(self, on: 'bool' = None, brightness: 'float' = None, pattern: 'Projector.Pattern' = None, image: 'Projector.Image' = None, color: 'list[float]' = None, buffer: 'bytes' = None) -> 'Task':
602        """Apply projector settings."""
603        return Three.set_projector(self, on, brightness, pattern, image, color, buffer)
604
605    def shutdown(self) -> 'Task':
606        """Shutdown the scanner."""
607        return Three.shutdown(self)
608
609    def smooth(self, selection: 'ScanSelection' = None, taubin: 'Smooth.Taubin' = None) -> 'Task':
610        """Smooth a set of scans."""
611        return Three.smooth(self, selection, taubin)
612
613    def split_group(self, Input: 'int') -> 'Task':
614        """Split a scan group (ie. move its subgroups to its parent group)."""
615        return Three.split_group(self, Input)
616
617    def start_video(self) -> 'Task':
618        """Start the video stream."""
619        return Three.start_video(self)
620
621    def stop_video(self) -> 'Task':
622        """Stop the video stream."""
623        return Three.stop_video(self)
624
625    def system_info(self, updateMajor: 'bool' = None, updateNightly: 'bool' = None) -> 'Task':
626        """Get system information."""
627        return Three.system_info(self, updateMajor, updateNightly)
628
629    def transform_group(self, index: 'int', name: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task':
630        """Apply a rigid transformation to a group."""
631        return Three.transform_group(self, index, name, color, visible, collapsed, rotation, translation)
632
633    def turntable_calibration(self) -> 'Task':
634        """Get the turntable calibration descriptor."""
635        return Three.turntable_calibration(self)
636
637    def update_settings(self, advanced: 'Advanced' = None, camera: 'Camera' = None, capture: 'Capture' = None, i18n: 'I18n' = None, projector: 'Projector' = None, style: 'Style' = None, turntable: 'Turntable' = None, tutorials: 'Tutorials' = None, viewer: 'Viewer' = None, software: 'Software' = None) -> 'Task':
638        """Update scanner settings."""
639        return Three.update_settings(self, advanced, camera, capture, i18n, projector, style, turntable, tutorials, viewer, software)
640
641    def upload_project(self, buffer: 'bytes') -> 'Task':
642        """Upload a project to the scanner."""
643        return Three.upload_project(self, buffer)

Main class to manage and communicate with the Matter and Form THREE 3D Scanner via websocket.

Attributes: * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error. * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}} * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks.

Scanner( OnTask: Optional[Callable[[MF.V3.Task.Task], NoneType]] = None, OnMessage: Optional[Callable[[str], NoneType]] = None, OnBuffer: Optional[Callable[[Any, bytes], NoneType]] = None)
67    def __init__(self,
68        OnTask: Optional[Callable[[Task], None]] = None,
69        OnMessage: Optional[Callable[[str], None]] = None,
70        OnBuffer: Optional[Callable[[Any, bytes], None]] = None,
71        ):
72        """
73        Initializes the Scanner object.
74
75        Args:
76            * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error.
77            * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}}
78            * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks.
79        """
80        self.__isConnected = False
81
82        self.OnTask = OnTask
83        self.OnMessage = OnMessage
84        self.OnBuffer = OnBuffer
85        
86        self.__task_return_event = threading.Event()
87        
88        # Dynamically add methods from Three to Scanner
89        # self._add_three_methods()

Initializes the Scanner object.

Args: * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error. * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}} * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks.

OnTask
OnMessage
OnBuffer
def Connect(self, URI: str, timeoutSec=5) -> bool:
100    def Connect(self, URI:str, timeoutSec=5) -> bool:
101        """
102        Attempts to connect to the scanner using the specified URI and timeout.
103
104        Args:
105            * URI (str): The URI of the websocket server.
106            * timeoutSec (int): Timeout in seconds, default is 5.
107
108        Returns:
109            bool: True if connection is successful, raises Exception otherwise.
110
111        Raises:
112            Exception: If connection fails within the timeout or due to an error.
113        """
114        print('Connecting to: ', URI)
115        self.__URI = URI
116        self.__isConnected = False
117        self.__error = None
118
119        self.__serverVersion__= None
120
121        self.websocket = websocket.WebSocketApp(self.__URI,
122                              on_open=self.__OnOpen,
123                              on_close=self.__OnClose,
124                              on_error=self.__OnError,
125                              on_message=self.__OnMessage,
126                              )
127        
128        wst = threading.Thread(target=self.websocket.run_forever)
129        wst.daemon = True
130        wst.start()
131
132        # Wait for connection
133        start = time.time()
134        while time.time() < start + timeoutSec:
135            if self.__isConnected:
136                # Not checking versions => return True
137                    return True
138            elif self.__error:
139                raise Exception(self.__error)
140            time.sleep(0.1)
141        
142        raise Exception('Connection timeout')

Attempts to connect to the scanner using the specified URI and timeout.

Args: * URI (str): The URI of the websocket server. * timeoutSec (int): Timeout in seconds, default is 5.

Returns: bool: True if connection is successful, raises Exception otherwise.

Raises: Exception: If connection fails within the timeout or due to an error.

def Disconnect(self) -> None:
144    def Disconnect(self) -> None:
145        """
146        Close the websocket connection.
147        """
148        if self.__isConnected:
149            # Close the connection
150            self.websocket.close()
151            # Wait for the connection to be closed.
152            while self.__isConnected:
153                time.sleep(0.1)

Close the websocket connection.

def IsConnected(self) -> bool:
155    def IsConnected(self)-> bool:
156        """
157        Checks if the scanner is currently connected.
158
159        Returns:
160            bool: True if connected, False otherwise.
161        """
162        return self.__isConnected

Checks if the scanner is currently connected.

Returns: bool: True if connected, False otherwise.

def SendTask(self, task, buffer: bytes = None) -> Any:
288    def SendTask(self, task, buffer:bytes = None) -> Any:
289        """
290        Sends a task to the scanner.
291        Tasks are general control requests for the scanner. (eg. Camera exposure, or Get Image)
292
293        Creates a task, serializes it, and sends it via the websocket.
294
295        Args:
296            * task (Task): The task to send.
297            * buffer (bytes): The buffer data to send, default is None.
298
299        Returns:
300            Any: The task object that was sent.
301
302        Raises:
303            AssertionError: If the connection is not established.
304        """
305        assert self.__isConnected
306
307        # Update the index
308        task.Index = self.__taskIndex
309        task.Input.Index = self.__taskIndex
310        self.__taskIndex += 1
311
312        # Send the task
313        self.__task_return_event.clear()
314        
315        # Append the task
316        self.__tasks.append(task)
317
318        if buffer == None:
319            self.__SendTask(task)
320        else:
321            self.__SendTaskWithBuffer(task, buffer)
322
323        if task.Output:
324            # Wait for response
325            self.__task_return_event.wait()
326
327        self.__tasks.remove(task)
328
329        return task

Sends a task to the scanner. Tasks are general control requests for the scanner. (eg. Camera exposure, or Get Image)

Creates a task, serializes it, and sends it via the websocket.

Args: * task (Task): The task to send. * buffer (bytes): The buffer data to send, default is None.

Returns: Any: The task object that was sent.

Raises: AssertionError: If the connection is not established.

def add_merge_to_project(self) -> MF.V3.Task.Task:
385    def add_merge_to_project(self) -> 'Task':
386        """Add a merged scan to the current project."""
387        return Three.add_merge_to_project(self)

Add a merged scan to the current project.

def align( self, source: int, target: int, rough: MF.V3.Settings.Align.Align.Rough = None, fine: MF.V3.Settings.Align.Align.Fine = None) -> MF.V3.Task.Task:
389    def align(self, source: 'int', target: 'int', rough: 'Align.Rough' = None, fine: 'Align.Fine' = None) -> 'Task':
390        """Align two scan groups."""
391        return Three.align(self, source, target, rough, fine)

Align two scan groups.

def auto_focus( self, applyAll: bool, cameras: list[MF.V3.Settings.AutoFocus.AutoFocus.Camera] = None) -> MF.V3.Task.Task:
393    def auto_focus(self, applyAll: 'bool', cameras: 'list[AutoFocus.Camera]' = None) -> 'Task':
394        """Auto focus one or both cameras."""
395        return Three.auto_focus(self, applyAll, cameras)

Auto focus one or both cameras.

def bounding_box( self, selection: MF.V3.Settings.ScanSelection.ScanSelection, axisAligned: bool) -> MF.V3.Task.Task:
397    def bounding_box(self, selection: 'ScanSelection', axisAligned: 'bool') -> 'Task':
398        """Get the bounding box of a set of scan groups."""
399        return Three.bounding_box(self, selection, axisAligned)

Get the bounding box of a set of scan groups.

def calibrate_cameras(self) -> MF.V3.Task.Task:
401    def calibrate_cameras(self) -> 'Task':
402        """Calibrate the cameras."""
403        return Three.calibrate_cameras(self)

Calibrate the cameras.

def calibrate_turntable(self) -> MF.V3.Task.Task:
405    def calibrate_turntable(self) -> 'Task':
406        """Calibrate the turntable."""
407        return Three.calibrate_turntable(self)

Calibrate the turntable.

def calibration_capture_targets(self) -> MF.V3.Task.Task:
409    def calibration_capture_targets(self) -> 'Task':
410        """Get the calibration capture target for each camera calibration capture."""
411        return Three.calibration_capture_targets(self)

Get the calibration capture target for each camera calibration capture.

def camera_calibration(self) -> MF.V3.Task.Task:
413    def camera_calibration(self) -> 'Task':
414        """Get the camera calibration descriptor."""
415        return Three.camera_calibration(self)

Get the camera calibration descriptor.

def capture_image( self, selection: list[int] = None, codec: MF.V3.Settings.CaptureImage.CaptureImage.Codec = None, grayscale: bool = None) -> MF.V3.Task.Task:
417    def capture_image(self, selection: 'list[int]' = None, codec: 'CaptureImage.Codec' = None, grayscale: 'bool' = None) -> 'Task':
418        """Capture a single Image."""
419        return Three.capture_image(self, selection, codec, grayscale)

Capture a single Image.

def clear_settings(self) -> MF.V3.Task.Task:
421    def clear_settings(self) -> 'Task':
422        """Clear scanner settings and restore the default values."""
423        return Three.clear_settings(self)

Clear scanner settings and restore the default values.

def close_project(self) -> MF.V3.Task.Task:
425    def close_project(self) -> 'Task':
426        """Close the current open project."""
427        return Three.close_project(self)

Close the current open project.

def connect_wifi(self, ssid: str, password: str) -> MF.V3.Task.Task:
429    def connect_wifi(self, ssid: 'str', password: 'str') -> 'Task':
430        """Connect to a wifi network."""
431        return Three.connect_wifi(self, ssid, password)

Connect to a wifi network.

def copy_groups( self, sourceIndexes: list[int] = None, targetIndex: int = None, childPosition: int = None, nameSuffix: str = None, enumerate: bool = None) -> MF.V3.Task.Task:
433    def copy_groups(self, sourceIndexes: 'list[int]' = None, targetIndex: 'int' = None, childPosition: 'int' = None, nameSuffix: 'str' = None, enumerate: 'bool' = None) -> 'Task':
434        """Copy a set of scan groups in the current open project."""
435        return Three.copy_groups(self, sourceIndexes, targetIndex, childPosition, nameSuffix, enumerate)

Copy a set of scan groups in the current open project.

def depth_map( self, camera: MF.V3.Settings.Camera.Camera = None, projector: MF.V3.Settings.Projector.Projector = None, turntable: MF.V3.Settings.Turntable.Turntable = None, capture: MF.V3.Settings.Capture.Capture = None, processing: MF.V3.Settings.Scan.Scan.Processing = None, alignWithScanner: bool = None, centerAtOrigin: bool = None) -> MF.V3.Task.Task:
437    def depth_map(self, camera: 'Camera' = None, projector: 'Projector' = None, turntable: 'Turntable' = None, capture: 'Capture' = None, processing: 'Scan.Processing' = None, alignWithScanner: 'bool' = None, centerAtOrigin: 'bool' = None) -> 'Task':
438        """Capture a depth map."""
439        return Three.depth_map(self, camera, projector, turntable, capture, processing, alignWithScanner, centerAtOrigin)

Capture a depth map.

def detect_calibration_card(self, Input: int) -> MF.V3.Task.Task:
441    def detect_calibration_card(self, Input: 'int') -> 'Task':
442        """Detect the calibration card on one or both cameras."""
443        return Three.detect_calibration_card(self, Input)

Detect the calibration card on one or both cameras.

def download_project(self, Input: int) -> MF.V3.Task.Task:
445    def download_project(self, Input: 'int') -> 'Task':
446        """Download a project from the scanner."""
447        return Three.download_project(self, Input)

Download a project from the scanner.

def export( self, selection: MF.V3.Settings.ScanSelection.ScanSelection = None, texture: bool = None, merge: bool = None, format: MF.V3.Settings.Export.Export.Format = None, scale: float = None, color: MF.V3.Settings.Export.Export.Color = None) -> MF.V3.Task.Task:
449    def export(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task':
450        """Export a group of scans."""
451        return Three.export(self, selection, texture, merge, format, scale, color)

Export a group of scans.

def export_factory_calibration_logs(self) -> MF.V3.Task.Task:
453    def export_factory_calibration_logs(self) -> 'Task':
454        """Export factory calibration logs."""
455        return Three.export_factory_calibration_logs(self)

Export factory calibration logs.

def export_heat_map( self, selection: MF.V3.Settings.ScanSelection.ScanSelection = None, texture: bool = None, merge: bool = None, format: MF.V3.Settings.Export.Export.Format = None, scale: float = None, color: MF.V3.Settings.Export.Export.Color = None) -> MF.V3.Task.Task:
457    def export_heat_map(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task':
458        """Export a mesh with vertex colors generated by the 'HeatMap' task."""
459        return Three.export_heat_map(self, selection, texture, merge, format, scale, color)

Export a mesh with vertex colors generated by the 'HeatMap' task.

def export_logs(self, Input: bool = None) -> MF.V3.Task.Task:
461    def export_logs(self, Input: 'bool' = None) -> 'Task':
462        """Export scanner logs."""
463        return Three.export_logs(self, Input)

Export scanner logs.

def export_merge( self, selection: MF.V3.Settings.ScanSelection.ScanSelection = None, texture: bool = None, merge: bool = None, format: MF.V3.Settings.Export.Export.Format = None, scale: float = None, color: MF.V3.Settings.Export.Export.Color = None) -> MF.V3.Task.Task:
465    def export_merge(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task':
466        """Export a merged scan."""
467        return Three.export_merge(self, selection, texture, merge, format, scale, color)

Export a merged scan.

def factory_reset(self) -> MF.V3.Task.Task:
469    def factory_reset(self) -> 'Task':
470        """Reset the scanner to factory settings."""
471        return Three.factory_reset(self)

Reset the scanner to factory settings.

def flatten_group(self, Input: int) -> MF.V3.Task.Task:
473    def flatten_group(self, Input: 'int') -> 'Task':
474        """Flatten a scan group such that it only consists of single scans."""
475        return Three.flatten_group(self, Input)

Flatten a scan group such that it only consists of single scans.

def forget_wifi(self) -> MF.V3.Task.Task:
477    def forget_wifi(self) -> 'Task':
478        """Forget all wifi connections."""
479        return Three.forget_wifi(self)

Forget all wifi connections.

def has_cameras(self) -> MF.V3.Task.Task:
481    def has_cameras(self) -> 'Task':
482        """Check if the scanner has working cameras."""
483        return Three.has_cameras(self)

Check if the scanner has working cameras.

def has_projector(self) -> MF.V3.Task.Task:
485    def has_projector(self) -> 'Task':
486        """Check if the scanner has a working projector."""
487        return Three.has_projector(self)

Check if the scanner has a working projector.

def has_turntable(self) -> MF.V3.Task.Task:
489    def has_turntable(self) -> 'Task':
490        """Check if the scanner is connected to a working turntable."""
491        return Three.has_turntable(self)

Check if the scanner is connected to a working turntable.

def heat_map( self, sources: list[int] = None, targets: list[int] = None, outlierDistance: float = None) -> MF.V3.Task.Task:
493    def heat_map(self, sources: 'list[int]' = None, targets: 'list[int]' = None, outlierDistance: 'float' = None) -> 'Task':
494        """Compute the point-to-mesh distances of a source mesh to a target mesh and visualize as a heat map."""
495        return Three.heat_map(self, sources, targets, outlierDistance)

Compute the point-to-mesh distances of a source mesh to a target mesh and visualize as a heat map.

def import_file( self, name: str = None, scale: float = None, unit: MF.V3.Settings.Import.Import.Unit = None, center: bool = None, groupIndex: int = None) -> MF.V3.Task.Task:
497    def import_file(self, name: 'str' = None, scale: 'float' = None, unit: 'Import.Unit' = None, center: 'bool' = None, groupIndex: 'int' = None) -> 'Task':
498        """Import a set of 3D meshes to the current open project.  The meshes must be archived in a ZIP file."""
499        return Three.import_file(self, name, scale, unit, center, groupIndex)

Import a set of 3D meshes to the current open project. The meshes must be archived in a ZIP file.

def list_export_formats(self) -> MF.V3.Task.Task:
501    def list_export_formats(self) -> 'Task':
502        """List all export formats."""
503        return Three.list_export_formats(self)

List all export formats.

def list_groups(self) -> MF.V3.Task.Task:
505    def list_groups(self) -> 'Task':
506        """List the scan groups in the current open project."""
507        return Three.list_groups(self)

List the scan groups in the current open project.

def list_network_interfaces(self) -> MF.V3.Task.Task:
509    def list_network_interfaces(self) -> 'Task':
510        """List available wifi networks."""
511        return Three.list_network_interfaces(self)

List available wifi networks.

def list_projects(self) -> MF.V3.Task.Task:
513    def list_projects(self) -> 'Task':
514        """List all projects."""
515        return Three.list_projects(self)

List all projects.

def list_scans(self) -> MF.V3.Task.Task:
517    def list_scans(self) -> 'Task':
518        """List the scans in the current open project."""
519        return Three.list_scans(self)

List the scans in the current open project.

def list_settings(self) -> MF.V3.Task.Task:
521    def list_settings(self) -> 'Task':
522        """Get scanner settings."""
523        return Three.list_settings(self)

Get scanner settings.

def list_wifi(self) -> MF.V3.Task.Task:
525    def list_wifi(self) -> 'Task':
526        """List available wifi networks."""
527        return Three.list_wifi(self)

List available wifi networks.

def merge( self, selection: MF.V3.Settings.ScanSelection.ScanSelection = None, remesh: MF.V3.Settings.Merge.Merge.Remesh = None, simplify: MF.V3.Settings.Merge.Merge.Simplify = None, texturize: bool = None) -> MF.V3.Task.Task:
529    def merge(self, selection: 'ScanSelection' = None, remesh: 'Merge.Remesh' = None, simplify: 'Merge.Simplify' = None, texturize: 'bool' = None) -> 'Task':
530        """Merge two or more scan groups."""
531        return Three.merge(self, selection, remesh, simplify, texturize)

Merge two or more scan groups.

def merge_data( self, index: int, mergeStep: MF.V3.Settings.ScanData.ScanData.MergeStep = None, buffers: list[MF.V3.Settings.ScanData.ScanData.Buffer] = None, metadata: list[MF.V3.Settings.ScanData.ScanData.Metadata] = None) -> MF.V3.Task.Task:
533    def merge_data(self, index: 'int', mergeStep: 'ScanData.MergeStep' = None, buffers: 'list[ScanData.Buffer]' = None, metadata: 'list[ScanData.Metadata]' = None) -> 'Task':
534        """Download the raw scan data for the current merge process."""
535        return Three.merge_data(self, index, mergeStep, buffers, metadata)

Download the raw scan data for the current merge process.

def move_group(self, Input: list[int] = None) -> MF.V3.Task.Task:
537    def move_group(self, Input: 'list[int]' = None) -> 'Task':
538        """Move a scan group."""
539        return Three.move_group(self, Input)

Move a scan group.

def new_group( self, parentIndex: int = None, baseName: str = None, color: list[float] = None, visible: bool = None, collapsed: bool = None, rotation: list[float] = None, translation: list[float] = None) -> MF.V3.Task.Task:
541    def new_group(self, parentIndex: 'int' = None, baseName: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task':
542        """Create a new scan group."""
543        return Three.new_group(self, parentIndex, baseName, color, visible, collapsed, rotation, translation)

Create a new scan group.

def new_project(self, Input: str = None) -> MF.V3.Task.Task:
545    def new_project(self, Input: 'str' = None) -> 'Task':
546        """Create a new project."""
547        return Three.new_project(self, Input)

Create a new project.

def new_scan( self, camera: MF.V3.Settings.Camera.Camera = None, projector: MF.V3.Settings.Projector.Projector = None, turntable: MF.V3.Settings.Turntable.Turntable = None, capture: MF.V3.Settings.Capture.Capture = None, processing: MF.V3.Settings.Scan.Scan.Processing = None, alignWithScanner: bool = None, centerAtOrigin: bool = None) -> MF.V3.Task.Task:
549    def new_scan(self, camera: 'Camera' = None, projector: 'Projector' = None, turntable: 'Turntable' = None, capture: 'Capture' = None, processing: 'Scan.Processing' = None, alignWithScanner: 'bool' = None, centerAtOrigin: 'bool' = None) -> 'Task':
550        """Capture a new scan."""
551        return Three.new_scan(self, camera, projector, turntable, capture, processing, alignWithScanner, centerAtOrigin)

Capture a new scan.

def open_project(self, Input: int) -> MF.V3.Task.Task:
553    def open_project(self, Input: 'int') -> 'Task':
554        """Open an existing project."""
555        return Three.open_project(self, Input)

Open an existing project.

def pop_settings(self, Input: bool = None) -> MF.V3.Task.Task:
557    def pop_settings(self, Input: 'bool' = None) -> 'Task':
558        """Pop and restore scanner settings from the settings stack."""
559        return Three.pop_settings(self, Input)

Pop and restore scanner settings from the settings stack.

def push_settings(self) -> MF.V3.Task.Task:
561    def push_settings(self) -> 'Task':
562        """Push the current scanner settings to the settings stack."""
563        return Three.push_settings(self)

Push the current scanner settings to the settings stack.

def reboot(self) -> MF.V3.Task.Task:
565    def reboot(self) -> 'Task':
566        """Reboot the scanner."""
567        return Three.reboot(self)

Reboot the scanner.

def remove_groups(self, Input: list[int] = None) -> MF.V3.Task.Task:
569    def remove_groups(self, Input: 'list[int]' = None) -> 'Task':
570        """Remove selected scan groups."""
571        return Three.remove_groups(self, Input)

Remove selected scan groups.

def remove_projects(self, Input: list[int] = None) -> MF.V3.Task.Task:
573    def remove_projects(self, Input: 'list[int]' = None) -> 'Task':
574        """Remove selected projects."""
575        return Three.remove_projects(self, Input)

Remove selected projects.

def restore_factory_calibration(self) -> MF.V3.Task.Task:
577    def restore_factory_calibration(self) -> 'Task':
578        """Restore factory calibration."""
579        return Three.restore_factory_calibration(self)

Restore factory calibration.

def rotate_turntable(self, Input: int) -> MF.V3.Task.Task:
581    def rotate_turntable(self, Input: 'int') -> 'Task':
582        """Rotate the turntable."""
583        return Three.rotate_turntable(self, Input)

Rotate the turntable.

def scan_data( self, index: int, mergeStep: MF.V3.Settings.ScanData.ScanData.MergeStep = None, buffers: list[MF.V3.Settings.ScanData.ScanData.Buffer] = None, metadata: list[MF.V3.Settings.ScanData.ScanData.Metadata] = None) -> MF.V3.Task.Task:
585    def scan_data(self, index: 'int', mergeStep: 'ScanData.MergeStep' = None, buffers: 'list[ScanData.Buffer]' = None, metadata: 'list[ScanData.Metadata]' = None) -> 'Task':
586        """Download the raw scan data for a scan in the current open project."""
587        return Three.scan_data(self, index, mergeStep, buffers, metadata)

Download the raw scan data for a scan in the current open project.

def set_cameras( self, selection: list[int] = None, autoExposure: bool = None, exposure: int = None, analogGain: float = None, digitalGain: int = None, focus: int = None) -> MF.V3.Task.Task:
589    def set_cameras(self, selection: 'list[int]' = None, autoExposure: 'bool' = None, exposure: 'int' = None, analogGain: 'float' = None, digitalGain: 'int' = None, focus: 'int' = None) -> 'Task':
590        """Apply camera settings to one or both cameras."""
591        return Three.set_cameras(self, selection, autoExposure, exposure, analogGain, digitalGain, focus)

Apply camera settings to one or both cameras.

def set_group( self, index: int, name: str = None, color: list[float] = None, visible: bool = None, collapsed: bool = None, rotation: list[float] = None, translation: list[float] = None) -> MF.V3.Task.Task:
593    def set_group(self, index: 'int', name: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task':
594        """Set scan group properties."""
595        return Three.set_group(self, index, name, color, visible, collapsed, rotation, translation)

Set scan group properties.

def set_project(self, index: int = None, name: str = None) -> MF.V3.Task.Task:
597    def set_project(self, index: 'int' = None, name: 'str' = None) -> 'Task':
598        """Apply settings to the current open project."""
599        return Three.set_project(self, index, name)

Apply settings to the current open project.

def set_projector( self, on: bool = None, brightness: float = None, pattern: MF.V3.Settings.Projector.Projector.Pattern = None, image: MF.V3.Settings.Projector.Projector.Image = None, color: list[float] = None, buffer: bytes = None) -> MF.V3.Task.Task:
601    def set_projector(self, on: 'bool' = None, brightness: 'float' = None, pattern: 'Projector.Pattern' = None, image: 'Projector.Image' = None, color: 'list[float]' = None, buffer: 'bytes' = None) -> 'Task':
602        """Apply projector settings."""
603        return Three.set_projector(self, on, brightness, pattern, image, color, buffer)

Apply projector settings.

def shutdown(self) -> MF.V3.Task.Task:
605    def shutdown(self) -> 'Task':
606        """Shutdown the scanner."""
607        return Three.shutdown(self)

Shutdown the scanner.

def smooth( self, selection: MF.V3.Settings.ScanSelection.ScanSelection = None, taubin: MF.V3.Settings.Smooth.Smooth.Taubin = None) -> MF.V3.Task.Task:
609    def smooth(self, selection: 'ScanSelection' = None, taubin: 'Smooth.Taubin' = None) -> 'Task':
610        """Smooth a set of scans."""
611        return Three.smooth(self, selection, taubin)

Smooth a set of scans.

def split_group(self, Input: int) -> MF.V3.Task.Task:
613    def split_group(self, Input: 'int') -> 'Task':
614        """Split a scan group (ie. move its subgroups to its parent group)."""
615        return Three.split_group(self, Input)

Split a scan group (ie. move its subgroups to its parent group).

def start_video(self) -> MF.V3.Task.Task:
617    def start_video(self) -> 'Task':
618        """Start the video stream."""
619        return Three.start_video(self)

Start the video stream.

def stop_video(self) -> MF.V3.Task.Task:
621    def stop_video(self) -> 'Task':
622        """Stop the video stream."""
623        return Three.stop_video(self)

Stop the video stream.

def system_info( self, updateMajor: bool = None, updateNightly: bool = None) -> MF.V3.Task.Task:
625    def system_info(self, updateMajor: 'bool' = None, updateNightly: 'bool' = None) -> 'Task':
626        """Get system information."""
627        return Three.system_info(self, updateMajor, updateNightly)

Get system information.

def transform_group( self, index: int, name: str = None, color: list[float] = None, visible: bool = None, collapsed: bool = None, rotation: list[float] = None, translation: list[float] = None) -> MF.V3.Task.Task:
629    def transform_group(self, index: 'int', name: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task':
630        """Apply a rigid transformation to a group."""
631        return Three.transform_group(self, index, name, color, visible, collapsed, rotation, translation)

Apply a rigid transformation to a group.

def turntable_calibration(self) -> MF.V3.Task.Task:
633    def turntable_calibration(self) -> 'Task':
634        """Get the turntable calibration descriptor."""
635        return Three.turntable_calibration(self)

Get the turntable calibration descriptor.

def update_settings( self, advanced: MF.V3.Settings.Advanced.Advanced = None, camera: MF.V3.Settings.Camera.Camera = None, capture: MF.V3.Settings.Capture.Capture = None, i18n: MF.V3.Settings.I18n.I18n = None, projector: MF.V3.Settings.Projector.Projector = None, style: MF.V3.Settings.Style.Style = None, turntable: MF.V3.Settings.Turntable.Turntable = None, tutorials: MF.V3.Settings.Tutorials.Tutorials = None, viewer: MF.V3.Settings.Viewer.Viewer = None, software: MF.V3.Settings.Software.Software = None) -> MF.V3.Task.Task:
637    def update_settings(self, advanced: 'Advanced' = None, camera: 'Camera' = None, capture: 'Capture' = None, i18n: 'I18n' = None, projector: 'Projector' = None, style: 'Style' = None, turntable: 'Turntable' = None, tutorials: 'Tutorials' = None, viewer: 'Viewer' = None, software: 'Software' = None) -> 'Task':
638        """Update scanner settings."""
639        return Three.update_settings(self, advanced, camera, capture, i18n, projector, style, turntable, tutorials, viewer, software)

Update scanner settings.

def upload_project(self, buffer: bytes) -> MF.V3.Task.Task:
641    def upload_project(self, buffer: 'bytes') -> 'Task':
642        """Upload a project to the scanner."""
643        return Three.upload_project(self, buffer)

Upload a project to the scanner.