Compare commits

...

5 Commits

Author SHA1 Message Date
421cd97fe9 Add console log forwarding to New Relic and enable application logging
All checks were successful
Build and Deploy / build (push) Successful in 1m47s
- Add console shim to forward log/error/warn/info to New Relic
- Enable application_logging with forwarding in newrelic.cjs
- Import newrelic in server.js for recordLogEvent API
- Update CI workflow with New Relic config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 17:21:29 -06:00
58959fe347 Fix newrelic config to use .cjs extension for CommonJS
Rename newrelic.js to newrelic.cjs to work with ES module projects

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 15:18:20 -06:00
d9cd0692b5 Add New Relic Node.js APM monitoring to backend
- Install newrelic package for server-side APM
- Create newrelic.js configuration with distributed tracing enabled
- Update npm scripts to preload agent via -r flag for ES modules
- Correlates with existing browser agent for end-to-end tracing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 15:15:27 -06:00
3155cc930f Fix camera positioning, label z-fighting, and remove dead code
- Fix desktop camera to be directly above platform by resetting local position
- Increase label back offset from 0.001 to 0.005 to prevent z-fighting
- Use refreshBoundingInfo({}) for consistency with codebase
- Remove unused copyToPublic from pouchData.ts
- Remove dead DiagramEntityAdapter and integration/gizmo module
- Remove unused netlify functions and integration utilities
- Clean up unused imports and commented code across multiple files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 15:06:18 -06:00
8c2b7f9c7d Fix diagram text sync, resize handle positioning, and PouchDB delete handling
- Fix diagramObject text setter to update entity before notifying observers
- Improve ResizeGizmo handle positioning directly at corners/faces with constant screen-size scaling
- Fix PouchDB sync to handle deleted documents using _id field for compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 11:16:43 -06:00
26 changed files with 1810 additions and 885 deletions

View File

@ -57,6 +57,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
run: |
# Create .env.production with secrets (only accessible by immersive user)
echo "# Auto-generated by CI/CD - Do not edit manually" > /opt/immersive/.env.production

View File

@ -1,49 +0,0 @@
import {Handler, HandlerContext, HandlerEvent} from "@netlify/functions";
import axios from 'axios';
export const handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {
try {
switch (event.httpMethod) {
case 'POST':
const apiKey = event.headers['api-key'];
const query = event.body;
const response = await axios.post('https://api.newrelic.com/graphql',
query,
{headers: {'Api-Key': apiKey, 'Content-Type': 'application/json'}});
const data = await response.data;
return {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': 'https://cameras.immersiveidea.com',
'Access-Control-Allow-Credentials': 'true'
},
statusCode: 200,
body: JSON.stringify(data)
}
break;
case 'OPTIONS':
const headers = {
'Access-Control-Allow-Origin': 'https://cameras.immersiveidea.com',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': 'content-type, api-key',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE'
};
return {
statusCode: 204,
headers
}
break;
default:
return {
statusCode: 405,
body: 'Method Not Allowed'
}
}
} catch (error) {
console.log(error);
return {
statusCode: 500,
body: JSON.stringify(error)
}
}
};

View File

