diff --git a/CHANGELOG.md b/CHANGELOG.md
index be48703..8043167 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,26 @@ All notable changes to the "vscode-3dpreview" extension will be documented in th
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
-## v0.2.2
+## [Unreleased]
+### Added
+- Added comprehensive coordinate system support (`coordinateSystem`) with presets for major 3D platforms:
+ - OpenGL (default), Blender, Unity, Unreal, Maya, 3ds Max, OpenCV, COLMAP, NeRFStudio
+- Added custom coordinate system configuration (`customCoordinateSystem`) for user-defined axis mappings
+- Added GUI controls for coordinate system presets and custom axis configuration
+- Added automatic coordinate transformation with proper normal vector handling
+- Added hot reload support for coordinate system changes
+
+### Changed
+- Replaced simple coordinate convention with full coordinate system transformation
+- Default coordinate system is OpenGL (maintains backward compatibility)
+- Enhanced GUI with coordinate system folder and custom axes subfolder
+- Improved coordinate system handling with matrix transformations
+
+### Fixed
+- Improved coordinate system handling for 3D models with proper transformation matrices
+- Fixed normal vector transformations for coordinate system changes
+
+## [0.2.4] - Previous Release
- Default point size support ([#7](https://github.com/tatsy/vscode-3d-preview/issues/7))
- Revise JS codes to follow ES6.
diff --git a/README.md b/README.md
index ac9f264..b756410 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,50 @@ This extension supports 3D formats equally as [Open3D](http://www.open3d.org/doc

+## Configuration
+
+The extension provides several configuration options that can be set in VS Code settings:
+
+### Coordinate System
+
+- **Setting**: `3dpreview.coordinateSystem`
+- **Default**: `"opengl"`
+- **Options**: Platform presets and custom configuration
+
+This setting allows you to switch between different coordinate systems used by various 3D platforms:
+
+#### Platform Presets
+
+- **OpenGL** (default): +X right, +Y up, +Z forward
+- **Blender**: +X right, +Z up, -Y forward
+- **Unity**: +X right, +Y up, +Z forward (left-handed)
+- **Unreal**: +Y right, +Z up, +X forward
+- **Maya**: +X right, +Y up, +Z forward
+- **3ds Max**: +X right, +Z up, -Y forward
+- **OpenCV**: +X right, -Y up, +Z forward
+- **COLMAP**: +X right, -Y up, +Z forward
+- **NeRFStudio**: +X right, +Y up, +Z forward
+- **Custom**: User-defined axis configuration
+
+#### Custom Coordinate System
+
+When set to "custom", you can configure individual axes:
+
+- **Setting**: `3dpreview.customCoordinateSystem`
+- **Properties**:
+ - `rightAxis`: Which axis points right (`+x`, `-x`, `+y`, `-y`, `+z`, `-z`)
+ - `upAxis`: Which axis points up
+ - `forwardAxis`: Which axis points forward
+
+#### Usage
+
+You can change the coordinate system:
+1. Through VS Code settings (search for "3dpreview.coordinateSystem")
+2. Using the "Coordinate System" folder in the 3D viewer's control panel
+3. For custom systems, adjust individual axes in the "Custom Axes" subfolder
+
+This feature is particularly useful when working with 3D models from different software packages that use different coordinate system conventions.
+
## FAQ
- Q. When I drag and drop a mesh file, a blank display is shown.
diff --git a/media/viewer.js b/media/viewer.js
index e09849d..60d3574 100644
--- a/media/viewer.js
+++ b/media/viewer.js
@@ -31,6 +31,15 @@ class Viewer {
unit: 1,
};
+ // Initialize customCoordinateSystem if not provided
+ if (!this.params.customCoordinateSystem) {
+ this.params.customCoordinateSystem = {
+ rightAxis: '+x',
+ upAxis: '+y',
+ forwardAxis: '+z',
+ };
+ }
+
// Three JS instances
this.renderer = new THREE.WebGLRenderer({
alpha: true,
@@ -163,6 +172,167 @@ class Viewer {
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
+ getCoordinateSystemPresets() {
+ return {
+ opengl: { rightAxis: '+x', upAxis: '+y', forwardAxis: '+z' },
+ blender: { rightAxis: '+x', upAxis: '+z', forwardAxis: '-y' },
+ unity: { rightAxis: '+x', upAxis: '+y', forwardAxis: '+z' }, // Left-handed in Unity
+ unreal: { rightAxis: '+y', upAxis: '+z', forwardAxis: '+x' },
+ maya: { rightAxis: '+x', upAxis: '+y', forwardAxis: '+z' },
+ '3dsmax': { rightAxis: '+x', upAxis: '+z', forwardAxis: '-y' },
+ opencv: { rightAxis: '+x', upAxis: '-y', forwardAxis: '+z' },
+ colmap: { rightAxis: '+x', upAxis: '-y', forwardAxis: '+z' },
+ nerfstudio: { rightAxis: '+x', upAxis: '+y', forwardAxis: '+z' },
+ custom: this.params.customCoordinateSystem,
+ };
+ }
+
+ getTransformationMatrix(coordinateSystem) {
+ const presets = this.getCoordinateSystemPresets();
+ const system = presets[coordinateSystem] || presets.opengl;
+
+ // Create transformation matrix to convert from input coordinate system to OpenGL
+ const matrix = new THREE.Matrix4();
+
+ // Parse axis definitions
+ const parseAxis = (axis) => {
+ const sign = axis[0] === '+' ? 1 : -1;
+ const component = axis[1];
+ return { sign, component };
+ };
+
+ const right = parseAxis(system.rightAxis);
+ const up = parseAxis(system.upAxis);
+ const forward = parseAxis(system.forwardAxis);
+
+ // Build transformation matrix
+ // Target: OpenGL coordinate system (+X right, +Y up, +Z forward)
+ const elements = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
+
+ // Map input axes to OpenGL axes
+ const axisMap = { x: 0, y: 1, z: 2 };
+
+ // Right axis -> X (column 0)
+ elements[axisMap[right.component] * 4 + 0] = right.sign;
+
+ // Up axis -> Y (column 1)
+ elements[axisMap[up.component] * 4 + 1] = up.sign;
+
+ // Forward axis -> Z (column 2)
+ elements[axisMap[forward.component] * 4 + 2] = forward.sign;
+
+ matrix.set(
+ elements[0],
+ elements[1],
+ elements[2],
+ elements[3],
+ elements[4],
+ elements[5],
+ elements[6],
+ elements[7],
+ elements[8],
+ elements[9],
+ elements[10],
+ elements[11],
+ elements[12],
+ elements[13],
+ elements[14],
+ elements[15]
+ );
+
+ return matrix;
+ }
+
+ applyCoordinateConvention(geometry) {
+ const coordinateSystem = this.params.coordinateSystem || 'opengl';
+
+ if (coordinateSystem === 'opengl') {
+ return; // No transformation needed for OpenGL (Three.js default)
+ }
+
+ const transformMatrix = this.getTransformationMatrix(coordinateSystem);
+
+ // Apply transformation to positions
+ const positions = geometry.getAttribute('position');
+ if (positions) {
+ const positionArray = positions.array;
+ const vertex = new THREE.Vector3();
+
+ for (let i = 0; i < positionArray.length; i += 3) {
+ vertex.set(positionArray[i], positionArray[i + 1], positionArray[i + 2]);
+ vertex.applyMatrix4(transformMatrix);
+ positionArray[i] = vertex.x;
+ positionArray[i + 1] = vertex.y;
+ positionArray[i + 2] = vertex.z;
+ }
+ positions.needsUpdate = true;
+ }
+
+ // Apply transformation to normals
+ const normals = geometry.getAttribute('normal');
+ if (normals) {
+ const normalArray = normals.array;
+ const normal = new THREE.Vector3();
+ const normalMatrix = new THREE.Matrix3().getNormalMatrix(transformMatrix);
+
+ for (let i = 0; i < normalArray.length; i += 3) {
+ normal.set(normalArray[i], normalArray[i + 1], normalArray[i + 2]);
+ normal.applyMatrix3(normalMatrix);
+ normalArray[i] = normal.x;
+ normalArray[i + 1] = normal.y;
+ normalArray[i + 2] = normal.z;
+ }
+ normals.needsUpdate = true;
+ }
+ }
+
+ showCustomCoordinateControls() {
+ if (!this.customCoordControls.folder) {
+ this.customCoordControls.folder = this.gui.addFolder('Custom Axes');
+ this.customCoordControls.folder.open();
+
+ const axisOptions = ['+x', '-x', '+y', '-y', '+z', '-z'];
+
+ this.customCoordControls.rightAxis = this.customCoordControls.folder
+ .add(this.params.customCoordinateSystem, 'rightAxis', axisOptions)
+ .name('Right Axis')
+ .onChange(() => this.reloadMesh());
+
+ this.customCoordControls.upAxis = this.customCoordControls.folder
+ .add(this.params.customCoordinateSystem, 'upAxis', axisOptions)
+ .name('Up Axis')
+ .onChange(() => this.reloadMesh());
+
+ this.customCoordControls.forwardAxis = this.customCoordControls.folder
+ .add(this.params.customCoordinateSystem, 'forwardAxis', axisOptions)
+ .name('Forward Axis')
+ .onChange(() => this.reloadMesh());
+ }
+ }
+
+ hideCustomCoordinateControls() {
+ if (this.customCoordControls.folder) {
+ this.customCoordControls.folder.destroy();
+ this.customCoordControls = {};
+ }
+ }
+
+ reloadMesh() {
+ // Clear existing mesh objects
+ if (this.points) {
+ this.scene.remove(this.points);
+ }
+ if (this.mesh) {
+ this.scene.remove(this.mesh);
+ }
+ if (this.wireframe) {
+ this.scene.remove(this.wireframe);
+ }
+
+ // Reload with new coordinate convention
+ this.setMesh(this.params.fileToLoad);
+ }
+
setMesh(fileToLoad) {
const self = this;
const base = utils.basename(fileToLoad);
@@ -198,6 +368,9 @@ class Viewer {
geometry.setIndex(indices);
}
+ // Apply coordinate convention transform
+ self.applyCoordinateConvention(geometry);
+
// mesh support
const meshSupport = geometry.index !== null;
self.params.showMesh = meshSupport;
@@ -326,6 +499,40 @@ class Viewer {
.max(1)
.name('Fog')
.onChange(() => this.updateRender());
+ // Coordinate System controls
+ let coordFolder = this.gui.addFolder('Coordinate System');
+ coordFolder.open();
+
+ const coordinateSystemOptions = [
+ 'opengl',
+ 'blender',
+ 'unity',
+ 'unreal',
+ 'maya',
+ '3dsmax',
+ 'opencv',
+ 'colmap',
+ 'nerfstudio',
+ 'custom',
+ ];
+
+ coordFolder
+ .add(this.params, 'coordinateSystem', coordinateSystemOptions)
+ .name('Preset')
+ .onChange(() => {
+ if (this.params.coordinateSystem === 'custom') {
+ this.showCustomCoordinateControls();
+ } else {
+ this.hideCustomCoordinateControls();
+ }
+ this.reloadMesh();
+ });
+
+ // Custom coordinate system controls (initially hidden)
+ this.customCoordControls = {};
+ if (this.params.coordinateSystem === 'custom') {
+ this.showCustomCoordinateControls();
+ }
let folder = this.gui.addFolder('Grid Helper');
folder.open();
@@ -353,3 +560,13 @@ class Viewer {
}
const viewer = new Viewer();
+
+// Handle messages from VS Code
+window.addEventListener('message', (event) => {
+ const message = event.data;
+ switch (message.type) {
+ case 'modelRefresh':
+ viewer.reloadMesh();
+ break;
+ }
+});
diff --git a/package-lock.json b/package-lock.json
index 27a0b62..0eb74e7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,15 @@
{
"name": "vscode-3d-preview",
- "version": "0.2.2",
+ "version": "0.2.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vscode-3d-preview",
- "version": "0.2.2",
+ "version": "0.2.4",
+ "dependencies": {
+ "three": "^0.178.0"
+ },
"devDependencies": {
"@types/glob": "^8.1.0",
"@types/mocha": "^10.0.10",
@@ -4038,6 +4041,12 @@
"node": ">=6"
}
},
+ "node_modules/three": {
+ "version": "0.178.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.178.0.tgz",
+ "integrity": "sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==",
+ "license": "MIT"
+ },
"node_modules/tmp": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
diff --git a/package.json b/package.json
index f67c9cd..a9de7e4 100644
--- a/package.json
+++ b/package.json
@@ -129,6 +129,75 @@
"type": "number",
"default": 0,
"description": "Fog density."
+ },
+ "3dpreview.coordinateSystem": {
+ "type": "string",
+ "default": "opengl",
+ "enum": [
+ "opengl",
+ "blender",
+ "unity",
+ "unreal",
+ "maya",
+ "3dsmax",
+ "opencv",
+ "colmap",
+ "nerfstudio",
+ "custom"
+ ],
+ "description": "Coordinate system preset for different platforms."
+ },
+ "3dpreview.customCoordinateSystem": {
+ "type": "object",
+ "default": {
+ "rightAxis": "+x",
+ "upAxis": "+y",
+ "forwardAxis": "+z"
+ },
+ "properties": {
+ "rightAxis": {
+ "type": "string",
+ "enum": [
+ "+x",
+ "-x",
+ "+y",
+ "-y",
+ "+z",
+ "-z"
+ ],
+ "description": "Axis pointing to the right"
+ },
+ "upAxis": {
+ "type": "string",
+ "enum": [
+ "+x",
+ "-x",
+ "+y",
+ "-y",
+ "+z",
+ "-z"
+ ],
+ "description": "Axis pointing up"
+ },
+ "forwardAxis": {
+ "type": "string",
+ "enum": [
+ "+x",
+ "-x",
+ "+y",
+ "-y",
+ "+z",
+ "-z"
+ ],
+ "description": "Axis pointing forward"
+ }
+ },
+ "description": "Custom coordinate system configuration (used when coordinateSystem is set to 'custom')."
+ },
+ "3dpreview.hotReload": {
+ "type": "boolean",
+ "default": true,
+ "description": "Enable hot reload when 3D files are modified."
}
}
}
@@ -155,5 +224,8 @@
"typescript": "^5.7.3",
"vsce": "^2.15.0",
"vscode-test": "^1.6.1"
+ },
+ "dependencies": {
+ "three": "^0.178.0"
}
}
diff --git a/src/meshProvider.ts b/src/meshProvider.ts
index c03dfbe..ee23293 100644
--- a/src/meshProvider.ts
+++ b/src/meshProvider.ts
@@ -124,6 +124,12 @@ export class MeshViewProvider
pointColor: config.get("pointColor", "#cc0000"),
wireframeColor: config.get("wireframeColor", "#0000ff"),
fogDensity: config.get("fogDensity", 0.01),
+ coordinateSystem: config.get("coordinateSystem", "opengl"),
+ customCoordinateSystem: config.get("customCoordinateSystem", {
+ rightAxis: "+x",
+ upAxis: "+y",
+ forwardAxis: "+z"
+ }),
};
return ` {
+ test("Should have coordinate system setting", () => {
+ const config = vscode.workspace.getConfiguration("3dpreview");
+ const coordinateSystem = config.get("coordinateSystem", "opengl");
+
+ const validSystems = [
+ "opengl", "blender", "unity", "unreal", "maya", "3dsmax",
+ "opencv", "colmap", "nerfstudio", "custom"
+ ];
+
+ assert.ok(validSystems.includes(coordinateSystem),
+ "Coordinate system should be one of the valid presets");
+ });
+
+ test("Should default to OpenGL coordinate system", () => {
+ const config = vscode.workspace.getConfiguration("3dpreview");
+ const coordinateSystem = config.get("coordinateSystem", "opengl");
+
+ assert.strictEqual(coordinateSystem, "opengl",
+ "Default coordinate system should be OpenGL");
+ });
+
+ test("Should allow setting different coordinate systems", async () => {
+ const config = vscode.workspace.getConfiguration("3dpreview");
+
+ // Test setting to blender
+ await config.update("coordinateSystem", "blender", vscode.ConfigurationTarget.Global);
+
+ const coordinateSystem = config.get("coordinateSystem");
+ assert.strictEqual(coordinateSystem, "blender",
+ "Should be able to set coordinate system to blender");
+
+ // Reset to opengl
+ await config.update("coordinateSystem", "opengl", vscode.ConfigurationTarget.Global);
+ });
+
+ test("Should have custom coordinate system configuration", () => {
+ const config = vscode.workspace.getConfiguration("3dpreview");
+ const customCoordinateSystem = config.get("customCoordinateSystem", {
+ rightAxis: "+x",
+ upAxis: "+y",
+ forwardAxis: "+z"
+ });
+
+ assert.ok(customCoordinateSystem.rightAxis, "Custom coordinate system should have rightAxis");
+ assert.ok(customCoordinateSystem.upAxis, "Custom coordinate system should have upAxis");
+ assert.ok(customCoordinateSystem.forwardAxis, "Custom coordinate system should have forwardAxis");
+ });
+
+ test("Should open 3D file with coordinate system setting", async () => {
+ const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
+ if (!workspaceFolder) {
+ // Skip test if no workspace folder available
+ return;
+ }
+
+ const testFilePath = path.join(workspaceFolder.uri.fsPath, "data", "bunny.obj");
+ const testFileUri = vscode.Uri.file(testFilePath);
+
+ try {
+ // Open the 3D file
+ const document = await vscode.workspace.openTextDocument(testFileUri);
+ assert.ok(document, "Should be able to open 3D file");
+
+ // Check that coordinate system setting is available
+ const config = vscode.workspace.getConfiguration("3dpreview");
+ const coordinateSystem = config.get("coordinateSystem");
+ assert.ok(coordinateSystem, "Coordinate system setting should be available");
+
+ } catch (error) {
+ console.log("Note: This test requires the 3D file to be available in the workspace");
+ // This is expected if the test file doesn't exist
+ }
+ });
+});
\ No newline at end of file
diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts
new file mode 100644
index 0000000..fdb1df9
--- /dev/null
+++ b/src/test/suite/index.ts
@@ -0,0 +1,34 @@
+import * as path from 'path';
+import * as Mocha from 'mocha';
+import { glob } from 'glob';
+
+export function run(): Promise {
+ // Create the mocha test
+ const mocha = new Mocha({
+ ui: 'tdd',
+ color: true
+ });
+
+ const testsRoot = path.resolve(__dirname, '..');
+
+ return new Promise(async (c, e) => {
+ try {
+ const files = await glob('**/**.test.js', { cwd: testsRoot });
+
+ // Add files to the test suite
+ files.forEach((f: string) => mocha.addFile(path.resolve(testsRoot, f)));
+
+ // Run the mocha test
+ mocha.run(failures => {
+ if (failures > 0) {
+ e(new Error(`${failures} tests failed.`));
+ } else {
+ c();
+ }
+ });
+ } catch (err) {
+ console.error(err);
+ e(err);
+ }
+ });
+}
\ No newline at end of file