Move web/ to javascript/ and declare it via [tool.comfy] web. Rename the seven third-party bundles (playcanvas, spark, three.js, gsplat, OrbitControls, TrackballControls) to .mjs. Every reference to them is inside the HTML viewers, but ComfyUI auto-imports **/*.js under the web dir into the shared browser realm, so ~8MB of vendor code was being evaluated in the main page on every startup -- and with it 36 collision errors: document.body injection, document-level listeners, unguarded message listeners and self.onmessage writes. The importmap values, script srcs and three.module's internal ./three.core ref are rewritten to match. DisplayName becomes "GaussianPack": the namespace enforced on registerExtension names is derived from it, and "ComfyUI-GaussianPack" made that prefix invalid. The analysis extension is renamed to match. js/gaussian_preview.js hooked "PlyPreviewLoadGaussianPLYEnhance", a node id that appears nowhere in this repo, so the PLY dropdown auto-refresh had never run; it now targets LoadPLY and LoadPLYOutput, the two nodes that actually carry the ply_file widget. Guard its message listener on event.source. Asset seeding resolves the input directory through folder_paths.get_input_directory(), so it follows ComfyUI Desktop and --input-directory instead of writing into the code tree. comfy-test.toml moves to the current schema: [test.workflows] cuda (there is no "gpu" key -- the accelerator is named by backend, and the old spelling parsed to an empty list, so CUDA runners silently ran the full unfiltered set), an explicit [test.platforms] platforms allowlist (per-platform booleans are no longer supported), and the javascript level.
185 lines
7.5 KiB
JavaScript
185 lines
7.5 KiB
JavaScript
import { degreeForDim } from './constant.js';
|
|
/* ────────────────────────────── constants ─────────────────────────────── */
|
|
const END_MARKER = /\r?\nend_header\r?\n/; // handles \n and \r\n
|
|
/* ────────────────────────────── helpers ───────────────────────────────── */
|
|
function getFieldIndex(map, name) {
|
|
const idx = map.get(name);
|
|
if (idx === undefined) {
|
|
throw new Error(`[PLY] Missing required field "${name}"`);
|
|
}
|
|
return idx;
|
|
}
|
|
function byteSizeForType(type) {
|
|
switch (type) {
|
|
case 'float':
|
|
case 'int':
|
|
case 'uint':
|
|
case 'uint32':
|
|
case 'int32': return 4;
|
|
case 'double': return 8;
|
|
case 'uchar':
|
|
case 'char':
|
|
case 'uint8':
|
|
case 'int8': return 1;
|
|
case 'ushort':
|
|
case 'short':
|
|
case 'uint16':
|
|
case 'int16': return 2;
|
|
default:
|
|
throw new Error(`[PLY] Unsupported property type "${type}"`);
|
|
}
|
|
}
|
|
/* ────────────────────────────── parser ────────────────────────────────── */
|
|
export async function loadPly(readable) {
|
|
/* ---------- 1. Stream-safe header read -------------------------------- */
|
|
const reader = readable.getReader();
|
|
const decoder = new TextDecoder();
|
|
const headerChunks = [];
|
|
let headerText = '';
|
|
let binaryStartByteOffset = 0; // where vertex blob begins (byte index)
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done)
|
|
throw new Error('[PLY] Unexpected EOF before end_header');
|
|
headerChunks.push(value);
|
|
headerText += decoder.decode(value, { stream: true });
|
|
const m = headerText.match(END_MARKER);
|
|
if (!m)
|
|
continue; // need more data
|
|
const headerChars = headerText.indexOf(m[0]) + m[0].length;
|
|
binaryStartByteOffset = new TextEncoder().encode(headerText.slice(0, headerChars)).length;
|
|
// Trim sentinel from headerText (makes later `.split` cleaner)
|
|
headerText = headerText.slice(0, headerChars - m[0].length);
|
|
break;
|
|
}
|
|
/* ---------- 2. Stitch chunks & grab first payload bytes --------------- */
|
|
const headerTotal = headerChunks.reduce((s, c) => s + c.length, 0);
|
|
const stitched = new Uint8Array(headerTotal);
|
|
let off = 0;
|
|
for (const c of headerChunks) {
|
|
stitched.set(c, off);
|
|
off += c.length;
|
|
}
|
|
const initialBinary = stitched.subarray(binaryStartByteOffset);
|
|
/* ---------- 3. Robust header parsing ---------------------------------- */
|
|
const lines = headerText.split(/\r?\n/).filter(Boolean);
|
|
if (lines[0] !== 'ply') {
|
|
throw new Error('[PLY] Not a PLY file');
|
|
}
|
|
let formatOK = false;
|
|
let numPoints = 0;
|
|
const propDefs = [];
|
|
let strideBytes = 0;
|
|
let parsingVertexProps = false;
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (line.startsWith('format binary_little_endian')) {
|
|
formatOK = true;
|
|
continue;
|
|
}
|
|
if (line.startsWith('comment') || line.startsWith('obj_info'))
|
|
continue;
|
|
if (line.startsWith('element')) {
|
|
const [, elem, count] = line.split(/\s+/);
|
|
parsingVertexProps = (elem === 'vertex');
|
|
if (elem === 'vertex')
|
|
numPoints = Number(count);
|
|
continue;
|
|
}
|
|
if (line.startsWith('property') && parsingVertexProps) {
|
|
const [, type, name] = line.split(/\s+/);
|
|
const size = byteSizeForType(type);
|
|
propDefs.push({ name, type, offsetBytes: strideBytes });
|
|
strideBytes += size;
|
|
continue;
|
|
}
|
|
}
|
|
if (!formatOK)
|
|
throw new Error('[PLY] Only binary_little_endian supported');
|
|
if (!numPoints)
|
|
throw new Error('[PLY] Vertex count missing or zero');
|
|
/* ---------- 4. Fast lookup tables ------------------------------------- */
|
|
const fieldOffsetMap = new Map();
|
|
propDefs.forEach(({ name, offsetBytes }) => fieldOffsetMap.set(name, offsetBytes / 4) // convert to float index
|
|
);
|
|
const posIdx = ['x', 'y', 'z'].map(n => getFieldIndex(fieldOffsetMap, n));
|
|
const scaleIdx = ['scale_0', 'scale_1', 'scale_2'].map(n => getFieldIndex(fieldOffsetMap, n));
|
|
const rotIdx = ['rot_1', 'rot_2', 'rot_3', 'rot_0'].map(n => getFieldIndex(fieldOffsetMap, n));
|
|
const alphaIdx = [getFieldIndex(fieldOffsetMap, 'opacity')];
|
|
const colorIdx = ['f_dc_0', 'f_dc_1', 'f_dc_2'].map(n => getFieldIndex(fieldOffsetMap, n));
|
|
const shIdx = [];
|
|
for (let i = 0;; i++) {
|
|
const idx = fieldOffsetMap.get(`f_rest_${i}`);
|
|
if (idx === undefined)
|
|
break;
|
|
shIdx.push(idx);
|
|
}
|
|
const shDim = Math.floor(shIdx.length / 3);
|
|
const strideFloats = strideBytes / 4;
|
|
/* ---------- 5. Stream binary payload directly into pre-alloc buffer --- */
|
|
const totalBinaryBytes = numPoints * strideBytes;
|
|
const buffer = new ArrayBuffer(totalBinaryBytes);
|
|
const dest = new Uint8Array(buffer);
|
|
// paste the bytes we already have
|
|
dest.set(initialBinary, 0);
|
|
let cursor = initialBinary.length;
|
|
while (cursor < totalBinaryBytes) {
|
|
const { value, done } = await reader.read();
|
|
if (done)
|
|
throw new Error('[PLY] Truncated binary section');
|
|
dest.set(value, cursor);
|
|
cursor += value.length;
|
|
}
|
|
reader.releaseLock();
|
|
/* ---------- 6. Decode vertices via Float32 view ----------------------- */
|
|
const f32 = new Float32Array(buffer);
|
|
const result = {
|
|
numPoints,
|
|
shDegree: degreeForDim(shDim),
|
|
antialiased: false,
|
|
positions: new Float32Array(numPoints * 3),
|
|
scales: new Float32Array(numPoints * 3),
|
|
rotations: new Float32Array(numPoints * 4),
|
|
alphas: new Float32Array(numPoints),
|
|
colors: new Float32Array(numPoints * 3),
|
|
sh: new Float32Array(numPoints * shDim * 3),
|
|
};
|
|
for (let i = 0; i < numPoints; i++) {
|
|
const base = i * strideFloats;
|
|
/* positions, scales, colours */
|
|
result.positions.set([
|
|
f32[base + posIdx[0]],
|
|
f32[base + posIdx[1]],
|
|
f32[base + posIdx[2]],
|
|
], i * 3);
|
|
result.scales.set([
|
|
f32[base + scaleIdx[0]],
|
|
f32[base + scaleIdx[1]],
|
|
f32[base + scaleIdx[2]],
|
|
], i * 3);
|
|
result.colors.set([
|
|
f32[base + colorIdx[0]],
|
|
f32[base + colorIdx[1]],
|
|
f32[base + colorIdx[2]],
|
|
], i * 3);
|
|
/* rotations (quat wxyz order rot_0..3) */
|
|
result.rotations.set([
|
|
f32[base + rotIdx[0]],
|
|
f32[base + rotIdx[1]],
|
|
f32[base + rotIdx[2]],
|
|
f32[base + rotIdx[3]],
|
|
], i * 4);
|
|
/* alpha */
|
|
result.alphas[i] = f32[base + alphaIdx[0]];
|
|
/* spherical harmonics */
|
|
for (let j = 0; j < shDim; j++) {
|
|
const dst = (i * shDim + j) * 3;
|
|
result.sh[dst] = f32[base + shIdx[j]]; // R
|
|
result.sh[dst + 1] = f32[base + shIdx[j + shDim]]; // G
|
|
result.sh[dst + 2] = f32[base + shIdx[j + 2 * shDim]]; // B
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
// --------------------------------------------------------------------------
|