Реструктуризация проекта и генератор синтетических датасетов эхолота.

Перенесены backend/frontend/desktop/engine, добавлены вкладки конструктора сцен и генератора датасета с параметрами лучей и длины сетки рельефа, обновлены API и Docker-сборка.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-17 12:25:00 +03:00
co-authored by Cursor
parent 18a58f2e85
commit 4f253b860f
134 changed files with 5263 additions and 863 deletions
+215
View File
@@ -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;
}
},
},
});