save coords in json

This commit is contained in:
PozzettiAndrea
2026-05-13 00:00:18 +02:00
parent 92227bd0a0
commit 5e6b102b2d
2 changed files with 99 additions and 31 deletions
+1 -22
View File
@@ -1,37 +1,16 @@
[project]
name = "ComfyUI-TurntableGSViewer"
description = "Tiny Gaussian Splat turntable viewer node for ComfyUI."
version = "0.1.0"
version = "0.0.1"
license = {file = "LICENSE"}
# classifiers = [
# # For OS-independent nodes (works on all operating systems)
# "Operating System :: OS Independent",
#
# # OR for OS-specific nodes, specify the supported systems:
# "Operating System :: Microsoft :: Windows", # Windows specific
# "Operating System :: POSIX :: Linux", # Linux specific
# "Operating System :: MacOS", # macOS specific
#
# # GPU Accelerator support. Pick the ones that are supported by your extension.
# "Environment :: GPU :: NVIDIA CUDA", # NVIDIA CUDA support
# "Environment :: GPU :: AMD ROCm", # AMD ROCm support
# "Environment :: GPU :: Intel Arc", # Intel Arc support
# "Environment :: NPU :: Huawei Ascend", # Huawei Ascend support
# "Environment :: GPU :: Apple Metal", # Apple Metal support
# ]
dependencies = ["numpy>=1.23", "plyfile>=1.0.3"]
[project.urls]
Repository = "https://github.com/PozzettiAndrea/ComfyUI-TurntableGSViewer"
# Used by Comfy Registry https://registry.comfy.org
Documentation = "https://github.com/PozzettiAndrea/ComfyUI-TurntableGSViewer"
"Bug Tracker" = "https://github.com/PozzettiAndrea/ComfyUI-TurntableGSViewer/issues"
[tool.comfy]
PublisherId = "pozzettiandrea"
DisplayName = "ComfyUI-TurntableGSViewer"
Icon = ""
includes = []
# "requires-comfyui" = ">=1.0.0" # ComfyUI version compatibility
+98 -9
View File
@@ -239,6 +239,9 @@
canvas.style.outline = "none";
const focusCanvas = () => canvas.focus({ preventScroll: true });
canvas.addEventListener('pointerdown', focusCanvas);
// Persist camera state to workflow on every drag-release and wheel-zoom.
canvas.addEventListener('pointerup', () => emitCameraState());
canvas.addEventListener('wheel', () => emitCameraState(), { passive: true });
// gsplat.js components
let scene = null;
@@ -304,6 +307,65 @@
// Track the current vertical FOV (deg) and target so we can re-frame
// when the user changes settings.
let currentFovDeg = 50;
// Cached current orbit target, kept in sync with controls.setCameraTarget calls.
let currentTarget = { x: 0, y: 0, z: 0 };
// Pending restore from parent; consumed once after the next MESH_LOADED.
let pendingRestore = null;
// Push the live camera+settings state up to the parent so it can
// persist it in the workflow JSON via the DOM widget's getValue().
let _lastEmitted = "";
function emitCameraState() {
if (!camera) return;
const state = {
v: 1,
pos: [camera.position.x, camera.position.y, camera.position.z],
target: [currentTarget.x, currentTarget.y, currentTarget.z],
fov: currentFovDeg,
scale: currentScale,
opacity: currentOpacity,
};
const s = JSON.stringify(state);
if (s === _lastEmitted) return;
_lastEmitted = s;
try { window.parent.postMessage({ type: "CAMERA_STATE", state }, "*"); }
catch (e) { /* parent may be cross-origin or gone */ }
}
// Apply a state previously snapshotted via emitCameraState.
function applyCameraState(state) {
if (!state || !camera || !controls) return;
if (Array.isArray(state.pos)) {
camera.position.x = state.pos[0];
camera.position.y = state.pos[1];
camera.position.z = state.pos[2];
}
if (Array.isArray(state.target)) {
currentTarget = { x: state.target[0], y: state.target[1], z: state.target[2] };
controls.setCameraTarget(new SPLAT.Vector3(currentTarget.x, currentTarget.y, currentTarget.z));
}
if (typeof state.fov === "number") {
currentFovDeg = state.fov;
setFovInputs(state.fov);
const fovRad = state.fov * Math.PI / 180;
const canvasHeight = canvas.clientHeight || 512;
const focal = canvasHeight / (2 * Math.tan(fovRad / 2));
camera.data.fx = focal;
camera.data.fy = focal;
}
if (typeof state.scale === "number") {
currentScale = state.scale;
setScaleInputs(state.scale);
if (originalScales) scheduleScaleUpdate(state.scale);
}
if (typeof state.opacity === "number") {
currentOpacity = state.opacity;
setOpacityInputs(state.opacity);
updateOpacity(state.opacity);
}
controls.update();
console.log("[GaussianViewer] Restored camera state from workflow");
}
// Frame the camera on the splat's bounding box. Always centers on
// bounds.center(). Camera sits behind the center along -Z at a
@@ -333,6 +395,7 @@
camera.position.y = camPos.y;
camera.position.z = camPos.z;
controls.setCameraTarget(target);
currentTarget = { x: target.x, y: target.y, z: target.z };
initialCameraData = { position: camPos, target: target, fx: focal, fy: focal };
@@ -340,6 +403,7 @@
center.x.toFixed(2), center.y.toFixed(2), center.z.toFixed(2),
'maxExtent=', maxExtent.toFixed(2), 'radius=', radius.toFixed(2),
'fov=', fovDeg);
emitCameraState();
}
// Snap the camera onto one of the cardinal axes through the bbox center.
@@ -362,7 +426,9 @@
const target = new SPLAT.Vector3(center.x, center.y, center.z);
controls.setCameraTarget(target);
currentTarget = { x: target.x, y: target.y, z: target.z };
console.log('[GaussianViewer] view along', (sign > 0 ? '+' : '-') + axis.toUpperCase());
emitCameraState();
}
// Compatibility shim: kept so any caller passing extrinsics/intrinsics
@@ -469,6 +535,13 @@
infoPanel.classList.remove('hidden');
infoContent.innerHTML = `<span style="color:#6cc;">Gaussian Splat Loaded</span><br><span style="color:#888;">${filename}</span>`;
// Apply any pending camera-state restore now that the splat is loaded.
// Falls AFTER frameOnBounds so the user's saved view overrides the default framing.
if (pendingRestore) {
applyCameraState(pendingRestore);
pendingRestore = null;
}
// Notify parent
window.parent.postMessage({
type: 'MESH_LOADED',
@@ -511,6 +584,11 @@
// Set the target - this recalculates orbit state from current camera.position
if (initialCameraData.target) {
controls.setCameraTarget(initialCameraData.target);
currentTarget = {
x: initialCameraData.target.x,
y: initialCameraData.target.y,
z: initialCameraData.target.z,
};
}
console.log('[GaussianViewer] Reset camera to position:', pos.x, pos.y, pos.z);
@@ -523,6 +601,7 @@
console.log('[GaussianViewer] Reset camera to default position (0, 0, 5)');
}
controls.update();
emitCameraState();
}
}
@@ -644,26 +723,29 @@
if (currentSplat && currentSplat.data && currentSplat.data.scales && originalScales) {
scheduleScaleUpdate(newScale);
}
emitCameraState();
}
function updateOpacity(newOpacity) {
newOpacity = Math.max(0, Math.min(1, parseFloat(newOpacity) || 1.0));
currentOpacity = newOpacity;
setOpacityInputs(newOpacity);
if (!(currentSplat && currentSplat.data && currentSplat.data.colors && originalOpacities)) return;
const colors = currentSplat.data.colors;
for (let i = 0; i < originalOpacities.length; i++) {
colors[i * opacityStride + 3] = Math.round(originalOpacities[i] * newOpacity);
if (currentSplat && currentSplat.data && currentSplat.data.colors && originalOpacities) {
const colors = currentSplat.data.colors;
for (let i = 0; i < originalOpacities.length; i++) {
colors[i * opacityStride + 3] = Math.round(originalOpacities[i] * newOpacity);
}
currentSplat.data.changed = true;
currentSplat.data.detached = false;
}
currentSplat.data.changed = true;
currentSplat.data.detached = false;
emitCameraState();
}
function updateFov(newFov) {
newFov = Math.max(5, Math.min(170, parseFloat(newFov) || 50));
setFovInputs(newFov);
if (currentSplat) frameOnBounds(currentSplat, newFov);
else currentFovDeg = newFov;
if (currentSplat) frameOnBounds(currentSplat, newFov); // emits state itself
else { currentFovDeg = newFov; emitCameraState(); }
}
// FOV
@@ -692,11 +774,18 @@
// Listen for messages from parent
window.addEventListener('message', (event) => {
const { type, data, filename, extrinsics, intrinsics } = event.data;
const msg = event.data || {};
const { type, data, filename, extrinsics, intrinsics } = msg;
if (type === 'LOAD_MESH_DATA' && data) {
console.log('[GaussianViewer] Received LOAD_MESH_DATA, size:', data.byteLength);
loadPLYFromData(data, filename || 'gaussian.ply', extrinsics, intrinsics);
} else if (type === 'RESTORE_CAMERA_STATE' && msg.state) {
// Parent has a saved state for this node. Buffer it: if the
// splat is already loaded, apply now; otherwise the loader
// will pick it up after frameOnBounds.
if (currentSplat) applyCameraState(msg.state);
else pendingRestore = msg.state;
}
});