File size: 11,120 Bytes
6931c7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
#!/usr/bin/env python3


import textwrap

__doc__ = textwrap.dedent(
    """
    `meshviewer` is a program that allows you to display polygonal
    meshes produced by `mesh` package.

    Viewing a mesh on a local machine
    ---------------------------------

    The most straightforward use-case is viewing the mesh on the same
    machine where it is stored.  To do this simply run

    ```
    $ meshviewer view sphere.obj
    ```

    This will create an interactive window with your mesh rendering.
    You can render more than one mesh in the same window by passing
    several paths to `view` command

    ```
    $ meshviewer view sphere.obj cylinder.obj
    ```

    This will arrange the subplots horizontally in a row.  If you want
    a grid arrangement, you can specify the grid parameters explicitly

    ```
    $ meshviewer view -nx 2 -ny 2 *.obj
    ```

    Viewing a mesh from a remote machine
    ------------------------------------

    It is also possible to view a mesh stored on a remote machine.  To
    do this you need mesh to be installed on both the local and the
    remote machines.  You start by opening an empty viewer window
    listening on a network port

    ```
    (local) $ meshviewer open --port 3000
    ```

    To stream a shape to this viewer you have to either pick a port
    that is visible from the remote machine or by manually exposing
    the port when connecting.  For example, through SSH port
    forwarding

    ```
    (local) $ ssh -R 3000:127.0.0.1:3000 user@host
    ```

    Then on a remote machine you use `view` command pointing to the
    locally forwarded port

    ```
    (remote) $ meshviewer view -p 3000 sphere.obj
    ```

    This should display the remote mesh on your local viewer. In case it
    does not it might be caused by the network connection being closed
    before the mesh could be sent. To work around this one can try
    increasing the timeout up to 1 second

    ```
    (remote) $ meshviewer view -p 3000 --timeout 1 sphere.obj
    ```

    To take a snapshot you should locally run a `snap` command

    ```
    (local) $ meshviewer snap -p 3000 sphere.png
    ```
    """)


import argparse
import logging
import sys
import time

from psbody.mesh.mesh import Mesh
from psbody.mesh.meshviewer import (
    MESH_VIEWER_DEFAULT_TITLE,
    MESH_VIEWER_DEFAULT_SHAPE,
    MESH_VIEWER_DEFAULT_WIDTH,
    MESH_VIEWER_DEFAULT_HEIGHT,
    ZMQ_HOST,
    MeshViewerLocal,
    MeshViewerRemote)


logging.basicConfig(level=logging.INFO)


parser_root = argparse.ArgumentParser(
    add_help=False,
    description="View the polygonal meshes, locally and across the network",
    epilog=__doc__,
    formatter_class=argparse.RawTextHelpFormatter)

subparsers = parser_root.add_subparsers(dest="command")
subparsers.required = True

parser_open = subparsers.add_parser("open", add_help=False)
parser_open.add_argument(
    "-p", "--port",
    help="local port to listen for incoming commands",
    type=int)

parser_view = subparsers.add_parser("view", add_help=False)
parser_view.add_argument(
    "-h", "--host",
    help="remote host",
    metavar="HOSTNAME",
    type=str)
parser_view.add_argument(
    "-p", "--port",
    help="remote port",
    type=int)
parser_view.add_argument(
    "-ix", "--subwindow-index-horizontal",
    help="horizontal index of the target subwindow",
    metavar="INDEX",
    type=int)
parser_view.add_argument(
    "-iy", "--subwindow-index-vertical",
    help="vertical index of the target subwindow",
    metavar="INDEX",
    type=int)
parser_view.add_argument(
    "--timeout",
    help="wait for some time after sending the mesh to let it render",
    metavar="SECONDS",
    type=float,
    default=0.5)
parser_view.add_argument(
    "filename",
    help="path to the mesh file",
    type=str,
    nargs="+")

for parser in parser_open, parser_view:
    window_options = parser.add_argument_group("window options")
    window_options.add_argument(
        "-t", "--title",
        help="window title",
        type=str)
    window_options.add_argument(
        "-ww", "-wx", "--window-width",
        help="window width in pixels",
        metavar="PIXELS",
        type=int)
    window_options.add_argument(
        "-wh", "-wy", "--window-height",
        help="window height in pixels",
        metavar="PIXELS",
        type=int)
    window_options.add_argument(
        "-nx", "--subwindow-number-horizontal",
        help="number of horizontal subwindows",
        metavar="NUMBER",
        type=int)
    window_options.add_argument(
        "-ny", "--subwindow-number-vertical",
        help="number of vertical subwindows",
        metavar="NUMBER",
        type=int)

parser_snap = subparsers.add_parser("snap", add_help=False)
parser_snap.add_argument(
    "-h", "--host",
    help="remote host",
    type=str)
parser_snap.add_argument(
    "-p", "--port",
    help="remote port",
    type=int,
    required=True)
parser_snap.add_argument(
    "filename",
    help="path to the output snapshot",
    type=str)


for p in parser_root, parser_open, parser_view, parser_snap:
    p.add_argument("--help", action="help")


