Compare commits

..

No commits in common. "main" and "deepdiagram" have entirely different histories.

216 changed files with 4914 additions and 25677 deletions

View File

@ -1,75 +0,0 @@
name: Build and Deploy
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: linux_amd64
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
timeout-minutes: 5
- name: Build Front End
run: npm run build
timeout-minutes: 10
env:
NODE_OPTIONS: '--max-old-space-size=4096'
VITE_AUTH0_CLIENTID: ${{ secrets.VITE_AUTH0_CLIENTID }}
VITE_AUTH0_DOMAIN: ${{ secrets.VITE_AUTH0_DOMAIN }}
- name: Stop Service
run: |
sudo rc-service immersive stop || true
- name: Deploy to /opt/immersive
run: |
# Ensure group write so we can delete old files
sudo chmod -R g+w /opt/immersive || true
# Remove old files except data directory and env file
find /opt/immersive -mindepth 1 -maxdepth 1 ! -name 'data' ! -name '.env.production' -exec rm -rf {} +
# Copy built files to target
cp -r . /opt/immersive/
# Remove unnecessary directories
rm -rf /opt/immersive/.git /opt/immersive/.github
# Set permissions on start.sh and ensure group write for future deploys
chmod +x /opt/immersive/start.sh
sudo chmod -R g+w /opt/immersive
# Set ownership to immersive user
sudo chown -R immersive:immersive /opt/immersive
- name: Create Environment File
env:
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
echo "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}" >> /opt/immersive/.env.production
echo "CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID}" >> /opt/immersive/.env.production
echo "CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}" >> /opt/immersive/.env.production
echo "NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY}" >> /opt/immersive/.env.production
# Secure the environment file
sudo chown immersive:immersive /opt/immersive/.env.production
sudo chmod 600 /opt/immersive/.env.production
- name: Start Service
run: |
sudo rc-service immersive start

View File

@ -2,9 +2,9 @@ name: Node.js CI
on: on:
push: push:
branches: [ "deepdiagram" ] branches: [ "main" ]
pull_request: pull_request:
branches: [ "deepdiagram" ] branches: [ "main" ]
jobs: jobs:
build: build:

View File

@ -5,9 +5,9 @@ name: Node.js Github Side
on: on:
push: push:
branches: [ "deepdiagram" ] branches: [ "main" ]
pull_request: pull_request:
branches: [ "deepdiagram" ] branches: [ "main" ]
jobs: jobs:
build: build:

2
.gitignore vendored
View File

@ -25,5 +25,3 @@ dist-ssr
# Local Netlify folder # Local Netlify folder
.netlify .netlify
/data/
/.env.production

View File

@ -1,280 +0,0 @@
# Alpine Linux Service Setup
This guide covers installing and running Immersive as a service on Alpine Linux using OpenRC.
## Prerequisites
```bash
# Update packages
apk update
# Install Node.js 18+ and npm
apk add nodejs npm
# Install build dependencies (required for native modules like leveldown)
apk add python3 make g++ git
# Verify Node version (must be >= 18)
node --version
```
## Create Service User
Create a dedicated user to run the service (security best practice):
```bash
# Create immersive group and user (no login shell, no home directory)
addgroup -S immersive
adduser -S -G immersive -H -s /sbin/nologin immersive
# Create directories with proper ownership
mkdir -p /opt/immersive
mkdir -p /var/log/immersive
mkdir -p /var/run/immersive
chown -R immersive:immersive /opt/immersive
chown -R immersive:immersive /var/log/immersive
chown -R immersive:immersive /var/run/immersive
```
## Installation
```bash
# Create application directory
mkdir -p /opt/immersive
cd /opt/immersive
# Clone or copy the application
git clone <your-repo-url> .
# OR copy files manually
# Install dependencies
npm ci --production=false
# Build the application
NODE_OPTIONS='--max-old-space-size=4096' npm run build
# Copy Havok physics WASM (if not already done by build)
npm run havok
# Create data directory for PouchDB
mkdir -p /opt/immersive/data
# Set ownership to immersive user
chown -R immersive:immersive /opt/immersive
```
## Start Script
The `start.sh` script is included in the repository. After deployment, ensure it's executable:
```bash
chmod +x /opt/immersive/start.sh
```
The script sets up the environment and starts the Node.js server, logging output to `/var/log/immersive/`.
## OpenRC Service
Create `/etc/init.d/immersive`:
```bash
#!/sbin/openrc-run
name="immersive"
description="Immersive WebXR Diagramming Application"
command="/opt/immersive/start.sh"
command_user="immersive:immersive"
command_background="yes"
pidfile="/var/run/immersive/immersive.pid"
directory="/opt/immersive"
output_log="/var/log/immersive/app.log"
error_log="/var/log/immersive/error.log"
depend() {
need net
after firewall
}
start_pre() {
checkpath --directory --owner immersive:immersive --mode 0755 /var/log/immersive
checkpath --directory --owner immersive:immersive --mode 0755 /var/run/immersive
checkpath --file --owner immersive:immersive --mode 0644 /var/log/immersive/app.log
checkpath --file --owner immersive:immersive --mode 0644 /var/log/immersive/error.log
}
```
Make it executable and enable:
```bash
chmod +x /etc/init.d/immersive
rc-update add immersive default
```
## Service Management
```bash
# Start the service
rc-service immersive start
# Stop the service
rc-service immersive stop
# Restart the service
rc-service immersive restart
# Check status
rc-service immersive status
# View logs
tail -f /var/log/immersive/app.log
tail -f /var/log/immersive/error.log
```
## Log Rotation
Create `/etc/logrotate.d/immersive`:
```
/var/log/immersive/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0644 immersive immersive
postrotate
rc-service immersive restart > /dev/null 2>&1 || true
endscript
}
```
Install logrotate if not present:
```bash
apk add logrotate
```
## Environment Variables
Create `/opt/immersive/.env.production` for production settings:
```bash
# Server
NODE_ENV=production
PORT=3001
# Auth0 (if using authentication)
# VITE_AUTH0_DOMAIN=your-domain.auth0.com
# VITE_AUTH0_CLIENT_ID=your-client-id
# Database sync endpoint (optional)
# VITE_SYNCDB_ENDPOINT=https://your-couchdb-server.com
```
## Firewall (if using iptables)
```bash
# Allow port 3001
iptables -A INPUT -p tcp --dport 3001 -j ACCEPT
# Save rules
rc-service iptables save
```
## Reverse Proxy (Optional)
If using nginx as a reverse proxy:
```bash
apk add nginx
```
Create `/etc/nginx/http.d/immersive.conf`:
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
```
Enable and start nginx:
```bash
rc-update add nginx default
rc-service nginx start
```
## Gitea CI/CD Runner (Optional)
If using a Gitea Actions runner to deploy, grant the runner user write access to `/opt/immersive`:
```bash
# Add gitea-runner to immersive group
adduser gitea-runner immersive
# Set group write permissions on /opt/immersive
chmod -R g+w /opt/immersive
# Ensure new files inherit group ownership
chmod g+s /opt/immersive
# Allow runner to manage the service
# Add to /etc/sudoers.d/gitea-runner:
echo 'gitea-runner ALL=(ALL) NOPASSWD: /sbin/rc-service immersive *' > /etc/sudoers.d/gitea-runner
echo 'gitea-runner ALL=(ALL) NOPASSWD: /bin/chown -R immersive\:immersive /opt/immersive' >> /etc/sudoers.d/gitea-runner
chmod 440 /etc/sudoers.d/gitea-runner
```
The GitHub Actions workflow in `.github/workflows/build.yml` will handle deployment automatically on push to main.
## Troubleshooting
**Service fails to start:**
```bash
# Check logs
cat /var/log/immersive/error.log
# Run manually as immersive user to see errors
su -s /bin/sh immersive -c "cd /opt/immersive && NODE_ENV=production node server.js"
```
**Native module errors (leveldown):**
```bash
# Rebuild native modules
cd /opt/immersive
npm rebuild leveldown
```
**Permission issues:**
```bash
# Ensure proper ownership (must be immersive user)
chown -R immersive:immersive /opt/immersive
chown -R immersive:immersive /var/log/immersive
chown -R immersive:immersive /var/run/immersive
chmod -R 755 /opt/immersive
```
**Port already in use:**
```bash
# Find process using port 3001
lsof -i :3001
# Or
netstat -tlnp | grep 3001
```

137
CLAUDE.md
View File

@ -1,137 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is "immersive" - a WebXR/VR diagramming application built with BabylonJS and React. It allows users to create and interact with 3D diagrams in both standard web browsers and VR environments, with real-time collaboration via PouchDB sync.
## Build and Development Commands
### Development
- `npm run dev` - Start Vite dev server on port 3001 (DO NOT USE per user instructions)
- `npm run build` - Build production bundle (includes version bump)
- `npm run preview` - Preview production build on port 3001
- `npm test` - Run tests with Vitest
- `npm run socket` - Start WebSocket server for collaboration (port 8080)
- `npm run serverBuild` - Compile TypeScript server code
- `npm run havok` - Copy Havok physics WASM files to Vite deps
### Testing
- Run all tests: `npm test`
- No single test command is configured; tests use Vitest
## Architecture
### Core Technologies
- **BabylonJS 8.x**: 3D engine with WebXR support and Havok physics
- **React + Mantine**: UI framework for 2D interface and settings
- **PouchDB**: Client-side database with CouchDB sync for collaboration
- **Auth0**: Authentication provider
- **Vite**: Build tool and dev server
### Key Architecture Patterns
#### Singleton Scene Management
The application uses a singleton pattern for the BabylonJS Scene via `DefaultScene` (src/defaultScene.ts). Always access the scene through `DefaultScene.Scene` rather than creating new instances.
#### Observable-Based Event System
The application heavily uses BabylonJS Observables for event handling:
- **DiagramManager.onDiagramEventObservable**: Central hub for diagram entity changes
- **DiagramManager.onUserEventObservable**: User position/state updates for multiplayer
- **AppConfig.onConfigChangedObservable**: Application settings changes
- **controllerObservable**: VR controller input events
Event observers use a mask system (`DiagramEventObserverMask`) to distinguish:
- `FROM_DB`: Events coming from database sync (shouldn't trigger database writes)
- `TO_DB`: Events that should be persisted to database
#### Diagram Entity System
All 3D objects in the scene are represented by `DiagramEntity` types (src/diagram/types/diagramEntity.ts):
- Entities have a template reference (e.g., `#image-template`)
- Managed by `DiagramManager` which maintains a Map of `DiagramObject` instances
- Changes propagate through the Observable system to database and other clients
#### VR Controller Architecture
Controllers inherit from `AbstractController` with specialized implementations:
- `LeftController`: Menu interactions, navigation
- `RightController`: Object manipulation, selection
- Controllers communicate via `controllerObservable` with `ControllerEvent` messages
- `Rigplatform` manages the player rig and handles locomotion
#### Database & Sync
- `PouchdbPersistenceManager` (src/integration/database/pouchdbPersistenceManager.ts) handles all persistence
- Supports optional encryption via `Encryption` class
- Syncs to remote CouchDB via proxy (configured in vite.config.ts)
- URL pattern `/db/public/:db` or `/db/private/:db` determines database name
- Uses `presence.ts` for broadcasting user positions over WebSocket
### Project Structure
- `src/vrcore/`: Engine initialization and core VR setup
- `src/controllers/`: VR controller implementations and input handling
- `src/diagram/`: 3D diagram entities, management, and scene interaction
- `src/integration/`: Database sync, encryption, and presence system
- `src/menus/`: In-VR 3D menus (not React components)
- `src/objects/`: Reusable 3D objects (buttons, handles, avatars)
- `src/react/`: React UI components for 2D interface
- `src/util/`: Shared utilities and configuration
- `server/`: WebSocket server for real-time presence
### Configuration System
Two configuration systems exist (being migrated):
1. **AppConfig class** (src/util/appConfig.ts): Observable-based config with typed properties
2. **ConfigType** (bottom of appConfig.ts): Legacy localStorage-based config
Settings include snapping values, physics toggles, fly mode, and turn snap angles.
## Important Development Notes
### Proxy Configuration
The dev and preview servers proxy certain routes to production:
- `/sync/*` - Database sync endpoint
- `/create-db` - Database creation
- `/api/images` - Image uploads
### Physics System
- Uses Havok physics engine (requires WASM file via `npm run havok`)
- Physics can be enabled/disabled via AppConfig
- `customPhysics.ts` provides helper functions
### WebGPU Support
The engine initializer supports both WebGL and WebGPU backends via the `useWebGpu` parameter.
### Encryption
Databases can be optionally encrypted. The `Encryption` class handles AES encryption with password-derived keys. Salt is stored in metadata document.
### Environment Variables
- `VITE_USER_ENDPOINT`: User authentication endpoint
- `VITE_SYNCDB_ENDPOINT`: Remote database sync endpoint
Check `.env.local` for local configuration.
## Naming Conventions
### Tool and Material Naming
**Material Names:** Materials follow the pattern `material-{color}` where `{color}` is the hex color string (e.g., `material-#ff0000` for red).
**Tool Mesh Names:** Tools use the pattern `tool-{toolType}-{color}`:
- Example: `tool-BOX-#ff0000` (red box tool)
- ToolTypes: `BOX`, `SPHERE`, `CYLINDER`, `CONE`, `PLANE`, `PERSON`
**Tool Instance Names:** `tool-instance-{toolType}-{color}` (e.g., `tool-instance-BOX-#ff0000`)
**Implementation details:**
- 16 predefined toolbox colors (see docs/NAMING_CONVENTIONS.md)
- Materials created in `src/toolbox/functions/buildColor.ts`
- Tool meshes created in `src/toolbox/functions/buildTool.ts`
- When extracting colors from materials, use: `emissiveColor || diffuseColor` (priority order)
### Rendering Modes
Three rendering modes affect material properties:
1. **Lightmap with Lighting**: Uses `diffuseColor` + `lightmapTexture` (expensive)
2. **Unlit with Emissive Texture** (default): Uses `emissiveColor` + `emissiveTexture` (lightmap)
3. **Flat Emissive**: Uses only `emissiveColor` (fastest)
See `src/util/renderingMode.ts` and `src/util/lightmapGenerator.ts` for implementation.

View File

@ -1,403 +0,0 @@
# Express.js API Server Plan
## Goal
Add an Express.js backend server to handle API routes (starting with Claude API), with support for either combined or split deployment.
## Advantages Over Next.js Migration
- **Minimal frontend changes** - only API URL configuration
- **No routing changes** - keep react-router-dom as-is
- **Flexible deployment** - combined or split frontend/backend
- **Already partially exists** - `server.js` in root has Express + vite-express scaffolding
## Deployment Options
### Option A: Combined (Single Server)
```
Express Server (vite-express)
├── Serves static files from dist/
└── Handles /api/* routes
```
- Simpler setup, one deployment
- Good for: VPS, Railway, Fly.io, DigitalOcean App Platform
### Option B: Split (Separate Hosts)
```
Static Host (CDN) API Server (Node.js)
├── Cloudflare Pages ├── Railway
├── Netlify ├── Fly.io
├── Vercel ├── AWS Lambda
└── S3 + CloudFront └── Any VPS
Serves dist/ Handles /api/*
```
- Better scalability, cheaper static hosting
- Good for: High traffic, global CDN distribution
---
## Current State
### Existing `server.js` (incomplete)
```javascript
import express from "express";
import ViteExpress from "vite-express";
import dotenv from "dotenv";
import expressProxy from "express-http-proxy";
dotenv.config();
const app = express();
app.use("/api", expressProxy("local.immersiveidea.com"));
ViteExpress.listen(app, process.env.PORT || 3001, () => console.log("Server is listening..."));
```
### Missing Dependencies
The following packages are imported but not in package.json:
- `express`
- `vite-express`
- `express-http-proxy`
- `dotenv`
---
## Implementation Plan
### Phase 1: Install Dependencies
```bash
npm install express vite-express dotenv cors
```
- `express` - Web framework
- `vite-express` - Vite integration for combined deployment
- `dotenv` - Environment variable loading
- `cors` - Cross-origin support for split deployment
### Phase 2: Create API Routes Structure
Create a modular API structure:
```
server/
├── server.js # Existing WebSocket server (keep as-is)
├── api/
│ ├── index.js # Main API router
│ └── claude.js # Claude API proxy route
```
### Phase 3: Update Root `server.js`
Replace the current incomplete server.js with:
```javascript
import express from "express";
import ViteExpress from "vite-express";
import cors from "cors";
import dotenv from "dotenv";
import apiRoutes from "./server/api/index.js";
dotenv.config();
const app = express();
// CORS configuration for split deployment
// In combined mode, same-origin requests don't need CORS
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [];
if (allowedOrigins.length > 0) {
app.use(cors({
origin: allowedOrigins,
credentials: true,
}));
}
app.use(express.json());
// API routes
app.use("/api", apiRoutes);
// Check if running in API-only mode (split deployment)
const apiOnly = process.env.API_ONLY === "true";
if (apiOnly) {
// API-only mode: no static file serving
app.listen(process.env.PORT || 3000, () => {
console.log(`API server running on port ${process.env.PORT || 3000}`);
});
} else {
// Combined mode: Vite handles static files + SPA
ViteExpress.listen(app, process.env.PORT || 3001, () => {
console.log(`Server running on port ${process.env.PORT || 3001}`);
});
}
```
### Phase 4: Create API Router
**`server/api/index.js`**:
```javascript
import { Router } from "express";
import claudeRouter from "./claude.js";
const router = Router();
// Claude API proxy
router.use("/claude", claudeRouter);
// Health check
router.get("/health", (req, res) => {
res.json({ status: "ok" });
});
export default router;
```
**`server/api/claude.js`**:
```javascript
import { Router } from "express";
const router = Router();
const ANTHROPIC_API_URL = "https://api.anthropic.com";
router.post("/*", async (req, res) => {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
return res.status(500).json({ error: "API key not configured" });
}
// Get the path after /api/claude (e.g., /v1/messages)
const path = req.params[0] || req.path;
try {
const response = await fetch(`${ANTHROPIC_API_URL}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify(req.body),
});
const data = await response.json();
res.status(response.status).json(data);
} catch (error) {
console.error("Claude API error:", error);
res.status(500).json({ error: "Failed to proxy request to Claude API" });
}
});
export default router;
```
### Phase 5: Update Vite Config
Remove the Claude proxy from `vite.config.ts` since Express handles it now.
**Before** (lines 41-56):
```javascript
'^/api/claude': {
target: 'https://api.anthropic.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/claude/, ''),
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => {
const apiKey = env.ANTHROPIC_API_KEY;
// ...
});
}
}
```
**After**: Remove this block entirely. The Express server handles `/api/claude/*`.
Keep the other proxies (`/sync/*`, `/create-db`, `/api/images`) - they still proxy to deepdiagram.com in dev mode.
### Phase 6: Add API URL Configuration (for Split Deployment)
Create a utility to get the API base URL:
**`src/util/apiConfig.ts`**:
```typescript
// API base URL - empty string for same-origin (combined deployment)
// Set VITE_API_URL for split deployment (e.g., "https://api.yourdomain.com")
export const API_BASE_URL = import.meta.env.VITE_API_URL || '';
export function apiUrl(path: string): string {
return `${API_BASE_URL}${path}`;
}
```
**Update `src/react/services/diagramAI.ts`**:
```typescript
import { apiUrl } from '../../util/apiConfig';
// Change from:
const response = await fetch('/api/claude/v1/messages', { ... });
// To:
const response = await fetch(apiUrl('/api/claude/v1/messages'), { ... });
```
This change is backward-compatible:
- **Combined deployment**: `VITE_API_URL` is empty, calls go to same origin
- **Split deployment**: `VITE_API_URL=https://api.example.com`, calls go to API server
### Phase 7: Update package.json Scripts
```json
"scripts": {
"dev": "node server.js",
"build": "node versionBump.js && vite build",
"start": "NODE_ENV=production node server.js",
"start:api": "API_ONLY=true node server.js",
"test": "vitest",
"socket": "node server/server.js",
"serverBuild": "cd server && tsc"
}
```
**Changes:**
- `dev`: Runs Express + vite-express (serves Vite in dev mode)
- `start`: Combined mode - serves dist/ + API
- `start:api`: API-only mode for split deployment
- Removed `preview` (use `start` instead)
---
## File Changes Summary
| Action | File | Description |
|--------|------|-------------|
| Modify | `package.json` | Add dependencies, update scripts |
| Modify | `server.js` | Full Express server with CORS + API routes |
| Create | `server/api/index.js` | Main API router |
| Create | `server/api/claude.js` | Claude API proxy endpoint |
| Create | `src/util/apiConfig.ts` | API URL configuration utility |
| Modify | `src/react/services/diagramAI.ts` | Use apiUrl() for API calls |
| Modify | `vite.config.ts` | Remove `/api/claude` proxy block |
---
## How vite-express Works
`vite-express` is a simple integration that:
1. **Development**: Runs Vite's dev server as middleware, providing HMR
2. **Production**: Serves the built `dist/` folder as static files
This means:
- One server handles both API and frontend
- No CORS issues (same origin)
- HMR works in development
- Production-ready with `vite build`
---
## Production Deployment
### Option A: Combined Deployment
Single server handles both frontend and API:
```bash
# Build frontend
npm run build
# Start combined server (serves dist/ + API)
npm run start
```
**Environment variables (.env)**:
```bash
PORT=3001
ANTHROPIC_API_KEY=sk-ant-...
```
The Express server will:
1. Handle `/api/*` routes directly
2. Serve static files from `dist/`
3. Fall back to `dist/index.html` for SPA routing
### Option B: Split Deployment
Separate hosting for frontend (CDN) and API (Node server):
**API Server:**
```bash
# Start API-only server
npm run start:api
```
**Environment variables (.env for API server)**:
```bash
PORT=3000
API_ONLY=true
ANTHROPIC_API_KEY=sk-ant-...
ALLOWED_ORIGINS=https://your-frontend.com,https://www.your-frontend.com
```
**Frontend (Static Host):**
```bash
# Build with API URL configured
VITE_API_URL=https://api.yourdomain.com npm run build
# Deploy dist/ to your static host (Cloudflare Pages, Netlify, etc.)
```
**Environment variables (.env.production for frontend build)**:
```bash
VITE_API_URL=https://api.yourdomain.com
```
### Deployment Examples
| Deployment | Frontend | API Server | Cost |
|------------|----------|------------|------|
| Combined | Railway | (same) | ~$5/mo |
| Combined | Fly.io | (same) | Free tier |
| Split | Cloudflare Pages (free) | Railway ($5/mo) | ~$5/mo |
| Split | Netlify (free) | Fly.io (free) | Free |
| Split | Vercel (free) | AWS Lambda | Pay-per-use |
---
## Future API Routes
To add more API routes, create new files in `server/api/`:
```javascript
// server/api/index.js
import claudeRouter from "./claude.js";
import imagesRouter from "./images.js"; // future
import authRouter from "./auth.js"; // future
router.use("/claude", claudeRouter);
router.use("/images", imagesRouter);
router.use("/auth", authRouter);
```
---
## Migration Order
1. `npm install express vite-express dotenv cors`
2. Create `server/api/index.js`
3. Create `server/api/claude.js`
4. Create `src/util/apiConfig.ts`
5. Update `src/react/services/diagramAI.ts` to use `apiUrl()`
6. Update `server.js` (root) with full Express + CORS setup
7. Remove `/api/claude` proxy from `vite.config.ts`
8. Update `package.json` scripts
9. Test combined: `npm run dev` and verify Claude API works
10. (Optional) Test split: Set `VITE_API_URL` and `API_ONLY=true`
---
## Notes
- **WebSocket server unchanged**: `server/server.js` (port 8080) runs separately
- **Minimal frontend changes**: Only `diagramAI.ts` updated to use `apiUrl()`
- **Environment variables**: `ANTHROPIC_API_KEY` already in `.env.local`
- **Node version**: Requires Node 18+ for native `fetch`
- **CORS**: Only enabled when `ALLOWED_ORIGINS` is set (split deployment)
- **Backward compatible**: Works as combined deployment by default

View File

@ -1,30 +0,0 @@
Permissions Conditions Limitations
Commercial use
Distribution
Modification
Private use
License and copyright notice
Liability
Warranty
MIT License
Copyright (c) [2024] [Michael Mainguy]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,167 +0,0 @@
# Vite to Next.js Migration Plan
## Goal
Migrate from Vite to Next.js App Router to get proper API route support, with minimal changes to existing code.
## Configuration
- **Router**: App Router with `'use client'` on all pages
- **Rendering**: CSR only (no SSR) - simplifies migration since BabylonJS can't SSR
- **API Routes**: Claude API now, structured for future expansion
- **External Proxies**: Keep sync/create-db/images as Next.js rewrites to deepdiagram.com
---
## Phase 1: Setup (No Breaking Changes)
### 1.1 Install Next.js
```bash
npm install next
```
### 1.2 Create `next.config.js`
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{ source: '/sync/:path*', destination: 'https://www.deepdiagram.com/sync/:path*' },
{ source: '/create-db', destination: 'https://www.deepdiagram.com/create-db' },
{ source: '/api/images', destination: 'https://www.deepdiagram.com/api/images' },
];
},
webpack: (config, { isServer }) => {
config.experiments = { ...config.experiments, asyncWebAssembly: true };
return config;
},
};
module.exports = nextConfig;
```
### 1.3 Update `tsconfig.json`
Add path alias:
```json
"baseUrl": ".",
"paths": { "@/*": ["./*"] }
```
---
## Phase 2: Create New Files
### 2.1 `src/react/providers.tsx` (extract from webApp.tsx)
- Move Auth0Provider and FeatureProvider wrapping here
- Add `'use client'` directive
- Handle window/document checks for SSR safety
### 2.2 `app/layout.tsx`
- Root layout with html/body tags
- Metadata (title, favicon from current index.html)
- Import global CSS
### 2.3 `app/globals.css`
```css
@import '../src/react/styles.css';
@import '@mantine/core/styles.css';
```
### 2.4 `app/api/claude/[...path]/route.ts`
- POST handler that proxies to api.anthropic.com
- Injects `ANTHROPIC_API_KEY` from env
- Adds `x-api-key` and `anthropic-version` headers
### 2.5 Page files (all with `'use client'`)
| Route | File | Component |
|-------|------|-----------|
| `/` | `app/page.tsx` | About |
| `/documentation` | `app/documentation/page.tsx` | Documentation |
| `/examples` | `app/examples/page.tsx` | Examples |
| `/pricing` | `app/pricing/page.tsx` | Pricing |
| `/db/[visibility]/[db]` | `app/db/[visibility]/[db]/page.tsx` | VrExperience |
| 404 | `app/not-found.tsx` | NotFound |
### 2.6 `src/react/components/ProtectedPage.tsx`
- Next.js version of route protection
- Uses `useRouter` from `next/navigation` for redirects
---
## Phase 3: Modify Existing Files
### 3.1 `src/react/pages/vrExperience.tsx`
**Changes:**
- Remove `useParams()` from react-router-dom
- Accept `visibility` and `db` as props instead
- Replace `useNavigate()` with `useRouter()` from `next/navigation`
### 3.2 `src/react/pageHeader.tsx`
**Changes:**
- Replace `import {Link} from "react-router-dom"` with `import Link from "next/link"`
- Change `to={item.href}` to `href={item.href}` on Link components
### 3.3 `src/react/marketing/about.tsx`
**Changes:**
- Replace `useNavigate()` with `useRouter()` from `next/navigation`
- Change `navigate('/path')` to `router.push('/path')`
### 3.4 `package.json`
```json
"scripts": {
"dev": "next dev -p 3001",
"build": "node versionBump.js && next build",
"start": "next start -p 3001",
"test": "vitest",
"socket": "node server/server.js",
"serverBuild": "cd server && tsc"
}
```
---
## Phase 4: Delete Old Files
| File | Reason |
|------|--------|
| `vite.config.ts` | Replaced by next.config.js |
| `index.html` | Next.js generates HTML |
| `src/webApp.ts` | Entry point no longer needed |
| `src/react/webRouter.tsx` | Replaced by app/ routing |
| `src/react/webApp.tsx` | Logic moved to providers.tsx |
| `src/react/components/ProtectedRoute.tsx` | Replaced by ProtectedPage.tsx |
---
## Critical Files to Modify
- `src/react/pages/vrExperience.tsx` - useParams -> props
- `src/react/pageHeader.tsx` - react-router Link -> Next.js Link
- `src/react/marketing/about.tsx` - useNavigate -> useRouter
- `src/react/webApp.tsx` - extract to providers.tsx
- `package.json` - scripts update
- `tsconfig.json` - path aliases
---
## Migration Order
1. Install next, create next.config.js
2. Update tsconfig.json
3. Create app/globals.css
4. Create src/react/providers.tsx
5. Create app/layout.tsx
6. Create app/api/claude/[...path]/route.ts
7. Create src/react/components/ProtectedPage.tsx
8. Modify vrExperience.tsx (accept props)
9. Create all app/*/page.tsx files
10. Modify pageHeader.tsx (Next.js Link)
11. Modify about.tsx (useRouter)
12. Update package.json scripts
13. Delete old files (vite.config.ts, index.html, webApp.ts, webRouter.tsx, webApp.tsx)
14. Test all routes
---
## Notes
- **Havok WASM**: Move `HavokPhysics.wasm` to `public/` folder
- **react-router-dom**: Can be removed from dependencies after migration
- **vite devDependencies**: Can be removed (vite, vite-plugin-cp)

View File

@ -1,224 +0,0 @@
# Immersive - Product Roadmap
## Vision
Transform immersive into an accessible, intuitive WebXR diagramming platform that delivers a frictionless onboarding experience and sustainable growth path.
---
## Phase 1: Onboarding & User Experience (Q1 2025)
### 1.1 Frictionless Entry
**Goal:** Reduce barriers to entry for new users
- [ ] Redesign landing page to clearly guide users to immersive experience
- [ ] Create one-click "Enter VR" / "Try Demo" workflow
- [ ] Optimize initial load time and progressive loading
- [ ] Add clear device compatibility messaging (desktop/VR)
- [ ] Implement guest mode with no sign-in required for basic exploration
### 1.2 Marketing Content
**Goal:** Communicate value proposition effectively
- [ ] Create 3-5 demo videos showcasing key features (30-60 seconds each)
- Creating a basic diagram
- VR interaction showcase
- Collaboration features
- Template usage
- [ ] Develop tutorial video (2-3 minutes) explaining core workflows
- [ ] Autoplay video carousel on landing page
- [ ] Write marketing copy for landing page
- Hero section with clear value proposition
- Feature highlights
- Use case examples
- Call-to-action
### 1.3 In-Experience Tutorial
**Goal:** Replace external tutorial with immersive learning
- [ ] Remove existing external tutorial system
- [ ] Design in-VR tutorial experience with interactive steps
- [ ] Implement progressive disclosure (teach as users interact)
- [ ] Add contextual tooltips and hints in 3D space
- [ ] Create "first-time user" detection and guided walkthrough
- [ ] Add skip/replay tutorial options
### 1.4 Template System
**Goal:** Provide starting points for new users
- [ ] Design template/example diagram system
- [ ] Create 5-10 starter templates:
- Simple organizational chart
- Project workflow diagram
- Concept mapping example
- Architecture diagram
- Spatial layout example
- [ ] Build template browser UI (2D and VR)
- [ ] Implement "New from Template" workflow
- [ ] Add template preview/thumbnail generation
---
## Phase 2: Collaboration & Sync (Q2 2025)
### 2.1 Cross-Device Sharing
**Goal:** Enable seamless content sharing between desktop and Quest
- [ ] Research device-to-device sync options (WebRTC, local network)
- [ ] Design sync architecture without backend dependency
- [ ] Implement user content sync for signed-in users
- [ ] Add fallback to server-based sync when needed
- [ ] Create device pairing UI/workflow
- [ ] Test sync reliability across desktop ↔ Quest
- [ ] Add conflict resolution for simultaneous edits
---
## Phase 3: Immersion & Environment (Q2-Q3 2025)
### 3.1 Audio Integration
**Goal:** Enhance presence with ambient soundscapes
- [ ] Source/create ambient audio assets
- Nature sounds (birds, wind, water)
- Office ambience
- Abstract/focus music
- [ ] Implement spatial audio system
- [ ] Add audio settings (volume, on/off, environment selection)
- [ ] Create audio manager for seamless transitions
- [ ] Add positional audio for collaboration (optional user voices)
### 3.2 Environment System
**Goal:** Provide varied immersive environments
- [ ] Design environment switching architecture
- [ ] Create environment presets:
- Outdoor/nature scene
- Modern office
- Abstract/minimal space
- Workshop/studio
- [ ] Implement skybox and lighting variations
- [ ] Build environment selector UI (2D and VR)
- [ ] Optimize environment assets for performance
- [ ] Add environment-specific audio pairing
---
## Phase 4: User Feedback & Polish (Q3 2025)
### 4.1 In-VR Feedback Mechanism
**Goal:** Enable users to provide feedback without leaving VR
- [ ] Design in-VR feedback form/interface
- [ ] Implement voice-to-text option (VR accessibility)
- [ ] Add screenshot/recording attachment capability
- [ ] Create feedback submission backend
- [ ] Build feedback review dashboard
- [ ] Add "Report Bug" quick action in VR menu
### 4.2 Keyboard Improvements
**Goal:** Improve text input experience
- [ ] Test system keyboard integration (Quest/desktop)
- [ ] Evaluate custom keyboard vs. native keyboard UX
- [ ] Implement system keyboard fallback where supported
- [ ] Optimize keyboard positioning in VR space
- [ ] Add keyboard shortcuts for power users (desktop)
---
## Phase 5: Growth & Monetization (Q4 2025)
### 5.1 Marketing Roadmap
**Goal:** Build sustainable user acquisition
- [ ] Define target audience segments
- Educators
- Remote teams
- Designers/architects
- Knowledge workers
- [ ] Create content marketing strategy
- Blog posts on use cases
- Social media showcase
- Community building (Discord/Reddit)
- [ ] Develop SEO optimization plan
- [ ] Plan partnership outreach (VR communities, productivity tools)
- [ ] Create referral/sharing incentives
- [ ] Build analytics dashboard for user metrics
### 5.2 Monetization Strategy
**Goal:** Establish path to sustainability
**Potential Revenue Streams:**
- [ ] Freemium model research
- Free tier: Limited diagrams, basic features
- Pro tier: Unlimited diagrams, advanced features, collaboration
- [ ] Team/Enterprise pricing
- Private deployment options
- Admin controls
- Priority support
- [ ] Template marketplace
- Premium templates
- Community submissions (revenue share)
- [ ] Educational licensing
- Institutional pricing
- Classroom management features
**Implementation:**
- [ ] Define pricing tiers and feature gates
- [ ] Integrate payment processing (Stripe)
- [ ] Build subscription management UI
- [ ] Implement feature flags for tier differentiation
- [ ] Create upgrade prompts and conversion flow
- [ ] Add usage analytics for pricing optimization
---
## Success Metrics
### Phase 1-2 (Onboarding)
- Time to first diagram creation < 2 minutes
- Tutorial completion rate > 60%
- Return user rate (7-day) > 30%
### Phase 3-4 (Engagement)
- Average session duration > 15 minutes
- User satisfaction score > 4/5
- Feedback submission rate (active users) > 10%
### Phase 5 (Growth)
- Monthly active users growth > 20% MoM
- Free-to-paid conversion rate > 5%
- Customer acquisition cost < lifetime value
---
## Technical Debt & Infrastructure
### Ongoing Priorities
- [ ] Migration from legacy ConfigType to AppConfig
- [ ] Performance optimization (target 90fps in VR)
- [ ] Accessibility improvements (WCAG compliance)
- [ ] Testing coverage > 70%
- [ ] Documentation for contributors
- [ ] CI/CD pipeline enhancements
---
## Notes
**Dependencies:**
- Auth0 for user authentication
- PouchDB/CouchDB for data persistence
- BabylonJS 8.x for rendering
- Vite for build tooling
**Platform Support:**
- Desktop browsers (Chrome, Firefox, Edge)
- Meta Quest 2/3/Pro
- Future: PSVR2, Vision Pro (evaluate demand)
**Review Cadence:** Quarterly roadmap review and adjustment based on user feedback and metrics.
---
*Last Updated: 2025-11-19*

View File

@ -1,150 +0,0 @@
# Self-Hosted Diagram Sharing with Express-PouchDB
## Requirements (Confirmed)
- **Storage**: In-memory (ephemeral) - lost on server restart
- **Content**: Copy current diagram entities when creating share
- **Expiration**: No expiration - links work until server restart
- **Encryption**: None - keep it simple, anyone with link can access
## Architecture
```
┌─────────────────────────────────────────────────┐
│ Express Server (port 3001) │
├─────────────────────────────────────────────────┤
│ /api/share/* → Share management API │
│ /pouchdb/* → express-pouchdb (sync) │
│ /share/:uuid → Client share route │
│ /api/* → Existing API routes │
│ /* → Vite static files │
└─────────────────────────────────────────────────┘
└── In-Memory PouchDB (per share UUID)
```
## Implementation Steps
### Phase 1: Server-Side Setup
#### 1.1 Add Dependencies
```bash
npm install express-pouchdb pouchdb-adapter-memory
```
#### 1.2 Create PouchDB Server Service
**New file: `server/services/pouchdbServer.js`**
- Initialize PouchDB with memory adapter
- Track active share databases in a Map
- Export `getShareDB(shareId)`, `shareExists(shareId)`, `createPouchDBMiddleware()`
#### 1.3 Create Share API
**New file: `server/api/share.js`**
- `POST /api/share/create` - Generate UUID, create in-memory DB, copy entities
- `GET /api/share/:id/exists` - Check if share exists
- `GET /api/share/stats` - Debug endpoint for active shares
#### 1.4 Update API Router
**Edit: `server/api/index.js`**
- Add `import shareRouter from "./share.js"`
- Mount at `router.use("/share", shareRouter)`
#### 1.5 Mount Express-PouchDB
**Edit: `server.js`**
- Import `createPouchDBMiddleware` from pouchdbServer.js
- Mount at `app.use("/pouchdb", createPouchDBMiddleware())`
---
### Phase 2: Client-Side Integration
#### 2.1 Update URL Parsing
**Edit: `src/util/functions/getPath.ts`**
- Add `getPathInfo()` function returning `{ dbName, isShare, shareId }`
- Detect `/share/:uuid` pattern
#### 2.2 Update PouchDB Persistence Manager
**Edit: `src/integration/database/pouchdbPersistenceManager.ts`**
In `initLocal()`:
- Call `getPathInfo()` to detect share URLs
- If share: use `share-{uuid}` as local DB name, call `beginShareSync()`
Add new method `beginShareSync(shareId)`:
- Check share exists via `/api/share/:id/exists`
- Connect to `${origin}/pouchdb/share-${shareId}`
- Set up presence with `share-${shareId}` as DB name
- Begin live sync (no encryption)
#### 2.3 Add React Route
**Edit: `src/react/webRouter.tsx`**
- Add route `{ path: "/share/:uuid", element: <VrExperience isShare={true} /> }`
- No ProtectedRoute wrapper (public access)
#### 2.4 Add Share Button Handler
**Edit: `src/react/pages/vrExperience.tsx`**
- Add `isShare` prop
- Add `handleShare()` function:
1. Get all entities from local PouchDB
2. POST to `/api/share/create` with entities
3. Copy resulting URL to clipboard
4. Show confirmation
---
### Phase 3: Presence Integration
The WebSocket presence system already routes by database name. Since shares use `share-{uuid}` as the database name, presence works automatically.
**Edit: `server/server.js`** (WebSocket server)
- Update `originIsAllowed()` to allow localhost for development
---
## Files to Modify
| File | Action | Purpose |
|------|--------|---------|
| `package.json` | Edit | Add express-pouchdb, pouchdb-adapter-memory |
| `server.js` | Edit | Mount /pouchdb middleware |
| `server/api/index.js` | Edit | Add share router |
| `server/services/pouchdbServer.js` | Create | PouchDB memory initialization |
| `server/api/share.js` | Create | Share API endpoints |
| `server/server.js` | Edit | Allow localhost origins |
| `src/util/functions/getPath.ts` | Edit | Add getPathInfo() |
| `src/integration/database/pouchdbPersistenceManager.ts` | Edit | Add share sync logic |
| `src/react/webRouter.tsx` | Edit | Add /share/:uuid route |
| `src/react/pages/vrExperience.tsx` | Edit | Add share button handler |
| `src/util/featureConfig.ts` | Edit | Enable shareCollaborate feature |
---
## User Flow
### Creating a Share
1. User has a diagram open at `/db/public/mydiagram`
2. Clicks "Share" button
3. Client fetches all entities from local PouchDB
4. POSTs to `/api/share/create` with entities
5. Server creates in-memory DB, copies entities, returns UUID
6. Client copies `https://server.com/share/{uuid}` to clipboard
7. User shares link with collaborators
### Joining a Share
1. User navigates to `https://server.com/share/{uuid}`
2. React Router renders VrExperience with `isShare=true`
3. PouchdbPersistenceManager detects share URL
4. Checks `/api/share/:uuid/exists` - returns true
5. Creates local PouchDB `share-{uuid}`
6. Connects to `/pouchdb/share-{uuid}` for sync
7. Entities replicate to local, render in scene
8. Presence WebSocket connects with `share-{uuid}` as room
---
## Future Authentication (Not Implemented Now)
Structure allows easy addition later:
- express-pouchdb middleware can be wrapped with auth middleware
- Share API can require JWT/session tokens
- Could add password-protected shares
- Could add read-only vs read-write permissions

View File

@ -1,179 +0,0 @@
# Future Sync Strategy: Keeping Local and Public Clones in Sync
## Current State (v1)
- Sharing creates a **ONE-TIME COPY** from local to public
- Copies diverge independently after sharing
- No automatic sync between local and public versions
- Local diagrams are browser-only (IndexedDB via PouchDB)
- Public diagrams sync with server via express-pouchdb
### URL Scheme
| Route | Sync | Access | Status |
|-------|------|--------|--------|
| `/db/local/:id` | None | Browser-only | Implemented |
| `/db/public/:id` | Yes | Anyone | Implemented |
| `/db/private/:id` | Yes | Authorized users | Route only (no auth) |
## Future Options
### Option 1: Manual Push/Pull (Recommended for v2)
Add explicit user-triggered sync between local and public copies.
**Features:**
- "Push to Public" button - sends local changes to public copy
- "Pull from Public" button - gets public changes into local
- Track `lastSyncedAt` timestamp
- Show indicator when copies have diverged
- Conflict resolution: Last write wins (simple) or user choice (advanced)
**Pros:**
- User stays in control
- Clear mental model
- Simple to implement incrementally
**Cons:**
- Manual effort required
- Risk of forgetting to sync
### Option 2: Automatic Background Sync
Continuous bidirectional sync between local and public copies.
**Features:**
- Real-time sync like Google Docs
- Works across devices
- Offline-first with automatic merge
**Pros:**
- Seamless experience
- Always up to date
**Cons:**
- Complex conflict resolution (may need CRDTs)
- Higher performance overhead
- Harder to reason about state
### Option 3: Fork/Branch Model
One-way relationship: local is "draft", public is "published".
**Features:**
- Push only (local → public)
- No pull mechanism
- Public is the "source of truth" once published
**Pros:**
- Clear mental model
- No merge conflicts
- Simple implementation
**Cons:**
- Cannot incorporate public changes back to local
- Multiple people can't collaborate on draft
## Recommended Implementation (v2)
Implement **Option 1 (Manual Push/Pull)** as it provides the best balance of user control and simplicity.
### Data Model Changes
Add to diagram directory entry:
```typescript
interface DiagramEntry {
_id: string;
name: string;
description: string;
storageType: 'local' | 'public' | 'private';
createdAt: string;
// New fields for sync tracking
publicCopyId?: string; // ID of the public clone (if shared)
lastPushedAt?: string; // When changes were last pushed to public
lastPulledAt?: string; // When public changes were last pulled
publicVersion?: number; // Version number of public copy at last sync
}
```
### API Endpoints
```typescript
// Push local changes to public
POST /api/sync/push
Body: { localDbName: string, publicDbName: string }
Response: { success: boolean, documentsUpdated: number }
// Pull public changes to local
POST /api/sync/pull
Body: { localDbName: string, publicDbName: string }
Response: { success: boolean, documentsUpdated: number }
// Check if copies have diverged
GET /api/sync/status?local={localDbName}&public={publicDbName}
Response: {
diverged: boolean,
localChanges: number,
publicChanges: number,
lastSyncedAt: string
}
```
### UI Components
1. **Sync Status Indicator**
- Shows in header when viewing a local diagram that has a public copy
- Green check: In sync
- Orange dot: Changes pending
- Red warning: Conflicts detected
2. **Push/Pull Buttons**
- In hamburger menu under "Share" section
- "Push to Public" - shows confirmation with change count
- "Pull from Public" - shows confirmation with change count
3. **Divergence Warning Badge**
- Shows on diagram card in Manage Diagrams modal
- Indicates when local and public have diverged
4. **Conflict Resolution Dialog**
- Shows when both local and public have changes to same entity
- Options: Keep Local, Keep Public, Keep Both (creates duplicate)
### Implementation Phases
**Phase 1: Tracking**
- Add `publicCopyId` when sharing local → public
- Track sharing relationship in directory
**Phase 2: Push**
- Implement push from local to public
- Overwrite public with local changes
- Update `lastPushedAt` timestamp
**Phase 3: Pull**
- Implement pull from public to local
- Merge public changes into local
- Update `lastPulledAt` timestamp
**Phase 4: Status**
- Implement divergence detection
- Add UI indicators
- Show sync status in Manage Diagrams
**Phase 5: Conflict Resolution**
- Detect entity-level conflicts
- Show resolution dialog
- Allow user to choose resolution strategy
## Migration Notes
Existing diagrams without `storageType` are treated as `public` for backwards compatibility. When such diagrams are loaded, the UI should work correctly but sync tracking features won't be available until the diagram metadata is updated.
## Security Considerations
- Push/pull operations should validate that the user has access to both databases
- Public databases remain world-readable/writable
- Private database sync will require authentication tokens
- Rate limiting should be applied to sync operations

View File

@ -1,297 +0,0 @@
# VR Configuration Panel Implementation Plan
## Overview
Create an immersive WebXR configuration panel that mirrors the 2D ConfigModal functionality using BabylonJS AdvancedDynamicTexture (ADT). The panel will allow users to adjust all application settings directly in VR.
## Recommended Approach: AdvancedDynamicTexture (ADT)
**Why ADT?**
- Most common approach for WebXR UI in BabylonJS
- Existing pattern in codebase (see `src/menus/configMenu.ts`)
- Good balance of simplicity and functionality
- Native support for text, buttons, sliders, and dropdowns
- Easy integration with existing Handle pattern
**Estimated Effort**: 150-200 lines of code, 4-8 hours implementation time
## File Structure
```
src/menus/
├── vrConfigPanel.ts (NEW - main implementation)
└── configMenu.ts (REFERENCE - existing VR config example)
src/diagram/
└── diagramMenuManager.ts (MODIFY - add toolbox button)
src/util/
└── appConfig.ts (USE - singleton for config management)
```
## Implementation Phases
### Phase 1: Core Panel Setup
- [ ] Create `src/menus/vrConfigPanel.ts` file
- [ ] Implement class structure following Handle pattern:
```typescript
export class VRConfigPanel {
private _scene: Scene;
private _handleMesh: Mesh;
private _advancedTexture: AdvancedDynamicTexture;
private _configObserver: Observer<AppConfigType>;
constructor(scene: Scene) {
// Initialize panel
}
public get handleMesh(): Mesh {
return this._handleMesh;
}
public show(): void {
this._handleMesh.setEnabled(true);
}
public hide(): void {
this._handleMesh.setEnabled(false);
}
public dispose(): void {
// Cleanup
}
}
```
- [ ] Create base mesh (plane) for panel backing
- [ ] Set up AdvancedDynamicTexture with appropriate resolution (1024x1024 or 2048x2048)
- [ ] Position panel at comfortable viewing distance (0.5-0.7m from camera)
- [ ] Make panel grabbable via Handle pattern
**Reference Files**:
- `src/menus/inputTextView.ts` - Handle pattern implementation
- `src/menus/configMenu.ts` - ADT usage example
### Phase 2: UI Layout Structure
- [ ] Create main container (StackPanel for vertical layout)
- [ ] Add title text at top ("Configuration")
- [ ] Create 5 section containers (one for each config group):
1. Location Snap
2. Rotation Snap
3. Fly Mode
4. Snap Turn
5. Label Rendering Mode
- [ ] Style containers with padding and spacing
- [ ] Add visual separators between sections
**ADT Components to Use**:
- `StackPanel` - Main vertical container
- `TextBlock` - Labels and section titles
- `Rectangle` - Containers and separators
**Reference**: `src/menus/configMenu.ts:44-89` for existing layout patterns
### Phase 3: Location Snap Section
- [ ] Add "Location Snap" label
- [ ] Create enable/disable toggle button
- Shows "Enabled" or "Disabled"
- Updates `appConfigInstance` on click
- [ ] Add RadioGroup for snap values:
- Options: 1cm (.01), 5cm (.05), 10cm (.1), 50cm (.5), 1m (1)
- Default: 10cm (.1)
- Disable when snap is off
- [ ] Wire up to `appConfigInstance.setGridSnap(value)`
- [ ] Subscribe to config changes to update UI
**ADT Components**:
- `Button` - Toggle switch
- `RadioButton` + `TextBlock` - Value selection
- Color coding: enabled (green/myColor), disabled (gray)
**Reference ConfigModal**: `src/react/pages/configModal.tsx:83-94`
### Phase 4: Rotation Snap Section
- [ ] Add "Rotation Snap" label
- [ ] Create enable/disable toggle button
- [ ] Add RadioGroup for rotation values:
- Options: 22.5°, 45°, 90°, 180°, 360°
- Default: 90°
- Disable when snap is off
- [ ] Wire up to `appConfigInstance.setRotateSnap(value)`
- [ ] Subscribe to config changes to update UI
**Reference ConfigModal**: `src/react/pages/configModal.tsx:96-108`
### Phase 5: Fly Mode Section
- [ ] Add "Fly Mode" label
- [ ] Create toggle button
- Shows "Fly Mode Enabled" or "Fly Mode Disabled"
- [ ] Wire up to `appConfigInstance.setFlyMode(value)`
- [ ] Subscribe to config changes to update UI
**Reference ConfigModal**: `src/react/pages/configModal.tsx:109-112`
### Phase 6: Snap Turn Section
- [ ] Add "Snap Turn" label
- [ ] Create enable/disable toggle button
- [ ] Add RadioGroup for snap turn angles:
- Options: 22.5°, 45°, 90°, 180°, 360°
- Default: 45°
- Disable when snap is off
- [ ] Wire up to `appConfigInstance.setTurnSnap(value)`
- [ ] Subscribe to config changes to update UI
**Reference ConfigModal**: `src/react/pages/configModal.tsx:113-125`
### Phase 7: Label Rendering Mode Section
- [ ] Add "Label Rendering Mode" label
- [ ] Create RadioGroup for rendering modes:
- Fixed
- Billboard (Always Face Camera)
- Dynamic (Coming Soon) - disabled
- Distance-based (Coming Soon) - disabled
- [ ] Wire up to `appConfigInstance.setLabelRenderingMode(value)`
- [ ] Subscribe to config changes to update UI
- [ ] Style disabled options with gray text
**Reference ConfigModal**: `src/react/pages/configModal.tsx:126-135`
### Phase 8: Integration with Toolbox
- [ ] Modify `src/diagram/diagramMenuManager.ts` to instantiate VRConfigPanel
- [ ] Add "Config" button to toolbox (similar to "Exit VR" button pattern)
- [ ] Wire up button click to show/hide panel
- [ ] Position panel relative to camera when shown (see `positionComponentsRelativeToCamera`)
- [ ] Add parent relationship to platform for movement tracking
**Reference**:
- `src/diagram/diagramMenuManager.ts:85-97` - Exit button creation
- `src/util/functions/groundMeshObserver.ts:127-222` - Component positioning
### Phase 9: Observable Integration
- [ ] Subscribe to `appConfigInstance.onConfigChangedObservable` in constructor
- [ ] Update all UI elements when config changes externally
- [ ] Ensure Observable cleanup in dispose() method
- [ ] Test config changes from both VR panel and 2D ConfigModal
**Pattern**:
```typescript
this._configObserver = appConfigInstance.onConfigChangedObservable.add((config) => {
// Update UI elements to reflect new config
this.updateLocationSnapUI(config.locationSnap);
this.updateRotationSnapUI(config.rotateSnap);
// ... etc
});
```
### Phase 10: Testing & Polish
- [ ] Test all toggle switches update config correctly
- [ ] Test all radio button selections update config correctly
- [ ] Verify config changes propagate to DiagramObjects (label mode, snap behavior)
- [ ] Test panel positioning in VR (comfortable viewing distance)
- [ ] Test panel grabbability via Handle
- [ ] Verify panel follows platform movement
- [ ] Test config persistence (localStorage)
- [ ] Test config synchronization between VR panel and 2D ConfigModal
- [ ] Add visual feedback for button clicks (color changes, animations)
- [ ] Ensure proper cleanup on panel disposal
- [ ] Test in both WebXR and desktop modes
## Code Patterns to Follow
### 1. Toggle Button Pattern
```typescript
const toggleButton = Button.CreateSimpleButton("toggle", "Enabled");
toggleButton.width = "200px";
toggleButton.height = "40px";
toggleButton.color = "white";
toggleButton.background = "green";
toggleButton.onPointerClickObservable.add(() => {
const newValue = !currentValue;
toggleButton.textBlock.text = newValue ? "Enabled" : "Disabled";
toggleButton.background = newValue ? "green" : "gray";
appConfigInstance.setSomeSetting(newValue);
});
```
### 2. RadioGroup Pattern
```typescript
const radioGroup = new SelectionPanel("snapValues");
const options = [
{ value: 0.01, label: "1cm" },
{ value: 0.1, label: "10cm" },
// ... more options
];
options.forEach(option => {
const radio = new RadioButton();
radio.width = "20px";
radio.height = "20px";
radio.isChecked = (option.value === currentValue);
radio.onIsCheckedChangedObservable.add((checked) => {
if (checked) {
appConfigInstance.setGridSnap(option.value);
}
});
// Add label next to radio button
});
```
### 3. Config Observer Pattern
```typescript
this._configObserver = appConfigInstance.onConfigChangedObservable.add((config) => {
this.updateUIFromConfig(config);
});
// In dispose():
if (this._configObserver) {
appConfigInstance.onConfigChangedObservable.remove(this._configObserver);
}
```
## Key Integration Points
### AppConfig Singleton
- Import: `import {appConfigInstance} from "../util/appConfig";`
- Read: `appConfigInstance.current.locationSnap`
- Write: `appConfigInstance.setGridSnap(0.1)`
- Subscribe: `appConfigInstance.onConfigChangedObservable.add(callback)`
### DiagramMenuManager
- Instantiate panel: `this._vrConfigPanel = new VRConfigPanel(this._scene);`
- Add button to toolbox: Follow exit button pattern in `setupExitButton()`
- Show panel: `this._vrConfigPanel.show();`
- Position panel: Follow pattern in `groundMeshObserver.ts:127-222`
### Handle Pattern
- Make panel grabbable by controllers
- Parent to platform for world movement
- Use `_handleMesh` as root for entire panel UI
## Reference Files
1. **src/menus/configMenu.ts** - Existing VR config implementation with ADT
2. **src/menus/inputTextView.ts** - Handle pattern and ADT setup
3. **src/react/pages/configModal.tsx** - UI structure and config sections
4. **src/util/appConfig.ts** - Config singleton and setter methods
5. **src/diagram/diagramMenuManager.ts** - Toolbox button creation
6. **src/util/functions/groundMeshObserver.ts** - Component positioning
## Success Criteria
- [ ] All 5 config sections implemented and functional
- [ ] Config changes in VR panel update appConfigInstance
- [ ] Config changes propagate to all DiagramObjects
- [ ] Panel is grabbable and repositionable
- [ ] Panel follows platform movement
- [ ] Config persists to localStorage
- [ ] Synchronized with 2D ConfigModal
- [ ] Comfortable viewing experience in VR
- [ ] No memory leaks (proper Observable cleanup)
## Notes
- Start hidden (only show when user clicks toolbox button)
- Position at ~0.5m in front of camera when opened
- Use Y-axis billboard mode to keep panel upright but allow rotation
- Consider adding "Close" button at bottom of panel
- Match color scheme with existing UI (myColor theme)
- Test with both left and right controller grabbing

View File

@ -1,138 +0,0 @@
# Naming Conventions
## Tool and Material Naming
This document describes the naming conventions used for tools, materials, and related entities in the immersive WebXR application.
## Material Naming
Materials follow a consistent naming pattern based on their color:
**Format:** `material-{color}`
**Where:**
- `{color}` is the hex string representation of the material's color (e.g., `#ff0000` for red)
**Examples:**
- `material-#ff0000` - Red material
- `material-#00ff00` - Green material
- `material-#222222` - Dark gray material
**Implementation:**
```typescript
const material = new StandardMaterial("material-" + color.toHexString(), scene);
```
**Location:** Materials are created in:
- `src/toolbox/functions/buildColor.ts` - For toolbox color swatches
- `src/diagram/functions/buildMeshFromDiagramEntity.ts` - Fallback material creation via `buildMissingMaterial()`
## Tool Mesh Naming
Tool meshes use a compound naming pattern that includes both the tool type and color:
**Format:** `tool-{toolId}`
**Where:**
- `{toolId}` = `{toolType}-{color}`
- `{toolType}` is a value from the `ToolType` enum (e.g., `BOX`, `SPHERE`, `CYLINDER`, `CONE`, `PLANE`, `PERSON`)
- `{color}` is the hex string representation of the tool's color
**Examples:**
- `tool-BOX-#ff0000` - Red box tool
- `tool-SPHERE-#00ff00` - Green sphere tool
- `tool-CYLINDER-#0000ff` - Blue cylinder tool
- `tool-PLANE-#ffff00` - Yellow plane tool
**Implementation:**
```typescript
function toolId(tool: ToolType, color: Color3) {
return tool + "-" + color.toHexString();
}
const newItem = await buildMesh(tool, `tool-${id}`, colorParent.getScene());
// For example: `tool-BOX-#ff0000`
```
**Location:** Tool meshes are created in `src/toolbox/functions/buildTool.ts`
## Tool Colors
The application uses 16 predefined colors for the toolbox:
```typescript
const colors: string[] = [
"#222222", "#8b4513", "#006400", "#778899", // Row 1: Dark gray, Brown, Dark green, Light slate gray
"#4b0082", "#ff0000", "#ffa500", "#ffff00", // Row 2: Indigo, Red, Orange, Yellow
"#00ff00", "#00ffff", "#0000ff", "#ff00ff", // Row 3: Green, Cyan, Blue, Magenta
"#1e90ff", "#98fb98", "#ffe4b5", "#ff69b4" // Row 4: Dodger blue, Pale green, Moccasin, Hot pink
]
```
## Tool Types
Available tool types from the `ToolType` enum:
- `BOX` - Cube mesh
- `SPHERE` - Sphere mesh
- `CYLINDER` - Cylinder mesh
- `CONE` - Cone mesh
- `PLANE` - Flat plane mesh
- `PERSON` - Person/avatar mesh
## Material Color Access
When accessing material colors, use this priority order to handle both current and legacy materials:
```typescript
// For StandardMaterial
const stdMat = material as StandardMaterial;
const materialColor = stdMat.emissiveColor || stdMat.diffuseColor;
// Current rendering uses emissiveColor
// Legacy materials may have diffuseColor instead
```
## Rendering Modes
Materials can be rendered in three different modes, affecting how color properties are used:
### 1. Lightmap with Lighting
- Uses `diffuseColor` + `lightmapTexture`
- `disableLighting = false`
- Most expensive performance-wise
- Provides lighting illusion with actual lighting calculations
### 2. Unlit with Emissive Texture (Default)
- Uses `emissiveColor` + `emissiveTexture` (lightmap)
- `disableLighting = true`
- Best balance of visual quality and performance
- Provides lighting illusion without lighting calculations
### 3. Flat Emissive
- Uses only `emissiveColor`
- `disableLighting = true`
- Best performance
- No lighting illusion, flat shading
## Instance Naming
Instanced meshes (created from tool templates) follow this pattern:
**Format:** `tool-instance-{toolId}`
**Example:**
```typescript
const instance = new InstancedMesh("tool-instance-" + id, newItem);
// For example: `tool-instance-BOX-#ff0000`
```
These instances share materials with their source mesh and are used for visual feedback before creating actual diagram entities.
## Related Files
- `src/toolbox/functions/buildTool.ts` - Tool mesh creation and naming
- `src/toolbox/functions/buildColor.ts` - Material creation and color management
- `src/diagram/functions/buildMeshFromDiagramEntity.ts` - Diagram entity instantiation
- `src/toolbox/types/toolType.ts` - ToolType enum definition
- `src/util/lightmapGenerator.ts` - Lightmap texture generation and caching
- `src/util/renderingMode.ts` - Rendering mode enum and labels

View File

@ -3,57 +3,92 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport"/> <meta content="width=device-width, initial-scale=1" name="viewport"/>
<meta content="An immersive vr diagramming experience based using webxr version 0.0.8-14 (2024-12-29Z)" <meta content="An immersive vr diagramming experience based using webxr version 0.0.8-14 (2024-07-03T13:09:05.707Z) 4fdcc9694d3614be538e425110d1ab50cd20b302"
name="description"> name="description">
<meta content="width=device-width, initial-scale=1, height=device-height" name="viewport"> <meta content="width=device-width, initial-scale=1, height=device-height" name="viewport">
<!--<link href="/styles.css" rel="stylesheet"> --> <link href="/styles.css" rel="stylesheet">
<link href="/assets/dasfad/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"> <link href="/assets/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png">
<link href="/assets/dasfad/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"> <link href="/assets/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png">
<link href="/assets/dasfad/favicon-96x96.png" rel="icon" sizes="96x96" type="image/png"> <link href="/assets/favicon-96x96.png" rel="icon" sizes="96x96" type="image/png">
<link as="fetch" href="/node_modules/.vite/deps/HavokPhysics.wasm" rel="preload"> <title>Deep Diagram</title>
<title>DASFAD</title> <link as="script" href="/newRelic.js" rel="preload">
<!-- <link as="script" href="/newRelic.js" rel="preload"> <script defer src="/newRelic.js"></script>
<script defer src="/newRelic.js"></script> --> <script defer src="/src/webApp.ts" type="module"></script>
<script defer src="/src/vrApp.ts" type="module"></script>
<link href="/manifest.webmanifest" rel="manifest"/> <link href="/manifest.webmanifest" rel="manifest"/>
<!--<script src='/niceware.js'></script>--> <!--<script src='/niceware.js'></script>-->
<style> <style>
#feed {
display: none;
}
#keyboardHelp {
display: none;
width: 665px;
height: 312px;
}
#keyboardHelp .button {
background-color: white;
width: 16px;
height: 16px;
display: inline-block;
text-align: center;
color: #000000;
}
#keyboardHelp div {
background: transparent;
}
</style> </style>
</head> </head>
<body> <body>
<img id="loadingGrid" src="/assets/grid6.jpg"/>
<script> <script>
if (typeof navigator.serviceWorker !== 'undefined') {
if (localStorage.getItem('serviceWorkerVersion') !== '11') {
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
});
localStorage.setItem('serviceWorkerVersion', '11');
}
navigator.serviceWorker.register('/sw.js', {updateViaCache: 'none'});
}
</script>
<script>
/*
var SpeechRecognition = SpeechRecognition || webkitSpeechRecognition
var SpeechGrammarList = SpeechGrammarList || window.webkitSpeechGrammarList
var SpeechRecognitionEvent = SpeechRecognitionEvent || webkitSpeechRecognitionEvent
var recognition = new SpeechRecognition();
recognition.continuous = false;
recognition.lang = 'en-US';
recognition.interimResults = true;
recognition.maxAlternatives = 1;
recognition.onresult = function(event) {
console.log(event.results[0][0].transcript);
}
recognition.onend = function() {
console.log("recognition ended");
recognition.start();
}
console.log("starting recognition");
recognition.start();
/* if (typeof navigator.serviceWorker !== 'undefined') { */
if (localStorage.getItem('serviceWorkerVersion') !== '11') {
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
});
localStorage.setItem('serviceWorkerVersion', '11');
}
navigator.serviceWorker.register('/sw.js', {updateViaCache: 'none'});
}
*/
</script> </script>
<div class="webApp" id="webApp"> <div class="webApp" id="webApp">
</div> </div>
<script defer src="/src/webApp.ts" type="module"></script>
<!--<video id="feed" controls="" autoplay="" name="media"><source src="https://listen.broadcastify.com/1drb2xhywkg8nvz.mp3?nc=49099&amp;xan=xtf9912b41c" type="audio/mpeg"></video> --> <!--<video id="feed" controls="" autoplay="" name="media"><source src="https://listen.broadcastify.com/1drb2xhywkg8nvz.mp3?nc=49099&amp;xan=xtf9912b41c" type="audio/mpeg"></video> -->
<!--
<div class="scene"> <div class="scene">
<canvas id="gameCanvas"></canvas> <canvas id="gameCanvas"></canvas>
</div> </div>
-->
<!--<script defer src="/src/vrApp.ts" type="module"></script>-->
</body> </body>
</html> </html>

View File

@ -0,0 +1,49 @@
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

@ -0,0 +1,216 @@
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

@ -0,0 +1,22 @@
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)
}
}
};

View File

@ -1,48 +0,0 @@
'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*'
]
}
}

6936
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,76 +1,56 @@
{ {
"name": "immersive", "name": "immersive",
"private": true, "private": true,
"version": "0.0.8-48", "version": "0.0.8-16",
"type": "module", "type": "module",
"license": "MIT",
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"scripts": { "scripts": {
"dev": "node -r newrelic server.js", "dev": "vite",
"test": "vitest", "test": "vitest",
"build": "node versionBump.js && vite build", "build": "node versionBump.js && vite build",
"start": "NODE_ENV=production node -r newrelic server.js", "preview": "vite preview",
"start:api": "API_ONLY=true node -r newrelic server.js",
"socket": "node server/server.js", "socket": "node server/server.js",
"serve": "node server.js",
"serverBuild": "cd server && tsc", "serverBuild": "cd server && tsc",
"havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps" "havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps"
}, },
"dependencies": { "dependencies": {
"@auth0/auth0-react": "^2.2.4", "@babylonjs/core": "^7.21.5",
"@babylonjs/core": "^8.16.2", "@babylonjs/gui": "^7.21.5",
"@babylonjs/gui": "^8.16.2",
"@babylonjs/havok": "1.3.4", "@babylonjs/havok": "1.3.4",
"@babylonjs/inspector": "^8.16.2", "@babylonjs/inspector": "^7.21.5",
"@babylonjs/loaders": "^8.16.2", "@babylonjs/loaders": "^7.21.5",
"@babylonjs/materials": "^8.16.2", "@babylonjs/materials": "^7.21.5",
"@babylonjs/serializers": "^8.16.2", "@babylonjs/serializers": "^7.21.5",
"@emotion/react": "^11.13.0",
"@giphy/js-fetch-api": "^5.6.0",
"@giphy/react-components": "^9.6.0",
"@mantine/core": "^7.17.8",
"@mantine/form": "^7.17.8",
"@mantine/hooks": "^7.17.8",
"@maptiler/client": "1.8.1", "@maptiler/client": "1.8.1",
"@newrelic/browser-agent": "^1.306.0",
"@picovoice/cobra-web": "^2.0.3", "@picovoice/cobra-web": "^2.0.3",
"@picovoice/eagle-web": "^1.0.0", "@picovoice/eagle-web": "^1.0.0",
"@picovoice/web-voice-processor": "^4.0.9", "@picovoice/web-voice-processor": "^4.0.9",
"@tabler/icons-react": "^3.14.0",
"@types/node": "^18.14.0", "@types/node": "^18.14.0",
"@types/react": "^18.2.72", "@types/react": "^18.2.72",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
"axios": "^1.10.0", "axios": "^1.6.8",
"canvas-hypertxt": "1.0.3", "canvas-hypertxt": "1.0.3",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"events": "^3.3.0",
"express": "^5.2.1",
"express-pouchdb": "^4.2.0",
"hash-wasm": "4.11.0",
"hls.js": "^1.1.4", "hls.js": "^1.1.4",
"js-crypto-aes": "1.0.6",
"leveldown": "^6.1.1",
"loglevel": "^1.9.1", "loglevel": "^1.9.1",
"meaningful-string": "^1.4.0",
"newrelic": "^13.9.1",
"peer-lite": "2.0.2", "peer-lite": "2.0.2",
"pouchdb": "^8.0.1", "pouchdb": "^8.0.1",
"pouchdb-adapter-leveldb": "^9.0.0",
"pouchdb-adapter-memory": "^9.0.0",
"pouchdb-find": "^8.0.1", "pouchdb-find": "^8.0.1",
"query-string": "^8.1.0", "query-string": "^8.1.0",
"react-router-dom": "^6.26.1", "react-dom": "^18.2.0",
"recordrtc": "^5.6.0", "recordrtc": "^5.6.0",
"rfc4648": "^1.5.3", "rfc4648": "^1.5.3",
"round": "^2.0.1", "round": "^2.0.1",
"uint8-to-b64": "^1.0.2",
"use-pouchdb": "^2.0.2",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vite-express": "^0.21.1", "js-crypto-aes": "1.0.6",
"websocket": "^1.0.34", "events": "^3.3.0",
"websocket-ts": "^2.1.5" "hash-wasm": "4.11.0",
"uint8-to-b64": "^1.0.2",
"meaningful-string": "^1.4.0",
"websocket-ts": "^2.1.5",
"websocket": "^1.0.34"
}, },
"devDependencies": { "devDependencies": {
"@types/dom-to-image": "^2.6.7", "@types/dom-to-image": "^2.6.7",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@ -1,40 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="3200"
height="1355.480324331485" viewBox="0 0 3200 1355.480324331485">
<g transform="scale(10) translate(10, 10)">
<defs id="SvgjsDefs1385">
<linearGradient id="SvgjsLinearGradient1390">
<stop id="SvgjsStop1391" stop-color="#905e26" offset="0"></stop>
<stop id="SvgjsStop1392" stop-color="#f5ec9b" offset="0.5"></stop>
<stop id="SvgjsStop1393" stop-color="#905e26" offset="1"></stop>
</linearGradient>
<linearGradient id="SvgjsLinearGradient1394">
<stop id="SvgjsStop1395" stop-color="#905e26" offset="0"></stop>
<stop id="SvgjsStop1396" stop-color="#f5ec9b" offset="0.5"></stop>
<stop id="SvgjsStop1397" stop-color="#905e26" offset="1"></stop>
</linearGradient>
</defs>
<g id="SvgjsG1386" featureKey="aMgJeN-0"
transform="matrix(1.5610770874511997,0,0,1.5610770874511997,71.94613967240352,-53.841545411371435)"
fill="url(#SvgjsLinearGradient1390)">
<path xmlns="http://www.w3.org/2000/svg"
d="M29.399,57.112c0.615,0.308,1.077,0.846,1.077,1.383c0,1.039-1.038,1.961-1.999,1.462l-15.223-7.881 c-0.846-0.499-1.576-0.808-1.576-1.884c0-1.115,0.692-1.385,1.576-1.922l15.223-7.881c1.038-0.346,1.999,0.424,1.999,1.462 c0,0.575-0.461,1.114-1.077,1.423l-13.761,6.918L29.399,57.112z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M29.033,60.209c-0.208,0-0.413-0.052-0.608-0.152l-15.223-7.881c-0.086-0.05-0.165-0.095-0.242-0.141 c-0.748-0.431-1.395-0.804-1.395-1.843c0-1.057,0.594-1.406,1.346-1.849c0.093-0.054,0.187-0.11,0.284-0.169l15.229-7.885 c0.195-0.066,0.377-0.097,0.558-0.097c0.9,0,1.606,0.728,1.606,1.658c0,0.587-0.437,1.172-1.139,1.522l-13.562,6.818l13.562,6.818 c0.691,0.346,1.139,0.93,1.139,1.484C30.588,59.407,29.861,60.209,29.033,60.209z M28.982,40.419c-0.156,0-0.314,0.026-0.47,0.078 L13.306,48.37c-0.091,0.057-0.188,0.113-0.281,0.167c-0.743,0.439-1.234,0.728-1.234,1.655c0,0.91,0.537,1.219,1.281,1.648 c0.079,0.045,0.158,0.09,0.239,0.139l15.217,7.877c0.162,0.084,0.332,0.127,0.504,0.127c0.697,0,1.33-0.709,1.33-1.488 c0-0.465-0.407-0.98-1.014-1.283l-13.962-7.02l13.962-7.02c0.616-0.308,1.014-0.828,1.014-1.321 C30.363,41.048,29.756,40.419,28.982,40.419z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M46.385,64.416c-0.231,0.691-0.922,1.077-1.614,0.961c-0.769-0.153-1.269-0.885-1.154-1.692 c0-0.076,0.039-0.191,0.077-0.307l9.88-27.831c0.23-0.692,0.922-1.038,1.614-0.923c0.73,0.154,1.269,0.885,1.153,1.652 c0,0.078-0.039,0.193-0.077,0.27L46.385,64.416z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M45.016,65.511c-0.088,0-0.177-0.008-0.263-0.022c-0.837-0.167-1.371-0.949-1.247-1.819 c-0.001-0.076,0.038-0.193,0.079-0.318l9.883-27.842c0.207-0.618,0.778-1.02,1.457-1.02c0.094,0,0.188,0.009,0.281,0.023 c0.812,0.172,1.369,0.97,1.247,1.781c0.001,0.086-0.047,0.22-0.087,0.302l-9.875,27.856C46.281,65.084,45.688,65.511,45.016,65.511z M54.925,34.715c-0.58,0-1.068,0.34-1.244,0.867l-9.88,27.833c-0.035,0.104-0.07,0.213-0.07,0.27 c-0.108,0.767,0.349,1.439,1.062,1.582c0.071,0.012,0.147,0.018,0.223,0.018c0.575,0,1.083-0.363,1.263-0.904l9.881-27.872 c0.041-0.085,0.07-0.182,0.07-0.231c0.105-0.712-0.373-1.396-1.064-1.543C55.089,34.722,55.007,34.715,54.925,34.715z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M84.362,50.192L70.6,43.274c-0.614-0.309-1.075-0.848-1.075-1.423c0-1.038,1.037-1.962,1.998-1.462l15.223,7.881 c0.885,0.537,1.576,0.807,1.576,1.922c0,1.076-0.73,1.385-1.576,1.884l-15.223,7.881c-0.961,0.499-1.998-0.423-1.998-1.462 c0-0.537,0.461-1.075,1.075-1.383L84.362,50.192z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M70.967,60.209c-0.828,0-1.556-0.802-1.556-1.714c0-0.555,0.447-1.139,1.139-1.484l13.562-6.818L70.55,43.374 c-0.702-0.352-1.139-0.936-1.139-1.522c0-0.913,0.728-1.714,1.556-1.714c0.209,0,0.413,0.051,0.608,0.152l15.223,7.881 c0.104,0.062,0.198,0.119,0.29,0.173c0.753,0.442,1.347,0.792,1.347,1.849c0,1.039-0.646,1.412-1.395,1.843 c-0.078,0.046-0.157,0.091-0.237,0.138l-15.228,7.884C71.38,60.157,71.176,60.209,70.967,60.209z M70.967,40.363 c-0.696,0-1.33,0.709-1.33,1.488c0,0.493,0.397,1.013,1.014,1.321l13.962,7.02l-13.962,7.02c-0.606,0.305-1.014,0.82-1.014,1.283 c0,0.779,0.634,1.488,1.33,1.488c0.172,0,0.342-0.043,0.504-0.127l15.223-7.88c0.075-0.046,0.155-0.091,0.233-0.136 c0.744-0.43,1.282-0.738,1.282-1.648c0-0.928-0.491-1.216-1.235-1.655c-0.093-0.054-0.188-0.11-0.287-0.17l-15.216-7.876 C71.309,40.405,71.139,40.363,70.967,40.363z"></path>
</g>
<g id="SvgjsG1387" featureKey="8L6ael-0"
transform="matrix(3.168568052463937,0,0,3.168568052463937,-5.1330821487142195,52.17667742743372)"
fill="url(#SvgjsLinearGradient1394)">
<path d="M10.56 5.52 q1.22 0.46 2.2 1.48 q2.1 2.12 2.1 5.41 t-2.1 5.43 q-0.98 1.02 -2.2 1.48 q-1.26 0.52 -2.58 0.52 l-5.66 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -1.44 q0 -0.3 0.21 -0.51 t0.49 -0.21 l5.6 0 q1.78 0 2.89 -1.3 t1.11 -3.26 t-1.11 -3.26 t-2.89 -1.3 l-5.6 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.44 q0 -0.28 0.21 -0.49 t0.49 -0.21 l5.66 0 q1.32 0 2.58 0.52 z M32.208 18.86 q0.14 0.32 -0.07 0.65 t-0.57 0.33 l-13.32 0 q-0.18 0 -0.34 -0.09 t-0.24 -0.23 q-0.22 -0.32 -0.06 -0.66 l0.62 -1.46 q0.08 -0.2 0.26 -0.32 t0.38 -0.12 l9.32 0 l-3.28 -7.84 l-2.68 6.4 q-0.08 0.2 -0.25 0.31 t-0.39 0.11 l-1.68 0 q-0.38 0 -0.6 -0.32 q-0.08 -0.14 -0.1 -0.32 t0.04 -0.34 l4.06 -9.52 q0.08 -0.2 0.25 -0.32 t0.39 -0.12 l1.92 0 q0.22 0 0.39 0.12 t0.25 0.32 z M45.535999999999994 12.42 q0.86 0.54 1.31 1.32 t0.45 1.74 q0 2.26 -1.72 3.46 q-1.16 0.84 -3.14 1.06 l-0.08 0 q-0.28 0 -0.46 -0.18 q-0.24 -0.22 -0.24 -0.52 l0 -1.44 q0 -0.26 0.18 -0.46 t0.44 -0.24 q0.94 -0.1 1.46 -0.44 q0.4 -0.24 0.54 -0.62 q0.08 -0.22 0.08 -0.56 q0 -0.22 -0.08 -0.38 t-0.28 -0.3 q-0.58 -0.4 -1.5 -0.68 l-0.32 -0.1 q-1 -0.28 -1.84 -0.46 q-0.18 -0.04 -0.58 -0.14 l-0.22 -0.06 q-0.72 -0.18 -1.52 -0.5 q-1.18 -0.5 -1.94 -1.26 q-0.88 -0.88 -0.88 -2.32 q0 -1.98 1.52 -3.22 q1.04 -0.88 2.92 -1.12 q0.32 -0.04 0.55 0.18 t0.23 0.52 l0 1.44 q0 0.26 -0.16 0.46 t-0.41 0.23 t-0.51 0.11 q-0.58 0.22 -0.82 0.43 t-0.34 0.43 q-0.1 0.34 -0.1 0.64 q0 0.16 0.18 0.34 q0.28 0.28 0.82 0.5 q0.24 0.1 0.84 0.3 l2.5 0.62 l0.12 0.04 q0.94 0.26 1.36 0.4 q0.94 0.32 1.64 0.78 z M42.196 7.9 q-0.24 -0.06 -0.39 -0.25 t-0.15 -0.45 l0 -1.46 q0 -0.32 0.26 -0.54 q0.1 -0.1 0.26 -0.13 t0.3 -0.01 q1.76 0.28 2.86 1.2 q1.5 1.24 1.6 3.16 q0.02 0.28 -0.19 0.51 t-0.51 0.23 l-1.56 0 q-0.26 0 -0.46 -0.18 t-0.22 -0.44 q-0.08 -0.52 -0.34 -0.86 q-0.42 -0.5 -1.3 -0.76 q-0.06 0 -0.08 -0.02 l-0.08 0 z M39.855999999999995 17.08 q0.24 0.04 0.4 0.24 t0.16 0.44 l0 1.48 q0 0.32 -0.24 0.54 q-0.2 0.16 -0.46 0.16 l-0.1 0 q-1.86 -0.28 -3.06 -1.22 q-1.56 -1.24 -1.76 -3.46 q-0.04 -0.32 0.18 -0.55 t0.52 -0.23 l1.58 0 q0.28 0 0.48 0.19 t0.22 0.47 q0.06 1 0.98 1.54 q0.42 0.24 1.1 0.4 z M60.88399999999999 11.12 q0.3 0 0.51 0.21 t0.21 0.51 l0 1.48 q0 0.28 -0.21 0.49 t-0.51 0.21 l-6.58 0 l0 5.12 q0 0.28 -0.2 0.49 t-0.5 0.21 l-1.52 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -7.3 q0 -0.3 0.21 -0.51 t0.49 -0.21 l8.8 0 z M61.78399999999999 5 q0.28 0 0.49 0.21 t0.21 0.49 l0 1.46 q0 0.3 -0.21 0.51 t-0.49 0.21 l-9.7 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.46 q0 -0.28 0.21 -0.49 t0.49 -0.21 l9.7 0 z M79.512 18.86 q0.14 0.32 -0.07 0.65 t-0.57 0.33 l-13.32 0 q-0.18 0 -0.34 -0.09 t-0.24 -0.23 q-0.22 -0.32 -0.06 -0.66 l0.62 -1.46 q0.08 -0.2 0.26 -0.32 t0.38 -0.12 l9.32 0 l-3.28 -7.84 l-2.68 6.4 q-0.08 0.2 -0.25 0.31 t-0.39 0.11 l-1.68 0 q-0.38 0 -0.6 -0.32 q-0.08 -0.14 -0.1 -0.32 t0.04 -0.34 l4.06 -9.52 q0.08 -0.2 0.25 -0.32 t0.39 -0.12 l1.92 0 q0.22 0 0.39 0.12 t0.25 0.32 z M92 5.52 q1.22 0.46 2.2 1.48 q2.1 2.12 2.1 5.41 t-2.1 5.43 q-0.98 1.02 -2.2 1.48 q-1.26 0.52 -2.58 0.52 l-5.66 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -1.44 q0 -0.3 0.21 -0.51 t0.49 -0.21 l5.6 0 q1.78 0 2.89 -1.3 t1.11 -3.26 t-1.11 -3.26 t-2.89 -1.3 l-5.6 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.44 q0 -0.28 0.21 -0.49 t0.49 -0.21 l5.66 0 q1.32 0 2.58 0.52 z"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,912 +0,0 @@
%!PS-Adobe-3.0 EPSF-3.0
%Produced by poppler pdftops version: 22.05.0 (http://poppler.freedesktop.org)
%%Creator: Chromium
%%LanguageLevel: 3
%%DocumentSuppliedResources: (atend)
%%BoundingBox: 0 0 2400 1018
%%HiResBoundingBox: 0 0 2400 1017.12
%%DocumentSuppliedResources: (atend)
%%EndComments
%%BeginProlog
%%BeginResource: procset xpdf 3.00 0
%%Copyright: Copyright 1996-2011, 2022 Glyph & Cog, LLC
/xpdf 75 dict def xpdf begin
% PDF special state
/pdfDictSize 15 def
/pdfSetup {
/setpagedevice where {
pop 2 dict begin
/Policies 1 dict dup begin /PageSize 6 def end def
{ /Duplex true def } if
currentdict end setpagedevice
} {
pop
} ifelse
} def
/pdfSetupPaper {
% Change paper size, but only if different from previous paper size otherwise
% duplex fails. PLRM specifies a tolerance of 5 pts when matching paper size
% so we use the same when checking if the size changes.
/setpagedevice where {
pop currentpagedevice
/PageSize known {
2 copy
currentpagedevice /PageSize get aload pop
exch 4 1 roll
sub abs 5 gt
3 1 roll
sub abs 5 gt
or
} {
true
} ifelse
{
2 array astore
2 dict begin
/PageSize exch def
/ImagingBBox null def
currentdict end
setpagedevice
} {
pop pop
} ifelse
} {
pop
} ifelse
} def
/pdfStartPage {
pdfDictSize dict begin
/pdfFillCS [] def
/pdfFillXform {} def
/pdfStrokeCS [] def
/pdfStrokeXform {} def
/pdfFill [0] def
/pdfStroke [0] def
/pdfFillOP false def
/pdfStrokeOP false def
/pdfOPM false def
/pdfLastFill false def
/pdfLastStroke false def
/pdfTextMat [1 0 0 1 0 0] def
/pdfFontSize 0 def
/pdfCharSpacing 0 def
/pdfTextRender 0 def
/pdfPatternCS false def
/pdfTextRise 0 def
/pdfWordSpacing 0 def
/pdfHorizScaling 1 def
/pdfTextClipPath [] def
} def
/pdfEndPage { end } def
% PDF color state
/opm { dup /pdfOPM exch def
/setoverprintmode where{pop setoverprintmode}{pop}ifelse } def
/cs { /pdfFillXform exch def dup /pdfFillCS exch def
setcolorspace } def
/CS { /pdfStrokeXform exch def dup /pdfStrokeCS exch def
setcolorspace } def
/sc { pdfLastFill not { pdfFillCS setcolorspace } if
dup /pdfFill exch def aload pop pdfFillXform setcolor
/pdfLastFill true def /pdfLastStroke false def } def
/SC { pdfLastStroke not { pdfStrokeCS setcolorspace } if
dup /pdfStroke exch def aload pop pdfStrokeXform setcolor
/pdfLastStroke true def /pdfLastFill false def } def
/op { /pdfFillOP exch def
pdfLastFill { pdfFillOP setoverprint } if } def
/OP { /pdfStrokeOP exch def
pdfLastStroke { pdfStrokeOP setoverprint } if } def
/fCol {
pdfLastFill not {
pdfFillCS setcolorspace
pdfFill aload pop pdfFillXform setcolor
pdfFillOP setoverprint
/pdfLastFill true def /pdfLastStroke false def
} if
} def
/sCol {
pdfLastStroke not {
pdfStrokeCS setcolorspace
pdfStroke aload pop pdfStrokeXform setcolor
pdfStrokeOP setoverprint
/pdfLastStroke true def /pdfLastFill false def
} if
} def
% build a font
/pdfMakeFont {
4 3 roll findfont
4 2 roll matrix scale makefont
dup length dict begin
{ 1 index /FID ne { def } { pop pop } ifelse } forall
/Encoding exch def
currentdict
end
definefont pop
} def
/pdfMakeFont16 {
exch findfont
dup length dict begin
{ 1 index /FID ne { def } { pop pop } ifelse } forall
/WMode exch def
currentdict
end
definefont pop
} def
/pdfMakeFont16L3 {
1 index /CIDFont resourcestatus {
pop pop 1 index /CIDFont findresource /CIDFontType known
} {
false
} ifelse
{
0 eq { /Identity-H } { /Identity-V } ifelse
exch 1 array astore composefont pop
} {
pdfMakeFont16
} ifelse
} def
% graphics state operators
/q { gsave pdfDictSize dict begin } def
/Q {
end grestore
/pdfLastFill where {
pop
pdfLastFill {
pdfFillOP setoverprint
} {
pdfStrokeOP setoverprint
} ifelse
} if
/pdfOPM where {
pop
pdfOPM /setoverprintmode where{pop setoverprintmode}{pop}ifelse
} if
} def
/cm { concat } def
/d { setdash } def
/i { setflat } def
/j { setlinejoin } def
/J { setlinecap } def
/M { setmiterlimit } def
/w { setlinewidth } def
% path segment operators
/m { moveto } def
/l { lineto } def
/c { curveto } def
/re { 4 2 roll moveto 1 index 0 rlineto 0 exch rlineto
neg 0 rlineto closepath } def
/h { closepath } def
% path painting operators
/S { sCol stroke } def
/Sf { fCol stroke } def
/f { fCol fill } def
/f* { fCol eofill } def
% clipping operators
/W { clip newpath } def
/W* { eoclip newpath } def
/Ws { strokepath clip newpath } def
% text state operators
/Tc { /pdfCharSpacing exch def } def
/Tf { dup /pdfFontSize exch def
dup pdfHorizScaling mul exch matrix scale
pdfTextMat matrix concatmatrix dup 4 0 put dup 5 0 put
exch findfont exch makefont setfont } def
/Tr { /pdfTextRender exch def } def
/Tp { /pdfPatternCS exch def } def
/Ts { /pdfTextRise exch def } def
/Tw { /pdfWordSpacing exch def } def
/Tz { /pdfHorizScaling exch def } def
% text positioning operators
/Td { pdfTextMat transform moveto } def
/Tm { /pdfTextMat exch def } def
% text string operators
/xyshow where {
pop
/xyshow2 {
dup length array
0 2 2 index length 1 sub {
2 index 1 index 2 copy get 3 1 roll 1 add get
pdfTextMat dtransform
4 2 roll 2 copy 6 5 roll put 1 add 3 1 roll dup 4 2 roll put
} for
exch pop
xyshow
} def
}{
/xyshow2 {
currentfont /FontType get 0 eq {
0 2 3 index length 1 sub {
currentpoint 4 index 3 index 2 getinterval show moveto
2 copy get 2 index 3 2 roll 1 add get
pdfTextMat dtransform rmoveto
} for
} {
0 1 3 index length 1 sub {
currentpoint 4 index 3 index 1 getinterval show moveto
2 copy 2 mul get 2 index 3 2 roll 2 mul 1 add get
pdfTextMat dtransform rmoveto
} for
} ifelse
pop pop
} def
} ifelse
/cshow where {
pop
/xycp {
0 3 2 roll
{
pop pop currentpoint 3 2 roll
1 string dup 0 4 3 roll put false charpath moveto
2 copy get 2 index 2 index 1 add get
pdfTextMat dtransform rmoveto
2 add
} exch cshow
pop pop
} def
}{
/xycp {
currentfont /FontType get 0 eq {
0 2 3 index length 1 sub {
currentpoint 4 index 3 index 2 getinterval false charpath moveto
2 copy get 2 index 3 2 roll 1 add get
pdfTextMat dtransform rmoveto
} for
} {
0 1 3 index length 1 sub {
currentpoint 4 index 3 index 1 getinterval false charpath moveto
2 copy 2 mul get 2 index 3 2 roll 2 mul 1 add get
pdfTextMat dtransform rmoveto
} for
} ifelse
pop pop
} def
} ifelse
/Tj {
fCol
0 pdfTextRise pdfTextMat dtransform rmoveto
currentpoint 4 2 roll
pdfTextRender 1 and 0 eq {
2 copy xyshow2
} if
pdfTextRender 3 and dup 1 eq exch 2 eq or {
3 index 3 index moveto
2 copy
currentfont /FontType get 3 eq { fCol } { sCol } ifelse
xycp currentpoint stroke moveto
} if
pdfTextRender 4 and 0 ne {
4 2 roll moveto xycp
/pdfTextClipPath [ pdfTextClipPath aload pop
{/moveto cvx}
{/lineto cvx}
{/curveto cvx}
{/closepath cvx}
pathforall ] def
currentpoint newpath moveto
} {
pop pop pop pop
} ifelse
0 pdfTextRise neg pdfTextMat dtransform rmoveto
} def
/TJm { 0.001 mul pdfFontSize mul pdfHorizScaling mul neg 0
pdfTextMat dtransform rmoveto } def
/TJmV { 0.001 mul pdfFontSize mul neg 0 exch
pdfTextMat dtransform rmoveto } def
/Tclip { pdfTextClipPath cvx exec clip newpath
/pdfTextClipPath [] def } def
/Tclip* { pdfTextClipPath cvx exec eoclip newpath
/pdfTextClipPath [] def } def
% Level 2/3 image operators
/pdfImBuf 100 string def
/pdfImStr {
2 copy exch length lt {
2 copy get exch 1 add exch
} {
()
} ifelse
} def
/skipEOD {
{ currentfile pdfImBuf readline
not { pop exit } if
(%-EOD-) eq { exit } if } loop
} def
/pdfIm { image skipEOD } def
/pdfMask {
/ReusableStreamDecode filter
skipEOD
/maskStream exch def
} def
/pdfMaskEnd { maskStream closefile } def
/pdfMaskInit {
/maskArray exch def
/maskIdx 0 def
} def
/pdfMaskSrc {
maskIdx maskArray length lt {
maskArray maskIdx get
/maskIdx maskIdx 1 add def
} {
()
} ifelse
} def
/pdfImM { fCol imagemask skipEOD } def
/pr { 2 index 2 index 3 2 roll putinterval 4 add } def
/pdfImClip {
gsave
0 2 4 index length 1 sub {
dup 4 index exch 2 copy
get 5 index div put
1 add 3 index exch 2 copy
get 3 index div put
} for
pop pop rectclip
} def
/pdfImClipEnd { grestore } def
% shading operators
/colordelta {
false 0 1 3 index length 1 sub {
dup 4 index exch get 3 index 3 2 roll get sub abs 0.004 gt {
pop true
} if
} for
exch pop exch pop
} def
/funcCol { func n array astore } def
/funcSH {
dup 0 eq {
true
} {
dup 6 eq {
false
} {
4 index 4 index funcCol dup
6 index 4 index funcCol dup
3 1 roll colordelta 3 1 roll
5 index 5 index funcCol dup
3 1 roll colordelta 3 1 roll
6 index 8 index funcCol dup
3 1 roll colordelta 3 1 roll
colordelta or or or
} ifelse
} ifelse
{
1 add
4 index 3 index add 0.5 mul exch 4 index 3 index add 0.5 mul exch
6 index 6 index 4 index 4 index 4 index funcSH
2 index 6 index 6 index 4 index 4 index funcSH
6 index 2 index 4 index 6 index 4 index funcSH
5 3 roll 3 2 roll funcSH pop pop
} {
pop 3 index 2 index add 0.5 mul 3 index 2 index add 0.5 mul
funcCol sc
dup 4 index exch mat transform m
3 index 3 index mat transform l
1 index 3 index mat transform l
mat transform l pop pop h f*
} ifelse
} def
/axialCol {
dup 0 lt {
pop t0
} {
dup 1 gt {
pop t1
} {
dt mul t0 add
} ifelse
} ifelse
func n array astore
} def
/axialSH {
dup 0 eq {
true
} {
dup 8 eq {
false
} {
2 index axialCol 2 index axialCol colordelta
} ifelse
} ifelse
{
1 add 3 1 roll 2 copy add 0.5 mul
dup 4 3 roll exch 4 index axialSH
exch 3 2 roll axialSH
} {
pop 2 copy add 0.5 mul
axialCol sc
exch dup dx mul x0 add exch dy mul y0 add
3 2 roll dup dx mul x0 add exch dy mul y0 add
dx abs dy abs ge {
2 copy yMin sub dy mul dx div add yMin m
yMax sub dy mul dx div add yMax l
2 copy yMax sub dy mul dx div add yMax l
yMin sub dy mul dx div add yMin l
h f*
} {
exch 2 copy xMin sub dx mul dy div add xMin exch m
xMax sub dx mul dy div add xMax exch l
exch 2 copy xMax sub dx mul dy div add xMax exch l
xMin sub dx mul dy div add xMin exch l
h f*
} ifelse
} ifelse
} def
/radialCol {
dup t0 lt {
pop t0
} {
dup t1 gt {
pop t1
} if
} ifelse
func n array astore
} def
/radialSH {
dup 0 eq {
true
} {
dup 8 eq {
false
} {
2 index dt mul t0 add radialCol
2 index dt mul t0 add radialCol colordelta
} ifelse
} ifelse
{
1 add 3 1 roll 2 copy add 0.5 mul
dup 4 3 roll exch 4 index radialSH
exch 3 2 roll radialSH
} {
pop 2 copy add 0.5 mul dt mul t0 add
radialCol sc
encl {
exch dup dx mul x0 add exch dup dy mul y0 add exch dr mul r0 add
0 360 arc h
dup dx mul x0 add exch dup dy mul y0 add exch dr mul r0 add
360 0 arcn h f
} {
2 copy
dup dx mul x0 add exch dup dy mul y0 add exch dr mul r0 add
a1 a2 arcn
dup dx mul x0 add exch dup dy mul y0 add exch dr mul r0 add
a2 a1 arcn h
dup dx mul x0 add exch dup dy mul y0 add exch dr mul r0 add
a1 a2 arc
dup dx mul x0 add exch dup dy mul y0 add exch dr mul r0 add
a2 a1 arc h f
} ifelse
} ifelse
} def
end
%%EndResource
/CIDInit /ProcSet findresource begin
10 dict begin
begincmap
/CMapType 1 def
/CMapName /Identity-H def
/CIDSystemInfo 3 dict dup begin
/Registry (Adobe) def
/Ordering (Identity) def
/Supplement 0 def
end def
1 begincodespacerange
<0000> <ffff>
endcodespacerange
0 usefont
1 begincidrange
<0000> <ffff> 0
endcidrange
endcmap
currentdict CMapName exch /CMap defineresource pop
end
10 dict begin
begincmap
/CMapType 1 def
/CMapName /Identity-V def
/CIDSystemInfo 3 dict dup begin
/Registry (Adobe) def
/Ordering (Identity) def
/Supplement 0 def
end def
/WMode 1 def
1 begincodespacerange
<0000> <ffff>
endcodespacerange
0 usefont
1 begincidrange
<0000> <ffff> 0
endcidrange
endcmap
currentdict CMapName exch /CMap defineresource pop
end
end
%%EndProlog
%%BeginSetup
xpdf begin
%%EndSetup
pdfStartPage
%%EndPageSetup
[] 0 d
1 i
0 j
0 J
10 M
1 w
/DeviceGray {} cs
[0] sc
/DeviceGray {} CS
[0] SC
false op
false OP
{} settransfer
0 0 2400 1017.12 re
W
q
[0.24 0 0 -0.24 0 1017.12] cm
q
0 0 10000 4234.375 re
W*
q
[48.783241 0 0 48.766369 2560.8376 -1369.56274] cm
29.399 57.112 m
30.014 57.419998 30.476 57.958 30.476 58.494999 c
30.476 59.534 29.438 60.455997 28.476999 59.957001 c
13.254 52.076 l
12.408 51.577 11.678 51.268002 11.678 50.192001 c
11.678 49.077 12.37 48.807003 13.254 48.27 c
28.476999 40.389 l
29.514999 40.042999 30.476 40.813 30.476 41.851002 c
30.476 42.426003 30.014999 42.965 29.399 43.274002 c
15.638 50.192001 l
29.399 57.112 l
h
f
29.033001 60.209 m
28.825001 60.209 28.620001 60.157001 28.425001 60.056999 c
13.202002 52.175999 l
13.116001 52.125999 13.037002 52.080997 12.960002 52.035 c
12.212002 51.604 11.565002 51.230999 11.565002 50.192001 c
11.565002 49.135002 12.159002 48.786003 12.911002 48.343002 c
13.004003 48.289001 13.098002 48.233002 13.195003 48.174004 c
28.424004 40.289001 l
28.619003 40.223 28.801004 40.192001 28.982004 40.192001 c
29.882004 40.192001 30.588005 40.920002 30.588005 41.850002 c
30.588005 42.437004 30.151005 43.022003 29.449005 43.372002 c
15.887005 50.190002 l
29.449005 57.008003 l
30.140005 57.354004 30.588005 57.938004 30.588005 58.492004 c
30.587999 59.407001 29.861 60.209 29.033001 60.209 c
h
28.982 40.418999 m
28.826 40.418999 28.668001 40.445 28.512001 40.496998 c
13.306 48.369999 l
13.215 48.426998 13.118 48.482998 13.025 48.536999 c
12.282 48.975998 11.790999 49.264999 11.790999 50.191998 c
11.790999 51.101997 12.327999 51.410999 13.072 51.839996 c
13.151 51.884995 13.23 51.929996 13.311 51.978996 c
28.528 59.855995 l
28.690001 59.939995 28.860001 59.982994 29.032 59.982994 c
29.729 59.982994 30.362 59.273994 30.362 58.494995 c
30.362 58.029995 29.955 57.514996 29.348 57.211994 c
15.386 50.191994 l
29.348 43.171993 l
29.963999 42.863995 30.362 42.343994 30.362 41.850994 c
30.363001 41.048 29.756001 40.418999 28.982 40.418999 c
h
f
46.384998 64.416 m
46.153999 65.107002 45.462997 65.493004 44.771 65.376999 c
44.001999 65.223999 43.501999 64.491997 43.617001 63.684998 c
43.617001 63.608997 43.656002 63.493996 43.694 63.377998 c
53.574001 35.546997 l
53.804001 34.854996 54.496002 34.508999 55.188 34.623997 c
55.917999 34.777996 56.457001 35.508995 56.341 36.275997 c
56.341 36.353996 56.301998 36.468998 56.264 36.545998 c
46.384998 64.416 l
h
f
45.015999 65.511002 m
44.927998 65.511002 44.839001 65.502998 44.752998 65.488998 c
43.915997 65.321999 43.382 64.540001 43.505997 63.669998 c
43.504997 63.593998 43.543995 63.476997 43.584995 63.351997 c
53.467995 35.509998 l
53.674995 34.891998 54.245995 34.489998 54.924995 34.489998 c
55.018997 34.489998 55.112995 34.498997 55.205994 34.512997 c
56.017994 34.684998 56.574993 35.482998 56.452995 36.293995 c
56.453995 36.379993 56.405994 36.513996 56.365993 36.595993 c
46.490993 64.451996 l
46.280998 65.084 45.688 65.511002 45.015999 65.511002 c
h
54.924999 34.715 m
54.344997 34.715 53.856998 35.055 53.681 35.582001 c
43.800999 63.415001 l
43.765999 63.519001 43.730999 63.628002 43.730999 63.685001 c
43.622997 64.452003 44.079998 65.124001 44.792999 65.266998 c
44.863998 65.278999 44.939999 65.284996 45.015999 65.284996 c
45.591 65.284996 46.098999 64.921997 46.278999 64.380997 c
56.16 36.508995 l
56.201 36.423996 56.23 36.326996 56.23 36.277996 c
56.334999 35.565994 55.856998 34.881996 55.166 34.734997 c
55.089001 34.722 55.007 34.715 54.924999 34.715 c
h
f
84.362 50.192001 m
70.599998 43.273998 l
69.986 42.964996 69.525002 42.425999 69.525002 41.850998 c
69.525002 40.813 70.562004 39.888996 71.523003 40.388996 c
86.746002 48.269997 l
87.631004 48.806995 88.321999 49.076996 88.321999 50.191998 c
88.321999 51.267998 87.591995 51.576996 86.746002 52.075996 c
71.523003 59.956997 l
70.562004 60.455997 69.525002 59.533997 69.525002 58.494995 c
69.525002 57.957996 69.986 57.419994 70.599998 57.111996 c
84.362 50.192001 l
h
f
70.967003 60.209 m
70.139 60.209 69.411003 59.407001 69.411003 58.494999 c
69.411003 57.939999 69.858002 57.355999 70.550003 57.010998 c
84.112 50.192997 l
70.550003 43.374001 l
69.848 43.021999 69.411003 42.438 69.411003 41.852001 c
69.411003 40.939003 70.139 40.138 70.967003 40.138 c
71.176003 40.138 71.380005 40.188999 71.575005 40.290001 c
86.798004 48.171001 l
86.902 48.233002 86.996002 48.290001 87.088005 48.344002 c
87.841003 48.786003 88.435005 49.136002 88.435005 50.193001 c
88.435005 51.232002 87.789001 51.605 87.040009 52.035999 c
86.962006 52.082001 86.883011 52.126999 86.803009 52.174 c
71.575012 60.057999 l
71.379997 60.157001 71.176003 60.209 70.967003 60.209 c
h
70.967003 40.362999 m
70.271004 40.362999 69.637001 41.071999 69.637001 41.850998 c
69.637001 42.343998 70.034004 42.863998 70.651001 43.171997 c
84.612999 50.191998 l
70.651001 57.211998 l
70.044998 57.516998 69.637001 58.031998 69.637001 58.494999 c
69.637001 59.273998 70.271004 59.982998 70.967003 59.982998 c
71.139 59.982998 71.309006 59.939999 71.471001 59.855999 c
86.694 51.975998 l
86.768997 51.929996 86.848999 51.884998 86.927002 51.839996 c
87.671005 51.409996 88.209 51.101997 88.209 50.191998 c
88.209 49.263996 87.718002 48.975998 86.973999 48.536999 c
86.880997 48.482998 86.785995 48.426998 86.686996 48.367001 c
71.470993 40.491001 l
71.308998 40.404999 71.139 40.362999 70.967003 40.362999 c
h
f
Q
q
[99.016907 0 0 98.982658 152.13266 1942.3326] cm
10.56 5.52 m
11.373334 5.826667 12.106668 6.32 12.76 7 c
14.160001 8.413333 14.860001 10.216666 14.860001 12.41 c
14.860001 14.603334 14.160001 16.413334 12.76 17.84 c
12.106668 18.52 11.373334 19.013334 10.56 19.32 c
9.72 19.666666 8.860001 19.84 7.980001 19.84 c
2.320001 19.84 l
2.133334 19.84 1.970001 19.77 1.830001 19.630001 c
1.690001 19.490002 1.620001 19.326668 1.620001 19.140001 c
1.620001 17.700001 l
1.620001 17.500002 1.690001 17.330002 1.830001 17.190001 c
1.970001 17.049999 2.133334 16.98 2.320001 16.980001 c
7.920001 16.980001 l
9.106668 16.980001 10.070001 16.546669 10.81 15.680001 c
11.55 14.813335 11.92 13.726667 11.92 12.420001 c
11.92 11.113335 11.55 10.026668 10.81 9.160001 c
10.070001 8.293334 9.106668 7.860001 7.920001 7.860001 c
2.320001 7.860001 l
2.133334 7.860001 1.970001 7.79 1.830001 7.650001 c
1.690001 7.510001 1.620001 7.340001 1.620001 7.14 c
1.620001 5.7 l
1.620001 5.513333 1.690001 5.35 1.830001 5.21 c
1.970001 5.07 2.133334 5 2.320001 5 c
7.980001 5 l
8.860001 5 9.72 5.173333 10.56 5.52 c
h
32.208 18.860001 m
32.301334 19.073334 32.278 19.290001 32.138 19.51 c
31.998001 19.73 31.808001 19.84 31.568001 19.84 c
18.248001 19.84 l
18.128 19.84 18.014668 19.809999 17.908001 19.75 c
17.801334 19.690001 17.721334 19.613333 17.668001 19.52 c
17.521336 19.306667 17.501335 19.086668 17.608002 18.860001 c
18.228003 17.400002 l
18.281336 17.266668 18.368002 17.160002 18.488003 17.080002 c
18.608004 17.000002 18.734669 16.960003 18.868002 16.960001 c
28.188002 16.960001 l
24.908001 9.120001 l
22.228001 15.520001 l
22.174667 15.653334 22.091333 15.756667 21.978001 15.830001 c
21.864668 15.903335 21.734667 15.940002 21.588001 15.940001 c
19.908001 15.940001 l
19.654669 15.940001 19.454668 15.833334 19.308001 15.620001 c
19.254667 15.526668 19.221334 15.420001 19.208 15.300001 c
19.194666 15.180001 19.208 15.066669 19.248001 14.960001 c
23.308001 5.440001 l
23.361334 5.306667 23.444668 5.200001 23.558001 5.12 c
23.671333 5.04 23.801334 5 23.948 5.000001 c
25.868 5.000001 l
26.014666 5.000001 26.144667 5.04 26.257999 5.12 c
26.371332 5.2 26.454666 5.306667 26.507999 5.440001 c
32.208 18.860001 l
h
45.535999 12.42 m
46.109333 12.78 46.546001 13.22 46.846001 13.74 c
47.146 14.259999 47.296001 14.839999 47.296001 15.48 c
47.296001 16.986666 46.722668 18.139999 45.576 18.939999 c
44.802666 19.499998 43.756001 19.853333 42.436001 19.999998 c
42.355999 19.999998 l
42.169334 19.999998 42.015999 19.939999 41.896 19.819998 c
41.736 19.673332 41.655998 19.499998 41.655998 19.299997 c
41.655998 17.859997 l
41.655998 17.686663 41.716 17.533331 41.835999 17.399998 c
41.955997 17.266665 42.102665 17.186665 42.275997 17.159998 c
42.902664 17.09333 43.389328 16.946665 43.735996 16.719997 c
44.002663 16.559998 44.182663 16.353331 44.275997 16.099997 c
44.32933 15.95333 44.355999 15.766663 44.355999 15.539996 c
44.355999 15.39333 44.32933 15.266663 44.275997 15.159996 c
44.222664 15.05333 44.12933 14.953329 43.995998 14.859996 c
43.609329 14.593329 43.109329 14.366663 42.495998 14.179996 c
42.175999 14.079995 l
41.509331 13.893329 40.896 13.739995 40.335999 13.619995 c
40.216 13.593328 40.022663 13.546661 39.755997 13.479995 c
39.535995 13.419994 l
39.055996 13.299995 38.549328 13.133327 38.015995 12.919994 c
37.229328 12.586661 36.582661 12.166661 36.075996 11.659994 c
35.48933 11.073327 35.195995 10.299995 35.195995 9.339994 c
35.195995 8.019995 35.702663 6.946661 36.715996 6.119994 c
37.409328 5.533328 38.382664 5.159994 39.635994 4.999994 c
39.849327 4.973328 40.032661 5.033328 40.185993 5.179994 c
40.339325 5.326661 40.415993 5.499994 40.415993 5.699994 c
40.415993 7.139994 l
40.415993 7.313327 40.362659 7.466661 40.255993 7.599994 c
40.149326 7.733328 40.012661 7.809994 39.845993 7.829994 c
39.679325 7.849994 39.509327 7.886661 39.335995 7.939994 c
38.949326 8.08666 38.675995 8.229994 38.515995 8.369994 c
38.355995 8.509995 38.242664 8.653328 38.175995 8.799995 c
38.109329 9.026661 38.075996 9.239995 38.075996 9.439995 c
38.075996 9.546661 38.135998 9.659995 38.255997 9.779995 c
38.442661 9.966662 38.715996 10.133328 39.075996 10.279995 c
39.235996 10.346662 39.515999 10.446662 39.915997 10.579995 c
42.415997 11.199995 l
42.535995 11.239995 l
43.162663 11.413328 43.615993 11.546661 43.895996 11.639995 c
44.522663 11.853328 45.069328 12.113328 45.535995 12.419994 c
45.535999 12.42 l
h
42.195999 7.9 m
42.035999 7.86 41.905998 7.776667 41.806 7.65 c
41.706001 7.523334 41.656002 7.373334 41.655998 7.2 c
41.655998 5.74 l
41.655998 5.526667 41.742664 5.346667 41.915997 5.2 c
41.982662 5.133334 42.069328 5.09 42.175995 5.07 c
42.282661 5.05 42.38266 5.046667 42.475994 5.06 c
43.649326 5.246667 44.602661 5.646667 45.335995 6.26 c
46.335995 7.086667 46.869328 8.14 46.935993 9.42 c
46.949326 9.606667 46.885994 9.776667 46.745995 9.93 c
46.605995 10.083334 46.435997 10.160001 46.235996 10.16 c
44.675995 10.16 l
44.502663 10.16 44.349331 10.099999 44.215996 9.98 c
44.082661 9.86 44.009327 9.713333 43.995995 9.54 c
43.942661 9.193334 43.829327 8.906667 43.655994 8.68 c
43.375996 8.346667 42.942661 8.093333 42.355995 7.92 c
42.315994 7.92 42.289326 7.913333 42.275993 7.9 c
42.195992 7.9 l
42.195999 7.9 l
h
39.855999 17.08 m
40.015999 17.106667 40.149334 17.186666 40.256001 17.32 c
40.362667 17.453333 40.416 17.599998 40.416 17.76 c
40.416 19.24 l
40.416 19.453333 40.335999 19.633333 40.175999 19.780001 c
40.042664 19.886667 39.889332 19.940001 39.716 19.940001 c
39.616001 19.940001 l
38.375999 19.753334 37.355999 19.346666 36.556 18.720001 c
35.515999 17.893335 34.929333 16.740002 34.796001 15.260001 c
34.769333 15.046668 34.829334 14.863335 34.976002 14.710001 c
35.122669 14.556667 35.296001 14.480001 35.496002 14.480001 c
37.076004 14.480001 l
37.262669 14.480001 37.422668 14.543335 37.556004 14.670001 c
37.689339 14.796667 37.762672 14.953334 37.776005 15.140001 c
37.816006 15.806668 38.142673 16.320002 38.756004 16.68 c
39.036003 16.84 39.402668 16.973333 39.856003 17.08 c
39.855999 17.08 l
h
60.883999 11.12 m
61.084 11.12 61.253998 11.19 61.393997 11.33 c
61.533997 11.47 61.603996 11.64 61.603996 11.84 c
61.603996 13.32 l
61.603996 13.506666 61.533997 13.669999 61.393997 13.81 c
61.253998 13.95 61.084 14.02 60.883999 14.02 c
54.304001 14.02 l
54.304001 19.139999 l
54.304001 19.326666 54.237335 19.49 54.104 19.629999 c
53.970665 19.769999 53.804001 19.839998 53.604 19.839998 c
52.084 19.839998 l
51.897335 19.839998 51.734001 19.769999 51.593998 19.629999 c
51.453995 19.49 51.383995 19.326666 51.383999 19.139999 c
51.383999 11.839999 l
51.383999 11.639999 51.453999 11.469999 51.593998 11.329999 c
51.733997 11.189999 51.897331 11.119999 52.084 11.119999 c
60.883999 11.119999 l
60.883999 11.12 l
h
61.784 5 m
61.970665 5 62.133999 5.07 62.274002 5.21 c
62.414005 5.35 62.484005 5.513333 62.484001 5.7 c
62.484001 7.16 l
62.484001 7.36 62.414001 7.53 62.274002 7.67 c
62.134003 7.81 61.970669 7.88 61.784 7.88 c
52.084 7.88 l
51.897335 7.88 51.734001 7.81 51.593998 7.67 c
51.453995 7.53 51.383995 7.36 51.383999 7.16 c
51.383999 5.7 l
51.383999 5.513333 51.453999 5.35 51.593998 5.21 c
51.733997 5.07 51.897331 5.000001 52.084 5 c
61.784 5 l
h
79.512001 18.860001 m
79.605331 19.073334 79.582001 19.290001 79.442001 19.51 c
79.302002 19.73 79.112 19.84 78.872002 19.84 c
65.552002 19.84 l
65.431999 19.84 65.318672 19.809999 65.212006 19.75 c
65.105339 19.690001 65.025345 19.613333 64.972008 19.52 c
64.82534 19.306667 64.805344 19.086668 64.91201 18.860001 c
65.532013 17.400002 l
65.58535 17.266668 65.672012 17.160002 65.792015 17.080002 c
65.912018 17.000002 66.038681 16.960003 66.172012 16.960001 c
75.492012 16.960001 l
72.212013 9.120001 l
69.532013 15.520001 l
69.478676 15.653334 69.395348 15.756667 69.282013 15.830001 c
69.168678 15.903335 69.038681 15.940002 68.892014 15.940001 c
67.212013 15.940001 l
66.958679 15.940001 66.758682 15.833334 66.612015 15.620001 c
66.558678 15.526668 66.525345 15.420001 66.512016 15.300001 c
66.498688 15.180001 66.512016 15.066669 66.552017 14.960001 c
70.612015 5.440001 l
70.665352 5.306667 70.74868 5.200001 70.862015 5.12 c
70.975349 5.04 71.105347 5 71.252014 5.000001 c
73.172012 5.000001 l
73.31868 5.000001 73.448677 5.04 73.562012 5.12 c
73.675346 5.2 73.758675 5.306667 73.812012 5.440001 c
79.512001 18.860001 l
h
92 5.52 m
92.813332 5.826667 93.546669 6.32 94.199997 7 c
95.599998 8.413333 96.299995 10.216666 96.299995 12.41 c
96.299995 14.603334 95.599998 16.413334 94.199997 17.84 c
93.546661 18.52 92.813332 19.013334 92 19.32 c
91.159996 19.666666 90.299995 19.84 89.419998 19.84 c
83.759995 19.84 l
83.573326 19.84 83.409996 19.77 83.269997 19.630001 c
83.129997 19.490002 83.059998 19.326668 83.059998 19.140001 c
83.059998 17.700001 l
83.059998 17.500002 83.129997 17.330002 83.269997 17.190001 c
83.409996 17.049999 83.573326 16.98 83.759995 16.980001 c
89.359993 16.980001 l
90.546661 16.980001 91.509995 16.546669 92.249992 15.680001 c
92.98999 14.813335 93.359993 13.726667 93.359993 12.420001 c
93.359993 11.113335 92.98999 10.026668 92.249992 9.160001 c
91.509995 8.293334 90.546661 7.860001 89.359993 7.860001 c
83.759995 7.860001 l
83.573326 7.860001 83.409996 7.79 83.269997 7.650001 c
83.129997 7.510001 83.059998 7.340001 83.059998 7.14 c
83.059998 5.7 l
83.059998 5.513333 83.129997 5.35 83.269997 5.21 c
83.409996 5.07 83.573326 5 83.759995 5 c
89.419998 5 l
90.299995 5 91.159996 5.173333 92 5.52 c
h
f
Q
Q
Q
showpage
%%PageTrailer
pdfEndPage
%%Trailer
end
%%DocumentSuppliedResources:
%%EOF

View File

@ -1,920 +0,0 @@
%!PS-Adobe-3.0 EPSF-3.0
%Produced by poppler pdftops version: 22.05.0 (http://poppler.freedesktop.org)
%%Creator: Chromium
%%LanguageLevel: 3
%%DocumentSuppliedResources: (atend)
%%BoundingBox: 0 0 2400 1018
%%HiResBoundingBox: 0 0 2400 1017.12
%%DocumentSuppliedResources: (atend)
%%EndComments
%%BeginProlog
%%BeginResource: procset xpdf 3.00 0
%%Copyright: Copyright 1996-2011, 2022 Glyph & Cog, LLC
/xpdf 75 dict def xpdf begin
% PDF special state
/pdfDictSize 15 def
/pdfSetup {
/setpagedevice where {
pop 2 dict begin
/Policies 1 dict dup begin /PageSize 6 def end def
{ /Duplex true def } if
currentdict end setpagedevice
} {
pop
} ifelse
} def
/pdfSetupPaper {
% Change paper size, but only if different from previous paper size otherwise
% duplex fails. PLRM specifies a tolerance of 5 pts when matching paper size
% so we use the same when checking if the size changes.
/setpagedevice where {
pop currentpagedevice
/PageSize known {
2 copy
currentpagedevice /PageSize get aload pop
exch 4 1 roll
sub abs 5 gt
3 1 roll
sub abs 5 gt
or
} {
true
} ifelse
{
2 array astore
2 dict begin
/PageSize exch def
/ImagingBBox null def
currentdict end
setpagedevice
} {
pop pop
} ifelse
} {
pop
} ifelse
} def
/pdfStartPage {
pdfDictSize dict begin
/pdfFillCS [] def
/pdfFillXform {} def
/pdfStrokeCS [] def
/pdfStrokeXform {} def
/pdfFill [0] def
/pdfStroke [0] def
/pdfFillOP false def
/pdfStrokeOP false def
/pdfOPM false def
/pdfLastFill false def
/pdfLastStroke false def
/pdfTextMat [1 0 0 1 0 0] def
/pdfFontSize 0 def
/pdfCharSpacing 0 def
/pdfTextRender 0 def
/pdfPatternCS false def
/pdfTextRise 0 def
/pdfWordSpacing 0 def
/pdfHorizScaling 1 def
/pdfTextClipPath [] def
} def
/pdfEndPage { end } def
% PDF color state
/opm { dup /pdfOPM exch def
/setoverprintmode where{pop setoverprintmode}{pop}ifelse } def
/cs { /pdfFillXform exch def dup /pdfFillCS exch def
setcolorspace } def
/CS { /pdfStrokeXform exch def dup /pdfStrokeCS exch def
setcolorspace } def
/sc { pdfLastFill not { pdfFillCS setcolorspace } if
dup /pdfFill exch def aload pop pdfFillXform setcolor
/pdfLastFill true def /pdfLastStroke false def } def
/SC { pdfLastStroke not { pdfStrokeCS setcolorspace } if
dup /pdfStroke exch def aload pop pdfStrokeXform setcolor
/pdfLastStroke true def /pdfLastFill false def } def
/op { /pdfFillOP exch def
pdfLastFill { pdfFillOP setoverprint } if } def
/OP { /pdfStrokeOP exch def
pdfLastStroke { pdfStrokeOP setoverprint } if } def
/fCol {
pdfLastFill not {
pdfFillCS setcolorspace
pdfFill aload pop pdfFillXform setcolor
pdfFillOP setoverprint
/pdfLastFill true def /pdfLastStroke false def
} if
} def
/sCol {
pdfLastStroke not {
pdfStrokeCS setcolorspace
pdfStroke aload pop pdfStrokeXform setcolor
pdfStrokeOP setoverprint
/pdfLastStroke true def /pdfLastFill false def
} if
} def
% build a font
/pdfMakeFont {
4 3 roll findfont
4 2 roll matrix scale makefont
dup length dict begin
{ 1 index /FID ne { def } { pop pop } ifelse } forall
/Encoding exch def
currentdict
end
definefont pop
} def
/pdfMakeFont16 {
exch findfont
dup length dict begin
{ 1 index /FID ne { def } { pop pop } ifelse } forall
/WMode exch def
currentdict
end
definefont pop
} def
/pdfMakeFont16L3 {
1 index /CIDFont resourcestatus {
pop pop 1 index /CIDFont findresource /CIDFontType known
} {
false
} ifelse
{
0 eq { /Identity-H } { /Identity-V } ifelse
exch 1 array astore composefont pop
} {
pdfMakeFont16
} ifelse
} def
% graphics state operators
/q { gsave pdfDictSize dict begin } def
/Q {
end grestore
/pdfLastFill where {
pop
pdfLastFill {
pdfFillOP setoverprint
} {
pdfStrokeOP setoverprint
} ifelse
} if
/pdfOPM where {
pop
pdfOPM /setoverprintmode where{pop setoverprintmode}{pop}ifelse
} if
} def
/cm { concat } def
/d { setdash } def
/i { setflat } def
/j { setlinejoin } def
/J { setlinecap } def
/M { setmiterlimit } def
/w { setlinewidth } def
% path segment operators
/m { moveto } def
/l { lineto } def
/c { curveto } def
/re { 4 2 roll moveto 1 index 0 rlineto 0 exch rlineto
neg 0 rlineto closepath } def
/h { closepath } def
% path painting operators
/S { sCol stroke } def
/Sf { fCol stroke } def
/f { fCol fill } def
/f* { fCol eofill } def
% clipping operators
/W { clip newpath } def
/W* { eoclip newpath } def
/Ws { strokepath clip newpath } def
% text state operators
/Tc { /pdfCharSpacing exch def } def
/Tf { dup /pdfFontSize exch def
dup pdfHorizScaling mul exch matrix scale
pdfTextMat matrix concatmatrix dup 4 0 put dup 5 0 put
exch findfont exch makefont setfont } def
/Tr { /pdfTextRender exch def } def
/Tp { /pdfPatternCS exch def } def
/Ts { /pdfTextRise exch def } def
/Tw { /pdfWordSpacing exch def } def
/Tz { /pdfHorizScaling exch def } def
% text positioning operators
/Td { pdfTextMat transform moveto } def
/Tm { /pdfTextMat exch def } def
% text string operators
/xyshow where {
pop
/xyshow2 {
dup length array
0 2 2 index length 1 sub {
2 index 1 index 2 copy get 3 1 roll 1 add get
pdfTextMat dtransform
4 2 roll 2 copy 6 5 roll put 1 add 3 1 roll dup 4 2 roll put
} for
exch pop
xyshow
} def
}{
/xyshow2 {
currentfont /FontType get 0 eq {
0 2 3 index length 1 sub {
currentpoint 4 index 3 index 2 getinterval show moveto
2 copy get 2 index 3 2 roll 1 add get
pdfTextMat dtransform rmoveto
} for
} {
0 1 3 index length 1 sub {
currentpoint 4 index 3 index 1 getinterval show moveto
2 copy 2 mul get 2 index 3 2 roll 2 mul 1 add get
pdfTextMat dtransform rmoveto
} for
} ifelse
pop pop
} def
} ifelse
/cshow where {
pop
/xycp {
0 3 2 roll
{
pop pop currentpoint 3 2 roll
1 string dup 0 4 3 roll put false charpath moveto
2 copy get 2 index 2 index 1 add get
pdfTextMat dtransform rmoveto
2 add
} exch cshow
pop pop
} def
}{
/xycp {
currentfont /FontType get 0 eq {
0 2 3 index length 1 sub {
currentpoint 4 index 3 index 2 getinterval false charpath moveto
2 copy get 2 index 3 2 roll 1 add get
pdfTextMat dtransform rmoveto
} for
} {
0 1 3 index length 1 sub {
currentpoint 4 index 3 index 1 getinterval false charpath moveto
2 copy 2 mul get 2 index 3 2 roll 2 mul 1 add get
pdfTextMat dtransform rmoveto
} for
} ifelse
pop pop
} def
} ifelse
/Tj {
fCol
0 pdfTextRise pdfTextMat dtransform rmoveto
currentpoint 4 2 roll
pdfTextRender 1 and 0 eq {
2 copy xyshow2
} if
pdfTextRender 3 and dup 1 eq exch 2 eq or {
3 index 3 index moveto
2 copy
currentfont /FontType get 3 eq { fCol } { sCol } ifelse
xycp currentpoint stroke moveto
} if
pdfTextRender 4 and 0 ne {
4 2 roll moveto xycp
/pdfTextClipPath [ pdfTextClipPath aload pop
{/moveto cvx}
{/lineto cvx}
{/curveto cvx}
{/closepath cvx}
pathforall ] def
currentpoint newpath moveto
} {
pop pop pop pop
} ifelse
0 pdfTextRise neg pdfTextMat dtransform rmoveto
} def
/TJm { 0.001 mul pdfFontSize mul pdfHorizScaling mul neg 0
pdfTextMat dtransform rmoveto } def
/TJmV { 0.001 mul pdfFontSize mul neg 0 exch
pdfTextMat dtransform rmoveto } def
/Tclip { pdfTextClipPath cvx exec clip newpath
/pdfTextClipPath [] def } def
/Tclip* { pdfTextClipPath cvx exec eoclip newpath
/pdfTextClipPath [] def } def
% Level 2/3 image operators
/pdfImBuf 100 string def
/pdfImStr {
2 copy exch length lt {
2 copy get exch 1 add exch
} {
()
} ifelse
} def
/skipEOD {
{ currentfile pdfImBuf readline
not { pop exit } if
(%-EOD-) eq { exit } if } loop
} def
/pdfIm { image skipEOD } def
/pdfMask {
/ReusableStreamDecode filter
skipEOD
/maskStream exch def
} def
/pdfMaskEnd { maskStream closefile } def
/pdfMaskInit {
/maskArray exch def
/maskIdx 0 def
} def
/pdfMaskSrc {
maskIdx maskArray length lt {
maskArray maskIdx get
/maskIdx maskIdx 1 add def
} {
()
} ifelse
} def
/pdfImM { fCol imagemask skipEOD } def
/pr { 2 index 2 index 3 2 roll putinterval 4 add } def
/pdfImClip {
gsave
0 2 4 index length 1 sub {
dup 4 index exch 2 copy
get 5 index div put
1 add 3 index exch 2 copy
get 3 index div put
} for
pop pop rectclip
} def
/pdfImClipEnd { grestore } def
% shading operators
/colordelta {
false 0 1 3 index length 1 sub {
dup 4 index exch get 3 index 3 2 roll get sub abs 0.004 gt {
pop true
} if
} for
exch pop exch pop
} def
/funcCol { func n array astore } def
/funcSH {
dup 0 eq {
true
} {
dup 6 eq {
false
} {
4 index 4 index funcCol dup
6 index 4 index funcCol dup
3 1 roll colordelta 3 1 roll
5 index 5 index funcCol dup
3 1 roll colordelta 3 1 roll
6 index 8 index funcCol dup
3 1 roll colordelta 3 1 roll
colordelta or or or
} ifelse
} ifelse
{
1 add
4 index 3 index add 0.5 mul exch 4 index 3 index add 0.5 mul exch
6 index 6 index 4 index 4 index 4 index funcSH
2 index 6 index 6 index 4 index 4 index funcSH
6 index 2 index 4 index 6 index 4 index funcSH
5 3 roll 3 2 roll funcSH pop pop
} {
pop 3 index 2 index add 0.5 mul 3 index 2 index add 0.5 mul
funcCol sc
dup 4 index exch mat transform m
3 index 3 index mat transform l
1 index 3 index mat transform l
mat transform l pop pop h f*
} ifelse
} def
/axialCol {
dup 0 lt {
pop t0
} {
dup 1 gt {
pop t1
} {
dt mul t0 add
} ifelse
} ifelse
func n array astore
} def
/axialSH {
dup 0 eq {
true
} {
dup 8 eq {
false
} {
2 index axialCol 2 index axialCol colordelta
} ifelse
} ifelse
{
1 add 3 1 roll 2 copy add 0.5 mul
dup 4 3 roll exch 4 index axialSH
exch 3 2 roll axialSH
} {
pop 2 copy add 0.5 mul
axialCol sc
exch dup dx mul x0 add exch dy mul y0 add
3 2 roll dup dx mul x0 add exch dy mul y0 add
dx abs dy abs ge {
2 copy yMin sub dy mul dx div add yMin m
yMax sub dy mul dx div add yMax l
2 copy yMax sub dy mul dx div add yMax l
yMin sub dy mul dx div add yMin l
h f*
} {
exch 2 copy xMin sub dx mul dy div add xMin exch m
xMax sub dx mul dy div add xMax exch l
exch 2 copy xMax sub dx mul dy div add xMax exch l
xMin sub dx mul dy div add xMin exch l
h f*
} ifelse
} ifelse
} def
/radialCol {
dup t0 lt {
pop t0
} {
dup t1 gt {
pop t1
} if
} ifelse
func n array astore
} def
/radialSH {
dup 0 eq {
true
} {
dup 8 eq {
false
} {
2 index dt mul t0 add radialCol
2 index dt mul t0 add radialCol colordelta
} ifelse
} ifelse
{
1 add 3 1 roll 2 copy add 0.5 mul
dup 4 3 roll exch 4 index radialSH
exch 3 2 roll radialSH
} {
pop 2 copy add 0.5 mul dt mul t0 add
radialCol sc
encl {
exch dup dx mul x0 add exch dup dy mul y0 add exch dr mul r0 add
0 360 arc h
dup dx mul x0 add exch dup dy mul y0 add exch dr mul r0 add
360 0 arcn h f
} {
2 copy
dup dx mul x0 add exch dup dy mul y0 add exch dr mul r0 add
a1 a2 arcn
dup dx mul x0 add exch dup dy mul y0 add exch dr mul r0 add
a2 a1 arcn h
dup dx mul x0 add exch dup dy mul y0 add exch dr mul r0 add
a1 a2 arc
dup dx mul x0 add exch dup dy mul y0 add exch dr mul r0 add
a2 a1 arc h f
} ifelse
} ifelse
} def
end
%%EndResource
/CIDInit /ProcSet findresource begin
10 dict begin
begincmap
/CMapType 1 def
/CMapName /Identity-H def
/CIDSystemInfo 3 dict dup begin
/Registry (Adobe) def
/Ordering (Identity) def
/Supplement 0 def
end def
1 begincodespacerange
<0000> <ffff>
endcodespacerange
0 usefont
1 begincidrange
<0000> <ffff> 0
endcidrange
endcmap
currentdict CMapName exch /CMap defineresource pop
end
10 dict begin
begincmap
/CMapType 1 def
/CMapName /Identity-V def
/CIDSystemInfo 3 dict dup begin
/Registry (Adobe) def
/Ordering (Identity) def
/Supplement 0 def
end def
/WMode 1 def
1 begincodespacerange
<0000> <ffff>
endcodespacerange
0 usefont
1 begincidrange
<0000> <ffff> 0
endcidrange
endcmap
currentdict CMapName exch /CMap defineresource pop
end
end
%%EndProlog
%%BeginSetup
xpdf begin
%%EndSetup
pdfStartPage
%%EndPageSetup
[] 0 d
1 i
0 j
0 J
10 M
1 w
/DeviceGray {} cs
[0] sc
/DeviceGray {} CS
[0] SC
false op
false OP
{} settransfer
0 0 2400 1017.12 re
W
q
[0.24 0 0 -0.24 0 1017.12] cm
q
0 0 10000 4234.375 re
W*
q
[48.783241 0 0 48.766369 2560.8376 -1369.56274] cm
/DeviceRGB {} CS
[1 1 1] SC
/DeviceRGB {} cs
[1 1 1] sc
29.399 57.112 m
30.014 57.419998 30.476 57.958 30.476 58.494999 c
30.476 59.534 29.438 60.455997 28.476999 59.957001 c
13.254 52.076 l
12.408 51.577 11.678 51.268002 11.678 50.192001 c
11.678 49.077 12.37 48.807003 13.254 48.27 c
28.476999 40.389 l
29.514999 40.042999 30.476 40.813 30.476 41.851002 c
30.476 42.426003 30.014999 42.965 29.399 43.274002 c
15.638 50.192001 l
29.399 57.112 l
h
f
29.033001 60.209 m
28.825001 60.209 28.620001 60.157001 28.425001 60.056999 c
13.202002 52.175999 l
13.116001 52.125999 13.037002 52.080997 12.960002 52.035 c
12.212002 51.604 11.565002 51.230999 11.565002 50.192001 c
11.565002 49.135002 12.159002 48.786003 12.911002 48.343002 c
13.004003 48.289001 13.098002 48.233002 13.195003 48.174004 c
28.424004 40.289001 l
28.619003 40.223 28.801004 40.192001 28.982004 40.192001 c
29.882004 40.192001 30.588005 40.920002 30.588005 41.850002 c
30.588005 42.437004 30.151005 43.022003 29.449005 43.372002 c
15.887005 50.190002 l
29.449005 57.008003 l
30.140005 57.354004 30.588005 57.938004 30.588005 58.492004 c
30.587999 59.407001 29.861 60.209 29.033001 60.209 c
h
28.982 40.418999 m
28.826 40.418999 28.668001 40.445 28.512001 40.496998 c
13.306 48.369999 l
13.215 48.426998 13.118 48.482998 13.025 48.536999 c
12.282 48.975998 11.790999 49.264999 11.790999 50.191998 c
11.790999 51.101997 12.327999 51.410999 13.072 51.839996 c
13.151 51.884995 13.23 51.929996 13.311 51.978996 c
28.528 59.855995 l
28.690001 59.939995 28.860001 59.982994 29.032 59.982994 c
29.729 59.982994 30.362 59.273994 30.362 58.494995 c
30.362 58.029995 29.955 57.514996 29.348 57.211994 c
15.386 50.191994 l
29.348 43.171993 l
29.963999 42.863995 30.362 42.343994 30.362 41.850994 c
30.363001 41.048 29.756001 40.418999 28.982 40.418999 c
h
f
46.384998 64.416 m
46.153999 65.107002 45.462997 65.493004 44.771 65.376999 c
44.001999 65.223999 43.501999 64.491997 43.617001 63.684998 c
43.617001 63.608997 43.656002 63.493996 43.694 63.377998 c
53.574001 35.546997 l
53.804001 34.854996 54.496002 34.508999 55.188 34.623997 c
55.917999 34.777996 56.457001 35.508995 56.341 36.275997 c
56.341 36.353996 56.301998 36.468998 56.264 36.545998 c
46.384998 64.416 l
h
f
45.015999 65.511002 m
44.927998 65.511002 44.839001 65.502998 44.752998 65.488998 c
43.915997 65.321999 43.382 64.540001 43.505997 63.669998 c
43.504997 63.593998 43.543995 63.476997 43.584995 63.351997 c
53.467995 35.509998 l
53.674995 34.891998 54.245995 34.489998 54.924995 34.489998 c
55.018997 34.489998 55.112995 34.498997 55.205994 34.512997 c
56.017994 34.684998 56.574993 35.482998 56.452995 36.293995 c
56.453995 36.379993 56.405994 36.513996 56.365993 36.595993 c
46.490993 64.451996 l
46.280998 65.084 45.688 65.511002 45.015999 65.511002 c
h
54.924999 34.715 m
54.344997 34.715 53.856998 35.055 53.681 35.582001 c
43.800999 63.415001 l
43.765999 63.519001 43.730999 63.628002 43.730999 63.685001 c
43.622997 64.452003 44.079998 65.124001 44.792999 65.266998 c
44.863998 65.278999 44.939999 65.284996 45.015999 65.284996 c
45.591 65.284996 46.098999 64.921997 46.278999 64.380997 c
56.16 36.508995 l
56.201 36.423996 56.23 36.326996 56.23 36.277996 c
56.334999 35.565994 55.856998 34.881996 55.166 34.734997 c
55.089001 34.722 55.007 34.715 54.924999 34.715 c
h
f
84.362 50.192001 m
70.599998 43.273998 l
69.986 42.964996 69.525002 42.425999 69.525002 41.850998 c
69.525002 40.813 70.562004 39.888996 71.523003 40.388996 c
86.746002 48.269997 l
87.631004 48.806995 88.321999 49.076996 88.321999 50.191998 c
88.321999 51.267998 87.591995 51.576996 86.746002 52.075996 c
71.523003 59.956997 l
70.562004 60.455997 69.525002 59.533997 69.525002 58.494995 c
69.525002 57.957996 69.986 57.419994 70.599998 57.111996 c
84.362 50.192001 l
h
f
70.967003 60.209 m
70.139 60.209 69.411003 59.407001 69.411003 58.494999 c
69.411003 57.939999 69.858002 57.355999 70.550003 57.010998 c
84.112 50.192997 l
70.550003 43.374001 l
69.848 43.021999 69.411003 42.438 69.411003 41.852001 c
69.411003 40.939003 70.139 40.138 70.967003 40.138 c
71.176003 40.138 71.380005 40.188999 71.575005 40.290001 c
86.798004 48.171001 l
86.902 48.233002 86.996002 48.290001 87.088005 48.344002 c
87.841003 48.786003 88.435005 49.136002 88.435005 50.193001 c
88.435005 51.232002 87.789001 51.605 87.040009 52.035999 c
86.962006 52.082001 86.883011 52.126999 86.803009 52.174 c
71.575012 60.057999 l
71.379997 60.157001 71.176003 60.209 70.967003 60.209 c
h
70.967003 40.362999 m
70.271004 40.362999 69.637001 41.071999 69.637001 41.850998 c
69.637001 42.343998 70.034004 42.863998 70.651001 43.171997 c
84.612999 50.191998 l
70.651001 57.211998 l
70.044998 57.516998 69.637001 58.031998 69.637001 58.494999 c
69.637001 59.273998 70.271004 59.982998 70.967003 59.982998 c
71.139 59.982998 71.309006 59.939999 71.471001 59.855999 c
86.694 51.975998 l
86.768997 51.929996 86.848999 51.884998 86.927002 51.839996 c
87.671005 51.409996 88.209 51.101997 88.209 50.191998 c
88.209 49.263996 87.718002 48.975998 86.973999 48.536999 c
86.880997 48.482998 86.785995 48.426998 86.686996 48.367001 c
71.470993 40.491001 l
71.308998 40.404999 71.139 40.362999 70.967003 40.362999 c
h
f
Q
q
[99.016907 0 0 98.982658 152.13266 1942.3326] cm
/DeviceRGB {} CS
[1 1 1] SC
/DeviceRGB {} cs
[1 1 1] sc
10.56 5.52 m
11.373334 5.826667 12.106668 6.32 12.76 7 c
14.160001 8.413333 14.860001 10.216666 14.860001 12.41 c
14.860001 14.603334 14.160001 16.413334 12.76 17.84 c
12.106668 18.52 11.373334 19.013334 10.56 19.32 c
9.72 19.666666 8.860001 19.84 7.980001 19.84 c
2.320001 19.84 l
2.133334 19.84 1.970001 19.77 1.830001 19.630001 c
1.690001 19.490002 1.620001 19.326668 1.620001 19.140001 c
1.620001 17.700001 l
1.620001 17.500002 1.690001 17.330002 1.830001 17.190001 c
1.970001 17.049999 2.133334 16.98 2.320001 16.980001 c
7.920001 16.980001 l
9.106668 16.980001 10.070001 16.546669 10.81 15.680001 c
11.55 14.813335 11.92 13.726667 11.92 12.420001 c
11.92 11.113335 11.55 10.026668 10.81 9.160001 c
10.070001 8.293334 9.106668 7.860001 7.920001 7.860001 c
2.320001 7.860001 l
2.133334 7.860001 1.970001 7.79 1.830001 7.650001 c
1.690001 7.510001 1.620001 7.340001 1.620001 7.14 c
1.620001 5.7 l
1.620001 5.513333 1.690001 5.35 1.830001 5.21 c
1.970001 5.07 2.133334 5 2.320001 5 c
7.980001 5 l
8.860001 5 9.72 5.173333 10.56 5.52 c
h
32.208 18.860001 m
32.301334 19.073334 32.278 19.290001 32.138 19.51 c
31.998001 19.73 31.808001 19.84 31.568001 19.84 c
18.248001 19.84 l
18.128 19.84 18.014668 19.809999 17.908001 19.75 c
17.801334 19.690001 17.721334 19.613333 17.668001 19.52 c
17.521336 19.306667 17.501335 19.086668 17.608002 18.860001 c
18.228003 17.400002 l
18.281336 17.266668 18.368002 17.160002 18.488003 17.080002 c
18.608004 17.000002 18.734669 16.960003 18.868002 16.960001 c
28.188002 16.960001 l
24.908001 9.120001 l
22.228001 15.520001 l
22.174667 15.653334 22.091333 15.756667 21.978001 15.830001 c
21.864668 15.903335 21.734667 15.940002 21.588001 15.940001 c
19.908001 15.940001 l
19.654669 15.940001 19.454668 15.833334 19.308001 15.620001 c
19.254667 15.526668 19.221334 15.420001 19.208 15.300001 c
19.194666 15.180001 19.208 15.066669 19.248001 14.960001 c
23.308001 5.440001 l
23.361334 5.306667 23.444668 5.200001 23.558001 5.12 c
23.671333 5.04 23.801334 5 23.948 5.000001 c
25.868 5.000001 l
26.014666 5.000001 26.144667 5.04 26.257999 5.12 c
26.371332 5.2 26.454666 5.306667 26.507999 5.440001 c
32.208 18.860001 l
h
45.535999 12.42 m
46.109333 12.78 46.546001 13.22 46.846001 13.74 c
47.146 14.259999 47.296001 14.839999 47.296001 15.48 c
47.296001 16.986666 46.722668 18.139999 45.576 18.939999 c
44.802666 19.499998 43.756001 19.853333 42.436001 19.999998 c
42.355999 19.999998 l
42.169334 19.999998 42.015999 19.939999 41.896 19.819998 c
41.736 19.673332 41.655998 19.499998 41.655998 19.299997 c
41.655998 17.859997 l
41.655998 17.686663 41.716 17.533331 41.835999 17.399998 c
41.955997 17.266665 42.102665 17.186665 42.275997 17.159998 c
42.902664 17.09333 43.389328 16.946665 43.735996 16.719997 c
44.002663 16.559998 44.182663 16.353331 44.275997 16.099997 c
44.32933 15.95333 44.355999 15.766663 44.355999 15.539996 c
44.355999 15.39333 44.32933 15.266663 44.275997 15.159996 c
44.222664 15.05333 44.12933 14.953329 43.995998 14.859996 c
43.609329 14.593329 43.109329 14.366663 42.495998 14.179996 c
42.175999 14.079995 l
41.509331 13.893329 40.896 13.739995 40.335999 13.619995 c
40.216 13.593328 40.022663 13.546661 39.755997 13.479995 c
39.535995 13.419994 l
39.055996 13.299995 38.549328 13.133327 38.015995 12.919994 c
37.229328 12.586661 36.582661 12.166661 36.075996 11.659994 c
35.48933 11.073327 35.195995 10.299995 35.195995 9.339994 c
35.195995 8.019995 35.702663 6.946661 36.715996 6.119994 c
37.409328 5.533328 38.382664 5.159994 39.635994 4.999994 c
39.849327 4.973328 40.032661 5.033328 40.185993 5.179994 c
40.339325 5.326661 40.415993 5.499994 40.415993 5.699994 c
40.415993 7.139994 l
40.415993 7.313327 40.362659 7.466661 40.255993 7.599994 c
40.149326 7.733328 40.012661 7.809994 39.845993 7.829994 c
39.679325 7.849994 39.509327 7.886661 39.335995 7.939994 c
38.949326 8.08666 38.675995 8.229994 38.515995 8.369994 c
38.355995 8.509995 38.242664 8.653328 38.175995 8.799995 c
38.109329 9.026661 38.075996 9.239995 38.075996 9.439995 c
38.075996 9.546661 38.135998 9.659995 38.255997 9.779995 c
38.442661 9.966662 38.715996 10.133328 39.075996 10.279995 c
39.235996 10.346662 39.515999 10.446662 39.915997 10.579995 c
42.415997 11.199995 l
42.535995 11.239995 l
43.162663 11.413328 43.615993 11.546661 43.895996 11.639995 c
44.522663 11.853328 45.069328 12.113328 45.535995 12.419994 c
45.535999 12.42 l
h
42.195999 7.9 m
42.035999 7.86 41.905998 7.776667 41.806 7.65 c
41.706001 7.523334 41.656002 7.373334 41.655998 7.2 c
41.655998 5.74 l
41.655998 5.526667 41.742664 5.346667 41.915997 5.2 c
41.982662 5.133334 42.069328 5.09 42.175995 5.07 c
42.282661 5.05 42.38266 5.046667 42.475994 5.06 c
43.649326 5.246667 44.602661 5.646667 45.335995 6.26 c
46.335995 7.086667 46.869328 8.14 46.935993 9.42 c
46.949326 9.606667 46.885994 9.776667 46.745995 9.93 c
46.605995 10.083334 46.435997 10.160001 46.235996 10.16 c
44.675995 10.16 l
44.502663 10.16 44.349331 10.099999 44.215996 9.98 c
44.082661 9.86 44.009327 9.713333 43.995995 9.54 c
43.942661 9.193334 43.829327 8.906667 43.655994 8.68 c
43.375996 8.346667 42.942661 8.093333 42.355995 7.92 c
42.315994 7.92 42.289326 7.913333 42.275993 7.9 c
42.195992 7.9 l
42.195999 7.9 l
h
39.855999 17.08 m
40.015999 17.106667 40.149334 17.186666 40.256001 17.32 c
40.362667 17.453333 40.416 17.599998 40.416 17.76 c
40.416 19.24 l
40.416 19.453333 40.335999 19.633333 40.175999 19.780001 c
40.042664 19.886667 39.889332 19.940001 39.716 19.940001 c
39.616001 19.940001 l
38.375999 19.753334 37.355999 19.346666 36.556 18.720001 c
35.515999 17.893335 34.929333 16.740002 34.796001 15.260001 c
34.769333 15.046668 34.829334 14.863335 34.976002 14.710001 c
35.122669 14.556667 35.296001 14.480001 35.496002 14.480001 c
37.076004 14.480001 l
37.262669 14.480001 37.422668 14.543335 37.556004 14.670001 c
37.689339 14.796667 37.762672 14.953334 37.776005 15.140001 c
37.816006 15.806668 38.142673 16.320002 38.756004 16.68 c
39.036003 16.84 39.402668 16.973333 39.856003 17.08 c
39.855999 17.08 l
h
60.883999 11.12 m
61.084 11.12 61.253998 11.19 61.393997 11.33 c
61.533997 11.47 61.603996 11.64 61.603996 11.84 c
61.603996 13.32 l
61.603996 13.506666 61.533997 13.669999 61.393997 13.81 c
61.253998 13.95 61.084 14.02 60.883999 14.02 c
54.304001 14.02 l
54.304001 19.139999 l
54.304001 19.326666 54.237335 19.49 54.104 19.629999 c
53.970665 19.769999 53.804001 19.839998 53.604 19.839998 c
52.084 19.839998 l
51.897335 19.839998 51.734001 19.769999 51.593998 19.629999 c
51.453995 19.49 51.383995 19.326666 51.383999 19.139999 c
51.383999 11.839999 l
51.383999 11.639999 51.453999 11.469999 51.593998 11.329999 c
51.733997 11.189999 51.897331 11.119999 52.084 11.119999 c
60.883999 11.119999 l
60.883999 11.12 l
h
61.784 5 m
61.970665 5 62.133999 5.07 62.274002 5.21 c
62.414005 5.35 62.484005 5.513333 62.484001 5.7 c
62.484001 7.16 l
62.484001 7.36 62.414001 7.53 62.274002 7.67 c
62.134003 7.81 61.970669 7.88 61.784 7.88 c
52.084 7.88 l
51.897335 7.88 51.734001 7.81 51.593998 7.67 c
51.453995 7.53 51.383995 7.36 51.383999 7.16 c
51.383999 5.7 l
51.383999 5.513333 51.453999 5.35 51.593998 5.21 c
51.733997 5.07 51.897331 5.000001 52.084 5 c
61.784 5 l
h
79.512001 18.860001 m
79.605331 19.073334 79.582001 19.290001 79.442001 19.51 c
79.302002 19.73 79.112 19.84 78.872002 19.84 c
65.552002 19.84 l
65.431999 19.84 65.318672 19.809999 65.212006 19.75 c
65.105339 19.690001 65.025345 19.613333 64.972008 19.52 c
64.82534 19.306667 64.805344 19.086668 64.91201 18.860001 c
65.532013 17.400002 l
65.58535 17.266668 65.672012 17.160002 65.792015 17.080002 c
65.912018 17.000002 66.038681 16.960003 66.172012 16.960001 c
75.492012 16.960001 l
72.212013 9.120001 l
69.532013 15.520001 l
69.478676 15.653334 69.395348 15.756667 69.282013 15.830001 c
69.168678 15.903335 69.038681 15.940002 68.892014 15.940001 c
67.212013 15.940001 l
66.958679 15.940001 66.758682 15.833334 66.612015 15.620001 c
66.558678 15.526668 66.525345 15.420001 66.512016 15.300001 c
66.498688 15.180001 66.512016 15.066669 66.552017 14.960001 c
70.612015 5.440001 l
70.665352 5.306667 70.74868 5.200001 70.862015 5.12 c
70.975349 5.04 71.105347 5 71.252014 5.000001 c
73.172012 5.000001 l
73.31868 5.000001 73.448677 5.04 73.562012 5.12 c
73.675346 5.2 73.758675 5.306667 73.812012 5.440001 c
79.512001 18.860001 l
h
92 5.52 m
92.813332 5.826667 93.546669 6.32 94.199997 7 c
95.599998 8.413333 96.299995 10.216666 96.299995 12.41 c
96.299995 14.603334 95.599998 16.413334 94.199997 17.84 c
93.546661 18.52 92.813332 19.013334 92 19.32 c
91.159996 19.666666 90.299995 19.84 89.419998 19.84 c
83.759995 19.84 l
83.573326 19.84 83.409996 19.77 83.269997 19.630001 c
83.129997 19.490002 83.059998 19.326668 83.059998 19.140001 c
83.059998 17.700001 l
83.059998 17.500002 83.129997 17.330002 83.269997 17.190001 c
83.409996 17.049999 83.573326 16.98 83.759995 16.980001 c
89.359993 16.980001 l
90.546661 16.980001 91.509995 16.546669 92.249992 15.680001 c
92.98999 14.813335 93.359993 13.726667 93.359993 12.420001 c
93.359993 11.113335 92.98999 10.026668 92.249992 9.160001 c
91.509995 8.293334 90.546661 7.860001 89.359993 7.860001 c
83.759995 7.860001 l
83.573326 7.860001 83.409996 7.79 83.269997 7.650001 c
83.129997 7.510001 83.059998 7.340001 83.059998 7.14 c
83.059998 5.7 l
83.059998 5.513333 83.129997 5.35 83.269997 5.21 c
83.409996 5.07 83.573326 5 83.759995 5 c
89.419998 5 l
90.299995 5 91.159996 5.173333 92 5.52 c
h
f
Q
Q
Q
showpage
%%PageTrailer
pdfEndPage
%%Trailer
end
%%DocumentSuppliedResources:
%%EOF

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@ -1,40 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="3200"
height="1355.480324331485" viewBox="0 0 3200 1355.480324331485">
<g transform="scale(10) translate(10, 10)">
<defs id="SvgjsDefs1385">
<linearGradient id="SvgjsLinearGradient1390">
<stop id="SvgjsStop1391" stop-color="#905e26" offset="0"/>
<stop id="SvgjsStop1392" stop-color="#f5ec9b" offset="0.5"/>
<stop id="SvgjsStop1393" stop-color="#905e26" offset="1"/>
</linearGradient>
<linearGradient id="SvgjsLinearGradient1394">
<stop id="SvgjsStop1395" stop-color="#905e26" offset="0"/>
<stop id="SvgjsStop1396" stop-color="#f5ec9b" offset="0.5"/>
<stop id="SvgjsStop1397" stop-color="#905e26" offset="1"/>
</linearGradient>
</defs>
<g id="SvgjsG1386" featureKey="aMgJeN-0"
transform="matrix(1.5610770874511997,0,0,1.5610770874511997,71.94613967240352,-53.841545411371435)"
fill="#000">
<path xmlns="http://www.w3.org/2000/svg"
d="M29.399,57.112c0.615,0.308,1.077,0.846,1.077,1.383c0,1.039-1.038,1.961-1.999,1.462l-15.223-7.881 c-0.846-0.499-1.576-0.808-1.576-1.884c0-1.115,0.692-1.385,1.576-1.922l15.223-7.881c1.038-0.346,1.999,0.424,1.999,1.462 c0,0.575-0.461,1.114-1.077,1.423l-13.761,6.918L29.399,57.112z"/>
<path xmlns="http://www.w3.org/2000/svg"
d="M29.033,60.209c-0.208,0-0.413-0.052-0.608-0.152l-15.223-7.881c-0.086-0.05-0.165-0.095-0.242-0.141 c-0.748-0.431-1.395-0.804-1.395-1.843c0-1.057,0.594-1.406,1.346-1.849c0.093-0.054,0.187-0.11,0.284-0.169l15.229-7.885 c0.195-0.066,0.377-0.097,0.558-0.097c0.9,0,1.606,0.728,1.606,1.658c0,0.587-0.437,1.172-1.139,1.522l-13.562,6.818l13.562,6.818 c0.691,0.346,1.139,0.93,1.139,1.484C30.588,59.407,29.861,60.209,29.033,60.209z M28.982,40.419c-0.156,0-0.314,0.026-0.47,0.078 L13.306,48.37c-0.091,0.057-0.188,0.113-0.281,0.167c-0.743,0.439-1.234,0.728-1.234,1.655c0,0.91,0.537,1.219,1.281,1.648 c0.079,0.045,0.158,0.09,0.239,0.139l15.217,7.877c0.162,0.084,0.332,0.127,0.504,0.127c0.697,0,1.33-0.709,1.33-1.488 c0-0.465-0.407-0.98-1.014-1.283l-13.962-7.02l13.962-7.02c0.616-0.308,1.014-0.828,1.014-1.321 C30.363,41.048,29.756,40.419,28.982,40.419z"/>
<path xmlns="http://www.w3.org/2000/svg"
d="M46.385,64.416c-0.231,0.691-0.922,1.077-1.614,0.961c-0.769-0.153-1.269-0.885-1.154-1.692 c0-0.076,0.039-0.191,0.077-0.307l9.88-27.831c0.23-0.692,0.922-1.038,1.614-0.923c0.73,0.154,1.269,0.885,1.153,1.652 c0,0.078-0.039,0.193-0.077,0.27L46.385,64.416z"/>
<path xmlns="http://www.w3.org/2000/svg"
d="M45.016,65.511c-0.088,0-0.177-0.008-0.263-0.022c-0.837-0.167-1.371-0.949-1.247-1.819 c-0.001-0.076,0.038-0.193,0.079-0.318l9.883-27.842c0.207-0.618,0.778-1.02,1.457-1.02c0.094,0,0.188,0.009,0.281,0.023 c0.812,0.172,1.369,0.97,1.247,1.781c0.001,0.086-0.047,0.22-0.087,0.302l-9.875,27.856C46.281,65.084,45.688,65.511,45.016,65.511z M54.925,34.715c-0.58,0-1.068,0.34-1.244,0.867l-9.88,27.833c-0.035,0.104-0.07,0.213-0.07,0.27 c-0.108,0.767,0.349,1.439,1.062,1.582c0.071,0.012,0.147,0.018,0.223,0.018c0.575,0,1.083-0.363,1.263-0.904l9.881-27.872 c0.041-0.085,0.07-0.182,0.07-0.231c0.105-0.712-0.373-1.396-1.064-1.543C55.089,34.722,55.007,34.715,54.925,34.715z"/>
<path xmlns="http://www.w3.org/2000/svg"
d="M84.362,50.192L70.6,43.274c-0.614-0.309-1.075-0.848-1.075-1.423c0-1.038,1.037-1.962,1.998-1.462l15.223,7.881 c0.885,0.537,1.576,0.807,1.576,1.922c0,1.076-0.73,1.385-1.576,1.884l-15.223,7.881c-0.961,0.499-1.998-0.423-1.998-1.462 c0-0.537,0.461-1.075,1.075-1.383L84.362,50.192z"/>
<path xmlns="http://www.w3.org/2000/svg"
d="M70.967,60.209c-0.828,0-1.556-0.802-1.556-1.714c0-0.555,0.447-1.139,1.139-1.484l13.562-6.818L70.55,43.374 c-0.702-0.352-1.139-0.936-1.139-1.522c0-0.913,0.728-1.714,1.556-1.714c0.209,0,0.413,0.051,0.608,0.152l15.223,7.881 c0.104,0.062,0.198,0.119,0.29,0.173c0.753,0.442,1.347,0.792,1.347,1.849c0,1.039-0.646,1.412-1.395,1.843 c-0.078,0.046-0.157,0.091-0.237,0.138l-15.228,7.884C71.38,60.157,71.176,60.209,70.967,60.209z M70.967,40.363 c-0.696,0-1.33,0.709-1.33,1.488c0,0.493,0.397,1.013,1.014,1.321l13.962,7.02l-13.962,7.02c-0.606,0.305-1.014,0.82-1.014,1.283 c0,0.779,0.634,1.488,1.33,1.488c0.172,0,0.342-0.043,0.504-0.127l15.223-7.88c0.075-0.046,0.155-0.091,0.233-0.136 c0.744-0.43,1.282-0.738,1.282-1.648c0-0.928-0.491-1.216-1.235-1.655c-0.093-0.054-0.188-0.11-0.287-0.17l-15.216-7.876 C71.309,40.405,71.139,40.363,70.967,40.363z"/>
</g>
<g id="SvgjsG1387" featureKey="8L6ael-0"
transform="matrix(3.168568052463937,0,0,3.168568052463937,-5.1330821487142195,52.17667742743372)"
fill="#000">
<path d="M10.56 5.52 q1.22 0.46 2.2 1.48 q2.1 2.12 2.1 5.41 t-2.1 5.43 q-0.98 1.02 -2.2 1.48 q-1.26 0.52 -2.58 0.52 l-5.66 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -1.44 q0 -0.3 0.21 -0.51 t0.49 -0.21 l5.6 0 q1.78 0 2.89 -1.3 t1.11 -3.26 t-1.11 -3.26 t-2.89 -1.3 l-5.6 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.44 q0 -0.28 0.21 -0.49 t0.49 -0.21 l5.66 0 q1.32 0 2.58 0.52 z M32.208 18.86 q0.14 0.32 -0.07 0.65 t-0.57 0.33 l-13.32 0 q-0.18 0 -0.34 -0.09 t-0.24 -0.23 q-0.22 -0.32 -0.06 -0.66 l0.62 -1.46 q0.08 -0.2 0.26 -0.32 t0.38 -0.12 l9.32 0 l-3.28 -7.84 l-2.68 6.4 q-0.08 0.2 -0.25 0.31 t-0.39 0.11 l-1.68 0 q-0.38 0 -0.6 -0.32 q-0.08 -0.14 -0.1 -0.32 t0.04 -0.34 l4.06 -9.52 q0.08 -0.2 0.25 -0.32 t0.39 -0.12 l1.92 0 q0.22 0 0.39 0.12 t0.25 0.32 z M45.535999999999994 12.42 q0.86 0.54 1.31 1.32 t0.45 1.74 q0 2.26 -1.72 3.46 q-1.16 0.84 -3.14 1.06 l-0.08 0 q-0.28 0 -0.46 -0.18 q-0.24 -0.22 -0.24 -0.52 l0 -1.44 q0 -0.26 0.18 -0.46 t0.44 -0.24 q0.94 -0.1 1.46 -0.44 q0.4 -0.24 0.54 -0.62 q0.08 -0.22 0.08 -0.56 q0 -0.22 -0.08 -0.38 t-0.28 -0.3 q-0.58 -0.4 -1.5 -0.68 l-0.32 -0.1 q-1 -0.28 -1.84 -0.46 q-0.18 -0.04 -0.58 -0.14 l-0.22 -0.06 q-0.72 -0.18 -1.52 -0.5 q-1.18 -0.5 -1.94 -1.26 q-0.88 -0.88 -0.88 -2.32 q0 -1.98 1.52 -3.22 q1.04 -0.88 2.92 -1.12 q0.32 -0.04 0.55 0.18 t0.23 0.52 l0 1.44 q0 0.26 -0.16 0.46 t-0.41 0.23 t-0.51 0.11 q-0.58 0.22 -0.82 0.43 t-0.34 0.43 q-0.1 0.34 -0.1 0.64 q0 0.16 0.18 0.34 q0.28 0.28 0.82 0.5 q0.24 0.1 0.84 0.3 l2.5 0.62 l0.12 0.04 q0.94 0.26 1.36 0.4 q0.94 0.32 1.64 0.78 z M42.196 7.9 q-0.24 -0.06 -0.39 -0.25 t-0.15 -0.45 l0 -1.46 q0 -0.32 0.26 -0.54 q0.1 -0.1 0.26 -0.13 t0.3 -0.01 q1.76 0.28 2.86 1.2 q1.5 1.24 1.6 3.16 q0.02 0.28 -0.19 0.51 t-0.51 0.23 l-1.56 0 q-0.26 0 -0.46 -0.18 t-0.22 -0.44 q-0.08 -0.52 -0.34 -0.86 q-0.42 -0.5 -1.3 -0.76 q-0.06 0 -0.08 -0.02 l-0.08 0 z M39.855999999999995 17.08 q0.24 0.04 0.4 0.24 t0.16 0.44 l0 1.48 q0 0.32 -0.24 0.54 q-0.2 0.16 -0.46 0.16 l-0.1 0 q-1.86 -0.28 -3.06 -1.22 q-1.56 -1.24 -1.76 -3.46 q-0.04 -0.32 0.18 -0.55 t0.52 -0.23 l1.58 0 q0.28 0 0.48 0.19 t0.22 0.47 q0.06 1 0.98 1.54 q0.42 0.24 1.1 0.4 z M60.88399999999999 11.12 q0.3 0 0.51 0.21 t0.21 0.51 l0 1.48 q0 0.28 -0.21 0.49 t-0.51 0.21 l-6.58 0 l0 5.12 q0 0.28 -0.2 0.49 t-0.5 0.21 l-1.52 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -7.3 q0 -0.3 0.21 -0.51 t0.49 -0.21 l8.8 0 z M61.78399999999999 5 q0.28 0 0.49 0.21 t0.21 0.49 l0 1.46 q0 0.3 -0.21 0.51 t-0.49 0.21 l-9.7 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.46 q0 -0.28 0.21 -0.49 t0.49 -0.21 l9.7 0 z M79.512 18.86 q0.14 0.32 -0.07 0.65 t-0.57 0.33 l-13.32 0 q-0.18 0 -0.34 -0.09 t-0.24 -0.23 q-0.22 -0.32 -0.06 -0.66 l0.62 -1.46 q0.08 -0.2 0.26 -0.32 t0.38 -0.12 l9.32 0 l-3.28 -7.84 l-2.68 6.4 q-0.08 0.2 -0.25 0.31 t-0.39 0.11 l-1.68 0 q-0.38 0 -0.6 -0.32 q-0.08 -0.14 -0.1 -0.32 t0.04 -0.34 l4.06 -9.52 q0.08 -0.2 0.25 -0.32 t0.39 -0.12 l1.92 0 q0.22 0 0.39 0.12 t0.25 0.32 z M92 5.52 q1.22 0.46 2.2 1.48 q2.1 2.12 2.1 5.41 t-2.1 5.43 q-0.98 1.02 -2.2 1.48 q-1.26 0.52 -2.58 0.52 l-5.66 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -1.44 q0 -0.3 0.21 -0.51 t0.49 -0.21 l5.6 0 q1.78 0 2.89 -1.3 t1.11 -3.26 t-1.11 -3.26 t-2.89 -1.3 l-5.6 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.44 q0 -0.28 0.21 -0.49 t0.49 -0.21 l5.66 0 q1.32 0 2.58 0.52 z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -1,40 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="3200"
height="1355.480324331485" viewBox="0 0 3200 1355.480324331485">
<g transform="scale(10) translate(10, 10)">
<defs id="SvgjsDefs1385">
<linearGradient id="SvgjsLinearGradient1390">
<stop id="SvgjsStop1391" stop-color="#905e26" offset="0"></stop>
<stop id="SvgjsStop1392" stop-color="#f5ec9b" offset="0.5"></stop>
<stop id="SvgjsStop1393" stop-color="#905e26" offset="1"></stop>
</linearGradient>
<linearGradient id="SvgjsLinearGradient1394">
<stop id="SvgjsStop1395" stop-color="#905e26" offset="0"></stop>
<stop id="SvgjsStop1396" stop-color="#f5ec9b" offset="0.5"></stop>
<stop id="SvgjsStop1397" stop-color="#905e26" offset="1"></stop>
</linearGradient>
</defs>
<g id="SvgjsG1386" featureKey="aMgJeN-0"
transform="matrix(1.5610770874511997,0,0,1.5610770874511997,71.94613967240352,-53.841545411371435)"
fill="url(#SvgjsLinearGradient1390)">
<path xmlns="http://www.w3.org/2000/svg"
d="M29.399,57.112c0.615,0.308,1.077,0.846,1.077,1.383c0,1.039-1.038,1.961-1.999,1.462l-15.223-7.881 c-0.846-0.499-1.576-0.808-1.576-1.884c0-1.115,0.692-1.385,1.576-1.922l15.223-7.881c1.038-0.346,1.999,0.424,1.999,1.462 c0,0.575-0.461,1.114-1.077,1.423l-13.761,6.918L29.399,57.112z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M29.033,60.209c-0.208,0-0.413-0.052-0.608-0.152l-15.223-7.881c-0.086-0.05-0.165-0.095-0.242-0.141 c-0.748-0.431-1.395-0.804-1.395-1.843c0-1.057,0.594-1.406,1.346-1.849c0.093-0.054,0.187-0.11,0.284-0.169l15.229-7.885 c0.195-0.066,0.377-0.097,0.558-0.097c0.9,0,1.606,0.728,1.606,1.658c0,0.587-0.437,1.172-1.139,1.522l-13.562,6.818l13.562,6.818 c0.691,0.346,1.139,0.93,1.139,1.484C30.588,59.407,29.861,60.209,29.033,60.209z M28.982,40.419c-0.156,0-0.314,0.026-0.47,0.078 L13.306,48.37c-0.091,0.057-0.188,0.113-0.281,0.167c-0.743,0.439-1.234,0.728-1.234,1.655c0,0.91,0.537,1.219,1.281,1.648 c0.079,0.045,0.158,0.09,0.239,0.139l15.217,7.877c0.162,0.084,0.332,0.127,0.504,0.127c0.697,0,1.33-0.709,1.33-1.488 c0-0.465-0.407-0.98-1.014-1.283l-13.962-7.02l13.962-7.02c0.616-0.308,1.014-0.828,1.014-1.321 C30.363,41.048,29.756,40.419,28.982,40.419z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M46.385,64.416c-0.231,0.691-0.922,1.077-1.614,0.961c-0.769-0.153-1.269-0.885-1.154-1.692 c0-0.076,0.039-0.191,0.077-0.307l9.88-27.831c0.23-0.692,0.922-1.038,1.614-0.923c0.73,0.154,1.269,0.885,1.153,1.652 c0,0.078-0.039,0.193-0.077,0.27L46.385,64.416z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M45.016,65.511c-0.088,0-0.177-0.008-0.263-0.022c-0.837-0.167-1.371-0.949-1.247-1.819 c-0.001-0.076,0.038-0.193,0.079-0.318l9.883-27.842c0.207-0.618,0.778-1.02,1.457-1.02c0.094,0,0.188,0.009,0.281,0.023 c0.812,0.172,1.369,0.97,1.247,1.781c0.001,0.086-0.047,0.22-0.087,0.302l-9.875,27.856C46.281,65.084,45.688,65.511,45.016,65.511z M54.925,34.715c-0.58,0-1.068,0.34-1.244,0.867l-9.88,27.833c-0.035,0.104-0.07,0.213-0.07,0.27 c-0.108,0.767,0.349,1.439,1.062,1.582c0.071,0.012,0.147,0.018,0.223,0.018c0.575,0,1.083-0.363,1.263-0.904l9.881-27.872 c0.041-0.085,0.07-0.182,0.07-0.231c0.105-0.712-0.373-1.396-1.064-1.543C55.089,34.722,55.007,34.715,54.925,34.715z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M84.362,50.192L70.6,43.274c-0.614-0.309-1.075-0.848-1.075-1.423c0-1.038,1.037-1.962,1.998-1.462l15.223,7.881 c0.885,0.537,1.576,0.807,1.576,1.922c0,1.076-0.73,1.385-1.576,1.884l-15.223,7.881c-0.961,0.499-1.998-0.423-1.998-1.462 c0-0.537,0.461-1.075,1.075-1.383L84.362,50.192z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M70.967,60.209c-0.828,0-1.556-0.802-1.556-1.714c0-0.555,0.447-1.139,1.139-1.484l13.562-6.818L70.55,43.374 c-0.702-0.352-1.139-0.936-1.139-1.522c0-0.913,0.728-1.714,1.556-1.714c0.209,0,0.413,0.051,0.608,0.152l15.223,7.881 c0.104,0.062,0.198,0.119,0.29,0.173c0.753,0.442,1.347,0.792,1.347,1.849c0,1.039-0.646,1.412-1.395,1.843 c-0.078,0.046-0.157,0.091-0.237,0.138l-15.228,7.884C71.38,60.157,71.176,60.209,70.967,60.209z M70.967,40.363 c-0.696,0-1.33,0.709-1.33,1.488c0,0.493,0.397,1.013,1.014,1.321l13.962,7.02l-13.962,7.02c-0.606,0.305-1.014,0.82-1.014,1.283 c0,0.779,0.634,1.488,1.33,1.488c0.172,0,0.342-0.043,0.504-0.127l15.223-7.88c0.075-0.046,0.155-0.091,0.233-0.136 c0.744-0.43,1.282-0.738,1.282-1.648c0-0.928-0.491-1.216-1.235-1.655c-0.093-0.054-0.188-0.11-0.287-0.17l-15.216-7.876 C71.309,40.405,71.139,40.363,70.967,40.363z"></path>
</g>
<g id="SvgjsG1387" featureKey="8L6ael-0"
transform="matrix(3.168568052463937,0,0,3.168568052463937,-5.1330821487142195,52.17667742743372)"
fill="url(#SvgjsLinearGradient1394)">
<path d="M10.56 5.52 q1.22 0.46 2.2 1.48 q2.1 2.12 2.1 5.41 t-2.1 5.43 q-0.98 1.02 -2.2 1.48 q-1.26 0.52 -2.58 0.52 l-5.66 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -1.44 q0 -0.3 0.21 -0.51 t0.49 -0.21 l5.6 0 q1.78 0 2.89 -1.3 t1.11 -3.26 t-1.11 -3.26 t-2.89 -1.3 l-5.6 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.44 q0 -0.28 0.21 -0.49 t0.49 -0.21 l5.66 0 q1.32 0 2.58 0.52 z M32.208 18.86 q0.14 0.32 -0.07 0.65 t-0.57 0.33 l-13.32 0 q-0.18 0 -0.34 -0.09 t-0.24 -0.23 q-0.22 -0.32 -0.06 -0.66 l0.62 -1.46 q0.08 -0.2 0.26 -0.32 t0.38 -0.12 l9.32 0 l-3.28 -7.84 l-2.68 6.4 q-0.08 0.2 -0.25 0.31 t-0.39 0.11 l-1.68 0 q-0.38 0 -0.6 -0.32 q-0.08 -0.14 -0.1 -0.32 t0.04 -0.34 l4.06 -9.52 q0.08 -0.2 0.25 -0.32 t0.39 -0.12 l1.92 0 q0.22 0 0.39 0.12 t0.25 0.32 z M45.535999999999994 12.42 q0.86 0.54 1.31 1.32 t0.45 1.74 q0 2.26 -1.72 3.46 q-1.16 0.84 -3.14 1.06 l-0.08 0 q-0.28 0 -0.46 -0.18 q-0.24 -0.22 -0.24 -0.52 l0 -1.44 q0 -0.26 0.18 -0.46 t0.44 -0.24 q0.94 -0.1 1.46 -0.44 q0.4 -0.24 0.54 -0.62 q0.08 -0.22 0.08 -0.56 q0 -0.22 -0.08 -0.38 t-0.28 -0.3 q-0.58 -0.4 -1.5 -0.68 l-0.32 -0.1 q-1 -0.28 -1.84 -0.46 q-0.18 -0.04 -0.58 -0.14 l-0.22 -0.06 q-0.72 -0.18 -1.52 -0.5 q-1.18 -0.5 -1.94 -1.26 q-0.88 -0.88 -0.88 -2.32 q0 -1.98 1.52 -3.22 q1.04 -0.88 2.92 -1.12 q0.32 -0.04 0.55 0.18 t0.23 0.52 l0 1.44 q0 0.26 -0.16 0.46 t-0.41 0.23 t-0.51 0.11 q-0.58 0.22 -0.82 0.43 t-0.34 0.43 q-0.1 0.34 -0.1 0.64 q0 0.16 0.18 0.34 q0.28 0.28 0.82 0.5 q0.24 0.1 0.84 0.3 l2.5 0.62 l0.12 0.04 q0.94 0.26 1.36 0.4 q0.94 0.32 1.64 0.78 z M42.196 7.9 q-0.24 -0.06 -0.39 -0.25 t-0.15 -0.45 l0 -1.46 q0 -0.32 0.26 -0.54 q0.1 -0.1 0.26 -0.13 t0.3 -0.01 q1.76 0.28 2.86 1.2 q1.5 1.24 1.6 3.16 q0.02 0.28 -0.19 0.51 t-0.51 0.23 l-1.56 0 q-0.26 0 -0.46 -0.18 t-0.22 -0.44 q-0.08 -0.52 -0.34 -0.86 q-0.42 -0.5 -1.3 -0.76 q-0.06 0 -0.08 -0.02 l-0.08 0 z M39.855999999999995 17.08 q0.24 0.04 0.4 0.24 t0.16 0.44 l0 1.48 q0 0.32 -0.24 0.54 q-0.2 0.16 -0.46 0.16 l-0.1 0 q-1.86 -0.28 -3.06 -1.22 q-1.56 -1.24 -1.76 -3.46 q-0.04 -0.32 0.18 -0.55 t0.52 -0.23 l1.58 0 q0.28 0 0.48 0.19 t0.22 0.47 q0.06 1 0.98 1.54 q0.42 0.24 1.1 0.4 z M60.88399999999999 11.12 q0.3 0 0.51 0.21 t0.21 0.51 l0 1.48 q0 0.28 -0.21 0.49 t-0.51 0.21 l-6.58 0 l0 5.12 q0 0.28 -0.2 0.49 t-0.5 0.21 l-1.52 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -7.3 q0 -0.3 0.21 -0.51 t0.49 -0.21 l8.8 0 z M61.78399999999999 5 q0.28 0 0.49 0.21 t0.21 0.49 l0 1.46 q0 0.3 -0.21 0.51 t-0.49 0.21 l-9.7 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.46 q0 -0.28 0.21 -0.49 t0.49 -0.21 l9.7 0 z M79.512 18.86 q0.14 0.32 -0.07 0.65 t-0.57 0.33 l-13.32 0 q-0.18 0 -0.34 -0.09 t-0.24 -0.23 q-0.22 -0.32 -0.06 -0.66 l0.62 -1.46 q0.08 -0.2 0.26 -0.32 t0.38 -0.12 l9.32 0 l-3.28 -7.84 l-2.68 6.4 q-0.08 0.2 -0.25 0.31 t-0.39 0.11 l-1.68 0 q-0.38 0 -0.6 -0.32 q-0.08 -0.14 -0.1 -0.32 t0.04 -0.34 l4.06 -9.52 q0.08 -0.2 0.25 -0.32 t0.39 -0.12 l1.92 0 q0.22 0 0.39 0.12 t0.25 0.32 z M92 5.52 q1.22 0.46 2.2 1.48 q2.1 2.12 2.1 5.41 t-2.1 5.43 q-0.98 1.02 -2.2 1.48 q-1.26 0.52 -2.58 0.52 l-5.66 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -1.44 q0 -0.3 0.21 -0.51 t0.49 -0.21 l5.6 0 q1.78 0 2.89 -1.3 t1.11 -3.26 t-1.11 -3.26 t-2.89 -1.3 l-5.6 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.44 q0 -0.28 0.21 -0.49 t0.49 -0.21 l5.66 0 q1.32 0 2.58 0.52 z"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -1,40 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="3200"
height="1355.480324331485" viewBox="0 0 3200 1355.480324331485">
<rect fill="#292929" width="3200" height="1355.480324331485"/>
<g transform="scale(10) translate(10, 10)">
<defs id="SvgjsDefs1385">
<linearGradient id="SvgjsLinearGradient1390">
<stop id="SvgjsStop1391" stop-color="#905e26" offset="0"></stop>
<stop id="SvgjsStop1392" stop-color="#f5ec9b" offset="0.5"></stop>
<stop id="SvgjsStop1393" stop-color="#905e26" offset="1"></stop>
</linearGradient>
<linearGradient id="SvgjsLinearGradient1394">
<stop id="SvgjsStop1395" stop-color="#905e26" offset="0"></stop>
<stop id="SvgjsStop1396" stop-color="#f5ec9b" offset="0.5"></stop>
<stop id="SvgjsStop1397" stop-color="#905e26" offset="1"></stop>
</linearGradient>
</defs>
<g id="SvgjsG1386" featureKey="aMgJeN-0"
transform="matrix(1.5610770874511997,0,0,1.5610770874511997,71.94613967240352,-53.841545411371435)"
fill="url(#SvgjsLinearGradient1390)">
<path xmlns="http://www.w3.org/2000/svg"
d="M29.399,57.112c0.615,0.308,1.077,0.846,1.077,1.383c0,1.039-1.038,1.961-1.999,1.462l-15.223-7.881 c-0.846-0.499-1.576-0.808-1.576-1.884c0-1.115,0.692-1.385,1.576-1.922l15.223-7.881c1.038-0.346,1.999,0.424,1.999,1.462 c0,0.575-0.461,1.114-1.077,1.423l-13.761,6.918L29.399,57.112z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M29.033,60.209c-0.208,0-0.413-0.052-0.608-0.152l-15.223-7.881c-0.086-0.05-0.165-0.095-0.242-0.141 c-0.748-0.431-1.395-0.804-1.395-1.843c0-1.057,0.594-1.406,1.346-1.849c0.093-0.054,0.187-0.11,0.284-0.169l15.229-7.885 c0.195-0.066,0.377-0.097,0.558-0.097c0.9,0,1.606,0.728,1.606,1.658c0,0.587-0.437,1.172-1.139,1.522l-13.562,6.818l13.562,6.818 c0.691,0.346,1.139,0.93,1.139,1.484C30.588,59.407,29.861,60.209,29.033,60.209z M28.982,40.419c-0.156,0-0.314,0.026-0.47,0.078 L13.306,48.37c-0.091,0.057-0.188,0.113-0.281,0.167c-0.743,0.439-1.234,0.728-1.234,1.655c0,0.91,0.537,1.219,1.281,1.648 c0.079,0.045,0.158,0.09,0.239,0.139l15.217,7.877c0.162,0.084,0.332,0.127,0.504,0.127c0.697,0,1.33-0.709,1.33-1.488 c0-0.465-0.407-0.98-1.014-1.283l-13.962-7.02l13.962-7.02c0.616-0.308,1.014-0.828,1.014-1.321 C30.363,41.048,29.756,40.419,28.982,40.419z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M46.385,64.416c-0.231,0.691-0.922,1.077-1.614,0.961c-0.769-0.153-1.269-0.885-1.154-1.692 c0-0.076,0.039-0.191,0.077-0.307l9.88-27.831c0.23-0.692,0.922-1.038,1.614-0.923c0.73,0.154,1.269,0.885,1.153,1.652 c0,0.078-0.039,0.193-0.077,0.27L46.385,64.416z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M45.016,65.511c-0.088,0-0.177-0.008-0.263-0.022c-0.837-0.167-1.371-0.949-1.247-1.819 c-0.001-0.076,0.038-0.193,0.079-0.318l9.883-27.842c0.207-0.618,0.778-1.02,1.457-1.02c0.094,0,0.188,0.009,0.281,0.023 c0.812,0.172,1.369,0.97,1.247,1.781c0.001,0.086-0.047,0.22-0.087,0.302l-9.875,27.856C46.281,65.084,45.688,65.511,45.016,65.511z M54.925,34.715c-0.58,0-1.068,0.34-1.244,0.867l-9.88,27.833c-0.035,0.104-0.07,0.213-0.07,0.27 c-0.108,0.767,0.349,1.439,1.062,1.582c0.071,0.012,0.147,0.018,0.223,0.018c0.575,0,1.083-0.363,1.263-0.904l9.881-27.872 c0.041-0.085,0.07-0.182,0.07-0.231c0.105-0.712-0.373-1.396-1.064-1.543C55.089,34.722,55.007,34.715,54.925,34.715z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M84.362,50.192L70.6,43.274c-0.614-0.309-1.075-0.848-1.075-1.423c0-1.038,1.037-1.962,1.998-1.462l15.223,7.881 c0.885,0.537,1.576,0.807,1.576,1.922c0,1.076-0.73,1.385-1.576,1.884l-15.223,7.881c-0.961,0.499-1.998-0.423-1.998-1.462 c0-0.537,0.461-1.075,1.075-1.383L84.362,50.192z"></path>
<path xmlns="http://www.w3.org/2000/svg"
d="M70.967,60.209c-0.828,0-1.556-0.802-1.556-1.714c0-0.555,0.447-1.139,1.139-1.484l13.562-6.818L70.55,43.374 c-0.702-0.352-1.139-0.936-1.139-1.522c0-0.913,0.728-1.714,1.556-1.714c0.209,0,0.413,0.051,0.608,0.152l15.223,7.881 c0.104,0.062,0.198,0.119,0.29,0.173c0.753,0.442,1.347,0.792,1.347,1.849c0,1.039-0.646,1.412-1.395,1.843 c-0.078,0.046-0.157,0.091-0.237,0.138l-15.228,7.884C71.38,60.157,71.176,60.209,70.967,60.209z M70.967,40.363 c-0.696,0-1.33,0.709-1.33,1.488c0,0.493,0.397,1.013,1.014,1.321l13.962,7.02l-13.962,7.02c-0.606,0.305-1.014,0.82-1.014,1.283 c0,0.779,0.634,1.488,1.33,1.488c0.172,0,0.342-0.043,0.504-0.127l15.223-7.88c0.075-0.046,0.155-0.091,0.233-0.136 c0.744-0.43,1.282-0.738,1.282-1.648c0-0.928-0.491-1.216-1.235-1.655c-0.093-0.054-0.188-0.11-0.287-0.17l-15.216-7.876 C71.309,40.405,71.139,40.363,70.967,40.363z"></path>
</g>
<g id="SvgjsG1387" featureKey="8L6ael-0"
transform="matrix(3.168568052463937,0,0,3.168568052463937,-5.1330821487142195,52.17667742743372)"
fill="url(#SvgjsLinearGradient1394)">
<path d="M10.56 5.52 q1.22 0.46 2.2 1.48 q2.1 2.12 2.1 5.41 t-2.1 5.43 q-0.98 1.02 -2.2 1.48 q-1.26 0.52 -2.58 0.52 l-5.66 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -1.44 q0 -0.3 0.21 -0.51 t0.49 -0.21 l5.6 0 q1.78 0 2.89 -1.3 t1.11 -3.26 t-1.11 -3.26 t-2.89 -1.3 l-5.6 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.44 q0 -0.28 0.21 -0.49 t0.49 -0.21 l5.66 0 q1.32 0 2.58 0.52 z M32.208 18.86 q0.14 0.32 -0.07 0.65 t-0.57 0.33 l-13.32 0 q-0.18 0 -0.34 -0.09 t-0.24 -0.23 q-0.22 -0.32 -0.06 -0.66 l0.62 -1.46 q0.08 -0.2 0.26 -0.32 t0.38 -0.12 l9.32 0 l-3.28 -7.84 l-2.68 6.4 q-0.08 0.2 -0.25 0.31 t-0.39 0.11 l-1.68 0 q-0.38 0 -0.6 -0.32 q-0.08 -0.14 -0.1 -0.32 t0.04 -0.34 l4.06 -9.52 q0.08 -0.2 0.25 -0.32 t0.39 -0.12 l1.92 0 q0.22 0 0.39 0.12 t0.25 0.32 z M45.535999999999994 12.42 q0.86 0.54 1.31 1.32 t0.45 1.74 q0 2.26 -1.72 3.46 q-1.16 0.84 -3.14 1.06 l-0.08 0 q-0.28 0 -0.46 -0.18 q-0.24 -0.22 -0.24 -0.52 l0 -1.44 q0 -0.26 0.18 -0.46 t0.44 -0.24 q0.94 -0.1 1.46 -0.44 q0.4 -0.24 0.54 -0.62 q0.08 -0.22 0.08 -0.56 q0 -0.22 -0.08 -0.38 t-0.28 -0.3 q-0.58 -0.4 -1.5 -0.68 l-0.32 -0.1 q-1 -0.28 -1.84 -0.46 q-0.18 -0.04 -0.58 -0.14 l-0.22 -0.06 q-0.72 -0.18 -1.52 -0.5 q-1.18 -0.5 -1.94 -1.26 q-0.88 -0.88 -0.88 -2.32 q0 -1.98 1.52 -3.22 q1.04 -0.88 2.92 -1.12 q0.32 -0.04 0.55 0.18 t0.23 0.52 l0 1.44 q0 0.26 -0.16 0.46 t-0.41 0.23 t-0.51 0.11 q-0.58 0.22 -0.82 0.43 t-0.34 0.43 q-0.1 0.34 -0.1 0.64 q0 0.16 0.18 0.34 q0.28 0.28 0.82 0.5 q0.24 0.1 0.84 0.3 l2.5 0.62 l0.12 0.04 q0.94 0.26 1.36 0.4 q0.94 0.32 1.64 0.78 z M42.196 7.9 q-0.24 -0.06 -0.39 -0.25 t-0.15 -0.45 l0 -1.46 q0 -0.32 0.26 -0.54 q0.1 -0.1 0.26 -0.13 t0.3 -0.01 q1.76 0.28 2.86 1.2 q1.5 1.24 1.6 3.16 q0.02 0.28 -0.19 0.51 t-0.51 0.23 l-1.56 0 q-0.26 0 -0.46 -0.18 t-0.22 -0.44 q-0.08 -0.52 -0.34 -0.86 q-0.42 -0.5 -1.3 -0.76 q-0.06 0 -0.08 -0.02 l-0.08 0 z M39.855999999999995 17.08 q0.24 0.04 0.4 0.24 t0.16 0.44 l0 1.48 q0 0.32 -0.24 0.54 q-0.2 0.16 -0.46 0.16 l-0.1 0 q-1.86 -0.28 -3.06 -1.22 q-1.56 -1.24 -1.76 -3.46 q-0.04 -0.32 0.18 -0.55 t0.52 -0.23 l1.58 0 q0.28 0 0.48 0.19 t0.22 0.47 q0.06 1 0.98 1.54 q0.42 0.24 1.1 0.4 z M60.88399999999999 11.12 q0.3 0 0.51 0.21 t0.21 0.51 l0 1.48 q0 0.28 -0.21 0.49 t-0.51 0.21 l-6.58 0 l0 5.12 q0 0.28 -0.2 0.49 t-0.5 0.21 l-1.52 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -7.3 q0 -0.3 0.21 -0.51 t0.49 -0.21 l8.8 0 z M61.78399999999999 5 q0.28 0 0.49 0.21 t0.21 0.49 l0 1.46 q0 0.3 -0.21 0.51 t-0.49 0.21 l-9.7 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.46 q0 -0.28 0.21 -0.49 t0.49 -0.21 l9.7 0 z M79.512 18.86 q0.14 0.32 -0.07 0.65 t-0.57 0.33 l-13.32 0 q-0.18 0 -0.34 -0.09 t-0.24 -0.23 q-0.22 -0.32 -0.06 -0.66 l0.62 -1.46 q0.08 -0.2 0.26 -0.32 t0.38 -0.12 l9.32 0 l-3.28 -7.84 l-2.68 6.4 q-0.08 0.2 -0.25 0.31 t-0.39 0.11 l-1.68 0 q-0.38 0 -0.6 -0.32 q-0.08 -0.14 -0.1 -0.32 t0.04 -0.34 l4.06 -9.52 q0.08 -0.2 0.25 -0.32 t0.39 -0.12 l1.92 0 q0.22 0 0.39 0.12 t0.25 0.32 z M92 5.52 q1.22 0.46 2.2 1.48 q2.1 2.12 2.1 5.41 t-2.1 5.43 q-0.98 1.02 -2.2 1.48 q-1.26 0.52 -2.58 0.52 l-5.66 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -1.44 q0 -0.3 0.21 -0.51 t0.49 -0.21 l5.6 0 q1.78 0 2.89 -1.3 t1.11 -3.26 t-1.11 -3.26 t-2.89 -1.3 l-5.6 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.44 q0 -0.28 0.21 -0.49 t0.49 -0.21 l5.66 0 q1.32 0 2.58 0.52 z"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -1,40 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="3200"
height="1355.480324331485" viewBox="0 0 3200 1355.480324331485">
<g transform="scale(10) translate(10, 10)">
<defs id="SvgjsDefs1385">
<linearGradient id="SvgjsLinearGradient1390">
<stop id="SvgjsStop1391" stop-color="#905e26" offset="0"/>
<stop id="SvgjsStop1392" stop-color="#f5ec9b" offset="0.5"/>
<stop id="SvgjsStop1393" stop-color="#905e26" offset="1"/>
</linearGradient>
<linearGradient id="SvgjsLinearGradient1394">
<stop id="SvgjsStop1395" stop-color="#905e26" offset="0"/>
<stop id="SvgjsStop1396" stop-color="#f5ec9b" offset="0.5"/>
<stop id="SvgjsStop1397" stop-color="#905e26" offset="1"/>
</linearGradient>
</defs>
<g id="SvgjsG1386" featureKey="aMgJeN-0"
transform="matrix(1.5610770874511997,0,0,1.5610770874511997,71.94613967240352,-53.841545411371435)"
fill="#fff">
<path xmlns="http://www.w3.org/2000/svg"
d="M29.399,57.112c0.615,0.308,1.077,0.846,1.077,1.383c0,1.039-1.038,1.961-1.999,1.462l-15.223-7.881 c-0.846-0.499-1.576-0.808-1.576-1.884c0-1.115,0.692-1.385,1.576-1.922l15.223-7.881c1.038-0.346,1.999,0.424,1.999,1.462 c0,0.575-0.461,1.114-1.077,1.423l-13.761,6.918L29.399,57.112z"/>
<path xmlns="http://www.w3.org/2000/svg"
d="M29.033,60.209c-0.208,0-0.413-0.052-0.608-0.152l-15.223-7.881c-0.086-0.05-0.165-0.095-0.242-0.141 c-0.748-0.431-1.395-0.804-1.395-1.843c0-1.057,0.594-1.406,1.346-1.849c0.093-0.054,0.187-0.11,0.284-0.169l15.229-7.885 c0.195-0.066,0.377-0.097,0.558-0.097c0.9,0,1.606,0.728,1.606,1.658c0,0.587-0.437,1.172-1.139,1.522l-13.562,6.818l13.562,6.818 c0.691,0.346,1.139,0.93,1.139,1.484C30.588,59.407,29.861,60.209,29.033,60.209z M28.982,40.419c-0.156,0-0.314,0.026-0.47,0.078 L13.306,48.37c-0.091,0.057-0.188,0.113-0.281,0.167c-0.743,0.439-1.234,0.728-1.234,1.655c0,0.91,0.537,1.219,1.281,1.648 c0.079,0.045,0.158,0.09,0.239,0.139l15.217,7.877c0.162,0.084,0.332,0.127,0.504,0.127c0.697,0,1.33-0.709,1.33-1.488 c0-0.465-0.407-0.98-1.014-1.283l-13.962-7.02l13.962-7.02c0.616-0.308,1.014-0.828,1.014-1.321 C30.363,41.048,29.756,40.419,28.982,40.419z"/>
<path xmlns="http://www.w3.org/2000/svg"
d="M46.385,64.416c-0.231,0.691-0.922,1.077-1.614,0.961c-0.769-0.153-1.269-0.885-1.154-1.692 c0-0.076,0.039-0.191,0.077-0.307l9.88-27.831c0.23-0.692,0.922-1.038,1.614-0.923c0.73,0.154,1.269,0.885,1.153,1.652 c0,0.078-0.039,0.193-0.077,0.27L46.385,64.416z"/>
<path xmlns="http://www.w3.org/2000/svg"
d="M45.016,65.511c-0.088,0-0.177-0.008-0.263-0.022c-0.837-0.167-1.371-0.949-1.247-1.819 c-0.001-0.076,0.038-0.193,0.079-0.318l9.883-27.842c0.207-0.618,0.778-1.02,1.457-1.02c0.094,0,0.188,0.009,0.281,0.023 c0.812,0.172,1.369,0.97,1.247,1.781c0.001,0.086-0.047,0.22-0.087,0.302l-9.875,27.856C46.281,65.084,45.688,65.511,45.016,65.511z M54.925,34.715c-0.58,0-1.068,0.34-1.244,0.867l-9.88,27.833c-0.035,0.104-0.07,0.213-0.07,0.27 c-0.108,0.767,0.349,1.439,1.062,1.582c0.071,0.012,0.147,0.018,0.223,0.018c0.575,0,1.083-0.363,1.263-0.904l9.881-27.872 c0.041-0.085,0.07-0.182,0.07-0.231c0.105-0.712-0.373-1.396-1.064-1.543C55.089,34.722,55.007,34.715,54.925,34.715z"/>
<path xmlns="http://www.w3.org/2000/svg"
d="M84.362,50.192L70.6,43.274c-0.614-0.309-1.075-0.848-1.075-1.423c0-1.038,1.037-1.962,1.998-1.462l15.223,7.881 c0.885,0.537,1.576,0.807,1.576,1.922c0,1.076-0.73,1.385-1.576,1.884l-15.223,7.881c-0.961,0.499-1.998-0.423-1.998-1.462 c0-0.537,0.461-1.075,1.075-1.383L84.362,50.192z"/>
<path xmlns="http://www.w3.org/2000/svg"
d="M70.967,60.209c-0.828,0-1.556-0.802-1.556-1.714c0-0.555,0.447-1.139,1.139-1.484l13.562-6.818L70.55,43.374 c-0.702-0.352-1.139-0.936-1.139-1.522c0-0.913,0.728-1.714,1.556-1.714c0.209,0,0.413,0.051,0.608,0.152l15.223,7.881 c0.104,0.062,0.198,0.119,0.29,0.173c0.753,0.442,1.347,0.792,1.347,1.849c0,1.039-0.646,1.412-1.395,1.843 c-0.078,0.046-0.157,0.091-0.237,0.138l-15.228,7.884C71.38,60.157,71.176,60.209,70.967,60.209z M70.967,40.363 c-0.696,0-1.33,0.709-1.33,1.488c0,0.493,0.397,1.013,1.014,1.321l13.962,7.02l-13.962,7.02c-0.606,0.305-1.014,0.82-1.014,1.283 c0,0.779,0.634,1.488,1.33,1.488c0.172,0,0.342-0.043,0.504-0.127l15.223-7.88c0.075-0.046,0.155-0.091,0.233-0.136 c0.744-0.43,1.282-0.738,1.282-1.648c0-0.928-0.491-1.216-1.235-1.655c-0.093-0.054-0.188-0.11-0.287-0.17l-15.216-7.876 C71.309,40.405,71.139,40.363,70.967,40.363z"/>
</g>
<g id="SvgjsG1387" featureKey="8L6ael-0"
transform="matrix(3.168568052463937,0,0,3.168568052463937,-5.1330821487142195,52.17667742743372)"
fill="#fff">
<path d="M10.56 5.52 q1.22 0.46 2.2 1.48 q2.1 2.12 2.1 5.41 t-2.1 5.43 q-0.98 1.02 -2.2 1.48 q-1.26 0.52 -2.58 0.52 l-5.66 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -1.44 q0 -0.3 0.21 -0.51 t0.49 -0.21 l5.6 0 q1.78 0 2.89 -1.3 t1.11 -3.26 t-1.11 -3.26 t-2.89 -1.3 l-5.6 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.44 q0 -0.28 0.21 -0.49 t0.49 -0.21 l5.66 0 q1.32 0 2.58 0.52 z M32.208 18.86 q0.14 0.32 -0.07 0.65 t-0.57 0.33 l-13.32 0 q-0.18 0 -0.34 -0.09 t-0.24 -0.23 q-0.22 -0.32 -0.06 -0.66 l0.62 -1.46 q0.08 -0.2 0.26 -0.32 t0.38 -0.12 l9.32 0 l-3.28 -7.84 l-2.68 6.4 q-0.08 0.2 -0.25 0.31 t-0.39 0.11 l-1.68 0 q-0.38 0 -0.6 -0.32 q-0.08 -0.14 -0.1 -0.32 t0.04 -0.34 l4.06 -9.52 q0.08 -0.2 0.25 -0.32 t0.39 -0.12 l1.92 0 q0.22 0 0.39 0.12 t0.25 0.32 z M45.535999999999994 12.42 q0.86 0.54 1.31 1.32 t0.45 1.74 q0 2.26 -1.72 3.46 q-1.16 0.84 -3.14 1.06 l-0.08 0 q-0.28 0 -0.46 -0.18 q-0.24 -0.22 -0.24 -0.52 l0 -1.44 q0 -0.26 0.18 -0.46 t0.44 -0.24 q0.94 -0.1 1.46 -0.44 q0.4 -0.24 0.54 -0.62 q0.08 -0.22 0.08 -0.56 q0 -0.22 -0.08 -0.38 t-0.28 -0.3 q-0.58 -0.4 -1.5 -0.68 l-0.32 -0.1 q-1 -0.28 -1.84 -0.46 q-0.18 -0.04 -0.58 -0.14 l-0.22 -0.06 q-0.72 -0.18 -1.52 -0.5 q-1.18 -0.5 -1.94 -1.26 q-0.88 -0.88 -0.88 -2.32 q0 -1.98 1.52 -3.22 q1.04 -0.88 2.92 -1.12 q0.32 -0.04 0.55 0.18 t0.23 0.52 l0 1.44 q0 0.26 -0.16 0.46 t-0.41 0.23 t-0.51 0.11 q-0.58 0.22 -0.82 0.43 t-0.34 0.43 q-0.1 0.34 -0.1 0.64 q0 0.16 0.18 0.34 q0.28 0.28 0.82 0.5 q0.24 0.1 0.84 0.3 l2.5 0.62 l0.12 0.04 q0.94 0.26 1.36 0.4 q0.94 0.32 1.64 0.78 z M42.196 7.9 q-0.24 -0.06 -0.39 -0.25 t-0.15 -0.45 l0 -1.46 q0 -0.32 0.26 -0.54 q0.1 -0.1 0.26 -0.13 t0.3 -0.01 q1.76 0.28 2.86 1.2 q1.5 1.24 1.6 3.16 q0.02 0.28 -0.19 0.51 t-0.51 0.23 l-1.56 0 q-0.26 0 -0.46 -0.18 t-0.22 -0.44 q-0.08 -0.52 -0.34 -0.86 q-0.42 -0.5 -1.3 -0.76 q-0.06 0 -0.08 -0.02 l-0.08 0 z M39.855999999999995 17.08 q0.24 0.04 0.4 0.24 t0.16 0.44 l0 1.48 q0 0.32 -0.24 0.54 q-0.2 0.16 -0.46 0.16 l-0.1 0 q-1.86 -0.28 -3.06 -1.22 q-1.56 -1.24 -1.76 -3.46 q-0.04 -0.32 0.18 -0.55 t0.52 -0.23 l1.58 0 q0.28 0 0.48 0.19 t0.22 0.47 q0.06 1 0.98 1.54 q0.42 0.24 1.1 0.4 z M60.88399999999999 11.12 q0.3 0 0.51 0.21 t0.21 0.51 l0 1.48 q0 0.28 -0.21 0.49 t-0.51 0.21 l-6.58 0 l0 5.12 q0 0.28 -0.2 0.49 t-0.5 0.21 l-1.52 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -7.3 q0 -0.3 0.21 -0.51 t0.49 -0.21 l8.8 0 z M61.78399999999999 5 q0.28 0 0.49 0.21 t0.21 0.49 l0 1.46 q0 0.3 -0.21 0.51 t-0.49 0.21 l-9.7 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.46 q0 -0.28 0.21 -0.49 t0.49 -0.21 l9.7 0 z M79.512 18.86 q0.14 0.32 -0.07 0.65 t-0.57 0.33 l-13.32 0 q-0.18 0 -0.34 -0.09 t-0.24 -0.23 q-0.22 -0.32 -0.06 -0.66 l0.62 -1.46 q0.08 -0.2 0.26 -0.32 t0.38 -0.12 l9.32 0 l-3.28 -7.84 l-2.68 6.4 q-0.08 0.2 -0.25 0.31 t-0.39 0.11 l-1.68 0 q-0.38 0 -0.6 -0.32 q-0.08 -0.14 -0.1 -0.32 t0.04 -0.34 l4.06 -9.52 q0.08 -0.2 0.25 -0.32 t0.39 -0.12 l1.92 0 q0.22 0 0.39 0.12 t0.25 0.32 z M92 5.52 q1.22 0.46 2.2 1.48 q2.1 2.12 2.1 5.41 t-2.1 5.43 q-0.98 1.02 -2.2 1.48 q-1.26 0.52 -2.58 0.52 l-5.66 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.49 l0 -1.44 q0 -0.3 0.21 -0.51 t0.49 -0.21 l5.6 0 q1.78 0 2.89 -1.3 t1.11 -3.26 t-1.11 -3.26 t-2.89 -1.3 l-5.6 0 q-0.28 0 -0.49 -0.21 t-0.21 -0.51 l0 -1.44 q0 -0.28 0.21 -0.49 t0.49 -0.21 l5.66 0 q1.32 0 2.58 0.52 z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,3 +0,0 @@
"ID","NAME","POSITION-X","POSITION-Y","POSITION-Z", "ROTATION-X","ROTATION-Y","ROTATION-Z", "SCALE-X","SCALE-Y","SCALE-Z"
"id2533e7b6-118a-46d0-bad9-11e73462798b","Commerce",1,0,0,0,0,0,.1,.1,.1,
,Platform,1.3,0,0,0,0,0,.1,.1,.1,
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -1,5 +1,5 @@
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.1.0/workbox-sw.js'); importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.1.0/workbox-sw.js');
const VERSION = '0.0.8-19'; const VERSION = '@@VERSION';
const CACHE = "deepdiagram"; const CACHE = "deepdiagram";
const IMAGEDELIVERY_CACHE = "deepdiagram-images"; const IMAGEDELIVERY_CACHE = "deepdiagram-images";
const MAPTILE_CACHE = 'maptiler'; const MAPTILE_CACHE = 'maptiler';
@ -66,35 +66,32 @@ workbox.routing.registerRoute(
}) })
); );
/*workbox.routing.registerRoute( workbox.routing.registerRoute(
new RegExp('/assets/.*'), new RegExp('/assets/.*'),
new workbox.strategies.StaleWhileRevalidate({ new workbox.strategies.StaleWhileRevalidate({
cacheName: CACHE cacheName: CACHE
}) })
); );
*/
/*workbox.routing.registerRoute(
workbox.routing.registerRoute(
new RegExp('/db/.*'), new RegExp('/db/.*'),
new workbox.strategies.StaleWhileRevalidate({ new workbox.strategies.StaleWhileRevalidate({
cacheName: CACHE cacheName: CACHE
}) })
); );
*/
workbox.routing.registerRoute( workbox.routing.registerRoute(
new RegExp('/.*\\.glb'), new RegExp('/.*\\.glb'),
new workbox.strategies.StaleWhileRevalidate({ new workbox.strategies.StaleWhileRevalidate({
cacheName: CACHE cacheName: CACHE
}) })
); );
/*
workbox.routing.registerRoute( workbox.routing.registerRoute(
new RegExp('/.*\\.css'), new RegExp('/.*\\.css'),
new workbox.strategies.StaleWhileRevalidate({ new workbox.strategies.StaleWhileRevalidate({
cacheName: CACHE cacheName: CACHE
}) })
); );
*/

View File

@ -1,245 +0,0 @@
{
"name": "demo",
"dbName": "demo",
"exportDate": "2025-11-20T14:32:58.031Z",
"version": "1.0",
"entities": [
{
"id": "id0c8fd8ad-7dc8-41fa-b61f-b22c2d2b3eb8",
"position": {
"x": 0.4000000059604645,
"y": 1,
"z": 2.9000000953674316
},
"rotation": {
"x": 0,
"y": 3.141592653589793,
"z": 0
},
"last_seen": "2025-11-20T14:30:54.865Z",
"template": "#cylinder-template",
"scale": {
"x": 0.1,
"y": 0.1,
"z": 0.1
},
"color": "#FF00FF",
"text": "db",
"_id": "id0c8fd8ad-7dc8-41fa-b61f-b22c2d2b3eb8"
},
{
"from": "ide476ec05-9aac-42c9-87f1-ba7f18141767",
"to": "idb75dff6c-e056-4a15-b179-adfb2bec793a",
"type": "entity",
"template": "#connection-template",
"color": "#000000",
"id": "id18bf9938-a0b7-4e65-bf91-38697064698a",
"_id": "id18bf9938-a0b7-4e65-bf91-38697064698a"
},
{
"from": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9",
"to": "idb75dff6c-e056-4a15-b179-adfb2bec793a",
"type": "entity",
"template": "#connection-template",
"color": "#000000",
"id": "id47bf19b7-6263-44fa-b289-1f9f63aa6aff",
"_id": "id47bf19b7-6263-44fa-b289-1f9f63aa6aff"
},
{
"from": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9",
"to": "idac543949-c285-4f4b-ab07-d6fd2bbf7bb5",
"type": "entity",
"template": "#connection-template",
"color": "#000000",
"id": "id50594820-ace6-44c7-be5c-2b0747549c75",
"_id": "id50594820-ace6-44c7-be5c-2b0747549c75"
},
{
"from": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9",
"to": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9",
"type": "entity",
"template": "#connection-template",
"color": "#000000",
"id": "id562bf787-0d11-413c-bc2d-194bf05275fd",
"_id": "id562bf787-0d11-413c-bc2d-194bf05275fd"
},
{
"from": "idb75dff6c-e056-4a15-b179-adfb2bec793a",
"to": "id0c8fd8ad-7dc8-41fa-b61f-b22c2d2b3eb8",
"type": "entity",
"template": "#connection-template",
"color": "#000000",
"id": "id5b622c06-95f1-4d04-b023-b1a6851d2107",
"_id": "id5b622c06-95f1-4d04-b023-b1a6851d2107"
},
{
"id": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9",
"position": {
"x": 0.4000000059604645,
"y": 1.7000000476837158,
"z": 2.9000000953674316
},
"rotation": {
"x": 0,
"y": 3.141592653589793,
"z": 0
},
"last_seen": "2025-11-20T14:28:52.061Z",
"template": "#sphere-template",
"text": "browser",
"scale": {
"x": 0.1,
"y": 0.1,
"z": 0.1
},
"color": "#8B4513",
"_id": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9"
},
{
"from": "idac543949-c285-4f4b-ab07-d6fd2bbf7bb5",
"to": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9",
"type": "entity",
"template": "#connection-template",
"color": "#000000",
"id": "id6f4208a8-9b17-45a8-b030-83a80d7e09bf",
"_id": "id6f4208a8-9b17-45a8-b030-83a80d7e09bf"
},
{
"from": "idf1cf90c7-cc2f-4ccc-9bd6-274752f5b66f",
"to": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9",
"type": "entity",
"template": "#connection-template",
"color": "#000000",
"id": "id7205d022-db34-4705-8e3b-930ae0351376",
"_id": "id7205d022-db34-4705-8e3b-930ae0351376"
},
{
"from": "idb75dff6c-e056-4a15-b179-adfb2bec793a",
"to": "ide476ec05-9aac-42c9-87f1-ba7f18141767",
"type": "entity",
"template": "#connection-template",
"color": "#000000",
"id": "id74b9b638-8148-4d98-a715-981f7aebd3bb",
"_id": "id74b9b638-8148-4d98-a715-981f7aebd3bb"
},
{
"from": "id0c8fd8ad-7dc8-41fa-b61f-b22c2d2b3eb8",
"to": "idb75dff6c-e056-4a15-b179-adfb2bec793a",
"type": "entity",
"template": "#connection-template",
"color": "#000000",
"id": "id7af34b2d-f790-45c9-9fce-6c627de1410e",
"_id": "id7af34b2d-f790-45c9-9fce-6c627de1410e"
},
{
"from": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9",
"to": "idf1cf90c7-cc2f-4ccc-9bd6-274752f5b66f",
"type": "entity",
"template": "#connection-template",
"color": "#000000",
"id": "id8bdd38de-6ac9-44c1-95e9-015ed85c0b7a",
"_id": "id8bdd38de-6ac9-44c1-95e9-015ed85c0b7a"
},
{
"id": "idac543949-c285-4f4b-ab07-d6fd2bbf7bb5",
"position": {
"x": -0.6000000238418579,
"y": 1.7000000476837158,
"z": 2.799999952316284
},
"rotation": {
"x": -2.4492937051703357e-16,
"y": 3.141592653589793,
"z": -2.4492937051703357e-16
},
"last_seen": "2025-11-20T14:32:19.261Z",
"template": "#box-template",
"text": "api",
"scale": {
"x": 0.1,
"y": 0.1,
"z": 0.1
},
"color": "#0000FF",
"_id": "idac543949-c285-4f4b-ab07-d6fd2bbf7bb5"
},
{
"id": "idb75dff6c-e056-4a15-b179-adfb2bec793a",
"position": {
"x": 0.4000000059604645,
"y": 1.2999999523162842,
"z": 2.9000000953674316
},
"rotation": {
"x": 0,
"y": 3.141592653589793,
"z": 0
},
"last_seen": "2025-11-20T14:28:36.027Z",
"template": "#box-template",
"text": "server",
"scale": {
"x": 0.1,
"y": 0.1,
"z": 0.1
},
"color": "#006400",
"_id": "idb75dff6c-e056-4a15-b179-adfb2bec793a"
},
{
"from": "idb75dff6c-e056-4a15-b179-adfb2bec793a",
"to": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9",
"type": "entity",
"template": "#connection-template",
"color": "#000000",
"id": "idd6098a95-f534-4126-9aad-9948fdc724c6",
"_id": "idd6098a95-f534-4126-9aad-9948fdc724c6"
},
{
"id": "ide476ec05-9aac-42c9-87f1-ba7f18141767",
"position": {
"x": -0.6000000238418579,
"y": 1.2999999523162842,
"z": 2.799999952316284
},
"rotation": {
"x": -2.4492931757747437e-16,
"y": 3.141592653589793,
"z": -6.429647808784774e-40
},
"last_seen": "2025-11-20T14:30:59.486Z",
"template": "#box-template",
"scale": {
"x": 0.1,
"y": 0.1,
"z": 0.1
},
"color": "#0000FF",
"text": "api",
"_id": "ide476ec05-9aac-42c9-87f1-ba7f18141767"
},
{
"id": "idf1cf90c7-cc2f-4ccc-9bd6-274752f5b66f",
"position": {
"x": 0.4000000059604645,
"y": 2.200000047683716,
"z": 3
},
"rotation": {
"x": -2.4492937051703357e-16,
"y": 3.141592653589793,
"z": -2.4492937051703357e-16
},
"last_seen": "2025-11-20T14:28:58.876Z",
"template": "#person-template",
"text": "user",
"scale": {
"x": 0.1,
"y": 0.1,
"z": 0.1
},
"color": "#FFE4B5",
"_id": "idf1cf90c7-cc2f-4ccc-9bd6-274752f5b66f"
}
]
}

126
server.js
View File

@ -1,133 +1,13 @@
import express from "express"; import express from "express";
import ViteExpress from "vite-express"; import ViteExpress from "vite-express";
import cors from "cors";
import dotenv from "dotenv"; import dotenv from "dotenv";
import newrelic from "newrelic"; import expressProxy from "express-http-proxy";
import apiRoutes from "./server/api/index.js";
import { pouchApp, PouchDB } from "./server/services/databaseService.js";
import { dbAuthMiddleware } from "./server/middleware/dbAuth.js";
// Load .env.local first, then fall back to .env
dotenv.config({ path: '.env.local' });
dotenv.config(); 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(); const app = express();
app.use("/api", expressProxy("local.immersiveidea.com"));
// CORS configuration for split deployment ViteExpress.listen(app, process.env.PORT || 3001, () => console.log("Server is listening..."));
// In combined mode, same-origin requests don't need CORS
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [];
if (allowedOrigins.length > 0) {
app.use(cors({
origin: allowedOrigins,
credentials: true,
}));
}
// Parse JSON for all routes EXCEPT /pouchdb (express-pouchdb handles its own body parsing)
app.use((req, res, next) => {
if (req.path.startsWith('/pouchdb')) {
return next();
}
express.json()(req, res, next);
});
// API routes
app.use("/api", apiRoutes);
// Test endpoint to verify PouchDB is working
app.get("/pouchdb-test/:dbname", async (req, res) => {
try {
const dbName = req.params.dbname;
console.log(`[Test] Creating database: ${dbName}`);
const db = new PouchDB(dbName);
const info = await db.info();
console.log(`[Test] Database info:`, info);
// Try to add a test doc
const result = await db.put({ _id: 'test-doc', hello: 'world' });
console.log(`[Test] Added doc:`, result);
// Read it back
const doc = await db.get('test-doc');
console.log(`[Test] Got doc:`, doc);
res.json({ success: true, info, doc });
} catch (err) {
console.error(`[Test] Error:`, err);
res.status(500).json({ error: err.message, stack: err.stack });
}
});
// PouchDB database sync endpoint with auth middleware
// Public databases (/pouchdb/public-*) are accessible without auth
// Private databases (/pouchdb/private-*) require authentication
// Patch req.query for Express 5 compatibility with express-pouchdb
app.use("/pouchdb", dbAuthMiddleware, (req, res, next) => {
// Express 5 makes req.query read-only, but express-pouchdb needs to write to it
// Redefine as writable property
Object.defineProperty(req, 'query', {
value: { ...req.query },
writable: true,
configurable: true
});
next();
}, pouchApp, (err, req, res, next) => {
console.error('[PouchDB Error]', err);
res.status(500).json({ error: err.message, stack: err.stack });
});
// Check if running in API-only mode (split deployment)
const apiOnly = process.env.API_ONLY === "true";
if (apiOnly) {
// API-only mode: no static file serving
app.listen(process.env.PORT || 3000, () => {
console.log(`API server running on port ${process.env.PORT || 3000}`);
});
} else {
// Combined mode: Vite handles static files + SPA
ViteExpress.listen(app, process.env.PORT || 3001, () => {
console.log(`Server running on port ${process.env.PORT || 3001}`);
});
}

View File

@ -1,178 +0,0 @@
import { Router } from "express";
import { getSession, addMessage, getConversationForAPI } from "../services/sessionStore.js";
import { trackUsage, getUsageSummary, formatCost, getSessionUsage } from "../services/usageTracker.js";
const router = Router();
const ANTHROPIC_API_URL = "https://api.anthropic.com";
/**
* Build entity context string for the system prompt
*/
function buildEntityContext(entities) {
if (!entities || entities.length === 0) {
return "\n\nThe diagram is currently empty.";
}
const entityList = entities.map(e => {
const shape = e.template?.replace('#', '').replace('-template', '') || 'unknown';
const pos = e.position || { x: 0, y: 0, z: 0 };
return `- ${e.text || '(no label)'} (${shape}, ${e.color || 'unknown'}) at (${pos.x?.toFixed(1)}, ${pos.y?.toFixed(1)}, ${pos.z?.toFixed(1)})`;
}).join('\n');
return `\n\n## Current Diagram State\nThe diagram currently contains ${entities.length} entities:\n${entityList}`;
}
// Express 5 uses named parameters for wildcards
router.post("/*path", async (req, res) => {
const requestStart = Date.now();
console.log(`[Claude API] ========== REQUEST START ==========`);
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
console.error(`[Claude API] ERROR: API key not configured`);
return res.status(500).json({ error: "API key not configured" });
}
// Get the path after /api/claude (e.g., /v1/messages)
// Express 5 returns path segments as an array
const pathParam = req.params.path;
const path = "/" + (Array.isArray(pathParam) ? pathParam.join("/") : pathParam || "");
console.log(`[Claude API] Path: ${path}`);
// Check for session-based request
const { sessionId, ...requestBody } = req.body;
let modifiedBody = requestBody;
console.log(`[Claude API] Session ID: ${sessionId || 'none'}`);
console.log(`[Claude API] Model: ${requestBody.model}`);
console.log(`[Claude API] Messages count: ${requestBody.messages?.length || 0}`);
if (sessionId) {
const session = getSession(sessionId);
if (session) {
console.log(`[Claude API] Session found: ${session.entities.length} entities, ${session.conversationHistory.length} messages in history`);
// Inject entity context into system prompt
if (modifiedBody.system) {
const entityContext = buildEntityContext(session.entities);
console.log(`[Claude API] Entity context added (${entityContext.length} chars)`);
modifiedBody.system += entityContext;
}
// Get conversation history and merge with current messages
const historyMessages = getConversationForAPI(sessionId);
if (historyMessages.length > 0 && modifiedBody.messages) {
// Filter out any duplicate messages (in case client sent history too)
const currentContent = modifiedBody.messages[modifiedBody.messages.length - 1]?.content;
const filteredHistory = historyMessages.filter(msg => msg.content !== currentContent);
modifiedBody.messages = [...filteredHistory, ...modifiedBody.messages];
console.log(`[Claude API] Merged ${filteredHistory.length} history + ${modifiedBody.messages.length - filteredHistory.length} new = ${modifiedBody.messages.length} total messages`);
}
} else {
console.log(`[Claude API] WARNING: Session ${sessionId} not found`);
}
}
try {
console.log(`[Claude API] Sending request to Anthropic API...`);
const fetchStart = Date.now();
const response = await fetch(`${ANTHROPIC_API_URL}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify(modifiedBody),
});
const fetchDuration = Date.now() - fetchStart;
console.log(`[Claude API] Response received in ${fetchDuration}ms, status: ${response.status}`);
console.log(`[Claude API] Parsing response JSON...`);
const data = await response.json();
console.log(`[Claude API] Response parsed. Stop reason: ${data.stop_reason}, content blocks: ${data.content?.length || 0}`);
// Track and log token usage
if (data.usage) {
// Extract content for detailed tracking
const userMessage = requestBody.messages?.[requestBody.messages.length - 1];
const inputText = typeof userMessage?.content === 'string' ? userMessage.content : null;
const outputText = data.content
?.filter(c => c.type === 'text')
.map(c => c.text)
.join('\n') || null;
const toolCalls = data.content
?.filter(c => c.type === 'tool_use')
.map(c => ({ name: c.name, input: c.input })) || [];
const usageRecord = trackUsage(sessionId, modifiedBody.model, data.usage, {
inputText,
outputText,
toolCalls
});
console.log(`[Claude API] REQUEST USAGE: ${getUsageSummary(usageRecord)}`);
// Log cumulative session usage if session exists
if (sessionId) {
const sessionStats = getSessionUsage(sessionId);
if (sessionStats) {
console.log(`[Claude API] SESSION TOTALS (${sessionStats.requestCount} requests):`);
console.log(`[Claude API] Total input: ${sessionStats.totalInputTokens} tokens`);
console.log(`[Claude API] Total output: ${sessionStats.totalOutputTokens} tokens`);
console.log(`[Claude API] Total cost: ${formatCost(sessionStats.totalCost)}`);
}
}
}
if (data.error) {
console.error(`[Claude API] API returned error:`, data.error);
}
// If session exists and response is successful, store messages
if (sessionId && response.ok && data.content) {
const session = getSession(sessionId);
if (session) {
// Store the user message if it was new (only if it's a string, not tool results)
const userMessage = requestBody.messages?.[requestBody.messages.length - 1];
if (userMessage && userMessage.role === 'user' && typeof userMessage.content === 'string') {
addMessage(sessionId, {
role: 'user',
content: userMessage.content
});
console.log(`[Claude API] Stored user message to session`);
}
// Store the assistant response (text only, not tool use blocks)
const assistantContent = data.content
.filter(c => c.type === 'text')
.map(c => c.text)
.join('\n');
if (assistantContent) {
addMessage(sessionId, {
role: 'assistant',
content: assistantContent
});
console.log(`[Claude API] Stored assistant response to session (${assistantContent.length} chars)`);
}
}
}
const totalDuration = Date.now() - requestStart;
console.log(`[Claude API] ========== REQUEST COMPLETE (${totalDuration}ms) ==========`);
res.status(response.status).json(data);
} catch (error) {
const totalDuration = Date.now() - requestStart;
console.error(`[Claude API] ========== REQUEST FAILED (${totalDuration}ms) ==========`);
console.error(`[Claude API] Error:`, error);
console.error(`[Claude API] Error message:`, error.message);
console.error(`[Claude API] Error stack:`, error.stack);
res.status(500).json({ error: "Failed to proxy request to Claude API", details: error.message });
}
});
export default router;

View File

@ -1,213 +0,0 @@
import { Router } from "express";
import { getSession, addMessage, getConversationForAPI } from "../services/sessionStore.js";
import { trackUsage, getUsageSummary, formatCost, getSessionUsage } from "../services/usageTracker.js";
import { getCloudflareAccountId, getCloudflareApiToken } from "../services/providerConfig.js";
import {
claudeToolsToCloudflare,
claudeMessagesToCloudflare,
cloudflareResponseToClaude
} from "../services/toolConverter.js";
const router = Router();
/**
* Build entity context string for the system prompt
*/
function buildEntityContext(entities) {
if (!entities || entities.length === 0) {
return "\n\nThe diagram is currently empty.";
}
const entityList = entities.map(e => {
const shape = e.template?.replace('#', '').replace('-template', '') || 'unknown';
const pos = e.position || { x: 0, y: 0, z: 0 };
return `- ${e.text || '(no label)'} (${shape}, ${e.color || 'unknown'}) at (${pos.x?.toFixed(1)}, ${pos.y?.toFixed(1)}, ${pos.z?.toFixed(1)})`;
}).join('\n');
return `\n\n## Current Diagram State\nThe diagram currently contains ${entities.length} entities:\n${entityList}`;
}
// Express 5 uses named parameters for wildcards
router.post("/*path", async (req, res) => {
const requestStart = Date.now();
console.log(`[Cloudflare API] ========== REQUEST START ==========`);
const accountId = getCloudflareAccountId();
const apiToken = getCloudflareApiToken();
if (!accountId) {
console.error(`[Cloudflare API] ERROR: Account ID not configured`);
return res.status(500).json({ error: "Cloudflare account ID not configured" });
}
if (!apiToken) {
console.error(`[Cloudflare API] ERROR: API token not configured`);
return res.status(500).json({ error: "Cloudflare API token not configured" });
}
// Check for session-based request
const { sessionId, ...requestBody } = req.body;
let modifiedBody = { ...requestBody };
const model = requestBody.model;
console.log(`[Cloudflare API] Session ID: ${sessionId || 'none'}`);
console.log(`[Cloudflare API] Model: ${model}`);
console.log(`[Cloudflare API] Messages count: ${requestBody.messages?.length || 0}`);
// Build system prompt with entity context
let systemPrompt = modifiedBody.system || '';
if (sessionId) {
const session = getSession(sessionId);
if (session) {
console.log(`[Cloudflare API] Session found: ${session.entities.length} entities, ${session.conversationHistory.length} messages in history`);
// Inject entity context into system prompt
const entityContext = buildEntityContext(session.entities);
console.log(`[Cloudflare API] Entity context added (${entityContext.length} chars)`);
systemPrompt += entityContext;
// Get conversation history and merge with current messages
const historyMessages = getConversationForAPI(sessionId);
if (historyMessages.length > 0 && modifiedBody.messages) {
const currentContent = modifiedBody.messages[modifiedBody.messages.length - 1]?.content;
const filteredHistory = historyMessages.filter(msg => msg.content !== currentContent);
modifiedBody.messages = [...filteredHistory, ...modifiedBody.messages];
console.log(`[Cloudflare API] Merged ${filteredHistory.length} history + ${modifiedBody.messages.length - filteredHistory.length} new = ${modifiedBody.messages.length} total messages`);
}
} else {
console.log(`[Cloudflare API] WARNING: Session ${sessionId} not found`);
}
}
try {
// Convert to Cloudflare format
const cfMessages = claudeMessagesToCloudflare(modifiedBody.messages || [], systemPrompt);
const cfTools = modifiedBody.tools ? claudeToolsToCloudflare(modifiedBody.tools) : undefined;
// Build Cloudflare request body
const cfRequestBody = {
messages: cfMessages,
max_tokens: modifiedBody.max_tokens || 1024
};
// Only include tools if the model supports them
if (cfTools && cfTools.length > 0) {
cfRequestBody.tools = cfTools;
}
// Cloudflare endpoint: https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/{model}
const endpoint = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${model}`;
console.log(`[Cloudflare API] Sending request to: ${endpoint}`);
console.log(`[Cloudflare API] Request body messages: ${cfMessages.length}, tools: ${cfTools?.length || 0}`);
const requestBodyJson = JSON.stringify(cfRequestBody);
console.log(`[Cloudflare API] Full request body (${requestBodyJson.length} bytes):`);
console.log(requestBodyJson);
const fetchStart = Date.now();
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiToken}`,
},
body: JSON.stringify(cfRequestBody),
});
const fetchDuration = Date.now() - fetchStart;
console.log(`[Cloudflare API] Response received in ${fetchDuration}ms, status: ${response.status}`);
console.log(`[Cloudflare API] Parsing response JSON...`);
const cfData = await response.json();
if (!cfData.success) {
console.error(`[Cloudflare API] API returned error:`, cfData.errors);
return res.status(response.status).json({
error: cfData.errors?.[0]?.message || "Cloudflare API error",
details: cfData.errors
});
}
// Convert Cloudflare response to Claude format
const data = cloudflareResponseToClaude(cfData, model);
console.log(`[Cloudflare API] Response converted. Stop reason: ${data.stop_reason}, content blocks: ${data.content?.length || 0}`);
// Track and log token usage
if (data.usage) {
// Extract content for detailed tracking
const userMessage = requestBody.messages?.[requestBody.messages.length - 1];
const inputText = typeof userMessage?.content === 'string' ? userMessage.content : null;
const outputText = data.content
?.filter(c => c.type === 'text')
.map(c => c.text)
.join('\n') || null;
const toolCalls = data.content
?.filter(c => c.type === 'tool_use')
.map(c => ({ name: c.name, input: c.input })) || [];
const usageRecord = trackUsage(sessionId, model, data.usage, {
inputText,
outputText,
toolCalls
});
console.log(`[Cloudflare API] REQUEST USAGE: ${getUsageSummary(usageRecord)}`);
// Log cumulative session usage if session exists
if (sessionId) {
const sessionStats = getSessionUsage(sessionId);
if (sessionStats) {
console.log(`[Cloudflare API] SESSION TOTALS (${sessionStats.requestCount} requests):`);
console.log(`[Cloudflare API] Total input: ${sessionStats.totalInputTokens} tokens`);
console.log(`[Cloudflare API] Total output: ${sessionStats.totalOutputTokens} tokens`);
console.log(`[Cloudflare API] Total cost: ${formatCost(sessionStats.totalCost)}`);
}
}
}
// If session exists and response is successful, store messages
if (sessionId && response.ok && data.content) {
const session = getSession(sessionId);
if (session) {
// Store the user message if it was new (only if it's a string, not tool results)
const userMessage = requestBody.messages?.[requestBody.messages.length - 1];
if (userMessage && userMessage.role === 'user' && typeof userMessage.content === 'string') {
addMessage(sessionId, {
role: 'user',
content: userMessage.content
});
console.log(`[Cloudflare API] Stored user message to session`);
}
// Store the assistant response (text only, not tool use blocks)
const assistantContent = data.content
.filter(c => c.type === 'text')
.map(c => c.text)
.join('\n');
if (assistantContent) {
addMessage(sessionId, {
role: 'assistant',
content: assistantContent
});
console.log(`[Cloudflare API] Stored assistant response to session (${assistantContent.length} chars)`);
}
}
}
const totalDuration = Date.now() - requestStart;
console.log(`[Cloudflare API] ========== REQUEST COMPLETE (${totalDuration}ms) ==========`);
res.status(response.status).json(data);
} catch (error) {
const totalDuration = Date.now() - requestStart;
console.error(`[Cloudflare API] ========== REQUEST FAILED (${totalDuration}ms) ==========`);
console.error(`[Cloudflare API] Error:`, error);
console.error(`[Cloudflare API] Error message:`, error.message);
console.error(`[Cloudflare API] Error stack:`, error.stack);
res.status(500).json({ error: "Failed to proxy request to Cloudflare API", details: error.message });
}
});
export default router;

View File

@ -1,30 +0,0 @@
import { Router } from "express";
import claudeRouter from "./claude.js";
import ollamaRouter from "./ollama.js";
import cloudflareRouter from "./cloudflare.js";
import sessionRouter from "./session.js";
import userRouter from "./user.js";
const router = Router();
// Session management
router.use("/session", sessionRouter);
// User features
router.use("/user", userRouter);
// Claude API proxy
router.use("/claude", claudeRouter);
// Ollama API proxy
router.use("/ollama", ollamaRouter);
// Cloudflare Workers AI proxy
router.use("/cloudflare", cloudflareRouter);
// Health check
router.get("/health", (req, res) => {
res.json({ status: "ok" });
});
export default router;

View File

@ -1,178 +0,0 @@
import { Router } from "express";
import { getSession, addMessage, getConversationForAPI } from "../services/sessionStore.js";
import { getOllamaUrl } from "../services/providerConfig.js";
import {
claudeToolsToOllama,
claudeMessagesToOllama,
ollamaResponseToClaude
} from "../services/toolConverter.js";
const router = Router();
/**
* Build entity context string for the system prompt
*/
function buildEntityContext(entities) {
if (!entities || entities.length === 0) {
return "\n\nThe diagram is currently empty.";
}
const entityList = entities.map(e => {
const shape = e.template?.replace('#', '').replace('-template', '') || 'unknown';
const pos = e.position || { x: 0, y: 0, z: 0 };
return `- ${e.text || '(no label)'} (${shape}, ${e.color || 'unknown'}) at (${pos.x?.toFixed(1)}, ${pos.y?.toFixed(1)}, ${pos.z?.toFixed(1)})`;
}).join('\n');
return `\n\n## Current Diagram State\nThe diagram currently contains ${entities.length} entities:\n${entityList}`;
}
/**
* Handle Ollama chat requests
* Accepts Claude-format requests and converts them to Ollama format
*/
router.post("/*path", async (req, res) => {
const requestStart = Date.now();
console.log(`[Ollama API] ========== REQUEST START ==========`);
const ollamaUrl = getOllamaUrl();
console.log(`[Ollama API] Using Ollama at: ${ollamaUrl}`);
// Extract request body (Claude format)
const { sessionId, model, max_tokens, system, tools, messages } = req.body;
console.log(`[Ollama API] Session ID: ${sessionId || 'none'}`);
console.log(`[Ollama API] Model: ${model}`);
console.log(`[Ollama API] Messages count: ${messages?.length || 0}`);
// Build system prompt with entity context
let systemPrompt = system || '';
if (sessionId) {
const session = getSession(sessionId);
if (session) {
console.log(`[Ollama API] Session found: ${session.entities.length} entities, ${session.conversationHistory.length} messages in history`);
// Inject entity context into system prompt
const entityContext = buildEntityContext(session.entities);
console.log(`[Ollama API] Entity context added (${entityContext.length} chars)`);
systemPrompt += entityContext;
// Get conversation history and merge with current messages
const historyMessages = getConversationForAPI(sessionId);
if (historyMessages.length > 0 && messages) {
const currentContent = messages[messages.length - 1]?.content;
const filteredHistory = historyMessages.filter(msg => msg.content !== currentContent);
messages.unshift(...filteredHistory);
console.log(`[Ollama API] Merged ${filteredHistory.length} history messages`);
}
} else {
console.log(`[Ollama API] WARNING: Session ${sessionId} not found`);
}
}
// Convert to Ollama format
const ollamaMessages = claudeMessagesToOllama(messages || [], systemPrompt);
const ollamaTools = claudeToolsToOllama(tools);
const ollamaRequest = {
model: model,
messages: ollamaMessages,
stream: false,
options: {
num_predict: max_tokens || 1024
}
};
// Only add tools if there are any
if (ollamaTools.length > 0) {
ollamaRequest.tools = ollamaTools;
}
console.log(`[Ollama API] Converted to Ollama format: ${ollamaMessages.length} messages, ${ollamaTools.length} tools`);
try {
console.log(`[Ollama API] Sending request to Ollama...`);
const fetchStart = Date.now();
const response = await fetch(`${ollamaUrl}/api/chat`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(ollamaRequest)
});
const fetchDuration = Date.now() - fetchStart;
console.log(`[Ollama API] Response received in ${fetchDuration}ms, status: ${response.status}`);
if (!response.ok) {
const errorText = await response.text();
console.error(`[Ollama API] Error response:`, errorText);
return res.status(response.status).json({
error: `Ollama API error: ${response.status}`,
details: errorText
});
}
const ollamaData = await response.json();
console.log(`[Ollama API] Response parsed. Done: ${ollamaData.done}, model: ${ollamaData.model}`);
// Convert response back to Claude format
const claudeResponse = ollamaResponseToClaude(ollamaData);
console.log(`[Ollama API] Converted to Claude format. Stop reason: ${claudeResponse.stop_reason}, content blocks: ${claudeResponse.content.length}`);
// Store messages to session if applicable
if (sessionId && claudeResponse.content) {
const session = getSession(sessionId);
if (session) {
// Store the user message if it was new
const userMessage = messages?.[messages.length - 1];
if (userMessage && userMessage.role === 'user' && typeof userMessage.content === 'string') {
addMessage(sessionId, {
role: 'user',
content: userMessage.content
});
console.log(`[Ollama API] Stored user message to session`);
}
// Store the assistant response (text only)
const assistantContent = claudeResponse.content
.filter(c => c.type === 'text')
.map(c => c.text)
.join('\n');
if (assistantContent) {
addMessage(sessionId, {
role: 'assistant',
content: assistantContent
});
console.log(`[Ollama API] Stored assistant response to session (${assistantContent.length} chars)`);
}
}
}
const totalDuration = Date.now() - requestStart;
console.log(`[Ollama API] ========== REQUEST COMPLETE (${totalDuration}ms) ==========`);
res.json(claudeResponse);
} catch (error) {
const totalDuration = Date.now() - requestStart;
console.error(`[Ollama API] ========== REQUEST FAILED (${totalDuration}ms) ==========`);
console.error(`[Ollama API] Error:`, error);
// Check if it's a connection error
if (error.cause?.code === 'ECONNREFUSED') {
return res.status(503).json({
error: "Ollama is not running",
details: `Could not connect to Ollama at ${ollamaUrl}. Make sure Ollama is installed and running.`
});
}
res.status(500).json({
error: "Failed to proxy request to Ollama",
details: error.message
});
}
});
export default router;

View File

@ -1,176 +0,0 @@
import { Router } from "express";
import {
createSession,
getSession,
findSessionByDiagram,
syncEntities,
addMessage,
clearHistory,
deleteSession,
getStats
} from "../services/sessionStore.js";
import { getSessionUsage, getGlobalUsage, formatCost } from "../services/usageTracker.js";
const router = Router();
/**
* GET /api/session/debug/stats
* Get session statistics (for debugging)
* Query params:
* - details=true: Include full entity and conversation data
* NOTE: Must be before /:id routes to avoid matching "debug" as an id
*/
router.get("/debug/stats", (req, res) => {
const includeDetails = req.query.details === 'true';
const stats = getStats(includeDetails);
console.log('[Session Debug] Stats requested:', JSON.stringify(stats, null, 2));
res.json(stats);
});
/**
* GET /api/session/usage/global
* Get global token usage and cost statistics
* NOTE: Must be before /:id routes
*/
router.get("/usage/global", (req, res) => {
const usage = getGlobalUsage();
res.json({
...usage,
totalCostFormatted: formatCost(usage.totalCost),
uptimeFormatted: `${Math.round(usage.uptime / 1000 / 60)} minutes`
});
});
/**
* GET /api/session/:id/usage
* Get token usage and cost for a specific session
*/
router.get("/:id/usage", (req, res) => {
const usage = getSessionUsage(req.params.id);
if (!usage) {
return res.status(404).json({ error: "No usage data for session" });
}
res.json({
...usage,
totalCostFormatted: formatCost(usage.totalCost)
});
});
/**
* POST /api/session/create
* Create a new session or return existing one for a diagram
*/
router.post("/create", (req, res) => {
const { diagramId } = req.body;
if (!diagramId) {
return res.status(400).json({ error: "diagramId is required" });
}
// Check for existing session
let session = findSessionByDiagram(diagramId);
if (session) {
console.log(`[Session] Resuming existing session ${session.id} for diagram ${diagramId} (${session.conversationHistory.length} messages, ${session.entities.length} entities)`);
return res.json({
session,
isNew: false
});
}
// Create new session
session = createSession(diagramId);
console.log(`[Session] Created new session ${session.id} for diagram ${diagramId}`);
res.json({
session,
isNew: true
});
});
/**
* GET /api/session/:id
* Get session details including history
*/
router.get("/:id", (req, res) => {
const session = getSession(req.params.id);
if (!session) {
return res.status(404).json({ error: "Session not found" });
}
res.json({ session });
});
/**
* PUT /api/session/:id/sync
* Sync entities from client to server
*/
router.put("/:id/sync", (req, res) => {
const { entities } = req.body;
if (!entities || !Array.isArray(entities)) {
return res.status(400).json({ error: "entities array is required" });
}
const session = syncEntities(req.params.id, entities);
if (!session) {
return res.status(404).json({ error: "Session not found" });
}
console.log(`[Session ${req.params.id}] Synced ${entities.length} entities:`,
entities.map(e => `${e.text || '(no label)'} (${e.template})`).join(', ') || 'none');
res.json({ success: true, entityCount: entities.length });
});
/**
* POST /api/session/:id/message
* Add a message to history (used after successful Claude response)
*/
router.post("/:id/message", (req, res) => {
const { role, content, toolResults } = req.body;
if (!role || !content) {
return res.status(400).json({ error: "role and content are required" });
}
const session = addMessage(req.params.id, { role, content, toolResults });
if (!session) {
return res.status(404).json({ error: "Session not found" });
}
res.json({ success: true, messageCount: session.conversationHistory.length });
});
/**
* DELETE /api/session/:id/history
* Clear conversation history
*/
router.delete("/:id/history", (req, res) => {
const session = clearHistory(req.params.id);
if (!session) {
return res.status(404).json({ error: "Session not found" });
}
res.json({ success: true });
});
/**
* DELETE /api/session/:id
* Delete a session entirely
*/
router.delete("/:id", (req, res) => {
const deleted = deleteSession(req.params.id);
if (!deleted) {
return res.status(404).json({ error: "Session not found" });
}
res.json({ success: true });
});
export default router;

View File

@ -1,155 +0,0 @@
import { Router } from "express";
const router = Router();
// Feature configurations by tier
const FEATURE_CONFIGS = {
none: {
tier: 'none',
pages: {
examples: 'off',
documentation: 'off',
pricing: 'off',
vrExperience: 'off',
},
features: {
createDiagram: 'off',
createFromTemplate: 'off',
manageDiagrams: 'off',
shareCollaborate: 'off',
privateDesigns: 'off',
encryptedDesigns: 'off',
editData: 'off',
config: 'off',
enterImmersive: 'off',
launchMetaQuest: 'off',
},
limits: {
maxDiagrams: 0,
maxCollaborators: 0,
storageQuotaMB: 0,
},
},
free: {
tier: 'free',
pages: {
examples: 'on',
documentation: 'on',
pricing: 'on',
vrExperience: 'on',
},
features: {
createDiagram: 'on',
createFromTemplate: 'coming-soon',
manageDiagrams: 'on',
shareCollaborate: 'on',
privateDesigns: 'coming-soon',
encryptedDesigns: 'pro',
editData: 'on',
config: 'on',
enterImmersive: 'on',
launchMetaQuest: 'on',
},
limits: {
maxDiagrams: 6,
maxCollaborators: 0,
storageQuotaMB: 100,
},
},
basic: {
tier: 'basic',
pages: {
examples: 'on',
documentation: 'on',
pricing: 'on',
vrExperience: 'on',
},
features: {
createDiagram: 'on',
createFromTemplate: 'on',
manageDiagrams: 'on',
shareCollaborate: 'on',
privateDesigns: 'on',
encryptedDesigns: 'pro',
editData: 'on',
config: 'on',
enterImmersive: 'on',
launchMetaQuest: 'on',
},
limits: {
maxDiagrams: 25,
maxCollaborators: 0,
storageQuotaMB: 500,
},
},
pro: {
tier: 'pro',
pages: {
examples: 'on',
documentation: 'on',
pricing: 'on',
vrExperience: 'on',
},
features: {
createDiagram: 'on',
createFromTemplate: 'on',
manageDiagrams: 'on',
shareCollaborate: 'on',
privateDesigns: 'on',
encryptedDesigns: 'on',
editData: 'on',
config: 'on',
enterImmersive: 'on',
launchMetaQuest: 'on',
},
limits: {
maxDiagrams: -1,
maxCollaborators: -1,
storageQuotaMB: -1,
},
},
};
// Default tier for authenticated users without a specific tier
const DEFAULT_TIER = 'basic';
/**
* GET /api/user/features
* Returns feature configuration for the current user
*
* Query params:
* - tier: Override tier for testing (e.g., ?tier=pro)
*/
router.get("/features", (req, res) => {
// Allow tier override via query param for testing
const tierOverride = req.query.tier;
// TODO: In production, determine tier from JWT token or user database
// For now, use query param override or default to 'basic'
const tier = tierOverride && FEATURE_CONFIGS[tierOverride]
? tierOverride
: DEFAULT_TIER;
const config = FEATURE_CONFIGS[tier];
console.log(`[User] Returning feature config for tier: ${tier}`);
res.json(config);
});
/**
* GET /api/user/features/:tier
* Returns feature configuration for a specific tier (for testing/admin)
*/
router.get("/features/:tier", (req, res) => {
const { tier } = req.params;
const config = FEATURE_CONFIGS[tier];
if (!config) {
return res.status(404).json({ error: `Unknown tier: ${tier}` });
}
console.log(`[User] Returning feature config for tier: ${tier}`);
res.json(config);
});
export default router;

View File

@ -1,19 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDHTCCAgWgAwIBAgIJF3WqWLMk6JOlMA0GCSqGSIb3DQEBCwUAMCwxKjAoBgNV
BAMTIWRldi1nMGx0MThuZGJjcDZlYXJyLnVzLmF1dGgwLmNvbTAeFw0yNDA4MTUx
NTE1NDdaFw0zODA0MjQxNTE1NDdaMCwxKjAoBgNVBAMTIWRldi1nMGx0MThuZGJj
cDZlYXJyLnVzLmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAKfmuyqb2N8Tf5W9KTAy2D6FTkYICX+mcclyZS+0Mi7fLzSB7IeSuWXmuHoR
h5FHJ/Qp6eC1ahYs9WmAjFp81HPzZ/9hEbK3XrLMSta7zVldPTQjnt5sU/Zxr/M2
xMjHH2P3G231si+G20czvDWoItnyWs8rcE2wEcyiXM+/Ixgxoh8kfc9pqpNLXTvM
IvqAuxXbPeju3XccQ6B0lshN72EwV9yW73B0s7DuHsbBA0WHKYmcvdXgnQ1dU2/L
8BR5s/gJJE0MUh2qhsnKE3yUC/hTW7A0Qn0SMEZey04hvJWePnn59kv52DPVXZpZ
ql6ISehwn3hZdhHjpsoHbE48CN0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd
BgNVHQ4EFgQUbjF1ri0QhHovlQ2D5gPtDucLGhowDgYDVR0PAQH/BAQDAgKEMA0G
CSqGSIb3DQEBCwUAA4IBAQAxIRNANDbVrkXF6x/KHYgj5prb+4yHtmYb7tiRi51z
MNHNLkltNou3dWsS4tU/YgzHTof3SJe2CIg9xAgk0XTHZjxRtbwIY6Zc9Sgf/KKL
OxFIiNcIQIGDoKHWmv2w4qSrYBkH9hva4kCysjgIFNc+0il7DQR2ifwLOxQGl/AE
hSfexgUKjfrno12gBlNCNcP+Xyn9/G++eg9vV+RuGLLIyLX0d0Vl7/C1pGoDrNpO
m/3oxR4IRnhEfGBD+LdWvmmIuxzXM1hSbLYJbMotHqKZSh0XlEM6Mi12gMZi7sEC
lhbXs+4ecvTBFfGCWFyUISFoSwRRnpQnEM5DsZT/t/Z8
-----END CERTIFICATE-----

View File

@ -1,126 +0,0 @@
; CouchDB Configuration Settings
; Custom settings should be made in this file. They will override settings
; in default.ini, but unlike changes made to default.ini, this file won't be
; overwritten on server upgrade.
[couchdb]
database_dir = /var/snap/couchdb/common/data
view_index_dir = /var/snap/couchdb/common/data
;max_document_size = 4294967296 ; bytes
;os_process_timeout = 5000
uuid = dd27b78cfb458b894e0277173f176878
[couch_peruser]
; If enabled, couch_peruser ensures that a private per-user database
; exists for each document in _users. These databases are writable only
; by the corresponding user. Databases are in the following form:
; userdb-{hex encoded username}
enable = true
; If set to true and a user is deleted, the respective database gets
; deleted as well.
delete_dbs = true
; Set a default q value for peruser-created databases that is different from
; cluster / q
;q = 1
[log]
level = debug
[chttpd]
;port = 5984
bind_address = 127.0.0.1
authentication_handlers = {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}
;authentication_handlers = {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}
; Options for the MochiWeb HTTP server.
;server_options = [{backlog, 128}, {acceptor_pool_size, 16}]
; For more socket options, consult Erlang's module 'inet' man page.
;socket_options = [{sndbuf, 262144}, {nodelay, true}]
enable_cors = true
[httpd]
; NOTE that this only configures the "backend" node-local port, not the
; "frontend" clustered port. You probably don't want to change anything in
; this section.
; Uncomment next line to trigger basic-auth popup on unauthorized requests.
;WWW-Authenticate = Basic realm="administrator"
; Uncomment next line to set the configuration modification whitelist. Only
; whitelisted values may be changed via the /_config URLs. To allow the admin
; to change this value over HTTP, remember to include {httpd,config_whitelist}
; itself. Excluding it from the list would require editing this file to update
; the whitelist.
;config_whitelist = [{httpd,config_whitelist}, {log,level}, {etc,etc}]
[ssl]
;enable = true
;cert_file = /full/path/to/server_cert.pem
;key_file = /full/path/to/server_key.pem
;password = somepassword
; set to true to validate peer certificates
;verify_ssl_certificates = false
; Set to true to fail if the client does not send a certificate. Only used if verify_ssl_certificates is true.
;fail_if_no_peer_cert = false
; Path to file containing PEM encoded CA certificates (trusted
; certificates used for verifying a peer certificate). May be omitted if
; you do not want to verify the peer.
;cacert_file = /full/path/to/cacertf
; The verification fun (optional) if not specified, the default
; verification fun will be used.
;verify_fun = {Module, VerifyFun}
; maximum peer certificate depth
;ssl_certificate_max_depth = 1
; Reject renegotiations that do not live up to RFC 5746.
;secure_renegotiate = true
; The cipher suites that should be supported.
; Can be specified in erlang format "{ecdhe_ecdsa,aes_128_cbc,sha256}"
; or in OpenSSL format "ECDHE-ECDSA-AES128-SHA256".
;ciphers = ["ECDHE-ECDSA-AES128-SHA256", "ECDHE-ECDSA-AES128-SHA"]
; The SSL/TLS versions to support
;tls_versions = [tlsv1, 'tlsv1.1', 'tlsv1.2']
; To enable Virtual Hosts in CouchDB, add a vhost = path directive. All requests to
; the Virtual Host will be redirected to the path. In the example below all requests
; to http://example.com/ are redirected to /database.
; If you run CouchDB on a specific port, include the port number in the vhost:
; example.com:5984 = /database
[vhosts]
;example.com = /database/
; To create an admin account uncomment the '[admins]' section below and add a
; line in the format 'username = password'. When you next start CouchDB, it
; will change the password to a hash (so that your passwords don't linger
; around in plain-text files). You can add more admin accounts with more
; 'username = password' lines. Don't forget to restart CouchDB after
; changing this.
[admins]
admin = -pbkdf2-eeee185ee6142700c0e5a9e31b1d6d85ba952a49,f073f989f3201b55d953825d56acad2a,10
[chttpd_auth]
secret = a6bc1f1fd52803b4feae8f30b3944300
;authentication_handlers = {chttpd_auth, jwt_authentication_handler}
[jwt_auth]
roles_claim_path = metadata.databases
required_claims = exp,iat
;validate_claim_iss = https://dev-g0lt18ndbcp6earr.us.auth0.com/
;validate_claim_aud = sxAJub9Uo2mOE7iYCTOuQGhppGLEPWzb
[jwt_keys]
rsa:_default = -----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+a7KpvY3xN/lb0pMDLY\nPoVORggJf6ZxyXJlL7QyLt8vNIHsh5K5Zea4ehGHkUcn9Cnp4LVqFiz1aYCMWnzU\nc/Nn/2ERsrdessxK1rvNWV09NCOe3mxT9nGv8zbEyMcfY/cbbfWyL4bbRzO8Nagi\n2fJazytwTbARzKJcz78jGDGiHyR9z2mqk0tdO8wi+oC7Fds96O7ddxxDoHSWyE3v\nYTBX3JbvcHSzsO4exsEDRYcpiZy91eCdDV1Tb8vwFHmz+AkkTQxSHaqGycoTfJQL\n+FNbsDRCfRIwRl7LTiG8lZ4+efn2S/nYM9VdmlmqXohJ6HCfeFl2EeOmygdsTjwI\n3QIDAQAB\n-----END PUBLIC KEY-----\n
rsa:1R0ZY6dzJ7ttWk60bT0_V = -----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+a7KpvY3xN/lb0pMDLY\nPoVORggJf6ZxyXJlL7QyLt8vNIHsh5K5Zea4ehGHkUcn9Cnp4LVqFiz1aYCMWnzU\nc/Nn/2ERsrdessxK1rvNWV09NCOe3mxT9nGv8zbEyMcfY/cbbfWyL4bbRzO8Nagi\n2fJazytwTbARzKJcz78jGDGiHyR9z2mqk0tdO8wi+oC7Fds96O7ddxxDoHSWyE3v\nYTBX3JbvcHSzsO4exsEDRYcpiZy91eCdDV1Tb8vwFHmz+AkkTQxSHaqGycoTfJQL\n+FNbsDRCfRIwRl7LTiG8lZ4+efn2S/nYM9VdmlmqXohJ6HCfeFl2EeOmygdsTjwI\n3QIDAQAB\n-----END PUBLIC KEY-----\n
[cors]
origins = https://www.cybersecshield.com,https://cybersecshield.com,http://localhost:5173
headers = accept, authorization, content-type, origin, referer
credentials = true
methods = GET, PUT, POST, HEAD, DELETE

View File

@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+a7KpvY3xN/lb0pMDLY
PoVORggJf6ZxyXJlL7QyLt8vNIHsh5K5Zea4ehGHkUcn9Cnp4LVqFiz1aYCMWnzU
c/Nn/2ERsrdessxK1rvNWV09NCOe3mxT9nGv8zbEyMcfY/cbbfWyL4bbRzO8Nagi
2fJazytwTbARzKJcz78jGDGiHyR9z2mqk0tdO8wi+oC7Fds96O7ddxxDoHSWyE3v
YTBX3JbvcHSzsO4exsEDRYcpiZy91eCdDV1Tb8vwFHmz+AkkTQxSHaqGycoTfJQL
+FNbsDRCfRIwRl7LTiG8lZ4+efn2S/nYM9VdmlmqXohJ6HCfeFl2EeOmygdsTjwI
3QIDAQAB
-----END PUBLIC KEY-----

View File

@ -1,101 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
# Each node in the system must have a unique name. These are specified through
# the Erlang -name flag, which takes the form:
#
# -name nodename@<FQDN>
#
# or
#
# -name nodename@<IP-ADDRESS>
#
# CouchDB recommends the following values for this flag:
#
# 1. If this is a single node, not in a cluster, use:
# -name couchdb@127.0.0.1
#
# 2. If DNS is configured for this host, use the FQDN, such as:
# -name couchdb@my.host.domain.com
#
# 3. If DNS isn't configured for this host, use IP addresses only, such as:
# -name couchdb@192.168.0.1
#
# Do not rely on tricks with /etc/hosts or libresolv to handle anything
# other than the above 3 approaches correctly. They will not work reliably.
#
# Multiple CouchDBs running on the same machine can use couchdb1@, couchdb2@,
# etc.
-name couchdb@127.0.0.1
# All nodes must share the same magic cookie for distributed Erlang to work.
# Uncomment the following line and append a securely generated random value.
-setcookie eh.RauybPRHzP4-pXv
# Which interfaces should the node listen on?
-kernel inet_dist_use_interface {127,0,0,1}
# Tell kernel and SASL not to log anything
-kernel error_logger silent
-sasl sasl_error_logger false
# This will toggle to true in Erlang 25+. However since we don't use global
# any longer, and have our own auto-connection module, we can keep the
# existing global behavior to avoid surprises. See
# https://github.com/erlang/otp/issues/6470#issuecomment-1337421210 for more
# information about possible increased coordination and messages being sent on
# disconnections when this setting is enabled.
#
-kernel prevent_overlapping_partitions false
# Increase the pool of dirty IO schedulers from 10 to 16
# Dirty IO schedulers are used for file IO.
+SDio 16
# Comment this line out to enable the interactive Erlang shell on startup
+Bd -noinput
# Set maximum SSL session lifetime to reap terminated replication readers
-ssl session_lifetime 300
## TLS Distribution
## Use TLS for connections between Erlang cluster members.
## http://erlang.org/doc/apps/ssl/ssl_distribution.html
##
## Generate Cert(PEM) File
## This is just an example command to generate a certfile (PEM).
## This is not an endorsement of specific expiration limits, key sizes, or algorithms.
## $ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem
## $ cat key.pem cert.pem > dev/erlserver.pem && rm key.pem cert.pem
##
## Generate a Config File (couch_ssl_dist.conf)
## [{server,
## [{certfile, "</path/to/erlserver.pem>"},
## {secure_renegotiate, true}]},
## {client,
## [{secure_renegotiate, true}]}].
##
## CouchDB recommends the following values for no_tls flag:
## 1. Use TCP only, set to true, such as:
## -couch_dist no_tls true
## 2. Use TLS only, set to false, such as:
## -couch_dist no_tls false
## 3. Specify which node to use TCP, such as:
## -couch_dist no_tls \"*@127.0.0.1\"
##
## To ensure search works, make sure to set 'no_tls' option for the clouseau node.
## By default that would be "clouseau@127.0.0.1".
## Don't forget to override the paths to point to your certificate(s) and key(s)!
##
#-proto_dist couch
#-couch_dist no_tls '"clouseau@127.0.0.1"'
#-ssl_dist_optfile <path/to/couch_ssl_dist.conf>

View File

@ -1,88 +0,0 @@
/**
* Database authentication middleware.
* Allows public databases to be accessed without auth.
* Private databases require authentication.
*/
/**
* Middleware to handle database authentication based on path.
*
* Database naming patterns:
* / - Root endpoint, always allowed (server info)
* /local-{dbname} - Should never reach server (client-only), return 404
* /public-{dbname} - No auth required, anyone can read/write
* /private-{dbname} - Auth required
* /{dbname} - Treated as private by default
*/
export function dbAuthMiddleware(req, res, next) {
// Extract the database name (first segment after /pouchdb/)
const pathParts = req.path.split('/').filter(Boolean);
const dbName = pathParts[0] || '';
// Allow root endpoint (server info check)
if (req.path === '/' || req.path === '') {
console.log(`[DB Auth] Root access: ${req.method} ${req.path}`);
return next();
}
// Local databases should never reach the server (they're browser-only)
if (dbName.startsWith('local-')) {
console.log(`[DB Auth] Local database access rejected: ${req.method} ${req.path}`);
return res.status(404).json({
error: 'not_found',
reason: 'Local databases are browser-only and do not sync to server'
});
}
// Check if this is a public database (name starts with 'public-')
const isPublic = dbName.startsWith('public-');
if (isPublic) {
// No auth required for public databases
console.log(`[DB Auth] Public access: ${req.method} ${req.path}`);
return next();
}
// For private databases, check for auth header
const auth = req.headers.authorization;
if (!auth) {
console.log(`[DB Auth] Unauthorized access attempt: ${req.method} ${req.path}`);
return res.status(401).json({
error: 'unauthorized',
reason: 'Authentication required for private databases'
});
}
// Parse Basic auth header
if (auth.startsWith('Basic ')) {
try {
const credentials = Buffer.from(auth.slice(6), 'base64').toString();
const [username, password] = credentials.split(':');
// For now, accept any credentials for private databases
// TODO: Implement proper user verification
req.dbUser = { name: username };
console.log(`[DB Auth] Authenticated: ${username} accessing ${req.path}`);
return next();
} catch (err) {
console.log(`[DB Auth] Invalid auth header: ${err.message}`);
}
}
// TODO: Add JWT/Bearer token support for Auth0 integration
if (auth.startsWith('Bearer ')) {
// For now, accept bearer tokens without verification
// TODO: Verify JWT with Auth0
req.dbUser = { name: 'bearer-user' };
console.log(`[DB Auth] Bearer token access: ${req.path}`);
return next();
}
return res.status(401).json({
error: 'unauthorized',
reason: 'Invalid authentication'
});
}
export default dbAuthMiddleware;

View File

@ -1,62 +0,0 @@
/**
* Database service using express-pouchdb for self-hosted database sync.
* Provides PouchDB HTTP API compatible with client-side PouchDB replication.
*/
import PouchDB from 'pouchdb';
import PouchDBAdapterMemory from 'pouchdb-adapter-memory';
import expressPouchdb from 'express-pouchdb';
import path from 'path';
import { fileURLToPath } from 'url';
// Register memory adapter (works in Node.js without leveldown issues)
PouchDB.plugin(PouchDBAdapterMemory);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Data directory for persistent storage (used for logs)
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '../../data');
// Use memory adapter for now - data persists while server is running
// TODO: Switch to leveldb once version conflicts are resolved
const memPouchDB = PouchDB.defaults({
adapter: 'memory'
});
// Create express-pouchdb middleware
// Using 'minimumForPouchDB' mode for lightweight operation
// Include routes needed for PouchDB replication
const pouchApp = expressPouchdb(memPouchDB, {
mode: 'minimumForPouchDB',
overrideMode: {
include: [
'routes/root', // GET / - server info
'routes/db', // PUT/GET/DELETE /:db
'routes/all-dbs', // GET /_all_dbs
'routes/changes', // GET /:db/_changes
'routes/bulk-docs', // POST /:db/_bulk_docs
'routes/bulk-get', // POST /:db/_bulk_get
'routes/all-docs', // GET /:db/_all_docs
'routes/revs-diff', // POST /:db/_revs_diff
'routes/documents' // GET/PUT/DELETE /:db/:docid
]
},
logPath: path.join(DATA_DIR, 'logs', 'pouchdb.log')
});
console.log(`[Database] Initialized express-pouchdb with data dir: ${DATA_DIR}`);
// Test that PouchDB can create databases
(async () => {
try {
const testDb = new memPouchDB('_test_db');
const info = await testDb.info();
console.log('[Database] Test DB created successfully:', info);
await testDb.destroy();
console.log('[Database] Test DB destroyed');
} catch (err) {
console.error('[Database] Failed to create test database:', err);
}
})();
export { memPouchDB as PouchDB, pouchApp };

View File

@ -1,140 +0,0 @@
/**
* AI Provider Configuration
* Manages configuration for different AI providers (Claude, Ollama, Cloudflare)
*/
// Default configuration
const DEFAULT_PROVIDER = 'claude';
// Use 127.0.0.1 instead of localhost to avoid IPv6 resolution issues
const DEFAULT_OLLAMA_URL = 'http://127.0.0.1:11434';
const DEFAULT_CLOUDFLARE_ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID || '';
/**
* Get the current AI provider
* @returns {string} Provider name ('claude' or 'ollama')
*/
export function getProvider() {
return process.env.AI_PROVIDER || DEFAULT_PROVIDER;
}
/**
* Get Ollama API URL
* @returns {string} Ollama base URL
*/
export function getOllamaUrl() {
return process.env.OLLAMA_URL || DEFAULT_OLLAMA_URL;
}
/**
* Get Anthropic API URL
* @returns {string} Anthropic base URL
*/
export function getAnthropicUrl() {
return process.env.ANTHROPIC_API_URL || 'https://api.anthropic.com';
}
/**
* Get Cloudflare Account ID
* @returns {string} Cloudflare account ID
*/
export function getCloudflareAccountId() {
return process.env.CLOUDFLARE_ACCOUNT_ID || DEFAULT_CLOUDFLARE_ACCOUNT_ID;
}
/**
* Get Cloudflare API Token
* @returns {string} Cloudflare API token
*/
export function getCloudflareApiToken() {
return process.env.CLOUDFLARE_API_TOKEN || '';
}
/**
* Get Cloudflare Workers AI base URL
* @returns {string} Cloudflare Workers AI base URL
*/
export function getCloudflareUrl() {
const accountId = getCloudflareAccountId();
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run`;
}
/**
* Get provider configuration for a specific provider
* @param {string} provider - Provider name
* @returns {object} Provider configuration
*/
export function getProviderConfig(provider) {
switch (provider) {
case 'ollama':
return {
name: 'ollama',
baseUrl: getOllamaUrl(),
chatEndpoint: '/api/chat',
requiresAuth: false
};
case 'cloudflare':
return {
name: 'cloudflare',
baseUrl: getCloudflareUrl(),
chatEndpoint: '', // Model is appended to baseUrl
requiresAuth: true,
apiKey: getCloudflareApiToken(),
accountId: getCloudflareAccountId()
};
case 'claude':
default:
return {
name: 'claude',
baseUrl: getAnthropicUrl(),
chatEndpoint: '/v1/messages',
requiresAuth: true,
apiKey: process.env.ANTHROPIC_API_KEY
};
}
}
/**
* Determine provider from model ID
* @param {string} modelId - Model identifier
* @returns {string} Provider name
*/
export function getProviderFromModel(modelId) {
if (!modelId) return getProvider();
// Claude models start with 'claude-'
if (modelId.startsWith('claude-')) {
return 'claude';
}
// Cloudflare models start with '@cf/' or '@hf/'
if (modelId.startsWith('@cf/') || modelId.startsWith('@hf/')) {
return 'cloudflare';
}
// Known Ollama models
const ollamaModels = [
'llama', 'mistral', 'qwen', 'codellama', 'phi',
'gemma', 'neural-chat', 'starling', 'orca', 'vicuna',
'deepseek', 'dolphin', 'nous-hermes', 'openhermes'
];
for (const prefix of ollamaModels) {
if (modelId.toLowerCase().startsWith(prefix)) {
return 'ollama';
}
}
// Default to configured provider
return getProvider();
}
export default {
getProvider,
getOllamaUrl,
getAnthropicUrl,
getCloudflareAccountId,
getCloudflareApiToken,
getCloudflareUrl,
getProviderConfig,
getProviderFromModel
};

View File

@ -1,158 +0,0 @@
/**
* In-memory session store for diagram chat sessions.
* Stores conversation history and entity snapshots.
*/
import { v4 as uuidv4 } from 'uuid';
// Session structure:
// {
// id: string,
// diagramId: string,
// conversationHistory: Array<{role, content, toolResults?, timestamp}>,
// entities: Array<{id, template, text, color, position}>,
// createdAt: Date,
// lastAccess: Date
// }
const sessions = new Map();
// Session timeout (1 hour of inactivity)
const SESSION_TIMEOUT_MS = 60 * 60 * 1000;
/**
* Create a new session for a diagram
*/
export function createSession(diagramId) {
const id = uuidv4();
const session = {
id,
diagramId,
conversationHistory: [],
entities: [],
createdAt: new Date(),
lastAccess: new Date()
};
sessions.set(id, session);
return session;
}
/**
* Get a session by ID
*/
export function getSession(sessionId) {
const session = sessions.get(sessionId);
if (session) {
session.lastAccess = new Date();
}
return session || null;
}
/**
* Find existing session for a diagram
*/
export function findSessionByDiagram(diagramId) {
for (const [, session] of sessions) {
if (session.diagramId === diagramId) {
session.lastAccess = new Date();
return session;
}
}
return null;
}
/**
* Update entities snapshot for a session
*/
export function syncEntities(sessionId, entities) {
const session = sessions.get(sessionId);
if (!session) return null;
session.entities = entities;
session.lastAccess = new Date();
return session;
}
/**
* Add a message to conversation history
*/
export function addMessage(sessionId, message) {
const session = sessions.get(sessionId);
if (!session) return null;
session.conversationHistory.push({
...message,
timestamp: new Date()
});
session.lastAccess = new Date();
return session;
}
/**
* Get conversation history for API calls (formatted for Claude)
*/
export function getConversationForAPI(sessionId) {
const session = sessions.get(sessionId);
if (!session) return [];
// Convert to Claude message format
return session.conversationHistory.map(msg => ({
role: msg.role,
content: msg.content
}));
}
/**
* Clear conversation history but keep session
*/
export function clearHistory(sessionId) {
const session = sessions.get(sessionId);
if (!session) return null;
session.conversationHistory = [];
session.lastAccess = new Date();
return session;
}
/**
* Delete a session
*/
export function deleteSession(sessionId) {
return sessions.delete(sessionId);
}
/**
* Clean up expired sessions
*/
export function cleanupExpiredSessions() {
const now = Date.now();
for (const [id, session] of sessions) {
if (now - session.lastAccess.getTime() > SESSION_TIMEOUT_MS) {
sessions.delete(id);
}
}
}
// Run cleanup every 15 minutes
setInterval(cleanupExpiredSessions, 15 * 60 * 1000);
/**
* Get session stats (for debugging)
*/
export function getStats(includeDetails = false) {
return {
activeSessions: sessions.size,
sessions: Array.from(sessions.values()).map(s => ({
id: s.id,
diagramId: s.diagramId,
messageCount: s.conversationHistory.length,
entityCount: s.entities.length,
lastAccess: s.lastAccess,
// Include full details if requested
...(includeDetails && {
entities: s.entities,
conversationHistory: s.conversationHistory
})
}))
};
}

View File

@ -1,663 +0,0 @@
/**
* Tool Format Converter
* Converts between Claude and Ollama tool/function formats
*/
/**
* Convert Claude tool definition to Ollama function format
*
* Claude format:
* { name: "...", description: "...", input_schema: { type: "object", properties: {...} } }
*
* Ollama format:
* { type: "function", function: { name: "...", description: "...", parameters: {...} } }
*
* @param {object} claudeTool - Tool in Claude format
* @returns {object} Tool in Ollama format
*/
export function claudeToolToOllama(claudeTool) {
return {
type: "function",
function: {
name: claudeTool.name,
description: claudeTool.description,
parameters: claudeTool.input_schema
}
};
}
/**
* Convert array of Claude tools to Ollama format
* @param {Array} claudeTools - Array of Claude tool definitions
* @returns {Array} Array of Ollama function definitions
*/
export function claudeToolsToOllama(claudeTools) {
if (!claudeTools || !Array.isArray(claudeTools)) {
return [];
}
return claudeTools.map(claudeToolToOllama);
}
/**
* Convert Ollama tool call to Claude format
*
* Ollama format (in message):
* { tool_calls: [{ function: { name: "...", arguments: {...} } }] }
*
* Claude format:
* { type: "tool_use", id: "...", name: "...", input: {...} }
*
* @param {object} ollamaToolCall - Tool call from Ollama response
* @param {number} index - Index for generating unique ID
* @returns {object} Tool call in Claude format
*/
export function ollamaToolCallToClaude(ollamaToolCall, index = 0) {
const func = ollamaToolCall.function;
// Parse arguments if it's a string
let input = func.arguments;
if (typeof input === 'string') {
try {
input = JSON.parse(input);
} catch (e) {
console.warn('[ToolConverter] Failed to parse tool arguments:', e);
input = {};
}
}
return {
type: "tool_use",
id: `toolu_ollama_${Date.now()}_${index}`,
name: func.name,
input: input || {}
};
}
/**
* Convert Claude tool result to Ollama format
*
* Claude format (in messages):
* { role: "user", content: [{ type: "tool_result", tool_use_id: "...", content: "..." }] }
*
* Ollama format:
* { role: "tool", content: "...", name: "..." }
*
* @param {object} claudeToolResult - Tool result in Claude format
* @param {string} toolName - Name of the tool (from previous tool_use)
* @returns {object} Tool result in Ollama message format
*/
export function claudeToolResultToOllama(claudeToolResult, toolName) {
let content = claudeToolResult.content;
// Stringify if it's an object
if (typeof content === 'object') {
content = JSON.stringify(content);
}
return {
role: "tool",
content: content,
name: toolName
};
}
/**
* Convert Claude messages array to Ollama format
* Handles regular messages and tool result messages
*
* @param {Array} claudeMessages - Messages in Claude format
* @param {string} systemPrompt - System prompt to prepend
* @returns {Array} Messages in Ollama format
*/
export function claudeMessagesToOllama(claudeMessages, systemPrompt) {
const ollamaMessages = [];
// Add system message if provided
if (systemPrompt) {
ollamaMessages.push({
role: "system",
content: systemPrompt
});
}
// Track tool names for tool results
const toolNameMap = new Map();
for (const msg of claudeMessages) {
if (msg.role === 'user') {
// Check if it's a tool result message
if (Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === 'tool_result') {
const toolName = toolNameMap.get(block.tool_use_id) || 'unknown';
ollamaMessages.push(claudeToolResultToOllama(block, toolName));
} else if (block.type === 'text') {
ollamaMessages.push({
role: "user",
content: block.text
});
}
}
} else {
ollamaMessages.push({
role: "user",
content: msg.content
});
}
} else if (msg.role === 'assistant') {
// Handle assistant messages with potential tool calls
if (Array.isArray(msg.content)) {
let textContent = '';
const toolCalls = [];
for (const block of msg.content) {
if (block.type === 'text') {
textContent += block.text;
} else if (block.type === 'tool_use') {
// Track tool name for later tool results
toolNameMap.set(block.id, block.name);
toolCalls.push({
function: {
name: block.name,
// Ollama expects arguments as object, not string
arguments: block.input || {}
}
});
}
}
const assistantMsg = {
role: "assistant",
content: textContent || ""
};
if (toolCalls.length > 0) {
assistantMsg.tool_calls = toolCalls;
}
ollamaMessages.push(assistantMsg);
} else {
ollamaMessages.push({
role: "assistant",
content: msg.content
});
}
}
}
return ollamaMessages;
}
/**
* Convert Ollama response to Claude format
*
* @param {object} ollamaResponse - Response from Ollama API
* @returns {object} Response in Claude format
*/
export function ollamaResponseToClaude(ollamaResponse) {
const content = [];
const message = ollamaResponse.message;
// Add text content if present
if (message.content) {
content.push({
type: "text",
text: message.content
});
}
// Add tool calls if present
if (message.tool_calls && message.tool_calls.length > 0) {
for (let i = 0; i < message.tool_calls.length; i++) {
content.push(ollamaToolCallToClaude(message.tool_calls[i], i));
}
}
// Determine stop reason
let stopReason = "end_turn";
if (message.tool_calls && message.tool_calls.length > 0) {
stopReason = "tool_use";
} else if (ollamaResponse.done_reason === "length") {
stopReason = "max_tokens";
}
return {
id: `msg_ollama_${Date.now()}`,
type: "message",
role: "assistant",
content: content,
model: ollamaResponse.model,
stop_reason: stopReason,
usage: {
input_tokens: ollamaResponse.prompt_eval_count || 0,
output_tokens: ollamaResponse.eval_count || 0
}
};
}
// ============================================
// Cloudflare Workers AI Converters
// ============================================
/**
* Convert Claude tool definition to Cloudflare format
* Cloudflare uses OpenAI-compatible format
*
* Claude format:
* { name: "...", description: "...", input_schema: { type: "object", properties: {...} } }
*
* Cloudflare format:
* { type: "function", function: { name: "...", description: "...", parameters: {...} } }
*
* @param {object} claudeTool - Tool in Claude format
* @returns {object} Tool in Cloudflare format
*/
export function claudeToolToCloudflare(claudeTool) {
return {
type: "function",
function: {
name: claudeTool.name,
description: claudeTool.description,
parameters: claudeTool.input_schema
}
};
}
/**
* Convert array of Claude tools to Cloudflare format
* @param {Array} claudeTools - Array of Claude tool definitions
* @returns {Array} Array of Cloudflare function definitions
*/
export function claudeToolsToCloudflare(claudeTools) {
if (!claudeTools || !Array.isArray(claudeTools)) {
return [];
}
return claudeTools.map(claudeToolToCloudflare);
}
/**
* Convert Cloudflare tool call to Claude format
*
* Cloudflare format:
* { name: "...", arguments: {...} }
*
* Claude format:
* { type: "tool_use", id: "...", name: "...", input: {...} }
*
* @param {object} cfToolCall - Tool call from Cloudflare response
* @param {number} index - Index for generating unique ID
* @returns {object} Tool call in Claude format
*/
export function cloudflareToolCallToClaude(cfToolCall, index = 0) {
// Parse arguments if it's a string
let input = cfToolCall.arguments;
if (typeof input === 'string') {
try {
input = JSON.parse(input);
} catch (e) {
console.warn('[ToolConverter] Failed to parse Cloudflare tool arguments:', e);
input = {};
}
}
return {
type: "tool_use",
id: `toolu_cf_${Date.now()}_${index}`,
name: cfToolCall.name,
input: input || {}
};
}
/**
* Convert Claude messages array to Cloudflare format
* Cloudflare uses OpenAI-compatible message format
*
* IMPORTANT: Cloudflare Workers AI does NOT support multi-turn tool conversations.
* It crashes with error 3043 when conversation history contains tool_calls or tool results.
* We must strip tool call history and only keep text content from past messages.
*
* @param {Array} claudeMessages - Messages in Claude format
* @param {string} systemPrompt - System prompt to prepend
* @returns {Array} Messages in Cloudflare format
*/
export function claudeMessagesToCloudflare(claudeMessages, systemPrompt) {
const cfMessages = [];
// Add system message if provided
if (systemPrompt) {
cfMessages.push({
role: "system",
content: systemPrompt
});
}
// Cloudflare doesn't support tool call history in native format - convert to text
// so the model knows what tools were called and their results
for (const msg of claudeMessages) {
if (msg.role === 'user') {
if (Array.isArray(msg.content)) {
// Convert tool_result blocks to text summaries
const textParts = [];
for (const block of msg.content) {
if (block.type === 'text') {
textParts.push(block.text);
} else if (block.type === 'tool_result') {
// Convert tool result to readable text so model knows it was executed
textParts.push(`[Tool Result: ${block.content}]`);
}
}
if (textParts.length > 0) {
cfMessages.push({
role: "user",
content: textParts.join('\n')
});
}
} else {
cfMessages.push({
role: "user",
content: msg.content
});
}
} else if (msg.role === 'assistant') {
// For assistant messages, convert tool_use to text descriptions
const textParts = [];
if (Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === 'text') {
textParts.push(block.text);
} else if (block.type === 'tool_use') {
// Convert tool call to readable text so model knows it called this
const argsStr = JSON.stringify(block.input || {});
textParts.push(`[Called tool: ${block.name}(${argsStr})]`);
}
}
} else {
textParts.push(msg.content || '');
}
// Also handle pre-converted messages that might have tool_calls property
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
for (const tc of msg.tool_calls) {
const name = tc.function?.name || tc.name || 'unknown';
const args = tc.function?.arguments || tc.arguments || '{}';
textParts.push(`[Called tool: ${name}(${typeof args === 'string' ? args : JSON.stringify(args)})]`);
}
}
const textContent = textParts.filter(t => t).join('\n');
if (textContent) {
cfMessages.push({
role: "assistant",
content: textContent
});
}
} else if (msg.role === 'tool') {
// Convert tool messages to user messages with result text
cfMessages.push({
role: "user",
content: `[Tool Result (${msg.name || 'unknown'}): ${msg.content}]`
});
}
}
return cfMessages;
}
/**
* Try to repair and parse a potentially truncated JSON object
* @param {string} jsonStr - Potentially incomplete JSON string
* @returns {object|null} - Parsed object or null if unparseable
*/
function tryRepairAndParse(jsonStr) {
// First try as-is
try {
return JSON.parse(jsonStr);
} catch (e) {
// Try adding closing brackets
const repairs = [
jsonStr + '}',
jsonStr + '"}',
jsonStr + '}}',
jsonStr + '"}}',
jsonStr + ': null}}',
jsonStr + '": null}}'
];
for (const attempt of repairs) {
try {
const parsed = JSON.parse(attempt);
if (parsed.name) { // Must have a name to be valid
return parsed;
}
} catch (e2) {
// Continue trying
}
}
return null;
}
}
/**
* Parse tool calls from text response
* Handles multiple formats:
* 1. Mistral native: [TOOL_CALLS][{"name": "...", "arguments": {...}}, ...]
* 2. History format: [Called tool: name({args})]
*
* This parser is resilient to truncation - it will extract as many valid tool calls
* as possible even if the JSON is incomplete.
*
* @param {string} text - Text response that may contain embedded tool calls
* @returns {object} - { cleanText: string, toolCalls: array }
*/
function parseTextToolCalls(text) {
if (!text) return { cleanText: '', toolCalls: [] };
const toolCalls = [];
let cleanText = text;
// Format 1: [TOOL_CALLS][...] (Mistral native format)
const toolCallMatch = text.match(/\[TOOL_CALLS\]\s*(\[[\s\S]*)/);
if (toolCallMatch) {
const toolCallsJson = toolCallMatch[1];
// First try normal JSON.parse (for complete responses)
try {
const parsedCalls = JSON.parse(toolCallsJson);
if (Array.isArray(parsedCalls)) {
const validCalls = parsedCalls
.filter(call => call && call.name)
.map(call => ({
name: call.name,
arguments: call.arguments || {}
}));
console.log(`[ToolConverter] Parsed ${validCalls.length} tool calls from [TOOL_CALLS] JSON`);
cleanText = text.replace(/\[TOOL_CALLS\]\s*\[[\s\S]*/, '').trim();
return { cleanText, toolCalls: validCalls };
}
} catch (e) {
console.log('[ToolConverter] [TOOL_CALLS] JSON incomplete, attempting to extract individual tool calls...');
}
// JSON is truncated - extract individual tool calls using regex
const toolCallStarts = [];
const startPattern = /\{"name"\s*:\s*"/g;
let match;
while ((match = startPattern.exec(toolCallsJson)) !== null) {
toolCallStarts.push(match.index);
}
console.log(`[ToolConverter] Found ${toolCallStarts.length} potential tool call starts in [TOOL_CALLS]`);
for (let i = 0; i < toolCallStarts.length; i++) {
const start = toolCallStarts[i];
const end = toolCallStarts[i + 1] || toolCallsJson.length;
let segment = toolCallsJson.substring(start, end).replace(/,\s*$/, '');
const parsed = tryRepairAndParse(segment);
if (parsed && parsed.name) {
toolCalls.push({
name: parsed.name,
arguments: parsed.arguments || {}
});
console.log(`[ToolConverter] Extracted tool call from [TOOL_CALLS]: ${parsed.name}`);
}
}
cleanText = text.replace(/\[TOOL_CALLS\]\s*\[[\s\S]*/, '').trim();
if (toolCalls.length > 0) {
console.log(`[ToolConverter] Extracted ${toolCalls.length} tool calls from [TOOL_CALLS] format`);
return { cleanText, toolCalls };
}
}
// Format 2: [Called tool: name({args})] (history format the model might mimic)
// Match patterns like: [Called tool: create_entity({"shape": "box", ...})]
const calledToolPattern = /\[Called tool:\s*(\w+)\((\{[\s\S]*?\})\)\]/g;
let calledMatch;
const calledToolMatches = [];
while ((calledMatch = calledToolPattern.exec(text)) !== null) {
calledToolMatches.push({
fullMatch: calledMatch[0],
name: calledMatch[1],
argsStr: calledMatch[2]
});
}
if (calledToolMatches.length > 0) {
console.log(`[ToolConverter] Found ${calledToolMatches.length} [Called tool:] format tool calls`);
for (const match of calledToolMatches) {
try {
const args = JSON.parse(match.argsStr);
toolCalls.push({
name: match.name,
arguments: args
});
console.log(`[ToolConverter] Extracted tool call from [Called tool:]: ${match.name}`);
// Remove this match from clean text
cleanText = cleanText.replace(match.fullMatch, '');
} catch (e) {
console.warn(`[ToolConverter] Failed to parse [Called tool:] args for ${match.name}:`, e.message);
// Try to repair the JSON
const repaired = tryRepairAndParse(match.argsStr);
if (repaired) {
toolCalls.push({
name: match.name,
arguments: repaired
});
console.log(`[ToolConverter] Repaired and extracted tool call: ${match.name}`);
cleanText = cleanText.replace(match.fullMatch, '');
}
}
}
cleanText = cleanText.trim();
if (toolCalls.length > 0) {
console.log(`[ToolConverter] Extracted ${toolCalls.length} tool calls from [Called tool:] format`);
return { cleanText, toolCalls };
}
}
// No tool calls found
return { cleanText: text, toolCalls: [] };
}
/**
* Convert Cloudflare response to Claude format
*
* Cloudflare response format:
* {
* result: {
* response: "text output",
* tool_calls: [{ name: "...", arguments: {...} }]
* },
* success: true
* }
*
* Note: Some models (like Mistral) output tool calls as text in format:
* [TOOL_CALLS][{...}]
*
* @param {object} cfResponse - Response from Cloudflare Workers AI API
* @param {string} model - Model name used
* @returns {object} Response in Claude format
*/
export function cloudflareResponseToClaude(cfResponse, model) {
const content = [];
const result = cfResponse.result || cfResponse;
// Get tool calls from proper field or parse from text
let toolCalls = result.tool_calls || [];
let textResponse = result.response || '';
// Log raw response for debugging
console.log(`[ToolConverter] Raw response (first 500 chars): ${textResponse.substring(0, 500)}`);
console.log(`[ToolConverter] Native tool_calls present: ${toolCalls.length}`);
// Check if tool calls are embedded in text response (Mistral format or history format)
if (toolCalls.length === 0 && textResponse) {
console.log(`[ToolConverter] No native tool_calls, parsing text response...`);
const parsed = parseTextToolCalls(textResponse);
console.log(`[ToolConverter] Parsed ${parsed.toolCalls.length} tool calls from text`);
if (parsed.toolCalls.length > 0) {
toolCalls = parsed.toolCalls;
textResponse = parsed.cleanText;
}
}
// Add text content if present (after removing tool calls)
if (textResponse) {
content.push({
type: "text",
text: textResponse
});
}
// Add tool calls if present
if (toolCalls.length > 0) {
for (let i = 0; i < toolCalls.length; i++) {
content.push(cloudflareToolCallToClaude(toolCalls[i], i));
}
}
// Determine stop reason
let stopReason = "end_turn";
if (toolCalls.length > 0) {
stopReason = "tool_use";
}
// Extract usage if available
const usage = {
input_tokens: result.usage?.prompt_tokens || result.usage?.input_tokens || 0,
output_tokens: result.usage?.completion_tokens || result.usage?.output_tokens || 0
};
return {
id: `msg_cf_${Date.now()}`,
type: "message",
role: "assistant",
content: content,
model: model,
stop_reason: stopReason,
usage: usage
};
}
export default {
claudeToolToOllama,
claudeToolsToOllama,
ollamaToolCallToClaude,
claudeToolResultToOllama,
claudeMessagesToOllama,
ollamaResponseToClaude,
// Cloudflare converters
claudeToolToCloudflare,
claudeToolsToCloudflare,
cloudflareToolCallToClaude,
claudeMessagesToCloudflare,
cloudflareResponseToClaude
};

View File

@ -1,241 +0,0 @@
/**
* Token usage tracking and cost estimation service
*/
// Pricing per million tokens (as of Dec 2025)
const MODEL_PRICING = {
// Claude 4.5 models
"claude-opus-4-5-20251101": { input: 5.00, output: 25.00 },
"claude-sonnet-4-5-20250929": { input: 3.00, output: 15.00 },
"claude-haiku-4-5-20251001": { input: 1.00, output: 5.00 },
// Claude 4 models
"claude-opus-4-1-20250805": { input: 15.00, output: 75.00 },
"claude-sonnet-4-20250514": { input: 3.00, output: 15.00 },
// Claude 3.7/3.5 models
"claude-3-7-sonnet-20250219": { input: 3.00, output: 15.00 },
"claude-3-5-sonnet-20241022": { input: 3.00, output: 15.00 },
"claude-3-5-haiku-20241022": { input: 0.80, output: 4.00 },
// Claude 3 models
"claude-3-opus-20240229": { input: 15.00, output: 75.00 },
"claude-3-sonnet-20240229": { input: 3.00, output: 15.00 },
"claude-3-haiku-20240307": { input: 0.25, output: 1.25 },
// Cloudflare Workers AI models (approximate - based on neuron costs)
"@cf/mistralai/mistral-small-3.1-24b-instruct": { input: 0.30, output: 0.30 },
"@hf/nousresearch/hermes-2-pro-mistral-7b": { input: 0.10, output: 0.10 },
"@cf/meta/llama-3.3-70b-instruct-fp8-fast": { input: 0.20, output: 0.20 },
"@cf/meta/llama-3.1-70b-instruct": { input: 0.20, output: 0.20 },
"@cf/meta/llama-3.1-8b-instruct": { input: 0.05, output: 0.05 },
"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b": { input: 0.15, output: 0.15 },
"@cf/qwen/qwen2.5-coder-32b-instruct": { input: 0.15, output: 0.15 },
};
// Cache pricing multipliers
const CACHE_WRITE_MULTIPLIER = 1.25; // 25% more expensive to write cache
const CACHE_READ_MULTIPLIER = 0.10; // 90% cheaper to read from cache
// In-memory storage for usage tracking
const sessionUsage = new Map();
const globalUsage = {
totalInputTokens: 0,
totalOutputTokens: 0,
totalCacheCreationTokens: 0,
totalCacheReadTokens: 0,
totalCost: 0,
requestCount: 0,
byModel: {},
startTime: Date.now()
};
/**
* Get pricing for a model, with fallback to sonnet pricing
*/
function getModelPricing(model) {
// Try exact match first
if (MODEL_PRICING[model]) {
return MODEL_PRICING[model];
}
// Try to match by model family
if (model.includes("opus")) {
return MODEL_PRICING["claude-opus-4-5-20251101"];
}
if (model.includes("haiku")) {
return MODEL_PRICING["claude-haiku-4-5-20251001"];
}
// Cloudflare models - default to cheap pricing
if (model.startsWith("@cf/") || model.startsWith("@hf/")) {
return { input: 0.10, output: 0.10 };
}
// Default to sonnet pricing
return MODEL_PRICING["claude-sonnet-4-5-20250929"];
}
/**
* Calculate cost for a request
*/
function calculateCost(model, usage) {
const pricing = getModelPricing(model);
const perMillionDivisor = 1_000_000;
let cost = 0;
// Standard input tokens
const standardInputTokens = (usage.input_tokens || 0) - (usage.cache_read_input_tokens || 0);
cost += (standardInputTokens / perMillionDivisor) * pricing.input;
// Cache read tokens (90% cheaper)
if (usage.cache_read_input_tokens) {
cost += (usage.cache_read_input_tokens / perMillionDivisor) * pricing.input * CACHE_READ_MULTIPLIER;
}
// Cache creation tokens (25% more expensive)
if (usage.cache_creation_input_tokens) {
cost += (usage.cache_creation_input_tokens / perMillionDivisor) * pricing.input * CACHE_WRITE_MULTIPLIER;
}
// Output tokens
cost += ((usage.output_tokens || 0) / perMillionDivisor) * pricing.output;
return cost;
}
/**
* Track usage for a request
* @param {string} sessionId - Session identifier
* @param {string} model - Model used
* @param {object} usage - Token usage from API response
* @param {object} content - Optional input/output content for detailed tracking
* @param {string} content.inputText - User input text
* @param {string} content.outputText - Assistant output text
* @param {array} content.toolCalls - Tool calls made
*/
export function trackUsage(sessionId, model, usage, content = {}) {
if (!usage) return null;
const cost = calculateCost(model, usage);
// Truncate text for storage (keep first 500 chars)
const truncate = (text, maxLen = 500) => {
if (!text) return null;
return text.length > maxLen ? text.substring(0, maxLen) + '...' : text;
};
const usageRecord = {
timestamp: Date.now(),
model,
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
cacheCreationTokens: usage.cache_creation_input_tokens || 0,
cacheReadTokens: usage.cache_read_input_tokens || 0,
cost,
inputText: truncate(content.inputText),
outputText: truncate(content.outputText),
toolCalls: content.toolCalls || []
};
// Update session usage
if (sessionId) {
if (!sessionUsage.has(sessionId)) {
sessionUsage.set(sessionId, {
totalInputTokens: 0,
totalOutputTokens: 0,
totalCacheCreationTokens: 0,
totalCacheReadTokens: 0,
totalCost: 0,
requestCount: 0,
requests: [],
startTime: Date.now()
});
}
const session = sessionUsage.get(sessionId);
session.totalInputTokens += usageRecord.inputTokens;
session.totalOutputTokens += usageRecord.outputTokens;
session.totalCacheCreationTokens += usageRecord.cacheCreationTokens;
session.totalCacheReadTokens += usageRecord.cacheReadTokens;
session.totalCost += cost;
session.requestCount += 1;
session.requests.push(usageRecord);
// Keep only last 100 requests per session to limit memory
if (session.requests.length > 100) {
session.requests.shift();
}
}
// Update global usage
globalUsage.totalInputTokens += usageRecord.inputTokens;
globalUsage.totalOutputTokens += usageRecord.outputTokens;
globalUsage.totalCacheCreationTokens += usageRecord.cacheCreationTokens;
globalUsage.totalCacheReadTokens += usageRecord.cacheReadTokens;
globalUsage.totalCost += cost;
globalUsage.requestCount += 1;
// Track by model
if (!globalUsage.byModel[model]) {
globalUsage.byModel[model] = {
inputTokens: 0,
outputTokens: 0,
cost: 0,
requestCount: 0
};
}
globalUsage.byModel[model].inputTokens += usageRecord.inputTokens;
globalUsage.byModel[model].outputTokens += usageRecord.outputTokens;
globalUsage.byModel[model].cost += cost;
globalUsage.byModel[model].requestCount += 1;
return usageRecord;
}
/**
* Get usage for a session
*/
export function getSessionUsage(sessionId) {
return sessionUsage.get(sessionId) || null;
}
/**
* Get global usage stats
*/
export function getGlobalUsage() {
return {
...globalUsage,
uptime: Date.now() - globalUsage.startTime
};
}
/**
* Format cost as currency string
*/
export function formatCost(cost) {
return `$${cost.toFixed(6)}`;
}
/**
* Clear session usage (call when session ends)
*/
export function clearSessionUsage(sessionId) {
sessionUsage.delete(sessionId);
}
/**
* Get a formatted usage summary for logging
*/
export function getUsageSummary(usageRecord) {
if (!usageRecord) return "No usage data";
return [
`Input: ${usageRecord.inputTokens}`,
`Output: ${usageRecord.outputTokens}`,
usageRecord.cacheReadTokens ? `Cache read: ${usageRecord.cacheReadTokens}` : null,
usageRecord.cacheCreationTokens ? `Cache write: ${usageRecord.cacheCreationTokens}` : null,
`Cost: ${formatCost(usageRecord.cost)}`
].filter(Boolean).join(" | ");
}

View File

@ -1,144 +0,0 @@
/**
* Copy and messaging for upgrade paths and guest limitations
*/
export interface UpgradeBenefit {
title: string;
description: string;
icon?: string;
}
export const GUEST_LIMITATIONS = {
diagrams: {
limit: 3,
message: 'Guest mode is limited to 3 diagrams',
upgradeMessage: 'Sign up for unlimited diagrams',
},
storage: {
limit: 50, // MB
message: 'Guest mode uses 50MB local storage',
upgradeMessage: 'Get cloud storage with sync',
},
collaboration: {
message: 'Collaboration features are disabled in guest mode',
upgradeMessage: 'Sign up to collaborate with your team',
},
sync: {
message: 'Changes are stored locally only',
upgradeMessage: 'Sign up to sync across all your devices',
},
templates: {
message: 'Templates are not available in guest mode',
upgradeMessage: 'Sign up to access our template library',
},
};
export const UPGRADE_BENEFITS: UpgradeBenefit[] = [
{
title: 'Unlimited Diagrams',
description: 'Create as many diagrams as you need without limits',
},
{
title: 'Cloud Sync',
description: 'Access your work from desktop, VR headset, and any device',
},
{
title: 'Real-Time Collaboration',
description: 'Work together with your team in the same 3D space',
},
{
title: 'Template Library',
description: 'Jump-start your projects with pre-built templates',
},
{
title: 'Secure Cloud Storage',
description: 'Your diagrams safely backed up and encrypted',
},
{
title: 'Priority Support',
description: 'Get help when you need it from our support team',
},
];
export const GUEST_MODE_BANNER = {
title: 'You\'re in Guest Mode',
message: 'Your diagrams are saved locally. Sign up to sync across devices and collaborate with teams.',
ctaText: 'Sign Up Free',
};
export const UPGRADE_CTA = {
hero: {
title: 'Ready to unlock the full experience?',
subtitle: 'Sign up free to sync across devices, collaborate with teams, and create unlimited diagrams.',
primaryCta: 'Sign Up Free',
secondaryCta: 'Learn More',
},
inline: {
title: 'Want more?',
message: 'Sign up to unlock unlimited diagrams, cloud sync, and collaboration.',
ctaText: 'Sign Up',
},
limit: {
diagrams: {
title: 'Diagram Limit Reached',
message: 'You\'ve created 3 diagrams (guest limit). Sign up to create unlimited diagrams.',
ctaText: 'Upgrade Now',
},
},
};
/**
* Get the appropriate upgrade message based on context
*/
export function getUpgradeMessage(context: 'diagram-limit' | 'collaboration' | 'sync' | 'template'): {
title: string;
message: string;
benefits: string[];
} {
switch (context) {
case 'diagram-limit':
return {
title: 'Unlock Unlimited Diagrams',
message: 'Guest mode is limited to 3 diagrams. Sign up to create as many as you need.',
benefits: [
'Create unlimited diagrams',
'Cloud storage and backup',
'Access from any device',
'Real-time collaboration',
],
};
case 'collaboration':
return {
title: 'Collaborate in Real-Time',
message: 'Work together with your team in shared 3D space.',
benefits: [
'Invite unlimited collaborators',
'See changes in real-time',
'Meet as avatars in VR',
'Audit trail of all changes',
],
};
case 'sync':
return {
title: 'Sync Across All Devices',
message: 'Access your diagrams from desktop, VR, and mobile.',
benefits: [
'Cloud sync across devices',
'Work on Quest and desktop',
'Automatic backups',
'Secure cloud storage',
],
};
case 'template':
return {
title: 'Get Started Faster',
message: 'Access pre-built templates for common use cases.',
benefits: [
'Professional templates',
'Org charts and workflows',
'Architecture diagrams',
'Customizable examples',
],
};
}
}

View File

@ -7,40 +7,36 @@ import {
WebXRInputSource WebXRInputSource
} from "@babylonjs/core"; } from "@babylonjs/core";
import {DiagramManager} from "../diagram/diagramManager"; import {DiagramManager} from "../diagram/diagramManager";
import {DiagramEvent, DiagramEventType} from "../diagram/types/diagramEntity";
import log from "loglevel"; import log from "loglevel";
import {ControllerEventType, Controllers} from "./controllers";
import {grabAndClone} from "./functions/grabAndClone"; import {grabAndClone} from "./functions/grabAndClone";
import {ClickMenu} from "../menus/clickMenu"; import {ClickMenu} from "../menus/clickMenu";
import {motionControllerInitObserver} from "./functions/motionControllerInitObserver"; import {motionControllerObserver} from "./functions/motionControllerObserver";
import {DefaultScene} from "../defaultScene"; import {DefaultScene} from "../defaultScene";
import {DiagramEventObserverMask} from "../diagram/types/diagramEventObserverMask";
import {DiagramObject} from "../diagram/diagramObject"; import {DiagramObject} from "../diagram/diagramObject";
import {snapAll} from "./functions/snapAll";
import {MeshTypeEnum} from "../diagram/types/meshTypeEnum"; import {MeshTypeEnum} from "../diagram/types/meshTypeEnum";
import {getMeshType} from "./functions/getMeshType"; import {getMeshType} from "./functions/getMeshType";
import {viewOnly} from "../util/functions/getPath"; import {viewOnly} from "../util/functions/getPath";
import {ControllerEventType} from "./types/controllerEventType";
import {controllerObservable} from "./controllers";
import {grabMesh} from "../diagram/functions/grabMesh";
import {dropMesh} from "../diagram/functions/dropMesh";
const CLICK_TIME = 300; const CLICK_TIME = 300;
export abstract class AbstractController { export class Base {
static stickVector = Vector3.Zero(); static stickVector = Vector3.Zero();
protected readonly scene: Scene; protected readonly scene: Scene;
protected readonly xr: WebXRDefaultExperience; protected readonly xr: WebXRDefaultExperience;
protected readonly diagramManager: DiagramManager; protected readonly diagramManager: DiagramManager;
protected xrInputSource: WebXRInputSource; protected xrInputSource: WebXRInputSource;
protected speedFactor = 4; protected speedFactor = 4;
protected grabbedObject: DiagramObject = null; protected grabbedObject: DiagramObject = null;
protected grabbedMesh: AbstractMesh = null; protected grabbedMesh: AbstractMesh = null;
protected grabbedMeshType: MeshTypeEnum = null; protected grabbedMeshType: MeshTypeEnum = null;
protected controllers: Controllers;
private readonly _logger = log.getLogger('Base');
private readonly _logger = log.getLogger('AbstractController');
private _clickStart: number = 0; private _clickStart: number = 0;
private _clickMenu: ClickMenu; private _clickMenu: ClickMenu;
private _pickPoint: Vector3 = new Vector3(); private _pickPoint: Vector3 = new Vector3();
@ -52,6 +48,7 @@ export abstract class AbstractController {
diagramManager: DiagramManager) { diagramManager: DiagramManager) {
this._logger.debug('Base Controller Constructor called'); this._logger.debug('Base Controller Constructor called');
this.xrInputSource = controller; this.xrInputSource = controller;
this.controllers = diagramManager.controllers;
this.scene = DefaultScene.Scene; this.scene = DefaultScene.Scene;
this.xr = xr; this.xr = xr;
@ -68,8 +65,8 @@ export abstract class AbstractController {
this.diagramManager = diagramManager; this.diagramManager = diagramManager;
//@TODO THis works, but it uses initGrip, not sure if this is the best idea //@TODO THis works, but it uses initGrip, not sure if this is the best idea
this.xrInputSource.onMotionControllerInitObservable.add(motionControllerInitObserver, -1, false, this); this.xrInputSource.onMotionControllerInitObservable.add(motionControllerObserver, -1, false, this);
controllerObservable.add((event) => { this.controllers.controllerObservable.add((event) => {
this._logger.debug(event); this._logger.debug(event);
switch (event.type) { switch (event.type) {
case ControllerEventType.PULSE: case ControllerEventType.PULSE:
@ -110,6 +107,10 @@ export abstract class AbstractController {
} }
if (trigger.changes.pressed) { if (trigger.changes.pressed) {
if (trigger.pressed) { if (trigger.pressed) {
if (this.diagramManager.diagramMenuManager.scaleMenu.mesh == this._meshUnderPointer) {
return;
}
if (this._clickStart == 0) { if (this._clickStart == 0) {
this._clickStart = Date.now(); this._clickStart = Date.now();
window.setTimeout(() => { window.setTimeout(() => {
@ -140,33 +141,97 @@ export abstract class AbstractController {
}, -1, false, this); }, -1, false, this);
} }
protected notifyObserver(value: number, controllerEventType: ControllerEventType): number {
if (Math.abs(value) > .1) { private grab() {
controllerObservable.notifyObservers({ let mesh = this._meshUnderPointer
type: controllerEventType, if (!mesh || viewOnly()) {
value: value * this.speedFactor return;
}); }
return 1; this.grabbedMesh = mesh;
} else { this.grabbedMeshType = getMeshType(mesh, this.diagramManager);
return 0;
//displayDebug(mesh);
this._logger.debug("grabbing " + mesh.id + " type " + this.grabbedMeshType);
switch (this.grabbedMeshType) {
case MeshTypeEnum.ENTITY:
const diagramObject = this.diagramManager.getDiagramObject(mesh.id);
if (diagramObject.isGrabbable) {
diagramObject.baseTransform.setParent(this.xrInputSource.motionController.rootMesh);
diagramObject.grabbed = true;
this.grabbedObject = diagramObject;
}
break;
case MeshTypeEnum.HANDLE:
this.grabbedMesh.setParent(this.xrInputSource.motionController.rootMesh);
break;
case MeshTypeEnum.TOOL:
const clone = grabAndClone(this.diagramManager, mesh, this.xrInputSource.motionController.rootMesh);
this.grabbedObject = clone;
this.grabbedMesh = clone.mesh;
clone.grabbed = true;
} }
} }
protected initButton(button: WebXRControllerComponent, type: ControllerEventType) { private drop() {
if (button) { const mesh = this.grabbedMesh;
button.onButtonStateChangedObservable.add((value) => { if (!mesh) {
if (value.pressed) { return;
this._logger.debug(button.type, button.id, 'pressed'); }
controllerObservable.notifyObservers({type: type}); const diagramObject = this.grabbedObject;
switch (this.grabbedMeshType) {
case MeshTypeEnum.ENTITY:
if (diagramObject) {
diagramObject.baseTransform.setParent(null);
snapAll(this.grabbedObject.baseTransform, this.diagramManager.config, this._pickPoint);
diagramObject.mesh.computeWorldMatrix(true);
const event: DiagramEvent =
{
type: DiagramEventType.DROP,
entity: diagramObject.diagramEntity
}
this.diagramManager.onDiagramEventObservable.notifyObservers(event, DiagramEventObserverMask.ALL);
diagramObject.mesh.computeWorldMatrix(false);
diagramObject.grabbed = false;
} }
});
this.grabbedObject = null;
this.grabbedMesh = null;
this.grabbedMeshType = null;
break;
case MeshTypeEnum.TOOL:
this.grabbedObject.baseTransform.setParent(null);
snapAll(this.grabbedObject.baseTransform, this.diagramManager.config, this._pickPoint);
diagramObject.mesh.computeWorldMatrix(true);
const event: DiagramEvent =
{
type: DiagramEventType.DROP,
entity: diagramObject.diagramEntity
}
this.diagramManager.onDiagramEventObservable.notifyObservers(event, DiagramEventObserverMask.ALL);
diagramObject.mesh.computeWorldMatrix(false);
this.grabbedObject.grabbed = false;
this.grabbedObject = null;
this.grabbedMesh = null;
this.grabbedMeshType = null;
break;
case MeshTypeEnum.HANDLE:
mesh.setParent(this.scene.getMeshByName("platform"));
const location = {
position: {x: mesh.position.x, y: mesh.position.y, z: mesh.position.z},
rotation: {x: mesh.rotation.x, y: mesh.rotation.y, z: mesh.rotation.z}
}
localStorage.setItem(mesh.id, JSON.stringify(location));
this.grabbedMesh = null;
this.grabbedMeshType = null;
this.grabbedObject = null;
break;
} }
} }
private click() { private click() {
let mesh = this.xr.pointerSelection.getMeshUnderPointer(this.xrInputSource.uniqueId); let mesh = this.xr.pointerSelection.getMeshUnderPointer(this.xrInputSource.uniqueId);
if (this.diagramManager.isDiagramObject(mesh)) {
if (mesh && this.diagramManager.isDiagramObject(mesh)) {
this._logger.debug("click on " + mesh.id); this._logger.debug("click on " + mesh.id);
if (this.diagramManager.diagramMenuManager.connectionPreview) { if (this.diagramManager.diagramMenuManager.connectionPreview) {
this.diagramManager.diagramMenuManager.connect(mesh); this.diagramManager.diagramMenuManager.connect(mesh);
@ -186,37 +251,11 @@ export abstract class AbstractController {
grip.onButtonStateChangedObservable.add(() => { grip.onButtonStateChangedObservable.add(() => {
if (grip.changes.pressed) { if (grip.changes.pressed) {
if (grip.pressed) { if (grip.pressed) {
this._logger.debug("=== SQUEEZE PRESSED ===");
this.grab(); this.grab();
} else { } else {
this._logger.debug("=== SQUEEZE RELEASED ===");
this.drop(); this.drop();
} }
} }
}); });
} }
private grab() {
if (viewOnly() || this._meshUnderPointer == null) {
return;
}
const {
grabbedMesh,
grabbedObject,
grabbedMeshType
} = grabMesh(this._meshUnderPointer, this.diagramManager, this.xrInputSource.motionController.rootMesh);
this.grabbedMesh = grabbedMesh;
this.grabbedObject = grabbedObject;
this.grabbedMeshType = grabbedMeshType;
}
private drop() {
const dropped = dropMesh(this.grabbedMesh, this.grabbedObject, this._pickPoint, this.grabbedMeshType, this.diagramManager);
if (dropped) {
this.grabbedMesh = null;
this.grabbedObject = null;
this.grabbedMeshType = null;
}
}
} }

View File

@ -1,6 +1,41 @@
import {AbstractMesh, Observable, TransformNode} from "@babylonjs/core"; import {AbstractMesh, Observable, TransformNode, Vector3, WebXRInputSource} from "@babylonjs/core";
import {ControllerEvent} from "./types/controllerEvent";
export type ControllerEvent = {
type: ControllerEventType,
value?: number,
startPosition?: Vector3,
endPosition?: Vector3,
duration?: number,
gripId?: string;
controller?: WebXRInputSource;
}
export var movable: TransformNode | AbstractMesh; export enum ControllerEventType {
export const controllerObservable: Observable<ControllerEvent> = new Observable(); GRIP = 'grip',
HIDE = 'hide',
SHOW = 'show',
PULSE = 'pulse',
SQUEEZE = 'squeeze',
CLICK = 'click',
Y_BUTTON = 'y-button',
X_BUTTON = 'x-button',
A_BUTTON = 'a-button',
B_BUTTON = 'b-button',
THUMBSTICK = 'thumbstick',
THUMBSTICK_CHANGED = 'thumbstickChanged',
DECREASE_VELOCITY = 'decreaseVelocity',
INCREASE_VELOCITY = 'decreaseVelocity',
LEFT_RIGHT = 'leftright',
FORWARD_BACK = 'forwardback',
TURN = 'turn',
UP_DOWN = 'updown',
TRIGGER = 'trigger',
MENU = 'menu',
MOTION = 'motion',
GAZEPOINT = 'gazepoint',
}
export class Controllers {
public movable: TransformNode | AbstractMesh;
public readonly controllerObservable: Observable<ControllerEvent> = new Observable();
}

View File

@ -0,0 +1,25 @@
import {HavokPlugin} from "@babylonjs/core";
import {DefaultScene} from "../../defaultScene";
import log from "loglevel";
export function beforeRenderObserver() {
if (this?.grabbedMesh?.physicsBody) {
const scene = DefaultScene.Scene;
const hk = (scene.getPhysicsEngine().getPhysicsPlugin() as HavokPlugin);
this.lastPosition = this?.grabbedMesh?.physicsBody?.transformNode.absolutePosition.clone();
if (this.grabbedMeshParentId) {
const parent = scene.getTransformNodeById(this.grabbedMeshParentId);
if (parent) {
hk.setPhysicsBodyTransformation(this.grabbedMesh.physicsBody, parent);
hk.sync(this.grabbedMesh.physicsBody);
} else {
log.getLogger('beforeRenderObserver').error("parent not found for " + this.grabbedMeshParentId);
}
} else {
log.getLogger('beforeRenderObserver').warn("no parent id");
}
}
}

View File

@ -0,0 +1,12 @@
import {DiagramEvent, DiagramEventType} from "../../diagram/types/diagramEntity";
import {toDiagramEntity} from "../../diagram/functions/toDiagramEntity";
import {AbstractMesh} from "@babylonjs/core";
export function buildDrop(mesh: AbstractMesh): DiagramEvent {
const entity = toDiagramEntity(mesh);
return {
type: DiagramEventType.DROP,
entity: entity
}
}

View File

@ -15,7 +15,6 @@ import {DefaultScene} from "../../defaultScene";
export function buildRig(xr: WebXRDefaultExperience): Mesh { export function buildRig(xr: WebXRDefaultExperience): Mesh {
const scene = DefaultScene.Scene; const scene = DefaultScene.Scene;
const rigMesh = MeshBuilder.CreateCylinder("platform", {diameter: .5, height: .01}, scene); const rigMesh = MeshBuilder.CreateCylinder("platform", {diameter: .5, height: .01}, scene);
rigMesh.setAbsolutePosition(new Vector3(0, .01, 5));
const cameratransform = new TransformNode("cameraTransform", scene); const cameratransform = new TransformNode("cameraTransform", scene);
cameratransform.parent = rigMesh; cameratransform.parent = rigMesh;
xr.baseExperience.onInitialXRPoseSetObservable.add(() => { xr.baseExperience.onInitialXRPoseSetObservable.add(() => {
@ -25,45 +24,25 @@ export function buildRig(xr: WebXRDefaultExperience): Mesh {
}); });
for (const cam of scene.cameras) { for (const cam of scene.cameras) {
cam.parent = cameratransform; cam.parent = cameratransform;
cam.position = new Vector3(0, 1.6, 0); // Reset to local position above platform
} }
scene.onActiveCameraChanged.add(() => { scene.onActiveCameraChanged.add(() => {
for (const cam of scene.cameras) { for (const cam of scene.cameras) {
cam.parent = cameratransform; cam.parent = cameratransform;
cam.position = new Vector3(0, 1.6, 0); // Reset to local position above platform
} }
}); });
rigMesh.setAbsolutePosition(new Vector3(0, .01, 5));
rigMesh.isPickable = false; rigMesh.isPickable = false;
const axis = new AxesViewer(scene, .25); const axis = new AxesViewer(scene, .25);
axis.zAxis.rotation.y = Math.PI; axis.zAxis.rotation.y = Math.PI;
rigMesh.lookAt(new Vector3(0, 0.01, 0)); rigMesh.lookAt(new Vector3(0, 0.01, 0));
rigMesh.visibility = 1; rigMesh.visibility = 1;
const rigAggregate =
// Only create physics aggregate if physics engine is available new PhysicsAggregate(
if (scene.getPhysicsEngine()) { rigMesh,
const rigAggregate = PhysicsShapeType.CYLINDER,
new PhysicsAggregate( {friction: 0, center: Vector3.Zero(), mass: 50, restitution: .01},
rigMesh, scene);
PhysicsShapeType.CYLINDER, rigAggregate.body.setMotionType(PhysicsMotionType.DYNAMIC);
{friction: 0, center: Vector3.Zero(), mass: 50, restitution: .01},
scene);
rigAggregate.body.setMotionType(PhysicsMotionType.DYNAMIC);
} else {
// Add physics aggregate once physics is initialized
scene.onReadyObservable.addOnce(() => {
if (scene.getPhysicsEngine()) {
const rigAggregate =
new PhysicsAggregate(
rigMesh,
PhysicsShapeType.CYLINDER,
{friction: 0, center: Vector3.Zero(), mass: 50, restitution: .01},
scene);
rigAggregate.body.setMotionType(PhysicsMotionType.DYNAMIC);
}
});
}
return rigMesh; return rigMesh;
} }

View File

@ -3,8 +3,6 @@ import {DiagramManager} from "../../diagram/diagramManager";
import {DiagramObject} from "../../diagram/diagramObject"; import {DiagramObject} from "../../diagram/diagramObject";
import log from "loglevel"; import log from "loglevel";
import {vectoxys} from "../../diagram/functions/vectorConversion"; import {vectoxys} from "../../diagram/functions/vectorConversion";
import {DiagramEntityType} from "../../diagram/types/diagramEntity";
import {DefaultScene} from "../../defaultScene";
export function grabAndClone(diagramManager: DiagramManager, mesh: AbstractMesh, parent: AbstractMesh): export function grabAndClone(diagramManager: DiagramManager, mesh: AbstractMesh, parent: AbstractMesh):
DiagramObject { DiagramObject {
@ -25,11 +23,10 @@ export function grabAndClone(diagramManager: DiagramManager, mesh: AbstractMesh,
color: mesh.metadata.color, color: mesh.metadata.color,
position: vectoxys(mesh.absolutePosition), position: vectoxys(mesh.absolutePosition),
rotation: vectoxys(mesh.absoluteRotationQuaternion.toEulerAngles()), rotation: vectoxys(mesh.absoluteRotationQuaternion.toEulerAngles()),
scale: vectoxys(mesh.scaling), scale: vectoxys(mesh.scaling)
type: DiagramEntityType.ENTITY
} }
const obj = new DiagramObject(DefaultScene.Scene, const obj = new DiagramObject(parent.getScene(),
diagramManager.onDiagramEventObservable, diagramManager.onDiagramEventObservable,
{ {
diagramEntity: entity, diagramEntity: entity,
@ -38,5 +35,7 @@ export function grabAndClone(diagramManager: DiagramManager, mesh: AbstractMesh,
obj.baseTransform.setParent(parent); obj.baseTransform.setParent(parent);
diagramManager.addObject(obj); diagramManager.addObject(obj);
return obj; return obj;
} }
} }

View File

@ -8,6 +8,7 @@ export function handleWasGrabbed(mesh: AbstractMesh): boolean {
logger.debug("handleWasGrabbed: mesh is a diagram entity"); logger.debug("handleWasGrabbed: mesh is a diagram entity");
return false; return false;
} else { } else {
const result = (mesh?.metadata?.handle == true); const result = (mesh?.metadata?.handle == true);
logger.debug("handleWasGrabbed: mesh ", result); logger.debug("handleWasGrabbed: mesh ", result);
return result; return result;

View File

@ -1,7 +1,7 @@
import log from "loglevel"; import log from "loglevel";
export function motionControllerInitObserver(init) { export function motionControllerObserver(init) {
const logger = log.getLogger('motionControllerObserver'); const logger = log.getLogger('motionControllerObserver');
logger.debug(init.components); logger.debug(init.components);
if (init.components['xr-standard-squeeze']) { if (init.components['xr-standard-squeeze']) {

View File

@ -0,0 +1,24 @@
import {AbstractMesh} from "@babylonjs/core";
import log from "loglevel";
export function reparent(mesh: AbstractMesh, previousParentId: string, grabbedMeshParentId: string) {
const logger = log.getLogger('reparent');
if (previousParentId) {
const parent = mesh.getScene().getMeshById(previousParentId);
if (parent) {
logger.warn('not yet implemented')
} else {
mesh.setParent(null);
}
} else {
const parent = mesh.getScene().getTransformNodeById(grabbedMeshParentId);
if (parent) {
logger.warn('setting parent to null', grabbedMeshParentId, parent)
mesh.setParent(null);
parent.dispose();
} else {
mesh.setParent(null);
}
}
}

View File

@ -0,0 +1,10 @@
import {AbstractMesh, TransformNode} from "@babylonjs/core";
export function setupTransformNode(mesh: TransformNode, parent: AbstractMesh) {
const transformNode = new TransformNode("grabAnchor, this.scene");
transformNode.id = "grabAnchor";
transformNode.position = mesh.position.clone();
transformNode.rotationQuaternion = mesh.rotationQuaternion.clone();
transformNode.setParent(parent);
return transformNode;
}

View File

@ -1,24 +1,15 @@
import {TransformNode, Vector3} from "@babylonjs/core"; import {TransformNode, Vector3} from "@babylonjs/core";
import {appConfigInstance} from "../../util/appConfig"; import {AppConfig} from "../../util/appConfig";
import {snapRotateVal} from "../../util/functions/snapRotateVal"; import {snapRotateVal} from "../../util/functions/snapRotateVal";
import {snapGridVal} from "../../util/functions/snapGridVal"; import {snapGridVal} from "../../util/functions/snapGridVal";
export function snapAll(node: TransformNode, pickPoint: Vector3) { export function snapAll(node: TransformNode, config: AppConfig, pickPoint: Vector3) {
const config = appConfigInstance.current;
const transform = new TransformNode('temp', node.getScene()); const transform = new TransformNode('temp', node.getScene());
transform.position = pickPoint; transform.position = pickPoint;
node.setParent(transform); node.setParent(transform);
if (config.rotateSnap > 0) { node.rotation = snapRotateVal(node.absoluteRotationQuaternion.toEulerAngles(), config.current.rotateSnap);
node.rotation = snapRotateVal(node.absoluteRotationQuaternion.toEulerAngles(), config.rotateSnap); transform.position = snapGridVal(transform.absolutePosition, config.current.gridSnap);
}
if (config.locationSnap > 0) {
transform.position = snapGridVal(transform.absolutePosition, config.locationSnap);
}
node.setParent(null); node.setParent(null);
if (config.locationSnap > 0) { node.position = snapGridVal(node.absolutePosition, config.current.gridSnap);
node.position = snapGridVal(node.absolutePosition, config.locationSnap);
}
transform.dispose(); transform.dispose();
} }

View File

@ -1,22 +1,23 @@
import {Vector3, WebXRControllerComponent, WebXRDefaultExperience, WebXRInputSource} from "@babylonjs/core"; import {Vector3, WebXRControllerComponent, WebXRDefaultExperience, WebXRInputSource} from "@babylonjs/core";
import {AbstractController} from "./abstractController"; import {Base} from "./base";
import {ControllerEventType} from "./controllers";
import log from "loglevel"; import log from "loglevel";
import {DiagramManager} from "../diagram/diagramManager"; import {DiagramManager} from "../diagram/diagramManager";
import {ControllerEventType} from "./types/controllerEventType"; import {DefaultScene} from "../defaultScene";
import {controllerObservable, movable} from "./controllers";
export class LeftController extends AbstractController { export class Left extends Base {
private leftLogger = log.getLogger('Left'); private leftLogger = log.getLogger('Left');
constructor(controller: constructor(controller:
WebXRInputSource, xr: WebXRDefaultExperience, diagramManager: DiagramManager) { WebXRInputSource, xr: WebXRDefaultExperience, diagramManager: DiagramManager) {
super(controller, xr, diagramManager); super(controller, xr, diagramManager);
const scene = DefaultScene.Scene;
this.xrInputSource.onMotionControllerInitObservable.add((init) => { this.xrInputSource.onMotionControllerInitObservable.add((init) => {
if (init.components['xr-standard-thumbstick']) { if (init.components['xr-standard-thumbstick']) {
init.components['xr-standard-thumbstick'] init.components['xr-standard-thumbstick']
.onAxisValueChangedObservable.add((value) => { .onAxisValueChangedObservable.add((value) => {
this.leftLogger.trace(`thumbstick moved ${value.x}, ${value.y}`); this.leftLogger.trace(`thumbstick moved ${value.x}, ${value.y}`);
if (!movable) { if (!this.controllers.movable) {
this.moveRig(value); this.moveRig(value);
} else { } else {
this.moveMovable(value); this.moveMovable(value);
@ -28,7 +29,7 @@ export class LeftController extends AbstractController {
init.components['xr-standard-thumbstick'].onButtonStateChangedObservable.add((value) => { init.components['xr-standard-thumbstick'].onButtonStateChangedObservable.add((value) => {
if (value.pressed) { if (value.pressed) {
this.leftLogger.trace('Left', 'thumbstick changed'); this.leftLogger.trace('Left', 'thumbstick changed');
controllerObservable.notifyObservers({ this.controllers.controllerObservable.notifyObservers({
type: ControllerEventType.DECREASE_VELOCITY, type: ControllerEventType.DECREASE_VELOCITY,
value: value.value value: value.value
}); });
@ -45,7 +46,7 @@ export class LeftController extends AbstractController {
.onButtonStateChangedObservable .onButtonStateChangedObservable
.add((button) => { .add((button) => {
this.leftLogger.trace('trigger pressed'); this.leftLogger.trace('trigger pressed');
controllerObservable.notifyObservers({ this.controllers.controllerObservable.notifyObservers({
type: ControllerEventType.TRIGGER, type: ControllerEventType.TRIGGER,
value: button.value, value: button.value,
controller: this.xrInputSource controller: this.xrInputSource
@ -59,7 +60,7 @@ export class LeftController extends AbstractController {
xbutton.onButtonStateChangedObservable.add((button) => { xbutton.onButtonStateChangedObservable.add((button) => {
if (button.pressed) { if (button.pressed) {
this.leftLogger.trace('X button pressed'); this.leftLogger.trace('X button pressed');
controllerObservable.notifyObservers({ this.controllers.controllerObservable.notifyObservers({
type: ControllerEventType.X_BUTTON, type: ControllerEventType.X_BUTTON,
value: button.value value: button.value
}); });
@ -73,7 +74,7 @@ export class LeftController extends AbstractController {
ybutton.onButtonStateChangedObservable.add((button) => { ybutton.onButtonStateChangedObservable.add((button) => {
if (button.pressed) { if (button.pressed) {
this.leftLogger.trace('Y button pressed'); this.leftLogger.trace('Y button pressed');
controllerObservable.notifyObservers({ this.controllers.controllerObservable.notifyObservers({
type: ControllerEventType.Y_BUTTON, type: ControllerEventType.Y_BUTTON,
value: button.value value: button.value
}); });
@ -84,23 +85,42 @@ export class LeftController extends AbstractController {
private moveMovable(value: { x: number, y: number }) { private moveMovable(value: { x: number, y: number }) {
if (Math.abs(value.x) > .1) { if (Math.abs(value.x) > .1) {
movable.position.x += .005 * Math.sign(value.x); this.controllers.movable.position.x += .005 * Math.sign(value.x);
} else { } else {
} }
if (Math.abs(value.y) > .1) { if (Math.abs(value.y) > .1) {
movable.position.y += -.005 * Math.sign(value.y); this.controllers.movable.position.y += -.005 * Math.sign(value.y);
} else { } else {
} }
} }
private moveRig(value: { x: number, y: number }) { private moveRig(value: { x: number, y: number }) {
AbstractController.stickVector.x = this.notifyObserver(value.x, ControllerEventType.LEFT_RIGHT); if (Math.abs(value.x) > .1) {
AbstractController.stickVector.y = this.notifyObserver(value.y, ControllerEventType.FORWARD_BACK); this.controllers.controllerObservable.notifyObservers({
if (AbstractController.stickVector.equals(Vector3.Zero())) { type: ControllerEventType.LEFT_RIGHT,
controllerObservable.notifyObservers({type: ControllerEventType.LEFT_RIGHT, value: 0}); value: value.x * this.speedFactor
controllerObservable.notifyObservers({type: ControllerEventType.FORWARD_BACK, value: 0}); });
Base.stickVector.x = 1;
} else {
Base.stickVector.x = 0;
}
if (Math.abs(value.y) > .1) {
this.controllers.controllerObservable.notifyObservers({
type: ControllerEventType.FORWARD_BACK,
value: value.y * this.speedFactor
});
Base.stickVector.y = 1;
} else {
Base.stickVector.y = 0;
}
if (Base.stickVector.equals(Vector3.Zero())) {
this.controllers.controllerObservable.notifyObservers({type: ControllerEventType.LEFT_RIGHT, value: 0});
this.controllers.controllerObservable.notifyObservers({type: ControllerEventType.FORWARD_BACK, value: 0});
} else {
} }
} }
} }

102
src/controllers/right.ts Normal file
View File

@ -0,0 +1,102 @@
import {Base} from "./base";
import {Vector3, WebXRControllerComponent, WebXRDefaultExperience, WebXRInputSource} from "@babylonjs/core";
import {ControllerEventType} from "./controllers";
import {DiagramManager} from "../diagram/diagramManager";
import log from "loglevel";
export class Right extends Base {
private rightLogger = log.getLogger("Right");
private initBButton(bbutton: WebXRControllerComponent) {
if (bbutton) {
bbutton.onButtonStateChangedObservable.add((button) => {
if (button.pressed) {
this.rightLogger.debug('B Button Pressed');
this.controllers.controllerObservable.notifyObservers({
type: ControllerEventType.B_BUTTON,
value: button.value
});
}
});
}
}
constructor(controller: WebXRInputSource,
xr: WebXRDefaultExperience,
diagramManager: DiagramManager
) {
super(controller, xr, diagramManager);
this.xrInputSource.onMotionControllerInitObservable.add((init) => {
this.initTrigger(init.components['xr-standard-trigger']);
this.initBButton(init.components['b-button']);
this.initAButton(init.components['a-button']);
this.initThumbstick(init.components['xr-standard-thumbstick']);
});
}
private initTrigger(trigger: WebXRControllerComponent) {
if (trigger) {
trigger
.onButtonStateChangedObservable
.add((button) => {
this.rightLogger.debug("right trigger pressed");
this.controllers.controllerObservable.notifyObservers({
type: ControllerEventType.TRIGGER,
value: button.value,
controller: this.xrInputSource
});
}, -1, false, this);
}
}
private initAButton(abutton: WebXRControllerComponent) {
if (abutton) {
abutton.onButtonStateChangedObservable.add((value) => {
if (value.pressed) {
this.rightLogger.debug('A button pressed');
this.controllers.controllerObservable.notifyObservers({type: ControllerEventType.MENU});
}
});
}
}
private initThumbstick(thumbstick: WebXRControllerComponent) {
if (thumbstick) {
thumbstick.onAxisValueChangedObservable.add((value) => {
this.rightLogger.trace(`thumbstick moved ${value.x}, ${value.y}`);
this.moveRig(value);
});
thumbstick.onButtonStateChangedObservable.add((value) => {
if (value.pressed) {
this.rightLogger.trace('Right', `thumbstick changed ${value.value}`);
this.controllers.controllerObservable.notifyObservers({
type: ControllerEventType.INCREASE_VELOCITY,
value: value.value
});
}
});
}
}
private moveRig(value) {
if (Math.abs(value.x) > .1) {
this.controllers.controllerObservable.notifyObservers({type: ControllerEventType.TURN, value: value.x});
} else {
this.controllers.controllerObservable.notifyObservers({type: ControllerEventType.TURN, value: 0});
}
if (Math.abs(value.y) > .1) {
this.controllers.controllerObservable.notifyObservers({
type: ControllerEventType.UP_DOWN,
value: value.y * this.speedFactor
});
Base.stickVector.z = 1;
} else {
this.controllers.controllerObservable.notifyObservers({type: ControllerEventType.UP_DOWN, value: 0});
Base.stickVector.z = 0;
}
if (Base.stickVector.equals(Vector3.Zero())) {
this.controllers.controllerObservable.notifyObservers({type: ControllerEventType.UP_DOWN, value: 0});
}
}
}

View File

@ -1,73 +0,0 @@
import {AbstractController} from "./abstractController";
import {Vector3, WebXRControllerComponent, WebXRDefaultExperience, WebXRInputSource} from "@babylonjs/core";
import {DiagramManager} from "../diagram/diagramManager";
import log from "loglevel";
import {ControllerEventType} from "./types/controllerEventType";
import {controllerObservable} from "./controllers";
export class RightController extends AbstractController {
private rightLogger = log.getLogger("Right");
constructor(controller: WebXRInputSource,
xr: WebXRDefaultExperience,
diagramManager: DiagramManager
) {
super(controller, xr, diagramManager);
this.xrInputSource.onMotionControllerInitObservable.add((init) => {
this.initTrigger(init.components['xr-standard-trigger']);
this.initButton(init.components['b-button'], ControllerEventType.B_BUTTON);
this.initButton(init.components['a-button'], ControllerEventType.MENU);
this.initThumbstick(init.components['xr-standard-thumbstick']);
});
}
private initTrigger(trigger: WebXRControllerComponent) {
if (trigger) {
trigger
.onButtonStateChangedObservable
.add((button) => {
this.rightLogger.debug("right trigger pressed");
controllerObservable.notifyObservers({
type: ControllerEventType.TRIGGER,
value: button.value,
controller: this.xrInputSource
});
}, -1, false, this);
}
}
private initThumbstick(thumbstick: WebXRControllerComponent) {
if (thumbstick) {
thumbstick.onAxisValueChangedObservable.add((value) => {
this.rightLogger.trace(`thumbstick moved ${value.x}, ${value.y}`);
this.moveRig(value);
});
thumbstick.onButtonStateChangedObservable.add((value) => {
if (value.pressed) {
this.rightLogger.trace('Right', `thumbstick changed ${value.value}`);
controllerObservable.notifyObservers({
type: ControllerEventType.INCREASE_VELOCITY,
value: value.value
});
}
});
}
}
private moveRig(value) {
if (Math.abs(value.x) > .1) {
controllerObservable.notifyObservers({type: ControllerEventType.TURN, value: value.x});
} else {
controllerObservable.notifyObservers({type: ControllerEventType.TURN, value: 0});
}
AbstractController.stickVector.z = this.notifyObserver(value.y, ControllerEventType.UP_DOWN);
if (AbstractController.stickVector.equals(Vector3.Zero())) {
controllerObservable.notifyObservers({type: ControllerEventType.UP_DOWN, value: 0});
}
}
}

View File

@ -1,20 +1,17 @@
import {Angle, Mesh, Observer, Quaternion, Scene, Vector3, WebXRDefaultExperience} from "@babylonjs/core"; import {Angle, Mesh, Quaternion, Scene, Vector3, WebXRDefaultExperience} from "@babylonjs/core";
import {RightController} from "./rightController"; import {Right} from "./right";
import {LeftController} from "./leftController"; import {Left} from "./left";
import {ControllerEvent, ControllerEventType, Controllers} from "./controllers";
import log from "loglevel"; import log from "loglevel";
import {DiagramManager} from "../diagram/diagramManager"; import {DiagramManager} from "../diagram/diagramManager";
import {buildRig} from "./functions/buildRig"; import {buildRig} from "./functions/buildRig";
import {DefaultScene} from "../defaultScene"; import {DefaultScene} from "../defaultScene";
import {ControllerEvent} from "./types/controllerEvent";
import {ControllerEventType} from "./types/controllerEventType";
import {controllerObservable} from "./controllers";
import {appConfigInstance} from "../util/appConfig";
import {AppConfigType} from "../util/appConfigType";
const RIGHT = "right"; const RIGHT = "right";
const LEFT = "left"; const LEFT = "left";
export class Rigplatform { export class Rigplatform {
public static instance: Rigplatform; public static instance: Rigplatform;
@ -22,21 +19,20 @@ export class Rigplatform {
public rigMesh: Mesh; public rigMesh: Mesh;
private _logger = log.getLogger('Rigplatform'); private _logger = log.getLogger('Rigplatform');
private readonly _controllers: Controllers;
private readonly _diagramManager: DiagramManager; private readonly _diagramManager: DiagramManager;
private readonly _scene: Scene; private readonly _scene: Scene;
private readonly _velocityArray = [0.01, 0.1, 1, 2, 5]; private readonly _velocityArray = [0.01, 0.1, 1, 2, 5];
private readonly _xr: WebXRDefaultExperience; private readonly _xr: WebXRDefaultExperience;
private _rightController: RightController; private _rightController: Right;
private _leftController: LeftController; private _leftController: Left;
private _turning: boolean = false; private _turning: boolean = false;
private _velocity: Vector3 = Vector3.Zero(); private _velocity: Vector3 = Vector3.Zero();
private _velocityIndex: number = 2; private _velocityIndex: number = 2;
private _turnVelocity: number = 0; private _turnVelocity: number = 0;
private _registered = false; private _registered = false;
private _yRotation: number = 0; private _yRotation: number = 0;
private _configObserver: Observer<AppConfigType>;
constructor( constructor(
xr: WebXRDefaultExperience, xr: WebXRDefaultExperience,
@ -44,34 +40,12 @@ export class Rigplatform {
) { ) {
this._scene = DefaultScene.Scene; this._scene = DefaultScene.Scene;
this._diagramManager = diagramManager; this._diagramManager = diagramManager;
this._controllers = diagramManager.controllers;
this._xr = xr; this._xr = xr;
this.rigMesh = buildRig(xr); this.rigMesh = buildRig(xr);
// Exit XR button is now created in toolbox class
this._fixRotation(); this._fixRotation();
this._initializeControllers(); this._initializeControllers();
this._registerVelocityObserver(); this._registerVelocityObserver();
this._subscribeToConfigChanges();
}
/**
* Subscribe to config changes to update flyMode and turnSnap at runtime
*/
private _subscribeToConfigChanges(): void {
this._configObserver = appConfigInstance.onConfigChangedObservable.add((config) => {
// Update fly mode if changed
if (config.flyMode !== this._flyMode) {
this.flyMode = config.flyMode;
this._logger.debug('Fly mode updated from config:', config.flyMode);
}
// Update turn snap if changed
if (config.turnSnap !== this.turnSnap) {
this.turnSnap = config.turnSnap;
this._logger.debug('Turn snap updated from config:', config.turnSnap);
}
});
} }
private _flyMode: boolean = true; private _flyMode: boolean = true;
@ -138,12 +112,12 @@ export class Rigplatform {
private _registerObserver() { private _registerObserver() {
if (this._registered) { if (this._registered) {
this._logger.warn('observer already registered, clearing and re registering'); this._logger.warn('observer already registered, clearing and re registering');
controllerObservable.clear(); this._controllers.controllerObservable.clear();
this._registered = false; this._registered = false;
} }
if (!this._registered) { if (!this._registered) {
this._registered = true; this._registered = true;
controllerObservable.add((event: ControllerEvent) => { this._controllers.controllerObservable.add((event: ControllerEvent) => {
this._logger.debug(event); this._logger.debug(event);
switch (event.type) { switch (event.type) {
case ControllerEventType.INCREASE_VELOCITY: case ControllerEventType.INCREASE_VELOCITY:
@ -191,12 +165,12 @@ export class Rigplatform {
switch (source.inputSource.handedness) { switch (source.inputSource.handedness) {
case RIGHT: case RIGHT:
if (!this._rightController) { if (!this._rightController) {
this._rightController = new RightController(source, this._xr, this._diagramManager); this._rightController = new Right(source, this._xr, this._diagramManager);
} }
break; break;
case LEFT: case LEFT:
if (!this._leftController) { if (!this._leftController) {
this._leftController = new LeftController(source, this._xr, this._diagramManager); this._leftController = new Left(source, this._xr, this._diagramManager);
} }
break; break;
} }
@ -238,25 +212,4 @@ export class Rigplatform {
} }
}, -1, false, this, false); }, -1, false, this, false);
} }
/**
* Clean up resources and observers
*/
public dispose(): void {
// Remove config observer
if (this._configObserver) {
appConfigInstance.onConfigChangedObservable.remove(this._configObserver);
this._configObserver = null;
}
// Clean up controllers
if (this._rightController) {
this._rightController = null;
}
if (this._leftController) {
this._leftController = null;
}
this._logger.debug('Rigplatform disposed');
}
} }

View File

@ -1,12 +0,0 @@
import {Vector3, WebXRInputSource} from "@babylonjs/core";
import {ControllerEventType} from "./controllerEventType";
export type ControllerEvent = {
type: ControllerEventType,
value?: number,
startPosition?: Vector3,
endPosition?: Vector3,
duration?: number,
gripId?: string;
controller?: WebXRInputSource;
}

View File

@ -1,24 +0,0 @@
export enum ControllerEventType {
GRIP = 'grip',
HIDE = 'hide',
SHOW = 'show',
PULSE = 'pulse',
SQUEEZE = 'squeeze',
CLICK = 'click',
Y_BUTTON = 'y-button',
X_BUTTON = 'x-button',
A_BUTTON = 'a-button',
B_BUTTON = 'b-button',
THUMBSTICK = 'thumbstick',
THUMBSTICK_CHANGED = 'thumbstickChanged',
DECREASE_VELOCITY = 'decreaseVelocity',
INCREASE_VELOCITY = 'decreaseVelocity',
LEFT_RIGHT = 'leftright',
FORWARD_BACK = 'forwardback',
TURN = 'turn',
UP_DOWN = 'updown',
TRIGGER = 'trigger',
MENU = 'menu',
MOTION = 'motion',
GAZEPOINT = 'gazepoint',
}

View File

@ -1,12 +1,9 @@
import {AbstractMesh, KeyboardEventTypes, Scene} from "@babylonjs/core"; import {AbstractMesh, KeyboardEventTypes, Scene} from "@babylonjs/core";
import {Rigplatform} from "./rigplatform"; import {Rigplatform} from "./rigplatform";
import {Controllers} from "./controllers";
import {DiagramManager} from "../diagram/diagramManager"; import {DiagramManager} from "../diagram/diagramManager";
import {wheelHandler} from "./functions/wheelHandler"; import {wheelHandler} from "./functions/wheelHandler";
import log, {Logger} from "loglevel"; import log, {Logger} from "loglevel";
import {DiagramEntityType, DiagramEventType, DiagramTemplates} from "../diagram/types/diagramEntity";
import {DiagramEventObserverMask} from "../diagram/types/diagramEventObserverMask";
import {getToolboxColors} from "../toolbox/toolbox";
export class WebController { export class WebController {
private readonly scene: Scene; private readonly scene: Scene;
@ -15,7 +12,7 @@ export class WebController {
private rig: Rigplatform; private rig: Rigplatform;
private diagramManager: DiagramManager; private diagramManager: DiagramManager;
private mouseDown: boolean = false; private mouseDown: boolean = false;
private readonly controllers: Controllers;
private upDownWheel: boolean = false; private upDownWheel: boolean = false;
private fowardBackWheel: boolean = false; private fowardBackWheel: boolean = false;
private canvas: HTMLCanvasElement; private canvas: HTMLCanvasElement;
@ -23,11 +20,11 @@ export class WebController {
constructor(scene: Scene, constructor(scene: Scene,
rig: Rigplatform, rig: Rigplatform,
diagramManager: DiagramManager, diagramManager: DiagramManager,
) { controllers: Controllers) {
this.scene = scene; this.scene = scene;
this.rig = rig; this.rig = rig;
this.diagramManager = diagramManager; this.diagramManager = diagramManager;
this.controllers = controllers;
this.canvas = document.querySelector('#gameCanvas'); this.canvas = document.querySelector('#gameCanvas');
//this.referencePlane = MeshBuilder.CreatePlane('referencePlane', {size: 10}, this.scene); //this.referencePlane = MeshBuilder.CreatePlane('referencePlane', {size: 10}, this.scene);
//this.referencePlane.setEnabled(false); //this.referencePlane.setEnabled(false);
@ -97,18 +94,6 @@ export class WebController {
*/ */
break; break;
case "T":
// Ctrl+Shift+T: Create test entities (sphere and box)
if (kbInfo.event.ctrlKey && kbInfo.event.shiftKey) {
this.createTestEntities();
}
break;
case "X":
// Ctrl+Shift+X: Clear all entities from diagram
if (kbInfo.event.ctrlKey && kbInfo.event.shiftKey) {
this.clearAllEntities();
}
break;
default: default:
this.logger.debug(kbInfo.event); this.logger.debug(kbInfo.event);
@ -175,7 +160,7 @@ export class WebController {
}); });
this.scene.onPointerDown = (evt) => { this.scene.onPointerDown = (evt, state) => {
if (evt.pointerType == "mouse") { if (evt.pointerType == "mouse") {
this.mouseDown = true; this.mouseDown = true;
/*if (evt.shiftKey) { /*if (evt.shiftKey) {
@ -255,57 +240,4 @@ export class WebController {
} }
this._mesh = mesh; this._mesh = mesh;
} }
/**
* Create test entities for testing ResizeGizmo
* Creates a sphere at (-0.25, 1.5, 4) and a box at (0.25, 1.5, 4)
*/
private createTestEntities(): void {
this.logger.info('Creating test entities (Ctrl+Shift+T)');
// Get first color from toolbox colors array
const firstColor = getToolboxColors()[0];
const colorHex = firstColor.replace('#', '');
// Create sphere
this.diagramManager.onDiagramEventObservable.notifyObservers({
type: DiagramEventType.ADD,
entity: {
id: `test-sphere-${colorHex}`,
type: DiagramEntityType.ENTITY,
template: DiagramTemplates.SPHERE,
position: { x: -0.25, y: 1.5, z: 4 },
scale: { x: 0.1, y: 0.1, z: 0.1 },
color: firstColor
}
}, DiagramEventObserverMask.ALL);
// Create box
this.diagramManager.onDiagramEventObservable.notifyObservers({
type: DiagramEventType.ADD,
entity: {
id: `test-box-${colorHex}`,
type: DiagramEntityType.ENTITY,
template: DiagramTemplates.BOX,
position: { x: 0.25, y: 1.5, z: 4 },
scale: { x: 0.1, y: 0.1, z: 0.1 },
color: firstColor
}
}, DiagramEventObserverMask.ALL);
this.logger.info(`Test entities created with color ${firstColor}: test-sphere-${colorHex} at (-0.25, 1.5, 4) and test-box-${colorHex} at (0.25, 1.5, 4)`);
}
/**
* Clear all entities from the diagram
*/
private clearAllEntities(): void {
this.logger.info('Clearing all entities from diagram (Ctrl+Shift+X)');
this.diagramManager.onDiagramEventObservable.notifyObservers({
type: DiagramEventType.CLEAR
}, DiagramEventObserverMask.TO_DB);
this.logger.info('All entities cleared from diagram');
}
} }

View File

@ -4,10 +4,6 @@ import log from "loglevel";
export class DefaultScene { export class DefaultScene {
private static _Scene: Scene; private static _Scene: Scene;
private static _UtilityScene: Scene;
public static get UtilityScene(): Scene {
return this._UtilityScene;
}
public static get Scene(): Scene { public static get Scene(): Scene {
if (!DefaultScene._Scene) { if (!DefaultScene._Scene) {

View File

@ -1,8 +1,8 @@
import {AbstractActionManager, AbstractMesh, ActionManager, Observable, Scene, Vector3, WebXRDefaultExperience} from "@babylonjs/core"; import {AbstractActionManager, AbstractMesh, ActionManager, Observable, Scene} from "@babylonjs/core";
import {DiagramEntity, DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity"; import {DiagramEntity, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
import log from "loglevel"; import log from "loglevel";
import {Controllers} from "../controllers/controllers";
import {appConfigInstance} from "../util/appConfig"; import {AppConfig} from "../util/appConfig";
import {buildEntityActionManager} from "./functions/buildEntityActionManager"; import {buildEntityActionManager} from "./functions/buildEntityActionManager";
import {DefaultScene} from "../defaultScene"; import {DefaultScene} from "../defaultScene";
import {DiagramMenuManager} from "./diagramMenuManager"; import {DiagramMenuManager} from "./diagramMenuManager";
@ -11,14 +11,12 @@ import {DiagramObject} from "./diagramObject";
import {getMe} from "../util/me"; import {getMe} from "../util/me";
import {UserModelType} from "../users/userTypes"; import {UserModelType} from "../users/userTypes";
import {vectoxys} from "./functions/vectorConversion"; import {vectoxys} from "./functions/vectorConversion";
import {controllerObservable} from "../controllers/controllers";
import {ControllerEvent} from "../controllers/types/controllerEvent";
import {HEX_TO_COLOR_NAME, TEMPLATE_TO_SHAPE} from "../react/types/chatTypes";
export class DiagramManager { export class DiagramManager {
private readonly _logger = log.getLogger('DiagramManager'); private readonly _logger = log.getLogger('DiagramManager');
private readonly _controllerObservable: Observable<ControllerEvent>; public readonly _config: AppConfig;
private readonly _controllers: Controllers;
private readonly _diagramEntityActionManager: ActionManager; private readonly _diagramEntityActionManager: ActionManager;
public readonly onDiagramEventObservable: Observable<DiagramEvent> = new Observable(); public readonly onDiagramEventObservable: Observable<DiagramEvent> = new Observable();
public readonly onUserEventObservable: Observable<UserModelType> = new Observable(); public readonly onUserEventObservable: Observable<UserModelType> = new Observable();
@ -29,20 +27,16 @@ export class DiagramManager {
private _moving: number = 10; private _moving: number = 10;
private _i: number = 0; private _i: number = 0;
public get diagramMenuManager(): DiagramMenuManager {
return this._diagramMenuManager;
}
public setXR(xr: WebXRDefaultExperience): void {
this._diagramMenuManager.setXR(xr);
}
constructor(readyObservable: Observable<boolean>) { constructor(readyObservable: Observable<boolean>) {
this._me = getMe(); this._me = getMe();
this._scene = DefaultScene.Scene; this._scene = DefaultScene.Scene;
this._diagramMenuManager = new DiagramMenuManager(this.onDiagramEventObservable, controllerObservable, readyObservable); this._config = new AppConfig();
this._diagramEntityActionManager = buildEntityActionManager(controllerObservable); this._controllers = new Controllers();
this._diagramMenuManager = new DiagramMenuManager(this.onDiagramEventObservable, this._controllers, this._config, readyObservable);
this._diagramEntityActionManager = buildEntityActionManager(this._controllers);
this.onDiagramEventObservable.add(this.onDiagramEvent, DiagramEventObserverMask.FROM_DB, true, this); this.onDiagramEventObservable.add(this.onDiagramEvent, DiagramEventObserverMask.FROM_DB, true, this);
this.onUserEventObservable.add((user) => { this.onUserEventObservable.add((user) => {
if (user.id != this._me) { if (user.id != this._me) {
this._logger.debug('user event', user); this._logger.debug('user event', user);
@ -86,7 +80,6 @@ export class DiagramManager {
template: '#image-template', template: '#image-template',
image: event.detail.data, image: event.detail.data,
text: event.detail.name, text: event.detail.name,
type: DiagramEntityType.ENTITY,
position: {x: 0, y: 1.6, z: 0}, position: {x: 0, y: 1.6, z: 0},
rotation: {x: 0, y: Math.PI, z: 0}, rotation: {x: 0, y: Math.PI, z: 0},
scale: {x: 1, y: 1, z: 1}, scale: {x: 1, y: 1, z: 1},
@ -103,223 +96,18 @@ export class DiagramManager {
} }
}); });
// Chat event listeners for AI-powered diagram creation
document.addEventListener('chatCreateEntity', (event: CustomEvent) => {
const {entity} = event.detail;
this._logger.debug('chatCreateEntity', entity);
// Generate a default label if none is provided
if (!entity.text) {
entity.text = this.generateDefaultLabel(entity);
this._logger.debug('Generated default label:', entity.text);
}
const object = new DiagramObject(this._scene, this.onDiagramEventObservable, {
diagramEntity: entity,
actionManager: this._diagramEntityActionManager
});
this._diagramObjects.set(entity.id, object);
this.onDiagramEventObservable.notifyObservers({
type: DiagramEventType.ADD,
entity: entity
}, DiagramEventObserverMask.TO_DB);
});
document.addEventListener('chatRemoveEntity', (event: CustomEvent) => {
const {target} = event.detail;
this._logger.debug('chatRemoveEntity', target);
const entity = this.findEntityByIdOrLabel(target);
if (entity) {
const diagramObject = this._diagramObjects.get(entity.id);
if (diagramObject) {
diagramObject.dispose();
this._diagramObjects.delete(entity.id);
this.onDiagramEventObservable.notifyObservers({
type: DiagramEventType.REMOVE,
entity: entity
}, DiagramEventObserverMask.TO_DB);
}
}
});
document.addEventListener('chatModifyEntity', (event: CustomEvent) => {
const {target, updates} = event.detail;
this._logger.debug('chatModifyEntity', target, updates);
const entity = this.findEntityByIdOrLabel(target);
if (entity) {
const diagramObject = this._diagramObjects.get(entity.id);
if (diagramObject) {
// Apply updates using setters (each setter handles its own DB notification)
if (updates.text !== undefined) {
diagramObject.text = updates.text;
}
if (updates.color !== undefined) {
diagramObject.color = updates.color;
}
if (updates.position !== undefined) {
diagramObject.position = updates.position;
}
if (updates.scale !== undefined) {
diagramObject.scale = updates.scale;
}
if (updates.rotation !== undefined) {
diagramObject.rotation = updates.rotation;
}
}
} else {
this._logger.warn('chatModifyEntity: entity not found:', target);
}
});
document.addEventListener('chatModifyConnection', (event: CustomEvent) => {
const {target, updates} = event.detail;
this._logger.debug('chatModifyConnection', target, updates);
let connection: DiagramEntity | undefined;
// Check if target is a connection:fromId:toId format
if (target.startsWith('connection:')) {
const parts = target.split(':');
if (parts.length === 3) {
const fromId = parts[1];
const toId = parts[2];
// Find connection by from/to
connection = Array.from(this._diagramObjects.values())
.map(obj => obj.diagramEntity)
.find(e => e.template === '#connection-template' && e.from === fromId && e.to === toId);
}
} else {
// Find by label (text)
connection = this.findEntityByIdOrLabel(target);
// Verify it's a connection
if (connection && connection.template !== '#connection-template') {
this._logger.warn('chatModifyConnection: found entity is not a connection:', target);
connection = undefined;
}
}
if (connection) {
const diagramObject = this._diagramObjects.get(connection.id);
if (diagramObject) {
if (updates.text !== undefined) {
diagramObject.text = updates.text;
}
if (updates.color !== undefined) {
diagramObject.color = updates.color;
}
}
} else {
this._logger.warn('chatModifyConnection: connection not found:', target);
}
});
document.addEventListener('chatListEntities', () => {
this._logger.debug('chatListEntities');
const entities = Array.from(this._diagramObjects.values()).map(obj => ({
id: obj.diagramEntity.id,
template: obj.diagramEntity.template,
text: obj.diagramEntity.text || '',
color: obj.diagramEntity.color,
position: obj.diagramEntity.position
}));
const responseEvent = new CustomEvent('chatListEntitiesResponse', {
detail: {entities},
bubbles: true
});
document.dispatchEvent(responseEvent);
});
// Resolve entity label/ID to actual entity ID and label
document.addEventListener('chatResolveEntity', (event: CustomEvent) => {
const {target, requestId} = event.detail;
this._logger.debug('chatResolveEntity', target);
const entity = this.findEntityByIdOrLabel(target);
const responseEvent = new CustomEvent('chatResolveEntityResponse', {
detail: {
requestId,
target,
entityId: entity?.id || null,
entityLabel: entity?.text || null,
found: !!entity
},
bubbles: true
});
document.dispatchEvent(responseEvent);
});
// Clear all entities from the diagram
document.addEventListener('chatClearDiagram', () => {
this._logger.debug('chatClearDiagram - removing all entities');
const entitiesToRemove = Array.from(this._diagramObjects.keys());
for (const id of entitiesToRemove) {
const diagramObject = this._diagramObjects.get(id);
if (diagramObject) {
const entity = diagramObject.diagramEntity;
diagramObject.dispose();
this._diagramObjects.delete(id);
this.onDiagramEventObservable.notifyObservers({
type: DiagramEventType.REMOVE,
entity: entity
}, DiagramEventObserverMask.TO_DB);
}
}
this._logger.debug(`Cleared ${entitiesToRemove.length} entities`);
});
// Get current camera position and orientation
// Camera may be parented to a platform, so we use world-space coordinates
document.addEventListener('chatGetCamera', () => {
this._logger.debug('chatGetCamera');
const camera = this._scene.activeCamera;
if (!camera) {
this._logger.warn('No active camera found');
return;
}
// World-space position (accounts for parent transforms)
const position = camera.globalPosition;
// World-space forward direction (where camera is looking)
const forward = camera.getForwardRay(1).direction;
// World up vector
const worldUp = new Vector3(0, 1, 0);
// Compute ground-projected forward (for intuitive forward/back movement)
// This ignores pitch so looking up/down doesn't affect horizontal movement
const groundForward = new Vector3(forward.x, 0, forward.z);
const groundForwardLength = groundForward.length();
if (groundForwardLength > 0.001) {
groundForward.scaleInPlace(1 / groundForwardLength);
} else {
// Looking straight up/down - use a fallback forward
groundForward.set(0, 0, -1);
}
// Compute right vector (perpendicular to groundForward in XZ plane)
// Right = Cross(groundForward, worldUp) gives left, so we negate or swap
const groundRight = Vector3.Cross(worldUp, groundForward).normalize();
const responseEvent = new CustomEvent('chatGetCameraResponse', {
detail: {
position: {x: position.x, y: position.y, z: position.z},
forward: {x: forward.x, y: forward.y, z: forward.z},
groundForward: {x: groundForward.x, y: groundForward.y, z: groundForward.z},
groundRight: {x: groundRight.x, y: groundRight.y, z: groundRight.z}
},
bubbles: true
});
document.dispatchEvent(responseEvent);
});
this._logger.debug("DiagramManager constructed"); this._logger.debug("DiagramManager constructed");
} }
public get actionManager(): AbstractActionManager { public get actionManager(): AbstractActionManager {
return this._diagramEntityActionManager; return this._diagramEntityActionManager;
} }
public get diagramMenuManager(): DiagramMenuManager {
return this._diagramMenuManager;
}
public getDiagramObject(id: string) { public getDiagramObject(id: string) {
return this._diagramObjects.get(id); return this._diagramObjects.get(id);
} }
@ -328,6 +116,11 @@ export class DiagramManager {
return this._diagramObjects.has(mesh?.id) return this._diagramObjects.has(mesh?.id)
} }
public get controllers(): Controllers {
return this._controllers;
}
public createCopy(id: string): DiagramObject { public createCopy(id: string): DiagramObject {
const diagramObject = this._diagramObjects.get(id); const diagramObject = this._diagramObjects.get(id);
if (!diagramObject) { if (!diagramObject) {
@ -342,72 +135,14 @@ export class DiagramManager {
this._diagramObjects.set(diagramObject.diagramEntity.id, diagramObject); this._diagramObjects.set(diagramObject.diagramEntity.id, diagramObject);
} }
public get config() { public get config(): AppConfig {
return appConfigInstance; return this._config;
} }
private findEntityByIdOrLabel(target: string): DiagramEntity | null {
// First try direct ID match
const byId = this._diagramObjects.get(target);
if (byId) {
return byId.diagramEntity;
}
// Then try label match (case-insensitive)
const targetLower = target.toLowerCase();
for (const [, obj] of this._diagramObjects) {
if (obj.diagramEntity.text?.toLowerCase() === targetLower) {
return obj.diagramEntity;
}
}
return null;
}
/**
* Generates a default label for an entity based on its color and shape.
* Format: "{color} {shape} {number}" e.g., "blue box 1", "red sphere 2"
* The number is determined by counting existing entities with the same prefix.
*/
private generateDefaultLabel(entity: DiagramEntity): string {
// Get color name from hex
const colorHex = entity.color?.toLowerCase() || '#0000ff';
const colorName = HEX_TO_COLOR_NAME[colorHex] || 'blue';
// Get shape name from template
const shapeName = TEMPLATE_TO_SHAPE[entity.template] || 'box';
// Create the prefix (e.g., "blue box")
const prefix = `${colorName} ${shapeName}`;
// Count existing entities with labels starting with this prefix
let maxNumber = 0;
for (const [, obj] of this._diagramObjects) {
const label = obj.diagramEntity.text?.toLowerCase() || '';
if (label.startsWith(prefix)) {
// Extract the number from the end of the label
const match = label.match(new RegExp(`^${prefix}\\s*(\\d+)$`));
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNumber) {
maxNumber = num;
}
}
}
}
// Return the next number in sequence
return `${prefix} ${maxNumber + 1}`;
}
private onDiagramEvent(event: DiagramEvent) { private onDiagramEvent(event: DiagramEvent) {
let diagramObject = this._diagramObjects.get(event?.entity?.id); let diagramObject = this._diagramObjects.get(event?.entity?.id);
switch (event.type) { switch (event.type) {
case DiagramEventType.CLEAR:
this._diagramObjects.forEach((value) => {
value.dispose();
});
this._diagramObjects.clear();
break;
case DiagramEventType.ADD: case DiagramEventType.ADD:
if (diagramObject) { if (diagramObject) {
diagramObject.fromDiagramEntity(event.entity); diagramObject.fromDiagramEntity(event.entity);

View File

@ -1,52 +1,49 @@
import {DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity"; import {DiagramEvent, DiagramEventType} from "./types/diagramEntity";
import {AbstractMesh, ActionEvent, Observable, Scene, Vector3, WebXRDefaultExperience, WebXRInputSource} from "@babylonjs/core"; import {AbstractMesh, ActionEvent, Observable, Scene, Vector3, WebXRInputSource} from "@babylonjs/core";
import {InputTextView} from "../information/inputTextView"; import {InputTextView} from "../information/inputTextView";
import {DefaultScene} from "../defaultScene"; import {DefaultScene} from "../defaultScene";
import {ControllerEvent, ControllerEventType, Controllers} from "../controllers/controllers";
import log from "loglevel"; import log from "loglevel";
import {Toolbox} from "../toolbox/toolbox"; import {Toolbox} from "../toolbox/toolbox";
import {ClickMenu} from "../menus/clickMenu"; import {ClickMenu} from "../menus/clickMenu";
import {ConfigMenu} from "../menus/configMenu";
import {AppConfig} from "../util/appConfig";
import {DiagramEventObserverMask} from "./types/diagramEventObserverMask"; import {DiagramEventObserverMask} from "./types/diagramEventObserverMask";
import {ConnectionPreview} from "../menus/connectionPreview"; import {ConnectionPreview} from "../menus/connectionPreview";
import {ScaleMenu2} from "../menus/ScaleMenu2";
import {viewOnly} from "../util/functions/getPath"; import {viewOnly} from "../util/functions/getPath";
import {GroupMenu} from "../menus/groupMenu"; import {GroupMenu} from "../menus/groupMenu";
import {ControllerEvent} from "../controllers/types/controllerEvent";
import {ControllerEventType} from "../controllers/types/controllerEventType";
import {ResizeGizmo} from "../gizmos/ResizeGizmo";
import {VRConfigPanel} from "../menus/vrConfigPanel";
export class DiagramMenuManager { export class DiagramMenuManager {
public readonly toolbox: Toolbox; public readonly toolbox: Toolbox;
public readonly scaleMenu: ScaleMenu2;
public readonly configMenu: ConfigMenu;
private readonly _notifier: Observable<DiagramEvent>; private readonly _notifier: Observable<DiagramEvent>;
private readonly _inputTextView: InputTextView; private readonly _inputTextView: InputTextView;
private readonly _vrConfigPanel: VRConfigPanel;
private _groupMenu: GroupMenu; private _groupMenu: GroupMenu;
private readonly _scene: Scene; private readonly _scene: Scene;
private _logger = log.getLogger('DiagramMenuManager'); private _logger = log.getLogger('DiagramMenuManager');
private _connectionPreview: ConnectionPreview; private _connectionPreview: ConnectionPreview;
private _activeResizeGizmo: ResizeGizmo | null = null;
private _xr: WebXRDefaultExperience | null = null;
constructor(notifier: Observable<DiagramEvent>, controllerObservable: Observable<ControllerEvent>, readyObservable: Observable<boolean>) { constructor(notifier: Observable<DiagramEvent>, controllers: Controllers, config: AppConfig, readyObservable: Observable<boolean>) {
this._scene = DefaultScene.Scene; this._scene = DefaultScene.Scene;
this._notifier = notifier; this._notifier = notifier;
this._inputTextView = new InputTextView(controllerObservable); this._inputTextView = new InputTextView(controllers);
this._vrConfigPanel = new VRConfigPanel(this._scene); this.configMenu = new ConfigMenu(config);
//this.configMenu = new ConfigMenu(config);
this._inputTextView.onTextObservable.add((evt) => { this._inputTextView.onTextObservable.add((evt) => {
const event = { const event = {type: DiagramEventType.MODIFY, entity: {id: evt.id, text: evt.text}}
type: DiagramEventType.MODIFY,
entity: {id: evt.id, text: evt.text, type: DiagramEntityType.ENTITY}
}
this._notifier.notifyObservers(event, DiagramEventObserverMask.FROM_DB); this._notifier.notifyObservers(event, DiagramEventObserverMask.FROM_DB);
}); });
this.toolbox = new Toolbox(readyObservable); this.toolbox = new Toolbox(readyObservable);
this.scaleMenu = new ScaleMenu2(this._notifier);
if (viewOnly()) { if (viewOnly()) {
this.toolbox.handleMesh.setEnabled(false); this.toolbox.handleMesh.setEnabled(false);
//this.scaleMenu.handleMesh.setEnabled(false)
this.configMenu.handleTransformNode.setEnabled(false);
} }
controllerObservable.add((event: ControllerEvent) => { controllers.controllerObservable.add((event: ControllerEvent) => {
if (event.type == ControllerEventType.B_BUTTON) { if (event.type == ControllerEventType.B_BUTTON) {
if (event.value > .8) { if (event.value > .8) {
const platform = this._scene.getMeshByName("platform"); const platform = this._scene.getMeshByName("platform");
@ -65,10 +62,9 @@ export class DiagramMenuManager {
if (inputY > (cameraPos.y - .2)) { if (inputY > (cameraPos.y - .2)) {
this._inputTextView.handleMesh.position.y = localCamera.y - .2; this._inputTextView.handleMesh.position.y = localCamera.y - .2;
} }
const configY = this._inputTextView.handleMesh.absolutePosition.y;
const configY = this._vrConfigPanel.handleMesh.absolutePosition.y;
if (configY > (cameraPos.y - .2)) { if (configY > (cameraPos.y - .2)) {
this._vrConfigPanel.handleMesh.position.y = localCamera.y - .2; this.configMenu.handleTransformNode.position.y = localCamera.y - .2;
} }
} }
} }
@ -90,38 +86,6 @@ export class DiagramMenuManager {
this._inputTextView.show(mesh); this._inputTextView.show(mesh);
} }
public activateResizeGizmo(mesh: AbstractMesh) {
// Dispose existing gizmo if any
if (this._activeResizeGizmo) {
this._activeResizeGizmo.dispose();
this._activeResizeGizmo = null;
}
// XR must be available to create resize gizmo
if (!this._xr) {
this._logger.warn('Cannot activate resize gizmo: XR not initialized');
return;
}
// Create new resize gizmo for the mesh
this._activeResizeGizmo = new ResizeGizmo(mesh, this._xr);
// Listen for scale end event to notify diagram manager
this._activeResizeGizmo.onScaleEnd.add(() => {
this.notifyAll({
type: DiagramEventType.MODIFY,
entity: {id: mesh.id, type: DiagramEntityType.ENTITY}
});
});
}
public disposeResizeGizmo() {
if (this._activeResizeGizmo) {
this._activeResizeGizmo.dispose();
this._activeResizeGizmo = null;
}
}
public createClickMenu(mesh: AbstractMesh, input: WebXRInputSource): ClickMenu { public createClickMenu(mesh: AbstractMesh, input: WebXRInputSource): ClickMenu {
const clickMenu = new ClickMenu(mesh); const clickMenu = new ClickMenu(mesh);
clickMenu.onClickMenuObservable.add((evt: ActionEvent) => { clickMenu.onClickMenuObservable.add((evt: ActionEvent) => {
@ -129,10 +93,7 @@ export class DiagramMenuManager {
switch (evt.source.id) { switch (evt.source.id) {
case "remove": case "remove":
this.notifyAll({ this.notifyAll({type: DiagramEventType.REMOVE, entity: {id: clickMenu.mesh.id}});
type: DiagramEventType.REMOVE,
entity: {id: clickMenu.mesh.id, type: DiagramEntityType.ENTITY}
});
break; break;
case "label": case "label":
this.editText(clickMenu.mesh); this.editText(clickMenu.mesh);
@ -141,13 +102,13 @@ export class DiagramMenuManager {
this._connectionPreview = new ConnectionPreview(clickMenu.mesh.id, input, evt.additionalData.pickedPoint, this._notifier); this._connectionPreview = new ConnectionPreview(clickMenu.mesh.id, input, evt.additionalData.pickedPoint, this._notifier);
break; break;
case "size": case "size":
this.activateResizeGizmo(clickMenu.mesh); this.scaleMenu.show(clickMenu.mesh);
break; break;
case "group": case "group":
this._groupMenu = new GroupMenu(clickMenu.mesh); this._groupMenu = new GroupMenu(clickMenu.mesh);
break; break;
case "close": case "close":
this.disposeResizeGizmo(); this.scaleMenu.hide();
break; break;
} }
this._logger.debug(evt); this._logger.debug(evt);
@ -160,19 +121,4 @@ export class DiagramMenuManager {
private notifyAll(event: DiagramEvent) { private notifyAll(event: DiagramEvent) {
this._notifier.notifyObservers(event, DiagramEventObserverMask.ALL); this._notifier.notifyObservers(event, DiagramEventObserverMask.ALL);
} }
public setXR(xr: WebXRDefaultExperience): void {
this._xr = xr;
this.toolbox.setXR(xr, this);
}
public toggleVRConfigPanel(): void {
// Toggle visibility of VR config panel
const isEnabled = this._vrConfigPanel.handleMesh.isEnabled(false);
if (isEnabled) {
this._vrConfigPanel.hide();
} else {
this._vrConfigPanel.show();
}
}
} }

View File

@ -1,7 +1,6 @@
import { import {
AbstractActionManager, AbstractActionManager,
AbstractMesh, AbstractMesh,
Color3,
Curve3, Curve3,
GreasedLineMesh, GreasedLineMesh,
InstancedMesh, InstancedMesh,
@ -10,11 +9,10 @@ import {
Observer, Observer,
Ray, Ray,
Scene, Scene,
StandardMaterial,
TransformNode, TransformNode,
Vector3 Vector3
} from "@babylonjs/core"; } from "@babylonjs/core";
import {DiagramEntity, DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity"; import {DiagramEntity, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
import {buildMeshFromDiagramEntity} from "./functions/buildMeshFromDiagramEntity"; import {buildMeshFromDiagramEntity} from "./functions/buildMeshFromDiagramEntity";
import {toDiagramEntity} from "./functions/toDiagramEntity"; import {toDiagramEntity} from "./functions/toDiagramEntity";
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
@ -22,22 +20,6 @@ import {createLabel} from "./functions/createLabel";
import {DiagramEventObserverMask} from "./types/diagramEventObserverMask"; import {DiagramEventObserverMask} from "./types/diagramEventObserverMask";
import log, {Logger} from "loglevel"; import log, {Logger} from "loglevel";
import {xyztovec} from "./functions/vectorConversion"; import {xyztovec} from "./functions/vectorConversion";
import {AnimatedLineTexture} from "../util/animatedLineTexture";
import {getToolboxColors} from "../toolbox/toolbox";
import {findClosestColor} from "../util/functions/findClosestColor";
import {appConfigInstance} from "../util/appConfig";
/**
* Converts a Color3 to a hex color string
* @param color - BabylonJS Color3
* @returns Hex color string (e.g., '#ff0000')
*/
function color3ToHex(color: Color3): string {
const r = Math.floor(color.r * 255).toString(16).padStart(2, '0');
const g = Math.floor(color.g * 255).toString(16).padStart(2, '0');
const b = Math.floor(color.b * 255).toString(16).padStart(2, '0');
return `#${r}${g}${b}`;
}
type DiagramObjectOptionsType = { type DiagramObjectOptionsType = {
diagramEntity?: DiagramEntity, diagramEntity?: DiagramEntity,
@ -47,7 +29,6 @@ type DiagramObjectOptionsType = {
export class DiagramObject { export class DiagramObject {
private readonly _logger: Logger = log.getLogger('DiagramObject'); private readonly _logger: Logger = log.getLogger('DiagramObject');
private _group: TransformNode;
private _scene: Scene; private _scene: Scene;
public grabbed: boolean = false; public grabbed: boolean = false;
private _from: string; private _from: string;
@ -59,25 +40,16 @@ export class DiagramObject {
private _labelBack: InstancedMesh; private _labelBack: InstancedMesh;
private _meshesPresent: boolean = false; private _meshesPresent: boolean = false;
private _positionHash: string; private _positionHash: string;
private _fromPosition: number = 0;
private _toPosition: number = 0;
private _disposed: boolean = false; private _disposed: boolean = false;
private _fromMesh: AbstractMesh; private _fromMesh: AbstractMesh;
private _toMesh: AbstractMesh; private _toMesh: AbstractMesh;
private _meshRemovedObserver: Observer<AbstractMesh>; private _meshRemovedObserver: Observer<AbstractMesh>;
private _configObserver: Observer<any>;
// Position caching for connection optimization
private _lastFromPosition: Vector3 = null;
private _lastToPosition: Vector3 = null;
private _positionTolerance: number = 0.001;
constructor(scene: Scene, eventObservable: Observable<DiagramEvent>, options?: DiagramObjectOptionsType) { constructor(scene: Scene, eventObservable: Observable<DiagramEvent>, options?: DiagramObjectOptionsType) {
this._eventObservable = eventObservable; this._eventObservable = eventObservable;
this._scene = scene; this._scene = scene;
// Subscribe to config changes to update label rendering mode
this._configObserver = appConfigInstance.onConfigChangedObservable.add(() => {
this.updateLabelRenderingMode();
});
if (options) { if (options) {
this._logger.debug('DiagramObject constructor called with options', options); this._logger.debug('DiagramObject constructor called with options', options);
if (options.diagramEntity) { if (options.diagramEntity) {
@ -142,84 +114,6 @@ export class DiagramObject {
return this._diagramEntity; return this._diagramEntity;
} }
public set position(value: { x: number; y: number; z: number }) {
if (this._baseTransform) {
this._baseTransform.position = new Vector3(value.x, value.y, value.z);
if (this._diagramEntity) {
this._diagramEntity.position = value;
}
this._eventObservable.notifyObservers({
type: DiagramEventType.MODIFY,
entity: this._diagramEntity
}, DiagramEventObserverMask.TO_DB);
}
}
public set scale(value: { x: number; y: number; z: number }) {
if (this._mesh) {
this._mesh.scaling = new Vector3(value.x, value.y, value.z);
if (this._diagramEntity) {
this._diagramEntity.scale = value;
}
this._eventObservable.notifyObservers({
type: DiagramEventType.MODIFY,
entity: this._diagramEntity
}, DiagramEventObserverMask.TO_DB);
// Update label position since entity size changed
this.updateLabelPosition();
}
}
public set rotation(value: { x: number; y: number; z: number }) {
if (this._baseTransform) {
this._baseTransform.rotation = new Vector3(value.x, value.y, value.z);
if (this._diagramEntity) {
this._diagramEntity.rotation = value;
}
this._eventObservable.notifyObservers({
type: DiagramEventType.MODIFY,
entity: this._diagramEntity
}, DiagramEventObserverMask.TO_DB);
}
}
public set color(value: string) {
if (!this._diagramEntity || this._diagramEntity.color === value) {
return;
}
this._logger.debug('Changing color from', this._diagramEntity.color, 'to', value);
// Update the entity color
this._diagramEntity.color = value;
// Rebuild mesh with new color (since instances share materials)
// Must dispose old mesh FIRST, otherwise buildMeshFromDiagramEntity
// finds it by ID and returns the same mesh (which we then dispose!)
if (this._mesh) {
const actionManager = this._mesh.actionManager;
this._mesh.dispose();
this._mesh = null;
this._mesh = buildMeshFromDiagramEntity(this._diagramEntity, this._scene);
if (this._mesh) {
this._mesh.setParent(this._baseTransform);
this._mesh.position = Vector3.Zero();
this._mesh.rotation = Vector3.Zero();
if (actionManager) {
this._mesh.actionManager = actionManager;
}
} else {
this._logger.error('Failed to rebuild mesh with new color');
}
}
this._eventObservable.notifyObservers({
type: DiagramEventType.MODIFY,
entity: this._diagramEntity
}, DiagramEventObserverMask.TO_DB);
}
public set text(value: string) { public set text(value: string) {
if (this._label) { if (this._label) {
this._label.dispose(); this._label.dispose();
@ -227,106 +121,50 @@ export class DiagramObject {
if (this._labelBack) { if (this._labelBack) {
this._labelBack.dispose(); this._labelBack.dispose();
} }
const textChanged = this._diagramEntity.text != value; if (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({ this._eventObservable.notifyObservers({
type: DiagramEventType.MODIFY, type: DiagramEventType.MODIFY,
entity: this._diagramEntity entity: this._diagramEntity
}, DiagramEventObserverMask.TO_DB); }, DiagramEventObserverMask.TO_DB);
} }
this._diagramEntity.text = value;
this._label = createLabel(value); this._label = createLabel(value);
this._label.parent = this._baseTransform; this._label.parent = this._baseTransform;
this._labelBack = new InstancedMesh('labelBack' + value, (this._label as Mesh)); this._labelBack = new InstancedMesh('labelBack' + value, (this._label as Mesh));
this._labelBack.parent = this._label; this._labelBack.parent = this._label;
this._labelBack.metadata = {exportable: true}; this._labelBack.metadata = {exportable: true};
this.updateLabelPosition(); this.updateLabelPosition();
this.updateLabelRenderingMode();
}
private updateLabelRenderingMode() {
if (!this._label) {
return;
}
const mode = appConfigInstance.current.labelRenderingMode || 'billboard';
// Reset billboard mode first
this._label.billboardMode = Mesh.BILLBOARDMODE_NONE;
if (this._labelBack) {
this._labelBack.billboardMode = Mesh.BILLBOARDMODE_NONE;
}
switch (mode) {
case 'billboard':
// Billboard mode - labels always face camera (Y-axis only to prevent tilting)
this._label.billboardMode = Mesh.BILLBOARDMODE_Y;
if (this._labelBack) {
this._labelBack.billboardMode = Mesh.BILLBOARDMODE_Y;
}
break;
case 'fixed':
// Fixed mode - no billboard (default state, already set above)
break;
case 'dynamic':
// Dynamic mode - to be implemented in future
// TODO: Implement screen-space positioning
this._logger.warn('Dynamic label rendering mode not yet implemented');
break;
case 'distance':
// Distance-based mode - to be implemented in future
// TODO: Implement distance-based offset
this._logger.warn('Distance-based label rendering mode not yet implemented');
break;
}
// Update label position/rotation based on new mode (connections need different rotation in billboard vs fixed)
this.updateLabelPosition();
} }
public updateLabelPosition() { public updateLabelPosition() {
if (this._label) { if (this._label) {
this._mesh.computeWorldMatrix(true); this._mesh.computeWorldMatrix(true);
this._mesh.refreshBoundingInfo({}); this._mesh.refreshBoundingInfo({});
const isBillboard = (appConfigInstance.current.labelRenderingMode || 'billboard') === 'billboard';
if (this._from && this._to) { if (this._from && this._to) {
// Connection labels (arrows/lines) //this._label.position.x = .06;
//this._label.position.z = .06;
this._label.position.y = .05; this._label.position.y = .05;
// Only set local rotation when NOT in billboard mode this._label.rotation.y = Math.PI / 2;
// Billboard mode handles rotation automatically - setting local rotation causes conflicts this._labelBack.rotation.y = Math.PI;
if (!isBillboard) { this._labelBack.position.z = 0.001
this._label.rotation.y = Math.PI / 2; //this._label.billboardMode = Mesh.BILLBOARDMODE_Y;
this._labelBack.rotation.y = Math.PI; //this._label.billboardMode = Mesh.BILLBOARDMODE_Y;
} else {
// Reset rotations for billboard mode
this._label.rotation.y = 0;
this._labelBack.rotation.y = Math.PI; // Back face still needs to be flipped
}
this._labelBack.position.z = 0.005;
} else { } else {
// Standard object labels - convert world space to parent's local space const top =
// This accounts for mesh scaling, which is not included in boundingBox.maximum this._mesh.getBoundingInfo().boundingBox.maximumWorld;
const top = this._mesh.getBoundingInfo().boundingBox.maximumWorld;
const temp = new TransformNode("temp", this._scene); const temp = new TransformNode("temp", this._scene);
temp.position = top; temp.position = top;
temp.setParent(this._baseTransform); temp.setParent(this._baseTransform);
const y = temp.position.y; const y = temp.position.y;
temp.dispose(); temp.dispose();
this._label.position.y = y + 0.06; this._label.position.y = y + .06;
//this._labelBack.position.y = y + .06;
this._labelBack.rotation.y = Math.PI; this._labelBack.rotation.y = Math.PI;
this._labelBack.position.z = 0.005; this._labelBack.position.z = 0.001
//this._label.billboardMode = Mesh.BILLBOARDMODE_Y;
} }
} }
} }
@ -339,7 +177,6 @@ export class DiagramObject {
position: oldEntity.position, position: oldEntity.position,
rotation: oldEntity.rotation, rotation: oldEntity.rotation,
scale: oldEntity.scale, scale: oldEntity.scale,
type: DiagramEntityType.ENTITY,
image: oldEntity.image, image: oldEntity.image,
template: oldEntity.template, template: oldEntity.template,
color: oldEntity.color, color: oldEntity.color,
@ -371,26 +208,28 @@ export class DiagramObject {
if (!this._meshRemovedObserver) { if (!this._meshRemovedObserver) {
this._meshRemovedObserver = this._scene.onMeshRemovedObservable.add((mesh) => { this._meshRemovedObserver = this._scene.onMeshRemovedObservable.add((mesh) => {
if (mesh && mesh.id) { if (mesh && mesh.id) {
// When an endpoint mesh is removed, don't immediately dispose the connection.
// Instead, clear the mesh references and reset the timer. The scene observer
// will try to re-find the meshes (handles entity modification where mesh is
// disposed and recreated with same ID). If meshes can't be found after
// timeout, the scene observer will dispose the connection.
switch (mesh.id) { switch (mesh.id) {
case this._from: case this._from:
this._fromMesh = null; this._fromMesh = null;
this._lastFromPosition = null;
this._meshesPresent = false; this._meshesPresent = false;
this._observingStart = Date.now(); // Reset timeout this._eventObservable.notifyObservers({
type: DiagramEventType.REMOVE,
entity: this._diagramEntity
}, DiagramEventObserverMask.ALL);
this.dispose();
break; break;
case this._to: case this._to:
this._toMesh = null; this._toMesh = null;
this._lastToPosition = null;
this._meshesPresent = false; this._meshesPresent = false;
this._observingStart = Date.now(); // Reset timeout this._eventObservable.notifyObservers({
break; type: DiagramEventType.REMOVE,
entity: this._diagramEntity
}, DiagramEventObserverMask.ALL);
this.dispose();
} }
} }
}, -1, false, this); }, -1, false, this);
} }
if (!this._sceneObserver) { if (!this._sceneObserver) {
@ -406,9 +245,6 @@ export class DiagramObject {
this._fromMesh = this._fromMesh || this._scene.getMeshById(this._from); this._fromMesh = this._fromMesh || this._scene.getMeshById(this._from);
this._toMesh = this._toMesh || this._scene.getMeshById(this._to); this._toMesh = this._toMesh || this._scene.getMeshById(this._to);
if (this._fromMesh && this._toMesh) { if (this._fromMesh && this._toMesh) {
// Reset cache to force initial update
this._lastFromPosition = null;
this._lastToPosition = null;
this.updateConnection(); this.updateConnection();
this._meshesPresent = true; this._meshesPresent = true;
} else { } else {
@ -458,8 +294,6 @@ export class DiagramObject {
this._logger.debug('DiagramObject dispose called for ', this._diagramEntity?.id) this._logger.debug('DiagramObject dispose called for ', this._diagramEntity?.id)
this._scene?.onAfterRenderObservable.remove(this._sceneObserver); this._scene?.onAfterRenderObservable.remove(this._sceneObserver);
this._sceneObserver = null; this._sceneObserver = null;
appConfigInstance?.onConfigChangedObservable.remove(this._configObserver);
this._configObserver = null;
this._mesh?.setParent(null); this._mesh?.setParent(null);
this._mesh?.dispose(true, false); this._mesh?.dispose(true, false);
this._mesh = null; this._mesh = null;
@ -470,37 +304,12 @@ export class DiagramObject {
this._scene = null; this._scene = null;
this._fromMesh = null; this._fromMesh = null;
this._toMesh = null; this._toMesh = null;
this._lastFromPosition = null;
this._lastToPosition = null;
this._scene?.onMeshRemovedObservable.remove(this._meshRemovedObserver); this._scene?.onMeshRemovedObservable.remove(this._meshRemovedObserver);
this._disposed = true; this._disposed = true;
} }
private hasConnectionMoved(): boolean {
if (!this._fromMesh || !this._toMesh) {
return false;
}
const currentFromPos = this._fromMesh.getAbsolutePosition();
const currentToPos = this._toMesh.getAbsolutePosition();
// First update - always consider it moved
if (this._lastFromPosition === null || this._lastToPosition === null) {
return true;
}
// Check if either endpoint has moved beyond tolerance
const fromMoved = Vector3.DistanceSquared(currentFromPos, this._lastFromPosition) >
(this._positionTolerance * this._positionTolerance);
const toMoved = Vector3.DistanceSquared(currentToPos, this._lastToPosition) >
(this._positionTolerance * this._positionTolerance);
return fromMoved || toMoved;
}
private updateConnection() { private updateConnection() {
// Early exit if positions haven't changed if (this._toMesh.absolutePosition.length() == this._toPosition && this._fromMesh.absolutePosition.length() == this._fromPosition) {
if (!this.hasConnectionMoved()) {
return; return;
} }
const curve: GreasedLineMesh = ((this._mesh as unknown) as GreasedLineMesh); const curve: GreasedLineMesh = ((this._mesh as unknown) as GreasedLineMesh);
@ -512,9 +321,6 @@ export class DiagramObject {
return false; return false;
} }
}); });
if (!hit || hit.length < 2) {
return; // No valid intersection found, skip update
}
if (hit[0].pickedMesh.id === this._to) { if (hit[0].pickedMesh.id === this._to) {
hit.reverse(); hit.reverse();
} }
@ -531,63 +337,10 @@ export class DiagramObject {
curve.setParent(null); curve.setParent(null);
curve.setPoints([p]); curve.setPoints([p]);
this._baseTransform.position = c.getPoints()[Math.floor(c.getPoints().length / 2)]; this._baseTransform.position = c.getPoints()[Math.floor(c.getPoints().length / 2)];
this._toPosition = this._toMesh.absolutePosition.length();
// Update connection texture color to match the "from" mesh using toolbox color this._fromPosition = this._fromMesh.absolutePosition.length();
let hexColor: string | null = null;
// Extract color using same priority system as toDiagramEntity
if (this._fromMesh.metadata?.color) {
// Priority 1: Explicit metadata color (most reliable)
hexColor = this._fromMesh.metadata.color;
} else if (this._fromMesh instanceof InstancedMesh && this._fromMesh.sourceMesh?.id) {
// Priority 2: Extract from tool mesh ID (e.g., "tool-#box-template-#FF0000")
const toolId = this._fromMesh.sourceMesh.id;
const parts = toolId.split('-');
if (parts.length >= 3 && parts[0] === 'tool') {
const color = parts.slice(2).join('-'); // Handle colors with dashes
if (color.startsWith('#')) {
hexColor = color.toLowerCase(); // Normalize to lowercase
}
}
} else {
// Priority 3: Fallback to material extraction
const fromMaterial = this._fromMesh.material as StandardMaterial;
if (fromMaterial) {
const fromColor = fromMaterial.diffuseColor || fromMaterial.emissiveColor || Color3.White();
hexColor = color3ToHex(fromColor);
}
}
if (hexColor) {
// Find the closest toolbox color
const availableColors = getToolboxColors();
const closestColor = findClosestColor(hexColor, availableColors);
// Get or create material
const material = curve.material as StandardMaterial;
if (material) {
// Check if we need to update the texture color
// Don't dispose cached textures - they're shared across connections!
const currentTextureName = material.emissiveTexture?.name || '';
const needsColorUpdate = !material.emissiveTexture ||
!currentTextureName.endsWith(closestColor);
if (needsColorUpdate) {
// Get cached texture for the new color (creates if needed)
const coloredTexture = AnimatedLineTexture.CreateColoredTexture(closestColor);
material.emissiveTexture = coloredTexture;
material.opacityTexture = coloredTexture;
}
// If color matches, keep existing texture reference (already correct)
}
}
// Update cached positions after successful update
this._lastFromPosition = this._fromMesh.getAbsolutePosition().clone();
this._lastToPosition = this._toMesh.getAbsolutePosition().clone();
curve.setParent(this._baseTransform); curve.setParent(this._baseTransform);
curve.setEnabled(true); curve.setEnabled(true);
console.log('done');
} }
} }

View File

@ -0,0 +1,47 @@
import {afterEach, describe, expect, it, vi} from 'vitest'
import {applyScaling} from './applyScaling'
import {Vector3} from "@babylonjs/core";
describe('applyScaling', () => {
afterEach(() => {
vi.restoreAllMocks();
})
it('should copy scaling', () => {
const oldMesh = {
scaling: {
clone: () => 'cloned'
}
}
const newMesh = {
scaling: null
}
applyScaling(oldMesh as any, newMesh as any, true, 0)
expect(newMesh.scaling).toBe('cloned')
})
it('scaling to be set to 1,1,1 if snap passed as null', () => {
const spy = vi.spyOn(Vector3, 'One');
//expect(spy).toHaveBeenCalledTimes(1);
const oldMesh = {
scaling: {}
}
const newMesh = {
scaling: null
}
applyScaling(oldMesh as any, newMesh as any, false, null)
expect(newMesh.scaling.x).toBe(1);
expect(newMesh.scaling.y).toBe(1);
expect(newMesh.scaling.z).toBe(1);
})
it('scaling to be set to 2,2,2 snap passed as Vector3(2,2,2)', () => {
const oldMesh = {
scaling: {}
}
const newMesh = {
scaling: new Vector3()
}
applyScaling(oldMesh as any, newMesh as any, false, 2)
expect(newMesh.scaling.x).toBe(2);
expect(newMesh.scaling.y).toBe(2);
expect(newMesh.scaling.z).toBe(2);
})
});

View File

@ -0,0 +1,16 @@
import {AbstractMesh, Vector3} from "@babylonjs/core";
export function applyScaling(oldMesh: AbstractMesh,
newMesh: AbstractMesh,
copy: boolean,
snap: number) {
if (copy) {
newMesh.scaling = oldMesh.scaling.clone();
} else {
if (snap) {
newMesh.scaling.set(snap, snap, snap);
} else {
newMesh.scaling = Vector3.One();
}
}
}

View File

@ -1,16 +1,14 @@
import { import {ActionManager, ExecuteCodeAction, HighlightLayer, InstancedMesh, StandardMaterial,} from "@babylonjs/core";
ActionManager, import {ControllerEventType, Controllers} from "../../controllers/controllers";
Color4,
ExecuteCodeAction,
InstancedMesh,
Observable,
} from "@babylonjs/core";
import log from "loglevel"; import log from "loglevel";
import {DefaultScene} from "../../defaultScene"; import {DefaultScene} from "../../defaultScene";
import {ControllerEventType} from "../../controllers/types/controllerEventType";
import {ControllerEvent} from "../../controllers/types/controllerEvent";
export function buildEntityActionManager(controllerObservable: Observable<ControllerEvent>) { export function buildEntityActionManager(controllers: Controllers) {
const highlightLayer = new HighlightLayer('highlightLayer', DefaultScene.Scene);
highlightLayer.innerGlow = false;
highlightLayer.outerGlow = true;
const logger = log.getLogger('buildEntityActionManager'); const logger = log.getLogger('buildEntityActionManager');
const actionManager = new ActionManager(DefaultScene.Scene); const actionManager = new ActionManager(DefaultScene.Scene);
/*actionManager.registerAction( /*actionManager.registerAction(
@ -20,31 +18,39 @@ export function buildEntityActionManager(controllerObservable: Observable<Contro
if (evt.meshUnderPointer) { if (evt.meshUnderPointer) {
try { try {
const mesh = evt.meshUnderPointer as InstancedMesh; const mesh = evt.meshUnderPointer as InstancedMesh;
//mesh.sourceMesh.renderOutline = true;
// Enable edges rendering on the instance itself (not source mesh) if (mesh.sourceMesh) {
if (!mesh.edgesRenderer) { const newMesh = mesh.sourceMesh.clone(mesh.sourceMesh.name + '_clone', null, true);
mesh.enableEdgesRendering(0.99); newMesh.metadata = {};
mesh.edgesWidth = .2; newMesh.parent = null;
mesh.edgesColor = new Color4(1, 1, 1, 1.0); newMesh.position = mesh.absolutePosition;
newMesh.rotationQuaternion = mesh.absoluteRotationQuaternion;
newMesh.scaling = mesh.scaling;
newMesh.setEnabled(true);
newMesh.isPickable = false;
highlightLayer.addMesh(newMesh, (mesh.sourceMesh.material as StandardMaterial).diffuseColor.multiplyByFloats(1.5, 1.5, 1.5));
highlightLayer.setEffectIntensity(newMesh, 1.2);
mesh.metadata.highlight = newMesh;
console.log(newMesh);
} }
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);
} }
} }
controllerObservable.notifyObservers({ controllers.controllerObservable.notifyObservers({
type: ControllerEventType.PULSE, type: ControllerEventType.PULSE,
gripId: evt?.additionalData?.pickResult?.gripTransform?.id gripId: evt?.additionalData?.pickResult?.gripTransform?.id
}); })
logger.debug(evt); logger.debug(evt);
}) })
); );
actionManager.registerAction( actionManager.registerAction(
new ExecuteCodeAction(ActionManager.OnPointerOutTrigger, (evt) => { new ExecuteCodeAction(ActionManager.OnPointerOutTrigger, (evt) => {
try { try {
const mesh = evt.source as InstancedMesh; const mesh = evt.source;
// Disable edges rendering on the instance itself if (mesh.metadata.highlight) {
if (mesh?.edgesRenderer) { mesh.metadata.highlight.dispose();
mesh.disableEdgesRendering(); mesh.metadata.highlight = null;
} }
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);

View File

@ -0,0 +1,73 @@
import {afterEach, describe, expect, it, vi} from 'vitest'
import {buildMeshFromDiagramEntity} from './buildMeshFromDiagramEntity'
import {DiagramEntityType} from "../types/diagramEntity";
import {Vector3} from "@babylonjs/core";
describe('buildMeshFromDiagramEntity', () => {
afterEach(() => {
vi.restoreAllMocks();
})
it('should return null if entity is null', () => {
const scene = {
getMeshById: () => null
}
const entity = buildMeshFromDiagramEntity(null, scene as any);
expect(entity).toBe(null);
});
it('should return existing mesh if id exists in scene', () => {
const material = 'material';
const scene = {
getMeshById: (id) => {
return {
id: id,
material: material
}
}
}
const dEntity = {
type: DiagramEntityType.USER,
}
const entity = buildMeshFromDiagramEntity(dEntity, scene as any);
expect(entity.material).toBe(material);
});
it('should generate new mesh if id is missing', () => {
vi.mock('../diagramConnection', () => {
const DiagramConnection = vi.fn();
DiagramConnection.prototype.mesh =
{
id: 'id',
material: 'material',
getChildren: vi.fn(),
getScene: vi.fn()
}
return {DiagramConnection}
});
const scene = {
getMeshById: () => {
return null;
},
}
const dEntity = {
type: DiagramEntityType.USER,
template: "#connection-template",
color: "$FF00FF",
position: {x: 1, y: 2, z: 3},
rotation: {x: 4, y: 5, z: 6},
scale: {x: 7, y: 8, z: 9},
text: 'new text'
}
const entity = buildMeshFromDiagramEntity(dEntity, scene as any);
expect(entity.id).toBe('id');
expect(entity.material).toBe('material');
expect(entity.position).toEqual(new Vector3(1, 2, 3));
expect(entity.rotation).toEqual(new Vector3(4, 5, 6));
expect(entity.scaling).toEqual(new Vector3(7, 8, 9));
expect(entity.metadata.text).toEqual('new text');
});
});

View File

@ -18,16 +18,6 @@ import log from "loglevel";
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
import {xyztovec} from "./vectorConversion"; import {xyztovec} from "./vectorConversion";
import {AnimatedLineTexture} from "../../util/animatedLineTexture"; import {AnimatedLineTexture} from "../../util/animatedLineTexture";
import {LightmapGenerator} from "../../util/lightmapGenerator";
import {getToolboxColors} from "../../toolbox/toolbox";
import {findClosestColor} from "../../util/functions/findClosestColor";
// Material sharing statistics
let materialStats = {
instancesCreated: 0,
materialsShared: 0,
materialsFallback: 0
};
export function buildMeshFromDiagramEntity(entity: DiagramEntity, scene: Scene): AbstractMesh { export function buildMeshFromDiagramEntity(entity: DiagramEntity, scene: Scene): AbstractMesh {
const logger = log.getLogger('buildMeshFromDiagramEntity'); const logger = log.getLogger('buildMeshFromDiagramEntity');
@ -82,7 +72,6 @@ function createNewInstanceIfNecessary(entity: DiagramEntity, scene: Scene): Abst
material.emissiveTexture = AnimatedLineTexture.Texture(); material.emissiveTexture = AnimatedLineTexture.Texture();
material.opacityTexture = AnimatedLineTexture.Texture(); material.opacityTexture = AnimatedLineTexture.Texture();
material.disableLighting = true; material.disableLighting = true;
material.metadata = { isConnection: true, preserveTextures: true }; // Preserve animated arrow textures
newMesh.setEnabled(false); newMesh.setEnabled(false);
break; break;
case DiagramTemplates.BOX: case DiagramTemplates.BOX:
@ -91,59 +80,12 @@ function createNewInstanceIfNecessary(entity: DiagramEntity, scene: Scene): Abst
case DiagramTemplates.CONE: case DiagramTemplates.CONE:
case DiagramTemplates.PLANE: case DiagramTemplates.PLANE:
case DiagramTemplates.PERSON: case DiagramTemplates.PERSON:
// Tool meshes are created with UPPERCASE hex codes (BabylonJS toHexString behavior) const toolMesh = scene.getMeshById("tool-" + entity.template + "-" + entity.color);
let toolMeshId = "tool-" + entity.template + "-" + entity.color?.toUpperCase();
let toolMesh = scene.getMeshById(toolMeshId);
// If exact color match not found, try to find closest color
if (!toolMesh && entity.color) {
const availableColors = getToolboxColors();
const closestColor = findClosestColor(entity.color, availableColors);
if (closestColor !== entity.color.toLowerCase()) {
logger.info(`Color ${entity.color} not found in toolbox, using closest match: ${closestColor}`);
// Tool IDs use uppercase hex codes
toolMeshId = "tool-" + entity.template + "-" + closestColor.toUpperCase();
toolMesh = scene.getMeshById(toolMeshId);
if (toolMesh) {
logger.info(`Successfully found tool mesh with closest color: ${toolMeshId}`);
} else {
logger.error(`Even with closest color, tool mesh not found: ${toolMeshId}`);
}
}
}
if (toolMesh && !oldMesh) { if (toolMesh && !oldMesh) {
// Verify tool mesh has material before creating instance
if (!toolMesh.material) {
logger.error(`Tool mesh ${toolMeshId} found but has no material! This should never happen.`);
logger.error(`Tool mesh state: enabled=${toolMesh.isEnabled()}, parent=${toolMesh.parent?.name}`);
// Don't create instance without material
break;
}
logger.debug(`Found tool mesh: ${toolMeshId}, material: ${toolMesh.material.id}`);
newMesh = new InstancedMesh(entity.id, (toolMesh as Mesh)); newMesh = new InstancedMesh(entity.id, (toolMesh as Mesh));
// InstancedMesh.material property delegates to sourceMesh.material automatically
logger.debug(`Created instance ${entity.id}, inherited material: ${newMesh.material?.id}`);
// Track material sharing statistics
materialStats.instancesCreated++;
if (newMesh.material) {
materialStats.materialsShared++;
}
// newMesh.metadata = {template: entity.template, exportable: true, tool: false}; // newMesh.metadata = {template: entity.template, exportable: true, tool: false};
} else { } else {
if (!toolMesh) { logger.warn('no tool mesh found for ' + entity.template + "-" + entity.color);
logger.warn(`No tool mesh found for ${toolMeshId}. Available tool meshes: ${
scene.meshes.filter(m => m.id.startsWith('tool-')).map(m => m.id).slice(0, 5).join(', ')
}...`);
}
if (oldMesh) {
logger.debug(`Skipping instance creation, mesh ${entity.id} already exists`);
}
} }
break; break;
default: default:
@ -159,11 +101,6 @@ function createNewInstanceIfNecessary(entity: DiagramEntity, scene: Scene): Abst
newMesh.metadata.tool = false; newMesh.metadata.tool = false;
} }
// Store color in metadata so it persists when entity is modified
if (entity.color) {
newMesh.metadata.color = entity.color;
}
} }
} }
return newMesh; return newMesh;
@ -175,7 +112,6 @@ function buildImage(entity: DiagramEntity, scene: Scene): AbstractMesh {
logger.debug("buildImage: entity is image"); logger.debug("buildImage: entity is image");
const plane = MeshBuilder.CreatePlane(entity.id, {size: 1}, scene); const plane = MeshBuilder.CreatePlane(entity.id, {size: 1}, scene);
const material = new StandardMaterial("planeMaterial", scene); const material = new StandardMaterial("planeMaterial", scene);
material.metadata = { isUI: true }; // Mark as UI to prevent rendering mode modifications
const image = new Image(); const image = new Image();
image.src = entity.image; image.src = entity.image;
material.emissiveTexture = new Texture(entity.image, scene); material.emissiveTexture = new Texture(entity.image, scene);
@ -225,31 +161,9 @@ function mapMetadata(entity: DiagramEntity, newMesh: AbstractMesh, scene: Scene)
/*if (entity.scale) { /*if (entity.scale) {
newMesh.scaling = xyztovec(entity.scale); newMesh.scaling = xyztovec(entity.scale);
}*/ }*/
// Material validation - InstancedMesh should automatically inherit material from source mesh
if (!newMesh.material && newMesh?.metadata?.template != "#object-template") { if (!newMesh.material && newMesh?.metadata?.template != "#object-template") {
logger.error(`MATERIAL SHARING FAILURE for mesh ${newMesh.id}:`); logger.warn("new material created, this shouldn't happen");
logger.error(` Template: ${newMesh.metadata?.template}`);
logger.error(` Color: ${entity.color}`);
logger.error(` Is InstancedMesh: ${newMesh instanceof InstancedMesh}`);
if (newMesh instanceof InstancedMesh) {
logger.error(` Source mesh: ${newMesh.sourceMesh?.id}`);
logger.error(` Source mesh material: ${newMesh.sourceMesh?.material?.id || 'MISSING'}`);
logger.error(` This indicates tool mesh was created without material!`);
}
// Create fallback material as last resort to prevent crashes
logger.warn(`Creating fallback material to prevent crash - this impacts performance!`);
newMesh.material = buildMissingMaterial("material-" + entity.id, scene, entity.color); newMesh.material = buildMissingMaterial("material-" + entity.id, scene, entity.color);
// Track fallback material creation
materialStats.materialsFallback++;
}
// Log material sharing statistics periodically
if (materialStats.instancesCreated > 0 && materialStats.instancesCreated % 10 === 0) {
const sharingRate = (materialStats.materialsShared / materialStats.instancesCreated * 100).toFixed(1);
logger.info(`Material Sharing Stats: ${materialStats.materialsShared}/${materialStats.instancesCreated} (${sharingRate}%), Fallbacks: ${materialStats.materialsFallback}`);
} }
if (entity.text) { if (entity.text) {
newMesh.metadata.text = entity.text; newMesh.metadata.text = entity.text;
@ -276,21 +190,9 @@ export function buildMissingMaterial(name: string, scene: Scene, color: string):
if (existingMaterial) { if (existingMaterial) {
return (existingMaterial as StandardMaterial); return (existingMaterial as StandardMaterial);
} }
const colorObj = Color3.FromHexString(color);
const newMaterial = new StandardMaterial(name, scene); const newMaterial = new StandardMaterial(name, scene);
newMaterial.id = name; newMaterial.id = name;
newMaterial.diffuseColor = Color3.FromHexString(color);
if (LightmapGenerator.ENABLED) {
// Lightmap as emissive texture (lighting illusion, no lighting calculations)
newMaterial.emissiveColor = colorObj;
newMaterial.emissiveTexture = LightmapGenerator.generateLightmapForColor(colorObj, scene);
newMaterial.disableLighting = true;
} else {
// Flat emissive-only rendering (no lighting illusion)
newMaterial.emissiveColor = colorObj;
newMaterial.disableLighting = true;
}
newMaterial.alpha = 1; newMaterial.alpha = 1;
return newMaterial; return newMaterial;
} }

View File

@ -32,12 +32,11 @@ function createDynamicTexture(text: string, font: string, DTWidth: number, DTHei
function createMaterial(dynamicTexture: DynamicTexture): Material { function createMaterial(dynamicTexture: DynamicTexture): Material {
const mat = new StandardMaterial("text-mat", DefaultScene.Scene); const mat = new StandardMaterial("text-mat", DefaultScene.Scene);
//mat.diffuseColor = Color3.Black(); //mat.diffuseColor = Color3.Black();
mat.disableLighting = true; mat.disableLighting = false;
mat.backFaceCulling = true; //mat.backFaceCulling = false;
mat.emissiveTexture = dynamicTexture; mat.emissiveTexture = dynamicTexture;
mat.diffuseTexture = dynamicTexture; mat.diffuseTexture = dynamicTexture;
mat.metadata = { exportable: true, isUI: true }; // Mark as UI to prevent rendering mode modifications mat.metadata = {exportable: true};
//mat.freeze(); //mat.freeze();
return mat; return mat;
} }

View File

@ -1,85 +0,0 @@
import {AbstractMesh, Vector3} from "@babylonjs/core";
import {DiagramManager} from "../diagramManager";
import {DiagramObject} from "../diagramObject";
import {MeshTypeEnum} from "../types/meshTypeEnum";
import {snapAll} from "../../controllers/functions/snapAll";
import {DiagramEvent, DiagramEventType} from "../types/diagramEntity";
import {DiagramEventObserverMask} from "../types/diagramEventObserverMask";
import {DefaultScene} from "../../defaultScene";
import {appConfigInstance} from "../../util/appConfig";
import {HandleConfig, Vec3} from "../../util/appConfigType";
export function dropMesh(mesh: AbstractMesh,
grabbedObject: DiagramObject,
pickPoint: Vector3,
grabbedMeshType: MeshTypeEnum,
diagramManager: DiagramManager): boolean {
if (!mesh) {
return false;
}
let dropped = false;
const diagramObject = grabbedObject;
switch (grabbedMeshType) {
case MeshTypeEnum.ENTITY:
if (diagramObject) {
diagramObject.baseTransform.setParent(null);
snapAll(grabbedObject.baseTransform, pickPoint);
diagramObject.mesh.computeWorldMatrix(true);
const event: DiagramEvent =
{
type: DiagramEventType.DROP,
entity: diagramObject.diagramEntity
}
diagramManager.onDiagramEventObservable.notifyObservers(event, DiagramEventObserverMask.ALL);
diagramObject.mesh.computeWorldMatrix(false);
diagramObject.grabbed = false;
dropped = true;
}
break;
case MeshTypeEnum.TOOL:
grabbedObject.baseTransform.setParent(null);
snapAll(grabbedObject.baseTransform, pickPoint);
diagramObject.mesh.computeWorldMatrix(true);
const event: DiagramEvent =
{
type: DiagramEventType.DROP,
entity: diagramObject.diagramEntity
}
diagramManager.onDiagramEventObservable.notifyObservers(event, DiagramEventObserverMask.ALL);
diagramObject.mesh.computeWorldMatrix(false);
grabbedObject.grabbed = false;
dropped = true;
break;
case MeshTypeEnum.HANDLE:
mesh.setParent(DefaultScene.Scene.getMeshByName("platform"));
// Get existing handle config or create new one
const existingConfig = appConfigInstance.getHandleConfig(mesh.id);
// Convert Vector3 to Vec3 for serialization
const position: Vec3 = {
x: mesh.position.x,
y: mesh.position.y,
z: mesh.position.z
};
const rotation: Vec3 = {
x: mesh.rotation.x,
y: mesh.rotation.y,
z: mesh.rotation.z
};
const handleConfig: HandleConfig = {
id: mesh.id,
label: existingConfig?.label || mesh.id, // Preserve label if exists
position: position,
rotation: rotation,
scale: existingConfig?.scale // Preserve scale if exists
};
// Save to AppConfig (which persists to localStorage)
appConfigInstance.setHandleConfig(handleConfig);
dropped = true;
break;
}
return dropped;
}

View File

@ -1,40 +0,0 @@
import {viewOnly} from "../../util/functions/getPath";
import {getMeshType} from "../../controllers/functions/getMeshType";
import {MeshTypeEnum} from "../types/meshTypeEnum";
import {grabAndClone} from "../../controllers/functions/grabAndClone";
import {AbstractMesh} from "@babylonjs/core";
import log from "loglevel";
import {DiagramManager} from "../diagramManager";
import {DiagramObject} from "../diagramObject";
export function grabMesh(mesh: AbstractMesh, diagramManager: DiagramManager, controllerMesh: AbstractMesh):
{ grabbedMesh: AbstractMesh | null, grabbedObject: DiagramObject | null, grabbedMeshType: MeshTypeEnum | null } {
const logger = log.getLogger('grabMesh');
if (!mesh || viewOnly()) {
return {grabbedMesh: null, grabbedObject: null, grabbedMeshType: null};
}
let grabbedMesh = mesh;
let grabbedObject: DiagramObject | null = null;
let grabbedMeshType = getMeshType(mesh, diagramManager);
//displayDebug(mesh);
logger.debug("grabbing " + mesh.id + " type " + grabbedMeshType);
switch (grabbedMeshType) {
case MeshTypeEnum.ENTITY:
const diagramObject = diagramManager.getDiagramObject(mesh.id);
if (diagramObject.isGrabbable) {
diagramObject.baseTransform.setParent(controllerMesh);
diagramObject.grabbed = true;
grabbedObject = diagramObject;
}
break;
case MeshTypeEnum.HANDLE:
grabbedMesh.setParent(controllerMesh);
break;
case MeshTypeEnum.TOOL:
const clone = grabAndClone(diagramManager, mesh, controllerMesh);
grabbedObject = clone;
grabbedMesh = clone.mesh;
clone.grabbed = true;
}
return {grabbedMesh, grabbedObject, grabbedMeshType};
}

View File

@ -1,4 +1,4 @@
import {AbstractMesh, InstancedMesh} from "@babylonjs/core"; import {AbstractMesh} from "@babylonjs/core";
import {DiagramEntity} from "../types/diagramEntity"; import {DiagramEntity} from "../types/diagramEntity";
import log from "loglevel"; import log from "loglevel";
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
@ -25,44 +25,20 @@ export function toDiagramEntity(mesh: AbstractMesh): DiagramEntity {
entity.from = mesh?.metadata?.from; entity.from = mesh?.metadata?.from;
entity.to = mesh?.metadata?.to; entity.to = mesh?.metadata?.to;
entity.scale = vectoxys(mesh.scaling); entity.scale = vectoxys(mesh.scaling);
if (mesh.material) {
// Extract color using fallback chain for reliability
if (mesh.metadata?.color) {
// Priority 1: Explicit metadata color (most reliable)
entity.color = mesh.metadata.color;
} else if (mesh instanceof InstancedMesh && mesh.sourceMesh?.id) {
// Priority 2: Extract from tool mesh ID (e.g., "tool-BOX-#FF0000")
const toolId = mesh.sourceMesh.id;
const parts = toolId.split('-');
if (parts.length >= 3 && parts[0] === 'tool') {
const color = parts.slice(2).join('-'); // Handle colors with dashes
if (color.startsWith('#')) {
entity.color = color.toLowerCase(); // Normalize to lowercase
}
}
} else if (mesh.material) {
// Priority 3: Fallback to material extraction (backwards compatibility)
switch (mesh.material.getClassName()) { switch (mesh.material.getClassName()) {
case "StandardMaterial": case "StandardMaterial":
const stdMat = mesh.material as any; entity.color = (mesh.material as any).diffuseColor.toHexString();
const stdColor = stdMat.emissiveColor || stdMat.diffuseColor;
if (stdColor) {
entity.color = stdColor.toHexString()?.toLowerCase();
}
break; break;
case "PBRMaterial": case "PBRMaterial":
const pbrMat = mesh.material as any; entity.color = (mesh.material as any).albedoColor.toHexString();
const pbrColor = pbrMat.emissiveColor || pbrMat.albedoColor;
if (pbrColor) {
entity.color = pbrColor.toHexString()?.toLowerCase();
}
break; break;
} }
} else { } else {
if (entity.template != "#object-template") { if (entity.template != "#object-template") {
logger.error("toDiagramEntity: mesh.material is null"); logger.error("toDiagramEntity: mesh.material is null");
} }
}
}
return entity; return entity;
} }

View File

@ -0,0 +1,54 @@
import {PresentationStep} from "./presentationStep";
import log, {Logger} from "loglevel";
import {Scene} from "@babylonjs/core";
import {isDiagramEntity} from "./functions/isDiagramEntity";
export class PresentationManager {
_currentStep: PresentationStep = null;
private scene: Scene;
private logger: Logger = log.getLogger("PresentationManager");
constructor(scene: Scene) {
this.scene = scene;
}
_steps: PresentationStep[] = [];
public get steps(): PresentationStep[] {
return this._steps;
}
public addStep(): PresentationStep {
const step = new PresentationStep();
this._currentStep = step;
if (this._steps.length > 0) {
this._steps[this._steps.length - 1].next = step;
} else {
this.scene.getActiveMeshes().forEach((mesh) => {
if (isDiagramEntity(mesh)) {
step.entities.push({
entity: mesh,
endPosition: mesh.position.clone(),
endRotation: mesh.rotation.clone(),
endScaling: mesh.scaling.clone()
})
step.duration = 1;
}
});
}
this._steps.push(step);
return step;
}
public play() {
this._currentStep.play();
if (this._currentStep.next) {
this._currentStep = this._currentStep.next;
}
}
public reset() {
this._currentStep = this._steps[0];
this._steps[0].play();
}
}

Some files were not shown because too many files have changed in this diff Show More