From c3da9b31f43204abfc11317dd3d72a466ee2921a Mon Sep 17 00:00:00 2001 From: Jameson Crate Date: Sun, 13 Jul 2025 13:33:10 -0700 Subject: [PATCH] add changing coordinate system between present options --- CHANGELOG.md | 21 +- README.md | 44 ++++ media/viewer.js | 217 ++++++++++++++++++++ package-lock.json | 13 +- package.json | 72 +++++++ src/meshProvider.ts | 6 + src/test/suite/coordinateConvention.test.ts | 79 +++++++ src/test/suite/index.ts | 34 +++ 8 files changed, 483 insertions(+), 3 deletions(-) create mode 100644 src/test/suite/coordinateConvention.test.ts create mode 100644 src/test/suite/index.ts 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 ![color_points](images/color_points.jpg) +## 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