feat: add model file library links

This commit is contained in:
2026-07-07 18:15:41 +08:00
parent 7e2e56a50d
commit b5440fa1c3
7 changed files with 358 additions and 6 deletions

View File

@@ -14,6 +14,52 @@ import { stepParseCacheDir } from "./stepParse.js";
import { listPlugins, getPlugin } from "./plugins/registry.js";
import { enqueueAlgorithmJob, algorithmJobStats, cancelAlgorithmJob, cancelAlgorithmJobsByTask } from "./algorithmJob.js";
async function validateRegistrationRunInput(input: {
taskId: number;
pointCloudFileIds?: number[];
cadFileIds?: number[];
allowDuplicateRegistration?: boolean;
}) {
const pointCloudFileIds = input.pointCloudFileIds ?? [];
const cadFileIds = input.cadFileIds ?? [];
if (pointCloudFileIds.length !== 1 || cadFileIds.length !== 1) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "整车配准必须恰好选择 1 个点云文件和 1 个 CAD 文件。",
});
}
const pointcloudFiles = await db.listTaskPointcloudFiles(input.taskId);
const pointcloud = pointcloudFiles.find((row) => row.id === pointCloudFileIds[0]);
if (!pointcloud) {
throw new TRPCError({ code: "NOT_FOUND", message: "选中的点云文件不存在。" });
}
if (pointcloud.source === "registered") {
throw new TRPCError({
code: "BAD_REQUEST",
message: "已配准点云不能再次作为配准输入,请选择原始点云进行配准。",
});
}
const cadFiles = await db.listTaskCadFiles(input.taskId);
const cad = cadFiles.find((row) => row.id === cadFileIds[0]);
if (!cad) {
throw new TRPCError({ code: "NOT_FOUND", message: "选中的 CAD 文件不存在。" });
}
const registeredChildren = pointcloudFiles
.filter((row) => row.source === "registered" && row.sourcePointcloudFileId === pointcloud.id)
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
if (registeredChildren.length > 0 && !input.allowDuplicateRegistration) {
const latest = registeredChildren[0];
throw new TRPCError({
code: "CONFLICT",
message: `点云「${pointcloud.name}」已有配准产物「${latest.name}」,重复配准需要确认。`,
});
}
}
export const appRouter = router({
system: systemRouter,
@@ -147,6 +193,23 @@ export const appRouter = router({
}),
}),
// ─── 车型点云文件 ─────────────────────────────────────────────────────────
modelPointcloudFile: router({
list: protectedProcedure
.input(z.object({ modelId: z.number() }))
.query(async ({ input }) => {
return db.listModelPointcloudFiles(input.modelId);
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteModelPointcloudFile(input.id);
return { success: true };
}),
}),
// ─── 任务 CAD 文件 ────────────────────────────────────────────────────────────
taskCadFile: router({
@@ -163,6 +226,27 @@ export const appRouter = router({
return { success: true };
}),
linkFromModel: protectedProcedure
.input(z.object({
taskId: z.number(),
modelCadFileIds: z.array(z.number()).min(1),
}))
.mutation(async ({ input }) => {
try {
const files = await db.linkTaskCadFilesFromModel(input.taskId, input.modelCadFileIds);
return { success: true, files };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
if (message.includes("another model")) {
throw new TRPCError({ code: "BAD_REQUEST", message: "只能关联当前车型的 CAD 库文件" });
}
if (message.includes("not found")) {
throw new TRPCError({ code: "NOT_FOUND", message: "任务或 CAD 库文件不存在" });
}
throw e;
}
}),
// 检查 CAD 文件是否已有三角化采样缓存(.cad_sample_cache/ 目录下存在对应 PLY)
hasSampleCache: protectedProcedure
.input(z.object({ cadFileIds: z.array(z.number()) }))
@@ -257,6 +341,27 @@ export const appRouter = router({
await db.deleteTaskPointcloudFile(input.id);
return { success: true };
}),
linkFromModel: protectedProcedure
.input(z.object({
taskId: z.number(),
modelPointcloudFileIds: z.array(z.number()).min(1),
}))
.mutation(async ({ input }) => {
try {
const files = await db.linkTaskPointcloudFilesFromModel(input.taskId, input.modelPointcloudFileIds);
return { success: true, files };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
if (message.includes("another model")) {
throw new TRPCError({ code: "BAD_REQUEST", message: "只能关联当前车型的点云库文件" });
}
if (message.includes("not found")) {
throw new TRPCError({ code: "NOT_FOUND", message: "任务或点云库文件不存在" });
}
throw e;
}
}),
}),
// ─── 任务分析结果 ────────────────────────────────────────────────────────────
@@ -467,6 +572,8 @@ export const appRouter = router({
pointCloudFileIds: z.array(z.number()).optional(),
/** 可选:任务级 CAD 文件 ID(空/缺省 = 不传 CAD) */
cadFileIds: z.array(z.number()).optional(),
/** 配准:允许同一原始点云再次生成配准产物 */
allowDuplicateRegistration: z.boolean().optional(),
}),
)
.mutation(async ({ input }) => {
@@ -477,6 +584,9 @@ export const appRouter = router({
message: `未注册的算法插件:${input.pluginId}`,
});
}
if (manifest.category === "配准") {
await validateRegistrationRunInput(input);
}
try {
const record = await db.createTaskAnalysisResult({
@@ -529,6 +639,8 @@ export const appRouter = router({
pointCloudFileIds: z.array(z.number()).optional(),
/** 可选:任务级 CAD 文件 ID(空/缺省 = 不传 CAD) */
cadFileIds: z.array(z.number()).optional(),
/** 配准:允许同一原始点云再次生成配准产物 */
allowDuplicateRegistration: z.boolean().optional(),
jobs: z
.array(
z.object({
@@ -542,6 +654,10 @@ export const appRouter = router({
)
.mutation(async ({ input }) => {
const created: { id: number; pluginId: string }[] = [];
const hasRegistrationJob = input.jobs.some((job) => getPlugin(job.pluginId)?.category === "配准");
if (hasRegistrationJob) {
await validateRegistrationRunInput(input);
}
for (const job of input.jobs) {
const manifest = getPlugin(job.pluginId);
if (!manifest) continue;