Реструктуризация проекта и генератор синтетических датасетов эхолота.
Перенесены backend/frontend/desktop/engine, добавлены вкладки конструктора сцен и генератора датасета с параметрами лучей и длины сетки рельефа, обновлены API и Docker-сборка. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
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;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user