237 lines
12 KiB
TypeScript
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();
|
|
});
|
|
}
|