@ -1,216 +0,0 @@
import axios from 'axios';
const baseurl = 'https://syncdb-service-d3f974de56ef.herokuapp.com/';
const auth = 'admin:stM8Lnm@Cuf-tWZHv';
const authToken = Buffer.from(auth).toString('base64');
type Params = {
username: string,
password: string,
db: string
}
async function checkDB(auth: string, db: string) {
try {
console.log('Checking for DB');
const exist = await axios.head(baseurl + db,
{headers: {'Authorization': 'Basic ' + auth}});
if (exist && exist.status == 200) {
console.log("DB Found");
return true;
}
} catch (err) {
console.log("DB not Found");
//console.log(err);
}
return false;
}
enum Access {
DENIED,
MISSING,
ALLOWED,
}
function getUserToken(params: Params) {
const userAuth = params.username + ':' + params.password;
return Buffer.from(userAuth).toString('base64');
}
async function checkIfDbExists(params: Params): Promise<Access> {
console.log("Checking if DB exists");
if (!params.username || !params.password || !params.db) {
throw new Error('No share key provided');
}
if (await checkDB(getUserToken(params), params.db)) {
return Access.ALLOWED;
}
if (await checkDB(authToken, params.db)) {
return Access.DENIED;
}
return Access.MISSING;
}
async function createDB(params: Params) {
console.log("Creating DB");
const response = await axios.put(
baseurl + params.db,
{},
{
headers: {
'Authorization': 'Basic ' + authToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
console.log(response.status);
console.log(response.data);
return response;
}
async function createUser(params: Params) {
try {
console.log("Checking for User");
const userResponse = await axios.head(
baseurl + '_users/org.couchdb.user:' + params.username,
{
headers: {
'Authorization': 'Basic ' + getUserToken(params),
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (userResponse.status == 200) {
console.log("User Found");
return userResponse;
}
} catch (err) {
console.log("User Missing");
}
console.log("Creating User");
const userResponse = await axios.put(
baseurl + '_users/org.couchdb.user:' + params.username,
{
_id: 'org.couchdb.user:' + params.username,
name: params.username,
password: params.password, roles: [], type: 'user'
},
{
headers: {
'Authorization': 'Basic ' + authToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
return userResponse;
}
async function authorizeUser(params: Params) {
console.log("Authorizing User");
return await axios.put(
baseurl + params.db + '/_security',
{admins: {names: [], roles: []}, members: {names: [params.username], roles: []}},
{
headers: {
'Authorization': 'Basic ' + authToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
}
export default async (req: Request): Promise<Response> => {
console.log(req.method);
try {
if (req.method == 'OPTIONS') {
const origin = req.headers.get('Origin');
const headers = req.headers.get('Access-Control-Request-Headers');
console.log(origin);
return new Response(
'OK',
{
headers: {
'Allow': 'POST',
'Max-Age': '30',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Origin': origin ? origin : 'https://cameras.immersiveidea.com',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': headers ? headers : 'Content-Type'
},
status: 200
});
}
} catch (err) {
return new Response(
JSON.stringify(err),
{
headers: {
'Allow': 'POST',
'Max-Age': '30',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Origin': origin ? origin : 'https://cameras.immersiveidea.com',
'Access-Control-Allow-Credentials': 'true'
},
status: 500
});
}
try {
const params = JSON.parse(await req.text());
console.log(params);
const createUserResponse = await createUser(params);
console.log(createUserResponse.status);
if (createUserResponse.status != 201 && createUserResponse.status != 200) {
throw new Error('Could not create User');
}
const exists = await checkIfDbExists(params);
switch (exists) {
case Access.ALLOWED:
console.log('Allowed');
return new Response('OK', {status: 200});
case Access.DENIED:
console.log('Denied');
return new Response('Denied', {status: 401});
case Access.MISSING:
console.log('Creating Missing DB');
const createDbResponse = await createDB(params);
if (createDbResponse.status != 201) {
throw new Error('Could not create DB');
}
}
const authorizeUserResponse = await authorizeUser(params);
if (authorizeUserResponse.status != 200) {
throw new Error('could not authorize user');
}
const origin = req.headers.get('origin');
console.log(origin);
return new Response(
'OK',
{
headers: [
['Content-Type', 'application/json'],
['Access-Control-Allow-Origin', origin],
['Access-Control-Allow-Credentials', 'true']
],
status: 200
}
)
} catch (err) {
console.log(err);
const response = {err: err};
return new Response('Error',
{status: 500}
)
}
}

View File

@ -1,22 +0,0 @@
import {Handler, HandlerContext, HandlerEvent} from "@netlify/functions";
import axios from 'axios';
export const handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {
try {
const response = await axios.post('https://api.assemblyai.com/v2/realtime/token', // use account token to get a temp user token
{expires_in: 3600}, // can set a TTL timer in seconds.
{headers: {authorization: process.env.VOICE_TOKEN}});
const data = await response.data;
return {
headers: {'Content-Type': 'application/json'},
statusCode: 200,
body: JSON.stringify(data)
}
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify(error)
}
}
};

48
newrelic.cjs Normal file
View File

@ -0,0 +1,48 @@
'use strict'
// Load .env.local first (has the secrets), then .env as fallback
require('dotenv').config({ path: '.env.local' });
require('dotenv').config();
/**
* New Relic Node.js APM Configuration
*
* This file configures the New Relic agent for backend monitoring.
* Requires NEW_RELIC_LICENSE_KEY environment variable to be set.
*
* Distributed tracing is enabled to correlate with browser agent traces.
*/
exports.config = {
app_name: ['dasfad-backend'],
license_key: process.env.NEW_RELIC_LICENSE_KEY,
distributed_tracing: {
enabled: true
},
logging: {
level: 'info'
},
application_logging: {
enabled: true,
forwarding: {
enabled: true,
max_samples_stored: 10000
},
local_decorating: {
enabled: true
}
},
allow_all_headers: true,
attributes: {
exclude: [
'request.headers.cookie',
'request.headers.authorization',
'request.headers.proxyAuthorization',
'request.headers.setCookie*',
'request.headers.x*',
'response.headers.cookie',
'response.headers.authorization',
'response.headers.proxyAuthorization',
'response.headers.setCookie*',
'response.headers.x*'
]
}
}

1665
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,18 @@
{
"name": "immersive",
"private": true,
"version": "0.0.8-45",
"version": "0.0.8-48",
"type": "module",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"dev": "node server.js",
"dev": "node -r newrelic server.js",
"test": "vitest",
"build": "node versionBump.js && vite build",
"start": "NODE_ENV=production node server.js",
"start:api": "API_ONLY=true node server.js",
"start": "NODE_ENV=production node -r newrelic server.js",
"start:api": "API_ONLY=true node -r newrelic server.js",
"socket": "node server/server.js",
"serverBuild": "cd server && tsc",
"havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps"
@ -33,6 +33,7 @@
"@mantine/form": "^7.17.8",
"@mantine/hooks": "^7.17.8",
"@maptiler/client": "1.8.1",
"@newrelic/browser-agent": "^1.306.0",
"@picovoice/cobra-web": "^2.0.3",
"@picovoice/eagle-web": "^1.0.0",
"@picovoice/web-voice-processor": "^4.0.9",
@ -40,7 +41,6 @@
"@types/node": "^18.14.0",
"@types/react": "^18.2.72",
"@types/react-dom": "^18.2.22",
"@newrelic/browser-agent": "^1.306.0",
"axios": "^1.10.0",
"canvas-hypertxt": "1.0.3",
"cors": "^2.8.5",
@ -54,6 +54,7 @@
"leveldown": "^6.1.1",
"loglevel": "^1.9.1",
"meaningful-string": "^1.4.0",
"newrelic": "^13.9.1",
"peer-lite": "2.0.2",
"pouchdb": "^8.0.1",
"pouchdb-adapter-leveldb": "^9.0.0",

View File

@ -2,6 +2,7 @@ import express from "express";
import ViteExpress from "vite-express";
import cors from "cors";
import dotenv from "dotenv";
import newrelic from "newrelic";
import apiRoutes from "./server/api/index.js";
import { pouchApp, PouchDB } from "./server/services/databaseService.js";
import { dbAuthMiddleware } from "./server/middleware/dbAuth.js";
@ -10,6 +11,46 @@ import { dbAuthMiddleware } from "./server/middleware/dbAuth.js";
dotenv.config({ path: '.env.local' });
dotenv.config();
// Console shim to forward logs to New Relic while preserving local output
const originalConsole = {
log: console.log.bind(console),
error: console.error.bind(console),
warn: console.warn.bind(console),
info: console.info.bind(console)
};
function forwardToNewRelic(level, args) {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ');
newrelic.recordLogEvent({
message,
level,
timestamp: Date.now()
});
}
console.log = (...args) => {
forwardToNewRelic('info', args);
originalConsole.log(...args);
};
console.error = (...args) => {
forwardToNewRelic('error', args);
originalConsole.error(...args);
};
console.warn = (...args) => {
forwardToNewRelic('warn', args);
originalConsole.warn(...args);
};
console.info = (...args) => {
forwardToNewRelic('info', args);
originalConsole.info(...args);
};
const app = express();
// CORS configuration for split deployment

View File

@ -25,10 +25,12 @@ export function buildRig(xr: WebXRDefaultExperience): Mesh {
});
for (const cam of scene.cameras) {
cam.parent = cameratransform;
cam.position = new Vector3(0, 1.6, 0); // Reset to local position above platform
}
scene.onActiveCameraChanged.add(() => {
for (const cam of scene.cameras) {
cam.parent = cameratransform;
cam.position = new Vector3(0, 1.6, 0); // Reset to local position above platform
}
});

View File

@ -227,13 +227,24 @@ export class DiagramObject {
if (this._labelBack) {
this._labelBack.dispose();
}
if (this._diagramEntity.text != value) {
const textChanged = this._diagramEntity.text != value;
// Update the entity text FIRST (before notifying observers)
this._diagramEntity.text = value;
// Also update mesh metadata to keep in sync with diagramEntity getter
if (this._mesh && this._mesh.metadata) {
this._mesh.metadata.text = value;
}
// THEN notify observers with the UPDATED entity
if (textChanged) {
this._eventObservable.notifyObservers({
type: DiagramEventType.MODIFY,
entity: this._diagramEntity
}, DiagramEventObserverMask.TO_DB);
}
this._diagramEntity.text = value;
this._label = createLabel(value);
this._label.parent = this._baseTransform;
this._labelBack = new InstancedMesh('labelBack' + value, (this._label as Mesh));
@ -303,7 +314,7 @@ export class DiagramObject {
this._label.rotation.y = 0;
this._labelBack.rotation.y = Math.PI; // Back face still needs to be flipped
}
this._labelBack.position.z = 0.001;
this._labelBack.position.z = 0.005;
} else {
// Standard object labels - convert world space to parent's local space
// This accounts for mesh scaling, which is not included in boundingBox.maximum
@ -315,7 +326,7 @@ export class DiagramObject {
temp.dispose();
this._label.position.y = y + 0.06;
this._labelBack.rotation.y = Math.PI;
this._labelBack.position.z = 0.001;
this._labelBack.position.z = 0.005;
}
}
}

View File

@ -16,7 +16,7 @@ import {
} from '@babylonjs/core';
import log from 'loglevel';
import { HandleState, CORNER_POSITIONS, FACE_POSITIONS} from './enums';
import { CORNER_POSITIONS, FACE_POSITIONS} from './enums';
import { ResizeGizmoEvent } from './types';
/**
@ -108,6 +108,10 @@ export class ResizeGizmo {
* Create corner and face handles
*/
private createHandles(): void {
// Ensure world matrix and bounding info are current
this._targetMesh.computeWorldMatrix(true);
this._targetMesh.refreshBoundingInfo({});
// Get bounding box for positioning
const targetBoundingInfo = this._targetMesh.getBoundingInfo();
const boundingBox = targetBoundingInfo.boundingBox;
@ -116,10 +120,10 @@ export class ResizeGizmo {
const innerCorners = boundingBox.vectorsWorld;
const worldMatrix = this._targetMesh.getWorldMatrix();
// Calculate handle size once (based on corner distance)
const handleSize = innerCorners[0].subtract(bboxCenter).length() * .5;
// Unit size - actual visual size controlled by updateHandleScaling()
const handleSize = 1.0;
// Create corner handles
// Create corner handles - positioned directly at bounding box corners
CORNER_POSITIONS.forEach((cornerDef, index) => {
const cornerPos = innerCorners[index];
@ -129,10 +133,8 @@ export class ResizeGizmo {
this._utilityLayer.utilityLayerScene
);
// Position outward from center so handle corner touches bounding box corner
const direction = cornerPos.subtract(bboxCenter).normalize();
const offset = direction.scale(handleSize * Math.sqrt(3) / 2);
handleMesh.position = cornerPos.add(offset);
// Position handle at the corner (scaling will make it small)
handleMesh.position = cornerPos.clone();
handleMesh.rotationQuaternion = this._targetMesh.absoluteRotationQuaternion;
handleMesh.material = this._handleMaterial;
handleMesh.isPickable = true;
@ -156,10 +158,8 @@ export class ResizeGizmo {
this._utilityLayer.utilityLayerScene
);
// Position outward from center so handle touches face center
const direction = faceCenterWorld.subtract(bboxCenter).normalize();
const offset = direction.scale(handleSize * Math.sqrt(3) / 2);
handleMesh.position = faceCenterWorld.add(offset);
// Position handle at the face center (scaling will make it small)
handleMesh.position = faceCenterWorld.clone();
handleMesh.rotationQuaternion = this._targetMesh.absoluteRotationQuaternion;
handleMesh.material = this._handleMaterial;
handleMesh.isPickable = true;
@ -197,15 +197,20 @@ export class ResizeGizmo {
}
/**
* Update handle scaling based on camera distance for consistent visual size
* Update handle scaling based on camera distance for constant screen size.
* Handles will appear the same visual size regardless of distance.
*/
private updateHandleScaling(): void {
const camera = this._scene.activeCamera;
if (!camera) return;
// Target angular size - tune this for desired visual size
// 0.03 means handles appear ~3cm at 1m distance, ~6cm at 2m, etc.
const targetAngularSize = 0.03;
for (const handle of this._handles) {
const distance = Vector3.Distance(camera.globalPosition, handle.position);
const scaleFactor = distance; // Adjust multiplier to control visual size
const scaleFactor = distance * targetAngularSize;
handle.scaling = new Vector3(scaleFactor, scaleFactor, scaleFactor);
}
}
@ -482,27 +487,24 @@ export class ResizeGizmo {
* Update handle positions and sizes to match current target mesh bounding box
*/
private updateHandleTransforms(): void {
// Ensure world matrix and bounding info are current
this._targetMesh.computeWorldMatrix(true);
this._targetMesh.refreshBoundingInfo({});
const targetBoundingInfo = this._targetMesh.getBoundingInfo();
const boundingBox = targetBoundingInfo.boundingBox;
const bboxCenter = boundingBox.centerWorld;
const extents = boundingBox.extendSize;
const innerCorners = boundingBox.vectorsWorld;
const worldMatrix = this._targetMesh.getWorldMatrix();
// Recalculate handle size based on new bounding box
const newHandleSize = innerCorners[0].subtract(bboxCenter).length() * .2;
let handleIndex = 0;
// Update corner handles (first 8 handles)
// Update corner handles (first 8 handles) - position at corners
for (let i = 0; i < CORNER_POSITIONS.length; i++) {
const handle = this._handles[handleIndex];
const cornerPos = innerCorners[i];
// Update position
const direction = cornerPos.subtract(bboxCenter).normalize();
const offset = direction.scale(newHandleSize * Math.sqrt(3) / 2);
handle.position = cornerPos.add(offset);
handle.position = cornerPos.clone();
handle.rotationQuaternion = this._targetMesh.absoluteRotationQuaternion;
handleIndex++;
@ -520,10 +522,7 @@ export class ResizeGizmo {
);
const faceCenterWorld = Vector3.TransformCoordinates(localFacePos, worldMatrix);
// Update position
const direction = faceCenterWorld.subtract(bboxCenter).normalize();
const offset = direction.scale(newHandleSize * Math.sqrt(3) / 2);
handle.position = faceCenterWorld.add(offset);
handle.position = faceCenterWorld.clone();
handle.rotationQuaternion = this._targetMesh.absoluteRotationQuaternion;
handleIndex++;

View File

@ -61,20 +61,25 @@ export class PouchData {
if (info.direction === 'pull' && info.change && info.change.docs) {
info.change.docs.forEach((doc) => {
if (doc._deleted) {
this.onDBEntityRemoveObservable.notifyObservers(doc);
} else if (doc.id && doc.id !== 'metadata') {
this.onDBEntityUpdateObservable.notifyObservers(doc);
// PouchDB deleted documents only have _id, not our custom id field
// Create an entity with id from _id for the remove handler
const entity = { id: doc._id, ...doc };
this.onDBEntityRemoveObservable.notifyObservers(entity);
} else if ((doc.id || doc._id) && (doc.id || doc._id) !== 'metadata') {
// Accept both id and _id for compatibility
const entity = doc.id ? doc : { id: doc._id, ...doc };
this.onDBEntityUpdateObservable.notifyObservers(entity);
}
});
}
})
.on('paused', (info) => {
this._logger.debug('[Sync] Paused - up to date');
this._logger.debug(`[Sync] Paused - up to date ${info}`);
})
.on('active', () => {
this._logger.debug('[Sync] Active - syncing');
})
.on('error', (err) => {
.on('error', (err: any) => {
this._logger.error('[Sync] Error:', err);
});
}
@ -197,52 +202,4 @@ export class PouchData {
this._logger.warn('CONFLICTS!', doc._conflicts);
}
}
/**
* Copy all documents from this database to a new public database.
* Used when sharing a local diagram to make it publicly accessible.
* @param newDbName - The name for the new public database
* @returns The URL path to the new public diagram
*/
public async copyToPublic(newDbName: string): Promise<string> {
this._logger.info(`[Copy] Starting copy to public-${newDbName}`);
// Create the remote public database
const remoteUrl = `${window.location.origin}/pouchdb/public-${newDbName}`;
const remoteDb = new PouchDB(remoteUrl);
try {
// Get all docs from local database
const allDocs = await this._db.allDocs({ include_docs: true });
this._logger.debug(`[Copy] Found ${allDocs.rows.length} documents to copy`);
// Copy each document to the remote database
for (const row of allDocs.rows) {
if (row.doc) {
// Remove PouchDB internal fields for clean insert
const { _rev, ...docWithoutRev } = row.doc;
try {
await remoteDb.put(docWithoutRev);
} catch (err) {
// Document might already exist if this is a retry
this._logger.warn(`[Copy] Failed to copy doc ${row.id}:`, err);
}
}
}
this._logger.info(`[Copy] Successfully copied ${allDocs.rows.length} documents`);
return `/db/public/${newDbName}`;
} catch (err) {
this._logger.error('[Copy] Error copying to public:', err);
throw err;
}
}
/**
* Get all documents in the database (for export/copy operations)
*/
public async getAllDocs(): Promise<any[]> {
const result = await this._db.allDocs({ include_docs: true });
return result.rows.map(row => row.doc).filter(doc => doc && doc.id !== 'metadata');
}
}

View File

@ -1,27 +0,0 @@
import axios from "axios";
import log from "loglevel";
export async function checkDb(localName: string, remoteDbName: string, password: string) {
const logger = log.getLogger('checkDb');
const dbs = await axios.get(import.meta.env.VITE_SYNCDB_ENDPOINT + 'list');
logger.debug(dbs.data);
if (dbs.data.indexOf(remoteDbName) == -1) {
logger.warn('sync target missing attempting to create');
const newdb = await axios.post(import.meta.env.VITE_CREATE_ENDPOINT,
{
"_id": "org.couchdb.user:" + localName,
"name": localName,
"password": password,
"roles": ["readers"],
"type": "user"
}
);
if (newdb.status == 201) {
logger.info('sync target created');
} else {
logger.warn('sync target not created', newdb);
return false;
}
}
return true;
}

View File

@ -1,17 +0,0 @@
export function hex_to_ascii(input) {
const hex = input.toString();
let output = '';
for (let n = 0; n < hex.length; n += 2) {
output += String.fromCharCode(parseInt(hex.substr(n, 2), 16));
}
return output;
}
export function ascii_to_hex(str) {
const arr1 = [];
for (let n = 0, l = str.length; n < l; n++) {
const hex = Number(str.charCodeAt(n)).toString(16);
arr1.push(hex);
}
return arr1.join('');
}

View File

@ -1,59 +0,0 @@
import log from "loglevel";
import {DiagramEntity, DiagramEntityType} from "../../diagram/types/diagramEntity";
import {Observable} from "@babylonjs/core";
import {Encryption} from "../encryption";
import {DiagramEventObserverMask} from "../../diagram/types/diagramEventObserverMask";
export async function syncDoc(info: any, onDBRemoveObservable: Observable<DiagramEntity>, onDBUpdateObservable: Observable<DiagramEntity>,
encryption: Encryption, key: string) {
const logger = log.getLogger('syncDoc');
logger.debug(info);
if (info.direction == 'pull') {
// @ts-ignore
const docs = info.change.docs;
let salt = null;
for (const doc of docs) {
if (doc.encrypted) {
if (salt != doc.encrypted.salt || (key && !encryption.ready)) {
await encryption.setPassword(key, doc.encrypted.salt);
salt = doc.encrypted.salt
}
const decrypted = await encryption.decryptToObject(doc.encrypted.encrypted, doc.encrypted.iv);
if (decrypted.type == DiagramEntityType.USER) {
//onUserObservable.notifyObservers(doc, -1);
} else {
logger.debug(decrypted);
if (doc._deleted) {
logger.debug('Delete', doc);
onDBRemoveObservable.notifyObservers({
id: doc._id,
template: decrypted.template,
type: doc.type
}, DiagramEventObserverMask.FROM_DB);
} else {
onDBUpdateObservable.notifyObservers(decrypted, DiagramEventObserverMask.FROM_DB);
}
}
} else {
if (doc.type == 'user') {
//onUserObservable.notifyObservers(doc, -1);
} else {
logger.debug(doc);
if (doc._deleted) {
logger.debug('Delete', doc);
onDBRemoveObservable.notifyObservers({
id: doc._id,
template: doc.template,
type: doc.type
}, DiagramEventObserverMask.FROM_DB);
} else {
if (doc.template) {
onDBUpdateObservable.notifyObservers(doc, DiagramEventObserverMask.FROM_DB);
}
}
}
}
}
}
}

View File

@ -1,166 +0,0 @@
/**
* DiagramEntity Integration Adapter for ResizeGizmo
* Bridges ResizeGizmo events to DiagramManager's persistence system
*
* This adapter lives in the integration layer to keep the ResizeGizmo
* system pure and reusable without diagram-specific dependencies.
*/
import { AbstractMesh } from "@babylonjs/core";
import { ResizeGizmoManager } from "../../gizmos/ResizeGizmo";
import { ResizeGizmoEvent } from "../../gizmos/ResizeGizmo";
/**
* Type definitions for DiagramManager integration (loosely coupled)
* These match the actual types in the codebase without importing them
*/
interface DiagramEntity {
id?: string;
template?: string;
position?: { x: number; y: number; z: number };
rotation?: { x: number; y: number; z: number };
scale?: { x: number; y: number; z: number };
[key: string]: any;
}
enum DiagramEventType {
MODIFY = "MODIFY"
}
interface DiagramEvent {
type: DiagramEventType;
entity: DiagramEntity;
}
enum DiagramEventObserverMask {
TO_DB = 2,
ALL = -1
}
interface DiagramEventNotifier {
notifyObservers(event: DiagramEvent, mask?: number): void;
}
interface DiagramManager {
onDiagramEventObservable: DiagramEventNotifier;
}
/**
* Converter function type for transforming BabylonJS meshes to DiagramEntities
*/
export type MeshToEntityConverter = (mesh: AbstractMesh) => DiagramEntity;
/**
* Adapter that connects ResizeGizmo to DiagramManager for persistence
* Uses dependency injection to remain loosely coupled from diagram internals
*
* @example
* ```typescript
* import { DiagramEntityAdapter } from './integration/gizmo';
* import { toDiagramEntity } from './diagram/functions/toDiagramEntity';
*
* // Create resize gizmo
* const gizmo = new ResizeGizmoManager(scene, {
* mode: ResizeGizmoMode.ALL
* });
*
* // Create adapter with injected converter
* const adapter = new DiagramEntityAdapter(
* gizmo,
* diagramManager,
* toDiagramEntity, // Injected dependency
* false // Don't persist on drag
* );
*
* // Now scale changes will automatically persist to database
* gizmo.attachToMesh(myDiagramMesh);
* ```
*/
export class DiagramEntityAdapter {
private _gizmo: ResizeGizmoManager;
private _diagramManager: DiagramManager;
private _meshConverter: MeshToEntityConverter;
private _persistOnDrag: boolean;
/**
* Create adapter
* @param gizmo ResizeGizmoManager instance
* @param diagramManager DiagramManager instance (or object with onDiagramEventObservable)
* @param meshConverter Function to convert BabylonJS mesh to DiagramEntity (injected dependency)
* @param persistOnDrag If true, persist on every drag update (can be expensive). If false, only persist on scale end.
*/
constructor(
gizmo: ResizeGizmoManager,
diagramManager: DiagramManager,
meshConverter: MeshToEntityConverter,
persistOnDrag: boolean = false
) {
this._gizmo = gizmo;
this._diagramManager = diagramManager;
this._meshConverter = meshConverter;
this._persistOnDrag = persistOnDrag;
this.setupEventListeners();
}
/**
* Setup event listeners
*/
private setupEventListeners(): void {
// Persist on scale end (always)
this._gizmo.onScaleEnd((event) => {
this.persistScaleChange(event);
});
// Optionally persist on drag
if (this._persistOnDrag) {
this._gizmo.onScaleDrag((event) => {
this.persistScaleChange(event);
});
}
}
/**
* Persist scale change to DiagramManager
*/
private persistScaleChange(event: ResizeGizmoEvent): void {
const mesh = event.mesh;
// Convert mesh to DiagramEntity using injected converter
// This properly extracts color from material and all other properties
const entity = this._meshConverter(mesh);
// Notify DiagramManager
this._diagramManager.onDiagramEventObservable.notifyObservers(
{
type: DiagramEventType.MODIFY,
entity
},
DiagramEventObserverMask.TO_DB
);
}
/**
* Enable/disable drag persistence
*/
setPersistOnDrag(enabled: boolean): void {
if (this._persistOnDrag === enabled) {
return;
}
this._persistOnDrag = enabled;
// Re-setup listeners
// Note: In a production implementation, you'd want to properly remove/add observers
// For now, this is a simplified version
console.warn("[DiagramEntityAdapter] Changing persistOnDrag at runtime may cause duplicate events");
}
/**
* Get persist on drag setting
*/
getPersistOnDrag(): boolean {
return this._persistOnDrag;
}
}

View File

@ -1,6 +0,0 @@
/**
* Gizmo Integration Layer
* Adapters for integrating gizmo systems with diagram persistence
*/
export { DiagramEntityAdapter, type MeshToEntityConverter } from './DiagramEntityAdapter';

View File

@ -84,30 +84,6 @@ export class AnimatedLineTexture {
return texture;
}
/**
* Removes a texture from the animation set when disposed
* WARNING: Do NOT call this on cached textures! Only for non-cached textures.
* Cached textures are shared across multiple connections.
* Use ClearCache() to dispose cached textures properly.
* @param texture - The texture to stop animating
*/
public static DisposeTexture(texture: Texture): void {
// Safety check: prevent disposing cached textures (they're shared!)
for (const [color, cachedTexture] of this._coloredTextureCache.entries()) {
if (cachedTexture === texture) {
console.error(
`AnimatedLineTexture.DisposeTexture: Attempted to dispose cached texture ` +
`"${texture.name}" (color: ${color}). This will break texture sharing! ` +
`Cached textures should not be disposed individually. Use ClearCache() instead.`
);
return; // Don't dispose - it's shared across multiple connections
}
}
// Only dispose non-cached textures
this._animatedTextures.delete(texture);
texture.dispose();
}
/**
* Preload textures for common colors to prevent first-render stutter
@ -119,27 +95,5 @@ export class AnimatedLineTexture {
});
}
/**
* Clear the texture cache and dispose all cached textures
* Use with caution - only call when no connections are using these textures
*/
public static ClearCache(): void {
this._coloredTextureCache.forEach((texture) => {
this._animatedTextures.delete(texture);
texture.dispose();
});
this._coloredTextureCache.clear();
}
/**
* Get cache statistics for debugging
* @returns Object with cache stats
*/
public static GetCacheStats(): { cachedColors: number; totalAnimatedTextures: number; colors: string[] } {
return {
cachedColors: this._coloredTextureCache.size,
totalAnimatedTextures: this._animatedTextures.size,
colors: Array.from(this._coloredTextureCache.keys())
};
}
}

View File

@ -149,32 +149,6 @@ export class AppConfig {
this.save();
}
/**
* Remove handle configuration by ID
* @param id Handle ID to remove
*/
public removeHandleConfig(id: string) {
if (!this._currentConfig.handles) {
return;
}
const initialLength = this._currentConfig.handles.length;
this._currentConfig.handles = this._currentConfig.handles.filter(h => h.id !== id);
if (this._currentConfig.handles.length < initialLength) {
this._logger.debug(`Removed handle config for ${id}`);
this.save();
}
}
/**
* Get all handle configurations
* @returns Array of all HandleConfig objects
*/
public getAllHandleConfigs(): HandleConfig[] {
return this._currentConfig.handles || [];
}
private save() {
localStorage.setItem('appConfig', JSON.stringify(this._currentConfig));
this.onConfigChangedObservable.notifyObservers(this._currentConfig, -1);

View File

@ -4,13 +4,11 @@ import {
Material,
MeshBuilder,
Observable,
PBRMaterial,
PhysicsAggregate,
PhysicsShapeType,
PointsCloudSystem,
Scene,
Sound,
Texture,
TransformNode,
Vector3
} from "@babylonjs/core";

View File

@ -1,14 +1,9 @@
import {HavokPlugin, Quaternion, Scene, Vector3} from "@babylonjs/core";
import {HavokPlugin, Scene, Vector3} from "@babylonjs/core";
import HavokPhysics from "@babylonjs/havok";
import {snapGridVal} from "./functions/snapGridVal";
import {snapRotateVal} from "./functions/snapRotateVal";
import {isDiagramEntity} from "../diagram/functions/isDiagramEntity";
import {appConfigInstance} from "./appConfig";
export class CustomPhysics {
private readonly scene: Scene;
constructor(scene: Scene) {
this.scene = scene;
}
@ -17,47 +12,6 @@ export class CustomPhysics {
const havok = await HavokPhysics();
const havokPlugin = new HavokPlugin(true, havok);
const scene = this.scene;
const physicsEnable = scene.enablePhysics(new Vector3(0, -9.8, 0), havokPlugin);
scene.collisionsEnabled = true;
scene.onAfterPhysicsObservable.add(() => {
const config = appConfigInstance.current;
scene.meshes.forEach((mesh) => {
if (isDiagramEntity(mesh) && mesh.physicsBody) {
const body = mesh.physicsBody;
const linearVelocity = new Vector3();
body.getLinearVelocityToRef(linearVelocity);
if (linearVelocity.length() < .1) {
body.disablePreStep = false;
// Apply location snap if enabled
if (config.locationSnap > 0) {
const pos: Vector3 = body.getObjectCenterWorld();
const val: Vector3 = snapGridVal(pos, config.locationSnap);
body.transformNode.position.set(val.x, val.y, val.z);
}
// Apply rotation snap if enabled
if (config.rotateSnap > 0) {
const rot: Quaternion =
Quaternion.FromEulerVector(
snapRotateVal(body.transformNode.rotationQuaternion.toEulerAngles(),
config.rotateSnap));
body.transformNode.rotationQuaternion.set(
rot.x, rot.y, rot.z, rot.w
);
}
scene.onAfterRenderObservable.addOnce(() => {
body.disablePreStep = true;
});
}
}
});
}
);
scene.enablePhysics(new Vector3(0, -9.8, 0), havokPlugin);
}
}

View File

@ -1,8 +1,3 @@
/**
* Device detection utilities for VR capability and device type
*/
import {Scene, WebXRDefaultExperience} from "@babylonjs/core";
export interface DeviceCapabilities {
isVRCapable: boolean;
isMobileVR: boolean;
@ -64,5 +59,4 @@ export async function getDeviceCapabilities(): Promise<DeviceCapabilities> {
isDesktop,
deviceType
};
}
}

View File

@ -11,33 +11,6 @@ export function getPath(): string {
return null;
}
/**
* Check if the current path is a local database (no sync)
* Local paths: /db/local/:db
*/
export function isLocalPath(): boolean {
const path = window.location.pathname.split('/');
return path.length >= 3 && path[1] === 'db' && path[2] === 'local';
}
/**
* Check if the current path is a public database
* Public paths: /db/public/:db
*/
export function isPublicPath(): boolean {
const path = window.location.pathname.split('/');
return path.length >= 3 && path[1] === 'db' && path[2] === 'public';
}
/**
* Check if the current path is a private database
* Private paths: /db/private/:db
*/
export function isPrivatePath(): boolean {
const path = window.location.pathname.split('/');
return path.length >= 3 && path[1] === 'db' && path[2] === 'private';
}
/**
* Get the database type from the current path
*/

View File

@ -157,8 +157,6 @@ function positionComponentsRelativeToCamera(scene: Scene, diagramManager: Diagra
logger.info('Horizontal left:', horizontalLeft);
logger.info('Platform world position:', platform.getAbsolutePosition());
// Position toolbox: Camera-relative positioning disabled to respect default/saved positions
// Handles now use their configured defaults or saved localStorage positions
const toolbox = diagramManager.diagramMenuManager.toolbox;
if (toolbox && toolbox.handleMesh) {
logger.info('Toolbox handleMesh using default/saved position:', {
@ -166,64 +164,13 @@ function positionComponentsRelativeToCamera(scene: Scene, diagramManager: Diagra
absolutePosition: toolbox.handleMesh.getAbsolutePosition().clone(),
rotation: toolbox.handleMesh.rotation.clone()
});
// Camera-relative positioning commented out - handles use their own defaults
/*
// Position at 45 degrees to the left, 0.45m away, slightly below eye level
// NOTE: User faces -Z direction by design, so negate forward offset
const forwardOffset = horizontalForward.scale(-0.3);
const leftOffset = horizontalLeft.scale(0.35);
const toolboxWorldPos = cameraWorldPos.add(forwardOffset).add(leftOffset);
toolboxWorldPos.y = cameraWorldPos.y - 0.3; // Below eye level
logger.info('Calculated toolbox world position:', toolboxWorldPos);
logger.info('Forward offset:', forwardOffset);
logger.info('Left offset:', leftOffset);
const toolboxLocalPos = Vector3.TransformCoordinates(toolboxWorldPos, platform.getWorldMatrix().invert());
logger.info('Calculated toolbox local position:', toolboxLocalPos);
toolbox.handleMesh.position = toolboxLocalPos;
// Orient toolbox to face the user
const toolboxToCamera = cameraWorldPos.subtract(toolboxWorldPos).normalize();
const toolboxYaw = Math.atan2(toolboxToCamera.x, toolboxToCamera.z);
toolbox.handleMesh.rotation.y = toolboxYaw;
logger.info('Toolbox handleMesh AFTER positioning:', {
position: toolbox.handleMesh.position.clone(),
absolutePosition: toolbox.handleMesh.getAbsolutePosition().clone(),
rotation: toolbox.handleMesh.rotation.clone()
});
*/
}
// Position input text view: Camera-relative positioning disabled to respect default/saved positions
// Handles now use their configured defaults or saved localStorage positions
const inputTextView = diagramManager.diagramMenuManager['_inputTextView'];
if (inputTextView && inputTextView.handleMesh) {
logger.info('InputTextView handleMesh using default/saved position:', {
position: inputTextView.handleMesh.position.clone(),
absolutePosition: inputTextView.handleMesh.getAbsolutePosition().clone()
});
// Camera-relative positioning commented out - handles use their own defaults
/*
// NOTE: User faces -Z direction by design, so negate forward offset
const inputWorldPos = cameraWorldPos.add(horizontalForward.scale(-0.5));
inputWorldPos.y = cameraWorldPos.y - 0.4; // Below eye level
logger.info('Calculated input world position:', inputWorldPos);
const inputLocalPos = Vector3.TransformCoordinates(inputWorldPos, platform.getWorldMatrix().invert());
logger.info('Calculated input local position:', inputLocalPos);
inputTextView.handleMesh.position = inputLocalPos;
logger.info('InputTextView handleMesh AFTER positioning:', {
position: inputTextView.handleMesh.position.clone(),
absolutePosition: inputTextView.handleMesh.getAbsolutePosition().clone()
});
*/
}
}

View File

@ -23,23 +23,6 @@ export function addSceneInspector() {
});
});
}
/*import("@babylonjs/core/Debug").then(() => {
import("@babylonjs/inspector").then(() => {
const web = document.querySelector('#webApp');
if (scene.debugLayer.isVisible()) {
if (web) {
(web as HTMLDivElement).style.display = 'block';
}
scene.debugLayer.hide();
} else {
scene.debugLayer.show();
if (web) {
(web as HTMLDivElement).style.display = 'none';
}
}
});
});*/
}
});
}

View File

@ -1,5 +1,4 @@
import {Color3, DynamicTexture, HemisphericLight, PointLight, Scene, StandardMaterial, Vector3} from "@babylonjs/core";
import {DefaultScene} from "../defaultScene";
import {RenderingMode} from "./renderingMode";
export class LightmapGenerator {