def dispatch_command(args):
    """
    Performs a sanity check of the passed arguments and then
    dispatches the appropriate command.
    """

    if args.command == "open":
        start_server(args)
        return

    if not args.port:
        client = start_local_client(args)
    else:
        client = start_remote_client(args)

    if args.command == "snap":
        take_snapshot(client, args)

    if args.command == "view":
        if args.port is not None:
            # Below is a list of contradicting settings: it futile to
            # try to change the parameters of a mesh viewer already
            # running on a remote machine.
            if args.title is not None:
                logging.warning(
                    "--title is ignored when working with remote viewer")

            if args.window_width is not None:
                logging.warning(
                    "--window-width is ignored when working with remote viewer")

            if args.window_height is not None:
                logging.warning(
                    "--window-height is ignored when working with remote viewer")

            if args.subwindow_number_horizontal is not None:
                logging.warning(
                    "--subwindow-number-horizontal is ignored when working "
                    "with remote viewer")

            if args.subwindow_number_vertical is not None:
                logging.warning(
                    "--subwindow-number-vertical is ignored when working "
                    "with remote viewer")

            # This one is a bit different: while it should be
            # technically possible to stream the mesh in a specific
            # subwindow, we currently don't support that.
            if (
                args.subwindow_index_horizontal is not None or
                args.subwindow_index_vertical is not None
            ):
                logging.warning(
                    "unfortunately, drawing to a specific subwindow is not "
                    "supported when working with remote viewer and the first "
                    "subwindow is going to be used instead")

        if (
            args.subwindow_index_horizontal is not None and
            args.subwindow_index_vertical is None
        ) or (
            args.subwindow_index_horizontal is None and
            args.subwindow_index_vertical is not None
        ):
            logging.fatal(
                "you have to specify both horizontal "
                "and vertical subwindow incides")
            return

        if (
            args.subwindow_index_horizontal is not None and
            args.subwindow_index_vertical is not None
        ):
            display_single_subwindow(client, args)
        else:
            display_multi_subwindows(client, args)

        # Basically, wait for send_pyobj() to actually send everything
        # before terminating.
        time.sleep(args.timeout)


def start_server(args):
    """
    Starts a meshviewer window on a local machine.

    This function opens a mesh viewer window that listens for command
    on a given port.
    """
    server = MeshViewerRemote(
        titlebar=args.title or MESH_VIEWER_DEFAULT_TITLE,
        subwins_vert=args.subwindow_number_vertical or MESH_VIEWER_DEFAULT_SHAPE[1],
        subwins_horz=args.subwindow_number_horizontal or MESH_VIEWER_DEFAULT_SHAPE[0],
        width=args.window_width or MESH_VIEWER_DEFAULT_WIDTH,
        height=args.window_height or MESH_VIEWER_DEFAULT_HEIGHT,
        port=args.port)
    return server


def start_local_client(args):
    """
    Starts a local meshviewer not connected to anywhere.

    This function internally opens a mesh viewer window listening on a
    random port.
    """
    client = MeshViewerLocal(
        titlebar=args.title or MESH_VIEWER_DEFAULT_TITLE,
        window_width=args.window_width or MESH_VIEWER_DEFAULT_WIDTH,
        window_height=args.window_height or MESH_VIEWER_DEFAULT_HEIGHT,
        shape=(
            args.subwindow_number_vertical or 1,
            args.subwindow_number_horizontal or len(args.filename),
        ),
        keepalive=True)
    return client


def start_remote_client(args):
    """
    Starts a meshviewer client connected to a remote machine.

    This function does not create a new window, but is necessary to
    stream the mesh to a remote viewer.
    """
    client = MeshViewerLocal(
        host=args.host or ZMQ_HOST,
        port=args.port)
    return client


def display_single_subwindow(client, args):
    """
    Displays a single mesh in a given subwindow.
    """
    ix = args.subwindow_index_horizontal
    iy = args.subwindow_index_vertical

    try:
        subwindow = client.get_subwindows()[iy][ix]
    except IndexError:
        logging.fatal(
            "cannot find subwindow ({}, {}). "
            "The current viewer shape is {}x{} subwindows, "
            "indexing is zero-based."
            .format(ix, iy, *client.shape))
        return

    meshes = [Mesh(filename=filename) for filename in args.filename]
    subwindow.set_static_meshes(meshes)


def display_multi_subwindows(client, args):
    """
    Displays a list of meshes. One mesh per subwindow.
    """
    grid = client.get_subwindows()

    subwindows = [
        subwindow
        for row in grid
        for subwindow in row
    ]

    if len(subwindows) < len(args.filename):
        logging.warning(
            "cannot display {0} meshes in {1} subwindows. "
            "Taking the first {1}.".format(
                len(args.filename), len(subwindows)))

    for subwindow, filename in zip(subwindows, args.filename):
        mesh = Mesh(filename=filename)
        subwindow.set_static_meshes([mesh])


def take_snapshot(client, args):
    """
    Take snapshot and dump it into a file.
    """
    client.save_snapshot(args.filename)


if __name__ == "__main__":
    args = parser_root.parse_args()
    dispatch_command(args)
    sys.exit(0)