Compare commits
188 Commits
diagramMan
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| add1ece149 | |||
| 421cd97fe9 | |||
| 58959fe347 | |||
| d9cd0692b5 | |||
| 3155cc930f | |||
| 8c2b7f9c7d | |||
| 960c64984e | |||
| d79f4efa98 | |||
| 8bfe7bb174 | |||
| 03217f3e65 | |||
| fd81ba3be7 | |||
| c58ce483dd | |||
| 13ecd5a626 | |||
| 82807dcfce | |||
| 739775ea94 | |||
| e2216c17e8 | |||
| 33019c116b | |||
| bd833e236a | |||
| 0916829ba2 | |||
| 122c0d2ab0 | |||
| 1e174e81d3 | |||
| a772372b2b | |||
| 74a2d179b9 | |||
| 5891dfd6b7 | |||
| a7aa385d98 | |||
| c9dc61b918 | |||
| 2fd87b2d14 | |||
| 7769910027 | |||
| 1152ab0d0c | |||
| 1ccdab2780 | |||
| e714c3d3df | |||
| 54e5017c38 | |||
| 8a78e45440 | |||
| 1c50dd5c84 | |||
| 31dd8a89da | |||
| 1098f03c7d | |||
| 0e053bf69c | |||
| 0b81605bdf | |||
| 7849bf4eb2 | |||
| e329b95f2f | |||
| 25963d5289 | |||
| aa0810be02 | |||
| 15c6617151 | |||
| adc80c54c4 | |||
| 0e318e7cc7 | |||
| 5091ca0bab | |||
| 3002181160 | |||
| f8ae71a962 | |||
| c66da87401 | |||
| 3cf3d996dc | |||
| 5889a1ed79 | |||
| be311e6dc8 | |||
| aa41895675 | |||
| 970f6fc78a | |||
| c1503d959e | |||
| 6ea6eaaac7 | |||
| 2915717a3a | |||
| 6643379133 | |||
| 1ab3deae92 | |||
| 016b1fe6e2 | |||
| ebad30ce4d | |||
| 0712abe729 | |||
| 2c3fba31d3 | |||
| c815db4594 | |||
| 43100ad650 | |||
| af52d5992c | |||
| 5fbf2b87c1 | |||
| 204ef670f9 | |||
| 26b48b26c8 | |||
| 02c08b35f2 | |||
| bda0735c7f | |||
| c7887d7d8f | |||
| 3f02fc7ea5 | |||
| 100c5e612c | |||
| d59c7b6e6e | |||
| 0ad61bdde9 | |||
| 4a9d7acc41 | |||
| 6ad04bb21a | |||
| 293c74d7c1 | |||
| 6d2049e1f6 | |||
| cf0f359921 | |||
| 58668443c4 | |||
| 9d5234b629 | |||
| 5ce0c9ce4f | |||
| 8c04b40d03 | |||
| cdf59db5b6 | |||
| f2b9e78e45 | |||
| 4e6c3a63d0 | |||
| e69d008bfa | |||
| 5d3cad0def | |||
| 4f39030ed4 | |||
| 2397ddcd4c | |||
| b9152678b8 | |||
| a9c8d3dbad | |||
| 60758ed84d | |||
| 53ca47d63e | |||
| afdf765a8f | |||
| 71da2dd6a2 | |||
| 17206abca7 | |||
| 263879d215 | |||
| 4cb50e5c6a | |||
| ba2d9a7886 | |||
| 83279fa5b0 | |||
| b443e1854b | |||
| c00fc55462 | |||
| b198605643 | |||
| 2486107041 | |||
| a07b53f2a7 | |||
| 1d6c82a16a | |||
| 1de6270f79 | |||
| 4fdcc9694d | |||
| 4c300dc73b | |||
| e0d85a6a3d | |||
| d08e86e92f | |||
| 4e1436b0cc | |||
| cf278fed3a | |||
| ae73f3e74b | |||
| 540658e3d0 | |||
| 1d94143b21 | |||
| 648876c06b | |||
| bb9c3ec396 | |||
| dec0041c21 | |||
| a334f13e6f | |||
| da38df7df4 | |||
| 2d1a3ba5d6 | |||
| 3d3f73c259 | |||
| d6941fd1bf | |||
| 4a95028fe8 | |||
| ffe8f60f38 | |||
| 9e7833b149 | |||
| e405dc1598 | |||
| cb2675bf27 | |||
| 724cd79ab3 | |||
| f07ea11817 | |||
| 7315e3397a | |||
| 7806760153 | |||
| 7561a06b69 | |||
| 06333e9123 | |||
| d8d91dd688 | |||
| fa8865d013 | |||
| fbafd747d3 | |||
| 63d1e627ad | |||
| 1a3e9b879e | |||
| d0b08b72e2 | |||
| f26aa01211 | |||
| 6ec28efe78 | |||
| cdaff97614 | |||
| e85adc1386 | |||
| 2872026ac9 | |||
| 4c26dca6c5 | |||
| 15fdb938ee | |||
| bbe54dc3e3 | |||
| 01874b9e9e | |||
| 3ade3d4d6a | |||
| 24ae4aad41 | |||
| ae3a94b8d4 | |||
| 735bd4bb2f | |||
| 41aeceed69 | |||
| 5a1c86a0dd | |||
| 7ccca76119 | |||
| 2debefd556 | |||
| c86ed5e9a0 | |||
| ce9316d20a | |||
| 1e36ca20d9 | |||
| 0acde00ecc | |||
| d8cdb019fb | |||
| bf7d419df2 | |||
| fd774c0be2 | |||
| 08569de94d | |||
| d864c2e562 | |||
| a016aa749b | |||
| b788b64df5 | |||
| dc3c3c56a1 | |||
| 1dd192cd4d | |||
| d82df88296 | |||
| eb4281ac30 | |||
| e27a77d674 | |||
| 2f29b0a2de | |||
| 791481e564 | |||
| 0e4d815225 | |||
| f479f6043f | |||
| 4db349581b | |||
| d761a59d6d | |||
| 36e4b04957 | |||
| 5c22c15076 | |||
| 48c0535c8f | |||
| f87190af86 | |||
| d17fc0897d |
75
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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
|
||||||
5
.github/workflows/main.yml
vendored
@ -2,9 +2,9 @@ name: Node.js CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "deepdiagram" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "deepdiagram" ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -16,5 +16,4 @@ jobs:
|
|||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
- run: echo "test"
|
|
||||||
- run: cp -r ./dist/* /var/www/deepdiagram
|
- run: cp -r ./dist/* /var/www/deepdiagram
|
||||||
|
|||||||
4
.github/workflows/node.js.yml
vendored
@ -5,9 +5,9 @@ name: Node.js Github Side
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "deepdiagram" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "deepdiagram" ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
2
.gitignore
vendored
@ -25,3 +25,5 @@ dist-ssr
|
|||||||
|
|
||||||
# Local Netlify folder
|
# Local Netlify folder
|
||||||
.netlify
|
.netlify
|
||||||
|
/data/
|
||||||
|
/.env.production
|
||||||
|
|||||||
280
ALPINE_SERVICE.md
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
# 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
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# 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.
|
||||||
403
EXPRESS_API_PLAN.md
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
# 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
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
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.
|
||||||
167
NEXT_MIGRATION_PLAN.md
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# 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
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
# 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
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# 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
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
# 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
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
# 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
|
||||||
138
docs/NAMING_CONVENTIONS.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# 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
|
||||||
85
index.html
@ -3,76 +3,57 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
||||||
<meta content="An immersive vr diagramming experience based using webxr version 0.2" name="description">
|
<meta content="An immersive vr diagramming experience based using webxr version 0.0.8-14 (2024-12-29Z)"
|
||||||
|
name="description">
|
||||||
<meta content="width=device-width, initial-scale=1, height=device-height" name="viewport">
|
<meta content="width=device-width, initial-scale=1, height=device-height" name="viewport">
|
||||||
<link href="/styles.css" rel="stylesheet">
|
<!--<link href="/styles.css" rel="stylesheet"> -->
|
||||||
<link href="/assets/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png">
|
<link href="/assets/dasfad/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/dasfad/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png">
|
||||||
<link href="/assets/favicon-96x96.png" rel="icon" sizes="96x96" type="image/png">
|
<link href="/assets/dasfad/favicon-96x96.png" rel="icon" sizes="96x96" type="image/png">
|
||||||
<title>Deep Diagram</title>
|
<link as="fetch" href="/node_modules/.vite/deps/HavokPhysics.wasm" rel="preload">
|
||||||
<link as="script" href="/newRelic.js" rel="preload">
|
<title>DASFAD</title>
|
||||||
<script src="/newRelic.js"></script>
|
<!-- <link as="script" href="/newRelic.js" rel="preload">
|
||||||
|
<script defer src="/newRelic.js"></script> -->
|
||||||
|
|
||||||
|
|
||||||
<link href="/manifest.webmanifest" rel="manifest"/>
|
<link href="/manifest.webmanifest" rel="manifest"/>
|
||||||
<!--<script src='/niceware.js'></script>-->
|
<!--<script src='/niceware.js'></script>-->
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<img id="loadingGrid" src="/assets/grid3.jpg"/>
|
|
||||||
<script>
|
<script>
|
||||||
/*
|
|
||||||
var SpeechRecognition = SpeechRecognition || webkitSpeechRecognition
|
/* if (typeof navigator.serviceWorker !== 'undefined') {
|
||||||
var SpeechGrammarList = SpeechGrammarList || window.webkitSpeechGrammarList
|
|
||||||
var SpeechRecognitionEvent = SpeechRecognitionEvent || webkitSpeechRecognitionEvent
|
|
||||||
var recognition = new SpeechRecognition();
|
if (localStorage.getItem('serviceWorkerVersion') !== '11') {
|
||||||
recognition.continuous = false;
|
caches.keys().then(cacheNames => {
|
||||||
recognition.lang = 'en-US';
|
cacheNames.forEach(cacheName => {
|
||||||
recognition.interimResults = true;
|
caches.delete(cacheName);
|
||||||
recognition.maxAlternatives = 1;
|
});
|
||||||
recognition.onresult = function(event) {
|
});
|
||||||
console.log(event.results[0][0].transcript);
|
localStorage.setItem('serviceWorkerVersion', '11');
|
||||||
}
|
}
|
||||||
recognition.onend = function() {
|
navigator.serviceWorker.register('/sw.js', {updateViaCache: 'none'});
|
||||||
console.log("recognition ended");
|
|
||||||
recognition.start();
|
|
||||||
}
|
}
|
||||||
console.log("starting recognition");
|
|
||||||
recognition.start();
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="webApp" id="webApp">
|
<div class="webApp" id="webApp">
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script src="/src/webApp.ts" type="module"></script>
|
<script defer src="/src/webApp.ts" type="module"></script>
|
||||||
<script src="/src/vrApp.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">
|
<div class="scene">
|
||||||
<canvas id="gameCanvas"></canvas>
|
<canvas id="gameCanvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
<!--<script defer src="/src/vrApp.ts" type="module"></script>-->
|
||||||
</body>
|
</body>
|
||||||
<style>
|
|
||||||
#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>
|
|
||||||
</html>
|
</html>
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import {Handler, HandlerContext, HandlerEvent} from "@netlify/functions";
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
export const handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {
|
|
||||||
try {
|
|
||||||
switch (event.httpMethod) {
|
|
||||||
case 'POST':
|
|
||||||
const apiKey = event.headers['api-key'];
|
|
||||||
const query = event.body;
|
|
||||||
const response = await axios.post('https://api.newrelic.com/graphql',
|
|
||||||
query,
|
|
||||||
{headers: {'Api-Key': apiKey, 'Content-Type': 'application/json'}});
|
|
||||||
const data = await response.data;
|
|
||||||
return {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Access-Control-Allow-Origin': 'https://cameras.immersiveidea.com',
|
|
||||||
'Access-Control-Allow-Credentials': 'true'
|
|
||||||
},
|
|
||||||
statusCode: 200,
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'OPTIONS':
|
|
||||||
const headers = {
|
|
||||||
'Access-Control-Allow-Origin': 'https://cameras.immersiveidea.com',
|
|
||||||
'Access-Control-Allow-Credentials': 'true',
|
|
||||||
'Access-Control-Allow-Headers': 'content-type, api-key',
|
|
||||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE'
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
statusCode: 204,
|
|
||||||
headers
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
statusCode: 405,
|
|
||||||
body: 'Method Not Allowed'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return {
|
|
||||||
statusCode: 500,
|
|
||||||
body: JSON.stringify(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,216 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
const baseurl = 'https://syncdb-service-d3f974de56ef.herokuapp.com/';
|
|
||||||
const auth = 'admin:stM8Lnm@Cuf-tWZHv';
|
|
||||||
const authToken = Buffer.from(auth).toString('base64');
|
|
||||||
|
|
||||||
|
|
||||||
type Params = {
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
db: string
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkDB(auth: string, db: string) {
|
|
||||||
try {
|
|
||||||
console.log('Checking for DB');
|
|
||||||
const exist = await axios.head(baseurl + db,
|
|
||||||
{headers: {'Authorization': 'Basic ' + auth}});
|
|
||||||
if (exist && exist.status == 200) {
|
|
||||||
console.log("DB Found");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log("DB not Found");
|
|
||||||
//console.log(err);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Access {
|
|
||||||
DENIED,
|
|
||||||
MISSING,
|
|
||||||
ALLOWED,
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserToken(params: Params) {
|
|
||||||
const userAuth = params.username + ':' + params.password;
|
|
||||||
return Buffer.from(userAuth).toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkIfDbExists(params: Params): Promise<Access> {
|
|
||||||
console.log("Checking if DB exists");
|
|
||||||
if (!params.username || !params.password || !params.db) {
|
|
||||||
throw new Error('No share key provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await checkDB(getUserToken(params), params.db)) {
|
|
||||||
return Access.ALLOWED;
|
|
||||||
}
|
|
||||||
if (await checkDB(authToken, params.db)) {
|
|
||||||
return Access.DENIED;
|
|
||||||
}
|
|
||||||
return Access.MISSING;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createDB(params: Params) {
|
|
||||||
console.log("Creating DB");
|
|
||||||
|
|
||||||
const response = await axios.put(
|
|
||||||
baseurl + params.db,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Basic ' + authToken,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(response.status);
|
|
||||||
console.log(response.data);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createUser(params: Params) {
|
|
||||||
try {
|
|
||||||
console.log("Checking for User");
|
|
||||||
const userResponse = await axios.head(
|
|
||||||
baseurl + '_users/org.couchdb.user:' + params.username,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Basic ' + getUserToken(params),
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (userResponse.status == 200) {
|
|
||||||
console.log("User Found");
|
|
||||||
return userResponse;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log("User Missing");
|
|
||||||
}
|
|
||||||
console.log("Creating User");
|
|
||||||
const userResponse = await axios.put(
|
|
||||||
baseurl + '_users/org.couchdb.user:' + params.username,
|
|
||||||
{
|
|
||||||
_id: 'org.couchdb.user:' + params.username,
|
|
||||||
name: params.username,
|
|
||||||
password: params.password, roles: [], type: 'user'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Basic ' + authToken,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return userResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authorizeUser(params: Params) {
|
|
||||||
console.log("Authorizing User");
|
|
||||||
return await axios.put(
|
|
||||||
baseurl + params.db + '/_security',
|
|
||||||
{admins: {names: [], roles: []}, members: {names: [params.username], roles: []}},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Basic ' + authToken,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async (req: Request): Promise<Response> => {
|
|
||||||
console.log(req.method);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (req.method == 'OPTIONS') {
|
|
||||||
const origin = req.headers.get('Origin');
|
|
||||||
const headers = req.headers.get('Access-Control-Request-Headers');
|
|
||||||
console.log(origin);
|
|
||||||
return new Response(
|
|
||||||
'OK',
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Allow': 'POST',
|
|
||||||
'Max-Age': '30',
|
|
||||||
'Access-Control-Allow-Methods': 'POST',
|
|
||||||
'Access-Control-Allow-Origin': origin ? origin : 'https://cameras.immersiveidea.com',
|
|
||||||
'Access-Control-Allow-Credentials': 'true',
|
|
||||||
'Access-Control-Allow-Headers': headers ? headers : 'Content-Type'
|
|
||||||
},
|
|
||||||
status: 200
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify(err),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Allow': 'POST',
|
|
||||||
'Max-Age': '30',
|
|
||||||
'Access-Control-Allow-Methods': 'POST',
|
|
||||||
'Access-Control-Allow-Origin': origin ? origin : 'https://cameras.immersiveidea.com',
|
|
||||||
'Access-Control-Allow-Credentials': 'true'
|
|
||||||
},
|
|
||||||
status: 500
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
const params = JSON.parse(await req.text());
|
|
||||||
console.log(params);
|
|
||||||
const createUserResponse = await createUser(params);
|
|
||||||
console.log(createUserResponse.status);
|
|
||||||
if (createUserResponse.status != 201 && createUserResponse.status != 200) {
|
|
||||||
throw new Error('Could not create User');
|
|
||||||
}
|
|
||||||
|
|
||||||
const exists = await checkIfDbExists(params);
|
|
||||||
switch (exists) {
|
|
||||||
case Access.ALLOWED:
|
|
||||||
console.log('Allowed');
|
|
||||||
return new Response('OK', {status: 200});
|
|
||||||
case Access.DENIED:
|
|
||||||
console.log('Denied');
|
|
||||||
return new Response('Denied', {status: 401});
|
|
||||||
case Access.MISSING:
|
|
||||||
console.log('Creating Missing DB');
|
|
||||||
const createDbResponse = await createDB(params);
|
|
||||||
if (createDbResponse.status != 201) {
|
|
||||||
throw new Error('Could not create DB');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const authorizeUserResponse = await authorizeUser(params);
|
|
||||||
if (authorizeUserResponse.status != 200) {
|
|
||||||
throw new Error('could not authorize user');
|
|
||||||
}
|
|
||||||
const origin = req.headers.get('origin');
|
|
||||||
console.log(origin);
|
|
||||||
return new Response(
|
|
||||||
'OK',
|
|
||||||
{
|
|
||||||
headers: [
|
|
||||||
['Content-Type', 'application/json'],
|
|
||||||
['Access-Control-Allow-Origin', origin],
|
|
||||||
['Access-Control-Allow-Credentials', 'true']
|
|
||||||
],
|
|
||||||
status: 200
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
const response = {err: err};
|
|
||||||
return new Response('Error',
|
|
||||||
{status: 500}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import {Handler, HandlerContext, HandlerEvent} from "@netlify/functions";
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
export const handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('https://api.assemblyai.com/v2/realtime/token', // use account token to get a temp user token
|
|
||||||
{expires_in: 3600}, // can set a TTL timer in seconds.
|
|
||||||
{headers: {authorization: process.env.VOICE_TOKEN}});
|
|
||||||
|
|
||||||
const data = await response.data;
|
|
||||||
return {
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
statusCode: 200,
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
statusCode: 500,
|
|
||||||
body: JSON.stringify(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
48
newrelic.cjs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
'use strict'
|
||||||
|
// Load .env.local first (has the secrets), then .env as fallback
|
||||||
|
require('dotenv').config({ path: '.env.local' });
|
||||||
|
require('dotenv').config();
|
||||||
|
/**
|
||||||
|
* New Relic Node.js APM Configuration
|
||||||
|
*
|
||||||
|
* This file configures the New Relic agent for backend monitoring.
|
||||||
|
* Requires NEW_RELIC_LICENSE_KEY environment variable to be set.
|
||||||
|
*
|
||||||
|
* Distributed tracing is enabled to correlate with browser agent traces.
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.config = {
|
||||||
|
app_name: ['dasfad-backend'],
|
||||||
|
license_key: process.env.NEW_RELIC_LICENSE_KEY,
|
||||||
|
distributed_tracing: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
logging: {
|
||||||
|
level: 'info'
|
||||||
|
},
|
||||||
|
application_logging: {
|
||||||
|
enabled: true,
|
||||||
|
forwarding: {
|
||||||
|
enabled: true,
|
||||||
|
max_samples_stored: 10000
|
||||||
|
},
|
||||||
|
local_decorating: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
allow_all_headers: true,
|
||||||
|
attributes: {
|
||||||
|
exclude: [
|
||||||
|
'request.headers.cookie',
|
||||||
|
'request.headers.authorization',
|
||||||
|
'request.headers.proxyAuthorization',
|
||||||
|
'request.headers.setCookie*',
|
||||||
|
'request.headers.x*',
|
||||||
|
'response.headers.cookie',
|
||||||
|
'response.headers.authorization',
|
||||||
|
'response.headers.proxyAuthorization',
|
||||||
|
'response.headers.setCookie*',
|
||||||
|
'response.headers.x*'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
9650
package-lock.json
generated
Normal file
67
package.json
@ -1,55 +1,76 @@
|
|||||||
{
|
{
|
||||||
"name": "immersive",
|
"name": "immersive",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.2 d",
|
"version": "0.0.8-48",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "node -r newrelic server.js",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"build": "vite build",
|
"build": "node versionBump.js && vite build",
|
||||||
"preview": "vite preview",
|
"start": "NODE_ENV=production node -r newrelic server.js",
|
||||||
"serve": "node server.js",
|
"start:api": "API_ONLY=true node -r newrelic server.js",
|
||||||
|
"socket": "node server/server.js",
|
||||||
"serverBuild": "cd server && tsc",
|
"serverBuild": "cd server && tsc",
|
||||||
"havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps"
|
"havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babylonjs/core": "^7.3.1",
|
"@auth0/auth0-react": "^2.2.4",
|
||||||
"@babylonjs/gui": "^7.3.1",
|
"@babylonjs/core": "^8.16.2",
|
||||||
|
"@babylonjs/gui": "^8.16.2",
|
||||||
"@babylonjs/havok": "1.3.4",
|
"@babylonjs/havok": "1.3.4",
|
||||||
"@babylonjs/inspector": "^7.3.1",
|
"@babylonjs/inspector": "^8.16.2",
|
||||||
"@babylonjs/loaders": "^7.3.1",
|
"@babylonjs/loaders": "^8.16.2",
|
||||||
"@babylonjs/materials": "^7.3.1",
|
"@babylonjs/materials": "^8.16.2",
|
||||||
"@babylonjs/procedural-textures": "^7.3.1",
|
"@babylonjs/serializers": "^8.16.2",
|
||||||
"@babylonjs/serializers": "^7.3.1",
|
"@emotion/react": "^11.13.0",
|
||||||
|
"@giphy/js-fetch-api": "^5.6.0",
|
||||||
|
"@giphy/react-components": "^9.6.0",
|
||||||
|
"@mantine/core": "^7.17.8",
|
||||||
|
"@mantine/form": "^7.17.8",
|
||||||
|
"@mantine/hooks": "^7.17.8",
|
||||||
|
"@maptiler/client": "1.8.1",
|
||||||
|
"@newrelic/browser-agent": "^1.306.0",
|
||||||
"@picovoice/cobra-web": "^2.0.3",
|
"@picovoice/cobra-web": "^2.0.3",
|
||||||
"@picovoice/eagle-web": "^1.0.0",
|
"@picovoice/eagle-web": "^1.0.0",
|
||||||
"@picovoice/web-voice-processor": "^4.0.9",
|
"@picovoice/web-voice-processor": "^4.0.9",
|
||||||
"@typed-mxgraph/typed-mxgraph": "^1.0.8",
|
"@tabler/icons-react": "^3.14.0",
|
||||||
"@types/dom-to-image": "^2.6.7",
|
|
||||||
"@types/file-saver": "^2.0.6",
|
|
||||||
"@types/node": "^18.14.0",
|
"@types/node": "^18.14.0",
|
||||||
"@types/react": "^18.2.72",
|
"@types/react": "^18.2.72",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.10.0",
|
||||||
"babylon-html": "^0.0.3",
|
"canvas-hypertxt": "1.0.3",
|
||||||
"dom-to-image-more": "^3.3.0",
|
"cors": "^2.8.5",
|
||||||
"earcut": "^2.2.4",
|
"dotenv": "^17.2.3",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"file-saver": "^2.0.5",
|
"express": "^5.2.1",
|
||||||
|
"express-pouchdb": "^4.2.0",
|
||||||
|
"hash-wasm": "4.11.0",
|
||||||
"hls.js": "^1.1.4",
|
"hls.js": "^1.1.4",
|
||||||
|
"js-crypto-aes": "1.0.6",
|
||||||
|
"leveldown": "^6.1.1",
|
||||||
"loglevel": "^1.9.1",
|
"loglevel": "^1.9.1",
|
||||||
"niceware": "^4.0.0",
|
"meaningful-string": "^1.4.0",
|
||||||
|
"newrelic": "^13.9.1",
|
||||||
|
"peer-lite": "2.0.2",
|
||||||
"pouchdb": "^8.0.1",
|
"pouchdb": "^8.0.1",
|
||||||
|
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||||
|
"pouchdb-adapter-memory": "^9.0.0",
|
||||||
"pouchdb-find": "^8.0.1",
|
"pouchdb-find": "^8.0.1",
|
||||||
"query-string": "^8.1.0",
|
"query-string": "^8.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-router-dom": "^6.26.1",
|
||||||
"recordrtc": "^5.6.0",
|
"recordrtc": "^5.6.0",
|
||||||
"rfc4648": "^1.5.3",
|
"rfc4648": "^1.5.3",
|
||||||
"round": "^2.0.1",
|
"round": "^2.0.1",
|
||||||
"uuid": "^9.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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dom-to-image": "^2.6.7",
|
"@types/dom-to-image": "^2.6.7",
|
||||||
|
|||||||
BIN
public/assets/dasfad-logo.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
40
public/assets/dasfad-logo.svg
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 8.1 KiB |
BIN
public/assets/dasfad/194885175_padded_logo.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
@ -0,0 +1,912 @@
|
|||||||
|
%!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
|
||||||
@ -0,0 +1,920 @@
|
|||||||
|
%!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
|
||||||
BIN
public/assets/dasfad/Logo Files/For Web/Favicons/Android.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/assets/dasfad/Logo Files/For Web/Favicons/browser.png
Normal file
|
After Width: | Height: | Size: 143 B |
BIN
public/assets/dasfad/Logo Files/For Web/Favicons/iPhone.png
Normal file
|
After Width: | Height: | Size: 1014 B |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 86 KiB |
@ -0,0 +1,40 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 8.0 KiB |
@ -0,0 +1,40 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 8.1 KiB |
@ -0,0 +1,40 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 8.2 KiB |
@ -0,0 +1,40 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 8.0 KiB |
BIN
public/assets/dasfad/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 679 B |
BIN
public/assets/dasfad/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 896 B |
BIN
public/assets/dasfad/favicon-512x512.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
public/assets/dasfad/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/assets/grid4.jpg
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
public/assets/grid6.jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 32 KiB |
BIN
public/assets/icons/point.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
public/assets/icons/video.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
8
public/assets/icons/video.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g data-name="Layer 2">
|
||||||
|
<g data-name="video">
|
||||||
|
<rect width="24" height="24" opacity="0"/>
|
||||||
|
<path d="M21 7.15a1.7 1.7 0 0 0-1.85.3l-2.15 2V8a3 3 0 0 0-3-3H5a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h9a3 3 0 0 0 3-3v-1.45l2.16 2a1.74 1.74 0 0 0 1.16.45 1.68 1.68 0 0 0 .69-.15 1.6 1.6 0 0 0 1-1.48V8.63A1.6 1.6 0 0 0 21 7.15z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 438 B |
BIN
public/assets/models/person.stl
Normal file
300
public/assets/models/tinker.obj
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
# Object Export From Tinkercad Server 2015
|
||||||
|
|
||||||
|
mtllib obj.mtl
|
||||||
|
|
||||||
|
o obj_0
|
||||||
|
v 4 3.5 4
|
||||||
|
v 4 3.568 3.482
|
||||||
|
v 4 3.768 3
|
||||||
|
v 4 4.086 2.586
|
||||||
|
v 4 4.5 2.268
|
||||||
|
v 4 4.982 2.068
|
||||||
|
v 4 5.5 2
|
||||||
|
v 4 6.018 2.068
|
||||||
|
v 4 6.5 2.268
|
||||||
|
v 4 6.914 2.586
|
||||||
|
v 4 7.232 3
|
||||||
|
v 4 7.432 3.482
|
||||||
|
v 4 7.5 4
|
||||||
|
v 4 7.432 4.518
|
||||||
|
v 4 7.232 5
|
||||||
|
v 4 6.914 5.414
|
||||||
|
v 4 6.5 5.732
|
||||||
|
v 4 6.018 5.932
|
||||||
|
v 4 5.5 6
|
||||||
|
v 4 4.982 5.932
|
||||||
|
v 4 4.5 5.732
|
||||||
|
v 4 4.086 5.414
|
||||||
|
v 4 3.768 5
|
||||||
|
v 4 3.568 4.518
|
||||||
|
v -8.308 4.5 7.952
|
||||||
|
v 1 4.7272 4.2072
|
||||||
|
v -8.587 4.5 7.81
|
||||||
|
v 1 4.7 4
|
||||||
|
v -8.81 4.5 7.587
|
||||||
|
v 1 4.8072 4.4
|
||||||
|
v -8.952 4.5 7.308
|
||||||
|
v 1 4.9344 4.5656
|
||||||
|
v -9 4.5 7.0043
|
||||||
|
v 0.952 6.5 7.308
|
||||||
|
v 1 5.1 4.6928
|
||||||
|
v 1 6.5 7.0043
|
||||||
|
v 0.81 6.5 7.587
|
||||||
|
v 1 5.2928 4.7728
|
||||||
|
v 0.587 6.5 7.81
|
||||||
|
v 0.308 6.5 7.952
|
||||||
|
v 1 5.5 4.8
|
||||||
|
v 0.0033 6.5 8
|
||||||
|
v 1 5.7072 4.7728
|
||||||
|
v 1 5.9 4.6928
|
||||||
|
v 1 6.0656 4.5656
|
||||||
|
v 1 6.1928 4.4
|
||||||
|
v 1 4.5 7.0043
|
||||||
|
v 0.952 4.5 7.308
|
||||||
|
v 0.81 4.5 7.587
|
||||||
|
v 0.587 4.5 7.81
|
||||||
|
v 0.308 4.5 7.952
|
||||||
|
v 1 6.2728 4.2072
|
||||||
|
v 0.0033 4.5 8
|
||||||
|
v 1 6.3 4
|
||||||
|
v -8.0043 4.5 0
|
||||||
|
v -8.952 4.5 0.692
|
||||||
|
v -9 4.5 0.9967
|
||||||
|
v 1 6.2728 3.7928
|
||||||
|
v -8.81 4.5 0.413
|
||||||
|
v -8.587 4.5 0.19
|
||||||
|
v 1 6.1928 3.6
|
||||||
|
v -8.308 4.5 0.048
|
||||||
|
v 1 6.0656 3.4344
|
||||||
|
v 1 5.9 3.3072
|
||||||
|
v -8.0043 6.5 8
|
||||||
|
v -8.308 6.5 7.952
|
||||||
|
v 1 5.7072 3.2272
|
||||||
|
v -8.587 6.5 7.81
|
||||||
|
v 1 5.5 3.2
|
||||||
|
v -8.81 6.5 7.587
|
||||||
|
v -8.952 6.5 7.308
|
||||||
|
v 1 5.2928 3.2272
|
||||||
|
v -9 6.5 7.0043
|
||||||
|
v 1 5.1 3.3072
|
||||||
|
v 1 4.9344 3.4344
|
||||||
|
v 1 4.8072 3.6
|
||||||
|
v 1 4.7272 3.7928
|
||||||
|
v -9 6.5 0.9967
|
||||||
|
v -8.0043 4.5 8
|
||||||
|
v 0.81 6.5 0.413
|
||||||
|
v 0.587 6.5 0.19
|
||||||
|
v 0.952 6.5 0.692
|
||||||
|
v 1 6.5 0.9967
|
||||||
|
v 0.0033 6.5 0
|
||||||
|
v 0.0033 4.5 0
|
||||||
|
v 0.308 4.5 0.048
|
||||||
|
v -8.0043 6.5 0
|
||||||
|
v 0.587 4.5 0.19
|
||||||
|
v 0.81 4.5 0.413
|
||||||
|
v 0.952 4.5 0.692
|
||||||
|
v 1 4.5 0.9967
|
||||||
|
v -8.952 6.5 0.692
|
||||||
|
v -8.81 6.5 0.413
|
||||||
|
v -8.587 6.5 0.19
|
||||||
|
v -8.308 6.5 0.048
|
||||||
|
v 0.308 6.5 0.048
|
||||||
|
# 96 vertices
|
||||||
|
|
||||||
|
g group_0_8273816
|
||||||
|
|
||||||
|
usemtl color_8273816
|
||||||
|
s 0
|
||||||
|
|
||||||
|
f 1 2 3
|
||||||
|
f 1 3 4
|
||||||
|
f 1 4 5
|
||||||
|
f 1 5 6
|
||||||
|
f 1 6 7
|
||||||
|
f 1 7 8
|
||||||
|
f 1 8 9
|
||||||
|
f 1 9 10
|
||||||
|
f 1 10 11
|
||||||
|
f 1 11 12
|
||||||
|
f 1 12 13
|
||||||
|
f 1 13 14
|
||||||
|
f 1 14 15
|
||||||
|
f 1 15 16
|
||||||
|
f 1 16 17
|
||||||
|
f 1 17 18
|
||||||
|
f 1 18 19
|
||||||
|
f 1 19 20
|
||||||
|
f 1 20 21
|
||||||
|
f 1 21 22
|
||||||
|
f 1 22 23
|
||||||
|
f 1 23 24
|
||||||
|
f 28 1 26
|
||||||
|
f 24 26 1
|
||||||
|
f 23 30 26
|
||||||
|
f 23 26 24
|
||||||
|
f 22 32 30
|
||||||
|
f 22 30 23
|
||||||
|
f 32 22 35
|
||||||
|
f 21 35 22
|
||||||
|
f 49 48 37
|
||||||
|
f 35 21 38
|
||||||
|
f 20 38 21
|
||||||
|
f 19 41 38
|
||||||
|
f 19 38 20
|
||||||
|
f 41 19 43
|
||||||
|
f 18 43 19
|
||||||
|
f 34 48 36
|
||||||
|
f 17 44 43
|
||||||
|
f 17 43 18
|
||||||
|
f 34 37 48
|
||||||
|
f 16 45 44
|
||||||
|
f 16 44 17
|
||||||
|
f 49 37 39
|
||||||
|
f 45 16 46
|
||||||
|
f 15 46 16
|
||||||
|
f 53 40 42
|
||||||
|
f 36 48 47
|
||||||
|
f 49 39 50
|
||||||
|
f 51 50 40
|
||||||
|
f 39 40 50
|
||||||
|
f 46 15 52
|
||||||
|
f 14 52 15
|
||||||
|
f 40 53 51
|
||||||
|
f 52 54 36
|
||||||
|
f 83 36 54
|
||||||
|
f 54 58 83
|
||||||
|
f 58 61 83
|
||||||
|
f 61 63 83
|
||||||
|
f 93 60 59
|
||||||
|
f 64 83 63
|
||||||
|
f 69 72 91
|
||||||
|
f 72 74 91
|
||||||
|
f 73 33 71
|
||||||
|
f 74 75 91
|
||||||
|
f 75 76 91
|
||||||
|
f 25 65 66
|
||||||
|
f 66 68 27
|
||||||
|
f 66 27 25
|
||||||
|
f 29 27 70
|
||||||
|
f 68 70 27
|
||||||
|
f 91 76 77
|
||||||
|
f 91 77 28
|
||||||
|
f 70 71 31
|
||||||
|
f 70 31 29
|
||||||
|
f 71 33 31
|
||||||
|
f 65 25 79
|
||||||
|
f 57 33 78
|
||||||
|
f 73 78 33
|
||||||
|
f 42 65 53
|
||||||
|
f 53 65 79
|
||||||
|
f 81 88 86
|
||||||
|
f 80 89 88
|
||||||
|
f 80 88 81
|
||||||
|
f 89 80 90
|
||||||
|
f 82 90 80
|
||||||
|
f 82 83 90
|
||||||
|
f 91 90 83
|
||||||
|
f 84 86 85
|
||||||
|
f 55 87 85
|
||||||
|
f 84 85 87
|
||||||
|
f 95 87 55
|
||||||
|
f 86 91 85
|
||||||
|
f 88 91 86
|
||||||
|
f 89 90 88
|
||||||
|
f 91 88 90
|
||||||
|
f 92 56 78
|
||||||
|
f 93 56 92
|
||||||
|
f 94 60 93
|
||||||
|
f 94 95 60
|
||||||
|
f 62 60 95
|
||||||
|
f 56 55 57
|
||||||
|
f 91 57 85
|
||||||
|
f 62 95 55
|
||||||
|
f 55 85 57
|
||||||
|
f 59 55 56
|
||||||
|
f 60 62 59
|
||||||
|
f 55 59 62
|
||||||
|
f 78 56 57
|
||||||
|
f 93 59 56
|
||||||
|
f 29 31 27
|
||||||
|
f 33 27 31
|
||||||
|
f 27 33 25
|
||||||
|
f 13 54 52
|
||||||
|
f 13 52 14
|
||||||
|
f 54 13 58
|
||||||
|
f 12 58 13
|
||||||
|
f 49 50 57
|
||||||
|
f 51 57 50
|
||||||
|
f 53 57 51
|
||||||
|
f 79 33 53
|
||||||
|
f 61 58 11
|
||||||
|
f 12 11 58
|
||||||
|
f 63 61 10
|
||||||
|
f 11 10 61
|
||||||
|
f 10 9 64
|
||||||
|
f 10 64 63
|
||||||
|
f 49 57 48
|
||||||
|
f 91 47 57
|
||||||
|
f 25 33 79
|
||||||
|
f 57 53 33
|
||||||
|
f 47 48 57
|
||||||
|
f 9 8 67
|
||||||
|
f 9 67 64
|
||||||
|
f 96 73 81
|
||||||
|
f 84 73 96
|
||||||
|
f 69 67 7
|
||||||
|
f 8 7 67
|
||||||
|
f 80 81 73
|
||||||
|
f 7 6 72
|
||||||
|
f 7 72 69
|
||||||
|
f 74 72 5
|
||||||
|
f 6 5 72
|
||||||
|
f 93 92 94
|
||||||
|
f 78 94 92
|
||||||
|
f 95 78 87
|
||||||
|
f 65 42 83
|
||||||
|
f 75 74 4
|
||||||
|
f 5 4 74
|
||||||
|
f 66 83 68
|
||||||
|
f 70 68 83
|
||||||
|
f 4 3 76
|
||||||
|
f 4 76 75
|
||||||
|
f 39 37 40
|
||||||
|
f 42 40 37
|
||||||
|
f 42 37 34
|
||||||
|
f 3 2 77
|
||||||
|
f 3 77 76
|
||||||
|
f 34 36 42
|
||||||
|
f 28 77 1
|
||||||
|
f 2 1 77
|
||||||
|
f 36 83 42
|
||||||
|
f 65 83 66
|
||||||
|
f 71 70 83
|
||||||
|
f 73 71 83
|
||||||
|
f 82 80 73
|
||||||
|
f 83 82 73
|
||||||
|
f 87 78 84
|
||||||
|
f 94 78 95
|
||||||
|
f 73 84 78
|
||||||
|
f 96 86 84
|
||||||
|
f 81 86 96
|
||||||
|
f 67 69 83
|
||||||
|
f 64 67 83
|
||||||
|
f 69 91 83
|
||||||
|
f 91 28 47
|
||||||
|
f 28 26 47
|
||||||
|
f 26 30 47
|
||||||
|
f 30 32 47
|
||||||
|
f 38 41 47
|
||||||
|
f 32 35 47
|
||||||
|
f 35 38 47
|
||||||
|
f 41 36 47
|
||||||
|
f 41 43 36
|
||||||
|
f 43 44 36
|
||||||
|
f 44 45 36
|
||||||
|
f 45 46 36
|
||||||
|
f 46 52 36
|
||||||
|
# 188 faces
|
||||||
|
|
||||||
|
#end of obj_0
|
||||||
|
|
||||||
BIN
public/assets/screenshot2.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
BIN
public/assets/sounds/noise.mp3
Normal file
BIN
public/assets/textures/arrow.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/assets/textures/rockford.jpeg
Normal file
|
After Width: | Height: | Size: 688 KiB |
3
public/b2b2c.csv
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"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.
|
@ -2,6 +2,7 @@
|
|||||||
"name": "Deep Diagram",
|
"name": "Deep Diagram",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"start_url": "https://www.deepdiagram.com",
|
"start_url": "https://www.deepdiagram.com",
|
||||||
|
"id": "com.deepdiagram",
|
||||||
"scope": "https://www.deepdiagram.com",
|
"scope": "https://www.deepdiagram.com",
|
||||||
"short_name": "Deep Diagram",
|
"short_name": "Deep Diagram",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
@ -40,9 +41,9 @@
|
|||||||
"label": "Example of menus on the web"
|
"label": "Example of menus on the web"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/assets/screenshots3.png",
|
"src": "/assets/screenshot3.png",
|
||||||
"sizes": "1024x1024",
|
"sizes": "1024x1024",
|
||||||
"type": "image/jpg",
|
"type": "image/png",
|
||||||
"label": "Example showing the web app on the Oculus Quest"
|
"label": "Example showing the web app on the Oculus Quest"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
141
public/pages/privacy.html
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<title>Deep Diagram Privacy Policy</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1, height=device-height" name="viewport">
|
||||||
|
<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">
|
||||||
|
<link as="script" href="/newRelic.js" rel="preload">
|
||||||
|
<script src="/newRelic.js"></script>
|
||||||
|
<style>
|
||||||
|
body * {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#privacyPolicy {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
top: 9%;
|
||||||
|
left: 9%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
background-color: rgba(0, 0, .2, 0.6);
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 30px;
|
||||||
|
border-color: #FFD700;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #FFf0d0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<img alt="background grid" id="loadingGrid" src="/assets/grid3.jpg"/>
|
||||||
|
<div id="privacyPolicy">
|
||||||
|
<p>
|
||||||
|
This privacy policy ("policy") will help you understand how Immersive Idea LLC. ("us", "we",
|
||||||
|
"our") uses and protects the data you provide to us when you visit and use https://www.deepdiagram.com
|
||||||
|
website.
|
||||||
|
</p>
|
||||||
|
We reserve the right to change this policy at any given time, of which you will be
|
||||||
|
promptly updated. If you want to make sure that you are up to date with the latest
|
||||||
|
changes, we advise you to frequently visit this page.
|
||||||
|
<p>
|
||||||
|
<h1>What User Data We Collect</h1>
|
||||||
|
<p>
|
||||||
|
When you visit the website, we may collect the following data:
|
||||||
|
<ul>
|
||||||
|
<li>Your IP address.</li>
|
||||||
|
<li>Your contact information and email address.</li>
|
||||||
|
<li>Other information such as interests and preferences.</li>
|
||||||
|
<li>Data profile regarding your online behavior on our website.</li>
|
||||||
|
</ul>
|
||||||
|
<h1>Why We Collect Your Data</h1>
|
||||||
|
<p>
|
||||||
|
We are collecting your data for several reasons:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>To better understand your needs.</li>
|
||||||
|
<li>To improve our services and products.</li>
|
||||||
|
<li>To send you promotional emails containing the information we think you will find interesting.</li>
|
||||||
|
<li>To contact you to fill out surveys and participate in other types of market
|
||||||
|
research.
|
||||||
|
</li>
|
||||||
|
<li>To customize our website according to your online behavior and personal
|
||||||
|
preferences.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h1>
|
||||||
|
Safeguarding and Securing the Data
|
||||||
|
</h1>
|
||||||
|
<p>Immersive Idea LLC is committed to securing your data and keeping it confidential.</p>
|
||||||
|
<p>Immersive Idea LLC has done all in its power to prevent data theft, unauthorized access,
|
||||||
|
and disclosure by implementing the latest technologies and software, which help us
|
||||||
|
safeguard all the information we collect online.
|
||||||
|
</p>
|
||||||
|
<h1>Our Cookie Policy</h1>
|
||||||
|
<p>
|
||||||
|
Once you agree to allow our website to use cookies, you also agree to use the data it
|
||||||
|
collects regarding your online behavior (analyze web traffic, web pages you visit and
|
||||||
|
spend the most time on).
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The data we collect by using cookies is used to customize our website to your needs.
|
||||||
|
After we use the data for statistical analysis, the data is completely removed from our
|
||||||
|
systems.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Please note that cookies don't allow us to gain control of your computer in any way.
|
||||||
|
They are strictly used to monitor which pages you find useful and which you do not so
|
||||||
|
that we can provide a better experience for you.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
|
||||||
|
If you want to disable cookies, you can do it by accessing the settings of your internet
|
||||||
|
browser. You can visit https://www.internetcookies.com, which contains comprehensive
|
||||||
|
information on how to do this on a wide variety of browsers and devices.
|
||||||
|
</p>
|
||||||
|
<h1>Links to Other Websites</h1>
|
||||||
|
<p>
|
||||||
|
Our website may contain links that lead to other websites. If you click on these links
|
||||||
|
Immersive Idea LLC is not held responsible for your data and privacy protection. Visiting
|
||||||
|
those websites is not governed by this privacy policy agreement. Make sure to read the
|
||||||
|
privacy policy documentation of the website you go to from our website.
|
||||||
|
</p>
|
||||||
|
<h1>
|
||||||
|
Restricting the Collection of your Personal Data
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
|
||||||
|
At some point, you might wish to restrict the use and collection of your personal data.
|
||||||
|
You can achieve this by doing the following:</p>
|
||||||
|
<p>
|
||||||
|
When you are filling the forms on the website, make sure to check if there is a box
|
||||||
|
which you can leave unchecked, if you don't want to disclose your personal information.
|
||||||
|
If you have already agreed to share your information with us, feel free to contact us via
|
||||||
|
email (support@immersiveidea.com) and we will be more than happy to change this for you.</p>
|
||||||
|
<p>
|
||||||
|
Immersive Idea LLC will not lease, sell or distribute your personal information to any third
|
||||||
|
parties, unless we have your permission. We might do so if the law forces us. Your
|
||||||
|
personal information will be used when we need to send you promotional materials if
|
||||||
|
you agree to this privacy policy.
|
||||||
|
</p>
|
||||||
|
<a href="https://www.deepdiagram.com">Home</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@ -1,198 +1,34 @@
|
|||||||
body {
|
body {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
margin: 0px;
|
margin: 0;
|
||||||
padding: 0px;
|
padding: 0;
|
||||||
|
background-color: #000;
|
||||||
|
background-image: url("/assets/grid6.jpg");
|
||||||
aspect-ratio: auto;
|
aspect-ratio: auto;
|
||||||
font-family: Roboto, sans-serif;
|
font-family: Roboto, sans-serif;
|
||||||
font-size: large;
|
font-size: large;
|
||||||
color: #4444ee;
|
color: #4444ee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.scene {
|
.scene {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#gameCanvas {
|
#gameCanvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0px;
|
margin: 0;
|
||||||
padding: 0px;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
div.overlay {
|
|
||||||
position: absolute;
|
|
||||||
background: #000;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
z-index: 12;
|
|
||||||
width: 320px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.overlay div {
|
|
||||||
background-color: #000000;
|
|
||||||
color: #FFD700;
|
|
||||||
padding: 15px 25px;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.overlay div a {
|
|
||||||
display: inline-block;
|
|
||||||
text-decoration: none;
|
|
||||||
border-color: #FFD700;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
padding: 10px;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.overlay div a:visited, div.overlay div a:link {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.overlay div a:hover {
|
|
||||||
background-color: #FFD700;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.overlay div a {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
div.overlay input {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 10px auto;
|
|
||||||
text-decoration: none;
|
|
||||||
border-color: #FFD700;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
padding: 10px;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div#create {
|
|
||||||
left: 600px;
|
|
||||||
top: 400px;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
z-index: 14;
|
|
||||||
width: 320px;
|
|
||||||
height: 344px;
|
|
||||||
border: 3px inset #FFD700;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.overlay div a.cancel {
|
|
||||||
font-size: small;
|
|
||||||
font-weight: lighter;
|
|
||||||
font-style: italic;
|
|
||||||
background-color: #222211;
|
|
||||||
color: #EEC755;
|
|
||||||
}
|
|
||||||
|
|
||||||
#diagramListContent ul {
|
|
||||||
list-style-type: none;
|
|
||||||
text-align: left;
|
|
||||||
padding: 0;
|
|
||||||
padding-inline-start: 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#diagramList {
|
|
||||||
overflow: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
#diagramList > h1 {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#diagramListContent li {
|
|
||||||
margin: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#diagramListContent li a {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.overlay div a.cancel:hover {
|
|
||||||
background-color: #EEC700;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#main.mini {
|
|
||||||
left: 100px;
|
|
||||||
top: 200px;
|
|
||||||
width: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main.mini img, #tutorial img {
|
|
||||||
width: 160px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main.mini div a, #tutorial div a {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: x-large;
|
|
||||||
font-weight: bolder;
|
|
||||||
text-align: center;
|
|
||||||
color: #F9F9E9;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tutorial {
|
|
||||||
z-index: 15;
|
|
||||||
left: 100px;
|
|
||||||
top: 560px;
|
|
||||||
width: 160px;
|
|
||||||
height: 210px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#diagramList {
|
|
||||||
left: 340px;
|
|
||||||
top: 240px;
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#create {
|
|
||||||
left: 500px;
|
|
||||||
top: 340px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#closekey, #closekey a:active, #closekey a:visited, #closekey a:link {
|
|
||||||
position: relative;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
#enterXR {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#enterXR.inactive a {
|
|
||||||
background-color: #222222;
|
|
||||||
color: #555555;
|
|
||||||
border-color: #222222;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
#enterXR.inactive {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#loadingGrid {
|
#loadingGrid {
|
||||||
|
position: relative;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
84
public/sw.js
@ -1,8 +1,25 @@
|
|||||||
const VERSION = '0';
|
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.1.0/workbox-sw.js');
|
||||||
const CACHE = "pwabuilder-offline";
|
const VERSION = '0.0.8-19';
|
||||||
const PRECACHE_ASSETS = []
|
const CACHE = "deepdiagram";
|
||||||
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');
|
const IMAGEDELIVERY_CACHE = "deepdiagram-images";
|
||||||
|
const MAPTILE_CACHE = 'maptiler';
|
||||||
|
|
||||||
|
// TODO: replace the following with the correct offline fallback page i.e.: const offlineFallbackPage = "offline.html";
|
||||||
|
const offlineFallbackPage = "/";
|
||||||
|
/*self.addEventListener('install', async (event) => {
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
self.addEventListener('activate', async (event) => {
|
||||||
|
self.skipWaiting();
|
||||||
|
self.clients.matchAll({
|
||||||
|
type: 'window'
|
||||||
|
}).then(windowClients => {
|
||||||
|
windowClients.forEach((windowClient) => {
|
||||||
|
windowClient.navigate(windowClient.url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
self.addEventListener("message", (event) => {
|
self.addEventListener("message", (event) => {
|
||||||
if (event.data && event.data.type === "SKIP_WAITING") {
|
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
@ -10,33 +27,74 @@ self.addEventListener("message", (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*self.addEventListener('install', async (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE)
|
||||||
|
.then((cache) => cache.add(offlineFallbackPage))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
/*if (workbox.navigationPreload.isSupported()) {
|
||||||
|
workbox.navigationPreload.enable();
|
||||||
|
}*/
|
||||||
|
|
||||||
workbox.routing.registerRoute(
|
workbox.routing.registerRoute(
|
||||||
new RegExp('/.*\\.png'),
|
new RegExp('/.*\\.wasm'),
|
||||||
new workbox.strategies.StaleWhileRevalidate({
|
new workbox.strategies.StaleWhileRevalidate({
|
||||||
cacheName: CACHE
|
cacheName: CACHE
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
workbox.routing.registerRoute(
|
workbox.routing.registerRoute(
|
||||||
new RegExp('/.*\\.jpeg'),
|
new RegExp('.*api.maptiler.com/.*'),
|
||||||
|
new workbox.strategies.CacheFirst({
|
||||||
|
plugins: [
|
||||||
|
new workbox.expiration.ExpirationPlugin({
|
||||||
|
maxEntries: 256,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 30,
|
||||||
|
purgeOnQuotaError: true,
|
||||||
|
matchOptions: {
|
||||||
|
ignoreVary: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new workbox.cacheableResponse.CacheableResponsePlugin({
|
||||||
|
statuses: [0, 200]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
cacheName: MAPTILE_CACHE
|
||||||
|
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/*workbox.routing.registerRoute(
|
||||||
|
new RegExp('/assets/.*'),
|
||||||
new workbox.strategies.StaleWhileRevalidate({
|
new workbox.strategies.StaleWhileRevalidate({
|
||||||
cacheName: CACHE
|
cacheName: CACHE
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
workbox.routing.registerRoute(
|
*/
|
||||||
new RegExp('/.*\\.jpg'),
|
|
||||||
|
/*workbox.routing.registerRoute(
|
||||||
|
new RegExp('/db/.*'),
|
||||||
new workbox.strategies.StaleWhileRevalidate({
|
new workbox.strategies.StaleWhileRevalidate({
|
||||||
cacheName: CACHE
|
cacheName: CACHE
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
workbox.routing.registerRoute(
|
workbox.routing.registerRoute(
|
||||||
new RegExp('/.*\\.glb'),
|
new RegExp('/.*\\.glb'),
|
||||||
new workbox.strategies.StaleWhileRevalidate({
|
new workbox.strategies.StaleWhileRevalidate({
|
||||||
cacheName: CACHE
|
cacheName: CACHE
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
/*
|
||||||
|
|
||||||
workbox.routing.registerRoute(
|
workbox.routing.registerRoute(
|
||||||
new RegExp('/login'),
|
new RegExp('/.*\\.css'),
|
||||||
new workbox.strategies.NetworkFirst()
|
new workbox.strategies.StaleWhileRevalidate({
|
||||||
)
|
cacheName: CACHE
|
||||||
|
})
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
|||||||
245
public/templates/demo.json
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
{
|
||||||
|
"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,13 +1,133 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import ViteExpress from "vite-express";
|
import ViteExpress from "vite-express";
|
||||||
|
import cors from "cors";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import expressProxy from "express-http-proxy";
|
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";
|
||||||
|
|
||||||
|
// Load .env.local first, then fall back to .env
|
||||||
|
dotenv.config({ path: '.env.local' });
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
// Console shim to forward logs to New Relic while preserving local output
|
||||||
|
const originalConsole = {
|
||||||
|
log: console.log.bind(console),
|
||||||
|
error: console.error.bind(console),
|
||||||
|
warn: console.warn.bind(console),
|
||||||
|
info: console.info.bind(console)
|
||||||
|
};
|
||||||
|
|
||||||
|
function forwardToNewRelic(level, args) {
|
||||||
|
const message = args.map(arg =>
|
||||||
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
||||||
|
).join(' ');
|
||||||
|
|
||||||
|
newrelic.recordLogEvent({
|
||||||
|
message,
|
||||||
|
level,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log = (...args) => {
|
||||||
|
forwardToNewRelic('info', args);
|
||||||
|
originalConsole.log(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.error = (...args) => {
|
||||||
|
forwardToNewRelic('error', args);
|
||||||
|
originalConsole.error(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.warn = (...args) => {
|
||||||
|
forwardToNewRelic('warn', args);
|
||||||
|
originalConsole.warn(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.info = (...args) => {
|
||||||
|
forwardToNewRelic('info', args);
|
||||||
|
originalConsole.info(...args);
|
||||||
|
};
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use("/api", expressProxy("local.immersiveidea.com"));
|
|
||||||
|
|
||||||
ViteExpress.listen(app, process.env.PORT || 3001, () => console.log("Server is listening..."));
|
// 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}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
178
server/api/claude.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
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;
|
||||||
213
server/api/cloudflare.js
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
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;
|
||||||
30
server/api/index.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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;
|
||||||
178
server/api/ollama.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
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;
|
||||||
176
server/api/session.js
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
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;
|
||||||
155
server/api/user.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
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;
|
||||||
19
server/etc/cert.pem
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-----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-----
|
||||||
126
server/etc/local.ini
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
; 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
|
||||||
9
server/etc/pubkey.pem
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+a7KpvY3xN/lb0pMDLY
|
||||||
|
PoVORggJf6ZxyXJlL7QyLt8vNIHsh5K5Zea4ehGHkUcn9Cnp4LVqFiz1aYCMWnzU
|
||||||
|
c/Nn/2ERsrdessxK1rvNWV09NCOe3mxT9nGv8zbEyMcfY/cbbfWyL4bbRzO8Nagi
|
||||||
|
2fJazytwTbARzKJcz78jGDGiHyR9z2mqk0tdO8wi+oC7Fds96O7ddxxDoHSWyE3v
|
||||||
|
YTBX3JbvcHSzsO4exsEDRYcpiZy91eCdDV1Tb8vwFHmz+AkkTQxSHaqGycoTfJQL
|
||||||
|
+FNbsDRCfRIwRl7LTiG8lZ4+efn2S/nYM9VdmlmqXohJ6HCfeFl2EeOmygdsTjwI
|
||||||
|
3QIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
101
server/etc/vm.args
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# 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>
|
||||||
88
server/middleware/dbAuth.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
113
server/server.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import websocket from "websocket";
|
||||||
|
import http from "http";
|
||||||
|
import {sha512} from "hash-wasm";
|
||||||
|
import log from "loglevel";
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
const logger = log.getLogger("server");
|
||||||
|
logger.setLevel("DEBUG", false);
|
||||||
|
const WebSocketServer = websocket.server;
|
||||||
|
//const http = require('http');
|
||||||
|
//const sha512 = require('hash-wasm').sha512;
|
||||||
|
const connections = new Map();
|
||||||
|
const server = http.createServer(function (request, response) {
|
||||||
|
logger.info((new Date()) + ' Received request for ' + request.url);
|
||||||
|
response.writeHead(404);
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
server.listen(8080, function () {
|
||||||
|
logger.info((new Date()) + ' Server is listening on port 8080');
|
||||||
|
});
|
||||||
|
|
||||||
|
const wsServer = new WebSocketServer({
|
||||||
|
httpServer: server,
|
||||||
|
// You should not use autoAcceptConnections for production
|
||||||
|
// applications, as it defeats all standard cross-origin protection
|
||||||
|
// facilities built into the protocol and the browser. You should
|
||||||
|
// *always* verify the connection's origin and decide whether or not
|
||||||
|
// to accept it.
|
||||||
|
autoAcceptConnections: false
|
||||||
|
});
|
||||||
|
|
||||||
|
function originIsAllowed(origin) {
|
||||||
|
return origin.indexOf('deepdiagram') > -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
wsServer.on('request', async (request) => {
|
||||||
|
if (!originIsAllowed(request.origin)) {
|
||||||
|
// Make sure we only accept requests from an allowed origin
|
||||||
|
request.reject();
|
||||||
|
logger.error((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
|
||||||
|
const connection = request.accept('echo-protocol', request.origin);
|
||||||
|
const hash = await sha512(connection.socket.remoteAddress + '-' + connection.socket.remotePort);
|
||||||
|
connections.set(hash, {connection: connection, db: null});
|
||||||
|
|
||||||
|
|
||||||
|
logger.info((new Date()) + ' Connection accepted.', connections.length);
|
||||||
|
connections.forEach((conn, key) => {
|
||||||
|
if (key != hash) {
|
||||||
|
conn.connection.sendUTF('{ "type": "newconnect", "netAttr": "' + hash + '" }');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
connection.on('message', function (message) {
|
||||||
|
logger.debug(message);
|
||||||
|
if (message.type === 'utf8') {
|
||||||
|
logger.debug('Received Message: ' + message.utf8Data);
|
||||||
|
connections.forEach((conn, index) => {
|
||||||
|
const envelope = JSON.parse(message.utf8Data);
|
||||||
|
if (index !== hash) {
|
||||||
|
if (envelope.db === conn.db) {
|
||||||
|
envelope.netAddr = hash;
|
||||||
|
conn.connection.sendUTF(JSON.stringify(envelope));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!conn.db && envelope.db) {
|
||||||
|
conn.db = envelope.db;
|
||||||
|
logger.debug('DB set to ' + envelope.db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//connection.sendUTF(message.utf8Data);
|
||||||
|
} else if (message.type === 'binary') {
|
||||||
|
logger.debug('Received Binary Message of ' + message.binaryData.length + ' bytes');
|
||||||
|
connections.forEach((conn, index) => {
|
||||||
|
if (index !== hash) {
|
||||||
|
conn.connection.sendBytes(message.utf8Data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
connection.on('close', function (reasonCode, description) {
|
||||||
|
connections.delete(hash);
|
||||||
|
connections.forEach((conn, index) => {
|
||||||
|
conn.connection.sendUTF('{ "type": "close", "netAddr": "' + hash + '" }');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
connection.on('error', function (reasonCode, description) {
|
||||||
|
connections.delete(hash);
|
||||||
|
connections.forEach((conn, index) => {
|
||||||
|
conn.connection.sendUTF('{ "type": "error", "netAddr": "' + hash + '" }');
|
||||||
|
});
|
||||||
|
logger.info((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.', connections.length);
|
||||||
|
});
|
||||||
|
setInterval(() => {
|
||||||
|
const message = `{ "count": ${connections.size} }`
|
||||||
|
logger.debug(message);
|
||||||
|
connection.sendUTF(message);
|
||||||
|
}, 10000);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
||||||
62
server/services/databaseService.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* 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 };
|
||||||
140
server/services/providerConfig.js
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
};
|
||||||
158
server/services/sessionStore.js
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
663
server/services/toolConverter.js
Normal file
@ -0,0 +1,663 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
};
|
||||||
241
server/services/usageTracker.js
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
/**
|
||||||
|
* 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(" | ");
|
||||||
|
}
|
||||||
144
src/content/upgradeCopy.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
/**
|
||||||
|
* 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',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
222
src/controllers/abstractController.ts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import {
|
||||||
|
AbstractMesh,
|
||||||
|
Scene,
|
||||||
|
Vector3,
|
||||||
|
WebXRControllerComponent,
|
||||||
|
WebXRDefaultExperience,
|
||||||
|
WebXRInputSource
|
||||||
|
} from "@babylonjs/core";
|
||||||
|
import {DiagramManager} from "../diagram/diagramManager";
|
||||||
|
import log from "loglevel";
|
||||||
|
|
||||||
|
import {grabAndClone} from "./functions/grabAndClone";
|
||||||
|
import {ClickMenu} from "../menus/clickMenu";
|
||||||
|
import {motionControllerInitObserver} from "./functions/motionControllerInitObserver";
|
||||||
|
import {DefaultScene} from "../defaultScene";
|
||||||
|
|
||||||
|
import {DiagramObject} from "../diagram/diagramObject";
|
||||||
|
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 {
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
private readonly _logger = log.getLogger('AbstractController');
|
||||||
|
private _clickStart: number = 0;
|
||||||
|
private _clickMenu: ClickMenu;
|
||||||
|
private _pickPoint: Vector3 = new Vector3();
|
||||||
|
private _meshUnderPointer: AbstractMesh;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(controller: WebXRInputSource,
|
||||||
|
xr: WebXRDefaultExperience,
|
||||||
|
diagramManager: DiagramManager) {
|
||||||
|
this._logger.debug('Base Controller Constructor called');
|
||||||
|
this.xrInputSource = controller;
|
||||||
|
this.scene = DefaultScene.Scene;
|
||||||
|
this.xr = xr;
|
||||||
|
|
||||||
|
this.scene.onPointerObservable.add((pointerInfo) => {
|
||||||
|
if (pointerInfo?.pickInfo?.gripTransform?.id == this.xrInputSource?.grip?.id) {
|
||||||
|
if (pointerInfo.pickInfo.pickedMesh) {
|
||||||
|
this._pickPoint.copyFrom(pointerInfo.pickInfo.pickedPoint);
|
||||||
|
this._meshUnderPointer = pointerInfo.pickInfo.pickedMesh;
|
||||||
|
} else {
|
||||||
|
this._meshUnderPointer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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._logger.debug(event);
|
||||||
|
switch (event.type) {
|
||||||
|
case ControllerEventType.PULSE:
|
||||||
|
if (event.gripId == this?.xrInputSource?.grip?.id) {
|
||||||
|
this.xrInputSource?.motionController?.pulse(.35, 50)
|
||||||
|
.then(() => {
|
||||||
|
this._logger.debug("pulse done");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ControllerEventType.HIDE:
|
||||||
|
this.disable();
|
||||||
|
break;
|
||||||
|
case ControllerEventType.SHOW:
|
||||||
|
this.enable();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public disable() {
|
||||||
|
this.scene.preventDefaultOnPointerDown = true;
|
||||||
|
this.xrInputSource.motionController.rootMesh.setEnabled(false)
|
||||||
|
this.xrInputSource.pointer.setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enable() {
|
||||||
|
this.scene.preventDefaultOnPointerDown = false;
|
||||||
|
this.xrInputSource.motionController.rootMesh.setEnabled(true);
|
||||||
|
this.xrInputSource.pointer.setEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initClicker(trigger: WebXRControllerComponent) {
|
||||||
|
this._logger.debug("initTrigger");
|
||||||
|
trigger.onButtonStateChangedObservable.add(() => {
|
||||||
|
if (viewOnly()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (trigger.changes.pressed) {
|
||||||
|
if (trigger.pressed) {
|
||||||
|
if (this._clickStart == 0) {
|
||||||
|
this._clickStart = Date.now();
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (this._clickStart > 0) {
|
||||||
|
this._logger.debug("grabbing and cloning");
|
||||||
|
const clone = grabAndClone(this.diagramManager, this._meshUnderPointer, this.xrInputSource.motionController.rootMesh);
|
||||||
|
|
||||||
|
this.grabbedObject = clone;
|
||||||
|
this.grabbedMesh = clone.mesh;
|
||||||
|
this.grabbedMeshType = getMeshType(clone.mesh, this.diagramManager);
|
||||||
|
this._meshUnderPointer = clone.mesh;
|
||||||
|
clone.grabbed = true;
|
||||||
|
}
|
||||||
|
}, 300, this);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const clickEnd = Date.now();
|
||||||
|
if (this._clickStart > 0 && (clickEnd - this._clickStart) < CLICK_TIME) {
|
||||||
|
this.click();
|
||||||
|
} else {
|
||||||
|
if (this.grabbedObject || this.grabbedMesh) {
|
||||||
|
this.drop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._clickStart = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 click() {
|
||||||
|
let mesh = this.xr.pointerSelection.getMeshUnderPointer(this.xrInputSource.uniqueId);
|
||||||
|
|
||||||
|
if (mesh && this.diagramManager.isDiagramObject(mesh)) {
|
||||||
|
this._logger.debug("click on " + mesh.id);
|
||||||
|
if (this.diagramManager.diagramMenuManager.connectionPreview) {
|
||||||
|
this.diagramManager.diagramMenuManager.connect(mesh);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (this._clickMenu) {
|
||||||
|
this._clickMenu.dispose();
|
||||||
|
}
|
||||||
|
this._clickMenu = this.diagramManager.diagramMenuManager.createClickMenu(mesh, this.xrInputSource);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._logger.debug("click on nothing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initGrip(grip: WebXRControllerComponent) {
|
||||||
|
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,278 +0,0 @@
|
|||||||
import {
|
|
||||||
AbstractMesh,
|
|
||||||
Mesh,
|
|
||||||
PhysicsMotionType,
|
|
||||||
Scene,
|
|
||||||
TransformNode,
|
|
||||||
Vector3,
|
|
||||||
WebXRControllerComponent,
|
|
||||||
WebXRDefaultExperience,
|
|
||||||
WebXRInputSource
|
|
||||||
} from "@babylonjs/core";
|
|
||||||
import {DiagramEventObserverMask, DiagramManager} from "../diagram/diagramManager";
|
|
||||||
import {DiagramEvent, DiagramEventType} from "../diagram/types/diagramEntity";
|
|
||||||
import log from "loglevel";
|
|
||||||
import {ControllerEventType, Controllers} from "./controllers";
|
|
||||||
import {toDiagramEntity} from "../diagram/functions/toDiagramEntity";
|
|
||||||
import {setupTransformNode} from "./functions/setupTransformNode";
|
|
||||||
import {reparent} from "./functions/reparent";
|
|
||||||
import {snapGridVal} from "../util/functions/snapGridVal";
|
|
||||||
import {snapRotateVal} from "../util/functions/snapRotateVal";
|
|
||||||
import {grabAndClone} from "./functions/grabAndClone";
|
|
||||||
import {isDiagramEntity} from "../diagram/functions/isDiagramEntity";
|
|
||||||
import {ClickMenu} from "../menus/clickMenu";
|
|
||||||
import {displayDebug} from "../util/displayDebug";
|
|
||||||
import {beforeRenderObserver} from "./functions/beforeRenderObserver";
|
|
||||||
import {motionControllerObserver} from "./functions/motionControllerObserver";
|
|
||||||
import {handleWasGrabbed} from "./functions/handleWasGrabbed";
|
|
||||||
import {buildDrop} from "./functions/buildDrop";
|
|
||||||
import {pointable} from "./functions/pointable";
|
|
||||||
import {DefaultScene} from "../defaultScene";
|
|
||||||
|
|
||||||
const CLICK_TIME = 300;
|
|
||||||
const logger = log.getLogger('Base');
|
|
||||||
|
|
||||||
export class Base {
|
|
||||||
static stickVector = Vector3.Zero();
|
|
||||||
protected xrInputSource: WebXRInputSource;
|
|
||||||
protected speedFactor = 4;
|
|
||||||
protected readonly scene: Scene;
|
|
||||||
protected grabbedMesh: AbstractMesh = null;
|
|
||||||
protected grabbedMeshParentId: string = null;
|
|
||||||
protected previousParentId: string = null;
|
|
||||||
protected previousRotation: Vector3 = null;
|
|
||||||
protected previousScaling: Vector3 = null;
|
|
||||||
protected previousPosition: Vector3 = null;
|
|
||||||
private clickStart: number = 0;
|
|
||||||
protected readonly xr: WebXRDefaultExperience;
|
|
||||||
protected readonly diagramManager: DiagramManager;
|
|
||||||
private lastPosition: Vector3 = null;
|
|
||||||
protected controllers: Controllers;
|
|
||||||
private clickMenu: ClickMenu;
|
|
||||||
private pickPoint: Vector3 = new Vector3();
|
|
||||||
|
|
||||||
constructor(controller: WebXRInputSource,
|
|
||||||
xr: WebXRDefaultExperience,
|
|
||||||
diagramManager: DiagramManager) {
|
|
||||||
this.xrInputSource = controller;
|
|
||||||
this.controllers = diagramManager.controllers;
|
|
||||||
this.scene = DefaultScene.Scene;
|
|
||||||
this.xr = xr;
|
|
||||||
this.scene.onPointerObservable.add((pointerInfo) => {
|
|
||||||
if (pointerInfo.pickInfo.pickedMesh?.metadata?.template) {
|
|
||||||
const mesh = pointerInfo.pickInfo.pickedMesh;
|
|
||||||
const pos = mesh.absolutePosition;
|
|
||||||
this.pickPoint.copyFrom(pointerInfo.pickInfo.pickedPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
this.diagramManager = diagramManager;
|
|
||||||
this.scene.onBeforeRenderObservable.add(beforeRenderObserver, -1, false, this);
|
|
||||||
|
|
||||||
//@TODO THis works, but it uses initGrip, not sure if this is the best idea
|
|
||||||
this.xrInputSource.onMotionControllerInitObservable.add(motionControllerObserver, -1, false, this);
|
|
||||||
this.controllers.controllerObserver.add((event) => {
|
|
||||||
logger.debug(event);
|
|
||||||
switch (event.type) {
|
|
||||||
case ControllerEventType.PULSE:
|
|
||||||
if (event.gripId == this?.xrInputSource?.grip?.id) {
|
|
||||||
this.xrInputSource?.motionController?.pulse(.25, 30)
|
|
||||||
.then(() => {
|
|
||||||
logger.debug("pulse done");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ControllerEventType.HIDE:
|
|
||||||
this.disable();
|
|
||||||
break;
|
|
||||||
case ControllerEventType.SHOW:
|
|
||||||
this.enable();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public disable() {
|
|
||||||
this.scene.preventDefaultOnPointerDown = true;
|
|
||||||
this.xrInputSource.motionController.rootMesh.setEnabled(false)
|
|
||||||
this.xrInputSource.pointer.setEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enable() {
|
|
||||||
this.scene.preventDefaultOnPointerDown = false;
|
|
||||||
this.xrInputSource.motionController.rootMesh.setEnabled(true);
|
|
||||||
this.xrInputSource.pointer.setEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected initClicker(trigger: WebXRControllerComponent) {
|
|
||||||
logger.debug("initTrigger");
|
|
||||||
trigger.onButtonStateChangedObservable.add(() => {
|
|
||||||
if (trigger.changes.pressed) {
|
|
||||||
if (trigger.pressed) {
|
|
||||||
if (this.clickStart == 0) {
|
|
||||||
this.clickStart = Date.now();
|
|
||||||
window.setTimeout(() => {
|
|
||||||
if (this.clickStart > 0) {
|
|
||||||
logger.debug("grabbing and cloning");
|
|
||||||
this.grab(true);
|
|
||||||
}
|
|
||||||
}, 300, this);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const clickEnd = Date.now();
|
|
||||||
if (this.clickStart > 0 && (clickEnd - this.clickStart) < CLICK_TIME) {
|
|
||||||
this.click();
|
|
||||||
}
|
|
||||||
this.drop();
|
|
||||||
this.clickStart = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, -1, false, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private grab(cloneEntity: boolean = false) {
|
|
||||||
let mesh = this.xr.pointerSelection.getMeshUnderPointer(this.xrInputSource.uniqueId);
|
|
||||||
|
|
||||||
if (!mesh) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let player = false;
|
|
||||||
displayDebug(mesh);
|
|
||||||
if (!isDiagramEntity(mesh)) {
|
|
||||||
if (handleWasGrabbed(mesh)) {
|
|
||||||
mesh && mesh.setParent(this.xrInputSource.motionController.rootMesh);
|
|
||||||
this.grabbedMesh = mesh;
|
|
||||||
} else {
|
|
||||||
if (mesh?.parent?.parent?.metadata?.grabbable) {
|
|
||||||
if (mesh?.parent?.parent?.parent) {
|
|
||||||
mesh = (mesh?.parent?.parent?.parent as Mesh);
|
|
||||||
this.grabbedMesh = mesh;
|
|
||||||
player = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (mesh?.metadata?.template == '#connection-template') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.previousParentId = mesh?.parent?.id;
|
|
||||||
logger.warn("grabbed " + mesh?.id + " parent " + this.previousParentId);
|
|
||||||
this.previousRotation = mesh?.rotation.clone();
|
|
||||||
this.previousScaling = mesh?.scaling.clone();
|
|
||||||
this.previousPosition = mesh?.position.clone();
|
|
||||||
|
|
||||||
if ((!mesh.metadata?.grabClone || player) && !cloneEntity) {
|
|
||||||
if (mesh.physicsBody) {
|
|
||||||
const transformNode = setupTransformNode(mesh, this.xrInputSource.motionController.rootMesh);
|
|
||||||
mesh.physicsBody.setMotionType(PhysicsMotionType.ANIMATED);
|
|
||||||
this.grabbedMeshParentId = transformNode.id;
|
|
||||||
} else {
|
|
||||||
mesh.setParent(this.xrInputSource.motionController.rootMesh);
|
|
||||||
}
|
|
||||||
this.grabbedMesh = mesh;
|
|
||||||
} else {
|
|
||||||
logger.debug("cloning " + mesh?.id);
|
|
||||||
const clone = grabAndClone(this.diagramManager, mesh, this.xrInputSource.motionController.rootMesh);
|
|
||||||
clone.newMesh.metadata.grabClone = false;
|
|
||||||
clone.newMesh.metadata.tool = false;
|
|
||||||
this.grabbedMeshParentId = clone.transformNode.id;
|
|
||||||
this.grabbedMesh = clone.newMesh;
|
|
||||||
this.previousParentId = null;
|
|
||||||
const event: DiagramEvent = {
|
|
||||||
type: DiagramEventType.ADD,
|
|
||||||
entity: toDiagramEntity(clone.newMesh)
|
|
||||||
}
|
|
||||||
this.diagramManager.onDiagramEventObservable.notifyObservers(event, DiagramEventObserverMask.ALL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private drop() {
|
|
||||||
const mesh = this.grabbedMesh;
|
|
||||||
if (!mesh) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (handleWasGrabbed(mesh)) {
|
|
||||||
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));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reparent(mesh, this.previousParentId, this.grabbedMeshParentId);
|
|
||||||
this.grabbedMeshParentId = null;
|
|
||||||
if (!mesh.physicsBody) {
|
|
||||||
const transform = new TransformNode('temp', this.scene);
|
|
||||||
transform.position = this.pickPoint;
|
|
||||||
mesh.setParent(transform);
|
|
||||||
mesh.rotation = snapRotateVal(mesh.rotation, this.diagramManager._config.current.rotateSnap);
|
|
||||||
transform.position = snapGridVal(transform.position, this.diagramManager._config.current.gridSnap);
|
|
||||||
mesh.setParent(null);
|
|
||||||
mesh.position = snapGridVal(mesh.position, this.diagramManager._config.current.gridSnap);
|
|
||||||
//mesh.position = snapGridVal(mesh.position, this.diagramManager._config.current.gridSnap);
|
|
||||||
//mesh.setPivotPoint(transform.position, Space.WORLD)
|
|
||||||
|
|
||||||
|
|
||||||
//transform.dispose();
|
|
||||||
}
|
|
||||||
this.previousParentId = null;
|
|
||||||
this.previousScaling = null;
|
|
||||||
this.previousRotation = null;
|
|
||||||
this.previousPosition = null;
|
|
||||||
this.grabbedMesh = null;
|
|
||||||
if (isDiagramEntity(mesh) && (mesh?.metadata?.template.indexOf('#') == -1)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const event: DiagramEvent = buildDrop(mesh);
|
|
||||||
|
|
||||||
const body = mesh?.physicsBody;
|
|
||||||
if (body) {
|
|
||||||
body.setMotionType(PhysicsMotionType.DYNAMIC);
|
|
||||||
logger.debug(body.transformNode.absolutePosition);
|
|
||||||
logger.debug(this.lastPosition);
|
|
||||||
if (this.lastPosition) {
|
|
||||||
body.setLinearVelocity(body.transformNode.absolutePosition.subtract(this.lastPosition).scale(20));
|
|
||||||
//body.setLinearVelocity(this.lastPosition.subtract(body.transformNode.absolutePosition).scale(20));
|
|
||||||
logger.debug(this.lastPosition.subtract(body.transformNode.absolutePosition).scale(20));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.diagramManager.onDiagramEventObservable.notifyObservers(event, DiagramEventObserverMask.ALL);
|
|
||||||
}
|
|
||||||
|
|
||||||
private click() {
|
|
||||||
let mesh = this.xr.pointerSelection.getMeshUnderPointer(this.xrInputSource.uniqueId);
|
|
||||||
if (pointable(mesh)) {
|
|
||||||
logger.debug("click on " + mesh.id);
|
|
||||||
if (this.clickMenu && !this.clickMenu.isDisposed) {
|
|
||||||
if (this.clickMenu.isConnecting) {
|
|
||||||
this.clickMenu.connect(mesh);
|
|
||||||
this.clickMenu = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.clickMenu = this.diagramManager.diagramMenuManager.createClickMenu(mesh, this.xrInputSource.grip);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
logger.debug("click on nothing");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private initGrip(grip: WebXRControllerComponent) {
|
|
||||||
grip.onButtonStateChangedObservable.add(() => {
|
|
||||||
if (grip.changes.pressed) {
|
|
||||||
if (grip.pressed) {
|
|
||||||
this.grab();
|
|
||||||
} else {
|
|
||||||
this.drop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +1,6 @@
|
|||||||
import {AbstractMesh, Observable, TransformNode, Vector3, WebXRInputSource} from "@babylonjs/core";
|
import {AbstractMesh, Observable, TransformNode} from "@babylonjs/core";
|
||||||
|
import {ControllerEvent} from "./types/controllerEvent";
|
||||||
|
|
||||||
export type ControllerEvent = {
|
|
||||||
type: ControllerEventType,
|
|
||||||
value?: number,
|
|
||||||
startPosition?: Vector3,
|
|
||||||
endPosition?: Vector3,
|
|
||||||
duration?: number,
|
|
||||||
gripId?: string;
|
|
||||||
controller?: WebXRInputSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ControllerEventType {
|
export var movable: TransformNode | AbstractMesh;
|
||||||
GRIP = 'grip',
|
export const controllerObservable: Observable<ControllerEvent> = new Observable();
|
||||||
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 controllerObserver: Observable<ControllerEvent> = new Observable();
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import {HavokPlugin} from "@babylonjs/core";
|
|
||||||
import {DefaultScene} from "../../defaultScene";
|
|
||||||
|
|
||||||
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 {
|
|
||||||
this.logger.error("parent not found for " + this.grabbedMeshParentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.logger.warn("no parent id");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -10,34 +10,39 @@ import {
|
|||||||
WebXRDefaultExperience
|
WebXRDefaultExperience
|
||||||
} from "@babylonjs/core";
|
} from "@babylonjs/core";
|
||||||
|
|
||||||
import {buildStandardMaterial} from "../../materials/functions/buildStandardMaterial";
|
|
||||||
import {DefaultScene} from "../../defaultScene";
|
import {DefaultScene} from "../../defaultScene";
|
||||||
|
|
||||||
export function buildRig(xr: WebXRDefaultExperience): Mesh {
|
export function buildRig(xr: WebXRDefaultExperience): Mesh {
|
||||||
const scene = DefaultScene.Scene;
|
const scene = DefaultScene.Scene;
|
||||||
const rigMesh = MeshBuilder.CreateCylinder("platform", {diameter: .5, height: .01}, scene);
|
const rigMesh = MeshBuilder.CreateCylinder("platform", {diameter: .5, height: .01}, scene);
|
||||||
|
rigMesh.setAbsolutePosition(new Vector3(0, .01, 5));
|
||||||
const cameratransform = new TransformNode("cameraTransform", scene);
|
const cameratransform = new TransformNode("cameraTransform", scene);
|
||||||
cameratransform.parent = rigMesh;
|
cameratransform.parent = rigMesh;
|
||||||
xr.baseExperience.onInitialXRPoseSetObservable.add(() => {
|
xr.baseExperience.onInitialXRPoseSetObservable.add(() => {
|
||||||
xr.baseExperience.camera.parent = cameratransform;
|
xr.baseExperience.camera.parent = cameratransform;
|
||||||
xr.baseExperience.camera.position = new Vector3(0, 0, 0);
|
xr.baseExperience.camera.position = new Vector3(0, 0, 0);
|
||||||
|
cameratransform.rotation.set(0, Math.PI, 0);
|
||||||
});
|
});
|
||||||
for (const cam of scene.cameras) {
|
for (const cam of scene.cameras) {
|
||||||
cam.parent = cameratransform;
|
cam.parent = cameratransform;
|
||||||
|
cam.position = new Vector3(0, 1.6, 0); // Reset to local position above platform
|
||||||
}
|
}
|
||||||
scene.onActiveCameraChanged.add(() => {
|
scene.onActiveCameraChanged.add(() => {
|
||||||
for (const cam of scene.cameras) {
|
for (const cam of scene.cameras) {
|
||||||
cam.parent = cameratransform;
|
cam.parent = cameratransform;
|
||||||
|
cam.position = new Vector3(0, 1.6, 0); // Reset to local position above platform
|
||||||
}
|
}
|
||||||
cameratransform.rotation.set(0, Math.PI, 0);
|
|
||||||
});
|
});
|
||||||
rigMesh.material = buildStandardMaterial("rigMaterial", scene, "#2222ff");
|
|
||||||
rigMesh.setAbsolutePosition(new Vector3(0, .01, 4));
|
|
||||||
rigMesh.isPickable = false;
|
rigMesh.isPickable = false;
|
||||||
const axis = new AxesViewer(scene, .25);
|
const axis = new AxesViewer(scene, .25);
|
||||||
axis.zAxis.rotation.y = Math.PI;
|
axis.zAxis.rotation.y = Math.PI;
|
||||||
rigMesh.lookAt(new Vector3(0, 0.01, 0));
|
rigMesh.lookAt(new Vector3(0, 0.01, 0));
|
||||||
rigMesh.visibility = 1;
|
rigMesh.visibility = 1;
|
||||||
|
|
||||||
|
// Only create physics aggregate if physics engine is available
|
||||||
|
if (scene.getPhysicsEngine()) {
|
||||||
const rigAggregate =
|
const rigAggregate =
|
||||||
new PhysicsAggregate(
|
new PhysicsAggregate(
|
||||||
rigMesh,
|
rigMesh,
|
||||||
@ -45,5 +50,20 @@ export function buildRig(xr: WebXRDefaultExperience): Mesh {
|
|||||||
{friction: 0, center: Vector3.Zero(), mass: 50, restitution: .01},
|
{friction: 0, center: Vector3.Zero(), mass: 50, restitution: .01},
|
||||||
scene);
|
scene);
|
||||||
rigAggregate.body.setMotionType(PhysicsMotionType.DYNAMIC);
|
rigAggregate.body.setMotionType(PhysicsMotionType.DYNAMIC);
|
||||||
|
} else {
|
||||||
|
// Add physics aggregate once physics is initialized
|
||||||
|
scene.onReadyObservable.addOnce(() => {
|
||||||
|
if (scene.getPhysicsEngine()) {
|
||||||
|
const rigAggregate =
|
||||||
|
new PhysicsAggregate(
|
||||||
|
rigMesh,
|
||||||
|
PhysicsShapeType.CYLINDER,
|
||||||
|
{friction: 0, center: Vector3.Zero(), mass: 50, restitution: .01},
|
||||||
|
scene);
|
||||||
|
rigAggregate.body.setMotionType(PhysicsMotionType.DYNAMIC);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return rigMesh;
|
return rigMesh;
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/controllers/functions/getMeshType.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {AbstractMesh} from "@babylonjs/core";
|
||||||
|
import {MeshTypeEnum} from "../../diagram/types/meshTypeEnum";
|
||||||
|
import {Toolbox} from "../../toolbox/toolbox";
|
||||||
|
import {handleWasGrabbed} from "./handleWasGrabbed";
|
||||||
|
import {DiagramManager} from "../../diagram/diagramManager";
|
||||||
|
|
||||||
|
export function getMeshType(mesh: AbstractMesh, diagramManager: DiagramManager): MeshTypeEnum {
|
||||||
|
if (Toolbox.instance.isTool(mesh)) {
|
||||||
|
return MeshTypeEnum.TOOL;
|
||||||
|
}
|
||||||
|
if (handleWasGrabbed(mesh)) {
|
||||||
|
return MeshTypeEnum.HANDLE;
|
||||||
|
}
|
||||||
|
if (diagramManager.isDiagramObject(mesh)) {
|
||||||
|
return MeshTypeEnum.ENTITY;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -1,20 +1,42 @@
|
|||||||
import {AbstractMesh, TransformNode} from "@babylonjs/core";
|
import {AbstractMesh} from "@babylonjs/core";
|
||||||
import {DiagramManager} from "../../diagram/diagramManager";
|
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";
|
import {DefaultScene} from "../../defaultScene";
|
||||||
|
|
||||||
export function grabAndClone(diagramManager: DiagramManager, mesh: AbstractMesh, parent: AbstractMesh):
|
export function grabAndClone(diagramManager: DiagramManager, mesh: AbstractMesh, parent: AbstractMesh):
|
||||||
{ transformNode: TransformNode, newMesh: AbstractMesh } {
|
DiagramObject {
|
||||||
const scene = DefaultScene.Scene;
|
const logger = log.getLogger('grabAndClone');
|
||||||
const newMesh = diagramManager.createCopy(mesh, true);
|
if (diagramManager.isDiagramObject(mesh)) {
|
||||||
const transformNode = new TransformNode("grabAnchor", scene);
|
logger.debug('grabAndClone called with diagram object', mesh.id);
|
||||||
transformNode.id = "grabAnchor";
|
const diagramObject = diagramManager.createCopy(mesh.id);
|
||||||
transformNode.position = newMesh.position.clone();
|
if (!diagramObject) {
|
||||||
if (newMesh.rotationQuaternion) {
|
logger.warn('grabAndClone called with invalid diagram object', mesh.id);
|
||||||
transformNode.rotationQuaternion = newMesh.rotationQuaternion.clone();
|
return null;
|
||||||
|
}
|
||||||
|
diagramObject.baseTransform.setParent(parent);
|
||||||
|
diagramManager.addObject(diagramObject);
|
||||||
|
return diagramObject;
|
||||||
} else {
|
} else {
|
||||||
transformNode.rotation = newMesh.rotation.clone();
|
const entity = {
|
||||||
|
template: mesh.metadata.template,
|
||||||
|
color: mesh.metadata.color,
|
||||||
|
position: vectoxys(mesh.absolutePosition),
|
||||||
|
rotation: vectoxys(mesh.absoluteRotationQuaternion.toEulerAngles()),
|
||||||
|
scale: vectoxys(mesh.scaling),
|
||||||
|
type: DiagramEntityType.ENTITY
|
||||||
|
|
||||||
|
}
|
||||||
|
const obj = new DiagramObject(DefaultScene.Scene,
|
||||||
|
diagramManager.onDiagramEventObservable,
|
||||||
|
{
|
||||||
|
diagramEntity: entity,
|
||||||
|
actionManager: diagramManager.actionManager
|
||||||
|
});
|
||||||
|
obj.baseTransform.setParent(parent);
|
||||||
|
diagramManager.addObject(obj);
|
||||||
|
return obj;
|
||||||
}
|
}
|
||||||
transformNode.setParent(parent);
|
|
||||||
newMesh.setParent(transformNode);
|
|
||||||
return {transformNode: transformNode, newMesh: newMesh};
|
|
||||||
}
|
}
|
||||||
@ -1,10 +1,15 @@
|
|||||||
import {AbstractMesh} from "@babylonjs/core";
|
import {AbstractMesh} from "@babylonjs/core";
|
||||||
import {isDiagramEntity} from "../../diagram/functions/isDiagramEntity";
|
import {isDiagramEntity} from "../../diagram/functions/isDiagramEntity";
|
||||||
|
import log from "loglevel";
|
||||||
|
|
||||||
export function handleWasGrabbed(mesh: AbstractMesh): boolean {
|
export function handleWasGrabbed(mesh: AbstractMesh): boolean {
|
||||||
|
const logger = log.getLogger("handleWasGrabbed");
|
||||||
if (isDiagramEntity(mesh)) {
|
if (isDiagramEntity(mesh)) {
|
||||||
|
logger.debug("handleWasGrabbed: mesh is a diagram entity");
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return (mesh?.metadata?.handle == true);
|
const result = (mesh?.metadata?.handle == true);
|
||||||
|
logger.debug("handleWasGrabbed: mesh ", result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
|
|
||||||
|
|
||||||
|
export function motionControllerInitObserver(init) {
|
||||||
const logger = log.getLogger('motionControllerObserver');
|
const logger = log.getLogger('motionControllerObserver');
|
||||||
export function motionControllerObserver(init) {
|
|
||||||
logger.debug(init.components);
|
logger.debug(init.components);
|
||||||
if (init.components['xr-standard-squeeze']) {
|
if (init.components['xr-standard-squeeze']) {
|
||||||
this.initGrip(init.components['xr-standard-squeeze'])
|
this.initGrip(init.components['xr-standard-squeeze'])
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import {AbstractMesh} from "@babylonjs/core";
|
|
||||||
|
|
||||||
export function pointable(mesh: AbstractMesh): boolean {
|
|
||||||
return (mesh && mesh.metadata?.template &&
|
|
||||||
!mesh.metadata?.tool &&
|
|
||||||
!mesh.metadata?.handle &&
|
|
||||||
!mesh.metadata?.grabbable &&
|
|
||||||
!mesh.metadata?.grabClone);
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import {AbstractMesh} from "@babylonjs/core";
|
|
||||||
import log from "loglevel";
|
|
||||||
|
|
||||||
const logger = log.getLogger('reparent');
|
|
||||||
|
|
||||||
export function reparent(mesh: AbstractMesh, previousParentId: string, grabbedMeshParentId: string) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
24
src/controllers/functions/snapAll.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {TransformNode, Vector3} from "@babylonjs/core";
|
||||||
|
import {appConfigInstance} 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;
|
||||||
|
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.setParent(null);
|
||||||
|
if (config.locationSnap > 0) {
|
||||||
|
node.position = snapGridVal(node.absolutePosition, config.locationSnap);
|
||||||
|
}
|
||||||
|
|
||||||
|
transform.dispose();
|
||||||
|
}
|
||||||
@ -1,143 +0,0 @@
|
|||||||
import {
|
|
||||||
TransformNode,
|
|
||||||
Vector2,
|
|
||||||
Vector3,
|
|
||||||
WebXRControllerComponent,
|
|
||||||
WebXRDefaultExperience,
|
|
||||||
WebXRInputSource
|
|
||||||
} from "@babylonjs/core";
|
|
||||||
import {Base} from "./base";
|
|
||||||
import {ControllerEventType} from "./controllers";
|
|
||||||
import log from "loglevel";
|
|
||||||
import {DiagramManager} from "../diagram/diagramManager";
|
|
||||||
import {RoundButton} from "../objects/roundButton";
|
|
||||||
import {DefaultScene} from "../defaultScene";
|
|
||||||
|
|
||||||
const logger = log.getLogger('Left');
|
|
||||||
export class Left extends Base {
|
|
||||||
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) => {
|
|
||||||
logger.trace(`thumbstick moved ${value.x}, ${value.y}`);
|
|
||||||
if (!this.controllers.movable) {
|
|
||||||
this.moveRig(value);
|
|
||||||
} else {
|
|
||||||
this.moveMovable(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (init.components['x-button']) {
|
|
||||||
const transform = new TransformNode('x-button', scene);
|
|
||||||
transform.parent = controller.grip;
|
|
||||||
transform.rotation.x = Math.PI / 2;
|
|
||||||
transform.scaling = new Vector3(.2, .2, .2);
|
|
||||||
const xbutton = new RoundButton(transform, 'X', 'toggle toolbox menu', new Vector2(-.5, -.1));
|
|
||||||
const ybutton = new RoundButton(transform, 'Y', 'toggle settings menu', new Vector2(-.4, .1));
|
|
||||||
}
|
|
||||||
this.initXButton(init.components['x-button']);
|
|
||||||
this.initYButton(init.components['y-button']);
|
|
||||||
const buttonhome = new TransformNode('buttons', scene)
|
|
||||||
|
|
||||||
this.initTrigger(init.components['xr-standard-trigger']);
|
|
||||||
init.components['xr-standard-thumbstick'].onButtonStateChangedObservable.add((value) => {
|
|
||||||
if (value.pressed) {
|
|
||||||
logger.trace('Left', 'thumbstick changed');
|
|
||||||
this.controllers.controllerObserver.notifyObservers({
|
|
||||||
type: ControllerEventType.DECREASE_VELOCITY,
|
|
||||||
value: value.value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private initTrigger(trigger: WebXRControllerComponent) {
|
|
||||||
if (trigger) {
|
|
||||||
trigger
|
|
||||||
.onButtonStateChangedObservable
|
|
||||||
.add((button) => {
|
|
||||||
logger.trace('trigger pressed');
|
|
||||||
this.controllers.controllerObserver.notifyObservers({
|
|
||||||
type: ControllerEventType.TRIGGER,
|
|
||||||
value: button.value,
|
|
||||||
controller: this.xrInputSource
|
|
||||||
});
|
|
||||||
}, -1, false, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private initXButton(xbutton: WebXRControllerComponent) {
|
|
||||||
if (xbutton) {
|
|
||||||
xbutton.onButtonStateChangedObservable.add((button) => {
|
|
||||||
if (button.pressed) {
|
|
||||||
logger.trace('X button pressed');
|
|
||||||
this.controllers.controllerObserver.notifyObservers({
|
|
||||||
type: ControllerEventType.X_BUTTON,
|
|
||||||
value: button.value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private initYButton(ybutton: WebXRControllerComponent) {
|
|
||||||
if (ybutton) {
|
|
||||||
ybutton.onButtonStateChangedObservable.add((button) => {
|
|
||||||
if (button.pressed) {
|
|
||||||
logger.trace('Y button pressed');
|
|
||||||
this.controllers.controllerObserver.notifyObservers({
|
|
||||||
type: ControllerEventType.Y_BUTTON,
|
|
||||||
value: button.value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private moveMovable(value: { x: number, y: number }) {
|
|
||||||
if (Math.abs(value.x) > .1) {
|
|
||||||
this.controllers.movable.position.x += .005 * Math.sign(value.x);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
}
|
|
||||||
if (Math.abs(value.y) > .1) {
|
|
||||||
this.controllers.movable.position.y += -.005 * Math.sign(value.y);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private moveRig(value: { x: number, y: number }) {
|
|
||||||
if (Math.abs(value.x) > .1) {
|
|
||||||
this.controllers.controllerObserver.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.controllerObserver.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.controllerObserver.notifyObservers({type: ControllerEventType.LEFT_RIGHT, value: 0});
|
|
||||||
this.controllers.controllerObserver.notifyObservers({type: ControllerEventType.FORWARD_BACK, value: 0});
|
|
||||||
} else {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||