diff --git a/backend/api/src/orm/defaults/reset.js b/backend/api/src/orm/defaults/reset.js index d85978e..4fd75ee 100644 --- a/backend/api/src/orm/defaults/reset.js +++ b/backend/api/src/orm/defaults/reset.js @@ -4,6 +4,8 @@ import ArScene from "../models/arScene.js"; import ArAsset from "../models/arAsset.js"; import ArMesh from "../models/arMesh.js"; import ArLabel from "../models/arLabel.js"; +import ArPreset from "../models/arPreset.js"; +import ArAction from "../models/arAction.js"; export async function resetDatabase() { console.log(); @@ -13,5 +15,7 @@ export async function resetDatabase() { await ArAsset.destroy({where:{}}) await ArLabel.destroy({where:{}}) await ArMesh.destroy({where:{}}) + await ArAction.destroy({where:{}}) + await ArPreset.destroy({where:{}}) console.log("Database reset successfully."); } diff --git a/backend/api/src/orm/index.js b/backend/api/src/orm/index.js index 034e5a5..bd6a5dc 100644 --- a/backend/api/src/orm/index.js +++ b/backend/api/src/orm/index.js @@ -6,24 +6,34 @@ import {sequelize} from './database.js' import ArAsset from "./models/arAsset.js"; import ArMesh from "./models/arMesh.js"; import ArLabel from "./models/arLabel.js"; +import ArPreset from "./models/arPreset.js"; +import ArAction from "./models/arAction.js"; ArUser.hasMany(ArProject, { as: 'projects', foreignKey: 'userId', onDelete: 'CASCADE' }); ArProject.belongsTo(ArUser, { as: 'owner', foreignKey: 'userId' }); -ArProject.hasMany(ArScene, { as: 'scenes', foreignKey: 'projectId', onDelete: 'CASCADE'}); +ArProject.hasMany(ArScene, { as: 'scenes', foreignKey: 'projectId', onDelete: 'CASCADE' }); +ArProject.hasMany(ArPreset, { as: 'presets', foreignKey: 'projectId', onDelete: 'CASCADE' }); ArScene.belongsTo(ArProject, { as: 'project', foreignKey: 'projectId' }); ArScene.hasMany(ArAsset, { as: 'assets', foreignKey: 'sceneId', onDelete: 'CASCADE' }); -ArScene.hasMany(ArLabel, {as: 'labels', foreignKey: 'sceneId', onDelete: 'CASCADE' }); -ArScene.hasMany(ArMesh, {as: 'meshes', foreignKey: 'sceneId', onDelete: 'CASCADE' }); +ArScene.hasMany(ArLabel, { as: 'labels', foreignKey: 'sceneId', onDelete: 'CASCADE' }); +ArScene.hasMany(ArMesh, { as: 'meshes', foreignKey: 'sceneId', onDelete: 'CASCADE' }); ArAsset.belongsTo(ArScene, { as: 'scene', foreignKey: 'sceneId' }); ArMesh.belongsTo(ArScene, { as: 'scene', foreignKey: 'sceneId' }); ArLabel.belongsTo(ArScene, { as: 'scene', foreignKey: 'sceneId' }); +ArPreset.belongsTo(ArProject, { as: 'project', foreignKey: 'projectId' }); +ArPreset.hasMany(ArAction, { as: 'actions', foreignKey: 'presetId', onDelete: 'CASCADE' }); + +ArAction.belongsTo(ArPreset, { as: 'preset', foreignKey: 'presetId' }); +ArAction.belongsTo(ArAsset, { as: 'targetAsset', foreignKey: 'targetAssetId' }); +ArAction.belongsTo(ArScene, { as: 'targetScene', foreignKey: 'targetSceneId' }); + export async function initializeDatabase (options) { return await sequelize.sync(options); } -export {ArUser, ArProject, ArScene, ArAsset, ArLabel, ArMesh} +export { ArUser, ArProject, ArScene, ArAsset, ArLabel, ArMesh, ArPreset, ArAction } diff --git a/backend/api/src/orm/models/arAction.js b/backend/api/src/orm/models/arAction.js new file mode 100644 index 0000000..049ed21 --- /dev/null +++ b/backend/api/src/orm/models/arAction.js @@ -0,0 +1,41 @@ +import { DataTypes } from 'sequelize' +import { sequelize } from '../database.js' + +/** + * @typedef {Object} ArActionObject + * @property {string} id + * @property {string} event + * @property {string} targetSceneId + * @property {string} targetAssetId + * @property {Object} parameters + * @property {string} presetId + */ + +export default sequelize.define('ArAction', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + event: { + type: DataTypes.STRING, + allowNull: false + }, + targetSceneId: { + type: DataTypes.UUID, + allowNull: true + }, + targetAssetId: { + type: DataTypes.UUID, + allowNull: true + }, + parameters: { + type: DataTypes.JSON, + allowNull: true, + defaultValue: {} + }, + presetId: { + type: DataTypes.UUID, + allowNull: false + } +}) diff --git a/backend/api/src/orm/models/arPreset.js b/backend/api/src/orm/models/arPreset.js new file mode 100644 index 0000000..2ee8c16 --- /dev/null +++ b/backend/api/src/orm/models/arPreset.js @@ -0,0 +1,35 @@ +import { DataTypes } from 'sequelize' +import { sequelize } from '../database.js' + +/** + * @typedef {Object} ArPresetObject + * @property {string} id + * @property {string} bigText + * @property {string} icon + * @property {string} text + * @property {string} projectId + */ + +export default sequelize.define('ArPreset', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + bigText: { + type: DataTypes.STRING, + allowNull: true + }, + icon: { + type: DataTypes.STRING, + allowNull: true + }, + text: { + type: DataTypes.STRING, + allowNull: true + }, + projectId: { + type: DataTypes.UUID, + allowNull: false + } +}) diff --git a/backend/api/src/orm/models/arProject.js b/backend/api/src/orm/models/arProject.js index 3ba7b1e..68c6b9c 100644 --- a/backend/api/src/orm/models/arProject.js +++ b/backend/api/src/orm/models/arProject.js @@ -11,7 +11,6 @@ import { sequelize } from '../database.js' * @property {string} unit * @property {string} calibrationMessage * @property { "ar" | "vr" } displayMode - * @property {string} presets * @property {string} userId */ @@ -66,11 +65,6 @@ export default sequelize.define('ArProject', { allowNull: false, defaultValue: "ar" }, - presets:{ - type: DataTypes.JSON, - allowNull: false, - defaultValue: [] - }, userId:{ type: DataTypes.UUID, allowNull: false, diff --git a/backend/api/src/routes/project.js b/backend/api/src/routes/project.js index 0515dbe..238b0d3 100644 --- a/backend/api/src/routes/project.js +++ b/backend/api/src/routes/project.js @@ -1,6 +1,6 @@ import express from 'express' import {baseUrl} from "./baseUrl.js"; -import {ArMesh, ArAsset, ArLabel, ArProject, ArScene, ArUser} from "../orm/index.js"; +import {ArMesh, ArAsset, ArLabel, ArProject, ArScene, ArUser, ArPreset, ArAction} from "../orm/index.js"; import {sequelize} from "../orm/database.js"; import authMiddleware, {optionnalAuthMiddleware} from "../middlewares/auth.js"; import { @@ -23,6 +23,44 @@ const router = express.Router() const PAGE_LENGTH = 20; +const projectInclude = [ + { + model: ArScene, + as: "scenes", + separate: true, + order: [['index', 'ASC']], + include: [ + { + model: ArMesh, + as: "meshes" + }, + { + model: ArAsset, + as: "assets", + where: { hideInViewer: false }, + required: false + }, + { + model: ArLabel, + as: "labels", + }, + ], + }, + { + model: ArUser, + as: "owner", + attributes: ["username"], + }, + { + model: ArPreset, + as: "presets", + include: [{ + model: ArAction, + as: "actions" + }] + } +]; + router.get(baseUrl+'projects/:page', optionnalAuthMiddleware, async (req, res) => { const page = parseInt(req.params.page); try{ @@ -107,39 +145,7 @@ router.get(baseUrl+'project/:projectId', optionnalAuthMiddleware, async (req, re let project = await ArProject.findOne({ where, attributes, - include:[ - { - model: ArScene, - as: "scenes", - separate: true, - order: [['index', 'ASC']], - include:[ - { - model:ArMesh, - as:"meshes" - }, - { - model: ArAsset, - as: "assets", - where:{ - hideInViewer: false - }, - required:false - }, - { - model: ArLabel, - as: "labels", - }, - ], - }, - - { - model: ArUser, - as: "owner", - attributes: ["username"], - } - - ], + include: projectInclude }) res.set({ @@ -208,8 +214,47 @@ router.put(baseUrl+'projects/:projectId', authMiddleware, uploadCover.single('up } } + // gestion des presets + if (req.body.presets) { + const presetsData = typeof req.body.presets === 'string' + ? JSON.parse(req.body.presets) + : req.body.presets; + + // Supprimer les anciens presets + const ArPreset = sequelize.models.ArPreset; + const ArAction = sequelize.models.ArAction; + + await ArPreset.destroy({ where: { projectId: project.id } }); + + // Créer les nouveaux + for (const presetData of presetsData) { + const newPreset = await ArPreset.create({ + projectId: project.id, + bigText: presetData.bigText, + icon: presetData.icon, + text: presetData.text + }); - return res.status(200).send(project) + if (presetData.actions && Array.isArray(presetData.actions)) { + for (const actionData of presetData.actions) { + await ArAction.create({ + presetId: newPreset.id, + event: actionData.event, + targetSceneId: actionData.targetSceneId || actionData.parameters?.sceneId || null, + targetAssetId: actionData.targetAssetId || actionData.parameters?.assetId || null, + parameters: actionData.parameters || {} + }); + } + } + } + } + + const updatedProject = await ArProject.findOne({ + where: { id: projectId }, + include: projectInclude + }) + + return res.status(200).send(updatedProject) }catch (e){ console.log(e) res.set({ @@ -431,19 +476,49 @@ router.put(baseUrl+'project/:projectId/presets', authMiddleware, async (req, res return res.status(404).send({ error: 'No project found'}); try { + const ArPreset = sequelize.models.ArPreset; + const ArAction = sequelize.models.ArAction; + + // Supprimer les anciens presets + await ArPreset.destroy({ where: { projectId: project.id } }); + + // Créer les nouveaux + const presetsData = req.body.presets; + + if (presetsData && Array.isArray(presetsData)) { + for (const presetData of presetsData) { + const newPreset = await ArPreset.create({ + projectId: project.id, + bigText: presetData.bigText, + icon: presetData.icon, + text: presetData.text + }); - await project.update({ - presets: req.body.presets - }, { - returning: true - }) + if (presetData.actions && Array.isArray(presetData.actions)) { + for (const actionData of presetData.actions) { + await ArAction.create({ + presetId: newPreset.id, + event: actionData.event, + targetSceneId: actionData.targetSceneId || actionData.parameters?.sceneId || null, + targetAssetId: actionData.targetAssetId || actionData.parameters?.assetId || null, + parameters: actionData.parameters || {} + }); + } + } + } + } - return res.status(200).send(project) + const updatedProject = await ArProject.findOne({ + where: { id: projectId }, + include: projectInclude + }); + + return res.status(200).send(updatedProject) } catch(e) { console.log(e); res.status(400); - return res.send({error: 'Unable to fetch project'}); + return res.send({ error: 'Unable to save presets'}); } @@ -543,23 +618,23 @@ router.get(baseUrl+'project/:projectId/export', authMiddleware, async (req, res) as: "scenes", separate: true, order: [['index', 'ASC']], - attributes: {exclude: ['id']}, + //attributes: {exclude: ['id']}, include:[ { model:ArMesh, as:"meshes", - attributes: {exclude: ['id']} + // exclude: [id] }, { model: ArAsset, as: "assets", required:false, - attributes: {exclude: ['id']} + // exclude: [id] }, { model: ArLabel, as: "labels", - attributes: {exclude: ['id']} + // exclude: [id] }, ], }, @@ -568,6 +643,16 @@ router.get(baseUrl+'project/:projectId/export', authMiddleware, async (req, res) model: ArUser, as: "owner", attributes: ["username"], + }, + { + model: sequelize.models.ArPreset, + as: "presets", + include:[ + { + model: sequelize.models.ArAction, + as: "actions" + } + ] } ], @@ -637,6 +722,20 @@ router.post(baseUrl+'project/import', authMiddleware, uploadProject.single("zip" const projectObj = JSON.parse(data) projectObj.userId = token.id + const presetsData = projectObj.presets || []; + delete projectObj.presets; + const sourceScenesData = JSON.parse(JSON.stringify(projectObj.scenes || [])); + + const removeIds = (obj) => { + if (Array.isArray(obj)) obj.forEach(removeIds); + else if (typeof obj === 'object' && obj !== null) { + delete obj.id; + Object.values(obj).forEach(removeIds); + } + }; + removeIds(projectObj.scenes); + + const project = await ArProject.create(projectObj, { include: [ { @@ -662,22 +761,87 @@ router.post(baseUrl+'project/import', authMiddleware, uploadProject.single("zip" }) // modifier l'url des fichiers (assets, cover...) - project.pictureUrl = updateUrl(project.pictureUrl, project.id) await project.save() - for(let scene of project.scenes) { + const sceneMap = new Map(); + const assetMap = new Map(); + const labelMap = new Map(); + + for(let i = 0; i < project.scenes.length; i++) { + const scene = project.scenes[i]; + const oldSceneData = sourceScenesData[i]; + + if(oldSceneData && oldSceneData.id) { + sceneMap.set(oldSceneData.id, scene.id); + } + scene.envmapUrl = updateUrl(scene.envmapUrl, project.id) - for(let asset of scene.assets) { + for(let j = 0; j < scene.assets.length; j++) { + const asset = scene.assets[j]; + const oldAssetData = oldSceneData ? oldSceneData.assets[j] : null; + + if(oldAssetData && oldAssetData.id) { + assetMap.set(oldAssetData.id, asset.id); + } + asset.url = updateUrl(asset.url, project.id) await asset.save() } + for(let j = 0; j < scene.labels.length; j++) { + const label = scene.labels[j]; + const oldLabelData = oldSceneData ? oldSceneData.labels[j] : null; + + if(oldLabelData && oldLabelData.id) { + labelMap.set(oldLabelData.id, label.id); + } + } + await scene.save() } + for (const presetData of presetsData) { + const newPreset = await ArPreset.create({ + projectId: project.id, + bigText: presetData.bigText, + icon: presetData.icon, + text: presetData.text + }); + + if (presetData.actions && Array.isArray(presetData.actions)) { + for (const actionData of presetData.actions) { + + let newTargetSceneId = null; + if (actionData.targetSceneId) { + newTargetSceneId = sceneMap.get(actionData.targetSceneId); + } + + let newTargetAssetId = null; + if (actionData.targetAssetId) { + newTargetAssetId = assetMap.get(actionData.targetAssetId); + } + + const newParams = { ...actionData.parameters }; + if (newParams.sceneId) newParams.sceneId = sceneMap.get(newParams.sceneId) || newParams.sceneId; + if (newParams.assetId) newParams.assetId = assetMap.get(newParams.assetId) || newParams.assetId; + if (newParams.labelId) newParams.labelId = labelMap.get(newParams.labelId) || newParams.labelId; + + + await ArAction.create({ + presetId: newPreset.id, + event: actionData.event, + targetSceneId: newTargetSceneId, + targetAssetId: newTargetAssetId, + parameters: newParams + }); + } + } + } + + const projectDir = getProjectDirectory(project.id) await fs.renameSync(dataFolder+"/files", projectDir) diff --git a/frontend/user/src/js/socket/socketConnection.js b/frontend/user/src/js/socket/socketConnection.js index 10a0dcf..047914b 100644 --- a/frontend/user/src/js/socket/socketConnection.js +++ b/frontend/user/src/js/socket/socketConnection.js @@ -29,24 +29,27 @@ export class SocketConnection { this.recording = ref(false) this.actionsRecord = [] - this.socket.onAny((event, ...args) => this.handleActionManager(event, ...args)) + this.socket.onAny((event, payload) => this.handleActionManager(event, payload)) } - send(event, ...args) { + send(event, payload, callback = null) { if(this.recording) { - this.actionsRecord.push({event, args}) + this.actionsRecord.push({event, payload}) } else { - this.socket.emit(event, ...args) + if(callback) + this.socket.emit(event, payload, callback) + else + this.socket.emit(event, payload) } - this.handleActionManager(event, ...args) + this.handleActionManager(event, payload) } addListener(event, handler) { this.socket.on(event, handler) } - handleActionManager(event, ...args) { + handleActionManager(event, payload) { if(!this.socketActionManager) return if(event.startsWith("presentation:action:")) { @@ -55,7 +58,7 @@ export class SocketConnection { Object.getOwnPropertyNames(Object.getPrototypeOf(this.socketActionManager)).includes(eventName) && typeof this.socketActionManager[eventName] === 'function' ) - this.socketActionManager[eventName](...args) + this.socketActionManager[eventName](payload) else console.error("SocketActionManager : event "+eventName+" not found") } diff --git a/frontend/user/src/views/PresentationView.vue b/frontend/user/src/views/PresentationView.vue index 3accdb4..92efaf1 100644 --- a/frontend/user/src/views/PresentationView.vue +++ b/frontend/user/src/views/PresentationView.vue @@ -151,10 +151,39 @@ function resetScene() { socket.send("presentation:action:reset", {}) } -function applyPreset(preset) { - for(const action of preset.actions) { - socket.send(action.event, ...action.args) - } +const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + +async function applyPreset(preset) { + if (!preset || !preset.actions) return; + + let actionsToRun = [...preset.actions]; + actionsToRun.sort((a, b) => { + const getRank = (event) => { + if (event.includes('reset')) return 0; + if (event.includes('scene')) return 1; + return 2; + }; + return getRank(a.event) - getRank(b.event); + }); + + for (const action of actionsToRun) { + const effectiveAssetId = action.targetAssetId || action.assetId || action.parameters?.assetId; + const effectiveSceneId = action.targetSceneId || action.sceneId || action.parameters?.sceneId; + const payload = { + ...action.parameters, + targetAssetId: effectiveAssetId, + targetSceneId: effectiveSceneId, + assetId: effectiveAssetId, + sceneId: effectiveSceneId, + parameters: { + ...action.parameters, + assetId: effectiveAssetId, + sceneId: effectiveSceneId + } + }; + + socket.send(action.event, payload); + } } function showAll() { @@ -193,17 +222,35 @@ function createPreset() { } async function savePresets() { + + const presetsToSend = editingPresets.value.map(preset => ({ + ...preset, + actions: (preset.actions || []).map(action => { + // Handle both new recordings (payload) and existing records (parameters) + const parameters = action.payload || action.parameters || {}; + + return { + event: action.event, + // Extract IDs explicitly for the backend Relation + targetSceneId: action.targetSceneId || parameters.sceneId || null, + targetAssetId: action.targetAssetId || parameters.assetId || null, + parameters: parameters + } + }) + })) + const res = await fetch(`${ENDPOINT}project/${project.id}/presets`,{ method: "PUT", headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token.value}`, }, - body: JSON.stringify({presets: editingPresets.value}), + body: JSON.stringify({presets: presetsToSend }), }) if(res.ok) { - project.presets = editingPresets.value + const updatedProjectData = await res.json(); + project.presets = updatedProjectData.presets; } else { toast.error(res.status + " : " + res.statusText, { position: toast.POSITION.BOTTOM_RIGHT