Перенесены backend/frontend/desktop/engine, добавлены вкладки конструктора сцен и генератора датасета с параметрами лучей и длины сетки рельефа, обновлены API и Docker-сборка. Co-authored-by: Cursor <cursoragent@cursor.com>
216 lines
7.7 KiB
JavaScript
216 lines
7.7 KiB
JavaScript
import { defineStore } from "pinia";
|
||
import { api } from "@/api/client";
|
||
|
||
export const useDatasetStore = defineStore("dataset", {
|
||
state: () => ({
|
||
busy: false,
|
||
previewBusy: false,
|
||
statusText: "Выберите .obj модель и нажмите «Сгенерировать».",
|
||
count: 5,
|
||
seed: 42,
|
||
outputDir: "sonar_dataset",
|
||
resolvedOutputDir: null,
|
||
modelFile: null,
|
||
modelFileName: "",
|
||
objectScale: 1,
|
||
beamCount: 45,
|
||
lengthCount: 45,
|
||
lastResult: null,
|
||
selectedStem: null,
|
||
previewPoints: [],
|
||
previewStem: null,
|
||
previewMeta: null,
|
||
highlightClass: null,
|
||
classLabels: { 0: "background", 1: "object" },
|
||
classCounts: {},
|
||
logLines: [],
|
||
}),
|
||
getters: {
|
||
stats(state) {
|
||
return state.lastResult?.stats || null;
|
||
},
|
||
written(state) {
|
||
return state.lastResult?.written || [];
|
||
},
|
||
previewDir(state) {
|
||
return state.resolvedOutputDir || state.outputDir || "sonar_dataset";
|
||
},
|
||
selectedScene(state) {
|
||
const stem = state.selectedStem;
|
||
if (!stem) return null;
|
||
return (state.lastResult?.written || []).find((item) => item.stem === stem) || null;
|
||
},
|
||
canGenerate(state) {
|
||
return !!state.modelFile && !state.busy;
|
||
},
|
||
classOptions(state) {
|
||
const labels = state.classLabels || { 0: "background", 1: "object" };
|
||
const counts = state.classCounts || {};
|
||
const keys = Object.keys(labels).length
|
||
? Object.keys(labels)
|
||
: Object.keys(counts);
|
||
return keys
|
||
.map((k) => Number(k))
|
||
.sort((a, b) => a - b)
|
||
.map((id) => ({
|
||
id,
|
||
label: labels[String(id)] || labels[id] || `class ${id}`,
|
||
count: counts[String(id)] ?? counts[id] ?? 0,
|
||
}));
|
||
},
|
||
},
|
||
actions: {
|
||
pushLog(line) {
|
||
this.logLines.push(String(line));
|
||
if (this.logLines.length > 200) {
|
||
this.logLines = this.logLines.slice(-200);
|
||
}
|
||
},
|
||
setHighlightClass(value) {
|
||
if (value === null || value === undefined || value === "" || value === "all") {
|
||
this.highlightClass = null;
|
||
return;
|
||
}
|
||
this.highlightClass = Number(value);
|
||
},
|
||
applyPreviewPayload(result, writtenMeta = null) {
|
||
this.previewStem = result.stem;
|
||
this.previewPoints = result.points || [];
|
||
this.classCounts = result.classCounts || {};
|
||
if (result.classLabels) {
|
||
this.classLabels = result.classLabels;
|
||
}
|
||
this.previewMeta = {
|
||
pointCount: result.pointCount,
|
||
previewCount: result.previewCount,
|
||
visibility: writtenMeta?.visibility,
|
||
hasObject: writtenMeta?.hasObject,
|
||
objectPointCount: writtenMeta?.objectPointCount,
|
||
classCounts: result.classCounts || {},
|
||
};
|
||
},
|
||
setModelFile(file) {
|
||
if (!file) {
|
||
this.modelFile = null;
|
||
this.modelFileName = "";
|
||
this.statusText = "Выберите .obj модель и нажмите «Сгенерировать».";
|
||
return;
|
||
}
|
||
const name = String(file.name || "");
|
||
if (!name.toLowerCase().endsWith(".obj")) {
|
||
this.modelFile = null;
|
||
this.modelFileName = "";
|
||
this.statusText = "Нужен файл формата .obj";
|
||
return;
|
||
}
|
||
this.modelFile = file;
|
||
this.modelFileName = name;
|
||
this.statusText = `Модель: ${name}`;
|
||
this.pushLog(`Выбрана модель: ${name}`);
|
||
},
|
||
async generate() {
|
||
if (this.busy) return;
|
||
if (!this.modelFile) {
|
||
this.statusText = "Сначала выберите файл модели .obj";
|
||
return;
|
||
}
|
||
this.busy = true;
|
||
this.statusText = "Генерация датасета…";
|
||
this.pushLog(
|
||
`Старт: count=${this.count}, seed=${this.seed}, beams=${this.beamCount}, length=${this.lengthCount}, scale=${this.objectScale}, dir=${this.outputDir}, model=${this.modelFileName}`,
|
||
);
|
||
try {
|
||
const result = await api.datasetGenerate({
|
||
count: Number(this.count) || 5,
|
||
seed: Number(this.seed) || 0,
|
||
outputDir: String(this.outputDir || "sonar_dataset"),
|
||
objectScale: Number(this.objectScale) || 1,
|
||
beamCount: Number(this.beamCount) || 45,
|
||
lengthCount: Number(this.lengthCount) || 45,
|
||
modelFile: this.modelFile,
|
||
});
|
||
this.lastResult = result;
|
||
this.resolvedOutputDir = result?.outputDir || null;
|
||
const s = result?.stats || {};
|
||
this.statusText = `Готово: ${result.count} сцен → ${result.outputDir}`;
|
||
this.pushLog(
|
||
`Модель: ${result.objectName || this.modelFileName} (${result.objectVertexCount || "?"} вершин), scale=${result.objectScale ?? this.objectScale}, ширина=${result.beamCount ?? this.beamCount}, длина=${result.lengthCount ?? this.lengthCount}. class 1 = object.`,
|
||
);
|
||
this.pushLog(
|
||
`Записано ${result.count} сцен. С объектом: ${s.withObject}, без: ${s.withoutObject}.`,
|
||
);
|
||
this.pushLog(
|
||
`Видимость: nearly_hidden=${s.nearly_hidden || 0}, partial=${s.partial || 0}, visible=${s.visible || 0}, absent=${s.absent || 0}.`,
|
||
);
|
||
for (const item of result.written || []) {
|
||
this.pushLog(
|
||
`${item.stem}: pts=${item.pointCount}, object=${item.objectPointCount}, ${item.visibility}`,
|
||
);
|
||
}
|
||
|
||
const initialStem =
|
||
result?.preview?.stem ||
|
||
result?.written?.find((item) => item.hasObject)?.stem ||
|
||
result?.written?.[0]?.stem ||
|
||
null;
|
||
|
||
if (result?.preview?.points?.length && result.preview.stem === initialStem) {
|
||
this.selectedStem = initialStem;
|
||
const meta = result.written?.find((w) => w.stem === initialStem);
|
||
this.applyPreviewPayload(
|
||
{
|
||
stem: initialStem,
|
||
points: result.preview.points,
|
||
pointCount: result.preview.pointCount,
|
||
previewCount: result.preview.points.length,
|
||
classCounts: result.preview.classCounts || {},
|
||
classLabels: result.preview.classLabels || result.classLabels,
|
||
},
|
||
meta,
|
||
);
|
||
} else if (initialStem) {
|
||
await this.selectScene(initialStem);
|
||
} else {
|
||
this.selectedStem = null;
|
||
this.previewStem = null;
|
||
this.previewPoints = [];
|
||
this.previewMeta = null;
|
||
this.classCounts = {};
|
||
}
|
||
} catch (error) {
|
||
this.statusText = `Ошибка: ${error.message}`;
|
||
this.pushLog(`Ошибка: ${error.message}`);
|
||
throw error;
|
||
} finally {
|
||
this.busy = false;
|
||
}
|
||
},
|
||
async selectScene(stem) {
|
||
if (!stem || this.previewBusy) return;
|
||
if (stem === this.previewStem && this.previewPoints?.length) {
|
||
this.selectedStem = stem;
|
||
return;
|
||
}
|
||
this.previewBusy = true;
|
||
this.selectedStem = stem;
|
||
this.statusText = `Загрузка превью: ${stem}…`;
|
||
try {
|
||
const result = await api.datasetPreview({
|
||
stem,
|
||
outputDir: String(this.previewDir),
|
||
});
|
||
const writtenMeta = this.written.find((item) => item.stem === result.stem);
|
||
this.applyPreviewPayload(result, writtenMeta);
|
||
this.statusText = `Превью: ${result.stem} (${result.pointCount} точек)`;
|
||
this.pushLog(`Превью загружено: ${result.stem} (${result.pointCount} pts)`);
|
||
} catch (error) {
|
||
this.statusText = `Ошибка превью: ${error.message}`;
|
||
this.pushLog(`Ошибка превью: ${error.message}`);
|
||
throw error;
|
||
} finally {
|
||
this.previewBusy = false;
|
||
}
|
||
},
|
||
},
|
||
});
|