Compare commits
5 Commits
960c64984e
...
421cd97fe9
| Author | SHA1 | Date | |
|---|---|---|---|
| 421cd97fe9 | |||
| 58959fe347 | |||
| d9cd0692b5 | |||
| 3155cc930f | |||
| 8c2b7f9c7d |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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
48
newrelic.cjs
Normal 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
1665
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -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",
|
||||
|
||||
41
server.js
41
server.js
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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++;
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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('');
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Gizmo Integration Layer
|
||||
* Adapters for integrating gizmo systems with diagram persistence
|
||||
*/
|
||||
|
||||
export { DiagramEntityAdapter, type MeshToEntityConverter } from './DiagramEntityAdapter';
|
||||
@ -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())
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -4,13 +4,11 @@ import {
|
||||
Material,
|
||||
MeshBuilder,
|
||||
Observable,
|
||||
PBRMaterial,
|
||||
PhysicsAggregate,
|
||||
PhysicsShapeType,
|
||||
PointsCloudSystem,
|
||||
Scene,
|
||||
Sound,
|
||||
Texture,
|
||||
TransformNode,
|
||||
Vector3
|
||||
} from "@babylonjs/core";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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()
|
||||
});
|
||||
*/
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
});
|
||||
});*/
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user