auto-git:

[add] src/spline-corridor/spline-repeater-renderer.ts
This commit is contained in:
2026-05-13 03:43:14 +02:00
parent f9bb2334f0
commit 579bd9d24b

View File

@@ -0,0 +1,144 @@
import { Group, MathUtils } from "three";
import {
disposeModelInstance,
instantiateModelTemplate
} from "../assets/gltf-model-import";
import type { Terrain } from "../document/terrains";
import { applyRendererRenderCategoryFromMaterial } from "../rendering/render-layers";
import { loadBundledSplineCorridorModelTemplate } from "./bundled-spline-corridor-model-loader";
import {
deriveSplineRepeaterInstances,
type SplineRepeaterPathLike
} from "./spline-repeaters";
interface SplineRepeaterRendererInput {
paths: readonly SplineRepeaterPathLike[];
terrains: readonly Terrain[];
}
interface SplineRepeaterRendererOptions {
onRebuilt?: () => void;
onDiagnostic?: (message: string) => void;
}
export class SplineRepeaterRenderer {
readonly group = new Group();
private requestId = 0;
private activeGroup: Group | null = null;
private readonly onRebuilt: (() => void) | undefined;
private readonly onDiagnostic: ((message: string) => void) | undefined;
constructor(options: SplineRepeaterRendererOptions = {}) {
this.group.name = "splineRepeaters";
this.group.userData.nonPickable = true;
this.onRebuilt = options.onRebuilt;
this.onDiagnostic = options.onDiagnostic;
}
sync(input: SplineRepeaterRendererInput) {
const requestId = this.requestId + 1;
this.requestId = requestId;
void this.rebuildAsync(requestId, input);
}
clear() {
this.requestId += 1;
this.clearActiveGroup();
}
dispose() {
this.clear();
}
private clearActiveGroup() {
if (this.activeGroup === null) {
return;
}
this.group.remove(this.activeGroup);
disposeModelInstance(this.activeGroup);
this.activeGroup = null;
}
private emitDiagnostic(message: string) {
if (this.onDiagnostic !== undefined) {
this.onDiagnostic(message);
return;
}
console.warn(message);
}
private async rebuildAsync(
requestId: number,
input: SplineRepeaterRendererInput
) {
const instances = input.paths.flatMap((path) =>
deriveSplineRepeaterInstances({
path,
terrains: input.terrains
})
);
const nextGroup = new Group();
nextGroup.name = "splineRepeaterInstances";
nextGroup.userData.nonPickable = true;
for (const instance of instances) {
let template: Group;
try {
template = await loadBundledSplineCorridorModelTemplate(
instance.asset.bundledPath
);
} catch (error) {
this.emitDiagnostic(
error instanceof Error
? error.message
: `Bundled spline corridor model failed to load from ${instance.asset.bundledPath}.`
);
continue;
}
if (requestId !== this.requestId) {
disposeModelInstance(nextGroup);
return;
}
const model = instantiateModelTemplate(template);
model.name = `SplineRepeater:${instance.asset.id}`;
model.position.set(
instance.position.x,
instance.position.y,
instance.position.z
);
model.rotation.set(0, MathUtils.degToRad(instance.yawDegrees), 0);
model.scale.setScalar(instance.scale);
model.userData.nonPickable = true;
model.userData.pathId = instance.pathId;
model.userData.pathRepeaterId = instance.repeaterId;
model.userData.splineCorridorAssetId = instance.asset.id;
applyRendererRenderCategoryFromMaterial(model);
nextGroup.add(model);
}
if (requestId !== this.requestId) {
disposeModelInstance(nextGroup);
return;
}
this.clearActiveGroup();
if (nextGroup.children.length === 0) {
disposeModelInstance(nextGroup);
this.onRebuilt?.();
return;
}
this.activeGroup = nextGroup;
this.group.add(nextGroup);
this.onRebuilt?.();
}
}