Compare commits
No commits in common. "main" and "deepdiagram" have entirely different histories.
main
...
deepdiagra
75
.github/workflows/build.yml
vendored
@ -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
|
||||
4
.github/workflows/main.yml
vendored
@ -2,9 +2,9 @@ name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "deepdiagram" ]
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "deepdiagram" ]
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
4
.github/workflows/node.js.yml
vendored
@ -5,9 +5,9 @@ name: Node.js Github Side
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "deepdiagram" ]
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "deepdiagram" ]
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
2
.gitignore
vendored
@ -25,5 +25,3 @@ dist-ssr
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
/data/
|
||||
/.env.production
|
||||
|
||||
@ -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
@ -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.
|
||||
@ -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
|
||||
30
LICENSE.txt
@ -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.
|
||||
@ -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)
|
||||
224
ROADMAP.md
@ -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*
|
||||
150
SHARING_PLAN.md
@ -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
|
||||
179
SYNC_PLAN.md
@ -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
|
||||
297
VRCONFIGPLAN.md
@ -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
|
||||
@ -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
|
||||
95
index.html
@ -3,57 +3,92 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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">
|
||||
<meta content="width=device-width, initial-scale=1, height=device-height" name="viewport">
|
||||
<!--<link href="/styles.css" rel="stylesheet"> -->
|
||||
<link href="/assets/dasfad/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/dasfad/favicon-96x96.png" rel="icon" sizes="96x96" type="image/png">
|
||||
<link as="fetch" href="/node_modules/.vite/deps/HavokPhysics.wasm" rel="preload">
|
||||
<title>DASFAD</title>
|
||||
<!-- <link as="script" href="/newRelic.js" rel="preload">
|
||||
<script defer src="/newRelic.js"></script> -->
|
||||
|
||||
|
||||
<link href="/styles.css" rel="stylesheet">
|
||||
<link href="/assets/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png">
|
||||
<link href="/assets/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png">
|
||||
<link href="/assets/favicon-96x96.png" rel="icon" sizes="96x96" type="image/png">
|
||||
<title>Deep Diagram</title>
|
||||
<link as="script" href="/newRelic.js" rel="preload">
|
||||
<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"/>
|
||||
<!--<script src='/niceware.js'></script>-->
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<img id="loadingGrid" src="/assets/grid6.jpg"/>
|
||||
<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>
|
||||
<div class="webApp" id="webApp">
|
||||
|
||||
</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&xan=xtf9912b41c" type="audio/mpeg"></video> -->
|
||||
<!--
|
||||
<div class="scene">
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
</div>
|
||||
-->
|
||||
<!--<script defer src="/src/vrApp.ts" type="module"></script>-->
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
49
netlify/functions/nerdgraph/nerdgraph.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
};
|
||||
216
netlify/functions/users/users.mts
Normal 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}
|
||||
)
|
||||
}
|
||||
}
|
||||
22
netlify/functions/voice/voice.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
};
|
||||
48
newrelic.cjs
@ -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
58
package.json
@ -1,76 +1,56 @@
|
||||
{
|
||||
"name": "immersive",
|
||||
"private": true,
|
||||
"version": "0.0.8-48",
|
||||
"version": "0.0.8-16",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "node -r newrelic server.js",
|
||||
"dev": "vite",
|
||||
"test": "vitest",
|
||||
"build": "node versionBump.js && vite build",
|
||||
"start": "NODE_ENV=production node -r newrelic server.js",
|
||||
"start:api": "API_ONLY=true node -r newrelic server.js",
|
||||
"preview": "vite preview",
|
||||
"socket": "node server/server.js",
|
||||
"serve": "node server.js",
|
||||
"serverBuild": "cd server && tsc",
|
||||
"havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth0/auth0-react": "^2.2.4",
|
||||
"@babylonjs/core": "^8.16.2",
|
||||
"@babylonjs/gui": "^8.16.2",
|
||||
"@babylonjs/core": "^7.21.5",
|
||||
"@babylonjs/gui": "^7.21.5",
|
||||
"@babylonjs/havok": "1.3.4",
|
||||
"@babylonjs/inspector": "^8.16.2",
|
||||
"@babylonjs/loaders": "^8.16.2",
|
||||
"@babylonjs/materials": "^8.16.2",
|
||||
"@babylonjs/serializers": "^8.16.2",
|
||||
"@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",
|
||||
"@babylonjs/inspector": "^7.21.5",
|
||||
"@babylonjs/loaders": "^7.21.5",
|
||||
"@babylonjs/materials": "^7.21.5",
|
||||
"@babylonjs/serializers": "^7.21.5",
|
||||
"@maptiler/client": "1.8.1",
|
||||
"@newrelic/browser-agent": "^1.306.0",
|
||||
"@picovoice/cobra-web": "^2.0.3",
|
||||
"@picovoice/eagle-web": "^1.0.0",
|
||||
"@picovoice/web-voice-processor": "^4.0.9",
|
||||
"@tabler/icons-react": "^3.14.0",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/react": "^18.2.72",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"axios": "^1.10.0",
|
||||
"axios": "^1.6.8",
|
||||
"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",
|
||||
"js-crypto-aes": "1.0.6",
|
||||
"leveldown": "^6.1.1",
|
||||
"loglevel": "^1.9.1",
|
||||
"meaningful-string": "^1.4.0",
|
||||
"newrelic": "^13.9.1",
|
||||
"peer-lite": "2.0.2",
|
||||
"pouchdb": "^8.0.1",
|
||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||
"pouchdb-adapter-memory": "^9.0.0",
|
||||
"pouchdb-find": "^8.0.1",
|
||||
"query-string": "^8.1.0",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"recordrtc": "^5.6.0",
|
||||
"rfc4648": "^1.5.3",
|
||||
"round": "^2.0.1",
|
||||
"uint8-to-b64": "^1.0.2",
|
||||
"use-pouchdb": "^2.0.2",
|
||||
"uuid": "^9.0.1",
|
||||
"vite-express": "^0.21.1",
|
||||
"websocket": "^1.0.34",
|
||||
"websocket-ts": "^2.1.5"
|
||||
"js-crypto-aes": "1.0.6",
|
||||
"events": "^3.3.0",
|
||||
"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": {
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
|
||||
|
Before Width: | Height: | Size: 41 KiB |
@ -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 |
|
Before Width: | Height: | Size: 36 KiB |
@ -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
|
||||
@ -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
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 143 B |
|
Before Width: | Height: | Size: 1014 B |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 349 KiB |
|
Before Width: | Height: | Size: 86 KiB |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
|
Before Width: | Height: | Size: 679 B |
|
Before Width: | Height: | Size: 896 B |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
@ -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.
|
13
public/sw.js
@ -1,5 +1,5 @@
|
||||
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 IMAGEDELIVERY_CACHE = "deepdiagram-images";
|
||||
const MAPTILE_CACHE = 'maptiler';
|
||||
@ -66,35 +66,32 @@ workbox.routing.registerRoute(
|
||||
})
|
||||
);
|
||||
|
||||
/*workbox.routing.registerRoute(
|
||||
workbox.routing.registerRoute(
|
||||
new RegExp('/assets/.*'),
|
||||
new workbox.strategies.StaleWhileRevalidate({
|
||||
cacheName: CACHE
|
||||
})
|
||||
);
|
||||
*/
|
||||
|
||||
/*workbox.routing.registerRoute(
|
||||
|
||||
workbox.routing.registerRoute(
|
||||
new RegExp('/db/.*'),
|
||||
new workbox.strategies.StaleWhileRevalidate({
|
||||
cacheName: CACHE
|
||||
})
|
||||
);
|
||||
|
||||
*/
|
||||
|
||||
workbox.routing.registerRoute(
|
||||
new RegExp('/.*\\.glb'),
|
||||
new workbox.strategies.StaleWhileRevalidate({
|
||||
cacheName: CACHE
|
||||
})
|
||||
);
|
||||
/*
|
||||
workbox.routing.registerRoute(
|
||||
new RegExp('/.*\\.css'),
|
||||
new workbox.strategies.StaleWhileRevalidate({
|
||||
cacheName: CACHE
|
||||
})
|
||||
);
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@ -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
@ -1,133 +1,13 @@
|
||||
import express from "express";
|
||||
import ViteExpress from "vite-express";
|
||||
import cors from "cors";
|
||||
import dotenv from "dotenv";
|
||||
import newrelic from "newrelic";
|
||||
import apiRoutes from "./server/api/index.js";
|
||||
import { pouchApp, PouchDB } from "./server/services/databaseService.js";
|
||||
import { dbAuthMiddleware } from "./server/middleware/dbAuth.js";
|
||||
import expressProxy from "express-http-proxy";
|
||||
|
||||
// Load .env.local first, then fall back to .env
|
||||
dotenv.config({ path: '.env.local' });
|
||||
dotenv.config();
|
||||
|
||||
// Console shim to forward logs to New Relic while preserving local output
|
||||
const originalConsole = {
|
||||
log: console.log.bind(console),
|
||||
error: console.error.bind(console),
|
||||
warn: console.warn.bind(console),
|
||||
info: console.info.bind(console)
|
||||
};
|
||||
|
||||
function forwardToNewRelic(level, args) {
|
||||
const message = args.map(arg =>
|
||||
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
||||
).join(' ');
|
||||
|
||||
newrelic.recordLogEvent({
|
||||
message,
|
||||
level,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
console.log = (...args) => {
|
||||
forwardToNewRelic('info', args);
|
||||
originalConsole.log(...args);
|
||||
};
|
||||
|
||||
console.error = (...args) => {
|
||||
forwardToNewRelic('error', args);
|
||||
originalConsole.error(...args);
|
||||
};
|
||||
|
||||
console.warn = (...args) => {
|
||||
forwardToNewRelic('warn', args);
|
||||
originalConsole.warn(...args);
|
||||
};
|
||||
|
||||
console.info = (...args) => {
|
||||
forwardToNewRelic('info', args);
|
||||
originalConsole.info(...args);
|
||||
};
|
||||
|
||||
const app = express();
|
||||
app.use("/api", expressProxy("local.immersiveidea.com"));
|
||||
|
||||
// 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,
|
||||
}));
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
});
|
||||
}
|
||||
ViteExpress.listen(app, process.env.PORT || 3001, () => console.log("Server is listening..."));
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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-----
|
||||
@ -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
|
||||
@ -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-----
|
||||
@ -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>
|
||||
@ -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;
|
||||
@ -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 };
|
||||
@ -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
|
||||
};
|
||||
@ -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
|
||||
})
|
||||
}))
|
||||
};
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
@ -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(" | ");
|
||||
}
|
||||
@ -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',
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -7,40 +7,36 @@ import {
|
||||
WebXRInputSource
|
||||
} from "@babylonjs/core";
|
||||
import {DiagramManager} from "../diagram/diagramManager";
|
||||
import {DiagramEvent, DiagramEventType} from "../diagram/types/diagramEntity";
|
||||
import log from "loglevel";
|
||||
|
||||
import {ControllerEventType, Controllers} from "./controllers";
|
||||
import {grabAndClone} from "./functions/grabAndClone";
|
||||
import {ClickMenu} from "../menus/clickMenu";
|
||||
import {motionControllerInitObserver} from "./functions/motionControllerInitObserver";
|
||||
import {motionControllerObserver} from "./functions/motionControllerObserver";
|
||||
import {DefaultScene} from "../defaultScene";
|
||||
|
||||
import {DiagramEventObserverMask} from "../diagram/types/diagramEventObserverMask";
|
||||
import {DiagramObject} from "../diagram/diagramObject";
|
||||
import {snapAll} from "./functions/snapAll";
|
||||
import {MeshTypeEnum} from "../diagram/types/meshTypeEnum";
|
||||
import {getMeshType} from "./functions/getMeshType";
|
||||
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;
|
||||
|
||||
|
||||
export abstract class AbstractController {
|
||||
export class Base {
|
||||
static stickVector = Vector3.Zero();
|
||||
protected readonly scene: Scene;
|
||||
protected readonly xr: WebXRDefaultExperience;
|
||||
protected readonly diagramManager: DiagramManager;
|
||||
protected xrInputSource: WebXRInputSource;
|
||||
protected speedFactor = 4;
|
||||
|
||||
protected grabbedObject: DiagramObject = null;
|
||||
protected grabbedMesh: AbstractMesh = null;
|
||||
protected grabbedMeshType: MeshTypeEnum = null;
|
||||
protected controllers: Controllers;
|
||||
|
||||
|
||||
private readonly _logger = log.getLogger('AbstractController');
|
||||
private readonly _logger = log.getLogger('Base');
|
||||
private _clickStart: number = 0;
|
||||
private _clickMenu: ClickMenu;
|
||||
private _pickPoint: Vector3 = new Vector3();
|
||||
@ -52,6 +48,7 @@ export abstract class AbstractController {
|
||||
diagramManager: DiagramManager) {
|
||||
this._logger.debug('Base Controller Constructor called');
|
||||
this.xrInputSource = controller;
|
||||
this.controllers = diagramManager.controllers;
|
||||
this.scene = DefaultScene.Scene;
|
||||
this.xr = xr;
|
||||
|
||||
@ -68,8 +65,8 @@ export abstract class AbstractController {
|
||||
this.diagramManager = diagramManager;
|
||||
|
||||
//@TODO THis works, but it uses initGrip, not sure if this is the best idea
|
||||
this.xrInputSource.onMotionControllerInitObservable.add(motionControllerInitObserver, -1, false, this);
|
||||
controllerObservable.add((event) => {
|
||||
this.xrInputSource.onMotionControllerInitObservable.add(motionControllerObserver, -1, false, this);
|
||||
this.controllers.controllerObservable.add((event) => {
|
||||
this._logger.debug(event);
|
||||
switch (event.type) {
|
||||
case ControllerEventType.PULSE:
|
||||
@ -110,6 +107,10 @@ export abstract class AbstractController {
|
||||
}
|
||||
if (trigger.changes.pressed) {
|
||||
if (trigger.pressed) {
|
||||
if (this.diagramManager.diagramMenuManager.scaleMenu.mesh == this._meshUnderPointer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._clickStart == 0) {
|
||||
this._clickStart = Date.now();
|
||||
window.setTimeout(() => {
|
||||
@ -140,33 +141,97 @@ export abstract class AbstractController {
|
||||
}, -1, false, this);
|
||||
}
|
||||
|
||||
protected notifyObserver(value: number, controllerEventType: ControllerEventType): number {
|
||||
if (Math.abs(value) > .1) {
|
||||
controllerObservable.notifyObservers({
|
||||
type: controllerEventType,
|
||||
value: value * this.speedFactor
|
||||
});
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
|
||||
private grab() {
|
||||
let mesh = this._meshUnderPointer
|
||||
if (!mesh || viewOnly()) {
|
||||
return;
|
||||
}
|
||||
this.grabbedMesh = mesh;
|
||||
this.grabbedMeshType = getMeshType(mesh, this.diagramManager);
|
||||
|
||||
//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) {
|
||||
if (button) {
|
||||
button.onButtonStateChangedObservable.add((value) => {
|
||||
if (value.pressed) {
|
||||
this._logger.debug(button.type, button.id, 'pressed');
|
||||
controllerObservable.notifyObservers({type: type});
|
||||
private drop() {
|
||||
const mesh = this.grabbedMesh;
|
||||
if (!mesh) {
|
||||
return;
|
||||
}
|
||||
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() {
|
||||
let mesh = this.xr.pointerSelection.getMeshUnderPointer(this.xrInputSource.uniqueId);
|
||||
|
||||
if (mesh && this.diagramManager.isDiagramObject(mesh)) {
|
||||
if (this.diagramManager.isDiagramObject(mesh)) {
|
||||
this._logger.debug("click on " + mesh.id);
|
||||
if (this.diagramManager.diagramMenuManager.connectionPreview) {
|
||||
this.diagramManager.diagramMenuManager.connect(mesh);
|
||||
@ -186,37 +251,11 @@ export abstract class AbstractController {
|
||||
grip.onButtonStateChangedObservable.add(() => {
|
||||
if (grip.changes.pressed) {
|
||||
if (grip.pressed) {
|
||||
this._logger.debug("=== SQUEEZE PRESSED ===");
|
||||
this.grab();
|
||||
} else {
|
||||
this._logger.debug("=== SQUEEZE RELEASED ===");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,41 @@
|
||||
import {AbstractMesh, Observable, TransformNode} from "@babylonjs/core";
|
||||
import {ControllerEvent} from "./types/controllerEvent";
|
||||
import {AbstractMesh, Observable, TransformNode, Vector3, WebXRInputSource} from "@babylonjs/core";
|
||||
|
||||
export type ControllerEvent = {
|
||||
type: ControllerEventType,
|
||||
value?: number,
|
||||
startPosition?: Vector3,
|
||||
endPosition?: Vector3,
|
||||
duration?: number,
|
||||
gripId?: string;
|
||||
controller?: WebXRInputSource;
|
||||
}
|
||||
|
||||
export var movable: TransformNode | AbstractMesh;
|
||||
export const controllerObservable: Observable<ControllerEvent> = new Observable();
|
||||
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',
|
||||
}
|
||||
|
||||
export class Controllers {
|
||||
public movable: TransformNode | AbstractMesh;
|
||||
public readonly controllerObservable: Observable<ControllerEvent> = new Observable();
|
||||
}
|
||||
25
src/controllers/functions/beforeRenderObserver.ts
Normal 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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
12
src/controllers/functions/buildDrop.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@ -15,7 +15,6 @@ import {DefaultScene} from "../../defaultScene";
|
||||
export function buildRig(xr: WebXRDefaultExperience): Mesh {
|
||||
const scene = DefaultScene.Scene;
|
||||
const rigMesh = MeshBuilder.CreateCylinder("platform", {diameter: .5, height: .01}, scene);
|
||||
rigMesh.setAbsolutePosition(new Vector3(0, .01, 5));
|
||||
const cameratransform = new TransformNode("cameraTransform", scene);
|
||||
cameratransform.parent = rigMesh;
|
||||
xr.baseExperience.onInitialXRPoseSetObservable.add(() => {
|
||||
@ -25,45 +24,25 @@ export function buildRig(xr: WebXRDefaultExperience): Mesh {
|
||||
});
|
||||
for (const cam of scene.cameras) {
|
||||
cam.parent = cameratransform;
|
||||
cam.position = new Vector3(0, 1.6, 0); // Reset to local position above platform
|
||||
}
|
||||
scene.onActiveCameraChanged.add(() => {
|
||||
for (const cam of scene.cameras) {
|
||||
cam.parent = cameratransform;
|
||||
cam.position = new Vector3(0, 1.6, 0); // Reset to local position above platform
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
rigMesh.setAbsolutePosition(new Vector3(0, .01, 5));
|
||||
rigMesh.isPickable = false;
|
||||
const axis = new AxesViewer(scene, .25);
|
||||
axis.zAxis.rotation.y = Math.PI;
|
||||
rigMesh.lookAt(new Vector3(0, 0.01, 0));
|
||||
rigMesh.visibility = 1;
|
||||
|
||||
// Only create physics aggregate if physics engine is available
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const rigAggregate =
|
||||
new PhysicsAggregate(
|
||||
rigMesh,
|
||||
PhysicsShapeType.CYLINDER,
|
||||
{friction: 0, center: Vector3.Zero(), mass: 50, restitution: .01},
|
||||
scene);
|
||||
rigAggregate.body.setMotionType(PhysicsMotionType.DYNAMIC);
|
||||
return rigMesh;
|
||||
}
|
||||
|
||||
@ -3,8 +3,6 @@ import {DiagramManager} from "../../diagram/diagramManager";
|
||||
import {DiagramObject} from "../../diagram/diagramObject";
|
||||
import log from "loglevel";
|
||||
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):
|
||||
DiagramObject {
|
||||
@ -25,11 +23,10 @@ export function grabAndClone(diagramManager: DiagramManager, mesh: AbstractMesh,
|
||||
color: mesh.metadata.color,
|
||||
position: vectoxys(mesh.absolutePosition),
|
||||
rotation: vectoxys(mesh.absoluteRotationQuaternion.toEulerAngles()),
|
||||
scale: vectoxys(mesh.scaling),
|
||||
type: DiagramEntityType.ENTITY
|
||||
scale: vectoxys(mesh.scaling)
|
||||
|
||||
}
|
||||
const obj = new DiagramObject(DefaultScene.Scene,
|
||||
const obj = new DiagramObject(parent.getScene(),
|
||||
diagramManager.onDiagramEventObservable,
|
||||
{
|
||||
diagramEntity: entity,
|
||||
@ -38,5 +35,7 @@ export function grabAndClone(diagramManager: DiagramManager, mesh: AbstractMesh,
|
||||
obj.baseTransform.setParent(parent);
|
||||
diagramManager.addObject(obj);
|
||||
return obj;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ export function handleWasGrabbed(mesh: AbstractMesh): boolean {
|
||||
logger.debug("handleWasGrabbed: mesh is a diagram entity");
|
||||
return false;
|
||||
} else {
|
||||
|
||||
const result = (mesh?.metadata?.handle == true);
|
||||
logger.debug("handleWasGrabbed: mesh ", result);
|
||||
return result;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import log from "loglevel";
|
||||
|
||||
|
||||
export function motionControllerInitObserver(init) {
|
||||
export function motionControllerObserver(init) {
|
||||
const logger = log.getLogger('motionControllerObserver');
|
||||
logger.debug(init.components);
|
||||
if (init.components['xr-standard-squeeze']) {
|
||||
24
src/controllers/functions/reparent.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/controllers/functions/setupTransformNode.ts
Normal 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;
|
||||
}
|
||||
@ -1,24 +1,15 @@
|
||||
import {TransformNode, Vector3} from "@babylonjs/core";
|
||||
import {appConfigInstance} from "../../util/appConfig";
|
||||
import {AppConfig} from "../../util/appConfig";
|
||||
import {snapRotateVal} from "../../util/functions/snapRotateVal";
|
||||
import {snapGridVal} from "../../util/functions/snapGridVal";
|
||||
|
||||
export function snapAll(node: TransformNode, pickPoint: Vector3) {
|
||||
const config = appConfigInstance.current;
|
||||
export function snapAll(node: TransformNode, config: AppConfig, pickPoint: Vector3) {
|
||||
const transform = new TransformNode('temp', node.getScene());
|
||||
transform.position = pickPoint;
|
||||
node.setParent(transform);
|
||||
if (config.rotateSnap > 0) {
|
||||
node.rotation = snapRotateVal(node.absoluteRotationQuaternion.toEulerAngles(), config.rotateSnap);
|
||||
}
|
||||
if (config.locationSnap > 0) {
|
||||
transform.position = snapGridVal(transform.absolutePosition, config.locationSnap);
|
||||
}
|
||||
|
||||
node.rotation = snapRotateVal(node.absoluteRotationQuaternion.toEulerAngles(), config.current.rotateSnap);
|
||||
transform.position = snapGridVal(transform.absolutePosition, config.current.gridSnap);
|
||||
node.setParent(null);
|
||||
if (config.locationSnap > 0) {
|
||||
node.position = snapGridVal(node.absolutePosition, config.locationSnap);
|
||||
}
|
||||
|
||||
node.position = snapGridVal(node.absolutePosition, config.current.gridSnap);
|
||||
transform.dispose();
|
||||
}
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
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 {DiagramManager} from "../diagram/diagramManager";
|
||||
import {ControllerEventType} from "./types/controllerEventType";
|
||||
import {controllerObservable, movable} from "./controllers";
|
||||
import {DefaultScene} from "../defaultScene";
|
||||
|
||||
|
||||
export class LeftController extends AbstractController {
|
||||
export class Left extends Base {
|
||||
private leftLogger = log.getLogger('Left');
|
||||
constructor(controller:
|
||||
WebXRInputSource, xr: WebXRDefaultExperience, diagramManager: DiagramManager) {
|
||||
super(controller, xr, diagramManager);
|
||||
const scene = DefaultScene.Scene;
|
||||
this.xrInputSource.onMotionControllerInitObservable.add((init) => {
|
||||
if (init.components['xr-standard-thumbstick']) {
|
||||
init.components['xr-standard-thumbstick']
|
||||
.onAxisValueChangedObservable.add((value) => {
|
||||
this.leftLogger.trace(`thumbstick moved ${value.x}, ${value.y}`);
|
||||
if (!movable) {
|
||||
if (!this.controllers.movable) {
|
||||
this.moveRig(value);
|
||||
} else {
|
||||
this.moveMovable(value);
|
||||
@ -28,7 +29,7 @@ export class LeftController extends AbstractController {
|
||||
init.components['xr-standard-thumbstick'].onButtonStateChangedObservable.add((value) => {
|
||||
if (value.pressed) {
|
||||
this.leftLogger.trace('Left', 'thumbstick changed');
|
||||
controllerObservable.notifyObservers({
|
||||
this.controllers.controllerObservable.notifyObservers({
|
||||
type: ControllerEventType.DECREASE_VELOCITY,
|
||||
value: value.value
|
||||
});
|
||||
@ -45,7 +46,7 @@ export class LeftController extends AbstractController {
|
||||
.onButtonStateChangedObservable
|
||||
.add((button) => {
|
||||
this.leftLogger.trace('trigger pressed');
|
||||
controllerObservable.notifyObservers({
|
||||
this.controllers.controllerObservable.notifyObservers({
|
||||
type: ControllerEventType.TRIGGER,
|
||||
value: button.value,
|
||||
controller: this.xrInputSource
|
||||
@ -59,7 +60,7 @@ export class LeftController extends AbstractController {
|
||||
xbutton.onButtonStateChangedObservable.add((button) => {
|
||||
if (button.pressed) {
|
||||
this.leftLogger.trace('X button pressed');
|
||||
controllerObservable.notifyObservers({
|
||||
this.controllers.controllerObservable.notifyObservers({
|
||||
type: ControllerEventType.X_BUTTON,
|
||||
value: button.value
|
||||
});
|
||||
@ -73,7 +74,7 @@ export class LeftController extends AbstractController {
|
||||
ybutton.onButtonStateChangedObservable.add((button) => {
|
||||
if (button.pressed) {
|
||||
this.leftLogger.trace('Y button pressed');
|
||||
controllerObservable.notifyObservers({
|
||||
this.controllers.controllerObservable.notifyObservers({
|
||||
type: ControllerEventType.Y_BUTTON,
|
||||
value: button.value
|
||||
});
|
||||
@ -84,23 +85,42 @@ export class LeftController extends AbstractController {
|
||||
|
||||
private moveMovable(value: { x: number, y: number }) {
|
||||
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 {
|
||||
|
||||
}
|
||||
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 {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private moveRig(value: { x: number, y: number }) {
|
||||
AbstractController.stickVector.x = this.notifyObserver(value.x, ControllerEventType.LEFT_RIGHT);
|
||||
AbstractController.stickVector.y = this.notifyObserver(value.y, ControllerEventType.FORWARD_BACK);
|
||||
if (AbstractController.stickVector.equals(Vector3.Zero())) {
|
||||
controllerObservable.notifyObservers({type: ControllerEventType.LEFT_RIGHT, value: 0});
|
||||
controllerObservable.notifyObservers({type: ControllerEventType.FORWARD_BACK, value: 0});
|
||||
if (Math.abs(value.x) > .1) {
|
||||
this.controllers.controllerObservable.notifyObservers({
|
||||
type: ControllerEventType.LEFT_RIGHT,
|
||||
value: value.x * this.speedFactor
|
||||
});
|
||||
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
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,17 @@
|
||||
import {Angle, Mesh, Observer, Quaternion, Scene, Vector3, WebXRDefaultExperience} from "@babylonjs/core";
|
||||
import {RightController} from "./rightController";
|
||||
import {LeftController} from "./leftController";
|
||||
import {Angle, Mesh, Quaternion, Scene, Vector3, WebXRDefaultExperience} from "@babylonjs/core";
|
||||
import {Right} from "./right";
|
||||
import {Left} from "./left";
|
||||
import {ControllerEvent, ControllerEventType, Controllers} from "./controllers";
|
||||
import log from "loglevel";
|
||||
import {DiagramManager} from "../diagram/diagramManager";
|
||||
import {buildRig} from "./functions/buildRig";
|
||||
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 LEFT = "left";
|
||||
|
||||
|
||||
|
||||
export class Rigplatform {
|
||||
public static instance: Rigplatform;
|
||||
|
||||
@ -22,21 +19,20 @@ export class Rigplatform {
|
||||
public rigMesh: Mesh;
|
||||
|
||||
private _logger = log.getLogger('Rigplatform');
|
||||
|
||||
private readonly _controllers: Controllers;
|
||||
private readonly _diagramManager: DiagramManager;
|
||||
private readonly _scene: Scene;
|
||||
private readonly _velocityArray = [0.01, 0.1, 1, 2, 5];
|
||||
private readonly _xr: WebXRDefaultExperience;
|
||||
|
||||
private _rightController: RightController;
|
||||
private _leftController: LeftController;
|
||||
private _rightController: Right;
|
||||
private _leftController: Left;
|
||||
private _turning: boolean = false;
|
||||
private _velocity: Vector3 = Vector3.Zero();
|
||||
private _velocityIndex: number = 2;
|
||||
private _turnVelocity: number = 0;
|
||||
private _registered = false;
|
||||
private _yRotation: number = 0;
|
||||
private _configObserver: Observer<AppConfigType>;
|
||||
|
||||
constructor(
|
||||
xr: WebXRDefaultExperience,
|
||||
@ -44,34 +40,12 @@ export class Rigplatform {
|
||||
) {
|
||||
this._scene = DefaultScene.Scene;
|
||||
this._diagramManager = diagramManager;
|
||||
|
||||
this._controllers = diagramManager.controllers;
|
||||
this._xr = xr;
|
||||
this.rigMesh = buildRig(xr);
|
||||
// Exit XR button is now created in toolbox class
|
||||
|
||||
this._fixRotation();
|
||||
this._initializeControllers();
|
||||
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;
|
||||
@ -138,12 +112,12 @@ export class Rigplatform {
|
||||
private _registerObserver() {
|
||||
if (this._registered) {
|
||||
this._logger.warn('observer already registered, clearing and re registering');
|
||||
controllerObservable.clear();
|
||||
this._controllers.controllerObservable.clear();
|
||||
this._registered = false;
|
||||
}
|
||||
if (!this._registered) {
|
||||
this._registered = true;
|
||||
controllerObservable.add((event: ControllerEvent) => {
|
||||
this._controllers.controllerObservable.add((event: ControllerEvent) => {
|
||||
this._logger.debug(event);
|
||||
switch (event.type) {
|
||||
case ControllerEventType.INCREASE_VELOCITY:
|
||||
@ -191,12 +165,12 @@ export class Rigplatform {
|
||||
switch (source.inputSource.handedness) {
|
||||
case RIGHT:
|
||||
if (!this._rightController) {
|
||||
this._rightController = new RightController(source, this._xr, this._diagramManager);
|
||||
this._rightController = new Right(source, this._xr, this._diagramManager);
|
||||
}
|
||||
break;
|
||||
case LEFT:
|
||||
if (!this._leftController) {
|
||||
this._leftController = new LeftController(source, this._xr, this._diagramManager);
|
||||
this._leftController = new Left(source, this._xr, this._diagramManager);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -238,25 +212,4 @@ export class Rigplatform {
|
||||
}
|
||||
}, -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');
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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',
|
||||
}
|
||||
@ -1,12 +1,9 @@
|
||||
import {AbstractMesh, KeyboardEventTypes, Scene} from "@babylonjs/core";
|
||||
import {Rigplatform} from "./rigplatform";
|
||||
|
||||
import {Controllers} from "./controllers";
|
||||
import {DiagramManager} from "../diagram/diagramManager";
|
||||
import {wheelHandler} from "./functions/wheelHandler";
|
||||
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 {
|
||||
private readonly scene: Scene;
|
||||
@ -15,7 +12,7 @@ export class WebController {
|
||||
private rig: Rigplatform;
|
||||
private diagramManager: DiagramManager;
|
||||
private mouseDown: boolean = false;
|
||||
|
||||
private readonly controllers: Controllers;
|
||||
private upDownWheel: boolean = false;
|
||||
private fowardBackWheel: boolean = false;
|
||||
private canvas: HTMLCanvasElement;
|
||||
@ -23,11 +20,11 @@ export class WebController {
|
||||
constructor(scene: Scene,
|
||||
rig: Rigplatform,
|
||||
diagramManager: DiagramManager,
|
||||
) {
|
||||
controllers: Controllers) {
|
||||
this.scene = scene;
|
||||
this.rig = rig;
|
||||
this.diagramManager = diagramManager;
|
||||
|
||||
this.controllers = controllers;
|
||||
this.canvas = document.querySelector('#gameCanvas');
|
||||
//this.referencePlane = MeshBuilder.CreatePlane('referencePlane', {size: 10}, this.scene);
|
||||
//this.referencePlane.setEnabled(false);
|
||||
@ -97,18 +94,6 @@ export class WebController {
|
||||
|
||||
*/
|
||||
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:
|
||||
|
||||
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") {
|
||||
this.mouseDown = true;
|
||||
/*if (evt.shiftKey) {
|
||||
@ -255,57 +240,4 @@ export class WebController {
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
||||
@ -4,10 +4,6 @@ import log from "loglevel";
|
||||
|
||||
export class DefaultScene {
|
||||
private static _Scene: Scene;
|
||||
private static _UtilityScene: Scene;
|
||||
public static get UtilityScene(): Scene {
|
||||
return this._UtilityScene;
|
||||
}
|
||||
|
||||
public static get Scene(): Scene {
|
||||
if (!DefaultScene._Scene) {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import {AbstractActionManager, AbstractMesh, ActionManager, Observable, Scene, Vector3, WebXRDefaultExperience} from "@babylonjs/core";
|
||||
import {DiagramEntity, DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
|
||||
import {AbstractActionManager, AbstractMesh, ActionManager, Observable, Scene} from "@babylonjs/core";
|
||||
import {DiagramEntity, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
|
||||
import log from "loglevel";
|
||||
|
||||
import {appConfigInstance} from "../util/appConfig";
|
||||
import {Controllers} from "../controllers/controllers";
|
||||
import {AppConfig} from "../util/appConfig";
|
||||
import {buildEntityActionManager} from "./functions/buildEntityActionManager";
|
||||
import {DefaultScene} from "../defaultScene";
|
||||
import {DiagramMenuManager} from "./diagramMenuManager";
|
||||
@ -11,14 +11,12 @@ import {DiagramObject} from "./diagramObject";
|
||||
import {getMe} from "../util/me";
|
||||
import {UserModelType} from "../users/userTypes";
|
||||
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 {
|
||||
private readonly _logger = log.getLogger('DiagramManager');
|
||||
private readonly _controllerObservable: Observable<ControllerEvent>;
|
||||
public readonly _config: AppConfig;
|
||||
private readonly _controllers: Controllers;
|
||||
private readonly _diagramEntityActionManager: ActionManager;
|
||||
public readonly onDiagramEventObservable: Observable<DiagramEvent> = new Observable();
|
||||
public readonly onUserEventObservable: Observable<UserModelType> = new Observable();
|
||||
@ -29,20 +27,16 @@ export class DiagramManager {
|
||||
private _moving: number = 10;
|
||||
private _i: number = 0;
|
||||
|
||||
public get diagramMenuManager(): DiagramMenuManager {
|
||||
return this._diagramMenuManager;
|
||||
}
|
||||
|
||||
public setXR(xr: WebXRDefaultExperience): void {
|
||||
this._diagramMenuManager.setXR(xr);
|
||||
}
|
||||
|
||||
constructor(readyObservable: Observable<boolean>) {
|
||||
this._me = getMe();
|
||||
this._scene = DefaultScene.Scene;
|
||||
this._diagramMenuManager = new DiagramMenuManager(this.onDiagramEventObservable, controllerObservable, readyObservable);
|
||||
this._diagramEntityActionManager = buildEntityActionManager(controllerObservable);
|
||||
this._config = new AppConfig();
|
||||
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.onUserEventObservable.add((user) => {
|
||||
if (user.id != this._me) {
|
||||
this._logger.debug('user event', user);
|
||||
@ -86,7 +80,6 @@ export class DiagramManager {
|
||||
template: '#image-template',
|
||||
image: event.detail.data,
|
||||
text: event.detail.name,
|
||||
type: DiagramEntityType.ENTITY,
|
||||
position: {x: 0, y: 1.6, z: 0},
|
||||
rotation: {x: 0, y: Math.PI, z: 0},
|
||||
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");
|
||||
|
||||
}
|
||||
|
||||
public get actionManager(): AbstractActionManager {
|
||||
return this._diagramEntityActionManager;
|
||||
}
|
||||
|
||||
public get diagramMenuManager(): DiagramMenuManager {
|
||||
return this._diagramMenuManager;
|
||||
}
|
||||
|
||||
public getDiagramObject(id: string) {
|
||||
return this._diagramObjects.get(id);
|
||||
}
|
||||
@ -328,6 +116,11 @@ export class DiagramManager {
|
||||
return this._diagramObjects.has(mesh?.id)
|
||||
}
|
||||
|
||||
|
||||
public get controllers(): Controllers {
|
||||
return this._controllers;
|
||||
}
|
||||
|
||||
public createCopy(id: string): DiagramObject {
|
||||
const diagramObject = this._diagramObjects.get(id);
|
||||
if (!diagramObject) {
|
||||
@ -342,72 +135,14 @@ export class DiagramManager {
|
||||
this._diagramObjects.set(diagramObject.diagramEntity.id, diagramObject);
|
||||
}
|
||||
|
||||
public get config() {
|
||||
return appConfigInstance;
|
||||
public get config(): AppConfig {
|
||||
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) {
|
||||
let diagramObject = this._diagramObjects.get(event?.entity?.id);
|
||||
switch (event.type) {
|
||||
case DiagramEventType.CLEAR:
|
||||
this._diagramObjects.forEach((value) => {
|
||||
|
||||
value.dispose();
|
||||
});
|
||||
this._diagramObjects.clear();
|
||||
break;
|
||||
case DiagramEventType.ADD:
|
||||
if (diagramObject) {
|
||||
diagramObject.fromDiagramEntity(event.entity);
|
||||
|
||||
@ -1,52 +1,49 @@
|
||||
import {DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
|
||||
import {AbstractMesh, ActionEvent, Observable, Scene, Vector3, WebXRDefaultExperience, WebXRInputSource} from "@babylonjs/core";
|
||||
import {DiagramEvent, DiagramEventType} from "./types/diagramEntity";
|
||||
import {AbstractMesh, ActionEvent, Observable, Scene, Vector3, WebXRInputSource} from "@babylonjs/core";
|
||||
import {InputTextView} from "../information/inputTextView";
|
||||
import {DefaultScene} from "../defaultScene";
|
||||
import {ControllerEvent, ControllerEventType, Controllers} from "../controllers/controllers";
|
||||
import log from "loglevel";
|
||||
import {Toolbox} from "../toolbox/toolbox";
|
||||
import {ClickMenu} from "../menus/clickMenu";
|
||||
import {ConfigMenu} from "../menus/configMenu";
|
||||
import {AppConfig} from "../util/appConfig";
|
||||
import {DiagramEventObserverMask} from "./types/diagramEventObserverMask";
|
||||
import {ConnectionPreview} from "../menus/connectionPreview";
|
||||
import {ScaleMenu2} from "../menus/ScaleMenu2";
|
||||
import {viewOnly} from "../util/functions/getPath";
|
||||
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 {
|
||||
public readonly toolbox: Toolbox;
|
||||
public readonly scaleMenu: ScaleMenu2;
|
||||
public readonly configMenu: ConfigMenu;
|
||||
private readonly _notifier: Observable<DiagramEvent>;
|
||||
private readonly _inputTextView: InputTextView;
|
||||
private readonly _vrConfigPanel: VRConfigPanel;
|
||||
private _groupMenu: GroupMenu;
|
||||
private readonly _scene: Scene;
|
||||
private _logger = log.getLogger('DiagramMenuManager');
|
||||
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._notifier = notifier;
|
||||
this._inputTextView = new InputTextView(controllerObservable);
|
||||
this._vrConfigPanel = new VRConfigPanel(this._scene);
|
||||
//this.configMenu = new ConfigMenu(config);
|
||||
this._inputTextView = new InputTextView(controllers);
|
||||
this.configMenu = new ConfigMenu(config);
|
||||
|
||||
this._inputTextView.onTextObservable.add((evt) => {
|
||||
const event = {
|
||||
type: DiagramEventType.MODIFY,
|
||||
entity: {id: evt.id, text: evt.text, type: DiagramEntityType.ENTITY}
|
||||
}
|
||||
const event = {type: DiagramEventType.MODIFY, entity: {id: evt.id, text: evt.text}}
|
||||
this._notifier.notifyObservers(event, DiagramEventObserverMask.FROM_DB);
|
||||
});
|
||||
this.toolbox = new Toolbox(readyObservable);
|
||||
|
||||
this.scaleMenu = new ScaleMenu2(this._notifier);
|
||||
if (viewOnly()) {
|
||||
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.value > .8) {
|
||||
const platform = this._scene.getMeshByName("platform");
|
||||
@ -65,10 +62,9 @@ export class DiagramMenuManager {
|
||||
if (inputY > (cameraPos.y - .2)) {
|
||||
this._inputTextView.handleMesh.position.y = localCamera.y - .2;
|
||||
}
|
||||
|
||||
const configY = this._vrConfigPanel.handleMesh.absolutePosition.y;
|
||||
const configY = this._inputTextView.handleMesh.absolutePosition.y;
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
const clickMenu = new ClickMenu(mesh);
|
||||
clickMenu.onClickMenuObservable.add((evt: ActionEvent) => {
|
||||
@ -129,10 +93,7 @@ export class DiagramMenuManager {
|
||||
|
||||
switch (evt.source.id) {
|
||||
case "remove":
|
||||
this.notifyAll({
|
||||
type: DiagramEventType.REMOVE,
|
||||
entity: {id: clickMenu.mesh.id, type: DiagramEntityType.ENTITY}
|
||||
});
|
||||
this.notifyAll({type: DiagramEventType.REMOVE, entity: {id: clickMenu.mesh.id}});
|
||||
break;
|
||||
case "label":
|
||||
this.editText(clickMenu.mesh);
|
||||
@ -141,13 +102,13 @@ export class DiagramMenuManager {
|
||||
this._connectionPreview = new ConnectionPreview(clickMenu.mesh.id, input, evt.additionalData.pickedPoint, this._notifier);
|
||||
break;
|
||||
case "size":
|
||||
this.activateResizeGizmo(clickMenu.mesh);
|
||||
this.scaleMenu.show(clickMenu.mesh);
|
||||
break;
|
||||
case "group":
|
||||
this._groupMenu = new GroupMenu(clickMenu.mesh);
|
||||
break;
|
||||
case "close":
|
||||
this.disposeResizeGizmo();
|
||||
this.scaleMenu.hide();
|
||||
break;
|
||||
}
|
||||
this._logger.debug(evt);
|
||||
@ -160,19 +121,4 @@ export class DiagramMenuManager {
|
||||
private notifyAll(event: DiagramEvent) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import {
|
||||
AbstractActionManager,
|
||||
AbstractMesh,
|
||||
Color3,
|
||||
Curve3,
|
||||
GreasedLineMesh,
|
||||
InstancedMesh,
|
||||
@ -10,11 +9,10 @@ import {
|
||||
Observer,
|
||||
Ray,
|
||||
Scene,
|
||||
StandardMaterial,
|
||||
TransformNode,
|
||||
Vector3
|
||||
} from "@babylonjs/core";
|
||||
import {DiagramEntity, DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
|
||||
import {DiagramEntity, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
|
||||
import {buildMeshFromDiagramEntity} from "./functions/buildMeshFromDiagramEntity";
|
||||
import {toDiagramEntity} from "./functions/toDiagramEntity";
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
@ -22,22 +20,6 @@ import {createLabel} from "./functions/createLabel";
|
||||
import {DiagramEventObserverMask} from "./types/diagramEventObserverMask";
|
||||
import log, {Logger} from "loglevel";
|
||||
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 = {
|
||||
diagramEntity?: DiagramEntity,
|
||||
@ -47,7 +29,6 @@ type DiagramObjectOptionsType = {
|
||||
|
||||
export class DiagramObject {
|
||||
private readonly _logger: Logger = log.getLogger('DiagramObject');
|
||||
private _group: TransformNode;
|
||||
private _scene: Scene;
|
||||
public grabbed: boolean = false;
|
||||
private _from: string;
|
||||
@ -59,25 +40,16 @@ export class DiagramObject {
|
||||
private _labelBack: InstancedMesh;
|
||||
private _meshesPresent: boolean = false;
|
||||
private _positionHash: string;
|
||||
private _fromPosition: number = 0;
|
||||
private _toPosition: number = 0;
|
||||
private _disposed: boolean = false;
|
||||
private _fromMesh: AbstractMesh;
|
||||
private _toMesh: 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) {
|
||||
this._eventObservable = eventObservable;
|
||||
this._scene = scene;
|
||||
|
||||
// Subscribe to config changes to update label rendering mode
|
||||
this._configObserver = appConfigInstance.onConfigChangedObservable.add(() => {
|
||||
this.updateLabelRenderingMode();
|
||||
});
|
||||
|
||||
if (options) {
|
||||
this._logger.debug('DiagramObject constructor called with options', options);
|
||||
if (options.diagramEntity) {
|
||||
@ -142,84 +114,6 @@ export class DiagramObject {
|
||||
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) {
|
||||
if (this._label) {
|
||||
this._label.dispose();
|
||||
@ -227,106 +121,50 @@ export class DiagramObject {
|
||||
if (this._labelBack) {
|
||||
this._labelBack.dispose();
|
||||
}
|
||||
const textChanged = this._diagramEntity.text != value;
|
||||
|
||||
// Update the entity text FIRST (before notifying observers)
|
||||
this._diagramEntity.text = value;
|
||||
|
||||
// Also update mesh metadata to keep in sync with diagramEntity getter
|
||||
if (this._mesh && this._mesh.metadata) {
|
||||
this._mesh.metadata.text = value;
|
||||
}
|
||||
|
||||
// THEN notify observers with the UPDATED entity
|
||||
if (textChanged) {
|
||||
if (this._diagramEntity.text != value) {
|
||||
this._eventObservable.notifyObservers({
|
||||
type: DiagramEventType.MODIFY,
|
||||
entity: this._diagramEntity
|
||||
}, DiagramEventObserverMask.TO_DB);
|
||||
}
|
||||
|
||||
this._diagramEntity.text = value;
|
||||
this._label = createLabel(value);
|
||||
this._label.parent = this._baseTransform;
|
||||
this._labelBack = new InstancedMesh('labelBack' + value, (this._label as Mesh));
|
||||
this._labelBack.parent = this._label;
|
||||
this._labelBack.metadata = {exportable: true};
|
||||
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() {
|
||||
if (this._label) {
|
||||
this._mesh.computeWorldMatrix(true);
|
||||
this._mesh.refreshBoundingInfo({});
|
||||
|
||||
const isBillboard = (appConfigInstance.current.labelRenderingMode || 'billboard') === 'billboard';
|
||||
|
||||
if (this._from && this._to) {
|
||||
// Connection labels (arrows/lines)
|
||||
//this._label.position.x = .06;
|
||||
//this._label.position.z = .06;
|
||||
this._label.position.y = .05;
|
||||
// Only set local rotation when NOT in billboard mode
|
||||
// Billboard mode handles rotation automatically - setting local rotation causes conflicts
|
||||
if (!isBillboard) {
|
||||
this._label.rotation.y = Math.PI / 2;
|
||||
this._labelBack.rotation.y = Math.PI;
|
||||
} 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;
|
||||
this._label.rotation.y = Math.PI / 2;
|
||||
this._labelBack.rotation.y = Math.PI;
|
||||
this._labelBack.position.z = 0.001
|
||||
//this._label.billboardMode = Mesh.BILLBOARDMODE_Y;
|
||||
//this._label.billboardMode = Mesh.BILLBOARDMODE_Y;
|
||||
|
||||
} else {
|
||||
// Standard object labels - convert world space to parent's local space
|
||||
// This accounts for mesh scaling, which is not included in boundingBox.maximum
|
||||
const top = this._mesh.getBoundingInfo().boundingBox.maximumWorld;
|
||||
const top =
|
||||
this._mesh.getBoundingInfo().boundingBox.maximumWorld;
|
||||
const temp = new TransformNode("temp", this._scene);
|
||||
temp.position = top;
|
||||
temp.setParent(this._baseTransform);
|
||||
const y = temp.position.y;
|
||||
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.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,
|
||||
rotation: oldEntity.rotation,
|
||||
scale: oldEntity.scale,
|
||||
type: DiagramEntityType.ENTITY,
|
||||
image: oldEntity.image,
|
||||
template: oldEntity.template,
|
||||
color: oldEntity.color,
|
||||
@ -371,26 +208,28 @@ export class DiagramObject {
|
||||
if (!this._meshRemovedObserver) {
|
||||
this._meshRemovedObserver = this._scene.onMeshRemovedObservable.add((mesh) => {
|
||||
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) {
|
||||
case this._from:
|
||||
this._fromMesh = null;
|
||||
this._lastFromPosition = null;
|
||||
this._meshesPresent = false;
|
||||
this._observingStart = Date.now(); // Reset timeout
|
||||
this._eventObservable.notifyObservers({
|
||||
type: DiagramEventType.REMOVE,
|
||||
entity: this._diagramEntity
|
||||
}, DiagramEventObserverMask.ALL);
|
||||
this.dispose();
|
||||
break;
|
||||
case this._to:
|
||||
this._toMesh = null;
|
||||
this._lastToPosition = null;
|
||||
this._meshesPresent = false;
|
||||
this._observingStart = Date.now(); // Reset timeout
|
||||
break;
|
||||
this._eventObservable.notifyObservers({
|
||||
type: DiagramEventType.REMOVE,
|
||||
entity: this._diagramEntity
|
||||
}, DiagramEventObserverMask.ALL);
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}, -1, false, this);
|
||||
}
|
||||
if (!this._sceneObserver) {
|
||||
@ -406,9 +245,6 @@ export class DiagramObject {
|
||||
this._fromMesh = this._fromMesh || this._scene.getMeshById(this._from);
|
||||
this._toMesh = this._toMesh || this._scene.getMeshById(this._to);
|
||||
if (this._fromMesh && this._toMesh) {
|
||||
// Reset cache to force initial update
|
||||
this._lastFromPosition = null;
|
||||
this._lastToPosition = null;
|
||||
this.updateConnection();
|
||||
this._meshesPresent = true;
|
||||
} else {
|
||||
@ -458,8 +294,6 @@ export class DiagramObject {
|
||||
this._logger.debug('DiagramObject dispose called for ', this._diagramEntity?.id)
|
||||
this._scene?.onAfterRenderObservable.remove(this._sceneObserver);
|
||||
this._sceneObserver = null;
|
||||
appConfigInstance?.onConfigChangedObservable.remove(this._configObserver);
|
||||
this._configObserver = null;
|
||||
this._mesh?.setParent(null);
|
||||
this._mesh?.dispose(true, false);
|
||||
this._mesh = null;
|
||||
@ -470,37 +304,12 @@ export class DiagramObject {
|
||||
this._scene = null;
|
||||
this._fromMesh = null;
|
||||
this._toMesh = null;
|
||||
this._lastFromPosition = null;
|
||||
this._lastToPosition = null;
|
||||
this._scene?.onMeshRemovedObservable.remove(this._meshRemovedObserver);
|
||||
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() {
|
||||
// Early exit if positions haven't changed
|
||||
if (!this.hasConnectionMoved()) {
|
||||
if (this._toMesh.absolutePosition.length() == this._toPosition && this._fromMesh.absolutePosition.length() == this._fromPosition) {
|
||||
return;
|
||||
}
|
||||
const curve: GreasedLineMesh = ((this._mesh as unknown) as GreasedLineMesh);
|
||||
@ -512,9 +321,6 @@ export class DiagramObject {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!hit || hit.length < 2) {
|
||||
return; // No valid intersection found, skip update
|
||||
}
|
||||
if (hit[0].pickedMesh.id === this._to) {
|
||||
hit.reverse();
|
||||
}
|
||||
@ -531,63 +337,10 @@ export class DiagramObject {
|
||||
curve.setParent(null);
|
||||
curve.setPoints([p]);
|
||||
this._baseTransform.position = c.getPoints()[Math.floor(c.getPoints().length / 2)];
|
||||
|
||||
// Update connection texture color to match the "from" mesh using toolbox color
|
||||
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();
|
||||
|
||||
this._toPosition = this._toMesh.absolutePosition.length();
|
||||
this._fromPosition = this._fromMesh.absolutePosition.length();
|
||||
curve.setParent(this._baseTransform);
|
||||
curve.setEnabled(true);
|
||||
console.log('done');
|
||||
}
|
||||
}
|
||||
47
src/diagram/functions/applyScaling.test.ts
Normal 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);
|
||||
})
|
||||
});
|
||||
16
src/diagram/functions/applyScaling.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,14 @@
|
||||
import {
|
||||
ActionManager,
|
||||
Color4,
|
||||
ExecuteCodeAction,
|
||||
InstancedMesh,
|
||||
Observable,
|
||||
} from "@babylonjs/core";
|
||||
import {ActionManager, ExecuteCodeAction, HighlightLayer, InstancedMesh, StandardMaterial,} from "@babylonjs/core";
|
||||
import {ControllerEventType, Controllers} from "../../controllers/controllers";
|
||||
import log from "loglevel";
|
||||
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 actionManager = new ActionManager(DefaultScene.Scene);
|
||||
/*actionManager.registerAction(
|
||||
@ -20,31 +18,39 @@ export function buildEntityActionManager(controllerObservable: Observable<Contro
|
||||
if (evt.meshUnderPointer) {
|
||||
try {
|
||||
const mesh = evt.meshUnderPointer as InstancedMesh;
|
||||
|
||||
// Enable edges rendering on the instance itself (not source mesh)
|
||||
if (!mesh.edgesRenderer) {
|
||||
mesh.enableEdgesRendering(0.99);
|
||||
mesh.edgesWidth = .2;
|
||||
mesh.edgesColor = new Color4(1, 1, 1, 1.0);
|
||||
//mesh.sourceMesh.renderOutline = true;
|
||||
if (mesh.sourceMesh) {
|
||||
const newMesh = mesh.sourceMesh.clone(mesh.sourceMesh.name + '_clone', null, true);
|
||||
newMesh.metadata = {};
|
||||
newMesh.parent = null;
|
||||
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) {
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
controllerObservable.notifyObservers({
|
||||
controllers.controllerObservable.notifyObservers({
|
||||
type: ControllerEventType.PULSE,
|
||||
gripId: evt?.additionalData?.pickResult?.gripTransform?.id
|
||||
});
|
||||
})
|
||||
logger.debug(evt);
|
||||
})
|
||||
);
|
||||
actionManager.registerAction(
|
||||
new ExecuteCodeAction(ActionManager.OnPointerOutTrigger, (evt) => {
|
||||
try {
|
||||
const mesh = evt.source as InstancedMesh;
|
||||
// Disable edges rendering on the instance itself
|
||||
if (mesh?.edgesRenderer) {
|
||||
mesh.disableEdgesRendering();
|
||||
const mesh = evt.source;
|
||||
if (mesh.metadata.highlight) {
|
||||
mesh.metadata.highlight.dispose();
|
||||
mesh.metadata.highlight = null;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
|
||||
73
src/diagram/functions/buildMeshFromDiagramEntity.test.ts
Normal 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');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@ -18,16 +18,6 @@ import log from "loglevel";
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
import {xyztovec} from "./vectorConversion";
|
||||
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 {
|
||||
const logger = log.getLogger('buildMeshFromDiagramEntity');
|
||||
@ -82,7 +72,6 @@ function createNewInstanceIfNecessary(entity: DiagramEntity, scene: Scene): Abst
|
||||
material.emissiveTexture = AnimatedLineTexture.Texture();
|
||||
material.opacityTexture = AnimatedLineTexture.Texture();
|
||||
material.disableLighting = true;
|
||||
material.metadata = { isConnection: true, preserveTextures: true }; // Preserve animated arrow textures
|
||||
newMesh.setEnabled(false);
|
||||
break;
|
||||
case DiagramTemplates.BOX:
|
||||
@ -91,59 +80,12 @@ function createNewInstanceIfNecessary(entity: DiagramEntity, scene: Scene): Abst
|
||||
case DiagramTemplates.CONE:
|
||||
case DiagramTemplates.PLANE:
|
||||
case DiagramTemplates.PERSON:
|
||||
// Tool meshes are created with UPPERCASE hex codes (BabylonJS toHexString behavior)
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toolMesh = scene.getMeshById("tool-" + entity.template + "-" + entity.color);
|
||||
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));
|
||||
// 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};
|
||||
} else {
|
||||
if (!toolMesh) {
|
||||
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`);
|
||||
}
|
||||
logger.warn('no tool mesh found for ' + entity.template + "-" + entity.color);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -159,11 +101,6 @@ function createNewInstanceIfNecessary(entity: DiagramEntity, scene: Scene): Abst
|
||||
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;
|
||||
@ -175,7 +112,6 @@ function buildImage(entity: DiagramEntity, scene: Scene): AbstractMesh {
|
||||
logger.debug("buildImage: entity is image");
|
||||
const plane = MeshBuilder.CreatePlane(entity.id, {size: 1}, scene);
|
||||
const material = new StandardMaterial("planeMaterial", scene);
|
||||
material.metadata = { isUI: true }; // Mark as UI to prevent rendering mode modifications
|
||||
const image = new Image();
|
||||
image.src = entity.image;
|
||||
material.emissiveTexture = new Texture(entity.image, scene);
|
||||
@ -225,31 +161,9 @@ function mapMetadata(entity: DiagramEntity, newMesh: AbstractMesh, scene: Scene)
|
||||
/*if (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") {
|
||||
logger.error(`MATERIAL SHARING FAILURE for mesh ${newMesh.id}:`);
|
||||
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!`);
|
||||
logger.warn("new material created, this shouldn't happen");
|
||||
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) {
|
||||
newMesh.metadata.text = entity.text;
|
||||
@ -276,21 +190,9 @@ export function buildMissingMaterial(name: string, scene: Scene, color: string):
|
||||
if (existingMaterial) {
|
||||
return (existingMaterial as StandardMaterial);
|
||||
}
|
||||
const colorObj = Color3.FromHexString(color);
|
||||
const newMaterial = new StandardMaterial(name, scene);
|
||||
newMaterial.id = name;
|
||||
|
||||
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.diffuseColor = Color3.FromHexString(color);
|
||||
newMaterial.alpha = 1;
|
||||
return newMaterial;
|
||||
}
|
||||
@ -32,12 +32,11 @@ function createDynamicTexture(text: string, font: string, DTWidth: number, DTHei
|
||||
function createMaterial(dynamicTexture: DynamicTexture): Material {
|
||||
const mat = new StandardMaterial("text-mat", DefaultScene.Scene);
|
||||
//mat.diffuseColor = Color3.Black();
|
||||
mat.disableLighting = true;
|
||||
mat.backFaceCulling = true;
|
||||
mat.disableLighting = false;
|
||||
//mat.backFaceCulling = false;
|
||||
mat.emissiveTexture = dynamicTexture;
|
||||
mat.diffuseTexture = dynamicTexture;
|
||||
mat.metadata = { exportable: true, isUI: true }; // Mark as UI to prevent rendering mode modifications
|
||||
|
||||
mat.metadata = {exportable: true};
|
||||
//mat.freeze();
|
||||
return mat;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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};
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import {AbstractMesh, InstancedMesh} from "@babylonjs/core";
|
||||
import {AbstractMesh} from "@babylonjs/core";
|
||||
import {DiagramEntity} from "../types/diagramEntity";
|
||||
import log from "loglevel";
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
@ -25,44 +25,20 @@ export function toDiagramEntity(mesh: AbstractMesh): DiagramEntity {
|
||||
entity.from = mesh?.metadata?.from;
|
||||
entity.to = mesh?.metadata?.to;
|
||||
entity.scale = vectoxys(mesh.scaling);
|
||||
|
||||
// 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)
|
||||
if (mesh.material) {
|
||||
switch (mesh.material.getClassName()) {
|
||||
case "StandardMaterial":
|
||||
const stdMat = mesh.material as any;
|
||||
const stdColor = stdMat.emissiveColor || stdMat.diffuseColor;
|
||||
if (stdColor) {
|
||||
entity.color = stdColor.toHexString()?.toLowerCase();
|
||||
}
|
||||
entity.color = (mesh.material as any).diffuseColor.toHexString();
|
||||
break;
|
||||
case "PBRMaterial":
|
||||
const pbrMat = mesh.material as any;
|
||||
const pbrColor = pbrMat.emissiveColor || pbrMat.albedoColor;
|
||||
if (pbrColor) {
|
||||
entity.color = pbrColor.toHexString()?.toLowerCase();
|
||||
}
|
||||
entity.color = (mesh.material as any).albedoColor.toHexString();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (entity.template != "#object-template") {
|
||||
logger.error("toDiagramEntity: mesh.material is null");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
54
src/diagram/presentationManager.ts
Normal 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();
|
||||
}
|
||||
}
|
||||