Files
workinf_Blender_Wasm/web/app/src/three-adapter/nonmesh.ts
2026-08-14 18:08:29 -04:00

237 lines
12 KiB
TypeScript

import {
BufferGeometry,
Color,
Float32BufferAttribute,
Group,
Line,
LineBasicMaterial,
LineSegments,
Mesh,
MeshPhysicalMaterial,
Points,
PointsMaterial,
SphereGeometry,
type Object3D,
} from "../vendor/three/three.module.js";
import type { NonMeshDataIR, SceneNodeIR } from "../../../protocol/scene-ir";
import type { NonMeshGeometryChunk } from "../../../protocol/nonmesh-binary";
import type { CurveGizmoHandleIR } from "../../../protocol/nonmesh-interaction";
export type NonMeshElementKind = "CONTROL_POINT" | "HANDLE_LEFT" | "HANDLE_RIGHT";
export type NonMeshElementSelection = ReadonlyMap<string, ReadonlyMap<NonMeshElementKind, ReadonlySet<number>>>;
function blenderPosition(x: number, y: number, z: number): [number, number, number] {
return [x, z, -y];
}
function createControlPointGeometry(points: ArrayLike<number>, start: number, end: number, closed: boolean): BufferGeometry | null {
if (end - start < 2) return null;
const pointCount = end - start + (closed ? 1 : 0);
const positions = new Float32Array(pointCount * 3);
for (let local = 0; local < pointCount; local++) {
const index = closed && local === end - start ? start : start + local;
const point = index * 3;
const target = local * 3;
positions[target] = points[point];
positions[target + 1] = points[point + 2];
positions[target + 2] = -points[point + 1];
}
const geometry = new BufferGeometry();
geometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
return geometry;
}
function createCurvePreview(data: NonMeshDataIR, points: ArrayLike<number> = data.controlPoints ?? [], splineOffsets?: ArrayLike<number>): Object3D | null {
if (points.length < 6) return null;
const group = new Group();
const offsets = splineOffsets && splineOffsets.length > 1 ? splineOffsets : data.splineOffsets && data.splineOffsets.length > 1 ? data.splineOffsets : [0, points.length / 3];
const material = new LineBasicMaterial({ color: new Color(0x67b7ff), transparent: true, opacity: 0.95 });
const handlePoints = data.handlePoints;
const handlePointIndices = data.handlePointIndices ?? (handlePoints?.length === (points.length / 3) * 6 ? Array.from({ length: points.length / 3 }, (_, index) => index) : []);
const hasHandles = data.type === "CURVE" && Boolean(handlePoints && handlePointIndices.length > 0 && handlePoints.length === handlePointIndices.length * 6);
const handleLines: number[] = [];
const handlePositions: number[] = [];
const handleIndexMap: number[] = [];
const handleKindMap: NonMeshElementKind[] = [];
if (hasHandles) {
for (let handleIndex = 0; handleIndex < handlePointIndices.length; handleIndex++) {
const pointIndex = handlePointIndices[handleIndex];
const point = pointIndex * 3;
const handle = handleIndex * 6;
const anchor = blenderPosition(points[point], points[point + 1], points[point + 2]);
const left = blenderPosition(handlePoints![handle], handlePoints![handle + 1], handlePoints![handle + 2]);
const right = blenderPosition(handlePoints![handle + 3], handlePoints![handle + 4], handlePoints![handle + 5]);
handleLines.push(anchor[0], anchor[1], anchor[2], left[0], left[1], left[2], anchor[0], anchor[1], anchor[2], right[0], right[1], right[2]);
handlePositions.push(left[0], left[1], left[2], right[0], right[1], right[2]);
handleIndexMap.push(pointIndex, pointIndex);
handleKindMap.push("HANDLE_LEFT", "HANDLE_RIGHT");
}
}
for (let index = 0; index < offsets.length - 1; index++) {
const start = Math.max(0, Math.floor(offsets[index]));
const end = Math.min(points.length / 3, Math.floor(offsets[index + 1]));
const closed = data.cyclicU?.[index] ?? false;
const geometry = createControlPointGeometry(points, start, end, closed);
if (!geometry) continue;
const line = new Line(geometry, material.clone());
group.add(line);
}
const controlGeometry = new BufferGeometry();
const controlPositions = new Float32Array(points.length);
for (let index = 0; index < points.length; index += 3) {
controlPositions.set(blenderPosition(points[index], points[index + 1], points[index + 2]), index);
}
controlGeometry.setAttribute("position", new Float32BufferAttribute(controlPositions, 3));
const controls = new Points(controlGeometry, new PointsMaterial({ color: new Color(0x67b7ff), size: 0.09, sizeAttenuation: true, vertexColors: true }));
controls.userData.nonMeshDataId = data.id;
controls.userData.nonMeshPointIndexMap = Array.from({ length: points.length / 3 }, (_, index) => index);
controls.userData.nonMeshPointKindMap = Array.from({ length: points.length / 3 }, () => "CONTROL_POINT" as NonMeshElementKind);
group.add(controls);
if (hasHandles) {
const lineGeometry = new BufferGeometry();
lineGeometry.setAttribute("position", new Float32BufferAttribute(handleLines, 3));
const lines = new LineSegments(lineGeometry, new LineBasicMaterial({ color: new Color(0x9a7bff), transparent: true, opacity: 0.65 }));
lines.userData.nonMeshDataId = data.id;
lines.userData.nonMeshHandleLinePointIndexMap = [...handlePointIndices];
lines.userData.nonMeshHandleBasePositions = new Float32Array(handleLines);
group.add(lines);
const pointGeometry = new BufferGeometry();
pointGeometry.setAttribute("position", new Float32BufferAttribute(handlePositions, 3));
const handles = new Points(pointGeometry, new PointsMaterial({ color: new Color(0xd7b8ff), size: 0.1, sizeAttenuation: true, vertexColors: true }));
handles.userData.nonMeshDataId = data.id;
handles.userData.nonMeshPointIndexMap = handleIndexMap;
handles.userData.nonMeshPointKindMap = handleKindMap;
handles.userData.nonMeshHandleBasePositions = new Float32Array(handlePositions);
group.add(handles);
}
return group.children.length > 0 ? group : null;
}
function createPointPreview(data: NonMeshDataIR, points: ArrayLike<number> = data.controlPoints ?? []): Object3D | null {
if (points.length < 3) return null;
const positions = new Float32Array(points.length);
for (let index = 0; index < points.length; index += 3) {
positions[index] = points[index];
positions[index + 1] = points[index + 2];
positions[index + 2] = -points[index + 1];
}
const geometry = new BufferGeometry();
geometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
const pointObject = new Points(geometry, new PointsMaterial({ color: new Color(0xffc36b), size: 0.08, sizeAttenuation: true }));
pointObject.userData.nonMeshDataId = data.id;
pointObject.userData.nonMeshPointOffset = 0;
return pointObject;
}
function createMetaballPreview(data: NonMeshDataIR): Object3D | null {
if (!data.elements || data.elements.length === 0) return null;
const group = new Group();
for (const element of data.elements) {
const [x, y, z] = blenderPosition(...element.position);
const sphere = new Mesh(
new SphereGeometry(Math.max(0.001, element.radius), 16, 10),
new MeshPhysicalMaterial({ color: 0x6eb7ff, roughness: 0.36, metalness: 0.02 }),
);
sphere.position.set(x, y, z);
sphere.scale.set(...(element.scale.map((value) => value > 0 ? value : 1) as [number, number, number]));
sphere.castShadow = true;
sphere.receiveShadow = true;
sphere.userData.nonMeshDataId = data.id;
group.add(sphere);
}
return group;
}
function binaryPreviewData(data: NonMeshDataIR, chunks: readonly NonMeshGeometryChunk[]): { points: Float32Array; offsets?: Uint32Array } | null {
if (data.geometryStatus !== "binary") return null;
const matching = chunks.filter((chunk) => chunk.dataId === data.id).sort((left, right) => left.chunkIndex - right.chunkIndex);
if (matching.length === 0) return null;
const first = matching[0];
if (matching.some((chunk, index) => chunk.chunkIndex !== index || chunk.totalPointCount !== first.totalPointCount)) return null;
const points = new Float32Array(first.totalPointCount * 3);
for (const chunk of matching) points.set(new Float32Array(chunk.positions), chunk.pointOffset * 3);
return { points, offsets: first.curveOffsets ? new Uint32Array(first.curveOffsets) : undefined };
}
export function createNonMeshObject(data: NonMeshDataIR, chunks: readonly NonMeshGeometryChunk[] = []): Object3D | null {
const binary = binaryPreviewData(data, chunks);
if (data.geometryStatus !== "available" && !binary) return null;
const points = binary?.points ?? data.controlPoints;
const offsets = binary?.offsets;
if (data.type === "METABALL") return createMetaballPreview(data);
if (data.type === "POINT_CLOUD" || data.type === "CURVES" || data.type === "HAIR") return points ? createPointPreview(data, points) : null;
if (data.type === "CURVE" || data.type === "SURFACE" || data.type === "FONT") return points ? createCurvePreview(data, points, offsets) : null;
return null;
}
export function applyNonMeshTransform(object: Object3D, node: SceneNodeIR): void {
const [x, y, z] = node.transform.translation;
const [rx, ry, rz] = node.transform.rotationEuler;
object.position.set(x, z, -y);
object.rotation.set(rx, rz, -ry);
object.scale.set(...node.transform.scale);
object.name = node.name;
object.userData.sceneNodeId = node.id;
object.userData.blenderId = node.id;
object.traverse((child) => {
child.userData.sceneNodeId = node.id;
child.userData.blenderId = node.id;
});
}
export function applyNonMeshElementSelection(root: Object3D, selection: NonMeshElementSelection): void {
root.traverse((object) => {
const dataId = object.userData.nonMeshDataId;
const indexMap = object.userData.nonMeshPointIndexMap as number[] | undefined;
const kindMap = object.userData.nonMeshPointKindMap as NonMeshElementKind[] | undefined;
if (typeof dataId !== "string" || !indexMap || !kindMap || !(object instanceof Points) || !(object.material instanceof PointsMaterial)) return;
const selectedByKind = selection.get(dataId);
const colors = new Float32Array(indexMap.length * 3);
for (let index = 0; index < indexMap.length; index++) {
const selected = selectedByKind?.get(kindMap[index])?.has(indexMap[index]) ?? false;
const color = selected ? [1, 0.4, 0.1] : kindMap[index] === "CONTROL_POINT" ? [0.4, 0.72, 1] : [0.6, 0.48, 1];
colors[index * 3] = color[0];
colors[index * 3 + 1] = color[1];
colors[index * 3 + 2] = color[2];
}
object.geometry.setAttribute("color", new Float32BufferAttribute(colors, 3));
object.material.color.set(0xffffff);
object.material.vertexColors = true;
object.material.needsUpdate = true;
});
}
export function applyCurveHandlePreview(root: Object3D, dataId: string, handles: readonly CurveGizmoHandleIR[] | null): void {
const positionsByIdentity = new Map((handles ?? []).map((handle) => [`${handle.pointIndex}:${handle.side}`, handle.position]));
root.traverse((object) => {
if (object.userData.nonMeshDataId !== dataId || !(object instanceof Points || object instanceof LineSegments)) return;
const position = object.geometry.getAttribute("position");
const base = object.userData.nonMeshHandleBasePositions;
if (!(base instanceof Float32Array) || position.count * 3 !== base.length) return;
const values = new Float32Array(base);
if (object instanceof Points) {
const indexMap = object.userData.nonMeshPointIndexMap as number[] | undefined;
const kindMap = object.userData.nonMeshPointKindMap as NonMeshElementKind[] | undefined;
if (!indexMap || !kindMap) return;
for (let index = 0; index < indexMap.length; index++) {
const side = kindMap[index] === "HANDLE_LEFT" ? "LEFT" : kindMap[index] === "HANDLE_RIGHT" ? "RIGHT" : null;
const preview = side ? positionsByIdentity.get(`${indexMap[index]}:${side}`) : undefined;
if (preview) values.set(blenderPosition(...preview), index * 3);
}
}
else {
const indexMap = object.userData.nonMeshHandleLinePointIndexMap as number[] | undefined;
if (!indexMap) return;
for (let index = 0; index < indexMap.length; index++) {
const left = positionsByIdentity.get(`${indexMap[index]}:LEFT`);
const right = positionsByIdentity.get(`${indexMap[index]}:RIGHT`);
if (left) values.set(blenderPosition(...left), index * 12 + 3);
if (right) values.set(blenderPosition(...right), index * 12 + 9);
}
}
position.array.set(values);
position.needsUpdate = true;
object.geometry.computeBoundingSphere();
});
}