Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,14 @@ jobs:
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}

- name: Publish to Open VSX
if: ${{ env.OVSX_PAT != '' }}
run: npx --yes ovsx@0.10 publish python-ply-preview.vsix

- name: Create GitHub Release
run: gh release create "$GITHUB_REF_NAME" python-ply-preview.vsix --title "$GITHUB_REF_NAME" --generate-notes
env:
GH_TOKEN: ${{ github.token }}

# Last and non-blocking: an Open VSX problem must not hold back the Marketplace release.
# A failure shows as a warning on the run; publish by hand with `npx ovsx publish <vsix>`.
- name: Publish to Open VSX
if: ${{ env.OVSX_PAT != '' }}
continue-on-error: true
run: npx --yes ovsx@1 publish python-ply-preview.vsix
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ node_modules
.vscode-test/
*.vsix
test-fixtures/workspace/.vscode/
# Built by esbuild.mjs
media/viewer.js
4 changes: 2 additions & 2 deletions .vscode-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ function findPython() {
export default defineConfig({
files: 'out/test/**/*.test.js',
workspaceFolder: './test-fixtures/workspace',
// The integration tests drive a real debugpy session; 3D Preview is a declared dependency
installExtensions: ['ms-python.debugpy', 'tatsy.vscode-3d-preview'],
// The integration tests drive a real debugpy session
installExtensions: ['ms-python.debugpy'],
env: {
VIEWPLY_TEST_PYTHON: findPython(),
},
Expand Down
51 changes: 47 additions & 4 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,60 @@
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"label": "watch",
"dependsOn": [
"npm: watch",
"npm: watch:webview"
],
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": "build"
},
{
// Bundles the webview viewer; see esbuild.mjs
"type": "npm",
"script": "watch:webview",
"problemMatcher": {
"owner": "esbuild",
"source": "esbuild",
"fileLocation": "relative",
"pattern": [
{
"regexp": "^✘ \\[ERROR\\] (.*)$",
"message": 1
},
{
"regexp": "^\\s+(.*):(\\d+):(\\d+):$",
"file": 1,
"line": 2,
"column": 3
}
],
"background": {
"activeBegins": true,
"beginsPattern": "^\\[watch\\] build started",
"endsPattern": "^\\[watch\\] build finished"
}
},
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": "build"
}
]
}
1 change: 1 addition & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ images/*.gif
images/*.mp4
test-fixtures/**
.vscode-test.mjs
esbuild.mjs
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to the "python-ply-preview" extension will be documented in
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.


### 0.2.0

* **Built-in 3D viewer.** Point clouds open in the extension's own viewer, so the 3D Preview extension is no longer required. It has orbit controls, point size, RGB / height / solid coloring, a Z-up or Y-up toggle, axes and grid, and shows the point count and bounding box. Viewing the same variable again updates its tab.
* **Large clouds are subsampled** to 1,000,000 points by default (`pythonPlyPreview.maxPoints`), so the debugger and the viewer stay responsive. The viewer tells you when a cloud was subsampled.
* **Docker and remote debugging.** If the Python process can't write to your workspace, the point cloud is sent through the debugger instead.
* `.ply` files can be opened in the viewer with **Reopen Editor With...**.
* To keep using 3D Preview or another `.ply` editor, set `pythonPlyPreview.viewer` to `default`.

### 0.1.1

* Preview files left behind when VS Code was closed during a debug session are now removed after a day.
Expand Down
172 changes: 70 additions & 102 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,156 +1,124 @@
# VSCode Python PLY Preview
# Python PLY Preview

[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
[![Visual Studio Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/cmaranes.python-ply-preview)](https://marketplace.visualstudio.com/items?itemName=cmaranes.python-ply-preview)
![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/cmaranes.python-ply-preview)

🚀 Instantly visualize 3D point clouds from your Python debugger in VS Code\!
🚀 See your point clouds while you debug. Pause your Python program, right-click a variable, and it opens in a 3D viewer next to your code.

This extension enhances your debugging workflow by allowing you to preview point cloud variables as `.ply` files directly from the editor. It supports native **Open3D** point clouds, **NumPy** arrays, and **PyTorch** tensors.
Works with **NumPy** arrays, **PyTorch** tensors and **Open3D** point clouds. Only NumPy is needed in your environment, and no other extension.

**See it on the VS Marketplace:** [Python PLY Preview](https://marketplace.visualstudio.com/items?itemName=cmaranes.python-ply-preview)
![Viewing a point cloud from the Variables panel](images/viewer.gif)

## Features

* **3D viewer inside VS Code:** rotate, pan and zoom; color by RGB, by height or with a single color; Z-up or Y-up; axes, grid, point count and bounding box.
* **Preview from anywhere while debugging:** the Variables and Watch panels, the lightbulb on a variable, a selected expression such as `pcd.points` or `batch[0]`, or the Command Palette.
* **Any stack frame:** variables are read from the frame selected in the Call Stack.
* **Large clouds stay fast:** clouds over 1,000,000 points are subsampled before they leave Python (configurable).
* **Docker and remote debugging:** when the Python process can't share files with VS Code, the cloud comes through the debugger.
* **Open `.ply` files** in the same viewer.

-----

## How to Use

1. Set a breakpoint in your Python code.
2. Start your program in **Debug Mode** (F5).
3. When execution pauses, open a point cloud in any of these ways:
* Put the cursor on a variable (or **select an expression** such as `pcd.points` or `batch[0]`) and pick **View PLY** from the lightbulb / quick fix menu (`Ctrl+.`).
1. Start your program in **Debug Mode** (F5) and pause it at a breakpoint.
2. Open a point cloud in any of these ways:
* Right-click a variable in the **Variables** or **Watch** panel and pick **View as Point Cloud (PLY)**.
* Put the cursor on a variable, or **select an expression** such as `pcd.points` or `batch[0]`, and pick **View PLY** from the lightbulb / quick fix menu (`Ctrl+.`).
* Right-click in the editor, or run **Python PLY Preview: View as Point Cloud (PLY)** from the Command Palette.
4. The point cloud will be rendered in a new editor tab.
3. The point cloud opens in a viewer tab beside your code. Viewing the same variable again, for example after stepping, updates that tab.

The variable is evaluated in the stack frame selected in the **Call Stack** view, so you can preview variables from any frame or thread.

-----
### The viewer

## Demonstrations
* **Rotate** with the left mouse button, **pan** with the right one, **zoom** with the wheel. **Reset view** fits the cloud again.
* **Color** by the cloud's own **RGB** colors, by **height** along the up axis, or with a **solid** color.
* **Up** axis: **Z** (robotics, LiDAR) or **Y** (graphics). **Axes** shows where the origin is (X red, Y green, Z blue).
* The panel at the bottom shows the number of points and the bounding box.
* Your toolbar choices are remembered for the next viewer you open.

![PLY](images/ply.gif)
![NumPy](images/numpy.gif)
To open a `.ply` file in the viewer, right-click its tab and pick **Reopen Editor With...** > **Point Cloud Viewer**.

-----

## Quickstart: Example Code
## Try it

To test all features, you can use the following Python script. Place a breakpoint on the final `print` statement and inspect each of the defined variables.
Save this script, run it with the debugger (F5) and right-click the variables in the **Variables** panel when it stops.

```python
import open3d as o3d
import numpy as np
import torch

# 1. Create a basic NumPy array with XYZ points
points_np = np.random.rand(1000, 3)

# 2. Create a NumPy array with XYZ points and RGB colors
# Points are a spiral, colors transition from red to blue
z = np.linspace(0, 10, 1000)
x = np.sin(z) * 5
y = np.cos(z) * 5
points_spiral = np.vstack((x, y, z)).T
colors_gradient = np.zeros_like(points_spiral)
colors_gradient[:, 0] = np.linspace(0, 1, 1000) # Red channel
colors_gradient[:, 2] = np.linspace(1, 0, 1000) # Blue channel
points_and_colors_np = np.hstack((points_spiral, colors_gradient))

# 3. Create PyTorch Tensors from the NumPy arrays
points_torch = torch.from_numpy(points_np)
points_and_colors_torch = torch.from_numpy(points_and_colors_np)

# 4. Create a native Open3D PointCloud object
pcd_o3d = o3d.geometry.PointCloud()
pcd_o3d.points = o3d.utility.Vector3dVector(points_np)
pcd_o3d.paint_uniform_color([0.5, 0.5, 0.5]) # Gray color

# Set a breakpoint on the line below and hover over the variables to test
print("Breakpoint here: Hover over variables to preview them as PLY.")
```

-----
# XYZ: a noisy plane
plane = np.c_[np.random.rand(5000, 2) * 10, np.random.randn(5000) * 0.05]

## Requirements
# XYZ + RGB (floats in [0, 1]): a spiral going from red to blue
t = np.linspace(0, 10, 2000)
spiral = np.c_[np.sin(t) * 5, np.cos(t) * 5, t, np.linspace(0, 1, 2000), np.zeros(2000), np.linspace(1, 0, 2000)]

### 1\. Python Libraries
# XYZ + intensity, like a LiDAR scan: a ring of points around the origin
angles = np.random.rand(20000) * 2 * np.pi
distances = 10 + np.random.randn(20000)
lidar = np.c_[np.cos(angles) * distances, np.sin(angles) * distances, np.random.randn(20000) * 0.3, distances]

The extension only needs **NumPy** in your Python environment (`pip install numpy`). Open3D objects and PyTorch tensors are converted through NumPy, so `open3d` and `torch` are never imported by the extension. You only need them if your own code uses them.
# Two million points: subsampled to 1,000,000 (see pythonPlyPreview.maxPoints)
big = np.random.randn(2_000_000, 3)

### 2\. Supported Data Formats
# Also try a PyTorch tensor or an Open3D point cloud, if you have them installed:
# import torch; tensor = torch.from_numpy(spiral)
# import open3d as o3d; pcd = o3d.geometry.PointCloud(o3d.utility.Vector3dVector(plane))

breakpoint()
```

* **Open3D**: `o3d.geometry.PointCloud` and tensor-API `o3d.t.geometry.PointCloud` objects (with colors if present).
* **NumPy** arrays and **PyTorch** tensors (any device) with shape:
-----

## Supported Data

The extension only needs **NumPy** in your Python environment (`pip install numpy`). Open3D objects and PyTorch tensors are converted through NumPy, so `open3d` and `torch` are never imported by the extension.

* **Open3D**: `o3d.geometry.PointCloud` and tensor-API `o3d.t.geometry.PointCloud` objects, with colors if present.
* **NumPy** arrays and **PyTorch** tensors (on any device) with shape:
* `(n, 3)`: XYZ
* `(n, 4)`: XYZ + intensity (e.g. LiDAR), shown with a colormap
* `(n, 6)`: XYZ + RGB, as floats in `[0, 1]` or values in `[0, 255]`
* Channels-first `(3|4|6, n)` and batched `(1, n, c)` layouts are handled automatically.
* Points with `NaN`/`inf` coordinates are dropped.

### 3\. PLY Viewer Extension

This extension handles the *saving* of the point cloud. You need another extension to *view* the resulting `.ply` file. We recommend:

* [**3D Preview** by tatsy](https://marketplace.visualstudio.com/items?itemName=tatsy.vscode-3d-preview)

-----

## Known Issues
## Settings

* Files are written by the Python process to `.vscode/ply_preview` in your workspace, so the debuggee must be able to write to that path (attaching to a Docker container or remote process with path mappings is not supported yet).
| Setting | Default | Description |
|---|---|---|
| `pythonPlyPreview.maxPoints` | `1000000` | Larger clouds are randomly subsampled to this many points before export (the same points every time). `0` exports every point. |
| `pythonPlyPreview.viewer` | `builtin` | `default` opens the exported `.ply` file with your default `.ply` editor instead, for example [3D Preview](https://marketplace.visualstudio.com/items?itemName=tatsy.vscode-3d-preview). |

-----

## Release Notes

### 0.1.1

* Preview files left behind when VS Code was closed during a debug session are now removed after a day.

### 0.1.0

* The PLY file is now written only when you click **View PLY**, not every time the cursor moves. This makes the debugger much more responsive with large point clouds.
* Files are now saved as binary PLY, which is much faster and smaller than ASCII.
* New entry points: right-click in the **Variables** / **Watch** panels, the editor context menu, and the Command Palette.
* You can select an expression such as `pcd.points` or `batch[0]` to preview it.
* Uses the stack frame selected in the Call Stack instead of always the top frame of the first thread.
* Added support for `(n, 4)` XYZ + intensity, channels-first and `(1, n, c)` batched layouts, and `uint8` colors. Also added support for Open3D tensor-API point clouds.
* Dropped the `open3d` requirement: only `numpy` is needed. Non-finite points are filtered out.
* Requires VS Code 1.90 or newer.

### 0.0.7
## How it Works

* Replaced heavy Open3D dependency with pure NumPy and Python file I/O for array saving.
* Fixed a critical bug causing timeouts (`Cannot evaluate expression`) on macOS due to slow library loading.
* Improved robustness across different Python versions.
The Python process writes the point cloud as a binary PLY file to `.vscode/ply_preview` in your workspace, and the files are removed when the debug session ends. When the Python process can't write there (attaching to a Docker container or another machine), it writes to its own temp folder and the file is sent through the debugger instead.

If the viewer says WebGL is not available (for example in some virtual machines or remote desktops without GPU acceleration), set `pythonPlyPreview.viewer` to `default` and open the files with another editor.

### 0.0.6

* Modified README.md to include Visual Studio Marketplace badges.
* Removed the requirement to import `open3d` in the user script beforehand.
* Added automatic library check (Open3D, NumPy, PyTorch) and user notification if missing.
* Modified README.md to include Visual Studio Marketplace badges.

### 0.0.4

* Added support for PyTorch Tensors and NumPy arrays with color `(n, 6)`.
* Changed temporary file location to `.vscode/ply_preview`.
* Implemented automatic cleanup of temporary files after a debug session.
* Improved logic to avoid activating on variables in comments or strings.
-----

### 0.0.3
## Release Notes

* The `__pycache__` folder is now created if it doesn't exist.
### 0.2.0

### 0.0.2
* **Built-in 3D viewer.** Point clouds open in the extension's own viewer, so the 3D Preview extension is no longer required. It has orbit controls, point size, RGB / height / solid coloring, a Z-up or Y-up toggle, axes and grid, and shows the point count and bounding box. Viewing the same variable again updates its tab.
* **Large clouds are subsampled** to 1,000,000 points by default (`pythonPlyPreview.maxPoints`), so the debugger and the viewer stay responsive. The viewer tells you when a cloud was subsampled.
* **Docker and remote debugging.** If the Python process can't write to your workspace, the point cloud is sent through the debugger instead.
* `.ply` files can be opened in the viewer with **Reopen Editor With...**.
* To keep using 3D Preview or another `.ply` editor, set `pythonPlyPreview.viewer` to `default`.

* Fixed extension description and name.
* Added marketplace badges to the README.
### 0.1.1

### 0.0.1
* Preview files left behind when VS Code was closed during a debug session are now removed after a day.

* Initial release.
See the [changelog](CHANGELOG.md) for earlier versions.

-----

Expand Down
51 changes: 51 additions & 0 deletions esbuild.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Bundles the webview viewer (three.js included) into media/viewer.js.
// The extension itself is still compiled by tsc; run `node esbuild.mjs --watch` to rebuild on change.
import * as esbuild from 'esbuild';

const watch = process.argv.includes('--watch');

/** Prints errors in a format the problem matcher in .vscode/tasks.json understands. */
const problemMatcherPlugin = {
name: 'problem-matcher',
setup(build) {
build.onStart(() => {
if (watch) {
console.log('[watch] build started');
}
});
build.onEnd(result => {
for (const { text, location } of result.errors) {
console.error(`✘ [ERROR] ${text}`);
if (location) {
console.error(` ${location.file}:${location.line}:${location.column}:`);
}
}
if (watch) {
console.log('[watch] build finished');
}
});
},
};

const context = await esbuild.context({
entryPoints: ['src/webview/viewer.ts'],
outfile: 'media/viewer.js',
bundle: true,
format: 'iife',
platform: 'browser',
target: 'es2020',
minify: !watch,
sourcemap: watch ? 'inline' : false,
logLevel: 'silent',
plugins: [problemMatcherPlugin],
});

if (watch) {
await context.watch();
} else {
const result = await context.rebuild();
await context.dispose();
if (result.errors.length) {
process.exit(1);
}
}
Binary file removed images/example.mp4
Binary file not shown.
Binary file removed images/numpy.gif
Binary file not shown.
Binary file removed images/numpy.mp4
Binary file not shown.
Binary file removed images/ply.gif
Binary file not shown.
Binary file removed images/ply.mp4
Binary file not shown.
Binary file added images/viewer.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading