Procházet zdrojové kódy

feat: add embedded GCode viewer (#963)

* feat: add embedded GCode viewer

Adds PrettyGCode as a built-in GCode visualiser embedded directly in the
Bambuddy layout, so users can preview and inspect GCode files without
leaving the dashboard.
Nathen Fredrick před 1 měsícem
rodič
revize
3adce435ee
38 změnil soubory, kde provedl 14538 přidání a 19 odebrání
  1. 12 3
      backend/app/api/routes/library.py
  2. 61 13
      backend/app/main.py
  3. 85 0
      backend/tests/integration/test_gcode_viewer.py
  4. 115 0
      backend/tests/unit/test_library_file_path_guard.py
  5. 2 0
      frontend/src/App.tsx
  6. 2 1
      frontend/src/components/Layout.tsx
  7. 4 0
      frontend/src/i18n/locales/de.ts
  8. 4 0
      frontend/src/i18n/locales/en.ts
  9. 4 0
      frontend/src/i18n/locales/fr.ts
  10. 4 0
      frontend/src/i18n/locales/it.ts
  11. 4 0
      frontend/src/i18n/locales/ja.ts
  12. 4 0
      frontend/src/i18n/locales/pt-BR.ts
  13. 4 0
      frontend/src/i18n/locales/zh-CN.ts
  14. 4 0
      frontend/src/i18n/locales/zh-TW.ts
  15. 28 0
      frontend/src/pages/GCodeViewerPage.tsx
  16. 68 1
      frontend/vite.config.ts
  17. 60 0
      gcode_viewer/VENDORED.md
  18. 417 0
      gcode_viewer/css/prettygcode.css
  19. 264 0
      gcode_viewer/index.html
  20. 31 0
      gcode_viewer/js/Line2.js
  21. 104 0
      gcode_viewer/js/LineGeometry.js
  22. 391 0
      gcode_viewer/js/LineMaterial.js
  23. 65 0
      gcode_viewer/js/LineSegments2.js
  24. 258 0
      gcode_viewer/js/LineSegmentsGeometry.js
  25. 185 0
      gcode_viewer/js/Lut.js
  26. 797 0
      gcode_viewer/js/OBJLoader.js
  27. 877 0
      gcode_viewer/js/bambuddy_adapter.js
  28. 1070 0
      gcode_viewer/js/camera-controls.js
  29. 1672 0
      gcode_viewer/js/dat.gui.js
  30. 0 0
      gcode_viewer/js/helvetiker_bold.typeface.json
  31. 1 0
      gcode_viewer/js/jquery.min.js
  32. 8 0
      gcode_viewer/js/models/ExtruderNozzle.mtl
  33. 5282 0
      gcode_viewer/js/models/ExtruderNozzle.obj
  34. 1932 0
      gcode_viewer/js/prettygcode.js
  35. 142 0
      gcode_viewer/js/slider-shim.js
  36. 576 0
      gcode_viewer/js/three.min.js
  37. 0 0
      static/assets/index-BfEnlXcp.js
  38. 1 1
      static/index.html

+ 12 - 3
backend/app/api/routes/library.py

@@ -104,11 +104,20 @@ def to_absolute_path(relative_path: str | None) -> Path | None:
     """Convert a relative path (from database) to an absolute path for file operations."""
     """Convert a relative path (from database) to an absolute path for file operations."""
     if not relative_path:
     if not relative_path:
         return None
         return None
-    # Handle already-absolute paths (for backwards compatibility during migration)
     path = Path(relative_path)
     path = Path(relative_path)
+    # Handle already-absolute paths verbatim (backwards compatibility during migration).
+    # Legacy DB rows may store absolute paths that predate the base_dir layout; the
+    # traversal guard below only applies to relative paths coming from user input.
     if path.is_absolute():
     if path.is_absolute():
-        return path
-    return Path(app_settings.base_dir) / relative_path
+        return path.resolve()
+    base = Path(app_settings.base_dir).resolve()
+    resolved = (base / relative_path).resolve()
+    # Guard against path traversal — resolved path must stay inside base_dir.
+    # Use is_relative_to() to avoid the /data/app vs /data/app_evil prefix confusion
+    # that a plain startswith(str(base)) check would miss.
+    if not resolved.is_relative_to(base):
+        raise ValueError(f"Path escapes base directory: {relative_path!r}")
+    return resolved
 
 
 
 
 def calculate_file_hash(file_path: Path) -> str:
 def calculate_file_hash(file_path: Path) -> str:

+ 61 - 13
backend/app/main.py

@@ -1,5 +1,6 @@
 import asyncio
 import asyncio
 import logging
 import logging
+import mimetypes as _mimetypes
 import posixpath
 import posixpath
 import time
 import time
 from contextlib import asynccontextmanager
 from contextlib import asynccontextmanager
@@ -4331,19 +4332,37 @@ async def security_headers_middleware(request, call_next):
     #   - img-src data: / blob:: base64 thumbnails and Blob-URL timelapse previews.
     #   - img-src data: / blob:: base64 thumbnails and Blob-URL timelapse previews.
     #   - media-src blob:: timelapse video player uses Blob URLs.
     #   - media-src blob:: timelapse video player uses Blob URLs.
     #   - font-src data:: some icon fonts are embedded as data URIs.
     #   - font-src data:: some icon fonts are embedded as data URIs.
-    response.headers["Content-Security-Policy"] = (
-        "default-src 'self'; "
-        "script-src 'self'; "
-        "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
-        "img-src 'self' data: blob:; "
-        "media-src 'self' blob:; "
-        "connect-src 'self' ws: wss:; "
-        "font-src 'self' data: https://fonts.gstatic.com; "
-        "object-src 'none'; "
-        "base-uri 'self'; "
-        "frame-src 'self' http: https:; "
-        "frame-ancestors 'none';"
-    )
+    if request.url.path.startswith("/gcode-viewer"):
+        # The gcode viewer is embedded in an iframe served by this same origin,
+        # so frame-ancestors must allow 'self'.  prettygcode.js also uses eval()
+        # internally, so script-src needs 'unsafe-eval'.
+        response.headers["Content-Security-Policy"] = (
+            "default-src 'self'; "
+            "script-src 'self' 'unsafe-eval'; "
+            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
+            "img-src 'self' data: blob:; "
+            "media-src 'self' blob:; "
+            "connect-src 'self' ws: wss:; "
+            "font-src 'self' data: https://fonts.gstatic.com; "
+            "object-src 'none'; "
+            "base-uri 'self'; "
+            "frame-src 'self' http: https:; "
+            "frame-ancestors 'self';"
+        )
+    else:
+        response.headers["Content-Security-Policy"] = (
+            "default-src 'self'; "
+            "script-src 'self'; "
+            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
+            "img-src 'self' data: blob:; "
+            "media-src 'self' blob:; "
+            "connect-src 'self' ws: wss:; "
+            "font-src 'self' data: https://fonts.gstatic.com; "
+            "object-src 'none'; "
+            "base-uri 'self'; "
+            "frame-src 'self' http: https:; "
+            "frame-ancestors 'none';"
+        )
     if request.url.scheme == "https":
     if request.url.scheme == "https":
         response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
         response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
     return response
     return response
@@ -4591,6 +4610,35 @@ async def serve_sw_register():
     return {"error": "sw-register.js not found"}
     return {"error": "sw-register.js not found"}
 
 
 
 
+# ── GCode viewer static files ────────────────────────────────────────────────
+# Served via explicit routes so ordering is guaranteed (app.mount() loses
+# to the /{full_path:path} catch-all in some Starlette versions).
+_gcode_viewer_dir = (app_settings.static_dir.parent / "gcode_viewer").resolve()
+
+
+def _gcode_viewer_response(rel: str) -> FileResponse:
+    from fastapi import HTTPException as _HTTPException
+
+    safe = (_gcode_viewer_dir / rel).resolve()
+    if not safe.is_relative_to(_gcode_viewer_dir):
+        raise _HTTPException(status_code=403)
+    if safe.is_file():
+        mt, _ = _mimetypes.guess_type(str(safe))
+        return FileResponse(str(safe), media_type=mt or "application/octet-stream")
+    raise _HTTPException(status_code=404)
+
+
+@app.get("/gcode-viewer")
+@app.get("/gcode-viewer/")
+async def serve_gcode_viewer_index() -> FileResponse:
+    return _gcode_viewer_response("index.html")
+
+
+@app.get("/gcode-viewer/{file_path:path}")
+async def serve_gcode_viewer_file(file_path: str) -> FileResponse:
+    return _gcode_viewer_response(file_path)
+
+
 # Catch-all route for React Router (must be last)
 # Catch-all route for React Router (must be last)
 @app.get("/{full_path:path}")
 @app.get("/{full_path:path}")
 async def serve_spa(full_path: str):
 async def serve_spa(full_path: str):

+ 85 - 0
backend/tests/integration/test_gcode_viewer.py

@@ -0,0 +1,85 @@
+"""Integration tests for the /gcode-viewer static-file routes.
+
+Covers two behaviours added by the GCode viewer PR:
+
+1. Route ordering — /gcode-viewer/* is served by explicit @app.get routes
+   that are registered before the /{full_path:path} SPA catch-all, so the
+   GCode viewer is never accidentally served the React app HTML.
+
+2. Path-traversal guard — requests for paths that escape gcode_viewer/
+   (e.g. /gcode-viewer/../main.py) must return 403, not the file contents.
+"""
+
+import pytest
+from httpx import AsyncClient
+
+
+class TestGCodeViewerRouteOrdering:
+    """Verify the /gcode-viewer routes are reachable and distinct from the SPA."""
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_gcode_viewer_index_does_not_fall_through_to_spa(
+        self, async_client: AsyncClient
+    ):
+        """GET /gcode-viewer/ must not return the React SPA index.html.
+
+        If route ordering is broken the SPA catch-all returns 200 with
+        Content-Type: text/html and a <div id="root"> body.  The correct
+        response is either 200 (gcode_viewer/index.html present) or 404
+        (directory absent in CI) — never the SPA shell.
+        """
+        response = await async_client.get("/gcode-viewer/")
+        # 200 or 404 are both acceptable depending on whether gcode_viewer/
+        # exists in the test environment; the SPA catch-all always returns 200.
+        assert response.status_code in (200, 404)
+        # If a body came back it must NOT be the React SPA shell.
+        assert b'<div id="root">' not in response.content
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_gcode_viewer_no_trailing_slash_redirects_or_responds(
+        self, async_client: AsyncClient
+    ):
+        """GET /gcode-viewer (no trailing slash) is handled by the explicit route."""
+        response = await async_client.get("/gcode-viewer", follow_redirects=True)
+        assert response.status_code in (200, 404)
+        assert b'<div id="root">' not in response.content
+
+
+class TestGCodeViewerPathTraversal:
+    """Verify the path-traversal guard on /gcode-viewer/{file_path:path}.
+
+    HTTP clients (and servers) normalise plain `..` segments before the
+    request reaches a route handler, so `/gcode-viewer/../x` becomes `/x`
+    and hits the SPA catch-all rather than our guard — that normalisation is
+    itself a defence layer.  The actual at-risk form is URL-encoded dots
+    (`%2E%2E`) which survive normalisation and land in {file_path:path} as
+    the literal string `../x`.  We test that form here.
+    """
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_encoded_dotdot_traversal_is_forbidden(self, async_client: AsyncClient):
+        """GET /gcode-viewer/%2E%2E/main.py must return 403.
+
+        %2E%2E URL-decodes to .. which is not normalised away by httpx/
+        Starlette, so it reaches _gcode_viewer_response as '../main.py'.
+        Path.is_relative_to(gcode_viewer_dir) then blocks it with 403.
+        """
+        response = await async_client.get("/gcode-viewer/%2E%2E/main.py")
+        assert response.status_code == 403
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_encoded_nested_dotdot_traversal_is_forbidden(self, async_client: AsyncClient):
+        """GET /gcode-viewer/js/%2E%2E/%2E%2E/main.py must return 403."""
+        response = await async_client.get("/gcode-viewer/js/%2E%2E/%2E%2E/main.py")
+        assert response.status_code == 403
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_nonexistent_safe_path_returns_404(self, async_client: AsyncClient):
+        """A safe but nonexistent path returns 404, not 403."""
+        response = await async_client.get("/gcode-viewer/does-not-exist.js")
+        assert response.status_code == 404

+ 115 - 0
backend/tests/unit/test_library_file_path_guard.py

@@ -0,0 +1,115 @@
+"""Tests for to_absolute_path() path-traversal guard in library routes.
+
+Covers three behaviours added/changed by the GCode viewer PR:
+
+1. Relative paths that escape base_dir are rejected with ValueError.
+2. Path.is_relative_to() is used instead of startswith(str(base)),
+   avoiding the /data/app vs /data/app_evil prefix-confusion bug.
+3. Legacy absolute paths (pre-migration DB rows) are returned verbatim
+   instead of raising ValueError.
+"""
+
+from pathlib import Path
+from unittest.mock import patch
+
+import pytest
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+
+def _call(relative_path, base_dir):
+    """Call to_absolute_path with base_dir patched to *base_dir*."""
+    from backend.app.api.routes.library import to_absolute_path
+
+    with patch("backend.app.api.routes.library.app_settings") as mock_settings:
+        mock_settings.base_dir = str(base_dir)
+        return to_absolute_path(relative_path)
+
+
+# ---------------------------------------------------------------------------
+# None / empty guard
+# ---------------------------------------------------------------------------
+
+
+class TestNullInputs:
+    def test_none_returns_none(self, tmp_path):
+        assert _call(None, tmp_path) is None
+
+    def test_empty_string_returns_none(self, tmp_path):
+        assert _call("", tmp_path) is None
+
+
+# ---------------------------------------------------------------------------
+# Relative path traversal guard
+# ---------------------------------------------------------------------------
+
+
+class TestRelativePathTraversal:
+    def test_normal_relative_path_resolves(self, tmp_path):
+        """A safe relative path resolves to base_dir / rel."""
+        base = tmp_path / "data"
+        base.mkdir()
+        result = _call("files/model.gcode", base)
+        assert result == (base / "files" / "model.gcode").resolve()
+
+    def test_traversal_via_dotdot_raises(self, tmp_path):
+        """../etc/passwd must be rejected."""
+        base = tmp_path / "data"
+        base.mkdir()
+        with pytest.raises(ValueError, match="escapes base directory"):
+            _call("../etc/passwd", base)
+
+    def test_traversal_via_nested_dotdot_raises(self, tmp_path):
+        """files/../../etc/passwd must be rejected."""
+        base = tmp_path / "data"
+        base.mkdir()
+        with pytest.raises(ValueError, match="escapes base directory"):
+            _call("files/../../etc/passwd", base)
+
+    def test_prefix_confusion_is_blocked(self, tmp_path):
+        """Ensure /data/app_evil/secret is not permitted when base is /data/app.
+
+        A naive startswith(str(base)) check would allow this because
+        '/data/app_evil'.startswith('/data/app') is True.
+        Path.is_relative_to() must be used instead.
+        """
+        # Simulate: base = /tmp/.../data_app, sibling = /tmp/.../data_app_evil
+        base = tmp_path / "data_app"
+        sibling = tmp_path / "data_app_evil"
+        base.mkdir()
+        sibling.mkdir()
+
+        # Construct a relative path that resolves into the *sibling* dir.
+        # from base: ../data_app_evil/secret
+        with pytest.raises(ValueError, match="escapes base directory"):
+            _call("../data_app_evil/secret", base)
+
+
+# ---------------------------------------------------------------------------
+# Legacy absolute path pass-through
+# ---------------------------------------------------------------------------
+
+
+class TestLegacyAbsolutePaths:
+    def test_absolute_path_inside_base_is_returned(self, tmp_path):
+        """An absolute path that happens to be inside base_dir is returned as-is."""
+        base = tmp_path / "data"
+        base.mkdir()
+        abs_path = str(base / "archive" / "old.3mf")
+        result = _call(abs_path, base)
+        assert result == Path(abs_path).resolve()
+
+    def test_absolute_path_outside_base_is_returned(self, tmp_path):
+        """An absolute path outside base_dir is returned verbatim (legacy compat).
+
+        Pre-migration DB rows may store absolute paths that predate the
+        base_dir layout. These must NOT raise ValueError; callers are
+        responsible for further existence checks.
+        """
+        base = tmp_path / "data"
+        base.mkdir()
+        outside = tmp_path / "old_archive" / "legacy.3mf"
+        result = _call(str(outside), base)
+        assert result == outside.resolve()

+ 2 - 0
frontend/src/App.tsx

@@ -21,6 +21,7 @@ import { SystemInfoPage } from './pages/SystemInfoPage';
 import { LoginPage } from './pages/LoginPage';
 import { LoginPage } from './pages/LoginPage';
 import { SetupPage } from './pages/SetupPage';
 import { SetupPage } from './pages/SetupPage';
 import { NotificationsPage } from './pages/NotificationsPage';
 import { NotificationsPage } from './pages/NotificationsPage';
+import { GCodeViewerPage } from './pages/GCodeViewerPage';
 import { useWebSocket } from './hooks/useWebSocket';
 import { useWebSocket } from './hooks/useWebSocket';
 import { useStreamTokenSync } from './hooks/useCameraStreamToken';
 import { useStreamTokenSync } from './hooks/useCameraStreamToken';
 import { ThemeProvider } from './contexts/ThemeContext';
 import { ThemeProvider } from './contexts/ThemeContext';
@@ -195,6 +196,7 @@ function App() {
                   <Route path="groups" element={<Navigate to="/settings?tab=users" replace />} />
                   <Route path="groups" element={<Navigate to="/settings?tab=users" replace />} />
                   <Route path="system" element={<SystemInfoPage />} />
                   <Route path="system" element={<SystemInfoPage />} />
                   <Route path="notifications" element={<NotificationsPage />} />
                   <Route path="notifications" element={<NotificationsPage />} />
+                  <Route path="gcode-viewer" element={<GCodeViewerPage />} />
                   <Route path="external/:id" element={<ExternalLinkPage />} />
                   <Route path="external/:id" element={<ExternalLinkPage />} />
                 </Route>
                 </Route>
               </Routes>
               </Routes>

+ 2 - 1
frontend/src/components/Layout.tsx

@@ -1,6 +1,6 @@
 import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
 import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
 import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom';
 import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom';
-import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, Wrench, FolderKanban, FolderOpen, X, Menu, Info, Plug, Bug, LogOut, Key, Loader2, Disc3, ShieldAlert, Bell, type LucideIcon } from 'lucide-react';
+import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, Wrench, FolderKanban, FolderOpen, X, Menu, Info, Plug, Bug, LogOut, Key, Loader2, Disc3, ShieldAlert, Bell, Layers, type LucideIcon } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { useTheme } from '../contexts/ThemeContext';
 import { useTheme } from '../contexts/ThemeContext';
 import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
 import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
@@ -36,6 +36,7 @@ export const defaultNavItems: NavItem[] = [
   { id: 'projects', to: '/projects', icon: FolderKanban, labelKey: 'nav.projects' },
   { id: 'projects', to: '/projects', icon: FolderKanban, labelKey: 'nav.projects' },
   { id: 'inventory', to: '/inventory', icon: Disc3, labelKey: 'nav.inventory' },
   { id: 'inventory', to: '/inventory', icon: Disc3, labelKey: 'nav.inventory' },
   { id: 'files', to: '/files', icon: FolderOpen, labelKey: 'nav.files' },
   { id: 'files', to: '/files', icon: FolderOpen, labelKey: 'nav.files' },
+  { id: 'gcode-viewer', to: '/gcode-viewer', icon: Layers, labelKey: 'nav.gcodeViewer' },
   // User-account features: kept adjacent to Settings intentionally
   // User-account features: kept adjacent to Settings intentionally
   { id: 'notifications', to: '/notifications', icon: Bell, labelKey: 'nav.notifications' },
   { id: 'notifications', to: '/notifications', icon: Bell, labelKey: 'nav.notifications' },
   { id: 'settings', to: '/settings', icon: Settings, labelKey: 'nav.settings' },
   { id: 'settings', to: '/settings', icon: Settings, labelKey: 'nav.settings' },

+ 4 - 0
frontend/src/i18n/locales/de.ts

@@ -10,6 +10,7 @@ export default {
     projects: 'Projekte',
     projects: 'Projekte',
     inventory: 'Filament',
     inventory: 'Filament',
     files: 'Dateimanager',
     files: 'Dateimanager',
+    gcodeViewer: 'GCode-Viewer',
     notifications: 'Benachrichtigungen',
     notifications: 'Benachrichtigungen',
     settings: 'Einstellungen',
     settings: 'Einstellungen',
     system: 'System',
     system: 'System',
@@ -213,6 +214,7 @@ export default {
     chamberLightOff: 'Kammerbeleuchtung ausschalten',
     chamberLightOff: 'Kammerbeleuchtung ausschalten',
     // Files
     // Files
     files: 'Dateien',
     files: 'Dateien',
+    gcodeViewer: 'GCode-Viewer',
     browseFiles: 'Druckerdateien durchsuchen',
     browseFiles: 'Druckerdateien durchsuchen',
     // Smart plug
     // Smart plug
     autoOffAfterPrint: 'Automatisches Ausschalten nach Druck',
     autoOffAfterPrint: 'Automatisches Ausschalten nach Druck',
@@ -2891,6 +2893,7 @@ export default {
     lowDiskSpaceWarning: 'Warnung: Wenig Speicherplatz',
     lowDiskSpaceWarning: 'Warnung: Wenig Speicherplatz',
     lowDiskSpaceDetails: 'Nur {{free}} frei von {{total}} gesamt. Schwellenwert ist auf {{threshold}} GB eingestellt.',
     lowDiskSpaceDetails: 'Nur {{free}} frei von {{total}} gesamt. Schwellenwert ist auf {{threshold}} GB eingestellt.',
     files: 'Dateien',
     files: 'Dateien',
+    gcodeViewer: 'GCode-Viewer',
     folders: 'Ordner',
     folders: 'Ordner',
     size: 'Größe',
     size: 'Größe',
     free: 'Frei',
     free: 'Frei',
@@ -2994,6 +2997,7 @@ export default {
     createFirstButton: 'Erstes Projekt erstellen',
     createFirstButton: 'Erstes Projekt erstellen',
     create: 'Erstellen',
     create: 'Erstellen',
     files: 'Dateien',
     files: 'Dateien',
+    gcodeViewer: 'GCode-Viewer',
     prints: 'Drucke',
     prints: 'Drucke',
     plates: 'Platten',
     plates: 'Platten',
     parts: 'Teile',
     parts: 'Teile',

+ 4 - 0
frontend/src/i18n/locales/en.ts

@@ -10,6 +10,7 @@ export default {
     projects: 'Projects',
     projects: 'Projects',
     inventory: 'Filament',
     inventory: 'Filament',
     files: 'File Manager',
     files: 'File Manager',
+    gcodeViewer: 'GCode Viewer',
     notifications: 'Notifications',
     notifications: 'Notifications',
     settings: 'Settings',
     settings: 'Settings',
     system: 'System',
     system: 'System',
@@ -213,6 +214,7 @@ export default {
     chamberLightOff: 'Turn off chamber light',
     chamberLightOff: 'Turn off chamber light',
     // Files
     // Files
     files: 'Files',
     files: 'Files',
+    gcodeViewer: 'GCode Viewer',
     browseFiles: 'Browse printer files',
     browseFiles: 'Browse printer files',
     // Smart plug
     // Smart plug
     autoOffAfterPrint: 'Auto power-off after print',
     autoOffAfterPrint: 'Auto power-off after print',
@@ -2894,6 +2896,7 @@ export default {
     lowDiskSpaceWarning: 'Low disk space warning',
     lowDiskSpaceWarning: 'Low disk space warning',
     lowDiskSpaceDetails: 'Only {{free}} free of {{total}} total. Threshold is set to {{threshold}} GB in settings.',
     lowDiskSpaceDetails: 'Only {{free}} free of {{total}} total. Threshold is set to {{threshold}} GB in settings.',
     files: 'Files',
     files: 'Files',
+    gcodeViewer: 'GCode Viewer',
     folders: 'Folders',
     folders: 'Folders',
     size: 'Size',
     size: 'Size',
     free: 'Free',
     free: 'Free',
@@ -2997,6 +3000,7 @@ export default {
     createFirstButton: 'Create Your First Project',
     createFirstButton: 'Create Your First Project',
     create: 'Create',
     create: 'Create',
     files: 'Files',
     files: 'Files',
+    gcodeViewer: 'GCode Viewer',
     prints: 'Prints',
     prints: 'Prints',
     plates: 'plates',
     plates: 'plates',
     parts: 'parts',
     parts: 'parts',

+ 4 - 0
frontend/src/i18n/locales/fr.ts

@@ -10,6 +10,7 @@ export default {
     projects: 'Projets',
     projects: 'Projets',
     inventory: 'Filament',
     inventory: 'Filament',
     files: 'Gestionnaire de fichiers',
     files: 'Gestionnaire de fichiers',
+    gcodeViewer: 'Visionneuse GCode',
     notifications: 'Notifications',
     notifications: 'Notifications',
     settings: 'Paramètres',
     settings: 'Paramètres',
     system: 'Système',
     system: 'Système',
@@ -213,6 +214,7 @@ export default {
     chamberLightOff: 'Éteindre la lumière de la chambre',
     chamberLightOff: 'Éteindre la lumière de la chambre',
     // Files
     // Files
     files: 'Fichiers',
     files: 'Fichiers',
+    gcodeViewer: 'Visionneuse GCode',
     browseFiles: 'Parcourir les fichiers de l\'imprimante',
     browseFiles: 'Parcourir les fichiers de l\'imprimante',
     // Smart plug
     // Smart plug
     autoOffAfterPrint: 'Extinction auto après impression',
     autoOffAfterPrint: 'Extinction auto après impression',
@@ -2813,6 +2815,7 @@ export default {
     lowDiskSpaceWarning: 'Espace disque faible',
     lowDiskSpaceWarning: 'Espace disque faible',
     lowDiskSpaceDetails: '{{free}} libre sur {{total}}. Seuil : {{threshold}} Go.',
     lowDiskSpaceDetails: '{{free}} libre sur {{total}}. Seuil : {{threshold}} Go.',
     files: 'Fichiers',
     files: 'Fichiers',
+    gcodeViewer: 'Visionneuse GCode',
     folders: 'Dossiers',
     folders: 'Dossiers',
     size: 'Taille',
     size: 'Taille',
     free: 'Libre',
     free: 'Libre',
@@ -2916,6 +2919,7 @@ export default {
     createFirstButton: 'Créer votre premier projet',
     createFirstButton: 'Créer votre premier projet',
     create: 'Créer',
     create: 'Créer',
     files: 'Fichiers',
     files: 'Fichiers',
+    gcodeViewer: 'Visionneuse GCode',
     prints: 'Impressions',
     prints: 'Impressions',
     plates: 'plateaux',
     plates: 'plateaux',
     parts: 'pièces',
     parts: 'pièces',

+ 4 - 0
frontend/src/i18n/locales/it.ts

@@ -10,6 +10,7 @@ export default {
     projects: 'Progetti',
     projects: 'Progetti',
     inventory: 'Filamento',
     inventory: 'Filamento',
     files: 'File',
     files: 'File',
+    gcodeViewer: 'Visualizzatore GCode',
     notifications: 'Notifiche',
     notifications: 'Notifiche',
     settings: 'Impostazioni',
     settings: 'Impostazioni',
     system: 'Sistema',
     system: 'Sistema',
@@ -213,6 +214,7 @@ export default {
     chamberLightOff: 'Spegni luce camera',
     chamberLightOff: 'Spegni luce camera',
     // Files
     // Files
     files: 'File',
     files: 'File',
+    gcodeViewer: 'Visualizzatore GCode',
     browseFiles: 'Sfoglia file stampante',
     browseFiles: 'Sfoglia file stampante',
     // Smart plug
     // Smart plug
     autoOffAfterPrint: 'Spegnimento automatico dopo stampa',
     autoOffAfterPrint: 'Spegnimento automatico dopo stampa',
@@ -2812,6 +2814,7 @@ export default {
     lowDiskSpaceWarning: 'Avviso spazio disco basso',
     lowDiskSpaceWarning: 'Avviso spazio disco basso',
     lowDiskSpaceDetails: 'Solo {{free}} liberi su {{total}} totali. La soglia e {{threshold}} GB nelle impostazioni.',
     lowDiskSpaceDetails: 'Solo {{free}} liberi su {{total}} totali. La soglia e {{threshold}} GB nelle impostazioni.',
     files: 'File',
     files: 'File',
+    gcodeViewer: 'Visualizzatore GCode',
     folders: 'Cartelle',
     folders: 'Cartelle',
     size: 'Dimensione',
     size: 'Dimensione',
     free: 'Libero',
     free: 'Libero',
@@ -2915,6 +2918,7 @@ export default {
     createFirstButton: 'Crea il tuo primo progetto',
     createFirstButton: 'Crea il tuo primo progetto',
     create: 'Crea',
     create: 'Crea',
     files: 'File',
     files: 'File',
+    gcodeViewer: 'Visualizzatore GCode',
     prints: 'Stampe',
     prints: 'Stampe',
     plates: 'piatti',
     plates: 'piatti',
     parts: 'parti',
     parts: 'parti',

+ 4 - 0
frontend/src/i18n/locales/ja.ts

@@ -10,6 +10,7 @@ export default {
     projects: 'プロジェクト',
     projects: 'プロジェクト',
     inventory: 'フィラメント',
     inventory: 'フィラメント',
     files: 'ファイル管理',
     files: 'ファイル管理',
+    gcodeViewer: 'GCodeビューア',
     notifications: '通知',
     notifications: '通知',
     settings: '設定',
     settings: '設定',
     system: 'システム',
     system: 'システム',
@@ -212,6 +213,7 @@ export default {
     chamberLightOff: 'チャンバーライトをオフにしました',
     chamberLightOff: 'チャンバーライトをオフにしました',
     // Files
     // Files
     files: 'ファイル',
     files: 'ファイル',
+    gcodeViewer: 'GCodeビューア',
     browseFiles: 'プリンターのファイルを参照',
     browseFiles: 'プリンターのファイルを参照',
     // Smart plug
     // Smart plug
     autoOffAfterPrint: '印刷後に自動電源オフ',
     autoOffAfterPrint: '印刷後に自動電源オフ',
@@ -2851,6 +2853,7 @@ export default {
     lowDiskSpaceWarning: 'ディスク容量不足の警告',
     lowDiskSpaceWarning: 'ディスク容量不足の警告',
     lowDiskSpaceDetails: '{{total}}中{{free}}の空き容量のみ。しきい値は設定で{{threshold}}GBに設定されています。',
     lowDiskSpaceDetails: '{{total}}中{{free}}の空き容量のみ。しきい値は設定で{{threshold}}GBに設定されています。',
     files: 'ファイル',
     files: 'ファイル',
+    gcodeViewer: 'GCodeビューア',
     folders: 'フォルダ',
     folders: 'フォルダ',
     size: 'サイズ',
     size: 'サイズ',
     free: '空き:',
     free: '空き:',
@@ -2954,6 +2957,7 @@ export default {
     createFirstButton: '最初のプロジェクトを作成',
     createFirstButton: '最初のプロジェクトを作成',
     create: '作成',
     create: '作成',
     files: 'ファイル',
     files: 'ファイル',
+    gcodeViewer: 'GCodeビューア',
     prints: '印刷',
     prints: '印刷',
     plates: 'プレート',
     plates: 'プレート',
     parts: 'パーツ',
     parts: 'パーツ',

+ 4 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -10,6 +10,7 @@ export default {
     projects: 'Projetos',
     projects: 'Projetos',
     inventory: 'Inventário',
     inventory: 'Inventário',
     files: 'Gerenciador de Arquivos',
     files: 'Gerenciador de Arquivos',
+    gcodeViewer: 'Visualizador GCode',
     notifications: 'Notificações',
     notifications: 'Notificações',
     settings: 'Configurações',
     settings: 'Configurações',
     system: 'Sistema',
     system: 'Sistema',
@@ -213,6 +214,7 @@ export default {
     chamberLightOff: 'Desligar luz da câmara',
     chamberLightOff: 'Desligar luz da câmara',
     // Files
     // Files
     files: 'Arquivos',
     files: 'Arquivos',
+    gcodeViewer: 'Visualizador GCode',
     browseFiles: 'Procurar arquivos da impressora',
     browseFiles: 'Procurar arquivos da impressora',
     // Smart plug
     // Smart plug
     autoOffAfterPrint: 'Desligamento automático após impressão',
     autoOffAfterPrint: 'Desligamento automático após impressão',
@@ -2826,6 +2828,7 @@ export default {
     lowDiskSpaceWarning: 'Aviso de pouco espaço em disco',
     lowDiskSpaceWarning: 'Aviso de pouco espaço em disco',
     lowDiskSpaceDetails: 'Apenas {{free}} livres de {{total}} no total. O limite está definido para {{threshold}} GB nas configurações.',
     lowDiskSpaceDetails: 'Apenas {{free}} livres de {{total}} no total. O limite está definido para {{threshold}} GB nas configurações.',
     files: 'Arquivos',
     files: 'Arquivos',
+    gcodeViewer: 'Visualizador GCode',
     folders: 'Pastas',
     folders: 'Pastas',
     size: 'Tamanho',
     size: 'Tamanho',
     free: 'Livre',
     free: 'Livre',
@@ -2929,6 +2932,7 @@ export default {
     createFirstButton: 'Crie Seu Primeiro Projeto',
     createFirstButton: 'Crie Seu Primeiro Projeto',
     create: 'Criar',
     create: 'Criar',
     files: 'Arquivos',
     files: 'Arquivos',
+    gcodeViewer: 'Visualizador GCode',
     prints: 'Impressões',
     prints: 'Impressões',
     plates: 'Placas',
     plates: 'Placas',
     parts: 'Peças',
     parts: 'Peças',

+ 4 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -10,6 +10,7 @@ export default {
     projects: '项目',
     projects: '项目',
     inventory: '耗材',
     inventory: '耗材',
     files: '文件管理器',
     files: '文件管理器',
+    gcodeViewer: 'GCode查看器',
     notifications: '通知',
     notifications: '通知',
     settings: '设置',
     settings: '设置',
     system: '系统',
     system: '系统',
@@ -213,6 +214,7 @@ export default {
     chamberLightOff: '关闭腔室灯',
     chamberLightOff: '关闭腔室灯',
     // Files
     // Files
     files: '文件',
     files: '文件',
+    gcodeViewer: 'GCode查看器',
     browseFiles: '浏览打印机文件',
     browseFiles: '浏览打印机文件',
     // Smart plug
     // Smart plug
     autoOffAfterPrint: '打印后自动关机',
     autoOffAfterPrint: '打印后自动关机',
@@ -2878,6 +2880,7 @@ export default {
     lowDiskSpaceWarning: '磁盘空间不足警告',
     lowDiskSpaceWarning: '磁盘空间不足警告',
     lowDiskSpaceDetails: '仅剩 {{free}}(总共 {{total}})。阈值设置为 {{threshold}} GB。',
     lowDiskSpaceDetails: '仅剩 {{free}}(总共 {{total}})。阈值设置为 {{threshold}} GB。',
     files: '文件',
     files: '文件',
+    gcodeViewer: 'GCode查看器',
     folders: '文件夹',
     folders: '文件夹',
     size: '大小',
     size: '大小',
     free: '剩余',
     free: '剩余',
@@ -2981,6 +2984,7 @@ export default {
     createFirstButton: '创建您的第一个项目',
     createFirstButton: '创建您的第一个项目',
     create: '创建',
     create: '创建',
     files: '文件',
     files: '文件',
+    gcodeViewer: 'GCode查看器',
     prints: '打印',
     prints: '打印',
     plates: '板',
     plates: '板',
     parts: '零件',
     parts: '零件',

+ 4 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -10,6 +10,7 @@ export default {
     projects: '專案',
     projects: '專案',
     inventory: '耗材',
     inventory: '耗材',
     files: '檔案管理器',
     files: '檔案管理器',
+    gcodeViewer: 'GCode 檢視器',
     notifications: '通知',
     notifications: '通知',
     settings: '設定',
     settings: '設定',
     system: '系統',
     system: '系統',
@@ -213,6 +214,7 @@ export default {
     chamberLightOff: '關閉腔室燈',
     chamberLightOff: '關閉腔室燈',
     // Files
     // Files
     files: '檔案',
     files: '檔案',
+    gcodeViewer: 'GCode 檢視器',
     browseFiles: '瀏覽印表機檔案',
     browseFiles: '瀏覽印表機檔案',
     // Smart plug
     // Smart plug
     autoOffAfterPrint: '列印後自動關機',
     autoOffAfterPrint: '列印後自動關機',
@@ -2878,6 +2880,7 @@ export default {
     lowDiskSpaceWarning: '磁碟空間不足警告',
     lowDiskSpaceWarning: '磁碟空間不足警告',
     lowDiskSpaceDetails: '僅剩 {{free}}(總共 {{total}})。閾值設定為 {{threshold}} GB。',
     lowDiskSpaceDetails: '僅剩 {{free}}(總共 {{total}})。閾值設定為 {{threshold}} GB。',
     files: '檔案',
     files: '檔案',
+    gcodeViewer: 'GCode 檢視器',
     folders: '資料夾',
     folders: '資料夾',
     size: '大小',
     size: '大小',
     free: '剩餘',
     free: '剩餘',
@@ -2981,6 +2984,7 @@ export default {
     createFirstButton: '建立您的第一個項目',
     createFirstButton: '建立您的第一個項目',
     create: '建立',
     create: '建立',
     files: '檔案',
     files: '檔案',
+    gcodeViewer: 'GCode 檢視器',
     prints: '列印',
     prints: '列印',
     plates: '板',
     plates: '板',
     parts: '零件',
     parts: '零件',

+ 28 - 0
frontend/src/pages/GCodeViewerPage.tsx

@@ -0,0 +1,28 @@
+export function GCodeViewerPage() {
+  // Safety guard: if this React app is itself inside an iframe (e.g. the
+  // StaticFiles mount isn't registered and serve_spa returned us here),
+  // don't render another iframe — that would create an infinite loop.
+  if (window !== window.top) {
+    return (
+      <div style={{ padding: 32, color: '#f88' }}>
+        GCode viewer static files not found. Check that the{' '}
+        <code>gcode_viewer/</code> directory exists and restart uvicorn.
+      </div>
+    );
+  }
+
+  return (
+    // h-14 (3.5 rem) is the fixed header height defined in Layout.tsx.
+    // Subtracting it prevents a double scrollbar inside the layout shell.
+    <iframe
+      src="/gcode-viewer/"
+      title="GCode Viewer"
+      style={{
+        display: 'block',
+        width: '100%',
+        height: 'calc(100vh - 3.5rem)',
+        border: 'none',
+      }}
+    />
+  );
+}

+ 68 - 1
frontend/vite.config.ts

@@ -1,13 +1,80 @@
 import { defineConfig } from 'vite'
 import { defineConfig } from 'vite'
 import react from '@vitejs/plugin-react'
 import react from '@vitejs/plugin-react'
 import path from 'path'
 import path from 'path'
+import fs from 'fs'
+import type { Connect } from 'vite'
 
 
 // Backend port for dev server proxy (default: 8000)
 // Backend port for dev server proxy (default: 8000)
 const backendPort = process.env.BACKEND_PORT || '8000'
 const backendPort = process.env.BACKEND_PORT || '8000'
 const backendUrl = `http://localhost:${backendPort}`
 const backendUrl = `http://localhost:${backendPort}`
 
 
+// Absolute path to the gcode_viewer directory at the repo root
+const gcodeViewerDir = path.resolve(__dirname, '../gcode_viewer')
+
+// MIME types for static files served from gcode_viewer/
+const MIME: Record<string, string> = {
+  '.html': 'text/html; charset=utf-8',
+  '.js':   'application/javascript',
+  '.css':  'text/css',
+  '.obj':  'model/obj',
+  '.mtl':  'model/mtl',
+  '.png':  'image/png',
+  '.jpg':  'image/jpeg',
+  '.svg':  'image/svg+xml',
+  '.json': 'application/json',
+  '.woff': 'font/woff',
+  '.woff2':'font/woff2',
+}
+
+/**
+ * Vite dev-server plugin: serves ../gcode_viewer/ at /gcode-viewer/
+ * without needing a proxy to uvicorn.  In production uvicorn handles it
+ * via the StaticFiles mount in main.py.
+ */
+function serveGcodeViewer() {
+  return {
+    name: 'serve-gcode-viewer',
+    configureServer(server: { middlewares: Connect.Server }) {
+      server.middlewares.use((req, res, next) => {
+        const url = req.url ?? ''
+        if (!url.startsWith('/gcode-viewer')) return next()
+
+        // Strip prefix, default to index.html
+        let rel = url.slice('/gcode-viewer'.length)
+        if (rel === '' || rel === '/') rel = '/index.html'
+        // Strip query string
+        rel = rel.split('?')[0]
+
+        const absPath = path.join(gcodeViewerDir, rel)
+
+        try {
+          const stat = fs.statSync(absPath)
+          if (stat.isFile()) {
+            const ext = path.extname(absPath).toLowerCase()
+            res.setHeader('Content-Type', MIME[ext] ?? 'application/octet-stream')
+            res.end(fs.readFileSync(absPath))
+            return
+          }
+        } catch {
+          // file not found — fall through to index.html
+        }
+
+        // SPA fallback: serve index.html for any unmatched /gcode-viewer/* path
+        const index = path.join(gcodeViewerDir, 'index.html')
+        if (fs.existsSync(index)) {
+          res.setHeader('Content-Type', 'text/html; charset=utf-8')
+          res.end(fs.readFileSync(index))
+          return
+        }
+
+        next()
+      })
+    },
+  }
+}
+
 export default defineConfig({
 export default defineConfig({
-  plugins: [react()],
+  plugins: [react(), serveGcodeViewer()],
   build: {
   build: {
     outDir: '../static',
     outDir: '../static',
     emptyOutDir: true,
     emptyOutDir: true,

+ 60 - 0
gcode_viewer/VENDORED.md

@@ -0,0 +1,60 @@
+# Third-Party Notices — gcode_viewer
+
+The `gcode_viewer/` directory bundles the following third-party libraries.
+All licenses are compatible with Bambuddy's AGPL-3.0.
+
+---
+
+## PrettyGCode (OctoPrint plugin)
+
+- **File:** `js/prettygcode.js`
+- **Source:** https://github.com/Kragrathea/OctoPrint-PrettyGCode
+- **License:** AGPLv3
+
+---
+
+## three.js
+
+- **Files:** `js/three.min.js`, `js/OBJLoader.js`, `js/Line2.js`,
+  `js/LineGeometry.js`, `js/LineMaterial.js`, `js/LineSegments2.js`,
+  `js/LineSegmentsGeometry.js`, `js/Lut.js`
+- **Version:** r108
+- **Source:** https://github.com/mrdoob/three.js
+- **License:** MIT — https://github.com/mrdoob/three.js/blob/dev/LICENSE
+- **Note:** `OBJLoader`, `Line2`, `LineGeometry`, `LineMaterial`,
+  `LineSegments2`, `LineSegmentsGeometry`, and `Lut` are examples/extras
+  from three.js r108, same MIT licence.
+
+---
+
+## jQuery
+
+- **File:** `js/jquery.min.js`
+- **Version:** v3.7.1
+- **Source:** https://github.com/jquery/jquery
+- **License:** MIT — https://github.com/jquery/jquery/blob/main/LICENSE.txt
+
+---
+
+## dat.GUI
+
+- **File:** `js/dat.gui.js`
+- **Source:** https://github.com/dataarts/dat.gui
+- **License:** Apache 2.0 — https://github.com/dataarts/dat.gui/blob/master/LICENSE
+
+---
+
+## camera-controls
+
+- **File:** `js/camera-controls.js`
+- **Source:** https://github.com/yomotsu/camera-controls
+- **License:** MIT — https://github.com/yomotsu/camera-controls/blob/main/LICENSE
+
+---
+
+## Helvetiker Bold (typeface.js font)
+
+- **File:** `js/helvetiker_bold.typeface.json`
+- **Source:** Bundled with three.js examples; derived from M+ FONTS
+- **License:** M+ Font License (free for any use including commercial)
+  https://mplus-fonts.osdn.jp/about-en.html

+ 417 - 0
gcode_viewer/css/prettygcode.css

@@ -0,0 +1,417 @@
+/* Dark mode color variables */
+:root {
+    --pg-bg-light: #ffffff;
+    --pg-bg-dark: #1e1e1e;
+    --pg-text-light: #000000;
+    --pg-text-dark: #e0e0e0;
+    --pg-panel-light: rgba(255, 255, 255, 0.9);
+    --pg-panel-dark: rgba(30, 30, 30, 0.95);
+    --pg-border-light: #ddd;
+    --pg-border-dark: #444;
+    --pg-input-light: #e9e9e9;
+    --pg-input-dark: #2a2a2a;
+}
+
+#tab_plugin_prettygcode .gwin {
+    width: 100%;
+    height: 100%;
+    position: relative;
+}
+
+
+#tab_plugin_prettygcode .webcam_rotated {
+    transform: rotateZ(-90deg);
+}
+
+
+.pgfullscreen #tab_plugin_prettygcode .gwin {
+    top: 0px;
+    left: 0px;
+    width: 100%;
+    height: 100%;
+    position: absolute;
+}
+
+#tab_plugin_prettygcode .fstoggle {
+    top: 20px;
+    right: 60px;
+    position: absolute;
+    z-index: 10;
+}
+
+#tab_plugin_prettygcode .pgsettingstoggle {
+    top: 20px;
+    right: 95px;
+    position: absolute;
+    z-index: 10;
+}
+
+.pgfullscreen #tab_plugin_prettygcode .pgstatetoggle {
+    top: 20px;
+    left: 20px;
+    position: absolute;
+    z-index: 10;
+    display: unset;
+}
+
+#tab_plugin_prettygcode .pgstatetoggle {
+    display: none;
+}
+
+.pgfullscreen {
+    color: black;
+}
+
+.pgfullscreen #tab_plugin_prettygcode .pgfilestoggle {
+    top: 20px;
+    left: 95px;
+    position: absolute;
+    z-index: 10;
+    display: unset;
+}
+
+#tab_plugin_prettygcode .pgfilestoggle {
+    display: none;
+}
+
+.pgfullscreen #tab_plugin_prettygcode .pgcameratoggle {
+    bottom: 20px;
+    right: 40px;
+    position: absolute;
+    z-index: 10;
+    display: unset;
+}
+
+#tab_plugin_prettygcode .pgcameratoggle {
+    display: none;
+}
+
+.pgfullscreen .pgstatus {
+    display: unset;
+}
+
+.pgstatus {
+    display: none;
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    width: 100%;
+    font-size: large;
+    text-align: center;
+    background: #ffffff50;
+}
+
+.pgfullscreen #state_wrapper {
+    top: 20px;
+    left: 20px;
+    width: 300px;
+    position: absolute;
+    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+    background: rgba(255, 255, 255, 0.2);
+    z-index: 5;
+}
+
+.pgfullscreen #files_wrapper {
+    left: 95px;
+    top: 20px;
+    max-width: 80%;
+    width: 300px;
+    position: absolute;
+    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.70);
+    background: rgba(204, 204, 204, 0.9);
+    z-index: 6;
+}
+
+/* .pgfullscreen #files .gcode_files .entry {
+    padding: 5px;
+    line-height: 20px;
+    border-bottom: 1px solid #ddd;
+    position: relative;
+    width: 220px;
+    display: inline-grid;
+    background: white;
+    height: 100px;
+} */
+/* .pgfullscreen #files.collapse {
+    width:0px;
+} */
+
+.gwin #webcam_rotator {
+    bottom: 20px;
+    right: 40px;
+    position: absolute;
+    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+    z-index: 5;
+    display: none;
+}
+
+.pgfullscreen .gwin #webcam_rotator {
+    display: unset;
+}
+
+.pgfullscreen .gwin #webcam_rotator.pghidden {
+    display: none;
+}
+
+/* Hide notifications in fullscreen mode.*/
+/* todo. only do this if not logged in or as admin. 
+.pgfullscreen .ui-notify {
+    display:none;
+}
+*/
+.pgfullscreen .pghidden {
+    display: none;
+}
+
+.gwin .pghidden {
+    display: none;
+}
+
+/*dat gui*/
+#tab_plugin_prettygcode #mygui {
+    position: absolute;
+    right: 95px;
+    top: 20px;
+    opacity: 1.0;
+    z-index: 5;
+    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+    z-index: 4;
+    background: rgba(255, 255, 255, 1.0);
+    opacity: 0.8;
+
+}
+
+.gwin .dg li.save-row {
+    background: black;
+    color: yellow;
+}
+
+.gwin .dg li.save-row .button {
+    background: black;
+    color: yellow;
+}
+
+.gwin .dg li.save-row select {
+    height: 100%;
+}
+
+.gwin .dg .close-button {
+    display: none;
+}
+
+.gwin .dg li.save-row .button.revert {
+    display: none;
+}
+
+.gwin .dg li.save-row .button.gears {
+    display: none;
+}
+
+.gwin .has-save .save-row::before {
+    content: "Preset: ";
+}
+
+.gwin .dg li.save-row {
+    display: none;
+}
+
+.gwin .dg.main.taller-than-window .close-button {
+    border-top: 1px solid #ddd;
+}
+
+.gwin .dg.main .close-button {
+    background-color: #ccc;
+}
+
+.gwin .dg.main .close-button:hover {
+    background-color: #ddd;
+}
+
+.gwin .dg {
+    color: #555;
+    text-shadow: none !important;
+}
+
+.gwin .dg.main::-webkit-scrollbar {
+    background: #fafafa;
+}
+
+.gwin .dg.main::-webkit-scrollbar-thumb {
+    background: #bbb;
+}
+
+.gwin .dg li:not(.folder) {
+    background: #fafafa;
+    border-bottom: 1px solid #ddd;
+}
+
+.gwin .dg li.save-row .button {
+    text-shadow: none !important;
+}
+
+.gwin .dg li.title {
+    background: #e8e8e8 url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==) 6px 10px no-repeat;
+}
+
+.gwin .dg .cr.function:hover,
+.dg .cr.boolean:hover {
+    background: #fff;
+}
+
+.gwin .dg .c input[type=text] {
+    background: #e9e9e9;
+}
+
+.gwin .dg .c input[type=text]:hover {
+    background: #eee;
+}
+
+.gwin .dg .c input[type=text]:focus {
+    background: #eee;
+    color: #555;
+}
+
+.gwin .dg .c .slider {
+    background: #e9e9e9;
+}
+
+.gwin .dg .c .slider:hover {
+    background: #eee;
+}
+
+/*style slider to be more progress bar like*/
+.gwin #myslider .slider-track {
+    background: green;
+}
+
+.gwin #myslider .slider-handle {
+    width: 24px;
+}
+
+.gwin #myslider .tooltip {
+    display: none;
+}
+
+
+
+#tab_plugin_prettygcode code {
+    border: 1px black;
+    white-space: pre;
+}
+
+/*Support Octoprint Dashboard plugin*/
+
+
+.pgfullscreen #tab_plugin_dashboard {
+    display: unset;
+    position: absolute;
+    bottom: 20px;
+    left: 20px;
+    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+    z-index: 4;
+    background: rgba(255, 255, 255, 0.2);
+    opacity: 0.8;
+    /* width: 300px;  */
+    /* transform: scale(0.8); */
+}
+
+.pgfullscreen #tab_plugin_prettygcode .pgdashtoggle {
+    left: 20px;
+    bottom: 20px;
+    position: absolute;
+    z-index: 10;
+    display: unset;
+}
+
+#tab_plugin_prettygcode .pgdashtoggle {
+    display: none;
+}
+
+.pgfullscreen #tab_plugin_dashboard.pghidden {
+    display: none;
+}
+
+/* Dark Mode Styles */
+.pgdarkmode .pgfullscreen {
+    color: var(--pg-text-dark);
+}
+
+.pgdarkmode #state_wrapper {
+    background: var(--pg-panel-dark);
+    color: var(--pg-text-dark);
+}
+
+.pgdarkmode #files_wrapper {
+    background: var(--pg-panel-dark);
+    color: var(--pg-text-dark);
+}
+
+.pgdarkmode #tab_plugin_prettygcode #mygui {
+    background: var(--pg-panel-dark);
+}
+
+.pgdarkmode .gwin .dg {
+    color: var(--pg-text-dark);
+}
+
+.pgdarkmode .gwin .dg li:not(.folder) {
+    background: var(--pg-input-dark);
+    border-bottom: 1px solid var(--pg-border-dark);
+}
+
+.pgdarkmode .gwin .dg li.title {
+    background: #333 url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==) 6px 10px no-repeat;
+}
+
+.pgdarkmode .gwin .dg .cr.function:hover,
+.pgdarkmode .gwin .dg .cr.boolean:hover {
+    background: #3a3a3a;
+}
+
+.pgdarkmode .gwin .dg .c input[type=text] {
+    background: var(--pg-input-dark);
+    color: var(--pg-text-dark);
+}
+
+.pgdarkmode .gwin .dg .c input[type=text]:hover {
+    background: #333;
+}
+
+.pgdarkmode .gwin .dg .c input[type=text]:focus {
+    background: #333;
+    color: var(--pg-text-dark);
+}
+
+.pgdarkmode .gwin .dg .c .slider {
+    background: var(--pg-input-dark);
+}
+
+.pgdarkmode .gwin .dg .c .slider:hover {
+    background: #333;
+}
+
+.pgdarkmode .gwin .dg.main .close-button {
+    background-color: #333;
+}
+
+.pgdarkmode .gwin .dg.main .close-button:hover {
+    background-color: #444;
+}
+
+.pgdarkmode .gwin .dg.main::-webkit-scrollbar {
+    background: #1e1e1e;
+}
+
+.pgdarkmode .gwin .dg.main::-webkit-scrollbar-thumb {
+    background: #555;
+}
+
+.pgdarkmode #tab_plugin_dashboard {
+    background: var(--pg-panel-dark);
+    color: var(--pg-text-dark);
+}
+
+.pgdarkmode .pgstatus {
+    background: rgba(30, 30, 30, 0.5);
+    color: var(--pg-text-dark);
+}

+ 264 - 0
gcode_viewer/index.html

@@ -0,0 +1,264 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <!-- CSP: all scripts are local; unsafe-eval needed by Three.js shader compiler;
+       unsafe-inline needed by dat.GUI and jQuery for inline styles/event handlers -->
+  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; font-src 'self' data:; worker-src blob:;">
+  <title>PrettyGCode — Bambuddy</title>
+
+  <link rel="stylesheet" href="css/prettygcode.css">
+
+  <style>
+    *, *::before, *::after { box-sizing: border-box; }
+
+    html, body {
+      margin: 0; padding: 0;
+      height: 100%;
+      background: #1a1a1a;
+      color: #e0e0e0;
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+      font-size: 14px;
+    }
+
+    /* Top toolbar */
+    #bb-toolbar {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      padding: 6px 10px;
+      background: #111;
+      border-bottom: 1px solid #333;
+      flex-shrink: 0;
+      flex-wrap: wrap;
+    }
+    #bb-toolbar label { color: #aaa; font-size: 12px; white-space: nowrap; }
+    #bb-printer-select {
+      background: #222; color: #ddd;
+      border: 1px solid #444; border-radius: 4px;
+      padding: 3px 6px; font-size: 12px;
+    }
+    #bb-file-btn {
+      background: #2a6496; color: #fff;
+      border: none; border-radius: 4px;
+      padding: 4px 10px; cursor: pointer; font-size: 12px;
+    }
+    #bb-file-btn:hover { background: #1c4d72; }
+    #bb-current-file {
+      color: #9ecfff; font-size: 12px;
+      max-width: 300px; overflow: hidden;
+      text-overflow: ellipsis; white-space: nowrap;
+    }
+    #bb-open-settings {
+      margin-left: auto;
+      background: none; color: #aaa;
+      border: 1px solid #555; border-radius: 4px;
+      padding: 3px 8px; cursor: pointer; font-size: 12px;
+    }
+    #bb-open-settings:hover { color: #fff; border-color: #888; }
+
+    /* Playback controls */
+    #bb-play-btn {
+      background: #2a7a4a; color: #fff;
+      border: none; border-radius: 4px;
+      padding: 4px 10px; cursor: pointer; font-size: 14px;
+      min-width: 34px;
+    }
+    #bb-play-btn:hover { background: #1d5c38; }
+    #bb-play-btn:disabled { background: #444; cursor: default; }
+    #bb-play-speed {
+      background: #222; color: #ddd;
+      border: 1px solid #444; border-radius: 4px;
+      padding: 3px 5px; font-size: 12px;
+    }
+
+    /* File picker dropdown */
+    #bb-file-picker {
+      display: none;
+      position: fixed;   /* fixed so it's never clipped by viewer overflow */
+      top: 38px; left: 10px;
+      width: 320px;
+      background: #222; border: 1px solid #444;
+      border-radius: 6px; padding: 6px;
+      z-index: 9999;
+      box-shadow: 0 4px 16px rgba(0,0,0,0.6);
+    }
+    #bb-file-picker.bb-open { display: block; }
+
+    /* Main layout */
+    #bb-layout {
+      display: flex;
+      flex-direction: column;
+      height: 100vh;
+    }
+    #bb-viewer-wrap {
+      flex: 1;
+      position: relative;
+      overflow: hidden;
+    }
+
+    /* The prettygcode container — must fill its parent */
+    .page-container {
+      width: 100%;
+      height: 100%;
+      position: relative;
+    }
+
+    #tab_plugin_prettygcode {
+      width: 100%;
+      height: 100%;
+    }
+
+    .gwin {
+      width: 100%;
+      height: 100%;
+      position: relative;
+    }
+
+    #mycanvas {
+      width: 100%;
+      height: 100%;
+      display: block;
+    }
+
+    /* Webcam element cloned by prettygcode.js — keep hidden until fullscreen */
+    #webcam_rotator {
+      display: none;
+    }
+    #webcam_image { max-width: 100%; }
+
+    /* Control buttons inside .gwin */
+    .pgstatetoggle, .pgfilestoggle, .pgsettingstoggle, .fstoggle,
+    .pgdashtoggle,  .pgcameratoggle {
+      background: rgba(0,0,0,0.5);
+      border: 1px solid #666;
+      border-radius: 4px;
+      color: #ddd;
+      cursor: pointer;
+      font-size: 16px;
+      padding: 4px 7px;
+      z-index: 10;
+    }
+    .pgstatetoggle:hover, .pgfilestoggle:hover, .pgsettingstoggle:hover,
+    .fstoggle:hover, .pgdashtoggle:hover, .pgcameratoggle:hover {
+      background: rgba(60,60,60,0.8);
+    }
+
+    /* Slider shim styles */
+    .slider {
+      position: absolute;
+      right: 20px;
+      top: 5%;
+      height: 90%;
+      width: 28px;
+      z-index: 10;
+      cursor: pointer;
+      user-select: none;
+    }
+    .slider-vertical { writing-mode: vertical-lr; }
+    .slider-track {
+      position: absolute;
+      left: 50%;
+      top: 0; bottom: 0;
+      width: 4px;
+      transform: translateX(-50%);
+      background: #555;
+      border-radius: 2px;
+    }
+    .slider-selection {
+      position: absolute;
+      left: 0; right: 0;
+      bottom: 0;
+      background: #4a9;
+      border-radius: 2px;
+    }
+    .slider-handle {
+      position: absolute;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 24px; height: 24px;
+      background: #6cf;
+      border-radius: 50%;
+      line-height: 24px;
+      text-align: center;
+      font-size: 9px;
+      color: #111;
+      font-weight: bold;
+    }
+  </style>
+</head>
+<body>
+
+<div id="bb-layout">
+
+  <!-- Toolbar -->
+  <div id="bb-toolbar">
+    <label for="bb-printer-select">Printer:</label>
+    <select id="bb-printer-select"><option value="">Loading…</option></select>
+
+    <button id="bb-file-btn">&#128196; Load file</button>
+    <span id="bb-current-file">— no file loaded —</span>
+
+    <button id="bb-play-btn" title="Play layer animation" disabled>&#9654;</button>
+    <select id="bb-play-speed" title="Playback speed">
+      <option value="1">1&#215; slow</option>
+      <option value="3" selected>3&#215;</option>
+      <option value="10">10&#215; fast</option>
+      <option value="25">25&#215; turbo</option>
+    </select>
+
+  </div>
+
+  <!-- File picker (positioned relative to toolbar) -->
+  <div id="bb-file-picker"></div>
+
+  <!-- Viewer area -->
+  <div id="bb-viewer-wrap">
+    <div class="page-container">
+
+      <div id="tab_plugin_prettygcode">
+        <div class="gwin">
+          <canvas id="mycanvas"></canvas>
+          <div id="pgstatus" class="pgstatus">T:0/0 B:0/0</div>
+          <div id="mygui" class="pghidden">View Options</div>
+
+          <button class="pgstatetoggle" title="Toggle state window">&#9432;</button>
+          <button class="pgfilestoggle" title="Toggle file list">&#9776;</button>
+          <button class="pgsettingstoggle" title="Toggle settings">&#9881;</button>
+          <button class="pgcameratoggle" title="Toggle webcam">&#128247;</button>
+        </div>
+      </div>
+
+      <!-- Webcam source element — prettygcode.js clones this into .gwin -->
+      <div id="webcam_rotator" style="display:none;">
+        <img id="webcam_image" src="" alt="webcam">
+      </div>
+
+    </div>
+  </div><!-- /bb-viewer-wrap -->
+</div><!-- /bb-layout -->
+
+<!-- Scripts — load order matters -->
+<script src="js/jquery.min.js"></script>
+<script src="js/slider-shim.js"></script>
+<script src="js/three.min.js"></script>
+<script src="js/LineSegmentsGeometry.js"></script>
+<script src="js/LineGeometry.js"></script>
+<script src="js/LineMaterial.js"></script>
+<script src="js/LineSegments2.js"></script>
+<script src="js/Line2.js"></script>
+<script src="js/Lut.js"></script>
+<script src="js/OBJLoader.js"></script>
+<script src="js/camera-controls.js"></script>
+<script src="js/dat.gui.js"></script>
+
+<!-- Adapter MUST load before prettygcode.js -->
+<script src="js/bambuddy_adapter.js"></script>
+<script src="js/prettygcode.js"></script>
+
+<!-- Button wiring is in bambuddy_adapter.js — inline scripts are blocked by the
+     script-src CSP on this page (no 'unsafe-inline'). -->
+
+</body>
+</html>

+ 31 - 0
gcode_viewer/js/Line2.js

@@ -0,0 +1,31 @@
+/**
+ * @author WestLangley / http://github.com/WestLangley
+ *
+ */
+
+THREE.Line2 = function ( geometry, material ) {
+
+	THREE.LineSegments2.call( this );
+
+	this.type = 'Line2';
+
+	this.geometry = geometry !== undefined ? geometry : new THREE.LineGeometry();
+	this.material = material !== undefined ? material : new THREE.LineMaterial( { color: Math.random() * 0xffffff } );
+
+};
+
+THREE.Line2.prototype = Object.assign( Object.create( THREE.LineSegments2.prototype ), {
+
+	constructor: THREE.Line2,
+
+	isLine2: true,
+
+	copy: function ( /* source */ ) {
+
+		// todo
+
+		return this;
+
+	}
+
+} );

+ 104 - 0
gcode_viewer/js/LineGeometry.js

@@ -0,0 +1,104 @@
+/**
+ * @author WestLangley / http://github.com/WestLangley
+ *
+ */
+
+THREE.LineGeometry = function () {
+
+	THREE.LineSegmentsGeometry.call( this );
+
+	this.type = 'LineGeometry';
+
+};
+
+THREE.LineGeometry.prototype = Object.assign(Object.create(THREE.LineSegmentsGeometry.prototype), {
+
+    constructor: THREE.LineGeometry,
+
+    isLineGeometry: true,
+
+    setPositions: function (array, unpack = false ) {
+
+		// converts [ x1, y1, z1,  x2, y2, z2, ... ] to pairs format
+        if (unpack) {
+            var length = array.length - 3;
+            var points = new Float32Array(2 * length);
+
+            for (var i = 0; i < length; i += 3) {
+
+                points[2 * i] = array[i];
+                points[2 * i + 1] = array[i + 1];
+                points[2 * i + 2] = array[i + 2];
+
+                points[2 * i + 3] = array[i + 3];
+                points[2 * i + 4] = array[i + 4];
+                points[2 * i + 5] = array[i + 5];
+
+            }
+
+            THREE.LineSegmentsGeometry.prototype.setPositions.call(this, points);
+        }
+        else
+            THREE.LineSegmentsGeometry.prototype.setPositions.call(this, array);
+		return this;
+
+	},
+
+    setColors: function (array, unpack = false  ) {
+
+		// converts [ r1, g1, b1,  r2, g2, b2, ... ] to pairs format
+
+        if (unpack) {
+		    var length = array.length - 3;
+		    var colors = new Float32Array( 2 * length );
+
+		    for ( var i = 0; i < length; i += 3 ) {
+
+			    colors[ 2 * i ] = array[ i ];
+			    colors[ 2 * i + 1 ] = array[ i + 1 ];
+			    colors[ 2 * i + 2 ] = array[ i + 2 ];
+
+			    colors[ 2 * i + 3 ] = array[ i + 3 ];
+			    colors[ 2 * i + 4 ] = array[ i + 4 ];
+			    colors[ 2 * i + 5 ] = array[ i + 5 ];
+
+		    }
+
+		    THREE.LineSegmentsGeometry.prototype.setColors.call( this, colors );
+        }
+        else
+            THREE.LineSegmentsGeometry.prototype.setColors.call(this, array);
+
+		return this;
+
+	},
+
+	fromLine: function ( line ) {
+
+		var geometry = line.geometry;
+
+		if ( geometry.isGeometry ) {
+
+			this.setPositions( geometry.vertices );
+
+		} else if ( geometry.isBufferGeometry ) {
+
+			this.setPositions( geometry.position.array ); // assumes non-indexed
+
+		}
+
+		// set colors, maybe
+
+		return this;
+
+	},
+
+	copy: function ( /* source */ ) {
+
+		// todo
+
+		return this;
+
+	}
+
+} );

+ 391 - 0
gcode_viewer/js/LineMaterial.js

@@ -0,0 +1,391 @@
+/**
+ * @author WestLangley / http://github.com/WestLangley
+ *
+ * parameters = {
+ *  color: <hex>,
+ *  linewidth: <float>,
+ *  dashed: <boolean>,
+ *  dashScale: <float>,
+ *  dashSize: <float>,
+ *  gapSize: <float>,
+ *  resolution: <Vector2>, // to be set by renderer
+ * }
+ */
+
+THREE.UniformsLib.line = {
+
+	linewidth: { value: 1 },
+	resolution: { value: new THREE.Vector2( 1, 1 ) },
+	dashScale: { value: 1 },
+	dashSize: { value: 1 },
+	gapSize: { value: 1 } // todo FIX - maybe change to totalSize
+
+};
+
+THREE.ShaderLib[ 'line' ] = {
+
+	uniforms: THREE.UniformsUtils.merge( [
+		THREE.UniformsLib.common,
+		THREE.UniformsLib.fog,
+		THREE.UniformsLib.line
+	] ),
+
+	vertexShader:
+		`
+		#include <common>
+		#include <color_pars_vertex>
+		#include <fog_pars_vertex>
+		#include <logdepthbuf_pars_vertex>
+		#include <clipping_planes_pars_vertex>
+
+		uniform float linewidth;
+		uniform vec2 resolution;
+
+		attribute vec3 instanceStart;
+		attribute vec3 instanceEnd;
+
+		attribute vec3 instanceColorStart;
+		attribute vec3 instanceColorEnd;
+
+		varying vec2 vUv;
+
+		#ifdef USE_DASH
+
+			uniform float dashScale;
+			attribute float instanceDistanceStart;
+			attribute float instanceDistanceEnd;
+			varying float vLineDistance;
+
+		#endif
+
+		void trimSegment( const in vec4 start, inout vec4 end ) {
+
+			// trim end segment so it terminates between the camera plane and the near plane
+
+			// conservative estimate of the near plane
+			float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column
+			float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column
+			float nearEstimate = - 0.5 * b / a;
+
+			float alpha = ( nearEstimate - start.z ) / ( end.z - start.z );
+
+			end.xyz = mix( start.xyz, end.xyz, alpha );
+
+		}
+
+		void main() {
+
+			#ifdef USE_COLOR
+
+				vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd;
+
+			#endif
+
+			#ifdef USE_DASH
+
+				vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd;
+
+			#endif
+
+			float aspect = resolution.x / resolution.y;
+
+			vUv = uv;
+
+			// camera space
+			vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 );
+			vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );
+
+			// special case for perspective projection, and segments that terminate either in, or behind, the camera plane
+			// clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
+			// but we need to perform ndc-space calculations in the shader, so we must address this issue directly
+			// perhaps there is a more elegant solution -- WestLangley
+
+			bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column
+
+			if ( perspective ) {
+
+				if ( start.z < 0.0 && end.z >= 0.0 ) {
+
+					trimSegment( start, end );
+
+				} else if ( end.z < 0.0 && start.z >= 0.0 ) {
+
+					trimSegment( end, start );
+
+				}
+
+			}
+
+			// clip space
+			vec4 clipStart = projectionMatrix * start;
+			vec4 clipEnd = projectionMatrix * end;
+
+			// ndc space
+			vec2 ndcStart = clipStart.xy / clipStart.w;
+			vec2 ndcEnd = clipEnd.xy / clipEnd.w;
+
+			// direction
+			vec2 dir = ndcEnd - ndcStart;
+
+			// account for clip-space aspect ratio
+			dir.x *= aspect;
+			dir = normalize( dir );
+
+			// perpendicular to dir
+			vec2 offset = vec2( dir.y, - dir.x );
+
+			// undo aspect ratio adjustment
+			dir.x /= aspect;
+			offset.x /= aspect;
+
+			// sign flip
+			if ( position.x < 0.0 ) offset *= - 1.0;
+
+			// endcaps
+			if ( position.y < 0.0 ) {
+
+				offset += - dir;
+
+			} else if ( position.y > 1.0 ) {
+
+				offset += dir;
+
+			}
+
+			// adjust for linewidth
+			offset *= linewidth;
+
+			// adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
+			offset /= resolution.y;
+
+			// select end
+			vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd;
+
+			// back to clip space
+			offset *= clip.w;
+
+			clip.xy += offset;
+
+			gl_Position = clip;
+
+			vec4 mvPosition = ( position.y < 0.5 ) ? start : end; // this is an approximation
+
+			#include <logdepthbuf_vertex>
+			#include <clipping_planes_vertex>
+			#include <fog_vertex>
+
+		}
+		`,
+
+	fragmentShader:
+		`
+		uniform vec3 diffuse;
+		uniform float opacity;
+
+		#ifdef USE_DASH
+
+			uniform float dashSize;
+			uniform float gapSize;
+
+		#endif
+
+		varying float vLineDistance;
+
+		#include <common>
+		#include <color_pars_fragment>
+		#include <fog_pars_fragment>
+		#include <logdepthbuf_pars_fragment>
+		#include <clipping_planes_pars_fragment>
+
+		varying vec2 vUv;
+
+		void main() {
+
+			#include <clipping_planes_fragment>
+
+			#ifdef USE_DASH
+
+				if ( vUv.y < - 1.0 || vUv.y > 1.0 ) discard; // discard endcaps
+
+				if ( mod( vLineDistance, dashSize + gapSize ) > dashSize ) discard; // todo - FIX
+
+			#endif
+
+			if ( abs( vUv.y ) > 1.0 ) {
+
+				float a = vUv.x;
+				float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0;
+				float len2 = a * a + b * b;
+
+				if ( len2 > 1.0 ) discard;
+
+			}
+
+			vec4 diffuseColor = vec4( diffuse, opacity );
+
+			#include <logdepthbuf_fragment>
+			#include <color_fragment>
+
+			gl_FragColor = vec4( diffuseColor.rgb, diffuseColor.a );
+
+			#include <premultiplied_alpha_fragment>
+			#include <tonemapping_fragment>
+			#include <encodings_fragment>
+			#include <fog_fragment>
+
+		}
+		`
+};
+
+THREE.LineMaterial = function ( parameters ) {
+
+	THREE.ShaderMaterial.call( this, {
+
+		type: 'LineMaterial',
+
+		uniforms: THREE.UniformsUtils.clone( THREE.ShaderLib[ 'line' ].uniforms ),
+
+		vertexShader: THREE.ShaderLib[ 'line' ].vertexShader,
+		fragmentShader: THREE.ShaderLib[ 'line' ].fragmentShader
+
+	} );
+
+	this.dashed = false;
+
+	Object.defineProperties( this, {
+
+		color: {
+
+			enumerable: true,
+
+			get: function () {
+
+				return this.uniforms.diffuse.value;
+
+			},
+
+			set: function ( value ) {
+
+				this.uniforms.diffuse.value = value;
+
+			}
+
+		},
+
+		linewidth: {
+
+			enumerable: true,
+
+			get: function () {
+
+				return this.uniforms.linewidth.value;
+
+			},
+
+			set: function ( value ) {
+
+				this.uniforms.linewidth.value = value;
+
+			}
+
+		},
+
+		dashScale: {
+
+			enumerable: true,
+
+			get: function () {
+
+				return this.uniforms.dashScale.value;
+
+			},
+
+			set: function ( value ) {
+
+				this.uniforms.dashScale.value = value;
+
+			}
+
+		},
+
+		dashSize: {
+
+			enumerable: true,
+
+			get: function () {
+
+				return this.uniforms.dashSize.value;
+
+			},
+
+			set: function ( value ) {
+
+				this.uniforms.dashSize.value = value;
+
+			}
+
+		},
+
+		gapSize: {
+
+			enumerable: true,
+
+			get: function () {
+
+				return this.uniforms.gapSize.value;
+
+			},
+
+			set: function ( value ) {
+
+				this.uniforms.gapSize.value = value;
+
+			}
+
+		},
+
+		resolution: {
+
+			enumerable: true,
+
+			get: function () {
+
+				return this.uniforms.resolution.value;
+
+			},
+
+			set: function ( value ) {
+
+				this.uniforms.resolution.value.copy( value );
+
+			}
+
+		}
+
+	} );
+
+	this.setValues( parameters );
+
+};
+
+THREE.LineMaterial.prototype = Object.create( THREE.ShaderMaterial.prototype );
+THREE.LineMaterial.prototype.constructor = THREE.LineMaterial;
+
+THREE.LineMaterial.prototype.isLineMaterial = true;
+
+THREE.LineMaterial.prototype.copy = function ( source ) {
+
+	THREE.ShaderMaterial.prototype.copy.call( this, source );
+
+	this.color.copy( source.color );
+
+	this.linewidth = source.linewidth;
+
+	this.resolution = source.resolution;
+
+	// todo
+
+	return this;
+
+};
+

+ 65 - 0
gcode_viewer/js/LineSegments2.js

@@ -0,0 +1,65 @@
+/**
+ * @author WestLangley / http://github.com/WestLangley
+ *
+ */
+
+THREE.LineSegments2 = function ( geometry, material ) {
+
+	THREE.Mesh.call( this );
+
+	this.type = 'LineSegments2';
+
+	this.geometry = geometry !== undefined ? geometry : new THREE.LineSegmentsGeometry();
+	this.material = material !== undefined ? material : new THREE.LineMaterial( { color: Math.random() * 0xffffff } );
+
+};
+
+THREE.LineSegments2.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), {
+
+	constructor: THREE.LineSegments2,
+
+	isLineSegments2: true,
+
+	computeLineDistances: ( function () { // for backwards-compatability, but could be a method of LineSegmentsGeometry...
+
+		var start = new THREE.Vector3();
+		var end = new THREE.Vector3();
+
+		return function computeLineDistances() {
+
+			var geometry = this.geometry;
+
+			var instanceStart = geometry.attributes.instanceStart;
+			var instanceEnd = geometry.attributes.instanceEnd;
+			var lineDistances = new Float32Array( 2 * instanceStart.data.count );
+
+			for ( var i = 0, j = 0, l = instanceStart.data.count; i < l; i ++, j += 2 ) {
+
+				start.fromBufferAttribute( instanceStart, i );
+				end.fromBufferAttribute( instanceEnd, i );
+
+				lineDistances[ j ] = ( j === 0 ) ? 0 : lineDistances[ j - 1 ];
+				lineDistances[ j + 1 ] = lineDistances[ j ] + start.distanceTo( end );
+
+			}
+
+			var instanceDistanceBuffer = new THREE.InstancedInterleavedBuffer( lineDistances, 2, 1 ); // d0, d1
+
+			geometry.addAttribute( 'instanceDistanceStart', new THREE.InterleavedBufferAttribute( instanceDistanceBuffer, 1, 0 ) ); // d0
+			geometry.addAttribute( 'instanceDistanceEnd', new THREE.InterleavedBufferAttribute( instanceDistanceBuffer, 1, 1 ) ); // d1
+
+			return this;
+
+		};
+
+	}() ),
+
+	copy: function ( /* source */ ) {
+
+		// todo
+
+		return this;
+
+	}
+
+} );

+ 258 - 0
gcode_viewer/js/LineSegmentsGeometry.js

@@ -0,0 +1,258 @@
+/**
+ * @author WestLangley / http://github.com/WestLangley
+ *
+ */
+
+THREE.LineSegmentsGeometry = function () {
+
+	THREE.InstancedBufferGeometry.call( this );
+
+	this.type = 'LineSegmentsGeometry';
+
+	var positions = [ - 1, 2, 0, 1, 2, 0, - 1, 1, 0, 1, 1, 0, - 1, 0, 0, 1, 0, 0, - 1, - 1, 0, 1, - 1, 0 ];
+	var uvs = [ - 1, 2, 1, 2, - 1, 1, 1, 1, - 1, - 1, 1, - 1, - 1, - 2, 1, - 2 ];
+	var index = [ 0, 2, 1, 2, 3, 1, 2, 4, 3, 4, 5, 3, 4, 6, 5, 6, 7, 5 ];
+
+	this.setIndex( index );
+	this.addAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
+	this.addAttribute( 'uv', new THREE.Float32BufferAttribute( uvs, 2 ) );
+
+};
+
+THREE.LineSegmentsGeometry.prototype = Object.assign( Object.create( THREE.InstancedBufferGeometry.prototype ), {
+
+	constructor: THREE.LineSegmentsGeometry,
+
+	isLineSegmentsGeometry: true,
+
+	applyMatrix: function ( matrix ) {
+
+		var start = this.attributes.instanceStart;
+		var end = this.attributes.instanceEnd;
+
+		if ( start !== undefined ) {
+
+			matrix.applyToBufferAttribute( start );
+
+			matrix.applyToBufferAttribute( end );
+
+			start.data.needsUpdate = true;
+
+		}
+
+		if ( this.boundingBox !== null ) {
+
+			this.computeBoundingBox();
+
+		}
+
+		if ( this.boundingSphere !== null ) {
+
+			this.computeBoundingSphere();
+
+		}
+
+		return this;
+
+	},
+
+	setPositions: function ( array ) {
+
+		var lineSegments;
+
+		if ( array instanceof Float32Array ) {
+
+			lineSegments = array;
+
+		} else if ( Array.isArray( array ) ) {
+
+			lineSegments = new Float32Array( array );
+
+		}
+
+		var instanceBuffer = new THREE.InstancedInterleavedBuffer( lineSegments, 6, 1 ); // xyz, xyz
+
+		this.addAttribute( 'instanceStart', new THREE.InterleavedBufferAttribute( instanceBuffer, 3, 0 ) ); // xyz
+		this.addAttribute( 'instanceEnd', new THREE.InterleavedBufferAttribute( instanceBuffer, 3, 3 ) ); // xyz
+
+		//
+
+		this.computeBoundingBox();
+		this.computeBoundingSphere();
+
+		return this;
+
+	},
+
+	setColors: function ( array ) {
+
+		var colors;
+
+		if ( array instanceof Float32Array ) {
+
+			colors = array;
+
+		} else if ( Array.isArray( array ) ) {
+
+			colors = new Float32Array( array );
+
+		}
+
+		var instanceColorBuffer = new THREE.InstancedInterleavedBuffer( colors, 6, 1 ); // rgb, rgb
+
+		this.addAttribute( 'instanceColorStart', new THREE.InterleavedBufferAttribute( instanceColorBuffer, 3, 0 ) ); // rgb
+		this.addAttribute( 'instanceColorEnd', new THREE.InterleavedBufferAttribute( instanceColorBuffer, 3, 3 ) ); // rgb
+
+		return this;
+
+	},
+
+	fromWireframeGeometry: function ( geometry ) {
+
+		this.setPositions( geometry.attributes.position.array );
+
+		return this;
+
+	},
+
+	fromEdgesGeometry: function ( geometry ) {
+
+		this.setPositions( geometry.attributes.position.array );
+
+		return this;
+
+	},
+
+	fromMesh: function ( mesh ) {
+
+		this.fromWireframeGeometry( new THREE.WireframeGeometry( mesh.geometry ) );
+
+		// set colors, maybe
+
+		return this;
+
+	},
+
+	fromLineSegements: function ( lineSegments ) {
+
+		var geometry = lineSegments.geometry;
+
+		if ( geometry.isGeometry ) {
+
+			this.setPositions( geometry.vertices );
+
+		} else if ( geometry.isBufferGeometry ) {
+
+			this.setPositions( geometry.position.array ); // assumes non-indexed
+
+		}
+
+		// set colors, maybe
+
+		return this;
+
+	},
+
+	computeBoundingBox: function () {
+
+		var box = new THREE.Box3();
+
+		return function computeBoundingBox() {
+
+			if ( this.boundingBox === null ) {
+
+				this.boundingBox = new THREE.Box3();
+
+			}
+
+			var start = this.attributes.instanceStart;
+			var end = this.attributes.instanceEnd;
+
+			if ( start !== undefined && end !== undefined ) {
+
+				this.boundingBox.setFromBufferAttribute( start );
+
+				box.setFromBufferAttribute( end );
+
+				this.boundingBox.union( box );
+
+			}
+
+		};
+
+	}(),
+
+	computeBoundingSphere: function () {
+
+		var vector = new THREE.Vector3();
+
+		return function computeBoundingSphere() {
+
+			if ( this.boundingSphere === null ) {
+
+				this.boundingSphere = new THREE.Sphere();
+
+			}
+
+			if ( this.boundingBox === null ) {
+
+				this.computeBoundingBox();
+
+			}
+
+			var start = this.attributes.instanceStart;
+			var end = this.attributes.instanceEnd;
+
+			if ( start !== undefined && end !== undefined ) {
+
+				var center = this.boundingSphere.center;
+
+				this.boundingBox.getCenter( center );
+
+				var maxRadiusSq = 0;
+
+				for ( var i = 0, il = start.count; i < il; i ++ ) {
+
+					vector.fromBufferAttribute( start, i );
+					maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) );
+
+					vector.fromBufferAttribute( end, i );
+					maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) );
+
+				}
+
+				this.boundingSphere.radius = Math.sqrt( maxRadiusSq );
+
+				if ( isNaN( this.boundingSphere.radius ) ) {
+
+					console.error( 'THREE.LineSegmentsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', this );
+
+				}
+
+			}
+
+		};
+
+	}(),
+
+	toJSON: function () {
+
+		// todo
+
+	},
+
+	clone: function () {
+
+		// todo
+
+	},
+
+	copy: function ( /* source */ ) {
+
+		// todo
+
+		return this;
+
+	}
+
+} );

+ 185 - 0
gcode_viewer/js/Lut.js

@@ -0,0 +1,185 @@
+/**
+ * @author daron1337 / http://daron1337.github.io/
+ */
+
+THREE.Lut = function ( colormap, numberofcolors ) {
+
+	this.lut = [];
+	this.setColorMap( colormap, numberofcolors );
+	return this;
+
+};
+
+THREE.Lut.prototype = {
+
+	constructor: THREE.Lut,
+
+	lut: [], map: [], n: 256, minV: 0, maxV: 1,
+
+	set: function ( value ) {
+
+		if ( value instanceof THREE.Lut ) {
+
+			this.copy( value );
+
+		}
+
+		return this;
+
+	},
+
+	setMin: function ( min ) {
+
+		this.minV = min;
+
+		return this;
+
+	},
+
+	setMax: function ( max ) {
+
+		this.maxV = max;
+
+		return this;
+
+	},
+
+	setColorMap: function ( colormap, numberofcolors ) {
+
+		this.map = THREE.ColorMapKeywords[ colormap ] || THREE.ColorMapKeywords.rainbow;
+		this.n = numberofcolors || 32;
+
+		var step = 1.0 / this.n;
+
+		this.lut.length = 0;
+		for ( var i = 0; i <= 1; i += step ) {
+
+			for ( var j = 0; j < this.map.length - 1; j ++ ) {
+
+				if ( i >= this.map[ j ][ 0 ] && i < this.map[ j + 1 ][ 0 ] ) {
+
+					var min = this.map[ j ][ 0 ];
+					var max = this.map[ j + 1 ][ 0 ];
+
+					var minColor = new THREE.Color( this.map[ j ][ 1 ] );
+					var maxColor = new THREE.Color( this.map[ j + 1 ][ 1 ] );
+
+					var color = minColor.lerp( maxColor, ( i - min ) / ( max - min ) );
+
+					this.lut.push( color );
+
+				}
+
+			}
+
+		}
+
+		return this;
+
+	},
+
+	copy: function ( lut ) {
+
+		this.lut = lut.lut;
+		this.map = lut.map;
+		this.n = lut.n;
+		this.minV = lut.minV;
+		this.maxV = lut.maxV;
+
+		return this;
+
+	},
+
+	getColor: function ( alpha ) {
+
+		if ( alpha <= this.minV ) {
+
+			alpha = this.minV;
+
+		} else if ( alpha >= this.maxV ) {
+
+			alpha = this.maxV;
+
+		}
+
+		alpha = ( alpha - this.minV ) / ( this.maxV - this.minV );
+
+		var colorPosition = Math.round( alpha * this.n );
+		colorPosition == this.n ? colorPosition -= 1 : colorPosition;
+
+		return this.lut[ colorPosition ];
+
+	},
+
+	addColorMap: function ( colormapName, arrayOfColors ) {
+
+		THREE.ColorMapKeywords[ colormapName ] = arrayOfColors;
+
+	},
+
+	createCanvas: function () {
+
+		var canvas = document.createElement( 'canvas' );
+		canvas.width = 1;
+		canvas.height = this.n;
+
+		this.updateCanvas( canvas );
+
+		return canvas;
+
+	},
+
+	updateCanvas: function ( canvas ) {
+
+		var ctx = canvas.getContext( '2d', { alpha: false } );
+
+		var imageData = ctx.getImageData( 0, 0, 1, this.n );
+
+		var data = imageData.data;
+
+		var k = 0;
+
+		var step = 1.0 / this.n;
+
+		for ( var i = 1; i >= 0; i -= step ) {
+
+			for ( var j = this.map.length - 1; j >= 0; j -- ) {
+
+				if ( i < this.map[ j ][ 0 ] && i >= this.map[ j - 1 ][ 0 ] ) {
+
+					var min = this.map[ j - 1 ][ 0 ];
+					var max = this.map[ j ][ 0 ];
+
+					var minColor = new THREE.Color( this.map[ j - 1 ][ 1 ] );
+					var maxColor = new THREE.Color( this.map[ j ][ 1 ] );
+
+					var color = minColor.lerp( maxColor, ( i - min ) / ( max - min ) );
+
+					data[ k * 4 ] = Math.round( color.r * 255 );
+					data[ k * 4 + 1 ] = Math.round( color.g * 255 );
+					data[ k * 4 + 2 ] = Math.round( color.b * 255 );
+					data[ k * 4 + 3 ] = 255;
+
+					k += 1;
+
+				}
+
+			}
+
+		}
+
+		ctx.putImageData( imageData, 0, 0 );
+
+		return canvas;
+
+	}
+};
+
+THREE.ColorMapKeywords = {
+
+	"rainbow": [[ 0.0, 0x0000FF ], [ 0.2, 0x00FFFF ], [ 0.5, 0x00FF00 ], [ 0.8, 0xFFFF00 ], [ 1.0, 0xFF0000 ]],
+	"cooltowarm": [[ 0.0, 0x3C4EC2 ], [ 0.2, 0x9BBCFF ], [ 0.5, 0xDCDCDC ], [ 0.8, 0xF6A385 ], [ 1.0, 0xB40426 ]],
+	"blackbody": [[ 0.0, 0x000000 ], [ 0.2, 0x780000 ], [ 0.5, 0xE63200 ], [ 0.8, 0xFFFF00 ], [ 1.0, 0xFFFFFF ]],
+	"grayscale": [[ 0.0, 0x000000 ], [ 0.2, 0x404040 ], [ 0.5, 0x7F7F80 ], [ 0.8, 0xBFBFBF ], [ 1.0, 0xFFFFFF ]]
+
+};

+ 797 - 0
gcode_viewer/js/OBJLoader.js

@@ -0,0 +1,797 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+THREE.OBJLoader = ( function () {
+
+	// o object_name | g group_name
+	var object_pattern = /^[og]\s*(.+)?/;
+	// mtllib file_reference
+	var material_library_pattern = /^mtllib /;
+	// usemtl material_name
+	var material_use_pattern = /^usemtl /;
+
+	function ParserState() {
+
+		var state = {
+			objects: [],
+			object: {},
+
+			vertices: [],
+			normals: [],
+			colors: [],
+			uvs: [],
+
+			materialLibraries: [],
+
+			startObject: function ( name, fromDeclaration ) {
+
+				// If the current object (initial from reset) is not from a g/o declaration in the parsed
+				// file. We need to use it for the first parsed g/o to keep things in sync.
+				if ( this.object && this.object.fromDeclaration === false ) {
+
+					this.object.name = name;
+					this.object.fromDeclaration = ( fromDeclaration !== false );
+					return;
+
+				}
+
+				var previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined );
+
+				if ( this.object && typeof this.object._finalize === 'function' ) {
+
+					this.object._finalize( true );
+
+				}
+
+				this.object = {
+					name: name || '',
+					fromDeclaration: ( fromDeclaration !== false ),
+
+					geometry: {
+						vertices: [],
+						normals: [],
+						colors: [],
+						uvs: []
+					},
+					materials: [],
+					smooth: true,
+
+					startMaterial: function ( name, libraries ) {
+
+						var previous = this._finalize( false );
+
+						// New usemtl declaration overwrites an inherited material, except if faces were declared
+						// after the material, then it must be preserved for proper MultiMaterial continuation.
+						if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) {
+
+							this.materials.splice( previous.index, 1 );
+
+						}
+
+						var material = {
+							index: this.materials.length,
+							name: name || '',
+							mtllib: ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ),
+							smooth: ( previous !== undefined ? previous.smooth : this.smooth ),
+							groupStart: ( previous !== undefined ? previous.groupEnd : 0 ),
+							groupEnd: - 1,
+							groupCount: - 1,
+							inherited: false,
+
+							clone: function ( index ) {
+
+								var cloned = {
+									index: ( typeof index === 'number' ? index : this.index ),
+									name: this.name,
+									mtllib: this.mtllib,
+									smooth: this.smooth,
+									groupStart: 0,
+									groupEnd: - 1,
+									groupCount: - 1,
+									inherited: false
+								};
+								cloned.clone = this.clone.bind( cloned );
+								return cloned;
+
+							}
+						};
+
+						this.materials.push( material );
+
+						return material;
+
+					},
+
+					currentMaterial: function () {
+
+						if ( this.materials.length > 0 ) {
+
+							return this.materials[ this.materials.length - 1 ];
+
+						}
+
+						return undefined;
+
+					},
+
+					_finalize: function ( end ) {
+
+						var lastMultiMaterial = this.currentMaterial();
+						if ( lastMultiMaterial && lastMultiMaterial.groupEnd === - 1 ) {
+
+							lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3;
+							lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart;
+							lastMultiMaterial.inherited = false;
+
+						}
+
+						// Ignore objects tail materials if no face declarations followed them before a new o/g started.
+						if ( end && this.materials.length > 1 ) {
+
+							for ( var mi = this.materials.length - 1; mi >= 0; mi -- ) {
+
+								if ( this.materials[ mi ].groupCount <= 0 ) {
+
+									this.materials.splice( mi, 1 );
+
+								}
+
+							}
+
+						}
+
+						// Guarantee at least one empty material, this makes the creation later more straight forward.
+						if ( end && this.materials.length === 0 ) {
+
+							this.materials.push( {
+								name: '',
+								smooth: this.smooth
+							} );
+
+						}
+
+						return lastMultiMaterial;
+
+					}
+				};
+
+				// Inherit previous objects material.
+				// Spec tells us that a declared material must be set to all objects until a new material is declared.
+				// If a usemtl declaration is encountered while this new object is being parsed, it will
+				// overwrite the inherited material. Exception being that there was already face declarations
+				// to the inherited material, then it will be preserved for proper MultiMaterial continuation.
+
+				if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function' ) {
+
+					var declared = previousMaterial.clone( 0 );
+					declared.inherited = true;
+					this.object.materials.push( declared );
+
+				}
+
+				this.objects.push( this.object );
+
+			},
+
+			finalize: function () {
+
+				if ( this.object && typeof this.object._finalize === 'function' ) {
+
+					this.object._finalize( true );
+
+				}
+
+			},
+
+			parseVertexIndex: function ( value, len ) {
+
+				var index = parseInt( value, 10 );
+				return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
+
+			},
+
+			parseNormalIndex: function ( value, len ) {
+
+				var index = parseInt( value, 10 );
+				return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
+
+			},
+
+			parseUVIndex: function ( value, len ) {
+
+				var index = parseInt( value, 10 );
+				return ( index >= 0 ? index - 1 : index + len / 2 ) * 2;
+
+			},
+
+			addVertex: function ( a, b, c ) {
+
+				var src = this.vertices;
+				var dst = this.object.geometry.vertices;
+
+				dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
+				dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
+				dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
+
+			},
+
+			addVertexPoint: function ( a ) {
+
+				var src = this.vertices;
+				var dst = this.object.geometry.vertices;
+
+				dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
+
+			},
+
+			addVertexLine: function ( a ) {
+
+				var src = this.vertices;
+				var dst = this.object.geometry.vertices;
+
+				dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
+
+			},
+
+			addNormal: function ( a, b, c ) {
+
+				var src = this.normals;
+				var dst = this.object.geometry.normals;
+
+				dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
+				dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
+				dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
+
+			},
+
+			addColor: function ( a, b, c ) {
+
+				var src = this.colors;
+				var dst = this.object.geometry.colors;
+
+				dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
+				dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
+				dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
+
+			},
+
+			addUV: function ( a, b, c ) {
+
+				var src = this.uvs;
+				var dst = this.object.geometry.uvs;
+
+				dst.push( src[ a + 0 ], src[ a + 1 ] );
+				dst.push( src[ b + 0 ], src[ b + 1 ] );
+				dst.push( src[ c + 0 ], src[ c + 1 ] );
+
+			},
+
+			addUVLine: function ( a ) {
+
+				var src = this.uvs;
+				var dst = this.object.geometry.uvs;
+
+				dst.push( src[ a + 0 ], src[ a + 1 ] );
+
+			},
+
+			addFace: function ( a, b, c, ua, ub, uc, na, nb, nc ) {
+
+				var vLen = this.vertices.length;
+
+				var ia = this.parseVertexIndex( a, vLen );
+				var ib = this.parseVertexIndex( b, vLen );
+				var ic = this.parseVertexIndex( c, vLen );
+
+				this.addVertex( ia, ib, ic );
+
+				if ( ua !== undefined && ua !== '' ) {
+
+					var uvLen = this.uvs.length;
+					ia = this.parseUVIndex( ua, uvLen );
+					ib = this.parseUVIndex( ub, uvLen );
+					ic = this.parseUVIndex( uc, uvLen );
+					this.addUV( ia, ib, ic );
+
+				}
+
+				if ( na !== undefined && na !== '' ) {
+
+					// Normals are many times the same. If so, skip function call and parseInt.
+					var nLen = this.normals.length;
+					ia = this.parseNormalIndex( na, nLen );
+
+					ib = na === nb ? ia : this.parseNormalIndex( nb, nLen );
+					ic = na === nc ? ia : this.parseNormalIndex( nc, nLen );
+
+					this.addNormal( ia, ib, ic );
+
+				}
+
+				if ( this.colors.length > 0 ) {
+
+					this.addColor( ia, ib, ic );
+
+				}
+
+			},
+
+			addPointGeometry: function ( vertices ) {
+
+				this.object.geometry.type = 'Points';
+
+				var vLen = this.vertices.length;
+
+				for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) {
+
+					this.addVertexPoint( this.parseVertexIndex( vertices[ vi ], vLen ) );
+
+				}
+
+			},
+
+			addLineGeometry: function ( vertices, uvs ) {
+
+				this.object.geometry.type = 'Line';
+
+				var vLen = this.vertices.length;
+				var uvLen = this.uvs.length;
+
+				for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) {
+
+					this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) );
+
+				}
+
+				for ( var uvi = 0, l = uvs.length; uvi < l; uvi ++ ) {
+
+					this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) );
+
+				}
+
+			}
+
+		};
+
+		state.startObject( '', false );
+
+		return state;
+
+	}
+
+	//
+
+	function OBJLoader( manager ) {
+
+		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
+
+		this.materials = null;
+
+	}
+
+	OBJLoader.prototype = {
+
+		constructor: OBJLoader,
+
+		load: function ( url, onLoad, onProgress, onError ) {
+
+			var scope = this;
+
+			var loader = new THREE.FileLoader( scope.manager );
+			loader.setPath( this.path );
+			loader.load( url, function ( text ) {
+
+				onLoad( scope.parse( text ) );
+
+			}, onProgress, onError );
+
+		},
+
+		setPath: function ( value ) {
+
+			this.path = value;
+
+			return this;
+
+		},
+
+		setMaterials: function ( materials ) {
+
+			this.materials = materials;
+
+			return this;
+
+		},
+
+		parse: function ( text ) {
+
+			console.time( 'OBJLoader' );
+
+			var state = new ParserState();
+
+			if ( text.indexOf( '\r\n' ) !== - 1 ) {
+
+				// This is faster than String.split with regex that splits on both
+				text = text.replace( /\r\n/g, '\n' );
+
+			}
+
+			if ( text.indexOf( '\\\n' ) !== - 1 ) {
+
+				// join lines separated by a line continuation character (\)
+				text = text.replace( /\\\n/g, '' );
+
+			}
+
+			var lines = text.split( '\n' );
+			var line = '', lineFirstChar = '';
+			var lineLength = 0;
+			var result = [];
+
+			// Faster to just trim left side of the line. Use if available.
+			var trimLeft = ( typeof ''.trimLeft === 'function' );
+
+			for ( var i = 0, l = lines.length; i < l; i ++ ) {
+
+				line = lines[ i ];
+
+				line = trimLeft ? line.trimLeft() : line.trim();
+
+				lineLength = line.length;
+
+				if ( lineLength === 0 ) continue;
+
+				lineFirstChar = line.charAt( 0 );
+
+				// @todo invoke passed in handler if any
+				if ( lineFirstChar === '#' ) continue;
+
+				if ( lineFirstChar === 'v' ) {
+
+					var data = line.split( /\s+/ );
+
+					switch ( data[ 0 ] ) {
+
+						case 'v':
+							state.vertices.push(
+								parseFloat( data[ 1 ] ),
+								parseFloat( data[ 2 ] ),
+								parseFloat( data[ 3 ] )
+							);
+							if ( data.length >= 7 ) {
+
+								state.colors.push(
+									parseFloat( data[ 4 ] ),
+									parseFloat( data[ 5 ] ),
+									parseFloat( data[ 6 ] )
+
+								);
+
+							}
+							break;
+						case 'vn':
+							state.normals.push(
+								parseFloat( data[ 1 ] ),
+								parseFloat( data[ 2 ] ),
+								parseFloat( data[ 3 ] )
+							);
+							break;
+						case 'vt':
+							state.uvs.push(
+								parseFloat( data[ 1 ] ),
+								parseFloat( data[ 2 ] )
+							);
+							break;
+
+					}
+
+				} else if ( lineFirstChar === 'f' ) {
+
+					var lineData = line.substr( 1 ).trim();
+					var vertexData = lineData.split( /\s+/ );
+					var faceVertices = [];
+
+					// Parse the face vertex data into an easy to work with format
+
+					for ( var j = 0, jl = vertexData.length; j < jl; j ++ ) {
+
+						var vertex = vertexData[ j ];
+
+						if ( vertex.length > 0 ) {
+
+							var vertexParts = vertex.split( '/' );
+							faceVertices.push( vertexParts );
+
+						}
+
+					}
+
+					// Draw an edge between the first vertex and all subsequent vertices to form an n-gon
+
+					var v1 = faceVertices[ 0 ];
+
+					for ( var j = 1, jl = faceVertices.length - 1; j < jl; j ++ ) {
+
+						var v2 = faceVertices[ j ];
+						var v3 = faceVertices[ j + 1 ];
+
+						state.addFace(
+							v1[ 0 ], v2[ 0 ], v3[ 0 ],
+							v1[ 1 ], v2[ 1 ], v3[ 1 ],
+							v1[ 2 ], v2[ 2 ], v3[ 2 ]
+						);
+
+					}
+
+				} else if ( lineFirstChar === 'l' ) {
+
+					var lineParts = line.substring( 1 ).trim().split( " " );
+					var lineVertices = [], lineUVs = [];
+
+					if ( line.indexOf( "/" ) === - 1 ) {
+
+						lineVertices = lineParts;
+
+					} else {
+
+						for ( var li = 0, llen = lineParts.length; li < llen; li ++ ) {
+
+							var parts = lineParts[ li ].split( "/" );
+
+							if ( parts[ 0 ] !== "" ) lineVertices.push( parts[ 0 ] );
+							if ( parts[ 1 ] !== "" ) lineUVs.push( parts[ 1 ] );
+
+						}
+
+					}
+					state.addLineGeometry( lineVertices, lineUVs );
+
+				} else if ( lineFirstChar === 'p' ) {
+
+					var lineData = line.substr( 1 ).trim();
+					var pointData = lineData.split( " " );
+
+					state.addPointGeometry( pointData );
+
+				} else if ( ( result = object_pattern.exec( line ) ) !== null ) {
+
+					// o object_name
+					// or
+					// g group_name
+
+					// WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869
+					// var name = result[ 0 ].substr( 1 ).trim();
+					var name = ( " " + result[ 0 ].substr( 1 ).trim() ).substr( 1 );
+
+					state.startObject( name );
+
+				} else if ( material_use_pattern.test( line ) ) {
+
+					// material
+
+					state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries );
+
+				} else if ( material_library_pattern.test( line ) ) {
+
+					// mtl file
+
+					state.materialLibraries.push( line.substring( 7 ).trim() );
+
+				} else if ( lineFirstChar === 's' ) {
+
+					result = line.split( ' ' );
+
+					// smooth shading
+
+					// @todo Handle files that have varying smooth values for a set of faces inside one geometry,
+					// but does not define a usemtl for each face set.
+					// This should be detected and a dummy material created (later MultiMaterial and geometry groups).
+					// This requires some care to not create extra material on each smooth value for "normal" obj files.
+					// where explicit usemtl defines geometry groups.
+					// Example asset: examples/models/obj/cerberus/Cerberus.obj
+
+					/*
+					 * http://paulbourke.net/dataformats/obj/
+					 * or
+					 * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf
+					 *
+					 * From chapter "Grouping" Syntax explanation "s group_number":
+					 * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off.
+					 * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form
+					 * surfaces, smoothing groups are either turned on or off; there is no difference between values greater
+					 * than 0."
+					 */
+					if ( result.length > 1 ) {
+
+						var value = result[ 1 ].trim().toLowerCase();
+						state.object.smooth = ( value !== '0' && value !== 'off' );
+
+					} else {
+
+						// ZBrush can produce "s" lines #11707
+						state.object.smooth = true;
+
+					}
+					var material = state.object.currentMaterial();
+					if ( material ) material.smooth = state.object.smooth;
+
+				} else {
+
+					// Handle null terminated files without exception
+					if ( line === '\0' ) continue;
+
+					throw new Error( 'THREE.OBJLoader: Unexpected line: "' + line + '"' );
+
+				}
+
+			}
+
+			state.finalize();
+
+			var container = new THREE.Group();
+			container.materialLibraries = [].concat( state.materialLibraries );
+
+			for ( var i = 0, l = state.objects.length; i < l; i ++ ) {
+
+				var object = state.objects[ i ];
+				var geometry = object.geometry;
+				var materials = object.materials;
+				var isLine = ( geometry.type === 'Line' );
+				var isPoints = ( geometry.type === 'Points' );
+				var hasVertexColors = false;
+
+				// Skip o/g line declarations that did not follow with any faces
+				if ( geometry.vertices.length === 0 ) continue;
+
+				var buffergeometry = new THREE.BufferGeometry();
+
+				buffergeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( geometry.vertices, 3 ) );
+
+				if ( geometry.normals.length > 0 ) {
+
+					buffergeometry.addAttribute( 'normal', new THREE.Float32BufferAttribute( geometry.normals, 3 ) );
+
+				} else {
+
+					buffergeometry.computeVertexNormals();
+
+				}
+
+				if ( geometry.colors.length > 0 ) {
+
+					hasVertexColors = true;
+					buffergeometry.addAttribute( 'color', new THREE.Float32BufferAttribute( geometry.colors, 3 ) );
+
+				}
+
+				if ( geometry.uvs.length > 0 ) {
+
+					buffergeometry.addAttribute( 'uv', new THREE.Float32BufferAttribute( geometry.uvs, 2 ) );
+
+				}
+
+				// Create materials
+
+				var createdMaterials = [];
+
+				for ( var mi = 0, miLen = materials.length; mi < miLen; mi ++ ) {
+
+					var sourceMaterial = materials[ mi ];
+					var material = undefined;
+
+					if ( this.materials !== null ) {
+
+						material = this.materials.create( sourceMaterial.name );
+
+						// mtl etc. loaders probably can't create line materials correctly, copy properties to a line material.
+						if ( isLine && material && ! ( material instanceof THREE.LineBasicMaterial ) ) {
+
+							var materialLine = new THREE.LineBasicMaterial();
+							THREE.Material.prototype.copy.call( materialLine, material );
+							materialLine.color.copy( material.color );
+							materialLine.lights = false;
+							material = materialLine;
+
+						} else if ( isPoints && material && ! ( material instanceof THREE.PointsMaterial ) ) {
+
+							var materialPoints = new THREE.PointsMaterial( { size: 10, sizeAttenuation: false } );
+							THREE.Material.prototype.copy.call( materialPoints, material );
+							materialPoints.color.copy( material.color );
+							materialPoints.map = material.map;
+							materialPoints.lights = false;
+							material = materialPoints;
+
+						}
+
+					}
+
+					if ( ! material ) {
+
+						if ( isLine ) {
+
+							material = new THREE.LineBasicMaterial();
+
+						} else if ( isPoints ) {
+
+							material = new THREE.PointsMaterial( { size: 1, sizeAttenuation: false } );
+
+						} else {
+
+							material = new THREE.MeshPhongMaterial();
+
+						}
+
+						material.name = sourceMaterial.name;
+
+					}
+
+					material.flatShading = sourceMaterial.smooth ? false : true;
+					material.vertexColors = hasVertexColors ? THREE.VertexColors : THREE.NoColors;
+
+					createdMaterials.push( material );
+
+				}
+
+				// Create mesh
+
+				var mesh;
+
+				if ( createdMaterials.length > 1 ) {
+
+					for ( var mi = 0, miLen = materials.length; mi < miLen; mi ++ ) {
+
+						var sourceMaterial = materials[ mi ];
+						buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi );
+
+					}
+
+					if ( isLine ) {
+
+						mesh = new THREE.LineSegments( buffergeometry, createdMaterials );
+
+					} else if ( isPoints ) {
+
+						mesh = new THREE.Points( buffergeometry, createdMaterials );
+
+					} else {
+
+						mesh = new THREE.Mesh( buffergeometry, createdMaterials );
+
+					}
+
+				} else {
+
+					if ( isLine ) {
+
+						mesh = new THREE.LineSegments( buffergeometry, createdMaterials[ 0 ] );
+
+					} else if ( isPoints ) {
+
+						mesh = new THREE.Points( buffergeometry, createdMaterials[ 0 ] );
+
+					} else {
+
+						mesh = new THREE.Mesh( buffergeometry, createdMaterials[ 0 ] );
+
+					}
+
+				}
+
+				mesh.name = object.name;
+
+				container.add( mesh );
+
+			}
+
+			console.timeEnd( 'OBJLoader' );
+
+			return container;
+
+		}
+
+	};
+
+	return OBJLoader;
+
+} )();

+ 877 - 0
gcode_viewer/js/bambuddy_adapter.js

@@ -0,0 +1,877 @@
+/**
+ * bambuddy_adapter.js
+ * Bridges OctoPrint-PrettyGCode to Bambuddy's API.
+ *
+ * Load this BEFORE prettygcode.js. It provides:
+ *   - OCTOPRINT_VIEWMODELS shim
+ *   - Minimal KnockoutJS observable shim (ko.observable)
+ *   - fetch() + XHR interceptors for path rewriting
+ *   - Bambuddy WebSocket → fromCurrentData bridge
+ *   - File picker backed by Bambuddy's library API
+ *   - Settings load/save via plugin settings endpoint
+ *
+ * What works:
+ *   - Full 3D GCode visualisation
+ *   - Dark mode and all dat.GUI settings
+ *   - File selection from Bambuddy's file library
+ *   - Print progress highlight (% based)
+ *   - Auto-load currently printing file
+ *
+ * What doesn't work (Bambu hardware limitation):
+ *   - Live nozzle animation during printing — Bambu printers do not expose
+ *     GCode serial echo logs (Send: G1 X...), so PrintHeadSimulator has no input.
+ */
+
+(function () {
+    'use strict';
+
+    const API_BASE = '/api/v1';
+    const VIEWER_BASE = '/gcode-viewer'; // static assets now served from here
+
+    // -------------------------------------------------------------------------
+    // Auth helper
+    // -------------------------------------------------------------------------
+    function authHeaders() {
+        // sessionStorage is used when the user opts out of "remember me";
+        // fall back to localStorage for persistent sessions.
+        const token = sessionStorage.getItem('auth_token') ?? localStorage.getItem('auth_token');
+        return token ? { Authorization: 'Bearer ' + token } : {};
+    }
+
+    // When auth is enabled and the user has no valid token, every API call
+    // returns 401 and the viewer chrome stays on screen showing empty state.
+    // Intercept the first 401 and hand control back to the SPA, which owns
+    // the login flow and will redirect to /login when appropriate.
+    let _authRedirectFired = false;
+    function apiFetch(path, opts) {
+        return fetch(API_BASE + path, {
+            ...opts,
+            headers: { ...authHeaders(), ...(opts && opts.headers) },
+            cache: 'no-store',
+        }).then((response) => {
+            if (response.status === 401 && !_authRedirectFired) {
+                _authRedirectFired = true;
+                try {
+                    sessionStorage.removeItem('auth_token');
+                    localStorage.removeItem('auth_token');
+                } catch (e) { /* storage unavailable */ }
+                window.top.location.replace('/');
+            }
+            return response;
+        });
+    }
+
+    // -------------------------------------------------------------------------
+    // 1. Minimal KnockoutJS shim  (ko.observable / ko.computed)
+    // -------------------------------------------------------------------------
+    window.ko = {
+        observable: function (initial) {
+            var _val = initial;
+            var _subs = [];
+            var obs = function (newVal) {
+                if (arguments.length > 0) {
+                    _val = newVal;
+                    _subs.forEach(function (cb) { try { cb(newVal); } catch (e) {} });
+                }
+                return _val;
+            };
+            obs.subscribe = function (cb) {
+                _subs.push(cb);
+                return { dispose: function () { _subs = _subs.filter(function (s) { return s !== cb; }); } };
+            };
+            obs.peek = function () { return _val; };
+            return obs;
+        },
+        computed: function (fn) {
+            var obs = window.ko.observable(null);
+            try { obs(fn()); } catch (e) {}
+            return obs;
+        },
+        pureComputed: function (fn) { return window.ko.computed(fn); },
+        mapping: { fromJS: function (obj) { return obj; } },
+    };
+
+    // -------------------------------------------------------------------------
+    // 2. OCTOPRINT_VIEWMODELS registration shim
+    // -------------------------------------------------------------------------
+    window.OCTOPRINT_VIEWMODELS = [];
+
+    // -------------------------------------------------------------------------
+    // 3. Fake OctoPrint settings / printer profile / login viewmodels
+    // -------------------------------------------------------------------------
+    var fakeSettings = {
+        webcam: {
+            streamUrl: ko.observable(''),
+            flipH: ko.observable(false),
+            flipV: ko.observable(false),
+            rotate90: ko.observable(false),
+        },
+        plugins: {
+            prettygcode: {
+                darkMode: ko.observable(false),
+            },
+        },
+    };
+
+    // Bed sizes for common Bambu models (mm)
+    var BAMBU_BED_SIZES = {
+        'X1':       { width: 256, depth: 256, height: 256 },
+        'X1C':      { width: 256, depth: 256, height: 256 },
+        'X1E':      { width: 256, depth: 256, height: 256 },
+        'P1S':      { width: 256, depth: 256, height: 256 },
+        'P1P':      { width: 256, depth: 256, height: 256 },
+        'A1':       { width: 300, depth: 300, height: 300 },
+        'A1 Mini':  { width: 180, depth: 180, height: 180 },
+    };
+    var DEFAULT_BED = { width: 256, depth: 256, height: 256 };
+
+    var currentBed = Object.assign({}, DEFAULT_BED);
+
+    function makeFakeProfileData(bed) {
+        return {
+            volume: {
+                width:       ko.observable(bed.width),
+                depth:       ko.observable(bed.depth),
+                height:      ko.observable(bed.height),
+                origin:      ko.observable('lowerleft'),
+                formFactor:  ko.observable('rectangular'),
+                // Make custom_box a function so prettygcode.js uses width()/depth()/height()
+                custom_box:  function () { return false; },
+            },
+        };
+    }
+
+    var fakePrinterProfiles = {
+        currentProfileData: ko.observable(makeFakeProfileData(currentBed)),
+    };
+
+    var fakeLoginState = {
+        isUser:  ko.observable(true),
+        isAdmin: ko.observable(false),
+    };
+
+    var fakeControl = {};
+
+    // -------------------------------------------------------------------------
+    // 4. fetch() interceptor — rewrite OctoPrint paths to Bambuddy
+    // -------------------------------------------------------------------------
+    var _originalFetch = window.fetch.bind(window);
+    window.fetch = function (resource, init) {
+        var url = (typeof resource === 'string') ? resource
+                : (resource && resource.url) ? resource.url
+                : null;
+
+        if (url) {
+            // Normalize: strip scheme+host so regexes work on the path regardless
+            // of whether the browser resolved a relative URL to absolute.
+            var path = url.replace(/^https?:\/\/[^\/]+/, '');
+            // Also strip the viewer's own path prefix — the browser resolves relative URLs
+            // like 'downloads/files/local/...' to '/gcode-viewer/downloads/...' because
+            // the page is served from /gcode-viewer/. The regexes below expect bare paths.
+            path = path.replace(/^\/gcode-viewer(?=\/|$)/, '');
+
+            var newPath = path;
+
+            // OctoPrint file download  →  Bambuddy library download
+            newPath = newPath.replace(
+                /^\/?downloads\/files\/local\/__bambuddy_file_(\d+)$/,
+                API_BASE + '/library/files/$1/download'
+            );
+            // OctoPrint plugin static assets  →  gcode-viewer static files
+            newPath = newPath.replace(
+                /^\/?plugin\/prettygcode\/static\//,
+                VIEWER_BASE + '/'
+            );
+
+            if (newPath !== path) {
+                url = newPath;
+                resource = url; // always pass as string after rewriting
+            }
+
+            // Inject auth header for all Bambuddy API calls
+            if (url.startsWith(API_BASE)) {
+                var hdrs = authHeaders();
+                init = init || {};
+                init.headers = Object.assign({}, hdrs, init.headers || {});
+            }
+        }
+
+        var promise = _originalFetch(resource, init);
+
+        // Tee GCode downloads to build the layer map for sync + nozzle animation
+        if (url && url.match(/\/library\/files\/\d+\/download/)) {
+            promise = promise.then(function (response) {
+                var clone = response.clone();
+                clone.text().then(function (text) {
+                    gcodeLayerMap = parseGcodeLayerMap(text);
+                    lastFedLayer = -1;
+                    console.log('[PrettyGCode] Parsed ' + gcodeLayerMap.layerOffsets.length +
+                                ' layers for sync (' + Math.round(gcodeLayerMap.totalBytes / 1024) + ' KB)');
+                }).catch(function (e) {
+                    console.warn('[PrettyGCode] GCode layer parse failed:', e);
+                });
+                return response;
+            });
+        }
+
+        return promise;
+    };
+
+    // -------------------------------------------------------------------------
+    // 5. XHR interceptor — rewrite OctoPrint paths (used by THREE.OBJLoader etc.)
+    // -------------------------------------------------------------------------
+    var _origXHROpen = XMLHttpRequest.prototype.open;
+    XMLHttpRequest.prototype.open = function (method, url) {
+        if (typeof url === 'string') {
+            // Strip host if absolute, then rewrite OctoPrint static asset paths
+            var path = url.replace(/^https?:\/\/[^\/]+/, '');
+            path = path.replace(/^\/?plugin\/prettygcode\/static\//, VIEWER_BASE + '/');
+            url = path;
+        }
+        var args = Array.prototype.slice.call(arguments);
+        args[1] = url;
+        return _origXHROpen.apply(this, args);
+    };
+
+    // -------------------------------------------------------------------------
+    // 6. GCode layer parser
+    //
+    // Builds a layer map from the raw GCode text so we can:
+    //   a) Map layer_num → byte offset in file (drives prettygcode's filepos sync
+    //      and layer highlight, same as if OctoPrint were reporting filepos)
+    //   b) Extract a set of G0/G1 commands per layer to feed the PrintHeadSimulator
+    //      as synthetic "Send: G1 X... Y... Z..." entries, animating the nozzle model.
+    //
+    // Layer detection mirrors prettygcode.js: a new layer starts on the first extrusion
+    // at a Z position we haven't extruded at before.
+    // -------------------------------------------------------------------------
+    function parseGcodeLayerMap(text) {
+        var lines = text.split('\n');
+        var layerOffsets = [];  // layerOffsets[i] = byte pos in file where layer i starts
+        var layerCmds = [];     // layerCmds[i]    = array of ' G1 X... Y... Z...' strings
+        var byteOffset = 0;
+        var x = 0, y = 0, z = 0, e = 0;
+        var relative = false, relativeE = false;
+        var currentLayerZ = null;
+        var curCmds = [];
+
+        for (var i = 0; i < lines.length; i++) {
+            var raw = lines[i];
+            // +1 for the \n that was consumed by split
+            var lineBytes = raw.length + 1;
+
+            var cmd = raw.replace(/;.*$/, '').trim();
+            if (!cmd) { byteOffset += lineBytes; continue; }
+
+            var parts = cmd.split(/\s+/);
+            var g = parts[0].toUpperCase();
+
+            if (g === 'G90') { relative = false; relativeE = false; }
+            else if (g === 'G91') { relative = true; relativeE = true; }
+            else if (g === 'M82') { relativeE = false; }
+            else if (g === 'M83') { relativeE = true; }
+            else if (g === 'G92') {
+                // coordinate reset
+                for (var p = 1; p < parts.length; p++) {
+                    var k0 = parts[p][0].toUpperCase();
+                    var v0 = parseFloat(parts[p].slice(1));
+                    if (!isNaN(v0)) {
+                        if (k0 === 'X') x = v0;
+                        else if (k0 === 'Y') y = v0;
+                        else if (k0 === 'Z') z = v0;
+                        else if (k0 === 'E') e = v0;
+                    }
+                }
+            } else if (g === 'G0' || g === 'G1') {
+                var nx = x, ny = y, nz = z, ne = e;
+                var hasE = false;
+                for (var p = 1; p < parts.length; p++) {
+                    if (!parts[p]) continue;
+                    var k1 = parts[p][0].toUpperCase();
+                    var v1 = parseFloat(parts[p].slice(1));
+                    if (isNaN(v1)) continue;
+                    if (k1 === 'X') nx = relative ? x + v1 : v1;
+                    else if (k1 === 'Y') ny = relative ? y + v1 : v1;
+                    else if (k1 === 'Z') nz = relative ? z + v1 : v1;
+                    else if (k1 === 'E') { ne = relativeE ? e + v1 : v1; hasE = true; }
+                }
+
+                // New layer: first extrusion at a new Z (same logic as prettygcode.js)
+                if (hasE && ne > e && nz !== currentLayerZ) {
+                    currentLayerZ = nz;
+                    if (curCmds.length > 0) layerCmds.push(curCmds);
+                    else if (layerOffsets.length > 0) layerCmds.push([]); // gap layer
+                    curCmds = [];
+                    layerOffsets.push(byteOffset);
+                }
+
+                // Record movement commands for nozzle sim (keep arrays small — max 500/layer)
+                if ((hasE || nz !== z) && curCmds.length < 500) {
+                    curCmds.push(' G1 X' + nx.toFixed(3) +
+                                      ' Y' + ny.toFixed(3) +
+                                      ' Z' + nz.toFixed(3));
+                }
+
+                x = nx; y = ny; z = nz; e = ne;
+            }
+
+            byteOffset += lineBytes;
+        }
+        if (curCmds.length > 0) layerCmds.push(curCmds);
+
+        return {
+            layerOffsets: layerOffsets,
+            layerCmds:    layerCmds,
+            totalBytes:   byteOffset,
+        };
+    }
+
+    // -------------------------------------------------------------------------
+    // 8. State
+    // -------------------------------------------------------------------------
+    var viewModel = null;
+    var currentFileId = null;
+    var currentFilename = null;
+    var currentFileDate = 0; // stable epoch — only changes when a new file is loaded
+    var ws = null;
+    var wsReconnectTimer = null;
+    var printers = [];            // [{id, name, model, state, progress, subtask_name}]
+    var selectedPrinterId = null;
+    var gcodeLayerMap = null;     // parsed layer data: {layerOffsets, layerCmds, totalBytes}
+    var lastFedLayer = -1;        // last layer_num whose commands we fed to printHeadSim
+
+    // -------------------------------------------------------------------------
+    // 9. Bambuddy WebSocket
+    // -------------------------------------------------------------------------
+    function connectWebSocket() {
+        var token = localStorage.getItem('auth_token');
+        var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
+        // Do NOT put the token in the URL — it would appear in server logs.
+        // The WebSocket endpoint is currently unauthenticated server-side;
+        // all sensitive calls go through authenticated fetch() instead.
+        var wsUrl = proto + '//' + location.host + API_BASE + '/ws';
+
+        ws = new WebSocket(wsUrl);
+
+        ws.onopen = function () {
+            console.log('[PrettyGCode] Connected to Bambuddy WebSocket');
+        };
+
+        ws.onmessage = function (event) {
+            try {
+                var msg = JSON.parse(event.data);
+                if (msg.type === 'printer_status') {
+                    handlePrinterStatus(msg.printer_id, msg.data);
+                }
+            } catch (e) {}
+        };
+
+        ws.onclose = function () {
+            clearTimeout(wsReconnectTimer);
+            wsReconnectTimer = setTimeout(connectWebSocket, 3000);
+        };
+
+        ws.onerror = function () {
+            ws.close();
+        };
+    }
+
+    function bambuStateToOctoState(bambuState) {
+        var map = {
+            RUNNING:  'Printing',
+            PAUSE:    'Paused',
+            FAILED:   'Error',
+            FINISH:   'Operational',
+            IDLE:     'Operational',
+        };
+        return map[bambuState] || 'Operational';
+    }
+
+    function handlePrinterStatus(printerId, data) {
+        // Update printer list entry
+        var found = false;
+        for (var i = 0; i < printers.length; i++) {
+            if (printers[i].id === printerId) {
+                // Allowlist to prevent prototype pollution from crafted WS messages
+                var allowed2 = ['name', 'state', 'progress', 'layer_num', 'subtask_name', 'gcode_file', 'camera_url', 'model'];
+                allowed2.forEach(function (k) { if (k in data) printers[i][k] = data[k]; });
+                found = true;
+                break;
+            }
+        }
+        if (!found) {
+            // Only copy known, safe keys — avoids prototype pollution from a crafted WS message
+            var allowed = ['name', 'state', 'progress', 'layer_num', 'subtask_name', 'gcode_file', 'camera_url', 'model'];
+            var entry = { id: printerId };
+            allowed.forEach(function (k) { if (k in data) entry[k] = data[k]; });
+            printers.push(entry);
+        }
+
+        updatePrinterSelector();
+
+        // Only feed data for the selected printer
+        if (selectedPrinterId !== null && printerId !== selectedPrinterId) return;
+        if (selectedPrinterId === null && printers.length > 0) {
+            selectedPrinterId = printers[0].id;
+        }
+
+        if (!viewModel) return;
+
+        var printer = null;
+        for (var j = 0; j < printers.length; j++) {
+            if (printers[j].id === printerId) { printer = printers[j]; break; }
+        }
+        if (!printer) return;
+
+        // Update bed size from printer model
+        var bedKey = (printer.model || '').toUpperCase();
+        for (var modelName in BAMBU_BED_SIZES) {
+            if (bedKey.indexOf(modelName.toUpperCase()) !== -1) {
+                currentBed = BAMBU_BED_SIZES[modelName];
+                break;
+            }
+        }
+        // Replace the entire profile data so the subscribe() fires
+        fakePrinterProfiles.currentProfileData(makeFakeProfileData(currentBed));
+
+        // Auto-load currently printing file if it changed
+        var subtask = printer.subtask_name || printer.gcode_file || '';
+        if (subtask && subtask !== currentFilename) {
+            currentFilename = subtask;
+            tryAutoLoadPrintingFile(subtask);
+        }
+
+        // Update webcam URL
+        if (printer.camera_url) {
+            fakeSettings.webcam.streamUrl(printer.camera_url);
+        }
+
+        feedCurrentData(printer);
+    }
+
+    function feedCurrentData(printer) {
+        if (!viewModel || !viewModel.fromCurrentData) return;
+        var octoState = bambuStateToOctoState(printer.state || 'IDLE');
+        var isPrinting = octoState === 'Printing' || octoState === 'Paused';
+
+        // --- Layer sync via filepos -------------------------------------------
+        // prettygcode.js calls gcodeProxy.syncGcodeObjToFilePos(curPrintFilePos) each
+        // animation frame when printing + syncToProgress is on.  Pass the byte offset
+        // of the current layer so the highlight advances correctly.
+        var filepos = null;
+        var logs = [];
+
+        if (gcodeLayerMap && isPrinting) {
+            // Bambu layer_num is 1-based; our layerOffsets array is 0-based.
+            var layerIdx = Math.max(0, (printer.layer_num || 1) - 1);
+            layerIdx = Math.min(layerIdx, gcodeLayerMap.layerOffsets.length - 1);
+            filepos = gcodeLayerMap.layerOffsets[layerIdx] || 0;
+
+            // --- Nozzle animation via synthetic Send: commands -------------------
+            // PrintHeadSimulator.addCommand() expects "Send: G1 X... Y... Z..." entries.
+            // Feed the movement commands for the current layer once per layer change.
+            // The simulator interpolates them over real time, animating the nozzle model.
+            if (layerIdx !== lastFedLayer && gcodeLayerMap.layerCmds[layerIdx]) {
+                lastFedLayer = layerIdx;
+                var cmds = gcodeLayerMap.layerCmds[layerIdx];
+                // PrintHeadSimulator buffer is capped at 1000; feed at most 400 commands
+                // so there's room for the sim to drain before more arrive.
+                logs = cmds.slice(0, 400).map(function (c) { return 'Send:' + c; });
+            }
+        }
+
+        viewModel.fromCurrentData({
+            job: {
+                file: {
+                    path: currentFileId ? ('__bambuddy_file_' + currentFileId) : null,
+                    date: currentFileDate,
+                },
+                estimatedPrintTime: null,
+            },
+            state: {
+                text: octoState,
+                flags: { printing: octoState === 'Printing', paused: octoState === 'Paused' },
+            },
+            progress: {
+                filepos: filepos,
+                completion: (printer.progress || 0) / 100,
+                printTime: null,
+            },
+            currentZ: null,
+            logs: logs,
+        });
+    }
+
+    // -------------------------------------------------------------------------
+    // 8. Auto-load file when printer starts printing
+    // -------------------------------------------------------------------------
+    function tryAutoLoadPrintingFile(filename) {
+        // Search the library for a matching .gcode file
+        apiFetch('/library/files?sort_by=updated_at&sort_dir=desc', {})
+            .then(function (r) { return r.json(); })
+            .then(function (files) {
+                if (!Array.isArray(files)) return;
+                var match = files.find(function (f) {
+                    return f.filename === filename ||
+                           f.filename === filename + '.gcode' ||
+                           f.filename.replace(/\.gcode$/, '') === filename.replace(/\.gcode$/, '');
+                });
+                if (match) loadFileById(match.id, match.filename, match.file_size);
+            })
+            .catch(function () {});
+    }
+
+    // -------------------------------------------------------------------------
+    // 9. File loading
+    // -------------------------------------------------------------------------
+    function loadFileById(fileId, filename, fileSize) {
+        currentFileId = fileId;
+        currentFilename = filename;
+        currentFileDate = Date.now(); // new stable date so prettygcode loads exactly once
+        gcodeLayerMap = null;   // cleared here; re-populated when fetch() intercept fires
+        lastFedLayer = -1;
+        stopPlayback(true);
+        updateFilenameDisplay(filename);
+        // Enable play button once a file is loaded
+        var playBtn = document.getElementById('bb-play-btn');
+        if (playBtn) playBtn.disabled = false;
+        // Trigger prettygcode.js's updateJob — date must match currentFileDate exactly
+        // so subsequent feedCurrentData calls don't re-trigger the download
+        if (viewModel && viewModel.fromCurrentData) {
+            viewModel.fromCurrentData({
+                job: {
+                    file: {
+                        path: '__bambuddy_file_' + fileId,
+                        date: currentFileDate,
+                    },
+                    estimatedPrintTime: null,
+                },
+                state: { text: 'Operational', flags: { printing: false } },
+                progress: { filepos: null, completion: 0 },
+                currentZ: null,
+                logs: [],
+            });
+        }
+    }
+
+    function updateFilenameDisplay(filename) {
+        var el = document.getElementById('bb-current-file');
+        if (el) el.textContent = filename || '— no file loaded —';
+    }
+
+    // -------------------------------------------------------------------------
+    // 10. File picker
+    // -------------------------------------------------------------------------
+    function buildFilePicker() {
+        var container = document.getElementById('bb-file-picker');
+        if (!container) return;
+
+        var input = document.createElement('input');
+        input.type = 'text';
+        input.placeholder = 'Search .gcode files…';
+        input.className = 'bb-search';
+        input.style.cssText = 'width:100%;padding:4px 8px;background:#333;border:1px solid #555;color:#fff;border-radius:4px;margin-bottom:4px;box-sizing:border-box;';
+
+        var list = document.createElement('div');
+        list.style.cssText = 'max-height:180px;overflow-y:auto;';
+
+        container.appendChild(input);
+        container.appendChild(list);
+
+        var allFiles = [];
+
+        function render(files) {
+            list.innerHTML = '';
+            if (!files.length) {
+                list.innerHTML = '<div style="color:#888;padding:4px 6px;font-size:12px;">No .gcode files found in library</div>';
+                return;
+            }
+            files.forEach(function (f) {
+                var row = document.createElement('div');
+                row.textContent = f.filename;
+                row.title = f.filename;
+                row.style.cssText = 'padding:4px 6px;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-radius:3px;';
+                row.addEventListener('mouseenter', function () { row.style.background = '#444'; });
+                row.addEventListener('mouseleave', function () { row.style.background = ''; });
+                row.addEventListener('click', function () {
+                    loadFileById(f.id, f.filename, f.file_size);
+                    // Close picker
+                    container.classList.toggle('bb-open', false);
+                });
+                list.appendChild(row);
+            });
+        }
+
+        function loadFiles() {
+            list.innerHTML = '<div style="color:#aaa;padding:4px 6px;font-size:12px;">Loading files…</div>';
+            // include_root=false returns files from ALL folders, not just root level
+            apiFetch('/library/files?include_root=false', {})
+                .then(function (r) { return r.json(); })
+                .then(function (files) {
+                    if (!Array.isArray(files)) {
+                        list.innerHTML = '<div style="color:#f88;padding:4px 6px;font-size:12px;">Failed to load files</div>';
+                        return;
+                    }
+                    allFiles = files.filter(function (f) {
+                        return f.filename && f.filename.toLowerCase().endsWith('.gcode');
+                    });
+                    render(allFiles);
+                })
+                .catch(function () {
+                    list.innerHTML = '<div style="color:#f88;padding:4px 6px;font-size:12px;">Failed to load files — check auth token</div>';
+                });
+        }
+
+        input.addEventListener('input', function () {
+            var q = input.value.toLowerCase();
+            render(q ? allFiles.filter(function (f) { return f.filename.toLowerCase().indexOf(q) !== -1; }) : allFiles);
+        });
+
+        loadFiles();
+    }
+
+    // -------------------------------------------------------------------------
+    // 11. Printer selector
+    // -------------------------------------------------------------------------
+    function updatePrinterSelector() {
+        var sel = document.getElementById('bb-printer-select');
+        if (!sel) return;
+        var current = sel.value;
+        sel.innerHTML = '';
+        printers.forEach(function (p) {
+            var opt = document.createElement('option');
+            opt.value = p.id;
+            opt.textContent = (p.name || ('Printer ' + p.id)) + (p.state ? ' [' + p.state + ']' : '');
+            sel.appendChild(opt);
+        });
+        if (current) sel.value = current;
+        if (!sel.value && printers.length) {
+            sel.value = printers[0].id;
+            selectedPrinterId = printers[0].id;
+        }
+    }
+
+    // -------------------------------------------------------------------------
+    // 13. Initialise after DOM + scripts are ready
+    // -------------------------------------------------------------------------
+    function init() {
+        // Find the ViewModel registration that prettygcode.js pushed
+        var reg = null;
+        for (var i = 0; i < window.OCTOPRINT_VIEWMODELS.length; i++) {
+            if (window.OCTOPRINT_VIEWMODELS[i].construct) {
+                reg = window.OCTOPRINT_VIEWMODELS[i];
+                break;
+            }
+        }
+
+        if (!reg) {
+            console.error('[PrettyGCode] No ViewModel found in OCTOPRINT_VIEWMODELS');
+            return;
+        }
+
+        try {
+            viewModel = new reg.construct([
+                fakeSettings,
+                fakeLoginState,
+                fakePrinterProfiles,
+                fakeControl,
+            ]);
+        } catch (e) {
+            console.error('[PrettyGCode] ViewModel constructor failed:', e);
+            return;
+        }
+
+        if (viewModel.onAfterBinding) {
+            try { viewModel.onAfterBinding(); } catch (e) {}
+        }
+
+        // Trigger tab activation — this calls onTabChange which initialises the Three.js scene
+        if (viewModel.onTabChange) {
+            try { viewModel.onTabChange('#tab_plugin_prettygcode', ''); } catch (e) {
+                console.error('[PrettyGCode] onTabChange failed:', e);
+            }
+        }
+
+        connectWebSocket();
+
+        // Wire up printer selector
+        var sel = document.getElementById('bb-printer-select');
+        if (sel) {
+            sel.addEventListener('change', function () {
+                selectedPrinterId = parseInt(sel.value, 10) || null;
+            });
+        }
+
+        // Load initial printer list
+        apiFetch('/printers', {})
+            .then(function (r) { return r.json(); })
+            .then(function (list) {
+                if (!Array.isArray(list)) return;
+                list.forEach(function (p) {
+                    // Find existing entry (WS may have pushed one before API returned)
+                    var existing = null;
+                    for (var i = 0; i < printers.length; i++) {
+                        if (printers[i].id === p.id) { existing = printers[i]; break; }
+                    }
+                    if (existing) {
+                        // Fill in name/model that WS status messages don't carry
+                        if (p.name)  existing.name  = p.name;
+                        if (p.model) existing.model = p.model;
+                    } else {
+                        printers.push({ id: p.id, name: p.name, model: p.model, state: 'IDLE', progress: 0 });
+                    }
+                });
+                updatePrinterSelector();
+                // Try to get bed size from first printer model
+                if (list.length > 0 && list[0].model) {
+                    var m = list[0].model.toUpperCase();
+                    for (var modelName in BAMBU_BED_SIZES) {
+                        if (m.indexOf(modelName.toUpperCase()) !== -1) {
+                            currentBed = BAMBU_BED_SIZES[modelName];
+                            fakePrinterProfiles.currentProfileData(makeFakeProfileData(currentBed));
+                            break;
+                        }
+                    }
+                }
+                if (list.length > 0) selectedPrinterId = list[0].id;
+            })
+            .catch(function () {});
+
+        console.log('[PrettyGCode] Bambuddy adapter initialised');
+
+        // Wire up playback controls
+        var playBtn = document.getElementById('bb-play-btn');
+        var speedSel = document.getElementById('bb-play-speed');
+        if (playBtn) {
+            playBtn.addEventListener('click', function () {
+                if (isPlaying) stopPlayback();
+                else startPlayback();
+            });
+        }
+        if (speedSel) {
+            speedSel.addEventListener('change', function () {
+                layersPerTick = parseInt(speedSel.value, 10) || 1;
+                // Restart if already playing so speed takes effect immediately
+                if (isPlaying) { stopPlayback(); startPlayback(); }
+            });
+        }
+    }
+
+    // -------------------------------------------------------------------------
+    // 14. Playback engine
+    // -------------------------------------------------------------------------
+    var isPlaying = false;
+    var playInterval = null;
+    var layersPerTick = 1;   // layers advanced per 50 ms tick
+    var TICK_MS = 50;        // ~20 fps
+
+    function getSlider() { return $('#myslider-vertical'); }
+
+    function startPlayback() {
+        var $sl = getSlider();
+        if (!$sl.length) return;
+        var data = $sl.data('_pgslider');
+        if (!data) return;
+
+        var max = data.opts.max || 0;
+        if (max === 0) return;
+
+        // Restart from beginning if already at the end
+        var cur = data.opts.value || 0;
+        if (cur >= max) cur = 0;
+
+        // Suppress live-print sync while playing
+        var evStart = $.Event('slideStart'); evStart.value = cur; $sl.trigger(evStart);
+
+        _setSliderLayer($sl, cur);
+
+        isPlaying = true;
+        _updatePlayBtn();
+
+        playInterval = setInterval(function () {
+            var d = getSlider().data('_pgslider');
+            if (!d) { stopPlayback(); return; }
+            var next = (d.opts.value || 0) + layersPerTick;
+            if (next >= d.opts.max) {
+                next = d.opts.max;
+                _setSliderLayer(getSlider(), next);
+                stopPlayback(/* skipEvStop */ false);
+                return;
+            }
+            _setSliderLayer(getSlider(), next);
+        }, TICK_MS);
+    }
+
+    function stopPlayback(skipEvStop) {
+        if (playInterval) { clearInterval(playInterval); playInterval = null; }
+        isPlaying = false;
+        _updatePlayBtn();
+        if (!skipEvStop) {
+            var $sl = getSlider();
+            if ($sl.length) {
+                var d = $sl.data('_pgslider');
+                var evStop = $.Event('slideStop');
+                evStop.value = d ? d.opts.value : 0;
+                $sl.trigger(evStop);
+            }
+        }
+    }
+
+    function _setSliderLayer($sl, layer) {
+        $sl.slider('setValue', layer);
+        var ev = $.Event('slide'); ev.value = layer; $sl.trigger(ev);
+        $sl.find('.slider-handle').text(layer);
+    }
+
+    function _updatePlayBtn() {
+        var btn = document.getElementById('bb-play-btn');
+        if (btn) btn.textContent = isPlaying ? '⏸' : '▶';
+    }
+
+    // Run after all scripts have loaded.
+    // buildFilePicker() runs immediately at DOM-ready — independent of viewmodel
+    // init so the file picker is always functional even if prettygcode fails.
+    // init() (viewmodel + 3D canvas) runs 200 ms later to let prettygcode.js
+    // finish its own synchronous setup first.
+    function onDomReady() {
+        // Wire file-picker button — MUST be here (not an inline <script>) because
+        // the CSP on this page allows script-src 'self' but NOT 'unsafe-inline',
+        // so inline <script> blocks are blocked by the browser.
+        var fileBtn = document.getElementById('bb-file-btn');
+        var picker  = document.getElementById('bb-file-picker');
+        if (fileBtn && picker) {
+            fileBtn.addEventListener('click', function (e) {
+                picker.classList.toggle('bb-open');
+                e.stopPropagation();
+            });
+            // Clicking outside the picker closes it
+            document.addEventListener('click', function () {
+                picker.classList.remove('bb-open');
+            });
+            // Clicks inside the picker don't close it
+            picker.addEventListener('click', function (e) {
+                e.stopPropagation();
+            });
+        }
+
+        buildFilePicker();
+        setTimeout(init, 200);
+    }
+
+    if (document.readyState === 'loading') {
+        document.addEventListener('DOMContentLoaded', onDomReady);
+    } else {
+        onDomReady();
+    }
+
+    // -------------------------------------------------------------------------
+    // Public API
+    // -------------------------------------------------------------------------
+    window.BambuddyPrettyGCode = {
+        loadFile: loadFileById,
+        getViewModel: function () { return viewModel; },
+        play: startPlayback,
+        stop: stopPlayback,
+    };
+
+})();

+ 1070 - 0
gcode_viewer/js/camera-controls.js

@@ -0,0 +1,1070 @@
+/*!
+ * camera-controls
+ * https://github.com/yomotsu/camera-controls
+ * (c) 2017 @yomotsu
+ * Released under the MIT License.
+ */
+(function (global, factory) {
+	typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+	typeof define === 'function' && define.amd ? define(factory) :
+	(global = global || self, global.CameraControls = factory());
+}(this, function () { 'use strict';
+
+	function _classCallCheck(instance, Constructor) {
+	  if (!(instance instanceof Constructor)) {
+	    throw new TypeError("Cannot call a class as a function");
+	  }
+	}
+
+	function _defineProperties(target, props) {
+	  for (var i = 0; i < props.length; i++) {
+	    var descriptor = props[i];
+	    descriptor.enumerable = descriptor.enumerable || false;
+	    descriptor.configurable = true;
+	    if ("value" in descriptor) descriptor.writable = true;
+	    Object.defineProperty(target, descriptor.key, descriptor);
+	  }
+	}
+
+	function _createClass(Constructor, protoProps, staticProps) {
+	  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
+	  if (staticProps) _defineProperties(Constructor, staticProps);
+	  return Constructor;
+	}
+
+	function _inherits(subClass, superClass) {
+	  if (typeof superClass !== "function" && superClass !== null) {
+	    throw new TypeError("Super expression must either be null or a function");
+	  }
+
+	  subClass.prototype = Object.create(superClass && superClass.prototype, {
+	    constructor: {
+	      value: subClass,
+	      writable: true,
+	      configurable: true
+	    }
+	  });
+	  if (superClass) _setPrototypeOf(subClass, superClass);
+	}
+
+	function _getPrototypeOf(o) {
+	  _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
+	    return o.__proto__ || Object.getPrototypeOf(o);
+	  };
+	  return _getPrototypeOf(o);
+	}
+
+	function _setPrototypeOf(o, p) {
+	  _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
+	    o.__proto__ = p;
+	    return o;
+	  };
+
+	  return _setPrototypeOf(o, p);
+	}
+
+	function _assertThisInitialized(self) {
+	  if (self === void 0) {
+	    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+	  }
+
+	  return self;
+	}
+
+	function _possibleConstructorReturn(self, call) {
+	  if (call && (typeof call === "object" || typeof call === "function")) {
+	    return call;
+	  }
+
+	  return _assertThisInitialized(self);
+	}
+
+	// based on https://github.com/mrdoob/eventdispatcher.js/
+	var EventDispatcher =
+	/*#__PURE__*/
+	function () {
+	  function EventDispatcher() {
+	    _classCallCheck(this, EventDispatcher);
+
+	    this._listeners = {};
+	  }
+
+	  _createClass(EventDispatcher, [{
+	    key: "addEventListener",
+	    value: function addEventListener(type, listener) {
+	      var listeners = this._listeners;
+
+	      if (listeners[type] === undefined) {
+	        listeners[type] = [];
+	      }
+
+	      if (listeners[type].indexOf(listener) === -1) {
+	        listeners[type].push(listener);
+	      }
+	    }
+	  }, {
+	    key: "hasEventListener",
+	    value: function hasEventListener(type, listener) {
+	      var listeners = this._listeners;
+	      return listeners[type] !== undefined && listeners[type].indexOf(listener) !== -1;
+	    }
+	  }, {
+	    key: "removeEventListener",
+	    value: function removeEventListener(type, listener) {
+	      var listeners = this._listeners;
+	      var listenerArray = listeners[type];
+
+	      if (listenerArray !== undefined) {
+	        var index = listenerArray.indexOf(listener);
+
+	        if (index !== -1) {
+	          listenerArray.splice(index, 1);
+	        }
+	      }
+	    }
+	  }, {
+	    key: "dispatchEvent",
+	    value: function dispatchEvent(event) {
+	      var listeners = this._listeners;
+	      var listenerArray = listeners[event.type];
+
+	      if (listenerArray !== undefined) {
+	        event.target = this;
+	        var array = listenerArray.slice(0);
+
+	        for (var i = 0, l = array.length; i < l; i++) {
+	          array[i].call(this, event);
+	        }
+	      }
+	    }
+	  }]);
+
+	  return EventDispatcher;
+	}();
+
+	var THREE;
+
+	var _AXIS_Y;
+
+	var _v2;
+
+	var _v3A;
+
+	var _v3B;
+
+	var _v3C;
+
+	var _v4;
+
+	var _xColumn;
+
+	var _yColumn;
+
+	var _sphericalA;
+
+	var _sphericalB;
+
+	var EPSILON = 0.001;
+	var PI_2 = Math.PI * 2;
+	var STATE = {
+	  NONE: -1,
+	  ROTATE: 0,
+	  DOLLY: 1,
+	  TRUCK: 2,
+	  TOUCH_ROTATE: 3,
+	  TOUCH_DOLLY_TRUCK: 4,
+	  TOUCH_TRUCK: 5
+	};
+
+	var CameraControls =
+	/*#__PURE__*/
+	function (_EventDispatcher) {
+	  _inherits(CameraControls, _EventDispatcher);
+
+	  _createClass(CameraControls, null, [{
+	    key: "install",
+	    value: function install(libs) {
+	      THREE = libs.THREE;
+	      _AXIS_Y = new THREE.Vector3(0, 1, 0);
+	      _v2 = new THREE.Vector2();
+	      _v3A = new THREE.Vector3();
+	      _v3B = new THREE.Vector3();
+	      _v3C = new THREE.Vector3();
+	      _v4 = new THREE.Vector4();
+	      _xColumn = new THREE.Vector3();
+	      _yColumn = new THREE.Vector3();
+	      _sphericalA = new THREE.Spherical();
+	      _sphericalB = new THREE.Spherical();
+	    }
+	  }]);
+
+	  function CameraControls(camera, domElement) {
+	    var _this;
+
+	    var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+
+	    _classCallCheck(this, CameraControls);
+
+	    _this = _possibleConstructorReturn(this, _getPrototypeOf(CameraControls).call(this));
+	    _this._camera = camera;
+	    _this._yAxisUpSpace = new THREE.Quaternion().setFromUnitVectors(_this._camera.up, _AXIS_Y);
+	    _this._yAxisUpSpaceInverse = _this._yAxisUpSpace.clone().inverse();
+	    _this._state = STATE.NONE;
+	    _this.enabled = true;
+
+	    if (_this._camera.isPerspectiveCamera) {
+	      // How far you can dolly in and out ( PerspectiveCamera only )
+	      _this.minDistance = 0;
+	      _this.maxDistance = Infinity;
+	    } else if (_this._camera.isOrthographicCamera) {
+	      // How far you can zoom in and out ( OrthographicCamera only )
+	      _this.minZoom = 0;
+	      _this.maxZoom = Infinity;
+	    }
+
+	    _this.minPolarAngle = 0; // radians
+
+	    _this.maxPolarAngle = Math.PI/2; // radians /2 keeps it from going below horizion. 
+
+	    _this.minAzimuthAngle = -Infinity; // radians
+
+	    _this.maxAzimuthAngle = Infinity; // radians
+	    // Target cannot move outside of this box
+
+	    _this._boundary = new THREE.Box3(new THREE.Vector3(-Infinity, -Infinity, -Infinity), new THREE.Vector3(Infinity, Infinity, Infinity));
+	    _this.boundaryFriction = 0.0;
+	    _this._boundaryEnclosesCamera = false;
+	    _this.dampingFactor = 0.05;
+	    _this.draggingDampingFactor = 0.25;
+	    _this.azimuthRotateSpeed = 1.0;
+	    _this.polarRotateSpeed = 1.0;
+	    _this.dollySpeed = 1.0;
+	    _this.truckSpeed = 2.0;
+	    _this.dollyToCursor = false;
+	    _this.verticalDragToForward = false;
+	    _this._domElement = domElement;
+	    _this._viewport = null; // the location of focus, where the object orbits around
+
+	    _this._target = new THREE.Vector3();
+	    _this._targetEnd = new THREE.Vector3(); // rotation
+
+	    _this._spherical = new THREE.Spherical().setFromVector3(_this._camera.position.clone().applyQuaternion(_this._yAxisUpSpace));
+	    _this._sphericalEnd = _this._spherical.clone(); // reset
+
+	    _this._target0 = _this._target.clone();
+	    _this._position0 = _this._camera.position.clone();
+	    _this._zoom0 = _this._camera.zoom;
+	    _this._dollyControlAmount = 0;
+	    _this._dollyControlCoord = new THREE.Vector2();
+	    _this._hasUpdated = true;
+
+	    _this.update(0);
+
+	    if (!_this._domElement || options.ignoreDOMEventListeners) {
+	      _this._removeAllEventListeners = function () {};
+	    } else {
+	      var extractClientCoordFromEvent = function extractClientCoordFromEvent(event, out) {
+	        out.set(0, 0);
+
+	        if (isTouchEvent(event)) {
+	          for (var i = 0; i < event.touches.length; i++) {
+	            out.x += event.touches[i].clientX;
+	            out.y += event.touches[i].clientY;
+	          }
+
+	          out.x /= event.touches.length;
+	          out.y /= event.touches.length;
+	          return out;
+	        } else {
+	          out.set(event.clientX, event.clientY);
+	          return out;
+	        }
+	      };
+
+	      var onMouseDown = function onMouseDown(event) {
+	        if (!scope.enabled) return;
+	        event.preventDefault();
+	        var prevState = scope._state;
+
+              //changed to match CURA
+	        switch (event.button) {
+	          case THREE.MOUSE.LEFT:
+                scope._state = STATE.TRUCK;
+	            break;
+
+	          case THREE.MOUSE.MIDDLE:
+                    scope._state = STATE.TRUCK;
+	            //scope._state = STATE.DOLLY;
+	            break;
+
+	          case THREE.MOUSE.RIGHT:
+                scope._state = STATE.ROTATE;
+	            break;
+	        }
+
+	        if (prevState !== scope._state) {
+	          startDragging(event);
+	        }
+	      };
+
+	      var onTouchStart = function onTouchStart(event) {
+	        if (!scope.enabled) return;
+	        event.preventDefault();
+	        var prevState = scope._state;
+
+	        switch (event.touches.length) {
+	          case 1:
+	            // one-fingered touch: rotate
+	            scope._state = STATE.TOUCH_ROTATE;
+	            break;
+
+	          case 2:
+	            // two-fingered touch: dolly
+	            scope._state = STATE.TOUCH_DOLLY_TRUCK;
+	            break;
+
+	          case 3:
+	            // three-fingered touch: truck
+	            scope._state = STATE.TOUCH_TRUCK;
+	            break;
+	        }
+
+	        if (prevState !== scope._state) {
+	          startDragging(event);
+	        }
+	      };
+
+	      var onMouseWheel = function onMouseWheel(event) {
+	        if (!scope.enabled) return;
+	        event.preventDefault(); // Ref: https://github.com/cedricpinson/osgjs/blob/00e5a7e9d9206c06fdde0436e1d62ab7cb5ce853/sources/osgViewer/input/source/InputSourceMouse.js#L89-L103
+
+	        var mouseDeltaFactor = 120;
+	        var deltaYFactor = navigator.platform.indexOf('Mac') === 0 ? -1 : -3;
+	        var delta;
+
+	        if (event.wheelDelta !== undefined) {
+	          delta = event.wheelDelta / mouseDeltaFactor;
+	        } else if (event.deltaMode === 1) {
+	          delta = event.deltaY / deltaYFactor;
+	        } else {
+	          delta = event.deltaY / (10 * deltaYFactor);
+	        }
+
+	        var x, y;
+
+	        if (scope.dollyToCursor) {
+	          elementRect = scope._getClientRect(_v4);
+	          x = (event.clientX - elementRect.x) / elementRect.z * 2 - 1;
+	          y = (event.clientY - elementRect.y) / elementRect.w * -2 + 1;
+	        }
+
+	        dollyInternal(-delta, x, y);
+	      };
+
+	      var onContextMenu = function onContextMenu(event) {
+	        if (!scope.enabled) return;
+	        event.preventDefault();
+	      };
+
+	      var startDragging = function startDragging(event) {
+	        if (!scope.enabled) return;
+	        event.preventDefault();
+	        extractClientCoordFromEvent(event, _v2);
+	        elementRect = scope._getClientRect(_v4);
+	        dragStart.copy(_v2);
+
+	        if (scope._state === STATE.TOUCH_DOLLY_TRUCK) {
+	          // 2 finger pinch
+	          var dx = _v2.x - event.touches[1].clientX;
+	          var dy = _v2.y - event.touches[1].clientY;
+	          var distance = Math.sqrt(dx * dx + dy * dy);
+	          dollyStart.set(0, distance); // center coords of 2 finger truck
+
+	          var x = (event.touches[0].clientX + event.touches[1].clientX) * 0.5;
+	          var y = (event.touches[0].clientY + event.touches[1].clientY) * 0.5;
+	          dragStart.set(x, y);
+	        }
+
+	        document.addEventListener('mousemove', dragging, {
+	          passive: false
+	        });
+	        document.addEventListener('touchmove', dragging, {
+	          passive: false
+	        });
+	        document.addEventListener('mouseup', endDragging);
+	        document.addEventListener('touchend', endDragging);
+	        scope.dispatchEvent({
+	          type: 'controlstart',
+	          originalEvent: event
+	        });
+	      };
+
+	      var dragging = function dragging(event) {
+	        if (!scope.enabled) return;
+	        event.preventDefault();
+	        extractClientCoordFromEvent(event, _v2);
+	        var deltaX = dragStart.x - _v2.x;
+	        var deltaY = dragStart.y - _v2.y;
+	        dragStart.copy(_v2);
+
+	        switch (scope._state) {
+	          case STATE.ROTATE:
+	          case STATE.TOUCH_ROTATE:
+	            var theta = PI_2 * scope.azimuthRotateSpeed * deltaX / elementRect.z;
+	            var phi = PI_2 * scope.polarRotateSpeed * deltaY / elementRect.w;
+	            scope.rotate(theta, phi, true);
+	            break;
+
+	          case STATE.DOLLY:
+	            // not implemented
+	            break;
+
+	          case STATE.TOUCH_DOLLY_TRUCK:
+	            var dx = _v2.x - event.touches[1].clientX;
+	            var dy = _v2.y - event.touches[1].clientY;
+	            var distance = Math.sqrt(dx * dx + dy * dy);
+	            var dollyDelta = dollyStart.y - distance;
+	            var touchDollyFactor = 8;
+	            var dollyX = scope.dollyToCursor ? (dragStart.x - elementRect.x) / elementRect.z * 2 - 1 : 0;
+	            var dollyY = scope.dollyToCursor ? (dragStart.y - elementRect.y) / elementRect.w * -2 + 1 : 0;
+	            dollyInternal(dollyDelta / touchDollyFactor, dollyX, dollyY);
+	            dollyStart.set(0, distance);
+	            truckInternal(deltaX, deltaY);
+	            break;
+
+	          case STATE.TRUCK:
+	          case STATE.TOUCH_TRUCK:
+	            truckInternal(deltaX, deltaY);
+	            break;
+	        }
+
+	        scope.dispatchEvent({
+	          type: 'control',
+	          originalEvent: event
+	        });
+	      };
+
+	      var endDragging = function endDragging(event) {
+	        if (!scope.enabled) return;
+	        scope._state = STATE.NONE;
+	        document.removeEventListener('mousemove', dragging, {
+	          passive: false
+	        });
+	        document.removeEventListener('touchmove', dragging, {
+	          passive: false
+	        });
+	        document.removeEventListener('mouseup', endDragging);
+	        document.removeEventListener('touchend', endDragging);
+	        scope.dispatchEvent({
+	          type: 'controlend',
+	          originalEvent: event
+	        });
+	      };
+
+	      var truckInternal = function truckInternal(deltaX, deltaY) {
+	        if (scope._camera.isPerspectiveCamera) {
+	          var offset = _v3A.copy(scope._camera.position).sub(scope._target); // half of the fov is center to top of screen
+
+
+	          var fovInRad = scope._camera.fov * THREE.Math.DEG2RAD;
+	          var targetDistance = offset.length() * Math.tan(fovInRad / 2);
+	          var truckX = scope.truckSpeed * deltaX * targetDistance / elementRect.w;
+	          var pedestalY = scope.truckSpeed * deltaY * targetDistance / elementRect.w;
+
+	          if (scope.verticalDragToForward) {
+	            scope.truck(truckX, 0, true);
+	            scope.forward(-pedestalY, true);
+	          } else {
+	            scope.truck(truckX, pedestalY, true);
+	          }
+	        } else if (scope._camera.isOrthographicCamera) {
+	          // orthographic
+	          var _truckX = deltaX * (scope._camera.right - scope._camera.left) / scope._camera.zoom / elementRect.z;
+
+	          var _pedestalY = deltaY * (scope._camera.top - scope._camera.bottom) / scope._camera.zoom / elementRect.w;
+
+	          scope.truck(_truckX, _pedestalY, true);
+	        }
+	      };
+
+	      var dollyInternal = function dollyInternal(delta, x, y) {
+	        var dollyScale = Math.pow(0.95, -delta * scope.dollySpeed);
+
+	        if (scope._camera.isPerspectiveCamera) {
+	          var distance = scope._sphericalEnd.radius * dollyScale - scope._sphericalEnd.radius;
+	          var prevRadius = scope._sphericalEnd.radius;
+	          scope.dolly(distance);
+
+	          if (scope.dollyToCursor) {
+	            scope._dollyControlAmount += scope._sphericalEnd.radius - prevRadius;
+
+	            scope._dollyControlCoord.set(x, y);
+	          }
+	        } else if (scope._camera.isOrthographicCamera) {
+	          scope._camera.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope._camera.zoom * dollyScale));
+
+	          scope._camera.updateProjectionMatrix();
+
+	          scope._hasUpdated = true;
+	        }
+	      };
+
+	      var scope = _assertThisInitialized(_this);
+
+	      var dragStart = new THREE.Vector2();
+	      var dollyStart = new THREE.Vector2();
+	      var elementRect;
+
+	      _this._domElement.addEventListener('mousedown', onMouseDown);
+
+	      _this._domElement.addEventListener('touchstart', onTouchStart);
+
+	      _this._domElement.addEventListener('wheel', onMouseWheel);
+
+	      _this._domElement.addEventListener('contextmenu', onContextMenu);
+
+	      _this._removeAllEventListeners = function () {
+	        scope._domElement.removeEventListener('mousedown', onMouseDown);
+
+	        scope._domElement.removeEventListener('touchstart', onTouchStart);
+
+	        scope._domElement.removeEventListener('wheel', onMouseWheel);
+
+	        scope._domElement.removeEventListener('contextmenu', onContextMenu);
+
+	        document.removeEventListener('mousemove', dragging);
+	        document.removeEventListener('touchmove', dragging);
+	        document.removeEventListener('mouseup', endDragging);
+	        document.removeEventListener('touchend', endDragging);
+	      };
+	    }
+
+	    return _this;
+	  } // wrong. phi should be map to polar, but backward compatibility.
+
+
+	  _createClass(CameraControls, [{
+	    key: "rotate",
+	    // azimuthAngle in radian
+	    // polarAngle in radian
+	    value: function rotate(azimuthAngle, polarAngle, enableTransition) {
+	      this.rotateTo(this._sphericalEnd.theta + azimuthAngle, this._sphericalEnd.phi + polarAngle, enableTransition);
+	    } // azimuthAngle in radian
+	    // polarAngle in radian
+
+	  }, {
+	    key: "rotateTo",
+	    value: function rotateTo(azimuthAngle, polarAngle, enableTransition) {
+	      var theta = Math.max(this.minAzimuthAngle, Math.min(this.maxAzimuthAngle, azimuthAngle));
+	      var phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, polarAngle));
+	      this._sphericalEnd.theta = theta;
+	      this._sphericalEnd.phi = phi;
+
+	      this._sphericalEnd.makeSafe();
+
+	      if (!enableTransition) {
+	        this._spherical.theta = this._sphericalEnd.theta;
+	        this._spherical.phi = this._sphericalEnd.phi;
+	      }
+
+	      this._hasUpdated = true;
+	    }
+	  }, {
+	    key: "dolly",
+	    value: function dolly(distance, enableTransition) {
+	      if (this._camera.isOrthographicCamera) {
+	        console.warn('dolly is not available for OrthographicCamera');
+	        return;
+	      }
+
+	      this.dollyTo(this._sphericalEnd.radius + distance, enableTransition);
+	    }
+	  }, {
+	    key: "dollyTo",
+	    value: function dollyTo(distance, enableTransition) {
+	      if (this._camera.isOrthographicCamera) {
+	        console.warn('dolly is not available for OrthographicCamera');
+	        return;
+	      }
+
+	      this._sphericalEnd.radius = THREE.Math.clamp(distance, this.minDistance, this.maxDistance);
+
+	      if (!enableTransition) {
+	        this._spherical.radius = this._sphericalEnd.radius;
+	      }
+
+	      this._hasUpdated = true;
+	    }
+	  }, {
+	    key: "pan",
+	    value: function pan(x, y, enableTransition) {
+	      console.log('`pan` has been renamed to `truck`');
+	      this.truck(x, y, enableTransition);
+	    }
+	  }, {
+	    key: "truck",
+	    value: function truck(x, y, enableTransition) {
+	      this._camera.updateMatrix();
+
+	      _xColumn.setFromMatrixColumn(this._camera.matrix, 0);
+
+	      _yColumn.setFromMatrixColumn(this._camera.matrix, 1);
+
+	      _xColumn.multiplyScalar(x);
+
+	      _yColumn.multiplyScalar(-y);
+
+	      var offset = _v3A.copy(_xColumn).add(_yColumn);
+
+	      this._encloseToBoundary(this._targetEnd, offset, this.boundaryFriction);
+
+	      if (!enableTransition) {
+	        this._target.copy(this._targetEnd);
+	      }
+
+	      this._hasUpdated = true;
+	    }
+	  }, {
+	    key: "forward",
+	    value: function forward(distance, enableTransition) {
+	      _v3A.setFromMatrixColumn(this._camera.matrix, 0);
+
+	      _v3A.crossVectors(this._camera.up, _v3A);
+
+	      _v3A.multiplyScalar(distance);
+
+	      this._encloseToBoundary(this._targetEnd, _v3A, this.boundaryFriction);
+
+	      if (!enableTransition) {
+	        this._target.copy(this._targetEnd);
+	      }
+
+	      this._hasUpdated = true;
+	    }
+	  }, {
+	    key: "moveTo",
+	    value: function moveTo(x, y, z, enableTransition) {
+	      this._targetEnd.set(x, y, z);
+
+	      if (!enableTransition) {
+	        this._target.copy(this._targetEnd);
+	      }
+
+	      this._hasUpdated = true;
+	    }
+	  }, {
+	    key: "fitTo",
+	    value: function fitTo(box3OrObject, enableTransition) {
+	      var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+
+	      if (this._camera.isOrthographicCamera) {
+	        console.warn('fitTo is not supported for OrthographicCamera');
+	        return;
+	      }
+
+	      var paddingLeft = options.paddingLeft || 0;
+	      var paddingRight = options.paddingRight || 0;
+	      var paddingBottom = options.paddingBottom || 0;
+	      var paddingTop = options.paddingTop || 0;
+	      var boundingBox = box3OrObject.isBox3 ? box3OrObject.clone() : new THREE.Box3().setFromObject(box3OrObject);
+	      var size = boundingBox.getSize(_v3A);
+	      var boundingWidth = size.x + paddingLeft + paddingRight;
+	      var boundingHeight = size.y + paddingTop + paddingBottom;
+	      var boundingDepth = size.z;
+	      var distance = this.getDistanceToFit(boundingWidth, boundingHeight, boundingDepth);
+	      this.dollyTo(distance, enableTransition);
+	      var boundingBoxCenter = boundingBox.getCenter(_v3A);
+	      var cx = boundingBoxCenter.x - (paddingLeft * 0.5 - paddingRight * 0.5);
+	      var cy = boundingBoxCenter.y + (paddingTop * 0.5 - paddingBottom * 0.5);
+	      var cz = boundingBoxCenter.z;
+	      this.moveTo(cx, cy, cz, enableTransition);
+
+	      this._sanitizeSphericals();
+
+	      this.rotateTo(0, 90 * THREE.Math.DEG2RAD, enableTransition);
+	    }
+	  }, {
+	    key: "setLookAt",
+	    value: function setLookAt(positionX, positionY, positionZ, targetX, targetY, targetZ, enableTransition) {
+	      var position = _v3A.set(positionX, positionY, positionZ);
+
+	      var target = _v3B.set(targetX, targetY, targetZ);
+
+	      this._targetEnd.copy(target);
+
+	      this._sphericalEnd.setFromVector3(position.sub(target).applyQuaternion(this._yAxisUpSpace));
+
+	      this._sanitizeSphericals();
+
+	      if (!enableTransition) {
+	        this._target.copy(this._targetEnd);
+
+	        this._spherical.copy(this._sphericalEnd);
+	      }
+
+	      this._hasUpdated = true;
+	    }
+	  }, {
+	    key: "lerpLookAt",
+	    value: function lerpLookAt(positionAX, positionAY, positionAZ, targetAX, targetAY, targetAZ, positionBX, positionBY, positionBZ, targetBX, targetBY, targetBZ, t, enableTransition) {
+	      var positionA = _v3A.set(positionAX, positionAY, positionAZ);
+
+	      var targetA = _v3B.set(targetAX, targetAY, targetAZ);
+
+	      _sphericalA.setFromVector3(positionA.sub(targetA).applyQuaternion(this._yAxisUpSpace));
+
+	      var targetB = _v3A.set(targetBX, targetBY, targetBZ);
+
+	      this._targetEnd.copy(targetA).lerp(targetB, t); // tricky
+
+
+	      var positionB = _v3B.set(positionBX, positionBY, positionBZ);
+
+	      _sphericalB.setFromVector3(positionB.sub(targetB).applyQuaternion(this._yAxisUpSpace));
+
+	      var deltaTheta = _sphericalB.theta - _sphericalA.theta;
+	      var deltaPhi = _sphericalB.phi - _sphericalA.phi;
+	      var deltaRadius = _sphericalB.radius - _sphericalA.radius;
+
+	      this._sphericalEnd.set(_sphericalA.radius + deltaRadius * t, _sphericalA.phi + deltaPhi * t, _sphericalA.theta + deltaTheta * t);
+
+	      this._sanitizeSphericals();
+
+	      if (!enableTransition) {
+	        this._target.copy(this._targetEnd);
+
+	        this._spherical.copy(this._sphericalEnd);
+	      }
+
+	      this._hasUpdated = true;
+	    }
+	  }, {
+	    key: "setPosition",
+	    value: function setPosition(positionX, positionY, positionZ, enableTransition) {
+	      this.setLookAt(positionX, positionY, positionZ, this._targetEnd.x, this._targetEnd.y, this._targetEnd.z, enableTransition);
+	    }
+	  }, {
+	    key: "setTarget",
+	    value: function setTarget(targetX, targetY, targetZ, enableTransition) {
+	      var pos = this.getPosition(_v3A);
+	      this.setLookAt(pos.x, pos.y, pos.z, targetX, targetY, targetZ, enableTransition);
+	    }
+	  }, {
+	    key: "setBoundary",
+	    value: function setBoundary(box3) {
+	      if (!box3) {
+	        this._boundary.min.set(-Infinity, -Infinity, -Infinity);
+
+	        this._boundary.max.set(Infinity, Infinity, Infinity);
+
+	        this._hasUpdated = true;
+	        return;
+	      }
+
+	      this._boundary.copy(box3);
+
+	      this._boundary.clampPoint(this._targetEnd, this._targetEnd);
+
+	      this._hasUpdated = true;
+	    }
+	  }, {
+	    key: "setViewport",
+	    value: function setViewport(viewportOrX, y, width, height) {
+	      if (viewportOrX === null) {
+	        // null
+	        this._viewport = null;
+	        return;
+	      }
+
+	      this._viewport = this._viewport || new THREE.Vector4();
+
+	      if (typeof viewportOrX === 'number') {
+	        // number
+	        this._viewport.set(viewportOrX, y, width, height);
+	      } else {
+	        // Vector4
+	        this._viewport.copy(viewportOrX);
+	      }
+	    }
+	  }, {
+	    key: "getDistanceToFit",
+	    value: function getDistanceToFit(width, height, depth) {
+	      var camera = this._camera;
+	      var boundingRectAspect = width / height;
+	      var fov = camera.fov * THREE.Math.DEG2RAD;
+	      var aspect = camera.aspect;
+	      var heightToFit = boundingRectAspect < aspect ? height : width / aspect;
+	      return heightToFit * 0.5 / Math.tan(fov * 0.5) + depth * 0.5;
+	    }
+	  }, {
+	    key: "getTarget",
+	    value: function getTarget(out) {
+	      var _out = !!out && out.isVector3 ? out : new THREE.Vector3();
+
+	      return _out.copy(this._targetEnd);
+	    }
+	  }, {
+	    key: "getPosition",
+	    value: function getPosition(out) {
+	      var _out = !!out && out.isVector3 ? out : new THREE.Vector3();
+
+	      return _out.setFromSpherical(this._sphericalEnd).applyQuaternion(this._yAxisUpSpaceInverse).add(this._targetEnd);
+	    }
+	  }, {
+	    key: "reset",
+	    value: function reset(enableTransition) {
+	      this.setLookAt(this._position0.x, this._position0.y, this._position0.z, this._target0.x, this._target0.y, this._target0.z, enableTransition);
+	    }
+	  }, {
+	    key: "saveState",
+	    value: function saveState() {
+	      this._target0.copy(this._target);
+
+	      this._position0.copy(this._camera.position);
+
+	      this._zoom0 = this._camera.zoom;
+	    }
+	  }, {
+	    key: "updateCameraUp",
+	    value: function updateCameraUp() {
+	      this._yAxisUpSpace.setFromUnitVectors(this._camera.up, _AXIS_Y);
+
+	      this._yAxisUpSpaceInverse.copy(this._yAxisUpSpace).inverse();
+	    }
+	  }, {
+	    key: "update",
+	    value: function update(delta) {
+	      var currentDampingFactor = this._state === STATE.NONE ? this.dampingFactor : this.draggingDampingFactor;
+	      var lerpRatio = 1.0 - Math.exp(-currentDampingFactor * delta / 0.016);
+	      var deltaTheta = this._sphericalEnd.theta - this._spherical.theta;
+	      var deltaPhi = this._sphericalEnd.phi - this._spherical.phi;
+	      var deltaRadius = this._sphericalEnd.radius - this._spherical.radius;
+
+	      var deltaTarget = _v3A.subVectors(this._targetEnd, this._target);
+
+	      if (Math.abs(deltaTheta) > EPSILON || Math.abs(deltaPhi) > EPSILON || Math.abs(deltaRadius) > EPSILON || Math.abs(deltaTarget.x) > EPSILON || Math.abs(deltaTarget.y) > EPSILON || Math.abs(deltaTarget.z) > EPSILON) {
+	        this._spherical.set(this._spherical.radius + deltaRadius * lerpRatio, this._spherical.phi + deltaPhi * lerpRatio, this._spherical.theta + deltaTheta * lerpRatio);
+
+	        this._target.add(deltaTarget.multiplyScalar(lerpRatio));
+
+	        this._hasUpdated = true;
+	      } else {
+	        this._spherical.copy(this._sphericalEnd);
+
+	        this._target.copy(this._targetEnd);
+	      }
+
+	      if (this._dollyControlAmount !== 0) {
+	        if (this._camera.isPerspectiveCamera) {
+	          var direction = _v3A.copy(_v3A.setFromSpherical(this._sphericalEnd).applyQuaternion(this._yAxisUpSpaceInverse)).normalize().negate();
+
+	          var planeX = _v3B.copy(direction).cross(_v3C.copy(this._camera.up)).normalize();
+
+	          if (planeX.lengthSq() === 0) planeX.x = 1.0;
+
+	          var planeY = _v3C.crossVectors(planeX, direction);
+
+	          var worldToScreen = this._sphericalEnd.radius * Math.tan(this._camera.fov * THREE.Math.DEG2RAD * 0.5);
+	          var prevRadius = this._sphericalEnd.radius - this._dollyControlAmount;
+
+	          var _lerpRatio = (prevRadius - this._sphericalEnd.radius) / this._sphericalEnd.radius;
+
+	          var cursor = _v3A.copy(this._targetEnd).add(planeX.multiplyScalar(this._dollyControlCoord.x * worldToScreen * this._camera.aspect)).add(planeY.multiplyScalar(this._dollyControlCoord.y * worldToScreen));
+
+	          this._targetEnd.lerp(cursor, _lerpRatio);
+
+	          this._target.copy(this._targetEnd);
+	        }
+
+	        this._dollyControlAmount = 0;
+	      }
+
+	      this._spherical.makeSafe();
+
+	      this._camera.position.setFromSpherical(this._spherical).applyQuaternion(this._yAxisUpSpaceInverse).add(this._target);
+
+	      this._camera.lookAt(this._target);
+
+	      if (this._boundaryEnclosesCamera) {
+	        this._encloseToBoundary(this._camera.position.copy(this._target), _v3A.setFromSpherical(this._spherical).applyQuaternion(this._yAxisUpSpaceInverse), 1.0);
+	      }
+
+	      var updated = this._hasUpdated;
+	      this._hasUpdated = false;
+	      if (updated) this.dispatchEvent({
+	        type: 'update'
+	      });
+	      return updated;
+	    }
+	  }, {
+	    key: "toJSON",
+	    value: function toJSON() {
+	      return JSON.stringify({
+	        enabled: this.enabled,
+	        minDistance: this.minDistance,
+	        maxDistance: infinityToMaxNumber(this.maxDistance),
+	        minPolarAngle: this.minPolarAngle,
+	        maxPolarAngle: infinityToMaxNumber(this.maxPolarAngle),
+	        minAzimuthAngle: infinityToMaxNumber(this.minAzimuthAngle),
+	        maxAzimuthAngle: infinityToMaxNumber(this.maxAzimuthAngle),
+	        dampingFactor: this.dampingFactor,
+	        draggingDampingFactor: this.draggingDampingFactor,
+	        dollySpeed: this.dollySpeed,
+	        truckSpeed: this.truckSpeed,
+	        dollyToCursor: this.dollyToCursor,
+	        verticalDragToForward: this.verticalDragToForward,
+	        target: this._targetEnd.toArray(),
+	        position: this._camera.position.toArray(),
+	        target0: this._target0.toArray(),
+	        position0: this._position0.toArray()
+	      });
+	    }
+	  }, {
+	    key: "fromJSON",
+	    value: function fromJSON(json, enableTransition) {
+	      var obj = JSON.parse(json);
+	      var position = new THREE.Vector3().fromArray(obj.position);
+	      this.enabled = obj.enabled;
+	      this.minDistance = obj.minDistance;
+	      this.maxDistance = maxNumberToInfinity(obj.maxDistance);
+	      this.minPolarAngle = obj.minPolarAngle;
+	      this.maxPolarAngle = maxNumberToInfinity(obj.maxPolarAngle);
+	      this.minAzimuthAngle = maxNumberToInfinity(obj.minAzimuthAngle);
+	      this.maxAzimuthAngle = maxNumberToInfinity(obj.maxAzimuthAngle);
+	      this.dampingFactor = obj.dampingFactor;
+	      this.draggingDampingFactor = obj.draggingDampingFactor;
+	      this.dollySpeed = obj.dollySpeed;
+	      this.truckSpeed = obj.truckSpeed;
+	      this.dollyToCursor = obj.dollyToCursor;
+	      this.verticalDragToForward = obj.verticalDragToForward;
+
+	      this._target0.fromArray(obj.target0);
+
+	      this._position0.fromArray(obj.position0);
+
+	      this._targetEnd.fromArray(obj.target);
+
+	      this._sphericalEnd.setFromVector3(position.sub(this._target0).applyQuaternion(this._yAxisUpSpace));
+
+	      if (!enableTransition) {
+	        this._target.copy(this._targetEnd);
+
+	        this._spherical.copy(this._sphericalEnd);
+	      }
+
+	      this._hasUpdated = true;
+	    }
+	  }, {
+	    key: "dispose",
+	    value: function dispose() {
+	      this._removeAllEventListeners();
+	    }
+	  }, {
+	    key: "_encloseToBoundary",
+	    value: function _encloseToBoundary(position, offset, friction) {
+	      var offsetLength2 = offset.lengthSq();
+
+	      if (offsetLength2 === 0.0) {
+	        // sanity check
+	        return position;
+	      } // See: https://twitter.com/FMS_Cat/status/1106508958640988161
+
+
+	      var newTarget = _v3B.copy(offset).add(position); // target
+
+
+	      var clampedTarget = this._boundary.clampPoint(newTarget, _v3C); // clamped target
+
+
+	      var deltaClampedTarget = clampedTarget.sub(newTarget); // newTarget -> clampedTarget
+
+	      var deltaClampedTargetLength2 = deltaClampedTarget.lengthSq(); // squared length of deltaClampedTarget
+
+	      if (deltaClampedTargetLength2 === 0.0) {
+	        // when the position doesn't have to be clamped
+	        return position.add(offset);
+	      } else if (deltaClampedTargetLength2 === offsetLength2) {
+	        // when the position is completely stuck
+	        return position;
+	      } else if (friction === 0.0) {
+	        return position.add(offset).add(deltaClampedTarget);
+	      } else {
+	        var offsetFactor = 1.0 + friction * deltaClampedTargetLength2 / offset.dot(deltaClampedTarget);
+	        return position.add(_v3B.copy(offset).multiplyScalar(offsetFactor)).add(deltaClampedTarget.multiplyScalar(1.0 - friction));
+	      }
+	    }
+	  }, {
+	    key: "_sanitizeSphericals",
+	    value: function _sanitizeSphericals() {
+	      this._sphericalEnd.theta = this._sphericalEnd.theta % PI_2;
+	      this._spherical.theta += PI_2 * Math.round((this._sphericalEnd.theta - this._spherical.theta) / PI_2);
+	    }
+	    /**
+	     * Get its client rect and package into given `THREE.Vector4` .
+	     */
+
+	  }, {
+	    key: "_getClientRect",
+	    value: function _getClientRect(target) {
+	      var rect = this._domElement.getBoundingClientRect();
+
+	      target.x = rect.left;
+	      target.y = rect.top;
+
+	      if (this._viewport) {
+	        target.x += this._viewport.x;
+	        target.y += rect.height - this._viewport.w - this._viewport.y;
+	        target.z = this._viewport.z;
+	        target.w = this._viewport.w;
+	      } else {
+	        target.z = rect.width;
+	        target.w = rect.height;
+	      }
+
+	      return target;
+	    }
+	  }, {
+	    key: "phiSpeed",
+	    set: function set(speed) {
+	      console.warn('phiSpeed was renamed. use azimuthRotateSpeed instead');
+	      this.azimuthRotateSpeed = speed;
+	    } // wrong. theta should be map to azimuth, but backward compatibility.
+
+	  }, {
+	    key: "thetaSpeed",
+	    set: function set(speed) {
+	      console.warn('thetaSpeed was renamed. use polarRotateSpeed instead');
+	      this.polarRotateSpeed = speed;
+	    }
+	  }, {
+	    key: "boundaryEnclosesCamera",
+	    get: function get() {
+	      return this._boundaryEnclosesCamera;
+	    },
+	    set: function set(boundaryEnclosesCamera) {
+	      this._boundaryEnclosesCamera = boundaryEnclosesCamera;
+	      this._hasUpdated = true;
+	    }
+	  }]);
+
+	  return CameraControls;
+	}(EventDispatcher);
+
+	function isTouchEvent(event) {
+	  return 'TouchEvent' in window && event instanceof TouchEvent;
+	}
+
+	function infinityToMaxNumber(value) {
+	  if (isFinite(value)) return value;
+	  if (value < 0) return -Number.MAX_VALUE;
+	  return Number.MAX_VALUE;
+	}
+
+	function maxNumberToInfinity(value) {
+	  if (Math.abs(value) < Number.MAX_VALUE) return value;
+	  return value * Infinity;
+	}
+
+	return CameraControls;
+
+}));

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1672 - 0
gcode_viewer/js/dat.gui.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
gcode_viewer/js/helvetiker_bold.typeface.json


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
gcode_viewer/js/jquery.min.js


+ 8 - 0
gcode_viewer/js/models/ExtruderNozzle.mtl

@@ -0,0 +1,8 @@
+# Exported with QuadFace Tools (0.14.0)
+# SketchUp 17.2.2555
+# Model name: ExtruderNozzle
+
+newmtl FrontColor
+Ka 0.000000 0.000000 0.000000
+Kd 1.000000 1.000000 1.000000
+Ks 0.330000 0.330000 0.330000

+ 5282 - 0
gcode_viewer/js/models/ExtruderNozzle.obj

@@ -0,0 +1,5282 @@
+# Exported with QuadFace Tools (0.14.0)
+# SketchUp 17.2.2555
+# Model name: ExtruderNozzle
+# Units: Millimeters
+
+mtllib ExtruderNozzle.mtl
+
+s off
+
+o 0.4mm-Entity65806
+
+v -38.145117767222054 44.962894945694856 -19.885225814314904
+v -38.14511776723865 46.01780626902365 -19.83389566424689
+v -38.14511776725256 46.90150340408506 -19.679905214042353
+v -38.14511776726378 47.61398635087911 -19.423254463701483
+v -38.1451177672723 48.15525510940577 -19.0639434132241
+v -38.14511776727859 48.553959065749645 -18.59958461376977
+v -38.1451177672831 48.838747605995316 -18.02779061649835
+v -38.145117767285804 49.009620730142764 -17.348561421409745
+v -38.14511776728673 49.06657843819201 -16.56189702850386
+v -38.14511776728597 49.016783076664176 -15.826562785666162
+v -38.145117767283644 48.867396992080366 -15.235839729731639
+v -38.14511776728005 48.63820190340378 -14.765171244056345
+v -38.14511776727551 48.348979529597564 -14.390000711996242
+v -38.14511776727037 48.021557974345235 -14.097367696989332
+v -38.14511776726497 47.677765341330264 -13.874311762473534
+v -38.14511776725205 46.856482940238905 -13.571446823864981
+v -38.14511776723768 45.94243109849269 -13.403643276797984
+v -38.14511776722269 44.99018007530034 -13.347708761108905
+v -38.14511776720671 43.97449112577782 -13.39375241731601
+v -38.14511776719302 43.10477761963865 -13.531883385938057
+v -38.14511776718163 42.38103955688281 -13.762101666974694
+v -38.14511776717253 41.80327693751031 -14.084407260426273
+v -38.145117767165566 41.36159890203957 -14.519264013495638
+v -38.145117767160585 41.04611459098899 -15.087135773386535
+v -38.145117767157586 40.85682400435859 -15.788022540098602
+v -38.14511776715656 40.79372714214834 -16.62192431363202
+v -38.1451177671578 40.87421827448107 -17.534611898898017
+v -38.14511776716157 41.11569167147958 -18.25357506397321
+v -38.145117767167804 41.512690307223004 -18.819741503263817
+v -38.145117767176394 42.059757155790464 -19.274038911176618
+v -38.145117767184615 42.58294951595413 -19.531201257697848
+v -38.1451177671954 43.26848839726378 -19.724243549648875
+v -38.14511776720809 44.075446105312864 -19.844980248148275
+
+
+usemtl FrontColor
+
+f 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
+
+v -38.14511776719389 43.15661936588716 -12.212647369566978
+v -38.14511776719396 43.15661936588757 -9.838841093987147
+v -38.14511776715893 40.93015279017134 -9.838841093986765
+v -38.14511776715885 40.93015279017093 -12.212647369566598
+
+f 34 35 36 37
+
+v -38.14511776718262 42.42537789249141 -4.889318583755112
+v -38.14511776718249 42.42537789249072 -8.938431817042925
+v -38.145117767211254 44.253481575983216 -8.938431817043238
+v -38.14511776728712 49.06657843819401 -4.889318583756249
+v -38.145117767287175 49.06657843819434 -2.9520743818462685
+v -38.14511776720982 44.14979808348766 -2.952074381845428
+v -38.14511776720985 44.14979808348783 -1.9479816124047256
+v -38.14511776718272 42.42537789249192 -1.9479816124044296
+v -38.14511776718269 42.42537789249175 -2.952074381845132
+v -38.14511776715916 40.930152790172514 -2.9520743818448754
+v -38.14511776715909 40.93015279017218 -4.889318583754856
+
+f 38 39 40 41 42 43 44 45 46 47 48
+
+v -38.145117767252565 46.82186493205885 18.446024718597474
+v -38.1451177672531 46.85648294024432 18.005633667267304
+v -39.1451177672531 46.85648294022858 18.005633667267272
+v -39.14511776725257 46.82186493204311 18.446024718597442
+
+f 49 50 51 52
+
+v -38.145117767250404 46.7255143181401 -1.0257442317776448
+v -38.145117767250476 46.72551431814045 1.0479256181541248
+v -38.14511776723717 45.87967530040512 1.0479256181542687
+v -38.14511776724431 46.33260845183767 1.4981195983724251
+v -38.14511776724905 46.63274487748577 1.9523850315239066
+v -38.145117767251705 46.8005484245527 2.4558276474856693
+v -38.14511776725261 46.856482940241754 3.0535531761350274
+v -38.145117767251655 46.79509139863204 3.6752064844569676
+v -38.14511776724878 46.61091677380266 4.152035440800871
+v -38.14511776724398 46.305323322233825 4.519553346667171
+v -38.145117767237295 45.87967530040577 4.813273503556744
+v -38.14511776724481 46.356482940242145 5.313294820064179
+v -38.145117767249474 46.651844468209475 5.766974049259256
+v -38.145117767251904 46.805323322234116 6.247895775043584
+v -38.14511776725273 46.8564829402424 6.829644581319211
+v -38.1451177672522 46.82186493205694 7.27003563264938
+v -38.145117767250575 46.718010907500386 7.65933634462076
+v -38.14511776724786 46.54492086657275 7.997546717233266
+v -38.14511776724406 46.30259480927403 8.284666750487066
+v -38.1451177672391 45.986598902043504 8.513544756114603
+v -38.1451177672329 45.59249931132047 8.677029045848771
+v -38.14511776722548 45.12029603710494 8.775119619689207
+v -38.14511776721682 44.5699890793969 8.80781647763608
+v -38.14511776715955 40.930152790174525 8.807816477636708
+v -38.14511776715947 40.93015279017415 6.581349901920513
+v -38.145117767211424 44.23165347230238 6.581349901919858
+v -38.14511776721681 44.57408184883717 6.543278619518795
+v -38.145117767220604 44.81555524583578 6.4290647723153915
+v -38.145117767224136 45.040657565071754 6.178915556762899
+v -38.14511776722531 45.11569167148372 5.874409247076686
+v -38.14511776722418 45.0447503345123 5.520928261401373
+v -38.14511776722082 44.8319263235982 5.243568524020731
+v -38.145117767215 44.46221281745901 5.064094189095828
+v -38.14511776720648 43.92060299481235 5.0042694107875665
+v -38.14511776715942 40.93015279017387 5.0042694107881704
+v -38.14511776715935 40.930152790173494 2.777802835071885
+v -38.14511776720958 44.122512953884275 2.7778028350713377
+v -38.145117767217734 44.64093041636721 2.7339760956442607
+v -38.14511776722083 44.83806547775873 2.6367728214286186
+v -38.14511776722326 44.99290858826349 2.4875572689046743
+v -38.14511776722483 45.0931814395595 2.2972861229293606
+v -38.14511776722535 45.1266057233248 2.076916068359065
+v -38.1451177672242 45.054300129873184 1.7277943084682583
+v -38.14511776722078 44.83738334951842 1.4471411706783086
+v -38.145117767214764 44.455391535057274 1.2623270506237751
+v -38.14511776720583 43.88786083928646 1.200722343939035
+v -38.1451177671593 40.93015279017323 1.2007223439395427
+v -38.14511776715923 40.93015279017284 -1.025744231776652
+
+f 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
+
+v -39.145117767224136 45.04065756505602 6.178915556762865
+v -39.14511776722531 45.11569167146798 5.8744092470766525
+
+f 82 81 101 102
+
+v -38.145117767250774 46.72551431814201 10.15024485417054
+v -38.145117767250845 46.72551431814236 12.22391470410231
+v -38.14511776723754 45.87967530040703 12.223914704102457
+v -38.14511776724468 46.33260845183958 12.67410868432061
+v -38.14511776724942 46.63274487748768 13.128374117472001
+v -38.145117767252074 46.80054842455461 13.631816733433764
+v -38.14511776725297 46.856482940243666 14.229542262083122
+v -38.14511776725203 46.79509139863395 14.851195570405153
+v -38.14511776724915 46.61091677380456 15.328024526748969
+v -38.14511776724435 46.305323322235736 15.695542432615357
+v -38.14511776723766 45.879675300407676 15.989262589504841
+v -38.145117767245175 46.35648294024406 16.489283906012275
+v -38.14511776724984 46.65184446821139 16.94296313520735
+v -38.14511776725227 46.805323322236035 17.42388486099177
+v -38.145117767250944 46.7180109075023 18.835325430568854
+v -38.14511776724824 46.544920866574664 19.17353580318145
+v -38.145117767244436 46.30259480927594 19.46065583643525
+v -38.14511776723946 45.986598902045415 19.68953384206279
+v -38.14511776723327 45.592499311322385 19.853018131796865
+v -38.14511776722584 45.120296037106854 19.95110870563739
+v -38.14511776721719 44.56998907939881 19.983805563584266
+v -38.14511776715992 40.930152790176436 19.98380556358489
+v -38.145117767159846 40.93015279017606 17.757338987868607
+v -38.14511776721179 44.231653472304295 17.757338987868042
+v -38.14511776721717 44.57408184883908 17.71926770546689
+v -38.145117767220974 44.815555245837686 17.605053858263577
+v -38.145117767224505 45.04065756507367 17.354904642711084
+v -38.14511776722568 45.11569167148562 17.05039833302478
+v -38.14511776722455 45.04475033451421 16.696917347349558
+v -38.14511776722119 44.831926323600115 16.419557609968916
+v -38.14511776721537 44.46221281746092 16.240083275043922
+v -38.14511776720684 43.92060299481426 16.180258496735753
+v -38.14511776715979 40.93015279017578 16.180258496736265
+v -38.14511776715972 40.930152790175406 13.95379192102007
+v -38.145117767209946 44.122512953886186 13.953791921019523
+v -38.145117767218096 44.64093041636913 13.909965181592442
+v -38.1451177672212 44.83806547776065 13.812761907376801
+v -38.14511776722363 44.9929085882654 13.663546354852862
+v -38.14511776722521 45.09318143956141 13.473275208877459
+v -38.14511776722572 45.12660572332672 13.252905154307253
+v -38.145117767224576 45.054300129875095 12.903783394416353
+v -38.14511776722115 44.83738334952033 12.62313025662649
+v -38.14511776721513 44.455391535059185 12.43831613657196
+v -38.1451177672062 43.88786083928837 12.37671142988722
+v -38.14511776715966 40.930152790175136 12.376711429887727
+v -38.14511776715959 40.93015279017475 10.150244854171532
+
+f 103 104 105 106 107 108 109 110 111 112 113 114 115 116 50 49 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
+
+v -39.145117767237295 45.87967530039003 4.813273503556711
+v -39.14511776724481 46.35648294022641 5.313294820064145
+
+f 64 63 149 150
+
+v -39.14511776718249 42.42537789247499 -8.938431817042957
+v -39.14511776718262 42.42537789247568 -4.889318583755147
+v -39.14511776715909 40.93015279015645 -4.88931858375489
+v -39.14511776715916 40.930152790156775 -2.952074381844909
+v -39.14511776718268 42.425377892476014 -2.952074381845166
+v -39.14511776718272 42.42537789247619 -1.9479816124044633
+v -39.14511776720985 44.1497980834721 -1.9479816124047595
+v -39.14511776720982 44.14979808347192 -2.952074381845462
+v -39.145117767287175 49.06657843817861 -2.952074381846302
+v -39.14511776728711 49.066578438178276 -4.889318583756283
+v -39.145117767211254 44.253481575967484 -8.93843181704327
+
+f 151 152 153 154 155 156 157 158 159 160 161
+
+v -39.14511776715923 40.93015279015711 -1.025744231776686
+v -39.14511776725041 46.72551431812437 -1.0257442317776786
+
+f 53 100 162 163
+f 39 38 152 151
+
+v -39.145117767237174 45.87967530038939 1.0479256181542347
+v -39.145117767244315 46.33260845182193 1.4981195983723914
+
+f 56 55 164 165
+f 40 39 151 161
+
+v -39.145117767244066 46.30259480925829 8.284666750487032
+v -39.1451177672391 45.98659890202777 8.51354475611457
+
+f 72 71 166 167
+f 41 40 161 160
+
+v -39.14511776722083 44.838065477743 2.636772821428585
+v -39.14511776722326 44.99290858824775 2.48755726890464
+
+f 92 91 168 169
+f 42 41 160 159
+
+v -39.14511776725298 46.856482940227934 14.229542262083088
+v -39.145117767252025 46.79509139861821 14.85119557040512
+
+f 110 109 170 171
+f 43 42 159 158
+
+v -39.145117767159924 40.930152790160705 19.98380556358486
+v -39.145117767159846 40.93015279016033 17.75733898786857
+
+f 125 124 172 173
+f 44 43 158 157
+
+v -39.14511776725261 46.85648294022602 3.0535531761349937
+v -39.14511776725166 46.7950913986163 3.675206484456934
+
+f 60 59 174 175
+f 45 44 157 156
+
+v -39.145117767252735 46.85648294022666 6.829644581319177
+v -39.1451177672522 46.8218649320412 7.270035632649346
+
+f 68 67 176 177
+f 46 45 156 155
+
+v -39.14511776715955 40.93015279015879 8.807816477636674
+v -39.145117767159476 40.93015279015842 6.581349901920478
+
+f 77 76 178 179
+f 47 46 155 154
+
+v -39.1451177672181 44.64093041635339 13.909965181592408
+v -39.14511776722121 44.83806547774492 13.812761907376768
+
+f 139 138 180 181
+f 48 47 154 153
+
+v -39.14511776722421 45.054300129857445 1.7277943084682246
+v -39.14511776722078 44.83738334950269 1.4471411706782746
+
+f 96 95 182 183
+f 38 48 153 152
+
+v -39.14511776723754 45.8796753003913 12.223914704102423
+v -39.145117767244685 46.332608451823845 12.674108684320576
+
+f 106 105 184 185
+
+v -38.14511776724937 46.66744815170015 -4.889318583755837
+v -38.145117767209754 44.14979808348732 -4.889318583755409
+v -39.14511776720975 44.14979808347159 -4.8893185837554425
+v -39.14511776724937 46.66744815168441 -4.8893185837558715
+
+f 186 187 188 189
+
+v -39.145117767237664 45.879675300391945 15.989262589504808
+v -39.14511776724518 46.356482940228325 16.48928390601224
+
+f 114 113 190 191
+
+v -38.14511776720968 44.14979808348696 -7.028813808857733
+v -39.14511776720968 44.149798083471225 -7.028813808857767
+
+f 192 186 189 193
+
+v -39.145117767244436 46.3025948092602 19.46065583643522
+v -39.14511776723947 45.986598902029684 19.689533842062755
+
+f 120 119 194 195
+f 187 192 193 188
+
+v -39.14511776725048 46.72551431812472 1.047925618154091
+
+f 54 53 163 196
+
+v -39.14511776719396 43.15661936587184 -9.83884109398718
+v -39.14511776719389 43.15661936587143 -12.21264736956701
+v -39.14511776715885 40.93015279015519 -12.21264736956663
+v -39.14511776715893 40.9301527901556 -9.8388410939868
+
+f 197 198 199 200
+
+v -39.14511776724905 46.63274487747004 1.9523850315238729
+v -39.145117767251705 46.80054842453697 2.4558276474856355
+
+f 58 57 201 202
+f 35 34 198 197
+
+v -39.14511776724878 46.61091677378692 4.152035440800836
+v -39.14511776724399 46.305323322218086 4.519553346667137
+
+f 62 61 203 204
+f 36 35 197 200
+
+v -39.14511776724948 46.651844468193744 5.766974049259222
+v -39.14511776725191 46.80532332221838 6.24789577504355
+
+f 66 65 205 206
+f 37 36 200 199
+
+v -39.145117767250575 46.718010907484654 7.659336344620726
+v -39.14511776724787 46.544920866557014 7.997546717233232
+
+f 70 69 207 208
+f 34 37 199 198
+
+v -39.145117767232904 45.59249931130474 8.677029045848737
+v -39.14511776722548 45.120296037089204 8.775119619689173
+
+f 74 73 209 210
+
+v -39.14511776723865 46.01780626900792 -19.83389566424692
+v -39.145117767222054 44.962894945679125 -19.88522581431494
+v -39.145117767208085 44.07544610529713 -19.844980248148307
+v -39.145117767195394 43.26848839724804 -19.724243549648904
+v -39.145117767184615 42.58294951593839 -19.531201257697884
+v -39.14511776717639 42.059757155774726 -19.274038911176653
+v -39.145117767167804 41.51269030720727 -18.819741503263852
+v -39.145117767161565 41.11569167146385 -18.25357506397324
+v -39.14511776715779 40.87421827446534 -17.534611898898053
+v -39.14511776715656 40.79372714213262 -16.621924313632057
+v -39.14511776715758 40.856824004342855 -15.788022540098636
+v -39.145117767160585 41.04611459097326 -15.087135773386569
+v -39.14511776716556 41.36159890202383 -14.519264013495672
+v -39.14511776717253 41.80327693749458 -14.084407260426307
+v -39.14511776718163 42.38103955686707 -13.762101666974726
+v -39.14511776719302 43.10477761962291 -13.531883385938091
+v -39.14511776720671 43.97449112576209 -13.393752417316042
+v -39.14511776722269 44.990180075284606 -13.347708761108935
+v -39.14511776723767 45.94243109847695 -13.403643276798016
+v -39.14511776725205 46.856482940223174 -13.571446823865015
+v -39.14511776726496 47.67776534131453 -13.874311762473567
+v -39.14511776727037 48.0215579743295 -14.097367696989366
+v -39.14511776727551 48.34897952958183 -14.390000711996274
+v -39.14511776728005 48.638201903388044 -14.76517124405638
+v -39.14511776728364 48.86739699206464 -15.235839729731673
+v -39.14511776728597 49.016783076648444 -15.826562785666193
+v -39.14511776728673 49.06657843817628 -16.561897028503896
+v -39.145117767285804 49.009620730127025 -17.348561421409777
+v -39.14511776728309 48.83874760597958 -18.027790616498383
+v -39.14511776727859 48.55395906573391 -18.599584613769807
+v -39.1451177672723 48.15525510939004 -19.063943413224134
+v -39.14511776726378 47.61398635086338 -19.423254463701518
+v -39.14511776725256 46.90150340406933 -19.67990521404239
+
+f 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
+
+v -39.14511776721681 44.574081848821436 6.5432786195187616
+v -39.145117767220604 44.81555524582005 6.429064772315358
+
+f 80 79 244 245
+
+v -39.14511776715967 40.9301527901594 12.376711429887694
+v -39.1451177671596 40.93015279015902 10.150244854171499
+
+f 148 147 246 247
+
+v -39.145117767224185 45.04475033449657 5.520928261401339
+v -39.145117767220825 44.83192632358247 5.243568524020696
+
+f 84 83 248 249
+f 5 4 242 241
+
+v -39.14511776715979 40.93015279016006 16.180258496736233
+v -39.14511776715972 40.930152790159674 13.953791921020036
+
+f 136 135 250 251
+f 6 5 241 240
+
+v -39.14511776722484 45.09318143954376 2.297286122929327
+v -39.14511776722536 45.12660572330907 2.076916068359031
+
+f 94 93 252 253
+f 7 6 240 239
+
+v -39.145117767221194 44.83192632358438 16.41955760996888
+v -39.14511776721537 44.46221281744519 16.24008327504389
+
+f 133 132 254 255
+f 8 7 239 238
+
+v -39.14511776725078 46.72551431812628 10.150244854170506
+v -39.14511776725085 46.72551431812663 12.223914704102276
+
+f 104 103 256 257
+f 9 8 238 237
+
+v -39.145117767249424 46.63274487747195 13.128374117471967
+v -39.14511776725208 46.80054842453888 13.63181673343373
+
+f 108 107 258 259
+f 10 9 237 236
+
+v -39.145117767249154 46.61091677378883 15.328024526748935
+v -39.14511776724435 46.30532332222 15.695542432615323
+
+f 112 111 260 261
+f 11 10 236 235
+
+v -39.14511776724984 46.651844468195655 16.942963135207318
+v -39.14511776725227 46.805323322220296 17.423884860991734
+
+f 116 115 262 263
+f 12 11 235 234
+
+v -39.14511776725095 46.718010907486565 18.835325430568822
+v -39.14511776724824 46.544920866558925 19.173535803181416
+
+f 118 117 264 265
+f 13 12 234 233
+
+v -39.14511776723327 45.59249931130665 19.853018131796834
+v -39.14511776722585 45.120296037091116 19.95110870563736
+
+f 122 121 266 267
+f 14 13 233 232
+
+v -39.14511776721718 44.57408184882335 17.719267705466855
+v -39.145117767220974 44.81555524582196 17.605053858263542
+
+f 128 127 268 269
+f 15 14 232 231
+f 55 54 196 164
+
+v -39.14511776722115 44.8373833495046 12.623130256626457
+v -39.14511776721514 44.455391535043454 12.438316136571926
+
+f 145 144 270 271
+f 57 56 165 201
+f 22 21 225 224
+f 59 58 202 174
+f 23 22 224 223
+
+v -39.14511776722451 45.04065756505794 17.35490464271105
+v -39.14511776722568 45.11569167146989 17.05039833302475
+
+f 130 129 272 273
+f 24 23 223 222
+f 63 62 204 149
+f 25 24 222 221
+f 65 64 150 205
+
+v -39.145117767224576 45.05430012985936 12.903783394416319
+
+f 144 143 274 270
+f 67 66 206 176
+f 29 28 218 217
+f 69 68 177 207
+f 30 29 217 216
+f 71 70 208 166
+f 31 30 216 215
+f 73 72 167 209
+f 32 31 215 214
+
+v -39.14511776722364 44.99290858824966 13.663546354852828
+v -39.1451177672252 45.09318143954568 13.473275208877425
+
+f 141 140 275 276
+
+v -39.14511776722572 45.12660572331098 13.25290515430722
+
+f 143 142 277 274
+f 140 139 181 275
+
+v -38.145117767194556 43.21033696479485 -17.52864327679757
+v -38.14511776720181 43.67162618716864 -17.61305664651113
+v -39.14511776720181 43.67162618715291 -17.613056646511165
+v -39.14511776719455 43.21033696477912 -17.528643276797606
+
+f 278 279 280 281
+f 81 80 245 101
+
+v -38.145117767189205 42.870125505040434 -17.41046455919864
+v -39.145117767189205 42.8701255050247 -17.410464559198672
+
+f 282 278 281 283
+f 83 82 102 248
+
+v -38.14511776718536 42.625411998901306 -17.260225814314474
+v -39.14511776718536 42.62541199888557 -17.260225814314506
+
+f 284 282 283 285
+
+v -39.145117767215 44.46221281744327 5.064094189095794
+
+f 85 84 249 286
+
+v -38.14511776718261 42.45061663737337 -17.079632362745564
+v -39.14511776718261 42.45061663735764 -17.079632362745595
+
+f 287 284 285 288
+
+v -39.14511776715943 40.93015279015814 5.004269410788137
+v -39.145117767159356 40.930152790157756 2.777802835071851
+
+f 88 87 289 290
+
+v -38.14511776718097 42.34573942045663 -16.868684204491824
+v -39.14511776718097 42.3457394204409 -16.868684204491856
+
+f 291 287 288 292
+
+v -39.145117767217734 44.64093041635148 2.7339760956442265
+
+f 91 90 293 168
+
+v -38.14511776718043 42.31078034815107 -16.62738133955325
+v -39.14511776718043 42.31078034813534 -16.627381339553285
+
+f 294 291 292 295
+f 93 92 169 252
+
+v -38.145117767181375 42.370125505040626 -16.311555964382585
+v -39.14511776718137 42.370125505024895 -16.311555964382617
+
+f 296 294 295 297
+f 95 94 253 182
+
+v -38.14511776718419 42.54816097570916 -16.040751053059367
+v -39.14511776718418 42.54816097569342 -16.0407510530594
+
+f 298 296 297 299
+
+v -39.14511776721477 44.45539153504154 1.2623270506237414
+
+f 97 96 183 300
+
+v -38.14511776718901 42.8544365555182 -15.825198529184979
+v -39.14511776718901 42.85443655550247 -15.825198529185009
+
+f 301 298 299 302
+
+v -39.1451177671593 40.93015279015749 1.200722343939509
+
+f 100 99 303 162
+
+v -38.145117767196005 43.29850203982928 -15.675130316360976
+v -39.145117767196 43.29850203981354 -15.67513031636101
+
+f 304 301 302 305
+
+v -39.14511776720621 43.88786083927263 12.376711429887186
+v -39.145117767209946 44.12251295387045 13.953791921019489
+v -39.14511776720685 43.92060299479853 16.180258496735718
+v -39.14511776722455 45.04475033449848 16.696917347349522
+v -39.145117767211794 44.23165347228856 17.75733898786801
+v -39.14511776721719 44.56998907938308 19.983805563584234
+
+f 257 256 247 246 306 271 270 274 277 276 275 181 180 307 251 250 308 255 254 309 273 272 269 268 310 173 172 311 267 266 195 194 265 264 52 51 263 262 191 190 261 260 171 170 259 258 185 184
+
+v -38.14511776720633 43.95470940681429 -15.587135773387008
+v -39.14511776720633 43.95470940679855 -15.587135773387041
+
+f 312 304 305 313
+f 105 104 257 184
+f 142 141 276 277
+f 107 106 185 258
+
+v -38.145117767249516 46.699763977073474 -15.709748324547112
+v -38.14511776724212 46.22960708757826 -15.625334954833459
+v -39.14511776724212 46.22960708756253 -15.625334954833493
+v -39.145117767249516 46.699763977057735 -15.709748324547142
+
+f 314 315 316 317
+f 109 108 259 170
+
+v -38.14511776725485 47.03929330858777 -15.827927042146134
+v -39.14511776725485 47.03929330857204 -15.827927042146165
+
+f 318 314 317 319
+f 111 110 171 260
+
+v -38.145117767258625 47.27923191704614 -15.979871107630439
+v -39.145117767258625 47.2792319170304 -15.979871107630473
+
+f 320 318 319 321
+f 113 112 261 190
+
+v -38.14511776726132 47.45061663737352 -16.165580521000216
+v -39.14511776726132 47.45061663735779 -16.165580521000248
+
+f 322 320 321 323
+f 115 114 191 262
+
+v -38.145117767262924 47.55344746956994 -16.385055282255284
+v -39.145117767262924 47.55344746955421 -16.385055282255315
+
+f 324 322 323 325
+f 50 116 263 51
+
+v -38.14511776726346 47.58772441363539 -16.638295391395822
+v -39.14511776726346 47.587724413619654 -16.638295391395854
+
+f 326 324 325 327
+f 117 49 52 264
+
+v -38.145117767262896 47.552765341329746 -16.894775609676845
+v -39.145117767262896 47.552765341314014 -16.89477560967688
+
+f 328 326 327 329
+f 119 118 265 194
+
+v -38.14511776726124 47.44788812441293 -17.1130566465118
+v -39.14511776726124 47.447888124397195 -17.113056646511833
+
+f 330 328 329 331
+f 121 120 195 266
+
+v -38.14511776725849 47.27309276288493 -17.2931385019005
+v -39.14511776725848 47.2730927628692 -17.293138501900533
+
+f 332 330 331 333
+f 132 131 309 254
+
+v -38.14511776725463 47.02837925674576 -17.435021175843218
+v -39.145117767254625 47.02837925673003 -17.435021175843247
+
+f 334 332 333 335
+f 131 130 273 309
+
+v -39.14511776720584 43.88786083927072 1.2007223439390013
+v -39.145117767209584 44.12251295386854 2.777802835071304
+v -39.14511776720648 43.92060299479662 5.004269410787533
+v -39.145117767211424 44.23165347228665 6.5813499019198245
+v -39.145117767216824 44.56998907938116 8.807816477636047
+
+f 196 163 162 303 336 300 183 182 253 252 169 168 293 337 290 289 338 286 249 248 102 101 245 244 339 179 178 340 210 209 167 166 208 207 177 176 206 205 150 149 204 203 175 174 202 201 165 164
+f 129 128 269 272
+f 61 60 175 203
+f 103 148 247 256
+
+s 1
+f 75 74 210 340
+f 76 75 340 178
+
+s off
+
+s 2
+f 98 97 300 336
+f 99 98 336 303
+
+s off
+
+s 3
+f 134 133 255 308
+f 135 134 308 250
+
+s off
+
+s 4
+f 89 88 290 337
+f 90 89 337 293
+
+s off
+
+s 5
+f 137 136 251 307
+f 138 137 307 180
+
+s off
+
+s 6
+f 86 85 286 338
+f 87 86 338 289
+
+s off
+
+s 7
+f 78 77 179 339
+f 79 78 339 244
+
+s off
+
+s 8
+f 123 122 267 311
+f 124 123 311 172
+
+s off
+
+s 9
+
+v -38.145117767221166 44.89741063464512 -15.55780425906254
+v -39.14511776722116 44.89741063462938 -15.55780425906257
+
+f 341 312 313 342
+f 315 341 342 316
+
+s off
+
+s 10
+
+v -38.14511776722204 44.95743791977436 -17.680587342282134
+v -39.14511776722203 44.95743791975862 -17.68058734228217
+v -38.145117767242084 46.23097134405813 -17.619195800672557
+v -39.14511776724208 46.230971344042395 -17.61919580067259
+
+f 279 343 344 280
+f 343 345 346 344
+f 345 334 335 346
+
+s off
+
+s 11
+f 26 25 221 220
+f 27 26 220 219
+f 28 27 219 218
+
+s off
+
+s 12
+f 16 15 231 230
+f 17 16 230 229
+f 18 17 229 228
+f 19 18 228 227
+f 20 19 227 226
+f 21 20 226 225
+
+s off
+
+s 13
+f 146 145 271 306
+f 147 146 306 246
+
+s off
+
+s 14
+f 2 1 212 211
+f 1 33 213 212
+f 3 2 211 243
+f 33 32 214 213
+f 4 3 243 242
+
+s off
+
+s 15
+f 126 125 173 310
+f 127 126 310 268
+
+s off
+
+o Component#9-Entity65807
+
+v 2.945301029520563 5.1096575191569553e-14 0.7891910323263158
+v 2.6406846612193386 5.857960621576707e-14 1.5245999999995432
+v 2.156109997193689 6.571806570200106e-14 2.1561099971930178
+v 1.5245999999999662 5.500857444535861e-15 2.640684661219096
+v 0.7891910323265359 7.958733654674477e-15 2.9453010295205235
+v -5.639932965095795e-15 -1.2950174601075548e-15 3.049199999999887
+v -0.7891910323266712 -3.332493424073322e-14 2.9453010295204898
+v -1.5246000000002593 3.6177585743037127e-14 2.6406846612189945
+v -2.1561099971940947 -1.3769182735595278e-14 2.1561099971928934
+v -2.640684661219519 2.5893364774413395e-14 1.5245999999993514
+v -2.945301029520879 -2.4554877427808375e-14 0.7891910323261241
+v -3.0492000000001296 -1.891367440915998e-14 -1.6919798895287386e-13
+v -2.945301029520845 -2.6198855381760667e-14 -0.7891910323269926
+v -2.6406846612194177 2.2717443247626057e-14 -1.524600000000423
+v -2.156109997193971 -1.8260614031644855e-14 -2.156109997194439
+v -1.5245999999999773 3.067672829989649e-14 -2.640684661219773
+v -0.7891910323264568 -3.9460343490078516e-14 -2.945301029521223
+v 1.9739765377835283e-13 -7.646860513436011e-15 -3.049200000000564
+v 0.7891910323267501 1.823324405165038e-15 -2.945301029521189
+v 1.5246000000001578 -8.207182640174571e-26 -2.6406846612196713
+v 2.1561099971938127 6.122663440586941e-14 -2.156109997194315
+v 2.64068466121944 5.540368468922595e-14 -1.524600000000231
+v 2.945301029520597 4.945259723769933e-14 -0.7891910323268009
+v 3.0491999999999604 9.971796429953458e-15 3.3839597790574766e-14
+
+f 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
+
+v 13.927116005783446 18.100000000397998 -9.305801403078092
+v 11.844038584590566 18.10000000036521 -11.844038584874571
+v 9.305801402794309 18.10000000032528 -13.927116006067477
+v 6.409947491831249 18.100000000279703 -15.474982169563964
+v 3.267762893486249 18.100000000230263 -16.428153446754116
+v -2.8391422546292233e-10 18.10000000017885 -16.75000000000011
+v -3.2677628940540546 18.100000000127455 -16.42815344675433
+v -6.409947492399247 18.100000000077962 -15.474982169564381
+v -9.305801403362363 18.100000000032388 -13.927116006068097
+v -11.844038585158723 18.099999999992463 -11.84403858487535
+v -13.927116006351808 18.09999999995968 -9.305801403078974
+v -15.474982169848296 18.099999999935328 -6.4099474921159585
+v -16.428153447038447 18.099999999920346 -3.2677628937708283
+v -16.75000000028445 18.099999999915287 -8.177902799388903e-13
+v -16.42815344703866 18.099999999920342 3.26776289376943
+v -15.474982169848714 18.099999999935342 6.409947492114583
+v -13.927116006352383 18.0999999999597 9.305801403077698
+v -11.8440385851595 18.099999999992487 11.844038584874035
+v -9.305801403363281 18.100000000032416 13.927116006067122
+v -6.409947492400223 18.100000000077994 15.474982169563603
+v -3.2677628940551373 18.100000000127487 16.428153446753942
+v -2.850196523240811e-10 18.100000000178884 16.749999999999897
+v 3.2677628934851657 18.1000000002303 16.428153446754155
+v 6.409947491830229 18.100000000279735 15.474982169564026
+v 9.305801402793389 18.10000000032531 13.927116006067736
+v 11.844038584589788 18.100000000365235 11.844038584874813
+v 13.927116005782832 18.100000000398015 9.305801403078618
+v 15.47498216927936 18.100000000422405 6.409947492115603
+v 16.4281534464695 18.100000000437408 3.2677628937704957
+v 16.749999999715513 18.100000000442456 2.8763658121988553e-13
+v 16.42815344646973 18.10000000043741 -3.2677628937697625
+v 15.474982169279782 18.10000000042239 -6.409947492114938
+
+f 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
+
+v -5.639932965095795e-14 5.055091495354162 2.0491999999999284
+v 1.6919798895287386e-13 5.055091495354158 -2.049200000000075
+v 0.5303719872242838 5.055091495354155 -1.979375203231694
+v 1.0246000000001645 5.055091495354172 -1.774659257435125
+v 1.44900321600763 5.055091495354135 -1.449003216007568
+v 1.774659257435221 5.055091495354198 -1.0245999999998374
+v 1.979375203231694 5.055091495354176 -0.5303719872241428
+v 2.0492000000001034 5.055091495354144 -5.639932965095795e-15
+v 1.9793752032316319 5.055091495354177 0.5303719872240864
+v 1.774659257435108 5.055091495354203 1.0246000000001645
+v 1.449003216007489 5.055091495354138 1.4490032160076582
+v 1.0246000000000008 5.055091495354178 1.7746592574350462
+v 0.5303719872240639 5.0550914953541595 1.9793752032315812
+
+f 403 404 405 406 407 408 409 410 411 412 413 414 415
+
+v 38.105117766062314 28.750000000778638 22.000000000001027
+v 38.105117765639676 55.611427268239616 22.000000000001002
+v 27.89488223281188 58.80628616555381 27.894882233339317
+v 19.052558882605503 59.75000000004442 32.999999999833626
+v 10.210235532450994 58.80628616499729 38.10511776631532
+v -8.76355343848445e-10 55.61142726764018 43.999999999999794
+v -4.5373260704195673e-10 28.750000000179025 43.99999999999982
+v 10.21023553297417 25.555141103143175 38.105117766315345
+v 19.052558883158365 24.61142726837429 32.99999999983367
+v 27.894882233335053 25.555141103143175 27.89488223333935
+
+f 416 417 418 419 420 421 422 423 424 425
+
+v 10.21023553245423 58.80628616499581 -38.10511776631436
+v 19.052558882608817 59.750000000043045 -32.99999999983196
+v 27.8948822328153 58.80628616555197 -27.89488223333665
+v 38.105117765641126 55.611427268239574 -21.999999999998916
+v 38.10511776606377 28.75000000077861 -21.999999999998895
+v 27.894882233338475 25.555141103144855 -27.894882233336613
+v 19.05255888316168 24.61142726837554 -32.99999999983188
+v 10.210235532977409 25.555141103144493 -38.10511776631432
+v -4.508280415649324e-10 28.750000000178936 -44.00000000000024
+v -8.73467698170316e-10 55.61142726764009 -44.00000000000023
+
+f 426 427 428 429 430 431 432 433 434 435
+
+v -10.210235533609639 58.806286164063465 38.10511776671566
+v -19.052558883908798 59.74999999857461 33.0000000001668
+v -27.894882234155883 58.80628616350731 27.894882233630934
+v -38.10511776739093 55.6114272670407 21.999999999998472
+v -38.10511776696829 28.749999999579355 21.9999999999985
+v -27.8948822336327 25.55514110343408 27.89488223363097
+v -19.052558883355925 24.61142726864498 33.00000000016686
+v -10.210235533086417 25.55514110343441 38.10511776671569
+
+f 422 421 436 437 438 439 440 441 442 443
+
+v -27.894882234155656 58.80628616350545 -27.894882233631943
+v -19.05255888390757 59.74999999857299 -33.000000000167596
+v -10.210235533607756 58.80628616406176 -38.10511776671583
+v -10.210235533084534 25.555141103435954 -38.10511776671579
+v -19.052558883354695 24.61142726864648 -33.00000000016752
+v -27.894882233632487 25.555141103435783 -27.89488223363191
+v -38.10511776696684 28.749999999579327 -22.00000000000141
+v -38.10511776738947 55.61142726704066 -22.00000000000143
+
+f 444 445 446 435 434 447 448 449 450 451
+
+v 38.10511776559047 58.80628616601994 -10.2102355329204
+v 38.10511776557528 59.75000000077862 2.752287286966748e-12
+v 38.105117765589796 58.806286166019824 10.21023553292591
+v 38.10511776611297 25.5551411029984 10.210235532925944
+v 38.10511776612814 24.6114272682396 2.7692070858620354e-12
+v 38.10511776611365 25.555141102998228 -10.210235532920366
+
+f 452 453 454 417 416 455 456 457 430 429
+
+v -38.10511776744085 58.80628616303939 10.210235533137983
+v -38.10511776745531 59.74999999783886 5.639932965095795e-14
+v -38.10511776744018 58.80628616303939 -10.210235533137865
+v -38.10511776691696 25.5551411035806 -10.210235533137832
+v -38.105117766902445 24.61142726878116 9.023892744153272e-14
+v -38.10511776691764 25.555141103580663 10.210235533138016
+
+f 440 439 458 459 460 451 450 461 462 463
+
+v -0.5303719872241653 5.055091495354111 1.9793752032315473
+v -1.024600000000046 5.055091495354094 1.7746592574349784
+v -1.4490032160075117 5.055091495354097 1.4490032160075625
+v -1.774659257435108 5.05509149535409 1.0245999999996682
+v -1.979375203231553 5.055091495354098 0.5303719872239736
+v -2.0491999999999906 5.055091495354077 -1.4099832412739488e-13
+v -1.9793752032315362 5.055091495354097 -0.5303719872242556
+v -1.7746592574349953 5.055091495354086 -1.0246000000003337
+v -1.4490032160073707 5.055091495354094 -1.449003216007664
+v -1.0245999999998825 5.0550914953540875 -1.7746592574351927
+v -0.5303719872239454 5.055091495354106 -1.9793752032317278
+
+f 404 403 464 465 466 467 468 469 470 471 472 473 474
+
+s 16
+f 444 460 459 445
+f 460 444 451
+f 459 437 419 453 427 445
+f 437 459 458 438
+f 419 437 436 420
+f 453 419 418 454
+f 427 453 452 428
+f 445 427 426 446
+f 438 458 439
+f 420 436 421
+f 454 418 417
+f 428 452 429
+f 446 426 435
+
+s off
+
+s 17
+
+v 1.7746592574352604 5.5867955925254945e-14 -1.0245999999998316
+v 1.9793752032317504 1.188370324461363e-14 -0.5303719872241371
+v 2.049200000000143 -3.2828730560698284e-25 0.0
+v 1.449003216007664 -9.46053350531055e-15 -1.4490032160075454
+v 1.9793752032316714 1.2988530615624534e-14 0.5303719872240921
+v 1.0246000000001982 7.195984218224327e-15 -1.7746592574351194
+v 1.7746592574351476 5.800231850884409e-14 1.02460000000017
+v 0.5303719872243007 -8.704878610479937e-15 -1.9793752032316883
+v 1.449003216007523 -6.442088992184503e-15 1.449003216007681
+v 2.0867751970854441e-13 -6.605301570319899e-15 -2.0492000000000696
+v 1.0246000000000177 1.0892808655099818e-14 1.7746592574350517
+v 0.5303719872240976 -4.581606725850556e-15 1.9793752032315868
+v -1.6919798895287383e-14 -2.3365764029774522e-15 2.049199999999934
+
+f 475 476 409 408
+f 476 477 410 409
+f 478 475 408 407
+f 477 479 411 410
+f 480 478 407 406
+f 479 481 412 411
+f 482 480 406 405
+f 481 483 413 412
+f 484 482 405 404
+f 483 485 414 413
+f 485 486 415 414
+f 486 487 403 415
+
+s off
+
+s 18
+
+v -2.049199999999951 -3.1501609833844675e-14 -1.3535839116229907e-13
+v -1.9793752032314798 -1.0650542659304797e-14 -0.5303719872242499
+v -1.7746592574349558 -2.1824732761703373e-14 -1.024600000000328
+v -1.9793752032315135 -9.545715287309032e-15 0.5303719872239793
+v -1.449003216007337 -1.3779654911304438e-14 -1.4490032160076414
+v -1.7746592574350686 -1.9690370177785935e-14 1.0245999999996738
+v -1.0245999999998485 -1.9834686628725456e-14 -1.7746592574351872
+v -1.449003216007478 -1.076121039817839e-14 1.449003216007585
+v -0.5303719872239284 -4.3602712477750824e-15 -1.9793752032317222
+v -1.024600000000029 -1.6137862191849967e-14 1.774659257434984
+v -0.5303719872241315 -2.3699936314570165e-16 1.979375203231553
+
+f 488 489 470 469
+f 489 490 471 470
+f 491 488 469 468
+f 490 492 472 471
+f 493 491 468 467
+f 492 494 473 472
+f 495 493 467 466
+f 494 496 474 473
+f 497 495 466 465
+f 496 484 404 474
+f 498 497 465 464
+f 487 498 464 403
+
+s off
+
+s 19
+
+v -6.409947493763654 59.749999998854626 15.474982169562892
+v -3.2677628954185236 59.749999998904094 16.428153446753235
+v -1.6484057674404084e-09 59.74999999895549 16.74999999999919
+v -9.305801404726669 59.74999999880902 13.927116006066411
+v 3.2677628921217345 59.7499999990069 16.42815344675345
+v -11.844038586522943 59.74999999876911 11.844038584873319
+v 6.409947490466842 59.74999999905635 15.474982169563315
+v -13.92711600771577 59.74999999873632 9.305801403076977
+v 9.305801401430003 59.7499999991019 13.927116006067026
+v -15.4749821712121 59.749999998711964 6.409947492113861
+v 11.844038583226391 59.74999999914188 11.844038584874097
+v -16.42815344840206 59.74999999869694 3.2677628937686967
+v 13.92711600441944 59.749999999174634 9.305801403077895
+v -16.750000001647827 59.7499999986919 -1.5340617665060561e-12
+v 15.474982167915966 59.74999999919904 6.409947492114882
+v -16.428153448401844 59.74999999869695 -3.2677628937715615
+v 16.428153445106116 59.749999999214 3.2677628937697794
+v -15.474982171211682 59.749999998711935 -6.409947492116681
+v 16.749999998352123 59.74999999921908 -4.286349053472804e-13
+v -13.92711600771515 59.7499999987363 -9.305801403079695
+v 16.428153445106336 59.74999999921402 -3.267762893770479
+v -11.844038586522153 59.74999999876909 -11.84403858487607
+v 15.47498216791639 59.74999999919902 -6.40994749211566
+v -9.305801404725738 59.74999999880902 -13.92711600606882
+v 13.927116004420055 59.7499999991746 -9.305801403078803
+v -6.409947493762622 59.7499999988546 -15.474982169565092
+v 11.84403858322718 59.749999999141856 -11.844038584875305
+v -3.2677628954174973 59.74999999890407 -16.42815344675505
+v 9.305801401430934 59.7499999991019 -13.927116006068188
+v -1.6473003405792495e-09 59.74999999895549 -16.75000000000081
+v 6.409947490467875 59.74999999905633 -15.474982169564687
+v 3.267762892122806 59.74999999900688 -16.428153446754838
+
+f 499 500 391 390
+f 500 501 392 391
+f 502 499 390 389
+f 501 503 393 392
+f 504 502 389 388
+f 503 505 394 393
+f 506 504 388 387
+f 505 507 395 394
+f 508 506 387 386
+f 507 509 396 395
+f 510 508 386 385
+f 509 511 397 396
+f 512 510 385 384
+f 511 513 398 397
+f 514 512 384 383
+f 513 515 399 398
+f 516 514 383 382
+f 515 517 400 399
+f 518 516 382 381
+f 517 519 401 400
+f 520 518 381 380
+f 519 521 402 401
+f 522 520 380 379
+f 521 523 371 402
+f 524 522 379 378
+f 523 525 372 371
+f 526 524 378 377
+f 525 527 373 372
+f 528 526 377 376
+f 527 529 374 373
+f 530 528 376 375
+f 529 530 375 374
+
+s off
+
+s 20
+f 350 424 423 351
+f 349 425 424 350
+f 351 423 422 352
+f 348 416 425 349
+f 352 422 443 353
+f 347 455 416 348
+f 353 443 442 354
+f 370 456 455 347
+f 354 442 441 355
+f 369 457 456 370
+f 355 441 440 356
+f 368 430 457 369
+f 356 440 463 357
+f 367 431 430 368
+f 357 463 462 358
+f 366 432 431 367
+f 358 462 461 359
+f 365 433 432 366
+f 359 461 450 360
+f 364 434 433 365
+f 360 450 449 361
+f 363 447 434 364
+f 361 449 448 362
+f 362 448 447 363
+
+s off
+
+o Component#11-Entity82134
+
+v 32.011442334115046 73.51999999908122 7.049916206369744e-13
+v 31.396351445754817 73.5199999990715 -6.245122593388038
+v 29.57471637856653 73.51999999904285 -12.250248627818117
+v 26.61654154659754 73.51999999899633 -17.784604477499936
+v 22.63550794967682 73.51999999893368 -22.635507950832366
+v 17.784604476344636 73.51999999885729 -26.616541547753226
+v 12.250248626663202 73.51999999877026 -29.574716379722684
+v 6.245122592233193 73.51999999867571 -31.396351446911314
+v -1.1556391843470236e-09 73.51999999857749 -32.01144233527186
+v -6.245122594544573 73.51999999847916 -31.39635144691173
+v -12.250248628974648 73.51999999838465 -29.574716379723498
+v -17.784604478656288 73.51999999829758 -26.616541547754398
+v -22.63550795198869 73.51999999822124 -22.63550795083387
+v -26.61654154890976 73.5199999981586 -17.78460447750169
+v -29.574716380879085 73.51999999811203 -12.250248627820074
+v -31.396351448067772 73.51999999808342 -6.245122593390107
+v -32.011442336428395 73.51999999807373 -1.4099832412739488e-12
+v -31.396351448068177 73.51999999808345 6.245122593387705
+v -29.5747163808799 73.51999999811207 12.250248627817784
+v -26.616541548910888 73.51999999815865 17.78460447749922
+v -22.63550795199019 73.5199999982213 22.63550795083168
+v -17.78460447865803 73.51999999829768 26.616541547752885
+v -12.25024862897661 73.51999999838473 29.574716379722183
+v -6.245122594546632 73.51999999847924 31.396351446910824
+v -1.1577541592089345e-09 73.51999999857759 32.01144233527152
+v 6.24512259223118 73.51999999867577 31.39635144691122
+v 12.250248626661262 73.51999999877029 29.57471637972298
+v 17.784604476342878 73.51999999885736 26.616541547754057
+v 22.635507949675326 73.51999999893373 22.635507950833173
+v 26.61654154659635 73.51999999899635 17.784604477500977
+v 29.574716378565736 73.51999999904287 12.250248627819724
+v 31.396351445754416 73.51999999907156 6.245122593389775
+
+f 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562
+
+v 16.884938784290476 112.22999999884325 25.270096689467536
+v 21.490450720634783 112.2299999989158 21.490450722401935
+v 25.270096687700825 112.2299999989752 16.884938786057702
+v 28.078627010624203 112.22999999901944 11.630548121348268
+v 29.80811144413684 112.22999999904665 5.9292020148219615
+v 30.39208687136495 112.2299999990558 8.121503469737945e-13
+v 29.808111444137264 112.22999999904665 -5.929202014820596
+v 28.078627010625063 112.2299999990194 -11.630548121346827
+v 25.270096687702026 112.22999999897509 -16.884938786056615
+v 21.490450720636336 112.22999999891576 -21.49045072240094
+v 16.88493878429228 112.2299999988432 -25.27009668946702
+v 11.630548119582802 112.22999999876059 -28.078627012390356
+v 5.9292020130564085 112.2299999986708 -29.808111445902988
+v -1.7647970640410903e-09 112.22999999857754 -30.392086873130918
+v -5.9292020165860135 112.22999999848433 -29.808111445903382
+v -11.630548123112536 112.22999999839449 -28.078627012391124
+v -16.884938787822186 112.22999999831183 -25.27009668946814
+v -21.490450724166454 112.22999999823928 -21.49045072240236
+v -25.2700966912325 112.2299999981799 -16.884938786058285
+v -28.078627014155824 112.22999999813568 -11.630548121348868
+v -29.80811144766842 112.22999999810848 -5.929202014822565
+v -30.392086874896478 112.22999999809932 -1.376143643483374e-12
+v -29.808111447668804 112.22999999810848 5.929202014819812
+v -28.07862701415659 112.22999999813572 11.630548121346411
+v -25.27009669123365 112.22999999818002 16.884938786056033
+v -21.49045072416792 112.22999999823932 21.490450722400336
+v -16.884938787823856 112.22999999831188 25.270096689466424
+v -11.630548123114426 112.22999999839453 28.07862701238976
+v -5.929202016588084 112.22999999848437 29.808111445902206
+v -1.7668499996403852e-09 112.22999999857765 30.392086873130502
+v 5.929202013054248 112.22999999867085 29.80811144590278
+v 11.630548119580771 112.22999999876063 28.078627012390527
+
+f 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
+
+v 21.49045072111189 81.90999999891581 21.490450722401967
+v 16.884938784767588 81.90999999884335 25.270096689467568
+v 11.630548120057876 81.90999999876068 28.078627012390555
+v 5.929202013531409 81.90999999867098 29.8081114459028
+v -1.289717310726246e-09 81.90999999857775 30.39208687313053
+v -5.9292020161109225 81.90999999848437 29.80811144590224
+v -11.630548122637277 81.90999999839467 28.07862701238978
+v -16.8849387873468 81.90999999831192 25.270096689466456
+v -21.490450723690792 81.90999999823946 21.49045072240037
+v -25.27009669075656 81.90999999818007 16.88493878605606
+v -28.078627013679533 81.90999999813577 11.630548121346449
+v -29.808111447191788 81.90999999810857 5.929202014819847
+v -30.39208687441942 81.90999999809937 -1.3592238445880866e-12
+v -29.808111447191408 81.90999999810853 -5.929202014822531
+v -28.078627013678766 81.90999999813577 -11.630548121348845
+v -25.27009669075544 81.90999999817996 -16.884938786058278
+v -21.49045072368937 81.90999999823941 -21.49045072240233
+v -16.884938787345085 81.90999999831192 -25.270096689468105
+v -11.630548122635421 81.90999999839458 -28.078627012391113
+v -5.92920201610891 81.90999999848428 -29.808111445903346
+v -1.2876643751269512e-09 81.90999999857763 -30.392086873130907
+v 5.929202013533513 81.9099999986709 -29.808111445902952
+v 11.630548120059911 81.9099999987606 -28.078627012390346
+v 16.88493878476939 81.90999999884335 -25.27009668946699
+v 21.490450721113444 81.90999999891575 -21.49045072240093
+v 25.270096688179137 81.90999999897522 -16.88493878605661
+v 28.078627011102117 81.90999999901949 -11.630548121346804
+v 29.808111444614322 81.90999999904675 -5.929202014820563
+v 30.392086871841993 81.9099999990559 8.290701458690818e-13
+v 29.8081114446139 81.90999999904675 5.929202014821995
+v 28.07862701110125 81.90999999901949 11.630548121348305
+v 25.270096688177933 81.90999999897534 16.884938786057727
+
+f 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626
+
+v -29.57471638066248 59.74999999811229 -12.25024862782008
+v -26.616541548693096 59.74999999815883 -17.78460447750168
+v -22.635507951772002 59.74999999822145 -22.63550795083387
+v -17.78460447843961 59.749999998297795 -26.61654154775441
+v -12.250248628757989 59.749999998384894 -29.574716379723487
+v -6.245122594327853 59.749999998479375 -31.39635144691172
+v -9.389811594928688e-10 59.74999999857764 -32.01144233527187
+v 6.245122592449913 59.74999999867594 -31.396351446911304
+v 12.250248626879868 59.74999999877046 -29.574716379722695
+v 17.784604476561295 59.74999999885749 -26.61654154775321
+v 22.635507949893487 59.749999998933866 -22.635507950832356
+v 26.616541546814197 59.749999998996536 -17.784604477499936
+v 29.57471637878319 59.74999999904308 -12.250248627818111
+v 31.39635144597147 59.74999999907175 -6.245122593388038
+v 32.01144233433166 59.749999999081425 7.219114195322617e-13
+v 31.39635144597108 59.74999999907176 6.245122593389775
+v 29.574716378782394 59.7499999990431 12.250248627819753
+v 26.616541546813025 59.74999999899656 17.784604477501002
+v 22.635507949891984 59.74999999893391 22.635507950833187
+v 17.784604476559544 59.74999999885757 26.616541547754075
+v 12.25024862687792 59.7499999987705 29.57471637972299
+v 6.245122592447832 59.749999998676 31.39635144691122
+v -9.411017742877447e-10 59.749999998577756 32.01144233527154
+v -6.245122594329934 59.749999998479446 31.396351446910817
+v -12.250248628759907 59.74999999838494 29.574716379722183
+v -17.78460447844136 59.74999999829788 26.616541547752895
+v -22.635507951773526 59.7499999982215 22.63550795083169
+v -26.61654154869428 59.749999998158856 17.784604477499244
+v -29.57471638066328 59.74999999811229 12.2502486278178
+v -31.396351447851526 59.74999999808366 6.245122593387716
+v -32.01144233621173 59.74999999807393 -1.3987033753437572e-12
+v -31.396351447851107 59.74999999808363 -6.245122593390096
+
+f 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658
+
+s 21
+
+v 15.474982167798059 112.22999999882111 6.40994749211548
+v 16.428153444988258 112.22999999883608 3.267762893770377
+v 16.428153445814093 59.74999999883625 3.267762893770428
+v 15.474982168623942 59.749999998821266 6.409947492115547
+v 16.74999999823426 112.22999999884118 1.8047785488306544e-13
+v 16.749999999060098 59.749999998841304 2.19957385638736e-13
+v 13.927116004301578 112.22999999879677 9.30580140307849
+v 13.927116005127417 59.74999999879686 9.305801403078567
+v 16.42815344498848 112.22999999883608 -3.267762893769881
+v 16.428153445814313 59.74999999883624 -3.26776289376983
+v 11.844038583108542 112.22999999876397 11.844038584874689
+v 11.844038583934363 59.7499999987641 11.844038584874765
+v 15.474982167798482 112.22999999882106 -6.409947492115062
+v 15.474982168624365 59.749999998821245 -6.409947492114995
+v 9.30580140131215 112.229999998724 13.92711600606761
+v 9.30580140213799 59.74999999872415 13.927116006067694
+v 13.927116004302192 112.22999999879677 -9.305801403078172
+v 13.927116005128031 59.74999999879686 -9.30580140307811
+v 6.409947490348985 112.22999999867842 15.4749821695639
+v 6.409947491174829 59.74999999867859 15.474982169563983
+v 11.844038583109317 112.22999999876393 -11.844038584874683
+v 11.844038583935156 59.74999999876408 -11.844038584874621
+v 3.26776289200386 112.22999999862903 16.428153446754028
+v 3.2677628928297042 59.74999999862915 16.428153446754113
+v 9.305801401313076 112.22999999872395 -13.927116006067584
+v 9.30580140213891 59.74999999872413 -13.927116006067521
+v -1.7662634466120152e-09 112.22999999857758 16.74999999999977
+v -9.404362621978633e-10 59.749999998577714 16.749999999999854
+v 6.409947490350017 112.22999999867838 -15.474982169564077
+v 6.40994749117585 59.749999998678554 -15.47498216956401
+v -3.2677628955364435 112.22999999852615 16.428153446753814
+v -3.267762894710554 59.74999999852632 16.4281534467539
+v 3.267762892004954 112.22999999862904 -16.428153446754216
+v 3.2677628928307927 59.749999998629114 -16.428153446754155
+v -6.4099474938815115 112.22999999847679 15.474982169563477
+v -6.409947493055667 59.74999999847685 15.47498216956356
+v -1.7651580197508565e-09 112.22999999857754 -16.750000000000174
+v -9.393251954037395e-10 59.74999999857773 -16.75000000000012
+v -9.305801404844527 112.22999999843117 13.927116006066994
+v -9.305801404018682 59.749999998431264 13.92711600606708
+v -3.2677628955353493 112.22999999852617 -16.428153446754447
+v -3.2677628947095108 59.7499999985263 -16.428153446754376
+v -11.844038586640806 112.22999999839115 11.844038584873903
+v -11.844038585814973 59.749999998391324 11.844038584873976
+v -6.4099474938804795 112.22999999847674 -15.47498216956449
+v -6.409947493054647 59.749999998476824 -15.474982169564427
+v -13.927116007833678 112.2299999983584 9.305801403077572
+v -13.927116007007795 59.74999999835855 9.305801403077648
+v -9.305801404843596 112.22999999843113 -13.92711600606821
+v -9.305801404017762 59.74999999843126 -13.927116006068143
+v -15.474982171329978 112.2299999983341 6.409947492114458
+v -15.474982170504141 59.74999999833422 6.409947492114527
+v -11.844038586640016 112.22999999839111 -11.844038584875461
+v -11.844038585814184 59.749999998391324 -11.844038584875406
+v -16.42815344851997 112.2299999983191 3.2677628937692886
+v -16.428153447694083 59.749999998319204 3.2677628937693397
+v -13.927116007833074 112.2299999983584 -9.305801403079085
+v -13.927116007007191 59.749999998358525 -9.305801403079029
+v -16.7500000017657 112.22999999831396 -9.418688051709978e-13
+v -16.75000000093987 59.749999998314124 -9.023892744153272e-13
+v -15.474982171329545 112.22999999833404 -6.409947492116083
+v -15.474982170503706 59.74999999833416 -6.409947492116015
+v -16.42815344851973 112.2299999983191 -3.2677628937709695
+v -16.42815344769386 59.74999999831918 -3.267762893770919
+
+f 659 660 661 662
+f 660 663 664 661
+f 665 659 662 666
+f 663 667 668 664
+f 669 665 666 670
+f 667 671 672 668
+f 673 669 670 674
+f 671 675 676 672
+f 677 673 674 678
+f 675 679 680 676
+f 681 677 678 682
+f 679 683 684 680
+f 685 681 682 686
+f 683 687 688 684
+f 689 685 686 690
+f 687 691 692 688
+f 693 689 690 694
+f 691 695 696 692
+f 697 693 694 698
+f 695 699 700 696
+f 701 697 698 702
+f 699 703 704 700
+f 705 701 702 706
+f 703 707 708 704
+f 709 705 706 710
+f 707 711 712 708
+f 713 709 710 714
+f 711 715 716 712
+f 717 713 714 718
+f 715 719 720 716
+f 721 717 718 722
+f 719 721 722 720
+
+s off
+
+s 22
+
+v 24.02281298832787 73.51999999895546 6.260325591256332e-13
+v 23.561221372809463 73.5199999989483 -4.686618321851225
+v 23.56122137267746 81.90999999894849 -4.686618321851248
+v 24.022812988195874 81.9099999989556 6.147526931954417e-13
+v 22.194185233174704 73.5199999989267 -9.193132529880126
+v 22.194185233042703 81.90999999892686 -9.193132529880147
+v 23.561221372809154 73.51999999894828 4.686618321852297
+v 23.561221372677153 81.90999999894842 4.686618321852274
+v 19.974239001627073 73.51999999889179 -13.346359810353695
+v 19.974239001495068 81.90999999889193 -13.34635981035371
+v 22.194185233174057 73.51999999892675 9.193132529881073
+v 22.194185233042052 81.9099999989269 9.193132529881069
+v 16.98669396688474 73.51999999884478 -16.986693968040445
+v 16.986693966752735 81.90999999884492 -16.98669396804046
+v 19.97423900162614 73.51999999889185 13.346359810354672
+v 19.97423900149414 81.90999999889193 13.346359810354667
+v 13.346359809198331 73.5199999987875 -19.974239002783147
+v 13.346359809066328 81.90999999878761 -19.974239002783158
+v 16.98669396688357 73.5199999988448 16.98669396804103
+v 16.986693966751574 81.90999999884492 16.986693968041028
+v 9.193132528724732 73.51999999872214 -22.194185234331023
+v 9.193132528592729 81.90999999872224 -22.194185234331044
+v 13.34635980919692 73.51999999878757 19.97423900278333
+v 13.346359809064921 81.90999999878767 19.97423900278332
+v 4.686618320695941 73.51999999865126 -23.561221373966198
+v 4.686618320563916 81.9099999986514 -23.561221373966223
+v 9.193132528723174 73.51999999872214 22.1941852343311
+v 9.193132528591173 81.90999999872228 22.19418523433109
+v -1.1558196622019068e-09 73.51999999857749 -24.022812989484798
+v -1.2878222932499739e-09 81.90999999857763 -24.02281298948482
+v 4.686618320694294 73.5199999986513 23.561221373965815
+v 4.686618320562269 81.90999999865144 23.561221373965804
+v -4.686618323007581 73.5199999985037 -23.561221373966504
+v -4.686618323139651 81.90999999850382 -23.561221373966514
+v -1.157489082359575e-09 73.51999999857755 24.022812989484265
+v -1.2894917134076421e-09 81.90999999857767 24.02281298948426
+v -9.193132531036515 73.51999999843282 -22.19418523433162
+v -9.19313253116854 81.90999999843294 -22.19418523433163
+v -4.6866183230092275 73.51999999850375 23.561221373965505
+v -4.686618323141298 81.90999999850382 23.561221373965495
+v -13.346359811510224 73.51999999836741 -19.97423900278404
+v -13.34635981164225 81.90999999836755 -19.97423900278406
+v -9.193132531038026 73.51999999843284 22.194185234330675
+v -9.19313253117005 81.90999999843294 22.194185234330668
+v -16.986693969196878 73.51999999831017 -16.98669396804174
+v -16.98669396932892 81.9099999983103 -16.986693968041752
+v -13.346359811511634 73.51999999836745 19.974239002782628
+v -13.34635981164366 81.9099999983676 19.974239002782618
+v -19.974239003939502 73.51999999826319 -13.346359810355194
+v -19.97423900407149 81.90999999826329 -13.346359810355212
+v -16.9866939691981 73.5199999983102 16.986693968039912
+v -16.986693969330098 81.90999999831034 16.986693968039905
+v -22.194185235487456 73.51999999822823 -9.193132529881591
+v -22.194185235619443 81.90999999822836 -9.193132529881614
+v -19.97423900394043 73.51999999826319 13.346359810353352
+v -19.974239004072412 81.90999999826333 13.346359810353347
+v -23.56122137512252 73.51999999820669 -4.686618321852782
+v -23.56122137525452 81.90999999820686 -4.686618321852804
+v -22.194185235488067 73.51999999822824 9.193132529879426
+v -22.19418523562005 81.90999999822836 9.193132529879422
+v -24.022812990641278 73.51999999819952 -1.1449063919144464e-12
+v -24.02281299077326 81.9099999981997 -1.1505463248795421e-12
+v -23.56122137512287 73.51999999820669 4.68661832185074
+v -23.561221375254878 81.90999999820684 4.686618321850718
+
+f 723 724 725 726
+f 724 727 728 725
+f 729 723 726 730
+f 727 731 732 728
+f 733 729 730 734
+f 731 735 736 732
+f 737 733 734 738
+f 735 739 740 736
+f 741 737 738 742
+f 739 743 744 740
+f 745 741 742 746
+f 743 747 748 744
+f 749 745 746 750
+f 747 751 752 748
+f 753 749 750 754
+f 751 755 756 752
+f 757 753 754 758
+f 755 759 760 756
+f 761 757 758 762
+f 759 763 764 760
+f 765 761 762 766
+f 763 767 768 764
+f 769 765 766 770
+f 767 771 772 768
+f 773 769 770 774
+f 771 775 776 772
+f 777 773 774 778
+f 775 779 780 776
+f 781 777 778 782
+f 779 783 784 780
+f 785 781 782 786
+f 783 785 786 784
+
+s off
+
+s 23
+f 641 640 532 531
+f 640 639 533 532
+f 642 641 531 562
+f 639 638 534 533
+f 643 642 562 561
+f 638 637 535 534
+f 644 643 561 560
+f 637 636 536 535
+f 645 644 560 559
+f 636 635 537 536
+f 646 645 559 558
+f 635 634 538 537
+f 647 646 558 557
+f 634 633 539 538
+f 648 647 557 556
+f 633 632 540 539
+f 649 648 556 555
+f 632 631 541 540
+f 650 649 555 554
+f 631 630 542 541
+f 651 650 554 553
+f 630 629 543 542
+f 652 651 553 552
+f 629 628 544 543
+f 653 652 552 551
+f 628 627 545 544
+f 654 653 551 550
+f 627 658 546 545
+f 655 654 550 549
+f 658 657 547 546
+f 656 655 549 548
+f 657 656 548 547
+
+s off
+
+s 24
+f 604 603 588 587
+f 603 602 589 588
+f 605 604 587 586
+f 602 601 590 589
+f 606 605 586 585
+f 601 600 591 590
+f 607 606 585 584
+f 600 599 592 591
+f 608 607 584 583
+f 599 598 593 592
+f 609 608 583 582
+f 598 597 594 593
+f 610 609 582 581
+f 597 596 563 594
+f 611 610 581 580
+f 596 595 564 563
+f 612 611 580 579
+f 595 626 565 564
+f 613 612 579 578
+f 626 625 566 565
+f 614 613 578 577
+f 625 624 567 566
+f 615 614 577 576
+f 624 623 568 567
+f 616 615 576 575
+f 623 622 569 568
+f 617 616 575 574
+f 622 621 570 569
+f 618 617 574 573
+f 621 620 571 570
+f 619 618 573 572
+f 620 619 572 571
+
+s off
+
+o 0.4mm-Entity65811
+
+v -36.293669598964385 44.96289494572403 23.092028109287032
+v -36.249216385041805 46.01780626905352 23.117693184320682
+v -36.11585674323776 46.90150340411704 23.19468840942187
+v -35.8935906735524 47.61398635091458 23.323013784590515
+v -35.58241817598558 48.15525510944614 23.5026693098267
+v -35.180271659191696 48.553959065796334 23.734848709550626
+v -34.68508353182537 48.83874760604979 24.020745708182343
+v -34.09685379388653 49.0096207302065 24.36036030572191
+v -33.4155824453751 49.06657843826646 24.753692502169358
+v -32.77876431080131 49.016783076748645 25.121359623583075
+v -32.267183137756106 48.86739699217289 25.41672115154621
+v -31.85957227239505 48.63820190350271 25.65205539438057
+v -31.53466506087361 48.34897952970161 25.83964066040801
+v -31.281237435885263 48.021557974453266 25.98595716790942
+v -31.0880653301234 47.67776534144134 26.097485135165766
+v -30.825776599358637 46.856482940354105 26.248917604467923
+v -30.680454464738418 45.94243109861017 26.332819378000256
+v -30.63201375318809 44.990180075418586 26.360786635844406
+v -30.671888729130764 43.97449112589544 26.337764807741177
+v -30.791513656993683 43.10477761975438 26.268699323431115
+v -30.990888536776538 42.38103955699541 26.153590182914407
+v -31.270013368479628 41.80327693761852 25.99243738619086
+v -31.64661036363972 41.36159890214185 25.775009009659215
+v -32.13840173379434 41.046114591083544 25.491073129717734
+v -32.74538747894317 40.85682400444358 25.14062974636659
+v -33.46756759908639 40.79372714222198 24.723678859605698
+v -34.257978233650356 40.87421827454227 24.26733506697907
+v -34.88061859899743 41.11569167153098 23.907853484446495
+v -35.37093311820181 41.512690307266695 23.624770264805143
+v -35.76436621433815 42.05975715582796 23.39762156085191
+v -35.98707533933162 42.582949515988126 23.26904038759309
+v -36.154254868177546 43.268488397295144 23.172519241618925
+v -36.25881591626027 44.075446105342586 23.112150892370067
+
+f 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819
+
+v -29.649021753224446 43.15661936602088 26.928317331607445
+v -27.59324521489982 43.15661936605364 28.115220469380795
+v -27.593245214864464 40.9301527903374 28.115220469380983
+v -29.649021753189086 40.930152790304646 26.928317331607637
+
+f 820 821 822 823
+
+v -23.306832984404508 42.42537789272491 30.589981724462262
+v -26.813467907247812 42.42537789266905 28.56542510784662
+v -26.81346790727685 44.25348157616155 28.565425107846465
+v -23.30683298450998 49.066578438427506 30.58998172446169
+v -21.62913029231402 49.066578438454236 31.558603825403164
+v -21.629130292235935 44.149798083747555 31.558603825403587
+v -20.75956044613997 44.14979808376141 32.060650210116926
+v -20.759560446112584 42.4253778927655 32.060650210117075
+v -21.629130292208547 42.42537789275165 31.558603825403733
+v -21.6291302921848 40.93015279043241 31.55860382540386
+v -23.306832984380762 40.93015279040568 30.58998172446239
+
+f 824 825 826 827 828 829 830 831 832 833 834
+
+v -3.0978328785112534 46.82186493261031 42.257653375475684
+v -3.4792227165648333 46.85648294078977 42.03745784981368
+v -3.9792227165578558 46.85648294078191 42.90348325360215
+v -3.597832878504276 46.821864932602445 43.123678779264154
+
+f 835 836 837 838
+
+v -19.960879446234102 46.72551431842625 32.52176890042403
+v -18.165028677122972 46.72551431845485 33.558603825375435
+v -18.16502867710954 45.87967530071952 33.55860382537551
+v -17.7751492536151 46.332608452158205 33.783700815481446
+v -17.38174384844767 46.63274487781249 34.01083353205402
+v -16.9457497536777 46.80054842488629 34.262554840031385
+v -16.428104261375342 46.85648294058348 34.56141760435189
+v -15.889736704018432 46.79509139898223 34.87224425850852
+v -15.47679071455977 46.61091677415935 35.11065873667715
+v -15.158510871727609 46.30532332259552 35.294417689607734
+v -14.904141754249807 45.87967530077147 35.44127776805047
+v -14.471110591726113 46.35648294061466 35.691288426300694
+v -14.078212854076652 46.65184446858817 35.91812804089507
+v -13.661722422316053 46.805323322619365 36.15858890378387
+v -13.157913177458544 46.85648294063557 36.44946330691763
+v -12.776523339404962 46.821864932456116 36.66965883257964
+v -12.439379033123176 46.71801090790487 36.864309188562615
+v -12.146480258613261 46.54492086698184 37.0334143748665
+v -11.897827015875068 46.30259480968703 37.1769743914914
+v -11.699612848628206 45.986598902459626 37.291413394303575
+v -11.558031300591903 45.59249931173883 37.373155539169524
+v -11.473082371766468 45.120296037524625 37.422200826089046
+v -11.444766062151752 44.56998907981703 37.438549255062256
+v -11.444766062093946 40.930152790594654 37.43854925506257
+v -13.37294267735009 40.930152790563945 36.325315967220014
+v -13.372942677402605 44.23165347269219 36.32531596721969
+v -13.405913375122113 44.574081849226445 36.306280326019426
+v -13.504825468268475 44.815555246223504 36.24917340241851
+v -13.721461043678234 45.040657565456065 36.12409879464401
+v -13.98517124348155 45.11569167186388 35.971845639803036
+v -14.291294756831444 45.044750334887645 35.79510514696785
+v -14.531495335387834 44.83192632396977 35.65642527827946
+v -14.686924668755022 44.46221281782813 35.56668811081826
+v -14.738734446537466 43.92060299518065 35.53677572166455
+v -14.738734446489893 40.93015279054218 35.53677572166485
+v -16.666911061746116 40.930152790511464 34.42354243382225
+v -16.666911061796817 44.122512954222245 34.42354243382197
+v -16.704866131514045 44.640930416704585 34.40162906410874
+v -16.789046636319306 44.838065478094784 34.3530274270016
+v -16.91827109544781 44.9929085885975 34.278419650740666
+v -17.08305074147195 45.09318143989093 34.18328407775434
+v -17.273896806964608 45.12660572365323 34.07309905047073
+v -17.576245120044245 45.05430013019684 33.898538170527765
+v -17.819297867019873 44.83738334983826 33.75821160163475
+v -17.979351589959904 44.455391535374595 33.665804541608765
+v -18.03270283093289 43.887860839602936 33.63500218826683
+v -18.03270283088592 40.93015279048971 33.63500218826709
+v -19.960879446142066 40.93015279045899 32.521768900424526
+
+f 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886
+
+v -14.221461043671257 45.0406575654482 36.99012419843249
+v -14.485171243474573 45.115691671856005 36.837871043591505
+
+f 868 867 887 888
+
+v -10.282188985340316 46.72551431858044 38.10976344332012
+v -8.486338216229177 46.725514318609044 39.146598368271526
+v -8.486338216215744 45.879675300873714 39.146598368271604
+v -8.096458792721307 46.33260845231239 39.371695358377536
+v -7.703053387553957 46.63274487796668 39.59882807495006
+v -7.2670592927839825 46.800548425040475 39.85054938292743
+v -6.749413800481629 46.85648294073768 40.149412147247936
+v -6.211046243124638 46.79509139913643 40.460238801404614
+v -5.798100253666059 46.61091677431354 40.69865327957319
+v -5.479820410833819 46.305323322749715 40.882412232503825
+v -5.225451293356092 45.87967530092566 41.02927231094652
+v -4.792420130832401 46.356482940768856 41.27928296919674
+v -4.399522393182939 46.65184446874237 41.50612258379111
+v -3.983031961422259 46.80532332277356 41.74658344667996
+v -2.7606885722294634 46.718010908059064 42.452303731458656
+v -2.467789797719475 46.544920867136035 42.621408917762594
+v -2.219136554981272 46.30259480984122 42.7649689343875
+v -2.0209223877344096 45.986598902613814 42.879407937199666
+v -1.8793408396981914 45.592499311893015 42.961150082065565
+v -1.7943919108726796 45.12029603767882 43.01019536898514
+v -1.7660756012579586 44.569989079971215 43.02654379795835
+v -1.766075601200155 40.93015279074885 43.02654379795866
+v -3.6942522164563765 40.93015279071814 41.91331051011606
+v -3.694252216508811 44.231653472846375 41.91331051011578
+v -3.727222914228401 44.57408184938064 41.89427486891547
+v -3.8261350073746843 44.81555524637769 41.837167945314604
+v -4.042770582784441 45.04065756561026 41.712093337540104
+v -4.3064807825878395 45.11569167201806 41.55984018269908
+v -4.612604295937654 45.04475033504184 41.38309968986393
+v -4.852804874494043 44.83192632412396 41.24441982117555
+v -5.008234207861308 44.462212817982326 41.1546826537143
+v -5.060043985643673 43.920602995334846 41.12477026456064
+v -5.060043985596179 40.930152790696376 41.124770264560894
+v -6.988220600852327 40.93015279066565 40.01153697671833
+v -6.988220600903024 44.12251295437643 40.01153697671806
+v -7.026175670620249 44.64093041685878 39.98962360700483
+v -7.110356175425515 44.83806547824898 39.94102196989769
+v -7.239580634554014 44.99290858875169 39.86641419363676
+v -7.4043602805782385 45.093181440045115 39.771278620650385
+v -7.595206346070814 45.126605723807415 39.66109359336682
+v -7.897554659150532 45.05430013035104 39.486532713423806
+v -8.140607406126083 44.83738334999245 39.34620614453084
+v -8.300661129066112 44.45539153552878 39.253799084504855
+v -8.354012370039104 43.88786083975713 39.22299673116292
+v -8.354012369992128 40.930152790643895 39.22299673116317
+v -10.282188985248277 40.93015279061318 38.10976344332062
+
+f 889 890 891 892 893 894 895 896 897 898 899 900 901 902 836 835 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934
+
+v -15.40414175424283 45.87967530076361 36.30730317183894
+v -14.971110591719135 46.356482940606796 36.557313830089164
+
+f 850 849 935 936
+
+v -27.313467907240835 42.42537789266118 29.431450511635084
+v -23.806832984397527 42.42537789271705 31.456007128250732
+v -23.80683298437378 40.93015279039782 31.456007128250857
+v -22.12913029217782 40.93015279042454 32.42462922919233
+v -22.129130292201566 42.42537789274378 32.4246292291922
+v -21.259560446105603 42.42537789275764 32.926675613905545
+v -21.25956044613299 44.149798083753545 32.926675613905395
+v -22.129130292228954 44.14979808373969 32.42462922919205
+v -22.12913029230704 49.06657843844638 32.42462922919163
+v -23.806832984503 49.06657843841964 31.456007128250157
+v -27.313467907269864 44.253481576153675 29.43145051163493
+
+f 937 938 939 940 941 942 943 944 945 946 947
+
+v -20.46087944613509 40.93015279045112 33.387794304213
+v -20.460879446227125 46.725514318418384 33.387794304212505
+
+f 839 886 948 949
+f 825 824 938 937
+
+v -18.665028677102562 45.879675300711654 34.42462922916398
+v -18.275149253608124 46.33260845215034 34.64972621926992
+
+f 842 841 950 951
+f 826 825 937 947
+
+v -12.39782701586809 46.30259480967916 38.042999795279876
+v -12.199612848621229 45.98659890245176 38.157438798092045
+
+f 858 857 952 953
+f 827 826 947 946
+
+v -17.289046636312328 44.83806547808691 35.21905283079007
+v -17.418271095440833 44.992908588589636 35.144445054529136
+
+f 878 877 954 955
+f 828 827 946 945
+
+v -7.249413800474652 46.856482940729805 41.015437551036406
+v -6.711046243117661 46.79509139912856 41.326264205193084
+
+f 896 895 956 957
+f 829 828 945 944
+
+v -2.2660756011931773 40.93015279074098 43.892569201747136
+v -4.1942522164493985 40.93015279071027 42.77933591390453
+
+f 911 910 958 959
+f 830 829 944 943
+
+v -16.928104261368365 46.85648294057562 35.427443008140365
+v -16.389736704011455 46.79509139897437 35.73826966229699
+
+f 846 845 960 961
+f 831 830 943 942
+
+v -13.657913177451567 46.85648294062771 37.3154887107061
+v -13.276523339397984 46.82186493244825 37.53568423636811
+
+f 854 853 962 963
+f 832 831 942 941
+
+v -11.944766062086968 40.93015279058679 38.30457465885104
+v -13.872942677343113 40.93015279055608 37.19134137100849
+
+f 863 862 964 965
+f 833 832 941 940
+
+v -7.526175670613272 44.64093041685091 40.8556490107933
+v -7.610356175418538 44.83806547824111 40.80704737368616
+
+f 925 924 966 967
+f 834 833 940 939
+
+v -18.076245120037267 45.05430013018898 34.764563574316234
+v -18.319297867012896 44.83738334983039 34.62423700542322
+
+f 882 881 968 969
+f 824 834 939 938
+
+v -8.986338216208766 45.87967530086585 40.012623772060074
+v -8.59645879271433 46.33260845230453 40.23772076216601
+
+f 892 891 970 971
+
+v -23.30683298447188 46.66744815193365 30.5899817244619
+v -23.306832984431896 44.14979808372082 30.589981724462113
+v -23.806832984424915 44.14979808371296 31.45600712825058
+v -23.806832984464897 46.66744815192578 31.456007128250366
+
+f 972 973 974 975
+
+v -5.725451293349114 45.879675300917796 41.89529771473499
+v -5.292420130825423 46.35648294076098 42.14530837298521
+
+f 900 899 976 977
+
+v -25.159690200654634 44.14979808369131 29.520234111925888
+v -25.659690200647653 44.14979808368344 30.386259515714354
+
+f 978 972 975 979
+
+v -2.7191365549742943 46.30259480983335 43.63099433817597
+v -2.520922387727432 45.986598902605955 43.74543334098814
+
+f 906 905 980 981
+f 973 978 979 974
+
+v -18.665028677115995 46.72551431844698 34.42462922916391
+
+f 840 839 949 982
+
+v -28.093245214892843 43.15661936604577 28.98124587316926
+v -30.149021753217465 43.15661936601301 27.79434273539591
+v -30.149021753182108 40.93015279029678 27.794342735396103
+v -28.09324521485748 40.93015279032954 28.981245873169453
+
+f 983 984 985 986
+
+v -17.881743848440692 46.632744877804626 34.87685893584249
+v -17.44574975367072 46.80054842487842 35.128580243819854
+
+f 844 843 987 988
+f 821 820 984 983
+
+v -15.976790714552793 46.610916774151484 35.97668414046562
+v -15.658510871720631 46.305323322587654 36.1604430933962
+
+f 848 847 989 990
+f 822 821 983 986
+
+v -14.578212854069674 46.65184446858031 36.78415344468354
+v -14.161722422309076 46.8053233226115 37.02461430757235
+
+f 852 851 991 992
+f 823 822 986 985
+
+v -12.939379033116198 46.718010907897 37.730334592351085
+v -12.646480258606283 46.544920866973975 37.89943977865498
+
+f 856 855 993 994
+f 820 823 985 984
+
+v -12.058031300584926 45.592499311730954 38.239180942957994
+v -11.97308237175949 45.12029603751676 38.28822622987752
+
+f 860 859 995 996
+
+v -36.74921638503482 46.01780626904566 23.98371858810915
+v -36.79366959895741 44.962894945716165 23.958053513075498
+v -36.75881591625329 44.07544610533472 23.978176296158534
+v -36.65425486817056 43.26848839728728 24.03854464540739
+v -36.48707533932464 42.58294951598026 24.135065791381557
+v -36.264366214331176 42.059757155820094 24.26364696464038
+v -35.87093311819483 41.51269030725883 24.49079566859361
+v -35.38061859899045 41.115691671523116 24.77387888823496
+v -34.75797823364338 40.87421827453441 25.133360470767535
+v -33.9675675990794 40.79372714221412 25.589704263394164
+v -33.245387478936195 40.85682400443572 26.006655150155055
+v -32.63840173378736 41.04611459107567 26.3570985335062
+v -32.14661036363274 41.361598902133984 26.64103441344768
+v -31.77001336847265 41.803276937610654 26.858462789979328
+v -31.490888536769553 42.381039556987545 27.01961558670287
+v -31.291513656986705 43.10477761974652 27.13472472721958
+v -31.171888729123786 43.97449112588758 27.203790211529643
+v -31.132013753181113 44.99018007541072 27.22681203963287
+v -31.18045446473144 45.942431098602306 27.198844781788722
+v -31.325776599351652 46.85648294034624 27.11494300825639
+v -31.588065330116414 47.67776534143347 26.963510538954232
+v -31.781237435878285 48.02155797444541 26.85198257169789
+v -32.03466506086664 48.34897952969374 26.705666064196475
+v -32.35957227238807 48.638201903494846 26.518080798169038
+v -32.76718313774913 48.86739699216503 26.282746555334683
+v -33.27876431079433 49.01678307674078 25.98738502737154
+v -33.915582445368116 49.0665784382586 25.619717905957824
+v -34.59685379387955 49.009620730198634 25.226385709510375
+v -35.185083531818385 48.83874760604192 24.88677111197081
+v -35.68027165918471 48.55395906578847 24.60087411333909
+v -36.0824181759786 48.15525510943827 24.36869471361517
+v -36.39359067354542 47.613986350906714 24.18903918837898
+v -36.61585674323078 46.90150340410917 24.060713813210338
+
+f 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029
+
+v -13.905913375115135 44.57408184921858 37.172305729807896
+v -14.004825468261497 44.81555524621564 37.11519880620699
+
+f 866 865 1030 1031
+
+v -8.854012369985151 40.93015279063603 40.08902213495165
+v -10.7821889852413 40.93015279060531 38.975788847109094
+
+f 934 933 1032 1033
+
+v -14.791294756824467 45.04475033487979 36.66113055075632
+v -15.031495335380857 44.8319263239619 36.522450682067934
+
+f 870 869 1034 1035
+f 791 790 1028 1027
+
+v -5.560043985589202 40.9301527906885 41.990795668349364
+v -7.48822060084535 40.93015279065779 40.87756238050681
+
+f 922 921 1036 1037
+f 792 791 1027 1026
+
+v -17.583050741464973 45.093181439883054 35.049309481542814
+v -17.77389680695763 45.12660572364536 34.9391244542592
+
+f 880 879 1038 1039
+f 793 792 1026 1025
+
+v -5.352804874487066 44.8319263241161 42.110445224964025
+v -5.50823420785433 44.46221281797446 42.02070805750277
+
+f 919 918 1040 1041
+f 794 793 1025 1024
+
+v -10.782188985333338 46.72551431857257 38.97578884710859
+v -8.9863382162222 46.72551431860118 40.012623772059996
+
+f 890 889 1042 1043
+f 795 794 1024 1023
+
+v -8.20305338754698 46.63274487795882 40.46485347873853
+v -7.767059292777005 46.80054842503261 40.7165747867159
+
+f 894 893 1044 1045
+f 796 795 1023 1022
+
+v -6.298100253659082 46.61091677430567 41.56467868336166
+v -5.979820410826841 46.30532332274184 41.748437636292294
+
+f 898 897 1046 1047
+f 797 796 1022 1021
+
+v -4.899522393175961 46.6518444687345 42.37214798757959
+v -4.483031961415282 46.805323322765695 42.61260885046843
+
+f 902 901 1048 1049
+f 798 797 1021 1020
+
+v -3.260688572222486 46.71801090805119 43.318329135247126
+v -2.9677897977124976 46.54492086712816 43.48743432155106
+
+f 904 903 1050 1051
+f 799 798 1020 1019
+
+v -2.379340839691214 45.59249931188515 43.827175485854035
+v -2.294391910865702 45.120296037670954 43.87622077277361
+
+f 908 907 1052 1053
+f 800 799 1019 1018
+
+v -4.227222914221423 44.57408184937277 42.76030027270394
+v -4.326135007367706 44.815555246369826 42.703193349103074
+
+f 914 913 1054 1055
+f 801 800 1018 1017
+f 841 840 982 950
+
+v -8.640607406119106 44.837383349984584 40.21223154831931
+v -8.800661129059135 44.45539153552092 40.11982448829333
+
+f 931 930 1056 1057
+f 843 842 951 987
+f 808 807 1011 1010
+f 845 844 988 960
+f 809 808 1010 1009
+
+v -4.542770582777464 45.0406575656024 42.57811874132857
+v -4.806480782580862 45.1156916720102 42.42586558648755
+
+f 916 915 1058 1059
+f 810 809 1009 1008
+f 849 848 990 935
+f 811 810 1008 1007
+f 851 850 936 991
+
+v -8.397554659143553 45.054300130343165 40.352558117212276
+
+f 930 929 1060 1056
+f 853 852 992 962
+f 815 814 1004 1003
+f 855 854 963 993
+f 816 815 1003 1002
+f 857 856 994 952
+f 817 816 1002 1001
+f 859 858 953 995
+f 818 817 1001 1000
+
+v -7.739580634547036 44.99290858874383 40.732439597425234
+v -7.904360280571261 45.09318144003725 40.637304024438855
+
+f 927 926 1061 1062
+
+v -8.095206346063836 45.12660572379955 40.52711899715529
+
+f 929 928 1063 1060
+f 926 925 967 1061
+
+v -34.25280925532251 43.210336964856126 24.270319378029253
+v -34.3259133779211 43.671626187228775 24.22811269317306
+v -34.82591337791411 43.67162618722091 25.094138096961526
+v -34.752809255315526 43.21033696484827 25.136344781817716
+
+f 1064 1065 1066 1067
+f 867 866 1031 887
+
+v -34.150463483689336 42.870125505103324 24.329408736827894
+v -34.65046348368236 42.870125505095466 25.19543414061636
+
+f 1068 1064 1067 1069
+f 869 868 888 1034
+
+v -34.0203529139825 42.62541199896624 24.40452810926893
+v -34.52035291397552 42.62541199895838 25.270553513057397
+
+f 1070 1068 1069 1071
+
+v -15.186924668748045 44.462212817820266 36.43271351460673
+
+f 871 870 1035 1072
+
+v -33.86395439716323 42.450616637440774 24.49482483505212
+v -34.363954397156256 42.4506166374329 25.360850238840587
+
+f 1073 1070 1071 1074
+
+v -15.238734446482916 40.930152790534315 36.40280112545332
+v -17.16691106173914 40.93015279050359 35.28956783761072
+
+f 874 873 1075 1076
+
+v -33.68126793323145 42.345739420526904 24.600298914177518
+v -34.181267933224476 42.34573942051904 25.466324317965988
+
+f 1077 1073 1074 1078
+
+v -17.204866131507067 44.64093041669672 35.26765446789721
+
+f 877 876 1079 954
+
+v -33.472293522187165 42.31078034822463 24.72095034664512
+v -33.97229352218019 42.31078034821677 25.58697575043359
+
+f 1080 1077 1078 1081
+f 879 878 955 1038
+
+v -33.19878072412928 42.370125505118494 24.878863034228253
+v -33.6987807241223 42.37012550511062 25.74488843801672
+
+f 1082 1080 1081 1083
+f 881 880 1039 968
+
+v -32.96425679145549 42.54816097579071 25.014265489887972
+v -33.464256791448506 42.54816097578284 25.880290893676438
+
+f 1084 1082 1083 1085
+
+v -18.479351589952927 44.45539153536673 34.53182994539724
+
+f 883 882 969 1086
+
+v -32.77758282993437 42.85443655560269 25.12204175182366
+v -33.27758282992738 42.85443655559482 25.98806715561213
+
+f 1087 1084 1085 1088
+
+v -18.532702830878943 40.93015279048184 34.50102759205556
+
+f 886 885 1089 948
+
+v -32.64761994533464 43.29850203991581 25.197075858234612
+v -33.147619945327655 43.29850203990795 26.06310126202308
+
+f 1090 1087 1088 1091
+
+v -8.854012370032127 43.887860839749266 40.08902213495139
+v -7.488220600896047 44.12251295436857 40.87756238050654
+v -5.560043985636695 43.92060299532698 41.990795668349115
+v -5.112604295930677 45.044750335033974 42.2491250936524
+v -4.194252216501833 44.23165347283851 42.77933591390425
+v -2.2660756012509813 44.56998907996336 43.89256920174682
+
+f 1043 1042 1033 1032 1092 1057 1056 1060 1063 1062 1061 967 966 1093 1037 1036 1094 1041 1040 1095 1059 1058 1055 1054 1096 959 958 1097 1053 1052 981 980 1051 1050 838 837 1049 1048 977 976 1047 1046 957 956 1045 1044 971 970
+
+v -32.57141443573475 43.95470940690202 25.24107312972098
+v -33.07141443572777 43.95470940689415 26.107098533509447
+
+f 1098 1090 1091 1099
+f 891 890 1043 970
+f 928 927 1062 1063
+f 893 892 971 1044
+
+v -32.6776000199059 46.699763977159535 25.179766854141786
+v -32.60449589730709 46.22960708766547 25.22197353899802
+v -33.10449589730011 46.22960708765761 26.08799894278649
+v -33.17760001989892 46.69976397715167 26.045792257930252
+
+f 1100 1101 1102 1103
+f 895 894 1045 956
+
+v -32.779945791539134 47.03929330867223 25.1206774953431
+v -33.27994579153216 47.03929330866436 25.986702899131565
+
+f 1104 1100 1103 1105
+f 897 896 957 1046
+
+v -32.91153321220722 47.27923191712852 25.044705462602007
+v -33.41153321220023 47.279231917120654 25.91073086639047
+
+f 1106 1104 1105 1107
+f 899 898 1047 976
+
+v -33.0723622819108 47.45061663745338 24.951850755918414
+v -33.572362281903814 47.45061663744551 25.81787615970688
+
+f 1108 1106 1107 1109
+f 901 900 977 1048
+
+v -33.26243300064971 47.553447469646805 24.84211337529241
+v -33.76243300064273 47.55344746963893 25.70813877908088
+
+f 1110 1108 1109 1111
+f 836 902 1049 837
+
+v -33.48174536842412 47.5877244137088 24.715493320723912
+v -33.981745368417144 47.58772441370093 25.581518724512378
+
+f 1112 1110 1111 1113
+f 903 835 838 1050
+
+v -33.70386375302415 47.55276534139966 24.58725321158519
+v -34.20386375301717 47.5527653413918 25.45327861537365
+
+f 1114 1112 1113 1115
+f 905 904 1051 980
+
+v -33.89290067608685 47.447888124479874 24.478112693169233
+v -34.392900676079876 47.447888124472 25.3441380969577
+
+f 1116 1114 1115 1117
+f 907 906 981 1052
+
+v -34.04885613761208 47.273092762949425 24.38807176547614
+v -34.5488561376051 47.27309276294156 25.25409716926461
+
+f 1118 1116 1117 1119
+f 918 917 1095 1040
+
+v -34.17173013760006 47.028379256808314 24.317130428505774
+v -34.67173013759308 47.02837925680045 25.18315583229424
+
+f 1120 1118 1119 1121
+f 917 916 1059 1095
+
+v -18.532702830925913 43.88786083959507 34.50102759205531
+v -17.16691106178984 44.12251295421437 35.28956783761044
+v -15.238734446530488 43.920602995172786 36.402801125453024
+v -13.872942677395628 44.231653472684314 37.19134137100816
+v -11.944766062144774 44.56998907980916 38.30457465885073
+
+f 982 949 948 1089 1122 1086 969 968 1039 1038 955 954 1079 1123 1076 1075 1124 1072 1035 1034 888 887 1031 1030 1125 965 964 1126 996 995 953 952 994 993 963 962 992 991 936 935 990 989 961 960 988 987 951 950
+f 915 914 1055 1058
+f 847 846 961 989
+f 889 934 1033 1042
+
+s 25
+f 861 860 996 1126
+f 862 861 1126 964
+
+s off
+
+s 26
+f 884 883 1086 1122
+f 885 884 1122 1089
+
+s off
+
+s 27
+f 920 919 1041 1094
+f 921 920 1094 1036
+
+s off
+
+s 28
+f 875 874 1076 1123
+f 876 875 1123 1079
+
+s off
+
+s 29
+f 923 922 1037 1093
+f 924 923 1093 966
+
+s off
+
+s 30
+f 872 871 1072 1124
+f 873 872 1124 1075
+
+s off
+
+s 31
+f 864 863 965 1125
+f 865 864 1125 1030
+
+s off
+
+s 32
+f 909 908 1053 1097
+f 910 909 1097 958
+
+s off
+
+s 33
+
+v -32.546012599213 44.89741063473325 25.255738886883012
+v -33.04601259920602 44.89741063472538 26.121764290671475
+
+f 1127 1098 1099 1128
+f 1101 1127 1128 1102
+
+s off
+
+s 34
+
+v -34.384396676014525 44.95743791983357 24.19434734528803
+v -34.88439667600755 44.9574379198257 25.060372749076496
+v -34.33123004142293 46.23097134411818 24.22504311609239
+v -34.83123004141595 46.230971344110316 25.091068519880857
+
+f 1065 1129 1130 1066
+f 1129 1131 1132 1130
+f 1131 1120 1121 1132
+
+s off
+
+s 35
+f 812 811 1007 1006
+f 813 812 1006 1005
+f 814 813 1005 1004
+
+s off
+
+s 36
+f 802 801 1017 1016
+f 803 802 1016 1015
+f 804 803 1015 1014
+f 805 804 1014 1013
+f 806 805 1013 1012
+f 807 806 1012 1011
+
+s off
+
+s 37
+f 932 931 1057 1092
+f 933 932 1092 1032
+
+s off
+
+s 38
+f 788 787 998 997
+f 787 819 999 998
+f 789 788 997 1029
+f 819 818 1000 999
+f 790 789 1029 1028
+
+s off
+
+s 39
+f 912 911 959 1096
+f 913 912 1096 1054
+
+s off
+
+o 0.4mm-Entity65815
+
+v -1.8514481705170156 44.962894946265884 -42.97725392323342
+v -1.8959013844715482 46.017806269593976 -42.95158884819761
+v -2.0292610262996695 46.90150340465329 -42.87459362308995
+v -2.2515270960012232 47.613986351443835 -42.74626824791053
+v -2.562699593576366 48.1552551099656 -42.56661272265926
+v -2.9648461103715436 48.55395906630315 -42.334433322915835
+v -3.4600342377329674 48.83874760654102 -42.048536324260105
+v -4.048263975660716 49.009620730679224 -41.70892172669202
+v -4.729535324154868 49.06657843871775 -41.31558953021154
+v -5.3663534587092645 49.016783077179895 -40.94792240876694
+v -5.877934631735445 48.867396992588034 -40.652560880779
+v -6.28554549707788 48.63820190390503 -40.41722663792487
+v -6.6104527085811196 48.34897953009371 -40.22964137188169
+v -6.863880333552069 48.021557974837386 -40.08332486436799
+v -7.057052439297711 47.67776534181938 -39.97179689710227
+v -7.3193411700292845 46.85648294072389 -39.820364427787396
+v -7.464663304616669 45.94243109897539 -39.73646265424802
+v -7.513104016135676 44.99018007578228 -39.708495396401524
+v -7.473229040162155 43.97449112626039 -39.73151722450669
+v -7.3536041122752165 43.104777620123095 -39.80058270882255
+v -7.1542292324751715 42.38103955737039 -39.915691849348924
+v -6.87510440076171 41.803276938002284 -40.076844646086
+v -6.498507405598263 41.36159890253747 -40.294273022635906
+v -6.006716035447484 41.04611459149463 -40.57820890260123
+v -5.399730290309684 40.85682400487378 -40.92865228598181
+v -4.677550170184708 40.793727142674896 -41.345603172777714
+v -3.887139535645394 40.87421827502007 -41.80194696544267
+v -3.264499170323349 41.11569167202837 -42.16142854800544
+v -2.7741846511451924 41.51269030777951 -42.44451176767057
+v -2.380751555037077 42.05975715635315 -42.671660471642866
+v -2.158042430066305 42.58294951652032 -42.80024164491249
+v -1.990862901246635 43.268488397832606 -42.896762790894755
+v -1.8863018531922295 44.075446105883344 -42.95713114014869
+
+f 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165
+
+v -8.496096016014107 43.15661936635364 -39.14096470059082
+v -10.551872554281179 43.1566193663217 -37.95406156271779
+v -10.551872554246476 40.93015279060546 -37.9540615627176
+v -8.496096015979402 40.9301527906374 -39.14096470059063
+
+f 1166 1167 1168 1169
+
+v -14.83828478463349 42.425377892858094 -35.47930030742849
+v -11.331649861888344 42.42537789291258 -37.503856924214155
+v -11.331649861916839 44.25348157640507 -37.50385692421431
+v -14.838284784737006 49.06657843856068 -35.47930030742906
+v -16.515987476886 49.06657843853463 -34.51067820640624
+v -16.51598747680936 44.14979808382794 -34.51067820640582
+v -17.385557322880985 44.149798083814424 -34.00863182165031
+v -17.38555732285411 42.425377892818524 -34.00863182165016
+v -16.515987476782485 42.42537789283203 -34.51067820640567
+v -16.51598747675918 40.93015279051279 -34.51067820640554
+v -14.838284784610185 40.93015279053886 -35.47930030742835
+
+f 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180
+
+v -35.04728489009937 46.821864932107545 -23.811628655435182
+v -34.66589505205755 46.85648294029901 -24.031824181115688
+v -35.16589505209256 46.856482940291144 -24.897849584879918
+v -35.54728489013438 46.82186493209968 -24.677654059199412
+
+f 1181 1182 1183 1184
+
+v -18.18423832284554 46.72551431845413 -33.547513131304484
+v -19.9800890919064 46.72551431842623 -32.510678206266
+v -19.98008909189322 45.8796753006909 -32.51067820626592
+v -20.369968515390994 46.332608452117306 -32.28558121614108
+v -20.76337392055686 46.632744877759215 -32.058448499549435
+v -21.1993680153199 46.80054842481929 -31.806727191550934
+v -21.71701350760953 46.8564829405002 -31.507864427205327
+v -22.255381064949436 46.795091398882015 -31.19703777302259
+v -22.66832705439074 46.610916774046125 -30.958623294833945
+v -22.986606897204375 46.30532332247228 -30.774864341887927
+v -23.240976014661666 45.87967530064023 -30.628004263432853
+v -23.67400717718824 46.356482940469796 -30.37799360516163
+v -24.066904914835998 46.651844468430944 -30.151153990548213
+v -24.48339534658977 46.80532332244903 -29.910693127639206
+v -24.987204591434782 46.85648294044938 -29.61981872448103
+v -25.368594429476595 46.821864932257924 -29.39962319880052
+v -25.705738735745676 46.71801090769607 -29.2049728428012
+v -25.99863751024194 46.544920866763825 -29.03586765648311
+v -26.24729075296555 46.30259480946119 -28.892307639846152
+v -26.445504920196925 45.98659890222755 -28.777868637024365
+v -26.58708646821686 45.59249931150229 -28.696126492151564
+v -26.672035397025052 45.12029603728542 -28.647081205227913
+v -26.700351706621667 44.56998907957693 -28.630732776253325
+v -26.70035170656493 40.93015279035456 -28.630732776253016
+v -24.77217509136276 40.93015279038452 -29.743966064189063
+v -24.772175091414145 44.231653472512754 -29.743966064189394
+v -24.73920439370633 44.574081849048056 -29.763001705391254
+v -24.640292300570337 44.815555246048234 -29.82010862899696
+v -24.42365672517373 45.04065756528761 -29.945183236781965
+v -24.159946525380153 45.115691671703715 -30.097436391635732
+v -23.8538230120366 45.044750334737124 -30.27417688448576
+v -23.613622433480234 44.8319263238268 -30.412856753185793
+v -23.458193100105763 44.46221281769006 -30.50259392065453
+v -23.406383322307725 43.920602995044206 -30.532506309810753
+v -23.406383322261192 40.930152790405735 -30.532506309810454
+v -21.478206707058945 40.93015279043569 -31.645739597746548
+v -21.478206707108704 44.12251295414647 -31.64573959774682
+v -21.44025163740886 44.640930416630006 -31.667652967461898
+v -21.356071132612154 44.838065478022855 -31.716254604573123
+v -21.22684667349214 44.99290858852964 -31.790862380840316
+v -21.062067027475763 45.09318143982825 -31.885997953834636
+v -20.871220961989504 45.126605723596555 -31.996182981127497
+v -20.568872648916056 45.05430013014969 -32.17074386108513
+v -20.325819901940406 44.837383349798756 -32.311070429989925
+v -20.165766178992836 44.45539153534013 -32.40347749002366
+v -20.11241493800348 43.887860839570145 -32.434279843368195
+v -20.11241493795738 40.930152790456916 -32.43427984336793
+v -18.18423832275521 40.93015279048687 -33.54751313130399
+
+f 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232
+
+v -24.92365672520874 45.04065756527974 -30.81120864054619
+v -24.659946525415165 45.11569167169585 -30.963461795399958
+
+f 1214 1213 1233 1234
+
+v -27.862928783468387 46.72551431830375 -27.9595185879391
+v -29.658779552529243 46.72551431827585 -26.922683662900614
+v -29.658779552516062 45.87967530054052 -26.92268366290054
+v -30.048658976013844 46.332608451966934 -26.6975866727757
+v -30.442064381179627 46.63274487760884 -26.470453956184098
+v -30.87805847594267 46.80054842466891 -26.218732648185593
+v -31.395703968232294 46.85648294034982 -25.919869883839986
+v -31.934071525572286 46.795091398731635 -25.609043229657207
+v -32.34701751501351 46.610916773895745 -25.370628751468605
+v -32.66529735782722 46.305323322321904 -25.18686979852254
+v -32.91966647528443 45.87967530048985 -25.040009720067513
+v -33.352697637811005 46.35648294031941 -24.78999906179629
+v -33.74559537545876 46.651844468280565 -24.563159447182873
+v -34.16208580721261 46.805323322298655 -24.322698584273823
+v -35.384429196368444 46.71801090754569 -23.616978299435864
+v -35.67732797086479 46.544920866613445 -23.44787311311773
+v -35.9259812135884 46.302594809310804 -23.304313096480772
+v -36.12419538081977 45.98659890207716 -23.189874093658986
+v -36.26577692883963 45.59249931135191 -23.108131948786223
+v -36.3507258576479 45.120296037135034 -23.05908666186253
+v -36.379042167244506 44.56998907942655 -23.042738232887945
+v -36.37904216718778 40.930152790204176 -23.042738232887633
+v -34.45086555198553 40.93015279023414 -24.155971520823726
+v -34.45086555203699 44.231653472362375 -24.155971520824014
+v -34.417894854329106 44.57408184889768 -24.175007162025917
+v -34.31898276119318 44.81555524589785 -24.232114085631576
+v -34.102347185796575 45.04065756513724 -24.35718869341658
+v -33.83863698600292 45.115691671553336 -24.509441848270395
+v -33.53251347265944 45.044750334586745 -24.68618234112038
+v -33.29231289410308 44.83192632367643 -24.824862209820413
+v -33.13688356072853 44.46221281753968 -24.914599377289193
+v -33.08507378293057 43.920602994893834 -24.94451176644537
+v -33.08507378288396 40.930152790255356 -24.944511766445114
+v -31.156897167681787 40.93015279028532 -26.057745054381165
+v -31.15689716773155 44.1225129539961 -26.05774505438144
+v -31.118942098031702 44.640930416479634 -26.07965842409651
+v -31.034761593234997 44.83806547787248 -26.128260061207733
+v -30.905537134114986 44.992908588379265 -26.202867837474937
+v -30.740757488098534 45.09318143967787 -26.298003410469295
+v -30.549911422612354 45.126605723446175 -26.40818843776211
+v -30.247563109538824 45.05430012999931 -26.582749317719788
+v -30.00451036256325 44.83738334964838 -26.723075886624546
+v -29.844456639615686 44.45539153518975 -26.815482946658285
+v -29.791105398626325 43.88786083941977 -26.846285300002805
+v -29.791105398580225 40.930152790306536 -26.846285300002553
+v -27.862928783378056 40.93015279033649 -27.959518587938604
+
+f 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1182 1181 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280
+
+v -23.740976014696678 45.879675300632364 -31.49402966719708
+v -24.174007177223253 46.35648294046193 -31.244019008925854
+
+f 1196 1195 1281 1282
+
+v -11.831649861923355 42.425377892904706 -38.36988232797838
+v -15.3382847846685 42.42537789285023 -36.34532571119271
+v -15.338284784645193 40.93015279053099 -36.34532571119258
+v -17.015987476794187 40.93015279050493 -35.37670361016976
+v -17.015987476817493 42.425377892824166 -35.3767036101699
+v -17.885557322889117 42.42537789281065 -34.87465722541438
+v -17.885557322915993 44.14979808380656 -34.87465722541454
+v -17.015987476844373 44.14979808382007 -35.37670361017004
+v -17.01598747692101 49.066578438526754 -35.376703610170466
+v -15.338284784772014 49.06657843855282 -36.34532571119328
+v -11.83164986195185 44.253481576397206 -38.36988232797854
+
+f 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293
+
+v -18.684238322790222 40.930152790479006 -34.41353853506821
+v -18.684238322880553 46.725514318446265 -34.41353853506871
+
+f 1185 1232 1294 1295
+f 1171 1170 1284 1283
+
+v -20.480089091928228 45.879675300683026 -33.37670361003015
+v -20.869968515426006 46.33260845210944 -33.15160661990531
+
+f 1188 1187 1296 1297
+f 1172 1171 1283 1293
+
+v -26.747290753000563 46.30259480945332 -29.75833304361038
+v -26.945504920231937 45.98659890221968 -29.6438940407886
+
+f 1204 1203 1298 1299
+f 1173 1172 1293 1292
+
+v -21.856071132647166 44.83806547801499 -32.58228000833735
+v -21.726846673527152 44.99290858852177 -32.65688778460454
+
+f 1224 1223 1300 1301
+f 1174 1173 1292 1291
+
+v -31.895703968267306 46.856482940341955 -26.785895287604216
+v -32.4340715256073 46.79509139872376 -26.475068633421436
+
+f 1242 1241 1302 1303
+f 1175 1174 1291 1290
+
+v -36.879042167222785 40.93015279019631 -23.908763636651862
+v -34.95086555202054 40.93015279022627 -25.021996924587956
+
+f 1257 1256 1304 1305
+f 1176 1175 1290 1289
+
+v -22.21701350764454 46.856482940492334 -32.37388983096955
+v -22.755381064984448 46.79509139887414 -32.06306317678682
+
+f 1192 1191 1306 1307
+f 1177 1176 1289 1288
+
+v -25.487204591469794 46.856482940441516 -30.485844128245255
+v -25.86859442951161 46.82186493225005 -30.265648602564745
+
+f 1200 1199 1308 1309
+f 1178 1177 1288 1287
+
+v -27.200351706599943 40.93015279034669 -29.496758180017242
+v -25.272175091397774 40.93015279037665 -30.609991467953293
+
+f 1209 1208 1310 1311
+f 1179 1178 1287 1286
+
+v -31.618942098066714 44.64093041647176 -26.945683827860744
+v -31.53476159327001 44.83806547786462 -26.994285464971966
+
+f 1271 1270 1312 1313
+f 1180 1179 1286 1285
+
+v -21.068872648951068 45.05430013014182 -33.036769264849354
+v -20.825819901975418 44.83738334979088 -33.17709583375416
+
+f 1228 1227 1314 1315
+f 1170 1180 1285 1284
+
+v -30.158779552551074 45.87967530053265 -27.788709066664772
+v -30.548658976048856 46.33260845195906 -27.563612076539933
+
+f 1238 1237 1316 1317
+
+v -14.83828478469961 46.667448152066825 -35.47930030742885
+v -14.838284784660368 44.149798083854 -35.47930030742864
+v -15.338284784695377 44.149798083846136 -36.34532571119286
+v -15.33828478473462 46.66744815205896 -36.34532571119307
+
+f 1318 1319 1320 1321
+
+v -33.419666475319445 45.87967530048198 -25.90603512383174
+v -33.85269763784602 46.356482940311544 -25.65602446556052
+
+f 1246 1245 1322 1323
+
+v -12.985427568489493 44.14979808388279 -36.549047920054704
+v -13.485427568524505 44.14979808387493 -37.41507332381893
+
+f 1324 1318 1321 1325
+
+v -36.42598121362341 46.30259480930294 -24.170338500245002
+v -36.62419538085478 45.9865989020693 -24.055899497423216
+
+f 1252 1251 1326 1327
+f 1319 1324 1325 1320
+
+v -20.480089091941412 46.725514318418355 -33.376703610030226
+
+f 1186 1185 1295 1328
+
+v -11.05187255431619 43.156619366313826 -38.820086966482016
+v -8.996096016049117 43.156619366345765 -40.00699010435505
+v -8.996096016014414 40.930152790629535 -40.006990104354855
+v -11.051872554281486 40.930152790597596 -38.820086966481824
+
+f 1329 1330 1331 1332
+
+v -21.26337392059187 46.63274487775135 -32.92447390331366
+v -21.699368015354914 46.800548424811424 -32.67275259531516
+
+f 1190 1189 1333 1334
+f 1167 1166 1330 1329
+
+v -23.168327054425752 46.61091677403826 -31.824648698598168
+v -23.486606897239387 46.30532332246442 -31.640889745652153
+
+f 1194 1193 1335 1336
+f 1168 1167 1329 1332
+
+v -24.56690491487101 46.65184446842307 -31.01717939431244
+v -24.983395346624782 46.80532332244116 -30.77671853140344
+
+f 1198 1197 1337 1338
+f 1169 1168 1332 1331
+
+v -26.205738735780688 46.7180109076882 -30.070998246565427
+v -26.498637510276954 46.54492086675596 -29.901893060247335
+
+f 1202 1201 1339 1340
+f 1166 1169 1331 1330
+
+v -27.087086468251872 45.59249931149442 -29.56215189591579
+v -27.17203539706007 45.120296037277555 -29.513106608992146
+
+f 1206 1205 1341 1342
+
+v -2.395901384506559 46.0178062695861 -43.817614251961835
+v -2.3514481705520263 44.96289494625801 -43.84327932699764
+v -2.3863018532272404 44.07544610587547 -43.823156543912916
+v -2.4908629012816457 43.26848839782474 -43.76278819465898
+v -2.6580424301013155 42.58294951651246 -43.666267048676715
+v -2.880751555072088 42.059757156345285 -43.53768587540709
+v -3.274184651180203 41.51269030777164 -43.31053717143479
+v -3.76449917035836 41.1156916720205 -43.027453951769665
+v -4.387139535680405 40.8742182750122 -42.66797236920689
+v -5.177550170219719 40.79372714266703 -42.21162857654194
+v -5.899730290344695 40.85682400486591 -41.79467768974604
+v -6.506716035482494 41.04611459148676 -41.44423430636546
+v -6.998507405633275 41.3615989025296 -41.160298426400125
+v -7.375104400796721 41.80327693799442 -40.94287004985023
+v -7.654229232510182 42.38103955736253 -40.78171725311315
+v -7.853604112310228 43.10477762011522 -40.66660811258677
+v -7.9732290401971655 43.974491126252516 -40.59754262827091
+v -8.013104016170688 44.99018007577441 -40.57452080016575
+v -7.964663304651679 45.94243109896752 -40.60248805801225
+v -7.8193411700642965 46.85648294071602 -40.68638983155162
+v -7.557052439332721 47.67776534181151 -40.8378223008665
+v -7.36388033358708 48.02155797482952 -40.94935026813221
+v -7.11045270861613 48.348979530085835 -41.09566677564591
+v -6.785545497112892 48.63820190389717 -41.283252041689096
+v -6.377934631770455 48.86739699258017 -41.518586284543225
+v -5.866353458744276 49.01678307717202 -41.81394781253117
+v -5.229535324189878 49.06657843870988 -42.18161493397576
+v -4.548263975695727 49.00962073067136 -42.574947130456245
+v -3.9600342377679776 48.838747606533154 -42.91456172802433
+v -3.4648461104065547 48.553959066295285 -43.20045872668006
+v -3.062699593611377 48.155255109957736 -43.432638126423484
+v -2.751527096036234 47.613986351435976 -43.612293651674754
+v -2.5292610263346798 46.901503404645425 -43.740619026854176
+
+f 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375
+
+v -25.239204393741343 44.57408184904019 -30.629027109155487
+v -25.14029230060535 44.81555524604037 -30.686134032761185
+
+f 1212 1211 1376 1377
+
+v -30.291105398615237 40.93015279029867 -27.712310703766786
+v -28.362928783413068 40.93015279032863 -28.82554399170283
+
+f 1280 1279 1378 1379
+
+v -24.353823012071608 45.04475033472925 -31.140202288249995
+v -24.113622433515246 44.831926323818934 -31.278882156950026
+
+f 1216 1215 1380 1381
+f 1137 1136 1374 1373
+
+v -33.58507378291897 40.93015279024749 -25.810537170209344
+v -31.6568971677168 40.93015279027745 -26.92377045814539
+
+f 1268 1267 1382 1383
+f 1138 1137 1373 1372
+
+v -21.562067027510775 45.09318143982038 -32.752023357598866
+v -21.371220962024516 45.12660572358869 -32.86220838489172
+
+f 1226 1225 1384 1385
+f 1139 1138 1372 1371
+
+v -33.792312894138085 44.831926323668554 -25.690887613584646
+v -33.63688356076354 44.462212817531814 -25.780624781053422
+
+f 1265 1264 1386 1387
+f 1140 1139 1371 1370
+
+v -28.3629287835034 46.725514318295886 -28.825543991703334
+v -30.158779552564255 46.72551431826798 -27.78870906666484
+
+f 1236 1235 1388 1389
+f 1141 1140 1370 1369
+
+v -30.94206438121464 46.63274487760097 -27.336479359948328
+v -31.37805847597768 46.800548424661045 -27.084758051949827
+
+f 1240 1239 1390 1391
+f 1142 1141 1369 1368
+
+v -32.84701751504852 46.61091677388788 -26.236654155232834
+v -33.16529735786224 46.30532332231404 -26.052895202286766
+
+f 1244 1243 1392 1393
+f 1143 1142 1368 1367
+
+v -34.24559537549378 46.6518444682727 -25.4291848509471
+v -34.66208580724763 46.80532332229079 -25.188723988038053
+
+f 1248 1247 1394 1395
+f 1144 1143 1367 1366
+
+v -35.88442919640346 46.71801090753782 -24.483003703200094
+v -36.177327970899796 46.54492086660557 -24.313898516881956
+
+f 1250 1249 1396 1397
+f 1145 1144 1366 1365
+
+v -36.765776928874644 45.59249931134404 -23.974157352550453
+v -36.85072585768291 45.12029603712717 -23.92511206562676
+
+f 1254 1253 1398 1399
+f 1146 1145 1365 1364
+
+v -34.917894854364114 44.57408184888981 -25.041032565790147
+v -34.81898276122819 44.815555245889975 -25.098139489395805
+
+f 1260 1259 1400 1401
+f 1147 1146 1364 1363
+f 1187 1186 1328 1296
+
+v -30.50451036259826 44.837383349640504 -27.589101290388772
+v -30.344456639650698 44.45539153518188 -27.68150835042251
+
+f 1277 1276 1402 1403
+f 1189 1188 1297 1333
+f 1154 1153 1357 1356
+f 1191 1190 1334 1306
+f 1155 1154 1356 1355
+
+v -34.60234718583158 45.040657565129365 -25.223214097180808
+v -34.33863698603793 45.11569167154547 -25.375467252034625
+
+f 1262 1261 1404 1405
+f 1156 1155 1355 1354
+f 1195 1194 1336 1281
+f 1157 1156 1354 1353
+f 1197 1196 1282 1337
+
+v -30.747563109573836 45.05430012999144 -27.448774721484014
+
+f 1276 1275 1406 1402
+f 1199 1198 1338 1308
+f 1161 1160 1350 1349
+f 1201 1200 1309 1339
+f 1162 1161 1349 1348
+f 1203 1202 1340 1298
+f 1163 1162 1348 1347
+f 1205 1204 1299 1341
+f 1164 1163 1347 1346
+
+v -31.40553713415 44.99290858837139 -27.068893241239163
+v -31.240757488133546 45.093181439670005 -27.16402881423353
+
+f 1273 1272 1407 1408
+
+v -31.049911422647366 45.12660572343831 -27.274213841526343
+
+f 1275 1274 1409 1406
+f 1272 1271 1313 1407
+
+v -3.8923085140466105 43.21033696533375 -41.79896265439224
+v -3.819204391464585 43.6716261877087 -41.84116933925198
+v -4.3192043914995955 43.67162618770084 -42.707194743016196
+v -4.392308514081622 43.21033696532589 -42.66498805815646
+
+f 1410 1411 1412 1413
+f 1213 1212 1377 1233
+
+v -3.9946542856662104 42.87012550557773 -41.73987329558864
+v -4.494654285701221 42.87012550556987 -42.605898699352856
+
+f 1414 1410 1413 1415
+f 1215 1214 1234 1380
+
+v -4.1247648553617005 42.62541199943656 -41.66475392314129
+v -4.624764855396712 42.625411999428685 -42.53077932690552
+
+f 1416 1414 1415 1417
+
+v -23.958193100140775 44.46221281768219 -31.368619324418756
+
+f 1217 1216 1381 1418
+
+v -4.28116337217109 42.450616637906165 -41.57445719735051
+v -4.781163372206101 42.45061663789829 -42.44048260111474
+
+f 1419 1416 1417 1420
+
+v -23.906383322296204 40.93015279039787 -31.39853171357468
+v -21.978206707093957 40.930152790427826 -32.511765001510774
+
+f 1220 1219 1421 1422
+
+v -4.463849836094456 42.34573942098655 -41.46898311821626
+v -4.963849836129466 42.34573942097868 -42.33500852198048
+
+f 1423 1419 1420 1424
+
+v -21.940251637443872 44.64093041662214 -32.533678371226124
+
+f 1223 1222 1425 1300
+
+v -4.672824247131795 42.3107803486777 -41.34833168573852
+v -5.1728242471668056 42.31078034866983 -42.21435708950275
+
+f 1426 1423 1424 1427
+f 1225 1224 1301 1384
+
+v -4.946337045183895 42.37012550556295 -41.19041899814213
+v -5.446337045218906 42.37012550555509 -42.056444401906354
+
+f 1428 1426 1427 1429
+f 1227 1226 1385 1314
+
+v -5.18086097785672 42.54816097622779 -41.055016542471044
+v -5.680860977891731 42.548160976219926 -41.92104194623527
+
+f 1430 1428 1429 1431
+
+v -20.665766179027848 44.455391535332254 -33.26950289378789
+
+f 1229 1228 1315 1432
+
+v -5.367534939382255 42.854436556033896 -40.9472402805263
+v -5.867534939417266 42.85443655602603 -41.81326568429052
+
+f 1433 1430 1431 1434
+
+v -20.612414937992387 40.93015279044905 -33.300305247132165
+
+f 1232 1231 1435 1294
+
+v -5.497497823992323 43.29850204034293 -40.872206174109046
+v -5.9974978240273344 43.29850204033506 -41.738231577873265
+
+f 1436 1433 1434 1437
+
+v -30.291105398661337 43.8878608394119 -27.71231070376703
+v -31.656897167766562 44.12251295398822 -26.92377045814567
+v -33.58507378296558 43.92060299488596 -25.8105371702096
+v -34.03251347269445 45.04475033457888 -25.55220774488461
+v -34.950865552071996 44.23165347235451 -25.02199692458824
+v -36.879042167279515 44.56998907941868 -23.908763636652175
+
+f 1389 1388 1379 1378 1438 1403 1402 1406 1409 1408 1407 1313 1312 1439 1383 1382 1440 1387 1386 1441 1405 1404 1401 1400 1442 1305 1304 1443 1399 1398 1327 1326 1397 1396 1184 1183 1395 1394 1323 1322 1393 1392 1303 1302 1391 1390 1317 1316
+
+v -5.573703333610727 43.95470940732673 -40.82820890261898
+v -6.073703333645738 43.954709407318866 -41.69423430638321
+
+f 1444 1436 1437 1445
+f 1237 1236 1389 1316
+f 1274 1273 1408 1409
+f 1239 1238 1317 1390
+
+v -5.467517749528932 46.6997639775876 -40.88951517820333
+v -5.540621872110897 46.229607088091235 -40.84730849334355
+v -6.040621872145908 46.22960708808336 -41.713333897107766
+v -5.967517749563942 46.69976397757973 -41.75554058196755
+
+f 1446 1447 1448 1449
+f 1241 1240 1391 1302
+
+v -5.3651719779092435 47.039293309103506 -40.94860453700697
+v -5.865171977944255 47.03929330909564 -41.8146299407712
+
+f 1450 1446 1449 1451
+f 1243 1242 1303 1392
+
+v -5.233584557252395 47.27923191756394 -41.024576569754444
+v -5.733584557287406 47.279231917556075 -41.89060197351867
+
+f 1452 1450 1451 1453
+f 1245 1244 1393 1322
+
+v -5.072755487558712 47.450616637893866 -41.11743127644584
+v -5.5727554875937235 47.45061663788599 -41.98345668021006
+
+f 1454 1452 1453 1455
+f 1247 1246 1323 1394
+
+v -4.882684768828353 47.55344747009327 -41.22716865708106
+v -5.382684768863364 47.553447470085395 -42.09319406084528
+
+f 1456 1454 1455 1457
+f 1182 1248 1395 1183
+
+v -4.663372401061162 47.58772441416217 -41.353788711660194
+v -5.163372401096173 47.587724414154295 -42.21981411542442
+
+f 1458 1456 1457 1459
+f 1249 1181 1184 1396
+
+v -4.44125401646625 47.55276534186002 -41.48202882080969
+v -4.9412540165012615 47.55276534185215 -42.348054224573914
+
+f 1460 1458 1459 1461
+f 1251 1250 1397 1326
+
+v -4.2522170934055366 47.447888124946175 -41.59116933923481
+v -4.752217093440548 47.44788812493831 -42.45719474299903
+
+f 1462 1460 1461 1463
+f 1253 1252 1327 1398
+
+v -4.096261631879178 47.27309276342063 -41.68121026693546
+v -4.596261631914189 47.27309276341277 -42.54723567069968
+
+f 1464 1462 1463 1465
+f 1264 1263 1441 1386
+
+v -3.973387631886938 47.02837925728339 -41.752151603911784
+v -4.473387631921949 47.028379257275525 -42.618177007676
+
+f 1466 1464 1465 1467
+f 1263 1262 1405 1441
+
+v -20.61241493803849 43.88786083956228 -33.30030524713242
+v -21.978206707143716 44.12251295413861 -32.51176500151105
+v -23.906383322342737 43.92060299503634 -31.398531713574986
+v -25.272175091449157 44.23165347250489 -30.60999146795362
+v -27.20035170665668 44.56998907956906 -29.496758180017558
+
+f 1328 1295 1294 1435 1468 1432 1315 1314 1385 1384 1301 1300 1425 1469 1422 1421 1470 1418 1381 1380 1234 1233 1377 1376 1471 1311 1310 1472 1342 1341 1299 1298 1340 1339 1309 1308 1338 1337 1282 1281 1336 1335 1307 1306 1334 1333 1297 1296
+f 1261 1260 1401 1404
+f 1193 1192 1307 1335
+f 1235 1280 1379 1388
+
+s 40
+f 1207 1206 1342 1472
+f 1208 1207 1472 1310
+
+s off
+
+s 41
+f 1230 1229 1432 1468
+f 1231 1230 1468 1435
+
+s off
+
+s 42
+f 1266 1265 1387 1440
+f 1267 1266 1440 1382
+
+s off
+
+s 43
+f 1221 1220 1422 1469
+f 1222 1221 1469 1425
+
+s off
+
+s 44
+f 1269 1268 1383 1439
+f 1270 1269 1439 1312
+
+s off
+
+s 45
+f 1218 1217 1418 1470
+f 1219 1218 1470 1421
+
+s off
+
+s 46
+f 1210 1209 1311 1471
+f 1211 1210 1471 1376
+
+s off
+
+s 47
+f 1255 1254 1399 1443
+f 1256 1255 1443 1304
+
+s off
+
+s 48
+
+v -5.599105170161428 44.897410635157165 -40.81354314545572
+v -6.099105170196438 44.8974106351493 -41.679568549219944
+
+f 1473 1444 1445 1474
+f 1447 1473 1474 1448
+
+s off
+
+s 49
+
+v -3.7607210934132533 44.957437920315336 -41.87493468713984
+v -4.260721093448264 44.95743792030747 -42.74096009090406
+v -3.8138877280434356 46.230971344598274 -41.844238916332905
+v -4.313887728078446 46.23097134459041 -42.710264320097124
+
+f 1411 1475 1476 1412
+f 1475 1477 1478 1476
+f 1477 1466 1467 1478
+
+s off
+
+s 50
+f 1158 1157 1353 1352
+f 1159 1158 1352 1351
+f 1160 1159 1351 1350
+
+s off
+
+s 51
+f 1148 1147 1363 1362
+f 1149 1148 1362 1361
+f 1150 1149 1361 1360
+f 1151 1150 1360 1359
+f 1152 1151 1359 1358
+f 1153 1152 1358 1357
+
+s off
+
+s 52
+f 1278 1277 1403 1438
+f 1279 1278 1438 1378
+
+s off
+
+s 53
+f 1134 1133 1344 1343
+f 1133 1165 1345 1344
+f 1135 1134 1343 1375
+f 1165 1164 1346 1345
+f 1136 1135 1375 1374
+
+s off
+
+s 54
+f 1258 1257 1305 1442
+f 1259 1258 1442 1400
+
+s off
+
+o 0.4mm-Entity65812
+
+v 1.8514481680590678 44.96289494632424 42.97725392327796
+v 1.895901381981027 46.01780626965373 42.951588848243226
+v 2.029261023783209 46.901503404717246 42.87459362313881
+v 2.251527093465458 47.61398635151479 42.74626824796478
+v 2.562699591027929 48.15525511004635 42.56661272272106
+v 2.9648461078161965 48.553959066396544 42.3344333229874
+v 3.460034235175599 48.838747606649996 42.04853632434369
+v 4.0482639731062156 49.00962073080671 41.70892172678989
+v 4.7295353216081235 49.06657843886667 41.315589530325944
+v 5.366353456173014 49.016783077348855 40.9479224088968
+v 5.877934629211065 48.867396992773095 40.65256088092128
+v 6.285545494566426 48.63820190410292 40.41722663807705
+v 6.61045270608332 48.34897953030182 40.229641372041755
+v 6.863880331068126 48.02155797505347 40.083324864534205
+v 7.057052436827293 47.67776534204154 39.97179689727319
+v 7.31934116758839 46.85648294095431 39.82036442796468
+v 7.4646633022065725 45.942431099210374 39.73646265442883
+v 7.513104013756226 44.99018007601879 39.70849539658351
+v 7.473229037814107 43.97449112649565 39.73151722468771
+v 7.353604109952859 43.10477762035459 39.800582709000665
+v 7.154229230172795 42.381039557595614 39.915691849522204
+v 6.875104398473602 41.80327693821872 40.0768446462525
+v 6.498507403318777 41.36159890274206 40.29427302279327
+v 6.00671603317103 41.04611459168375 40.57820890274666
+v 5.399730288030681 40.85682400504379 40.928652286112495
+v 4.677550167897568 40.793727142822185 41.345603172890875
+v 3.8871395333446426 40.87421827514248 41.80194696553664
+v 3.264499168006271 41.11569167213119 42.16142854808429
+v 2.77418464880875 41.5126903078669 42.44451176773751
+v 2.380751552677904 42.05975715642817 42.671660471700264
+v 2.1580424276875463 42.58294951658833 42.80024164496448
+v 1.9908628988439607 43.268488397895354 42.89676279094269
+v 1.8863018507626965 44.0754461059428 42.95713114019408
+
+f 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511
+
+v 8.496096013706131 43.15661936662108 39.14096470079667
+v 10.55187255200202 43.156619366653835 37.95406156297355
+v 10.551872552037379 40.9301527909376 37.954061562973365
+v 8.49609601374149 40.93015279090485 39.14096470079649
+
+f 1512 1513 1514 1515
+
+v 14.838284782437416 42.42537789332511 35.47930030778831
+v 11.331649859643125 42.42537789326925 37.50385692448885
+v 11.331649859614092 44.25348157676175 37.503856924489
+v 14.838284782331945 49.0665784390277 35.47930030778886
+v 16.515987474504453 49.06657843905443 34.51067820680677
+v 16.515987474582538 44.14979808434775 34.51067820680636
+v 17.38555732066635 44.1497980843616 34.00863182207196
+v 17.38555732069374 42.42537789336569 34.00863182207182
+v 16.515987474609926 42.42537789335184 34.51067820680622
+v 16.515987474633672 40.930152791032604 34.51067820680609
+v 14.838284782461164 40.930152791005874 35.47930030778818
+
+f 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526
+
+v 35.047284888048196 46.82186493321048 23.8116286562856
+v 34.665895049999946 46.85648294138994 24.031824181956843
+v 35.16589505001394 46.856482941397815 24.89784958573321
+v 35.54728488806219 46.82186493321834 24.677654060061965
+
+f 1527 1528 1529 1530
+
+v 18.18423832056105 46.72551431902644 33.547513131745525
+v 19.98008908964708 46.72551431905504 32.510678206750626
+v 19.980089089660513 45.87967530131971 32.51067820675056
+v 20.3699685131495 46.332608452758386 32.28558121663518
+v 20.763373918311437 46.63274487841268 32.05844850005309
+v 21.19936801307531 46.800548425486475 31.806727192065168
+v 21.71701350537043 46.85648294118367 31.507864427732123
+v 22.255381062719817 46.79509139958242 31.197037773562464
+v 22.668327052172707 46.61091677475953 30.958623295383838
+v 22.98660689500042 46.30532332319571 30.774864342445547
+v 23.240976012474665 45.879675301371655 30.628004263996655
+v 23.674007174992305 46.35648294121484 30.377993605735945
+v 24.066904912636275 46.651844469188354 30.15115399113206
+v 24.483395344391052 46.80532332321955 29.91069312823317
+v 24.987204589241518 46.856482941235754 29.61981872508722
+v 25.36859442728977 46.8218649330563 29.399623199415974
+v 25.705738733566847 46.71801090850505 29.204972843424837
+v 25.998637508072665 46.54492086758202 29.03586765711385
+v 26.247290750807384 46.30259481028721 28.892307640482933
+v 26.445504918051473 45.98659890305981 28.777868637665964
+v 26.5870864660858 45.592499312339 28.696126492796598
+v 26.672035394910043 45.120296038124806 28.647081205875004
+v 26.700351704524365 44.56998908041721 28.630732776901112
+v 26.700351704582168 40.930152791194836 28.630732776900807
+v 24.772175089352977 40.930152791164126 29.743966064790047
+v 24.772175089300465 44.23165347329236 29.74396606479037
+v 24.739204391581417 44.57408184982663 29.763001705991435
+v 24.640292298436435 44.815555246823685 29.820108629594728
+v 24.42365672302971 45.040657566056254 29.945183237374476
+v 24.159946523230076 45.11569167246406 30.09743639222184
+v 23.853823009884458 45.044750335487834 30.274176885064442
+v 23.613622431331432 44.831926324569956 30.412856753758646
+v 23.458193097966415 44.46221281842832 30.502593921223607
+v 23.406383320184695 43.92060299578083 30.53250631037857
+v 23.406383320232266 40.93015279114236 30.532506310378277
+v 21.478206705002993 40.930152791111645 31.645739598267564
+v 21.478206704952292 44.122512954822426 31.645739598267834
+v 21.440251635235597 44.64093041730477 31.667652967981983
+v 21.35607113043151 44.838065478694965 31.716254605091162
+v 21.226846671304813 44.99290858919768 31.79086238135522
+v 21.06206702528298 45.09318144049111 31.88599795434554
+v 20.87122095979299 45.12660572425341 31.996182981633773
+v 20.568872646717573 45.05430013079703 32.17074386158406
+v 20.325819899745344 44.837383350438444 32.31107043048296
+v 20.165766176807555 44.45539153597478 32.40347749051281
+v 20.11241493583531 43.887860840203125 32.43427984385604
+v 20.11241493588228 40.9301527910899 32.43427984385579
+v 18.184238320653087 40.93015279105918 33.547513131745035
+
+f 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578
+
+v 24.9236567230437 45.04065756606412 30.811208641150838
+v 24.659946523244066 45.115691672471925 30.963461795998207
+
+f 1560 1559 1579 1580
+
+v 27.862928781319553 46.72551431918062 27.959518588615104
+v 29.65877955040559 46.72551431920922 26.922683663620212
+v 29.658779550419023 45.87967530147389 26.92268366362014
+v 30.04865897390801 46.33260845291257 26.697586673504762
+v 30.442064379069862 46.63274487856686 26.470453956922714
+v 30.87805847383374 46.80054842564065 26.21873264893479
+v 31.39570396612886 46.856482941337845 25.919869884601752
+v 31.934071523478327 46.795091399736606 25.609043230432043
+v 32.34701751293113 46.61091677491371 25.370628752253463
+v 32.66529735575892 46.30532332334989 25.18686979931513
+v 32.9196664732331 45.87967530152583 25.04000972086628
+v 33.35269763575073 46.35648294136902 24.78999906260557
+v 33.7455953733947 46.651844469342535 24.563159448001684
+v 34.16208580514956 46.805323323373734 24.32269858510275
+v 35.38442919432527 46.71801090865923 23.61697830029446
+v 35.677327968831165 46.5449208677362 23.447873113983434
+v 35.92598121156589 46.30259481044139 23.304313097352516
+v 36.124195378809986 45.98659890321398 23.189874094535547
+v 36.265776926844225 45.59249931249318 23.108131949666223
+v 36.35072585566855 45.12029603827899 23.05908666274459
+v 36.379042165282875 44.56998908057139 23.04273823377069
+v 36.37904216534068 40.930152791349016 23.042738233770386
+v 34.4508655501114 40.93015279131831 24.155971521659673
+v 34.45086555005897 44.23165347344654 24.15597152165995
+v 34.41789485233984 44.57408184998081 24.17500716286106
+v 34.318982759194945 44.815555246977866 24.232114086464314
+v 34.10234718378821 45.040657566210434 24.357188694244055
+v 33.8386369839885 45.11569167261823 24.50944184909147
+v 33.53251347064297 45.04475033564201 24.686182341934025
+v 33.29231289208994 44.83192632472414 24.824862210628226
+v 33.13688355872485 44.4622128185825 24.914599378093232
+v 33.0850737809432 43.92060299593502 24.944511767248155
+v 33.0850737809907 40.93015279129655 24.944511767247906
+v 31.1568971657615 40.93015279126583 26.057745055137143
+v 31.156897165710802 44.122512954976614 26.057745055137413
+v 31.118942095994107 44.64093041745895 26.079658424851562
+v 31.03476159119002 44.83806547884915 26.128260061960745
+v 30.905537132063326 44.99290858935187 26.2028678382248
+v 30.740757486041403 45.09318144064529 26.298003411215166
+v 30.549911420551496 45.12660572440759 26.408188438503352
+v 30.247563107476008 45.05430013095121 26.582749318453686
+v 30.004510360503854 44.83738335059263 26.723075887352536
+v 29.84445663756606 44.455391536128964 26.815482947382396
+v 29.791105396593814 43.887860840357305 26.846285300725626
+v 29.791105396640788 40.93015279124407 26.846285300725377
+v 27.862928781411593 40.93015279121336 27.959518588614618
+
+f 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1528 1527 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626
+
+v 23.74097601248866 45.87967530137952 31.49402966777302
+v 24.1740071750063 46.35648294122271 31.244019009512307
+
+f 1542 1541 1627 1628
+
+v 11.831649859657112 42.42537789327712 38.369882328265206
+v 15.338284782451405 42.42537789333298 36.34532571156466
+v 15.338284782475153 40.93015279101375 36.34532571156454
+v 17.015987474647662 40.93015279104047 35.37670361058245
+v 17.015987474623916 42.42537789335971 35.37670361058258
+v 17.885557320707726 42.425377893373565 34.87465722584818
+v 17.885557320680338 44.149798084369465 34.874657225848324
+v 17.01598747459653 44.149798084355616 35.37670361058272
+v 17.015987474518443 49.066578439062305 35.376703610583135
+v 15.338284782345934 49.06657843903557 36.34532571156522
+v 11.83164985962808 44.253481576769616 38.36988232826536
+
+f 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639
+
+v 18.684238320667077 40.930152791067044 34.413538535521404
+v 18.68423832057504 46.7255143190343 34.41353853552189
+
+f 1531 1578 1640 1641
+f 1517 1516 1630 1629
+
+v 20.480089089674504 45.87967530132757 33.37670361052693
+v 20.869968513163492 46.33260845276626 33.15160662041155
+
+f 1534 1533 1642 1643
+f 1518 1517 1629 1639
+
+v 26.74729075082137 46.302594810295076 29.758333044259302
+v 26.945504918065463 45.98659890306767 29.643894041442334
+
+f 1550 1549 1644 1645
+f 1519 1518 1639 1638
+
+v 21.856071130445503 44.83806547870283 32.582280008867535
+v 21.726846671318807 44.992908589205555 32.65688778513159
+
+f 1570 1569 1646 1647
+f 1520 1519 1638 1637
+
+v 31.895703966142843 46.85648294134572 26.785895288378118
+v 32.434071523492314 46.79509139974447 26.47506863420841
+
+f 1588 1587 1648 1649
+f 1521 1520 1637 1636
+
+v 36.87904216535466 40.93015279135688 23.908763637546752
+v 34.95086555012539 40.93015279132617 25.02199692543604
+
+f 1603 1602 1650 1651
+f 1522 1521 1636 1635
+
+v 22.217013505384422 46.85648294119154 32.37388983150849
+v 22.755381062733807 46.79509139959029 32.063063177338826
+
+f 1538 1537 1652 1653
+f 1523 1522 1635 1634
+
+v 25.487204589255512 46.85648294124363 30.485844128863583
+v 25.868594427303762 46.82186493306417 30.265648603192336
+
+f 1546 1545 1654 1655
+f 1524 1523 1634 1633
+
+v 27.20035170459616 40.9301527912027 29.49675818067717
+v 25.27217508936697 40.93015279117199 30.60999146856641
+
+f 1555 1554 1656 1657
+f 1525 1524 1633 1632
+
+v 31.61894209600809 44.64093041746682 26.94568382862793
+v 31.534761591204006 44.838065478857025 26.99428546573711
+
+f 1617 1616 1658 1659
+f 1526 1525 1632 1631
+
+v 21.068872646731567 45.0543001308049 33.03676926536043
+v 20.825819899759335 44.83738335044631 33.177095834259326
+
+f 1574 1573 1660 1661
+f 1516 1526 1631 1630
+
+v 30.15877955043301 45.879675301481754 27.788709067396507
+v 30.548658973921995 46.33260845292043 27.56361207728113
+
+f 1584 1583 1662 1663
+
+v 14.838284782370048 46.667448152533844 35.47930030778866
+v 14.838284782410028 44.14979808432101 35.47930030778845
+v 15.338284782424019 44.149798084328886 36.34532571156481
+v 15.338284782384036 46.66744815254171 36.345325711565025
+
+f 1664 1665 1666 1667
+
+v 33.419666473247084 45.8796753015337 25.906035124642646
+v 33.85269763576472 46.356482941376896 25.656024466381933
+
+f 1592 1591 1668 1669
+
+v 12.98542756621319 44.149798084291504 36.54904792036954
+v 13.485427566227175 44.14979808429938 37.4150733241459
+
+f 1670 1664 1667 1671
+
+v 36.425981211579874 46.30259481044926 24.17033850112888
+v 36.62419537882397 45.986598903221854 24.055899498311913
+
+f 1598 1597 1672 1673
+f 1665 1670 1671 1666
+
+v 20.48008908966107 46.72551431906291 33.376703610526995
+
+f 1532 1531 1641 1674
+
+v 11.051872552016006 43.1566193666617 38.82008696674991
+v 8.996096013720118 43.15661936662895 40.006990104573035
+v 8.996096013755476 40.930152790912715 40.00699010457285
+v 11.051872552051364 40.93015279094546 38.82008696674973
+
+f 1675 1676 1677 1678
+
+v 21.263373918325428 46.632744878420546 32.92447390382946
+v 21.699368013089302 46.80054842549434 32.67275259584154
+
+f 1536 1535 1679 1680
+f 1513 1512 1676 1675
+
+v 23.168327052186697 46.610916774767404 31.824648699160207
+v 23.48660689501441 46.305323323203574 31.640889746221916
+
+f 1540 1539 1681 1682
+f 1514 1513 1675 1678
+
+v 24.566904912650266 46.65184446919623 31.01717939490842
+v 24.983395344405043 46.80532332322741 30.776718532009532
+
+f 1544 1543 1683 1684
+f 1515 1514 1678 1677
+
+v 26.205738733580837 46.718010908512916 30.0709982472012
+v 26.498637508086652 46.54492086758989 29.90189306089022
+
+f 1548 1547 1685 1686
+f 1512 1515 1677 1676
+
+v 27.087086466099787 45.592499312346874 29.56215189657296
+v 27.172035394924034 45.12029603813267 29.513106609651373
+
+f 1552 1551 1687 1688
+
+v 2.3959013819950137 46.017806269661605 43.81761425201959
+v 2.351448168073054 44.962894946332106 43.84327932705432
+v 2.3863018507766833 44.07544610595067 43.82315654397044
+v 2.4908628988579475 43.268488397903226 43.76278819471905
+v 2.658042427701533 42.5829495165962 43.666267048740835
+v 2.880751552691891 42.059757156436035 43.53768587547663
+v 3.274184648822736 41.51269030787477 43.31053717151387
+v 3.7644991680202584 41.11569167213906 43.02745395186065
+v 4.38713953335863 40.87421827515035 42.667972369313
+v 5.177550167911554 40.79372714283006 42.21162857666723
+v 5.899730288044667 40.85682400505166 41.79467768988886
+v 6.5067160331850165 41.04611459169162 41.44423430652302
+v 6.998507403332763 41.361598902749925 41.16029842656963
+v 7.375104398487588 41.80327693822659 40.942870050028866
+v 7.654229230186782 42.381039557603486 40.78171725329857
+v 7.853604109966846 43.104777620362455 40.66660811277703
+v 7.9732290378280934 43.974491126503516 40.59754262846407
+v 8.013104013770214 44.990180076026654 40.57452080035987
+v 7.964663302220559 45.94243109921825 40.60248805820519
+v 7.819341167602375 46.85648294096218 40.68638983174104
+v 7.557052436841279 47.67776534204941 40.83782230104954
+v 7.363880331082112 48.02155797506134 40.94935026831057
+v 7.110452706097306 48.348979530309684 41.09566677581811
+v 6.785545494580412 48.63820190411079 41.283252041853416
+v 6.377934629225052 48.86739699278097 41.51858628469764
+v 5.866353456187 49.01678307735672 41.81394781267316
+v 5.22953532162211 49.06657843887454 42.181614934102306
+v 4.548263973120203 49.009620730814575 42.57494713056625
+v 3.9600342351895863 48.83874760665787 42.914561728120056
+v 3.464846107830183 48.55395906640441 43.20045872676376
+v 3.0626995910419157 48.15525511005421 43.432638126497416
+v 2.7515270934794445 47.613986351522655 43.61229365174114
+v 2.5292610237971958 46.90150340472511 43.74061902691516
+
+f 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721
+
+v 25.239204391595408 44.57408184983449 30.629027109767797
+v 25.14029229845043 44.81555524683156 30.686134033371097
+
+f 1558 1557 1722 1723
+
+v 30.291105396654775 40.93015279125194 27.71231070450174
+v 28.36292878142558 40.930152791221225 28.825543992390983
+
+f 1626 1625 1724 1725
+
+v 24.353823009898452 45.0447503354957 31.14020228884081
+v 24.113622431345423 44.83192632457782 31.27888215753501
+
+f 1562 1561 1726 1727
+f 1483 1482 1720 1719
+
+v 33.58507378100468 40.930152791304415 25.810537171024272
+v 31.656897165775487 40.9301527912737 26.923770458913513
+
+f 1614 1613 1728 1729
+f 1484 1483 1719 1718
+
+v 21.56206702529697 45.093181440498974 32.7520233581219
+v 21.37122095980698 45.12660572426128 32.862208385410135
+
+f 1572 1571 1730 1731
+f 1485 1484 1718 1717
+
+v 33.79231289210392 44.831926324732 25.69088761440459
+v 33.63688355873883 44.462212818590366 25.780624781869598
+
+f 1611 1610 1732 1733
+f 1486 1485 1717 1716
+
+v 28.36292878133354 46.725514319188484 28.825543992391474
+v 30.158779550419574 46.72551431921708 27.788709067396574
+
+f 1582 1581 1734 1735
+f 1487 1486 1716 1715
+
+v 30.94206437908385 46.63274487857473 27.336479360699084
+v 31.378058473847727 46.80054842564852 27.08475805271116
+
+f 1586 1585 1736 1737
+f 1488 1487 1715 1714
+
+v 32.847017512945115 46.61091677492158 26.23665415602983
+v 33.165297355772914 46.305323323357754 26.052895203091495
+
+f 1590 1589 1738 1739
+f 1489 1488 1714 1713
+
+v 34.24559537340869 46.6518444693504 25.42918485177805
+v 34.66208580516355 46.80532332338161 25.188723988879115
+
+f 1594 1593 1740 1741
+f 1490 1489 1713 1712
+
+v 35.88442919433926 46.7180109086671 24.483003704070825
+v 36.177327968845155 46.544920867744075 24.3138985177598
+
+f 1596 1595 1742 1743
+f 1491 1490 1712 1711
+
+v 36.765776926858216 45.592499312501054 23.97415735344259
+v 36.85072585568253 45.12029603828685 23.925112066520956
+
+f 1600 1599 1744 1745
+f 1492 1491 1711 1710
+
+v 34.91789485235383 44.57408184998867 25.041032566637426
+v 34.81898275920893 44.81555524698573 25.09813949024068
+
+f 1606 1605 1746 1747
+f 1493 1492 1710 1709
+f 1533 1532 1674 1642
+
+v 30.50451036051784 44.8373833506005 27.589101291128905
+v 30.344456637580045 44.45539153613683 27.681508351158758
+
+f 1623 1622 1748 1749
+f 1535 1534 1643 1679
+f 1500 1499 1703 1702
+f 1537 1536 1680 1652
+f 1501 1500 1702 1701
+
+v 34.6023471838022 45.04065756621831 25.22321409802042
+v 34.33863698400249 45.1156916726261 25.375467252867836
+
+f 1608 1607 1750 1751
+f 1502 1501 1701 1700
+f 1541 1540 1682 1627
+f 1503 1502 1700 1699
+f 1543 1542 1628 1683
+
+v 30.74756310748999 45.05430013095908 27.448774722230052
+
+f 1622 1621 1752 1748
+f 1545 1544 1684 1654
+f 1507 1506 1696 1695
+f 1547 1546 1655 1685
+f 1508 1507 1695 1694
+f 1549 1548 1686 1644
+f 1509 1508 1694 1693
+f 1551 1550 1645 1687
+f 1510 1509 1693 1692
+
+v 31.405537132077313 44.992908589359736 27.068893242001167
+v 31.240757486055386 45.093181440653154 27.164028814991536
+
+f 1619 1618 1753 1754
+
+v 31.04991142056548 45.126605724415455 27.274213842279714
+
+f 1621 1620 1755 1752
+f 1618 1617 1659 1753
+
+v 3.892308511672417 43.210336965456335 41.798962654486324
+v 3.8192043890748506 43.671626187828984 41.84116933934428
+v 4.319204389088838 43.67162618783685 42.707194743120645
+v 4.392308511686404 43.21033696546421 42.664988058262686
+
+f 1756 1757 1758 1759
+f 1559 1558 1723 1579
+
+v 3.994654283304157 42.870125505703534 41.73987329568521
+v 4.494654283318145 42.87012550571141 42.60589869946157
+
+f 1760 1756 1759 1761
+f 1561 1560 1580 1726
+
+v 4.124764853009173 42.62541199956645 41.66475392324102
+v 4.6247648530231595 42.62541199957432 42.530779327017385
+
+f 1762 1760 1761 1763
+
+v 23.958193097980406 44.462212818436186 31.368619324999973
+
+f 1563 1562 1727 1764
+
+v 4.281163369826254 42.45061663804098 41.574457197454045
+v 4.7811633698402405 42.45061663804885 42.44048260123041
+
+f 1765 1762 1763 1766
+
+v 23.906383320246256 40.93015279115023 31.398531714154647
+v 21.978206705016984 40.93015279111951 32.51176500204393
+
+f 1566 1565 1767 1768
+
+v 4.46384983375548 42.345739421127114 41.468983118324225
+v 4.9638498337694665 42.34573942113498 42.33500852210058
+
+f 1769 1765 1766 1770
+
+v 21.940251635249588 44.64093041731264 32.53367837175835
+
+f 1569 1568 1771 1646
+
+v 4.672824244796849 42.31078034882484 41.34833168585156
+v 5.172824244810835 42.31078034883271 42.214357089627924
+
+f 1772 1769 1770 1773
+f 1571 1570 1647 1730
+
+v 4.946337042850915 42.3701255057187 41.19041899826181
+v 5.446337042864901 42.37012550572657 42.056444402038174
+
+f 1774 1772 1773 1775
+f 1573 1572 1731 1660
+
+v 5.180860975521426 42.54816097639092 41.05501654259641
+v 5.6808609755354125 42.54816097639878 41.921041946372775
+
+f 1776 1774 1775 1777
+
+v 20.665766176821545 44.455391535982656 33.269502894289175
+
+f 1575 1574 1661 1778
+
+v 5.367534937039939 42.8544365562029 40.9472402806562
+v 5.867534937053925 42.854436556210764 41.813265684432565
+
+f 1779 1776 1777 1780
+
+v 20.612414935896275 40.93015279109777 33.30030524763216
+
+f 1578 1577 1781 1640
+
+v 5.497497821637855 43.29850204051602 40.8722061742421
+v 5.997497821651841 43.298502040523886 41.738231578018464
+
+f 1782 1779 1780 1783
+
+v 30.291105396607797 43.88786084036518 27.712310704501988
+v 31.65689716572479 44.12251295498448 26.923770458913783
+v 33.58507378095719 43.920602995942886 25.810537171024517
+v 34.03251347065696 45.04475033564988 25.55220774571039
+v 34.950865550072955 44.23165347345441 25.021996925436316
+v 36.87904216529686 44.56998908057926 23.908763637547057
+
+f 1735 1734 1725 1724 1784 1749 1748 1752 1755 1754 1753 1659 1658 1785 1729 1728 1786 1733 1732 1787 1751 1750 1747 1746 1788 1651 1650 1789 1745 1744 1673 1672 1743 1742 1530 1529 1741 1740 1669 1668 1739 1738 1649 1648 1737 1736 1663 1662
+
+v 5.573703331236677 43.95470940750223 40.828208902753886
+v 6.073703331250663 43.954709407510094 41.69423430653025
+
+f 1790 1782 1783 1791
+f 1583 1582 1735 1662
+f 1620 1619 1754 1755
+f 1585 1584 1663 1736
+
+v 5.467517747067012 46.699763977759744 40.88951517833565
+v 5.540621869664797 46.22960708826568 40.84730849347765
+v 6.040621869678783 46.229607088273546 41.71333389725401
+v 5.967517747080998 46.69976397776761 41.75554058211201
+
+f 1792 1793 1794 1795
+f 1587 1586 1737 1648
+
+v 5.365171975435205 47.03929330927243 40.94860453713681
+v 5.865171975449191 47.039293309280296 41.81462994091317
+
+f 1796 1792 1795 1797
+f 1589 1588 1649 1738
+
+v 5.233584554768961 47.27923191772873 41.02457656988109
+v 5.733584554782947 47.279231917736595 41.890601973657446
+
+f 1798 1796 1797 1799
+f 1591 1590 1739 1668
+
+v 5.072755485067631 47.45061663805359 41.117431276568574
+v 5.572755485081617 47.45061663806145 41.98345668034494
+
+f 1800 1798 1799 1801
+f 1593 1592 1669 1740
+
+v 4.882684766331373 47.55344747024701 41.22716865719918
+v 5.382684766345359 47.55344747025488 42.09319406097554
+
+f 1802 1800 1801 1803
+f 1528 1594 1741 1529
+
+v 4.663372398560028 47.58772441430901 41.35378871177299
+v 5.1633723985740145 47.587724414316874 42.219814115549354
+
+f 1804 1802 1803 1805
+f 1595 1527 1530 1742
+
+v 4.4412540139631025 47.55276534199987 41.482028820917094
+v 4.9412540139770895 47.55276534200774 42.348054224693456
+
+f 1806 1804 1805 1807
+f 1597 1596 1743 1672
+
+v 4.252217090903041 47.44788812508008 41.591169339337625
+v 4.752217090917027 47.44788812508795 42.45719474311398
+
+f 1808 1806 1807 1809
+f 1599 1598 1673 1744
+
+v 4.096261629379995 47.27309276354963 41.681210267034494
+v 4.596261629393982 47.2730927635575 42.54723567081085
+
+f 1810 1808 1809 1811
+f 1610 1609 1787 1732
+
+v 3.9733876293937347 47.02837925740852 41.752151604007835
+v 4.473387629407722 47.02837925741639 42.6181770077842
+
+f 1812 1810 1811 1813
+f 1609 1608 1751 1787
+
+v 20.6124149358493 43.887860840211 33.30030524763241
+v 21.978206704966286 44.12251295483029 32.5117650020442
+v 23.906383320198685 43.920602995788705 31.398531714154938
+v 25.272175089314455 44.231653473300234 30.609991468566733
+v 27.200351704538356 44.569989080425074 29.496758180677475
+
+f 1674 1641 1640 1781 1814 1778 1661 1660 1731 1730 1647 1646 1771 1815 1768 1767 1816 1764 1727 1726 1580 1579 1723 1722 1817 1657 1656 1818 1688 1687 1645 1644 1686 1685 1655 1654 1684 1683 1628 1627 1682 1681 1653 1652 1680 1679 1643 1642
+f 1607 1606 1747 1750
+f 1539 1538 1653 1681
+f 1581 1626 1725 1734
+
+s 55
+f 1553 1552 1688 1818
+f 1554 1553 1818 1656
+
+s off
+
+s 56
+f 1576 1575 1778 1814
+f 1577 1576 1814 1781
+
+s off
+
+s 57
+f 1612 1611 1733 1786
+f 1613 1612 1786 1728
+
+s off
+
+s 58
+f 1567 1566 1768 1815
+f 1568 1567 1815 1771
+
+s off
+
+s 59
+f 1615 1614 1729 1785
+f 1616 1615 1785 1658
+
+s off
+
+s 60
+f 1564 1563 1764 1816
+f 1565 1564 1816 1767
+
+s off
+
+s 61
+f 1556 1555 1657 1817
+f 1557 1556 1817 1722
+
+s off
+
+s 62
+f 1601 1600 1745 1789
+f 1602 1601 1789 1650
+
+s off
+
+s 63
+
+v 5.5991051677580685 44.89741063533346 40.81354314559124
+v 6.099105167772055 44.89741063534132 41.6795685493676
+
+f 1819 1790 1791 1820
+f 1793 1819 1820 1794
+
+s off
+
+s 64
+
+v 3.7607210909822375 44.957437920433776 41.874934687230734
+v 4.260721090996225 44.95743792044164 42.740960091007096
+v 3.8138877255730894 46.23097134471839 41.844238916425084
+v 4.313887725587076 46.23097134472626 42.710264320201446
+
+f 1757 1821 1822 1758
+f 1821 1823 1824 1822
+f 1823 1812 1813 1824
+
+s off
+
+s 65
+f 1504 1503 1699 1698
+f 1505 1504 1698 1697
+f 1506 1505 1697 1696
+
+s off
+
+s 66
+f 1494 1493 1709 1708
+f 1495 1494 1708 1707
+f 1496 1495 1707 1706
+f 1497 1496 1706 1705
+f 1498 1497 1705 1704
+f 1499 1498 1704 1703
+
+s off
+
+s 67
+f 1624 1623 1749 1784
+f 1625 1624 1784 1724
+
+s off
+
+s 68
+f 1480 1479 1690 1689
+f 1479 1511 1691 1690
+f 1481 1480 1689 1721
+f 1511 1510 1692 1691
+f 1482 1481 1721 1720
+
+s off
+
+s 69
+f 1604 1603 1651 1788
+f 1605 1604 1788 1746
+
+s off
+
+o 0.4mm-Entity65814
+
+v 36.293669596989105 44.96289494686609 -23.092028110168584
+v 36.2492163830327 46.017806270194185 -23.11769318520116
+v 36.11585674119898 46.9015034052535 -23.194688410299115
+v 35.893590671488084 47.61398635204405 -23.323013785462365
+v 35.58241817389987 48.15525511056581 -23.502669310690994
+v 35.1802716570878 48.55395906690336 -23.734848710405156
+v 34.68508352970557 48.83874760714123 -24.020745709024855
+v 34.09685379175311 49.00962073127943 -24.360360306550138
+v 33.41558244323034 49.06657843931795 -24.753692502981046
+v 32.77876430864919 49.01678307778009 -25.12135962437931
+v 32.26718313560152 48.867396993188244 -25.416721152330027
+v 31.85957227024196 48.63820190450524 -25.652055395154488
+v 31.53466505872507 48.34897953069391 -25.839640661174037
+v 31.28123743374347 48.021557975437595 -25.985957168669298
+v 31.088065327989717 47.67776534241958 -26.097485135920945
+v 30.825776597247124 46.856482941324096 -26.248917605216743
+v 30.680454462653632 45.942431099575586 -26.33281937874554
+v 30.632013751132593 44.99018007638248 -26.360786636588514
+v 30.67188872710779 43.97449112686059 -26.337764808486245
+v 30.791513654999754 43.1047776207233 -26.26869932417909
+v 30.99088853480817 42.381039557970595 -26.153590183667216
+v 31.270013366533362 41.803276938602494 -25.992437386950453
+v 31.64661036171263 41.361598903137676 -25.77500901042795
+v 32.13840173188407 41.04611459209484 -25.4910731304984
+v 32.745387477047366 40.85682400547398 -25.140629747161995
+v 33.46756759720268 40.793727143275106 -24.723678860418637
+v 34.257978231775205 40.87421827562027 -24.267335067811196
+v 34.88061859712341 41.11569167262858 -23.90785348529374
+v 35.37093311632216 41.51269030837972 -23.62477026566429
+v 35.7643662124468 42.05975715695337 -23.39762156172061
+v 35.98707533742694 42.58294951712053 -23.269040388467197
+v 36.15425486625362 43.268488398432815 -23.172519242497092
+v 36.25881591431242 44.075446106483554 -23.112150893250774
+
+f 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857
+
+v 29.64902175121287 43.15661936695384 -26.928317332327687
+v 27.59324521285943 43.1566193669219 -28.115220470051124
+v 27.59324521289413 40.930152791205664 -28.11522047005131
+v 29.64902175124757 40.9301527912376 -26.92831733232787
+
+f 1858 1859 1860 1861
+
+v 23.306832982327045 42.42537789345829 -30.589981725028533
+v 26.8134679052195 42.42537789351278 -28.565425108498022
+v 26.813467905191008 44.253481577005275 -28.565425108497866
+v 23.30683298222353 49.06657843916088 -30.58998172502798
+v 21.629130290004056 49.066578439134815 -31.558603825928714
+v 21.629130290080692 44.14979808442813 -31.558603825929133
+v 20.75956044397254 44.14979808441462 -32.06065021062136
+v 20.759560443999415 42.42537789341871 -32.06065021062151
+v 21.62913029010757 42.42537789343222 -31.55860382592928
+v 21.629130290130878 40.93015279111298 -31.558603825929403
+v 23.30683298235035 40.93015279113906 -30.58998172502866
+
+f 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872
+
+v 3.0978328760121765 46.82186493270771 -42.25765337555136
+v 3.4792227140700205 46.85648294089918 -42.0374578498986
+v 3.979222714042017 46.85648294090705 -42.90348325369921
+v 3.597832875984173 46.82186493271558 -43.12367877935196
+
+f 1873 1874 1875 1876
+
+v 19.960879443974427 46.72551431905432 -32.52176890090908
+v 18.165028674838123 46.72551431902641 -33.558603825816895
+v 18.165028674851307 45.87967530129108 -33.55860382581697
+v 17.77514925133715 46.332608452717494 -33.78370081591344
+v 17.38174384615476 46.632744878359404 -34.01083353247646
+v 16.9457497513734 46.80054842541948 -34.26255484044324
+v 16.428104259062028 46.85648294110038 -34.561417604751185
+v 15.889736701699503 46.795091399482196 -34.87224425889474
+v 15.47679071224085 46.61091677464631 -35.11065873705334
+v 15.158510869413844 46.30532332307247 -35.2944176899762
+v 14.904141751945867 45.87967530124042 -35.44127776841276
+v 14.471110589401103 46.35648294106998 -35.69128842665247
+v 14.07821285173684 46.651844469031126 -35.91812804123731
+v 13.661722419965571 46.805323323049215 -36.15858890411601
+v 13.157913175099392 46.85648294104957 -36.44946330723752
+v 12.776523337041555 46.821864932858105 -36.66965883289028
+v 12.439379030758309 46.71801090829625 -36.86430918886507
+v 12.146480256249736 46.544920867364006 -37.03341437516185
+v 11.897827013515684 46.302594810061365 -37.17697439178071
+v 11.699612846275985 45.98659890282773 -37.29141339458807
+v 11.558031298250102 45.59249931210247 -37.37315553945057
+v 11.473082369438337 45.1202960378856 -37.42220082636804
+v 11.44476605984054 44.56998908017711 -37.43854925534056
+v 11.444766059897274 40.930152790954736 -37.43854925534087
+v 13.372942675180445 40.9301527909847 -36.32531596754512
+v 13.372942675129062 44.23165347311294 -36.325315967544796
+v 13.405913372838262 44.57408184964824 -36.30628032634534
+v 13.504825465978412 44.815555246648415 -36.249173402746834
+v 13.72146104138412 45.04065756588779 -36.12409879497759
+v 13.985171241188775 45.1156916723039 -35.97184564014301
+v 14.291294754545191 45.044750335337305 -35.795105147315255
+v 14.531495333111645 44.83192632442699 -35.656425278632696
+v 14.686924666492645 44.46221281829024 -35.56668811117527
+v 14.738734444292858 43.920602995644394 -35.53677572202282
+v 14.738734444339393 40.93015279100592 -35.536775722023116
+v 16.66691105962264 40.93015279103588 -34.42354243422732
+v 16.666911059572882 44.12251295474666 -34.42354243422705
+v 16.704866129274325 44.6409304172302 -34.401629064514744
+v 16.789046634074566 44.838065478623044 -34.35302742740964
+v 16.91827109320001 44.99290858912983 -34.27841965115185
+v 17.083050739223307 45.09318144042844 -34.18328407816952
+v 17.273896804717584 45.12660572419674 -34.07309905089055
+v 17.576245117803733 45.05430013074987 -33.89853817095492
+v 17.819297864789593 44.837383350398945 -33.7582116020678
+v 17.97935158774389 44.455391535940315 -33.66580454204571
+v 18.032702828735488 43.88786084017034 -33.635002188705066
+v 18.032702828781588 40.93015279105711 -33.635002188705315
+v 19.96087944406476 40.93015279108706 -32.52176890090957
+
+f 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924
+
+v 14.221461041356118 45.04065756589566 -36.990124198778204
+v 14.48517124116077 45.11569167231176 -36.83787104394362
+
+f 1906 1905 1925 1926
+
+v 10.28218898294498 46.72551431890393 -38.109763443570195
+v 8.486338213808676 46.72551431887602 -39.14659836847802
+v 8.486338213821856 45.87967530114069 -39.14659836847809
+v 8.096458790307693 46.33260845256711 -39.371695358574556
+v 7.703053385125385 46.63274487820901 -39.598828075137526
+v 7.267059290344024 46.800548425269085 -39.850549383104315
+v 6.749413798032657 46.856482940949995 -40.14941214741226
+v 6.211046240670049 46.79509139933181 -40.46023880155586
+v 5.798100251211475 46.61091677449592 -40.698653279714414
+v 5.47982040838439 46.30532332292208 -40.88241223263732
+v 5.225451290916494 45.879675301090025 -41.02927231107384
+v 4.792420128371726 46.356482940919584 -41.27928296931355
+v 4.399522390707466 46.65184446888074 -41.50612258389839
+v 3.9830319589361203 46.80532332289883 -41.74658344677712
+v 2.760688569728931 46.718010908145864 -42.45230373152614
+v 2.4677897952202854 46.54492086721361 -42.62140891782297
+v 2.219136552486227 46.30259480991098 -42.76496893444183
+v 2.0209223852465334 45.986598902677336 -42.87940793724919
+v 1.8793408372207268 45.592499311952075 -42.961150082111644
+v 1.7943919084088862 45.12029603773521 -43.01019536902916
+v 1.7660755988110906 44.569989080026716 -43.02654379800168
+v 1.7660755988678227 40.93015279080434 -43.02654379800199
+v 3.6942522141510707 40.93015279083431 -41.9133105102062
+v 3.694252214099612 44.23165347296255 -41.91331051020592
+v 3.727222911808881 44.57408184949785 -41.89427486900641
+v 3.82613500494896 44.81555524649802 -41.837167945407955
+v 4.042770580354667 45.04065756573741 -41.71209333763871
+v 4.3064807801594025 45.1156916721535 -41.55984018280409
+v 4.6126042935157425 45.04475033518692 -41.383099689976376
+v 4.852804872082194 44.8319263242766 -41.24441982129382
+v 5.00823420546327 44.462212818139854 -41.154682653836346
+v 5.060043983263401 43.92060299549401 -41.12477026468394
+v 5.060043983310015 40.93015279085553 -41.12477026468419
+v 6.988220598593189 40.930152790885494 -40.01153697688844
+v 6.988220598543428 44.12251295459627 -40.01153697688817
+v 7.026175668244873 44.64093041707981 -39.98962360717586
+v 7.110356173045115 44.83806547847266 -39.94102197007077
+v 7.239580632170556 44.99290858897944 -39.86641419381297
+v 7.404360278193929 45.093181440278045 -39.7712786208306
+v 7.595206343688129 45.12660572404635 -39.66109359355166
+v 7.897554656774361 45.054300130599486 -39.48653271361599
+v 8.140607403760143 44.83738335024855 -39.346206144728924
+v 8.300661126714436 44.45539153578993 -39.253799084706834
+v 8.354012367706034 43.88786084001995 -39.22299673136619
+v 8.354012367752134 40.93015279090672 -39.222996731366436
+v 10.282188983035308 40.930152790936674 -38.10976344357069
+
+f 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1874 1873 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972
+
+v 15.404141751917862 45.87967530124828 -36.307303172213366
+v 14.971110589373101 46.35648294107784 -36.55731383045308
+
+f 1888 1887 1973 1974
+
+v 27.313467905191498 42.42537789352064 -29.43145051229863
+v 23.80683298229904 42.425377893466155 -31.456007128829143
+v 23.806832982322344 40.93015279114692 -31.456007128829267
+v 22.12913029010287 40.930152791120854 -32.42462922973001
+v 22.129130290079566 42.42537789344009 -32.42462922972988
+v 21.25956044397141 42.42537789342658 -32.92667561442211
+v 21.259560443944533 44.149798084422486 -32.926675614421974
+v 22.129130290052686 44.14979808443599 -32.42462922972973
+v 22.12913028997605 49.06657843914268 -32.42462922972932
+v 23.806832982195523 49.066578439168744 -31.456007128828585
+v 27.313467905163005 44.25348157701313 -29.431450512298476
+
+f 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985
+
+v 20.460879444036756 40.93015279109493 -33.38779430471018
+v 20.460879443946425 46.72551431906219 -33.38779430470969
+
+f 1877 1924 1986 1987
+f 1863 1862 1976 1975
+
+v 18.665028674823304 45.879675301298946 -34.42462922961757
+v 18.275149251309145 46.33260845272536 -34.64972621971405
+
+f 1880 1879 1988 1989
+f 1864 1863 1975 1985
+
+v 12.397827013487682 46.30259481006924 -38.042999795581316
+v 12.199612846247982 45.986598902835595 -38.157438798388675
+
+f 1896 1895 1990 1991
+f 1865 1864 1985 1984
+
+v 17.289046634046564 44.83806547863091 -35.219052831210256
+v 17.418271093172002 44.99290858913769 -35.14444505495246
+
+f 1916 1915 1992 1993
+f 1866 1865 1984 1983
+
+v 7.249413798004654 46.85648294095786 -41.01543755121287
+v 6.7110462406420455 46.79509139933967 -41.32626420535647
+
+f 1934 1933 1994 1995
+f 1867 1866 1983 1982
+
+v 2.2660755988398193 40.93015279081221 -43.8925692018026
+v 4.194252214123067 40.93015279084218 -42.77933591400681
+
+f 1949 1948 1996 1997
+f 1868 1867 1982 1981
+
+v 16.928104259034022 46.856482941108254 -35.42744300855179
+v 16.3897367016715 46.79509139949006 -35.73826966269535
+
+f 1884 1883 1998 1999
+f 1869 1868 1981 1980
+
+v 13.65791317507139 46.85648294105743 -37.315488711038135
+v 13.27652333701355 46.82186493286597 -37.535684236690884
+
+f 1892 1891 2000 2001
+f 1870 1869 1980 1979
+
+v 11.94476605986927 40.93015279096261 -38.30457465914148
+v 13.872942675152443 40.93015279099257 -37.191341371345736
+
+f 1901 1900 2002 2003
+f 1871 1870 1979 1978
+
+v 7.526175668216871 44.640930417087674 -40.85564901097648
+v 7.610356173017111 44.83806547848052 -40.80704737387138
+
+f 1963 1962 2004 2005
+f 1872 1871 1978 1977
+
+v 18.07624511777573 45.054300130757746 -34.764563574755535
+v 18.31929786476159 44.83738335040681 -34.62423700586841
+
+f 1920 1919 2006 2007
+f 1862 1872 1977 1976
+
+v 8.986338213793854 45.87967530114856 -40.0126237722787
+v 8.59645879027969 46.33260845257497 -40.23772076237517
+
+f 1930 1929 2008 2009
+
+v 23.306832982260925 46.66744815266702 -30.589981725028178
+v 23.30683298230017 44.1497980844542 -30.58998172502839
+v 23.806832982272162 44.149798084462056 -31.456007128828993
+v 23.80683298223292 46.667448152674886 -31.45600712882878
+
+f 2010 2011 2012 2013
+
+v 5.725451290888491 45.87967530109789 -41.89529771487445
+v 5.292420128343723 46.35648294092746 -42.14530837311416
+
+f 1938 1937 2014 2015
+
+v 25.159690198548876 44.14979808448299 -29.520234112537143
+v 25.659690198520874 44.149798084490854 -30.386259516337745
+
+f 2016 2010 2013 2017
+
+v 2.7191365524582243 46.30259480991884 -43.63099433824244
+v 2.5209223852185305 45.9865989026852 -43.7454333410498
+
+f 1944 1943 2018 2019
+f 2011 2016 2017 2012
+
+v 18.66502867481012 46.725514319034275 -34.42462922961751
+
+f 1878 1877 1987 2020
+
+v 28.093245212831423 43.15661936692977 -28.981245873851734
+v 30.14902175118486 43.1566193669617 -27.79434273612829
+v 30.149021751219564 40.93015279124547 -27.794342736128474
+v 28.093245212866126 40.93015279121353 -28.98124587385192
+
+f 2021 2022 2023 2024
+
+v 17.881743846126756 46.63274487836727 -34.87685893627707
+v 17.445749751345396 46.800548425427344 -35.12858024424385
+
+f 1882 1881 2025 2026
+f 1859 1858 2022 2021
+
+v 15.976790712212848 46.61091677465418 -35.976684140853955
+v 15.658510869385841 46.30532332308034 -36.160443093776806
+
+f 1886 1885 2027 2028
+f 1860 1859 2021 2024
+
+v 14.578212851708837 46.65184446903899 -36.784153445037916
+v 14.161722419937568 46.805323323057074 -37.024614307916615
+
+f 1890 1889 2029 2030
+f 1861 1860 2024 2023
+
+v 12.939379030730306 46.718010908304116 -37.730334592665685
+v 12.646480256221734 46.54492086737187 -37.89943977896245
+
+f 1894 1893 2031 2032
+f 1858 1861 2023 2022
+
+v 12.0580312982221 45.592499312110334 -38.23918094325118
+v 11.973082369410333 45.12029603789347 -38.28822623016865
+
+f 1898 1897 2033 2034
+
+v 36.7492163830047 46.01780627020205 -23.983718589001764
+v 36.7936695969611 44.96289494687396 -23.95805351396919
+v 36.75881591428442 44.07544610649142 -23.97817629705138
+v 36.65425486622562 43.26848839844069 -24.038544646297698
+v 36.48707533739893 42.5829495171284 -24.135065792267802
+v 36.2643662124188 42.05975715696123 -24.263646965521215
+v 35.870933116294154 41.51269030838758 -24.490795669464895
+v 35.380618597095406 41.115691672636444 -24.77387888909434
+v 34.757978231747195 40.87421827562814 -25.1333604716118
+v 33.96756759717468 40.79372714328297 -25.589704264219243
+v 33.245387477019364 40.85682400548185 -26.006655150962597
+v 32.638401731856064 41.046114592102704 -26.357098534299002
+v 32.14661036168462 41.36159890314554 -26.641034414228553
+v 31.77001336650536 41.80327693861035 -26.858462790751055
+v 31.49088853478017 42.38103955797846 -27.019615587467822
+v 31.29151365497175 43.10477762073116 -27.13472472797969
+v 31.171888727079786 43.97449112686845 -27.203790212286854
+v 31.132013751104584 44.990180076390345 -27.226812040389117
+v 31.18045446262563 45.94243109958346 -27.198844782546143
+v 31.32577659721912 46.85648294133196 -27.114943009017345
+v 31.58806532796171 47.67776534242745 -26.96351053972155
+v 31.781237433715468 48.021557975445454 -26.8519825724699
+v 32.034665058697065 48.34897953070178 -26.70566606497464
+v 32.35957227021395 48.6382019045131 -26.518080798955097
+v 32.76718313557351 48.8673969931961 -26.28274655613063
+v 33.278764308621184 49.01678307778796 -25.98738502817991
+v 33.91558244320233 49.06657843932582 -25.619717906781652
+v 34.59685379172511 49.0096207312873 -25.226385710350744
+v 35.18508352967757 48.838747607149095 -24.88677111282546
+v 35.68027165705979 48.553959066911226 -24.600874114205762
+v 36.082418173871865 48.15525511057368 -24.3686947144916
+v 36.39359067146008 47.61398635205191 -24.189039189262967
+v 36.61585674117097 46.901503405261366 -24.06071381409972
+
+f 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067
+
+v 13.905913372810257 44.57408184965611 -37.17230573014594
+v 14.004825465950407 44.81555524665628 -37.11519880654744
+
+f 1904 1903 2068 2069
+
+v 8.854012367724131 40.930152790914576 -40.08902213516705
+v 10.782188983007305 40.93015279094454 -38.975788847371305
+
+f 1972 1971 2070 2071
+
+v 14.791294754517187 45.04475033534517 -36.66113055111587
+v 15.031495333083642 44.83192632443485 -36.52245068243331
+
+f 1908 1907 2072 2073
+f 1829 1828 2066 2065
+
+v 5.560043983282012 40.930152790863396 -41.9907956684848
+v 7.488220598565187 40.93015279089336 -40.87756238068906
+
+f 1960 1959 2074 2075
+f 1830 1829 2065 2064
+
+v 17.583050739195304 45.0931814404363 -35.049309481970134
+v 17.773896804689578 45.12660572420461 -34.939124454691154
+
+f 1918 1917 2076 2077
+f 1831 1830 2064 2063
+
+v 5.3528048720541905 44.83192632428446 -42.11044522509443
+v 5.508234205435267 44.46221281814772 -42.02070805763696
+
+f 1957 1956 2078 2079
+f 1832 1831 2063 2062
+
+v 10.782188982916976 46.7255143189118 -38.975788847370815
+v 8.986338213780673 46.72551431888389 -40.01262377227862
+
+f 1928 1927 2080 2081
+f 1833 1832 2062 2061
+
+v 8.203053385097382 46.63274487821688 -40.464853478938146
+v 7.767059290316021 46.80054842527696 -40.71657478690493
+
+f 1932 1931 2082 2083
+f 1834 1833 2061 2060
+
+v 6.298100251183472 46.61091677450379 -41.56467868351503
+v 5.979820408356387 46.30532332292995 -41.748437636437934
+
+f 1936 1935 2084 2085
+f 1835 1834 2060 2059
+
+v 4.899522390679462 46.6518444688886 -42.372147987698995
+v 4.483031958908117 46.805323322906695 -42.612608850577736
+
+f 1940 1939 2086 2087
+f 1836 1835 2059 2058
+
+v 3.2606885697009282 46.71801090815372 -43.318329135326756
+v 2.9677897951922825 46.54492086722148 -43.487434321623574
+
+f 1942 1941 2088 2089
+f 1837 1836 2058 2057
+
+v 2.3793408371927236 45.59249931195994 -43.82717548591226
+v 2.294391908380883 45.120296037743074 -43.87622077282977
+
+f 1946 1945 2090 2091
+f 1838 1837 2057 2056
+
+v 4.227222911780878 44.57408184950572 -42.76030027280702
+v 4.326135004920957 44.81555524650589 -42.70319334920856
+
+f 1952 1951 2092 2093
+f 1839 1838 2056 2055
+f 1879 1878 2020 1988
+
+v 8.64060740373214 44.83738335025642 -40.21223154852953
+v 8.800661126686432 44.45539153579779 -40.11982448850745
+
+f 1969 1968 2094 2095
+f 1881 1880 1989 2025
+f 1846 1845 2049 2048
+f 1883 1882 2026 1998
+f 1847 1846 2048 2047
+
+v 4.542770580326664 45.04065756574528 -42.578118741439326
+v 4.806480780131399 45.11569167216137 -42.4258655866047
+
+f 1954 1953 2096 2097
+f 1848 1847 2047 2046
+f 1887 1886 2028 1973
+f 1849 1848 2046 2045
+f 1889 1888 1974 2029
+
+v 8.397554656746356 45.05430013060735 -40.352558117416606
+
+f 1968 1967 2098 2094
+f 1891 1890 2030 2000
+f 1853 1852 2042 2041
+f 1893 1892 2001 2031
+f 1854 1853 2041 2040
+f 1895 1894 2032 1990
+f 1855 1854 2040 2039
+f 1897 1896 1991 2033
+f 1856 1855 2039 2038
+
+v 7.739580632142553 44.992908588987305 -40.732439597613585
+v 7.9043602781659255 45.09318144028591 -40.63730402463121
+
+f 1965 1964 2099 2100
+
+v 8.095206343660125 45.12660572405422 -40.527118997352275
+
+f 1967 1966 2101 2098
+f 1964 1963 2005 2099
+
+v 34.25280925337377 43.21033696593396 -24.270319378861256
+v 34.32591337595887 43.67162618830891 -24.228112694006843
+v 34.82591337593086 43.67162618831678 -25.094138097807445
+v 34.75280925334577 43.21033696594183 -25.136344782661862
+
+f 2102 2103 2104 2105
+f 1905 1904 2069 1925
+
+v 34.150463481749874 42.87012550617794 -24.329408737657413
+v 34.650463481721864 42.87012550618581 -25.195434141458016
+
+f 2106 2102 2105 2107
+f 1907 1906 1926 2072
+
+v 34.020352912048914 42.62541200003676 -24.40452811009529
+v 34.52035291202091 42.62541200004463 -25.270553513895894
+
+f 2108 2106 2107 2109
+
+v 15.186924666464641 44.46221281829811 -36.43271351497589
+
+f 1909 1908 2073 2110
+
+v 33.86395439523296 42.45061663850637 -24.494824835874688
+v 34.36395439520495 42.45061663851424 -25.360850239675294
+
+f 2111 2108 2109 2112
+
+v 15.23873444431139 40.93015279101379 -36.40280112582372
+v 17.166911059594636 40.930152791043746 -35.289567838027935
+
+f 1912 1911 2113 2114
+
+v 33.68126793130192 42.34573942158676 -24.600298914995648
+v 34.181267931273915 42.34573942159462 -25.466324318796254
+
+f 2115 2111 2112 2116
+
+v 17.20486612924632 44.64093041723806 -35.26765446831536
+
+f 1915 1914 2117 1992
+
+v 33.472293520255796 42.310780349277906 -24.720950347458178
+v 33.972293520227794 42.31078034928577 -25.58697575125878
+
+f 2118 2115 2116 2119
+f 1917 1916 1993 2076
+
+v 33.198780722192204 42.370125506163156 -24.87886303503467
+v 33.698780722164194 42.37012550617102 -25.74488843883527
+
+f 2120 2118 2119 2121
+f 1919 1918 2077 2006
+
+v 32.96425678950953 42.548160976827994 -25.014265490688693
+v 33.464256789481524 42.54816097683586 -25.8802908944893
+
+f 2122 2120 2121 2123
+
+v 18.479351587715886 44.45539153594818 -34.531829945846326
+
+f 1921 1920 2007 2124
+
+v 32.77758282797615 42.8544365566341 -25.122041752619854
+v 33.277582827948144 42.85443655664197 -25.988067156420456
+
+f 2125 2122 2123 2126
+
+v 18.532702828753585 40.93015279106498 -34.50102759250593
+
+f 1924 1923 2127 1986
+
+v 32.64761994336062 43.298502040943134 -25.19707585902765
+v 33.147619943332614 43.29850204095101 -26.06310126282825
+
+f 2128 2125 2126 2129
+
+v 8.85401236767803 43.88786084002781 -40.0890221351668
+v 7.488220598515426 44.12251295460414 -40.87756238068879
+v 5.560043983235397 43.920602995501866 -41.99079566848455
+v 5.112604293487739 45.044750335194784 -42.24912509377699
+v 4.194252214071609 44.231653472970414 -42.77933591400653
+v 2.2660755987830874 44.56998908003458 -43.892569201802296
+
+f 2081 2080 2071 2070 2130 2095 2094 2098 2101 2100 2099 2005 2004 2131 2075 2074 2132 2079 2078 2133 2097 2096 2093 2092 2134 1997 1996 2135 2091 2090 2019 2018 2089 2088 1876 1875 2087 2086 2015 2014 2085 2084 1995 1994 2083 2082 2009 2008
+
+v 32.571414433739015 43.95470940792694 -25.24107313051217
+v 33.07141443371101 43.95470940793481 -26.107098534312776
+
+f 2136 2128 2129 2137
+f 1929 1928 2081 2008
+f 1966 1965 2100 2101
+f 1931 1930 2009 2082
+
+v 32.67760001782527 46.699763978187804 -25.179766854935554
+v 32.60449589524024 46.22960708869144 -25.221973539790017
+v 33.10449589521223 46.229607088699304 -26.08799894359062
+v 33.17760001779727 46.69976397819567 -26.045792258736164
+
+f 2138 2139 2140 2141
+f 1933 1932 2083 1994
+
+v 32.77994578944926 47.03929330970371 -25.120677496139358
+v 33.279945789421255 47.03929330971158 -25.98670289993996
+
+f 2142 2138 2141 2143
+f 1935 1934 1995 2084
+
+v 32.911533210111635 47.27923191816415 -25.04470546340146
+v 33.41153321008363 47.279231918172016 -25.910730867202062
+
+f 2144 2142 2143 2145
+f 1937 1936 2085 2014
+
+v 33.072362279812076 47.45061663849407 -24.95185075672177
+v 33.57236227978407 47.450616638501934 -25.817876160522378
+
+f 2146 2144 2145 2147
+f 1939 1938 2015 2086
+
+v 33.262432998550416 47.55344747069347 -24.842113376100382
+v 33.762432998522414 47.55344747070134 -25.708138779900988
+
+f 2148 2146 2147 2149
+f 1874 1940 2087 1875
+
+v 33.48174536632683 47.58772441476237 -24.715493321537206
+v 33.981745366298824 47.58772441477024 -25.581518725337812
+
+f 2150 2148 2149 2151
+f 1941 1873 1876 2088
+
+v 33.70386375093106 47.552765342460226 -24.587253212403876
+v 34.20386375090306 47.55276534246809 -25.453278616204482
+
+f 2152 2150 2151 2153
+f 1943 1942 2089 2018
+
+v 33.892900673999726 47.447888125546385 -24.47811269399251
+v 34.39290067397172 47.44788812555424 -25.344138097793117
+
+f 2154 2152 2153 2155
+f 1945 1944 2019 2090
+
+v 34.04885613553263 47.27309276402084 -24.388071766303206
+v 34.54885613550463 47.2730927640287 -25.254097170103808
+
+f 2156 2154 2155 2157
+f 1956 1955 2133 2078
+
+v 34.171730135530034 47.0283792578836 -24.31713042933582
+v 34.67173013550203 47.028379257891466 -25.183155833136425
+
+f 2158 2156 2157 2159
+f 1955 1954 2097 2133
+
+v 18.532702828707485 43.887860840178206 -34.50102759250568
+v 17.16691105954488 44.12251295475453 -35.289567838027665
+v 15.238734444264855 43.92060299565226 -36.40280112582343
+v 13.87294267510106 44.23165347312081 -37.19134137134541
+v 11.944766059812535 44.569989080184975 -38.304574659141174
+
+f 2020 1987 1986 2127 2160 2124 2007 2006 2077 2076 1993 1992 2117 2161 2114 2113 2162 2110 2073 2072 1926 1925 2069 2068 2163 2003 2002 2164 2034 2033 1991 1990 2032 2031 2001 2000 2030 2029 1974 1973 2028 2027 1999 1998 2026 2025 1989 1988
+f 1953 1952 2093 2096
+f 1885 1884 1999 2027
+f 1927 1972 2071 2080
+
+s 70
+f 1899 1898 2034 2164
+f 1900 1899 2164 2002
+
+s off
+
+s 71
+f 1922 1921 2124 2160
+f 1923 1922 2160 2127
+
+s off
+
+s 72
+f 1958 1957 2079 2132
+f 1959 1958 2132 2074
+
+s off
+
+s 73
+f 1913 1912 2114 2161
+f 1914 1913 2161 2117
+
+s off
+
+s 74
+f 1961 1960 2075 2131
+f 1962 1961 2131 2004
+
+s off
+
+s 75
+f 1910 1909 2110 2162
+f 1911 1910 2162 2113
+
+s off
+
+s 76
+f 1902 1901 2003 2163
+f 1903 1902 2163 2068
+
+s off
+
+s 77
+f 1947 1946 2091 2135
+f 1948 1947 2135 1996
+
+s off
+
+s 78
+
+v 32.54601259718725 44.897410635757375 -25.255738887673584
+v 33.04601259715924 44.89741063576524 -26.121764291474193
+
+f 2165 2136 2137 2166
+f 2139 2165 2166 2140
+
+s off
+
+s 79
+
+v 34.384396674012656 44.957437920915545 -24.194347346123234
+v 34.88439667398465 44.95743792092341 -25.06037274992384
+v 34.33123003938024 46.23097134519848 -24.225043116926305
+v 34.831230039352235 46.23097134520635 -25.09106852072691
+
+f 2103 2167 2168 2104
+f 2167 2169 2170 2168
+f 2169 2158 2159 2170
+
+s off
+
+s 80
+f 1850 1849 2045 2044
+f 1851 1850 2044 2043
+f 1852 1851 2043 2042
+
+s off
+
+s 81
+f 1840 1839 2055 2054
+f 1841 1840 2054 2053
+f 1842 1841 2053 2052
+f 1843 1842 2052 2051
+f 1844 1843 2051 2050
+f 1845 1844 2050 2049
+
+s off
+
+s 82
+f 1970 1969 2095 2130
+f 1971 1970 2130 2070
+
+s off
+
+s 83
+f 1826 1825 2036 2035
+f 1825 1857 2037 2036
+f 1827 1826 2035 2067
+f 1857 1856 2038 2037
+f 1828 1827 2067 2066
+
+s off
+
+s 84
+f 1950 1949 1997 2134
+f 1951 1950 2134 2092
+
+s off
+
+o 0.4mm-Entity65813
+
+v 38.145117766290035 44.96289494689525 19.8852258133884
+v 38.145117766272186 46.01780627022404 19.833895663320376
+v 38.14511776625455 46.90150340528545 19.67990521311584
+v 38.14511776623712 47.6139863520795 19.42325446277497
+v 38.14511776621989 48.15525511060616 19.063943412297583
+v 38.14511776620236 48.553959066950036 18.599584612843255
+v 38.145117766184015 48.8387476071957 18.02779061557183
+v 38.14511776616486 49.00962073134315 17.348561420483225
+v 38.14511776614489 49.066578439392394 16.561897027577345
+v 38.145117766127846 49.01678307786456 15.826562784739643
+v 38.145117766115874 48.86739699328075 15.235839728805121
+v 38.14511776610807 48.63820190460416 14.765171243129828
+v 38.145117766103525 48.34897953079795 14.390000711069726
+v 38.145117766101585 48.02155797554562 14.097367696062816
+v 38.145117766101585 47.67776534253064 13.874311761547016
+v 38.14511776610716 46.85648294143929 13.571446822938471
+v 38.14511776611748 45.94243109969307 13.40364327587147
+v 38.1451177661311 44.990180076500714 13.347708760182396
+v 38.1451177661482 43.9744911269782 13.3937524163895
+v 38.145117766165235 43.10477762083902 13.531883385011552
+v 38.1451177661822 42.38103955808319 13.762101666048189
+v 38.145117766199114 41.80327693871069 14.084407259499766
+v 38.1451177662166 41.361598903239944 14.519264012569135
+v 38.14511776623533 41.046114592189376 15.087135772460032
+v 38.145117766255304 40.85682400555897 15.788022539172102
+v 38.14511776627651 40.79372714334873 16.621924312705524
+v 38.145117766297375 40.874218275681464 17.53461189797152
+v 38.14511776631101 41.11569167267997 18.253575063046707
+v 38.145117766318485 41.512690308423394 18.819741502337315
+v 38.14511776632089 42.059757156990855 19.274038910250116
+v 38.14511776631889 42.58294951715452 19.531201256771343
+v 38.14511776631279 43.26848839846417 19.724243548722367
+v 38.14511776630302 44.07544610651326 19.844980247221766
+
+f 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203
+
+v 38.14511776613244 43.156619367087536 12.212647368640472
+v 38.14511776607488 43.15661936708794 9.83884109306064
+v 38.14511776610991 40.93015279137171 9.838841093060264
+v 38.14511776616747 40.930152791371306 12.212647368640095
+
+f 2204 2205 2206 2207
+
+v 38.1451177659664 42.42537789369178 4.88931858282861
+v 38.145117766064566 42.42537789369109 8.93843181611642
+v 38.1451177660358 44.253481577183585 8.93843181611673
+v 38.145117765861905 49.06657843939437 4.889318582829732
+v 38.14511776581494 49.0665784393947 2.9520743809197514
+v 38.1451177658923 44.149798084688015 2.9520743809189196
+v 38.14511776586796 44.149798084688186 1.9479816114782171
+v 38.14511776589509 42.42537789369228 1.9479816114779267
+v 38.14511776591943 42.42537789369211 2.952074380918629
+v 38.145117765942956 40.93015279137287 2.952074380918375
+v 38.14511776598992 40.93015279137254 4.889318582828356
+
+f 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218
+
+v 38.14511776533148 46.821864933259164 -18.446024719523987
+v 38.14511776534162 46.85648294144463 -18.005633668193816
+v 39.14511776534162 46.85648294146036 -18.005633668218064
+v 39.145117765331484 46.821864933274895 -18.446024719548234
+
+f 2219 2220 2221 2222
+
+v 38.14511776580507 46.725514319340455 1.0257442308511306
+v 38.1451177657548 46.7255143193408 -1.047925619080639
+v 38.1451177657681 45.879675301605474 -1.04792561908078
+v 38.145117765750065 46.33260845303801 -1.4981195992989365
+v 38.14511776573433 46.63274487868612 -1.952385032450418
+v 38.145117765719476 46.80054842575305 -2.4558276484121806
+v 38.14511776570411 46.8564829414421 -3.0535531770615414
+v 38.14511776569 46.79509139983238 -3.675206485383479
+v 38.14511776568134 46.610916775003 -4.1520354417273815
+v 38.14511776567724 46.305323323434166 -4.519553347593682
+v 38.145117765676815 45.879675301606106 -4.813273504483256
+v 38.14511776565719 46.35648294144248 -5.3132948209906905
+v 38.145117765641544 46.651844469409816 -5.766974050185767
+v 38.14511776562747 46.80532332343446 -6.247895775970095
+v 38.14511776561256 46.856482941442735 -6.829644582245722
+v 38.145117765602436 46.821864933257274 -7.270035633575891
+v 38.14511776559463 46.71801090870072 -7.659336345547271
+v 38.14511776558915 46.54492086777309 -7.997546718159777
+v 38.14511776558601 46.30259481047435 -8.284666751413578
+v 38.145117765585425 45.98659890324384 -8.513544757041116
+v 38.14511776558766 45.59249931252081 -8.677029046775283
+v 38.145117765592715 45.12029603830527 -8.775119620615714
+v 38.14511776560058 44.569989080597225 -8.807816478562593
+v 38.14511776565785 40.93015279137486 -8.807816478563208
+v 38.145117765711824 40.93015279137448 -6.581349902847013
+v 38.14511776565988 44.231653473502725 -6.581349902846364
+v 38.145117765655414 44.5740818500375 -6.543278620445307
+v 38.14511776565439 44.81555524703612 -6.429064773241897
+v 38.14511776565691 45.040657566272095 -6.17891555768941
+v 38.145117765663116 45.11569167268405 -5.874409248003192
+v 38.1451177656728 45.04475033571264 -5.520928262327884
+v 38.14511776568287 44.83192632479854 -5.243568524947236
+v 38.14511776569304 44.46221281865935 -5.064094190022334
+v 38.14511776570301 43.92060299601269 -5.004269411714075
+v 38.145117765750065 40.93015279137421 -5.00426941171467
+v 38.14511776580404 40.930152791373835 -2.777802835998385
+v 38.14511776575381 44.122512955084616 -2.7778028359978464
+v 38.14511776574672 44.64093041756756 -2.7339760965707662
+v 38.14511776574597 44.83806547895908 -2.636772822355127
+v 38.14511776574715 44.99290858946383 -2.4875572698311825
+v 38.145117765750186 45.09318144075984 -2.2972861238558693
+v 38.145117765755 45.12660572452515 -2.0769160692855735
+v 38.14511776576461 45.054300131073525 -1.7277943093947667
+v 38.14511776577482 44.837383350718774 -1.447141171604817
+v 38.145117765785315 44.45539153625762 -1.2623270515502836
+v 38.14511776579574 43.88786084048681 -1.2007223448655435
+v 38.14511776584228 40.93015279137358 -1.2007223448660427
+v 38.14511776589625 40.930152791373196 1.025744230850152
+
+f 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270
+
+v 39.14511776565691 45.04065756628783 -6.178915557713656
+v 39.145117765663116 45.11569167269979 -5.874409248027439
+
+f 2252 2251 2271 2272
+
+v 38.14511776553412 46.725514319342345 -10.150244855097052
+v 38.145117765483846 46.725514319342686 -12.223914705028822
+v 38.145117765497154 45.87967530160736 -12.223914705028967
+v 38.145117765479114 46.3326084530399 -12.674108685247122
+v 38.145117765463375 46.63274487868801 -13.128374118398513
+v 38.14511776544853 46.80054842575494 -13.631816734360275
+v 38.14511776543316 46.85648294144399 -14.229542263009634
+v 38.14511776541906 46.79509139983427 -14.851195571331663
+v 38.1451177654104 46.61091677500489 -15.32802452767548
+v 38.14511776540629 46.30532332343605 -15.695542433541867
+v 38.145117765405864 45.879675301607996 -15.989262590431354
+v 38.145117765386246 46.35648294144437 -16.489283906938784
+v 38.1451177653706 46.65184446941171 -16.942963136133862
+v 38.145117765356524 46.80532332343635 -17.423884861918282
+v 38.14511776532368 46.71801090870261 -18.835325431495367
+v 38.145117765318204 46.54492086777498 -19.173535804107964
+v 38.14511776531505 46.30259481047624 -19.460655837361763
+v 38.145117765314474 45.98659890324573 -19.6895338429893
+v 38.14511776531671 45.5924993125227 -19.853018132723378
+v 38.14511776532177 45.12029603830716 -19.951108706563897
+v 38.14511776532963 44.569989080599115 -19.983805564510778
+v 38.14511776538691 40.93015279137675 -19.983805564511393
+v 38.14511776544088 40.93015279137637 -17.75733898879511
+v 38.14511776538893 44.23165347350461 -17.757338988794547
+v 38.14511776538447 44.57408185003939 -17.719267706393403
+v 38.14511776538343 44.815555247038006 -17.605053859190082
+v 38.14511776538596 45.040657566273985 -17.354904643637596
+v 38.14511776539216 45.115691672685934 -17.050398333951286
+v 38.14511776540185 45.044750335714525 -16.69691734827607
+v 38.145117765411925 44.83192632480043 -16.41955761089542
+v 38.14511776542209 44.46221281866124 -16.240083275970427
+v 38.14511776543206 43.92060299601458 -16.18025849766226
+v 38.145117765479114 40.9301527913761 -16.180258497662766
+v 38.145117765533094 40.930152791375725 -13.95379192194657
+v 38.14511776548286 44.122512955086506 -13.953791921946028
+v 38.14511776547577 44.64093041756945 -13.909965182518954
+v 38.14511776547502 44.83806547896097 -13.812761908303312
+v 38.14511776547621 44.99290858946572 -13.663546355779367
+v 38.14511776547924 45.09318144076173 -13.473275209803964
+v 38.14511776548406 45.12660572452704 -13.252905155233758
+v 38.14511776549366 45.054300131075415 -12.90378339534286
+v 38.145117765503876 44.83738335072066 -12.623130257553003
+v 38.145117765514364 44.45539153625951 -12.438316137498466
+v 38.14511776552479 43.88786084048869 -12.376711430813726
+v 38.14511776557132 40.93015279137546 -12.376711430814227
+v 38.1451177656253 40.93015279137508 -10.150244855098032
+
+f 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2220 2219 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318
+
+v 39.14511776567682 45.87967530162184 -4.8132735045074995
+v 39.1451177656572 46.35648294145822 -5.313294821014937
+
+f 2234 2233 2319 2320
+
+v 39.145117766064566 42.42537789370682 8.938431816092178
+v 39.1451177659664 42.42537789370751 4.889318582804367
+v 39.145117765989916 40.930152791388274 4.889318582804113
+v 39.145117765942956 40.93015279138861 2.952074380894132
+v 39.14511776591943 42.42537789370784 2.952074380894386
+v 39.14511776589508 42.42537789370802 1.9479816114536834
+v 39.14511776586795 44.149798084703924 1.9479816114539739
+v 39.145117765892294 44.149798084703754 2.9520743808946763
+v 39.14511776581493 49.066578439410435 2.952074380895508
+v 39.145117765861905 49.0665784394101 4.889318582805489
+v 39.145117766035796 44.253481577199324 8.938431816092487
+
+f 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331
+
+v 39.14511776589625 40.93015279138893 1.0257442308259088
+v 39.145117765805075 46.725514319356186 1.0257442308268874
+
+f 2223 2270 2332 2333
+f 2209 2208 2322 2321
+
+v 39.145117765768106 45.879675301621205 -1.0479256191050232
+v 39.145117765750065 46.33260845305375 -1.4981195993231797
+
+f 2226 2225 2334 2335
+f 2210 2209 2321 2331
+
+v 39.14511776558601 46.30259481049009 -8.284666751437824
+v 39.145117765585425 45.98659890325957 -8.513544757065361
+
+f 2242 2241 2336 2337
+f 2211 2210 2331 2330
+
+v 39.14511776574597 44.83806547897481 -2.6367728223793705
+v 39.14511776574716 44.99290858947957 -2.487557269855426
+
+f 2262 2261 2338 2339
+f 2212 2211 2330 2329
+
+v 39.14511776543316 46.856482941459724 -14.22954226303388
+v 39.14511776541906 46.79509139985 -14.85119557135591
+
+f 2280 2279 2340 2341
+f 2213 2212 2329 2328
+
+v 39.14511776538691 40.93015279139248 -19.98380556453564
+v 39.14511776544088 40.93015279139211 -17.757338988819352
+
+f 2295 2294 2342 2343
+f 2214 2213 2328 2327
+
+v 39.145117765704114 46.856482941457834 -3.0535531770857847
+v 39.14511776569001 46.79509139984812 -3.675206485407722
+
+f 2230 2229 2344 2345
+f 2215 2214 2327 2326
+
+v 39.14511776561257 46.85648294145847 -6.829644582269968
+v 39.145117765602436 46.821864933273005 -7.270035633600138
+
+f 2238 2237 2346 2347
+f 2216 2215 2326 2325
+
+v 39.14511776565786 40.93015279139059 -8.807816478587453
+v 39.14511776571183 40.93015279139022 -6.581349902871258
+
+f 2247 2246 2348 2349
+f 2217 2216 2325 2324
+
+v 39.14511776547577 44.64093041758518 -13.9099651825432
+v 39.14511776547503 44.83806547897671 -13.812761908327559
+
+f 2309 2308 2350 2351
+f 2218 2217 2324 2323
+
+v 39.14511776576462 45.05430013108926 -1.72779430941901
+v 39.14511776577483 44.8373833507345 -1.4471411716290603
+
+f 2266 2265 2352 2353
+f 2208 2218 2323 2322
+
+v 39.145117765497154 45.879675301623095 -12.223914705053215
+v 39.145117765479114 46.33260845305564 -12.674108685271367
+
+f 2276 2275 2354 2355
+
+v 38.145117765899656 46.66744815290051 4.8893185828293255
+v 38.14511776593926 44.14979808468768 4.8893185828289
+v 39.14511776593926 44.14979808470341 4.889318582804657
+v 39.14511776589965 46.66744815291624 4.889318582805083
+
+f 2356 2357 2358 2359
+
+v 39.14511776540587 45.87967530162373 -15.9892625904556
+v 39.145117765386246 46.35648294146011 -16.48928390696303
+
+f 2284 2283 2360 2361
+
+v 38.14511776599113 44.149798084687326 7.028813807931225
+v 39.14511776599113 44.14979808470306 7.028813807906982
+
+f 2362 2356 2359 2363
+
+v 39.14511776531506 46.30259481049198 -19.46065583738601
+v 39.14511776531448 45.98659890326146 -19.689533843013546
+
+f 2290 2289 2364 2365
+f 2357 2362 2363 2358
+
+v 39.1451177657548 46.725514319356535 -1.0479256191048822
+
+f 2224 2223 2333 2366
+
+v 39.14511776607488 43.15661936710368 9.838841093036397
+v 39.14511776613244 43.156619367103275 12.212647368616228
+v 39.145117766167466 40.93015279138704 12.212647368615851
+v 39.14511776610991 40.93015279138744 9.83884109303602
+
+f 2367 2368 2369 2370
+
+v 39.145117765734334 46.63274487870185 -1.9523850324746612
+v 39.14511776571948 46.80054842576879 -2.455827648436424
+
+f 2228 2227 2371 2372
+f 2205 2204 2368 2367
+
+v 39.14511776568135 46.61091677501873 -4.152035441751625
+v 39.14511776567725 46.30532332344989 -4.519553347617925
+
+f 2232 2231 2373 2374
+f 2206 2205 2367 2370
+
+v 39.14511776564155 46.65184446942555 -5.766974050210013
+v 39.14511776562748 46.80532332345019 -6.247895775994341
+
+f 2236 2235 2375 2376
+f 2207 2206 2370 2369
+
+v 39.145117765594634 46.71801090871646 -7.659336345571518
+v 39.145117765589156 46.54492086778882 -7.997546718184023
+
+f 2240 2239 2377 2378
+f 2204 2207 2369 2368
+
+v 39.14511776558766 45.59249931253654 -8.677029046799529
+v 39.14511776559272 45.12029603832101 -8.775119620639959
+
+f 2244 2243 2379 2380
+
+v 39.145117766272186 46.01780627023978 19.833895663296133
+v 39.14511776629003 44.962894946910986 19.885225813364155
+v 39.14511776630302 44.07544610652899 19.844980247197523
+v 39.14511776631279 43.26848839847991 19.724243548698123
+v 39.14511776631889 42.582949517170256 19.5312012567471
+v 39.14511776632089 42.05975715700658 19.274038910225872
+v 39.145117766318485 41.512690308439126 18.81974150231307
+v 39.145117766311 41.115691672695704 18.253575063022463
+v 39.145117766297375 40.874218275697196 17.534611897947276
+v 39.14511776627651 40.793727143364464 16.62192431268128
+v 39.1451177662553 40.85682400557471 15.788022539147859
+v 39.14511776623533 41.04611459220511 15.08713577243579
+v 39.1451177662166 41.36159890325568 14.519264012544891
+v 39.14511776619911 41.80327693872643 14.084407259475526
+v 39.1451177661822 42.38103955809893 13.762101666023945
+v 39.145117766165235 43.104777620854755 13.531883384987312
+v 39.1451177661482 43.97449112699394 13.39375241636526
+v 39.1451177661311 44.99018007651645 13.347708760158149
+v 39.14511776611748 45.9424310997088 13.40364327584723
+v 39.14511776610716 46.85648294145502 13.571446822914224
+v 39.145117766101585 47.67776534254638 13.874311761522776
+v 39.145117766101585 48.02155797556134 14.097367696038573
+v 39.145117766103525 48.348979530813686 14.390000711045483
+v 39.14511776610807 48.63820190461989 14.765171243105586
+v 39.145117766115874 48.86739699329648 15.235839728780878
+v 39.145117766127846 49.01678307788029 15.8265627847154
+v 39.14511776614489 49.06657843940813 16.5618970275531
+v 39.14511776616486 49.009620731358886 17.34856142045898
+v 39.14511776618401 48.83874760721144 18.027790615547588
+v 39.145117766202354 48.553959066965774 18.59958461281901
+v 39.14511776621988 48.155255110621894 19.06394341227334
+v 39.145117766237114 47.61398635209524 19.423254462750727
+v 39.14511776625455 46.90150340530119 19.679905213091597
+
+f 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413
+
+v 39.14511776565542 44.57408185005324 -6.543278620469552
+v 39.14511776565439 44.81555524705186 -6.429064773266143
+
+f 2250 2249 2414 2415
+
+v 39.14511776557133 40.930152791391194 -12.376711430838474
+v 39.14511776562531 40.93015279139082 -10.15024485512228
+
+f 2318 2317 2416 2417
+
+v 39.1451177656728 45.044750335728374 -5.52092826235213
+v 39.145117765682876 44.831926324814276 -5.243568524971482
+
+f 2254 2253 2418 2419
+f 2175 2174 2412 2411
+
+v 39.145117765479114 40.93015279139184 -16.18025849768701
+v 39.145117765533094 40.930152791391464 -13.953791921970817
+
+f 2306 2305 2420 2421
+f 2176 2175 2411 2410
+
+v 39.14511776575019 45.09318144077558 -2.2972861238801126
+v 39.14511776575501 45.12660572454089 -2.0769160693098168
+
+f 2264 2263 2422 2423
+f 2177 2176 2410 2409
+
+v 39.145117765411925 44.831926324816166 -16.419557610919668
+v 39.14511776542209 44.46221281867698 -16.240083275994674
+
+f 2303 2302 2424 2425
+f 2178 2177 2409 2408
+
+v 39.145117765534124 46.725514319358076 -10.150244855121297
+v 39.14511776548385 46.725514319358425 -12.223914705053067
+
+f 2274 2273 2426 2427
+f 2179 2178 2408 2407
+
+v 39.14511776546338 46.63274487870374 -13.128374118422759
+v 39.14511776544853 46.80054842577067 -13.631816734384522
+
+f 2278 2277 2428 2429
+f 2180 2179 2407 2406
+
+v 39.1451177654104 46.61091677502062 -15.328024527699725
+v 39.1451177654063 46.30532332345178 -15.695542433566112
+
+f 2282 2281 2430 2431
+f 2181 2180 2406 2405
+
+v 39.1451177653706 46.65184446942744 -16.94296313615811
+v 39.145117765356524 46.80532332345208 -17.423884861942526
+
+f 2286 2285 2432 2433
+f 2182 2181 2405 2404
+
+v 39.14511776532368 46.71801090871834 -18.835325431519614
+v 39.145117765318204 46.54492086779071 -19.173535804132207
+
+f 2288 2287 2434 2435
+f 2183 2182 2404 2403
+
+v 39.14511776531672 45.59249931253843 -19.853018132747625
+v 39.14511776532177 45.1202960383229 -19.951108706588144
+
+f 2292 2291 2436 2437
+f 2184 2183 2403 2402
+
+v 39.14511776538447 44.57408185005513 -17.719267706417646
+v 39.14511776538344 44.81555524705374 -17.605053859214326
+
+f 2298 2297 2438 2439
+f 2185 2184 2402 2401
+f 2225 2224 2366 2334
+
+v 39.145117765503876 44.83738335073639 -12.623130257577248
+v 39.14511776551437 44.45539153627525 -12.438316137522712
+
+f 2315 2314 2440 2441
+f 2227 2226 2335 2371
+f 2192 2191 2395 2394
+f 2229 2228 2372 2344
+f 2193 2192 2394 2393
+
+v 39.14511776538597 45.040657566289724 -17.35490464366184
+v 39.145117765392165 45.11569167270167 -17.050398333975533
+
+f 2300 2299 2442 2443
+f 2194 2193 2393 2392
+f 2233 2232 2374 2319
+f 2195 2194 2392 2391
+f 2235 2234 2320 2375
+
+v 39.14511776549366 45.05430013109115 -12.903783395367105
+
+f 2314 2313 2444 2440
+f 2237 2236 2376 2346
+f 2199 2198 2388 2387
+f 2239 2238 2347 2377
+f 2200 2199 2387 2386
+f 2241 2240 2378 2336
+f 2201 2200 2386 2385
+f 2243 2242 2337 2379
+f 2202 2201 2385 2384
+
+v 39.14511776547621 44.99290858948145 -13.663546355803614
+v 39.14511776547924 45.09318144077747 -13.473275209828211
+
+f 2311 2310 2445 2446
+
+v 39.14511776548406 45.12660572454277 -13.252905155258004
+
+f 2313 2312 2447 2444
+f 2310 2309 2351 2445
+
+v 38.145117766260476 43.21033696599523 17.528643275871065
+v 38.14511776625526 43.67162618836903 17.613056645584624
+v 39.14511776625526 43.671626188384764 17.61305664556038
+v 39.14511776626047 43.21033696601097 17.52864327584682
+
+f 2448 2449 2450 2451
+f 2251 2250 2415 2271
+
+v 38.14511776626296 42.87012550624082 17.410464558272135
+v 39.145117766262956 42.87012550625656 17.410464558247888
+
+f 2452 2448 2451 2453
+f 2253 2252 2272 2418
+
+v 38.14511776626317 42.62541200010169 17.26022581338797
+v 39.14511776626317 42.62541200011743 17.260225813363725
+
+f 2454 2452 2453 2455
+
+v 39.145117765693044 44.46221281867509 -5.064094190046579
+
+f 2255 2254 2419 2456
+
+v 38.14511776626154 42.450616638573756 17.07963236181906
+v 39.145117766261535 42.450616638589494 17.079632361794815
+
+f 2457 2454 2455 2458
+
+v 39.145117765750065 40.93015279138995 -5.004269411738913
+v 39.145117765804045 40.930152791389574 -2.7778028360226283
+
+f 2258 2257 2459 2460
+
+v 38.145117766258075 42.34573942165702 16.86868420356532
+v 39.145117766258075 42.34573942167275 16.868684203541076
+
+f 2461 2457 2458 2462
+
+v 39.145117765746726 44.64093041758329 -2.7339760965950095
+
+f 2261 2260 2463 2338
+
+v 38.145117766252774 42.310780349351454 16.627381338626748
+v 39.145117766252774 42.31078034936719 16.627381338602504
+
+f 2464 2461 2462 2465
+f 2263 2262 2339 2422
+
+v 38.145117766244184 42.37012550624101 16.311555963456076
+v 39.145117766244184 42.37012550625674 16.311555963431836
+
+f 2466 2464 2465 2467
+f 2265 2264 2423 2352
+
+v 38.14511776623482 42.54816097690954 16.04075105213286
+v 39.14511776623481 42.548160976925274 16.040751052108618
+
+f 2468 2466 2467 2469
+
+v 39.145117765785315 44.45539153627336 -1.2623270515745268
+
+f 2267 2266 2353 2470
+
+v 38.14511776622477 42.85443655671858 15.825198528258472
+v 39.14511776622477 42.85443655673432 15.825198528234226
+
+f 2471 2468 2469 2472
+
+v 39.14511776584228 40.93015279138931 -1.200722344890286
+
+f 2270 2269 2473 2332
+
+v 38.14511776621415 43.29850204102966 15.675130315434469
+v 39.14511776621414 43.298502041045396 15.675130315410227
+
+f 2474 2471 2472 2475
+
+v 39.145117765524795 43.88786084050443 -12.376711430837972
+v 39.145117765482865 44.12251295510224 -13.953791921970275
+v 39.14511776543207 43.92060299603031 -16.180258497686502
+v 39.14511776540186 45.044750335730264 -16.696917348300314
+v 39.14511776538894 44.231653473520346 -17.757338988818795
+v 39.14511776532964 44.56998908061485 -19.983805564535025
+
+f 2427 2426 2417 2416 2476 2441 2440 2444 2447 2446 2445 2351 2350 2477 2421 2420 2478 2425 2424 2479 2443 2442 2439 2438 2480 2343 2342 2481 2437 2436 2365 2364 2435 2434 2222 2221 2433 2432 2361 2360 2431 2430 2341 2340 2429 2428 2355 2354
+
+v 38.145117766201686 43.954709408014665 15.587135772460503
+v 39.145117766201686 43.9547094080304 15.587135772436257
+
+f 2482 2474 2475 2483
+f 2275 2274 2427 2354
+f 2312 2311 2446 2447
+f 2277 2276 2355 2428
+
+v 38.14511776616147 46.69976397827386 15.709748323620596
+v 38.14511776616682 46.22960708877864 15.625334953906947
+v 39.14511776616682 46.22960708879438 15.625334953882703
+v 39.14511776616147 46.69976397828959 15.709748323596354
+
+f 2484 2485 2486 2487
+f 2279 2278 2429 2340
+
+v 38.145117766159 47.03929330978816 15.827927041219617
+v 39.14511776615899 47.039293309803895 15.827927041195373
+
+f 2488 2484 2487 2489
+f 2281 2280 2341 2430
+
+v 38.145117766158904 47.279231918246516 15.979871106703925
+v 39.145117766158904 47.279231918262255 15.979871106679681
+
+f 2490 2488 2489 2491
+f 2283 2282 2431 2360
+
+v 38.14511776616071 47.45061663857391 16.1655805200737
+v 39.14511776616071 47.45061663858964 16.165580520049456
+
+f 2492 2490 2491 2493
+f 2285 2284 2361 2432
+
+v 38.14511776616441 47.55344747077032 16.385055281328768
+v 39.14511776616441 47.55344747078606 16.385055281304524
+
+f 2494 2492 2493 2495
+f 2220 2286 2433 2221
+
+v 38.14511776617002 47.58772441483577 16.638295390469306
+v 39.14511776617001 47.58772441485151 16.638295390445062
+
+f 2496 2494 2495 2497
+f 2287 2219 2222 2434
+
+v 38.14511776617678 47.55276534253014 16.894775608750333
+v 39.14511776617678 47.55276534254586 16.89477560872609
+
+f 2498 2496 2497 2499
+f 2289 2288 2435 2364
+
+v 38.14511776618372 47.44788812561331 17.113056645585285
+v 39.14511776618372 47.44788812562905 17.11305664556104
+
+f 2500 2498 2499 2501
+f 2291 2290 2365 2436
+
+v 38.145117766190836 47.27309276408532 17.293138500973985
+v 39.145117766190836 47.27309276410105 17.29313850094974
+
+f 2502 2500 2501 2503
+f 2302 2301 2479 2424
+
+v 38.145117766198126 47.02837925794615 17.435021174916702
+v 39.145117766198126 47.02837925796188 17.435021174892455
+
+f 2504 2502 2503 2505
+f 2301 2300 2443 2479
+
+v 39.14511776579574 43.88786084050254 -1.2007223448897868
+v 39.14511776575382 44.12251295510035 -2.7778028360220897
+v 39.14511776570301 43.92060299602842 -5.004269411738318
+v 39.14511776565988 44.231653473518456 -6.58134990287061
+v 39.14511776560058 44.56998908061296 -8.807816478586838
+
+f 2366 2333 2332 2473 2506 2470 2353 2352 2423 2422 2339 2338 2463 2507 2460 2459 2508 2456 2419 2418 2272 2271 2415 2414 2509 2349 2348 2510 2380 2379 2337 2336 2378 2377 2347 2346 2376 2375 2320 2319 2374 2373 2345 2344 2372 2371 2335 2334
+f 2299 2298 2439 2442
+f 2231 2230 2345 2373
+f 2273 2318 2417 2426
+
+s 85
+f 2245 2244 2380 2510
+f 2246 2245 2510 2348
+
+s off
+
+s 86
+f 2268 2267 2470 2506
+f 2269 2268 2506 2473
+
+s off
+
+s 87
+f 2304 2303 2425 2478
+f 2305 2304 2478 2420
+
+s off
+
+s 88
+f 2259 2258 2460 2507
+f 2260 2259 2507 2463
+
+s off
+
+s 89
+f 2307 2306 2421 2477
+f 2308 2307 2477 2350
+
+s off
+
+s 90
+f 2256 2255 2456 2508
+f 2257 2256 2508 2459
+
+s off
+
+s 91
+f 2248 2247 2349 2509
+f 2249 2248 2509 2414
+
+s off
+
+s 92
+f 2293 2292 2437 2481
+f 2294 2293 2481 2342
+
+s off
+
+s 93
+
+v 38.14511776618615 44.897410635845496 15.557804258136027
+v 39.14511776618614 44.897410635861235 15.557804258111783
+
+f 2511 2482 2483 2512
+f 2485 2511 2512 2486
+
+s off
+
+s 94
+
+v 38.145117766236666 44.95743792097474 17.680587341355626
+v 39.145117766236666 44.95743792099048 17.680587341331382
+v 38.14511776621514 46.23097134525852 17.619195799746045
+v 39.14511776621514 46.230971345274256 17.6191957997218
+
+f 2449 2513 2514 2450
+f 2513 2515 2516 2514
+f 2515 2504 2505 2516
+
+s off
+
+s 95
+f 2196 2195 2391 2390
+f 2197 2196 2390 2389
+f 2198 2197 2389 2388
+
+s off
+
+s 96
+f 2186 2185 2401 2400
+f 2187 2186 2400 2399
+f 2188 2187 2399 2398
+f 2189 2188 2398 2397
+f 2190 2189 2397 2396
+f 2191 2190 2396 2395
+
+s off
+
+s 97
+f 2316 2315 2441 2476
+f 2317 2316 2476 2416
+
+s off
+
+s 98
+f 2172 2171 2382 2381
+f 2171 2203 2383 2382
+f 2173 2172 2381 2413
+f 2203 2202 2384 2383
+f 2174 2173 2413 2412
+
+s off
+
+s 99
+f 2296 2295 2343 2480
+f 2297 2296 2480 2438
+
+s off

+ 1932 - 0
gcode_viewer/js/prettygcode.js

@@ -0,0 +1,1932 @@
+$(function () {
+
+    console.log("Create PrettyGCode View Model");
+    function PrettyGCodeViewModel(parameters) {
+        var self = this;
+        self.printerProfiles = parameters[2];
+        self.controlViewModel = parameters[3];
+
+        //Parse terminal data for file and pos updates.
+        var curJobName = "";
+        var durJobDate = 0;//use date of file to check for update. 
+        function updateJob(job) {
+
+            if (durJobDate != job.file.date) {
+                curJobName = job.file.path;
+                durJobDate = job.file.date;
+                if (viewInitialized && gcodeProxy) {
+                    gcodeProxy.loadGcode('downloads/files/local/' + curJobName);
+                    printHeadSim = new PrintHeadSimulator();
+
+                    //terminalGcodeProxy = new GCodeParser();
+                    //terminalGcodeProxy;//used to display gcode actualy sent to printer.
+                }
+            }
+
+        }
+        self.fromHistoryData = function (data) {
+            if (!viewInitialized)
+                return;
+
+            updateJob(data.job);
+        };
+
+        /* Arc Interpolation Parameters */
+        self.mm_per_arc_segment = 1.0;  // The absolute longest length of an interpolated segment
+        self.min_arc_segments = 20;  // The minimum number of interpolated segments in a full circle, 0 to disable
+        // The absolute minimum length of an interpolated segment.
+        // Limited by mm_per_arc_segment as a max and min_arc_segments as a minimum, 0 to disable
+        self.min_mm_per_arc_segment = 0.1;
+        // This controls how many arcs will be drawn before the exact position of the
+        // next segment is recalculated.  Reduces the number of sin/cos calls.
+        // 0 to disable
+        self.n_arc_correction = 24;
+
+        // A function to interpolate arcs into straight segments.  Returns an array of positions
+        self.interpolateArc = function (state, arc) {
+            // This is adapted from the Marlin arc interpolation routine found at
+            // https://github.com/MarlinFirmware/Marlin/
+            // The license can be found here: https://github.com/MarlinFirmware/Marlin/blob/2.0.x/LICENSE
+            // This allows the rendered arcs to be VERY close to what would be printed,
+            // depending on the firmware settings.
+
+            // Create vars to hold the initial and current position so we don't affect the state
+            var initial_position = {}, current_position = {};
+            Object.assign(initial_position, state)
+            Object.assign(current_position, state)
+            // Create the results which contain the copied initial position
+            var interpolated_segments = [initial_position];
+
+            // note that arc.is_clockwise determines if this is a G2, else it is a G3
+            // I'm going to also extract all the necessary variables up front to make this easier
+            // to convert from the source c++ arc interpolation code
+
+            // Convert r format to i j format if necessary
+            // I have no code like this to test, so I am not 100% sure this will work as expected
+            // commenting out for now
+            /*
+            if (arc.r)
+            {
+
+                if (arc.x != current_position.x || arc.y != current_position.y) {
+                    var vector = {x: (arc.x - current_position.x)/2.0, y: (arc.y - current_position.y)/2.0};
+                    var e = arc.is_clockwise ^ (arc.r < 0) ? -1 : 1;
+                    var len = Math.sqrt(Math.pow(vector.x,2) + Math.pow(vector.y,2));
+                    var h2 = (arc.r - len) * (arc.r + len);
+                    var h = (h2 >= 0) ? Math.sqrt(h2) : 0.0;
+                    var bisector = {x: -1.0*vector.y, y: vector.x };
+                    arc.i = (vector.x + bisector.x) / len * e * h;
+                    arc.j = (vector.y + bisector.y) / len * e * h;
+                }
+            }*/
+
+            // Calculate the radius, we will be using it a lot.
+            var radius = Math.hypot(arc.i, arc.j);
+            // Radius Vector
+            var v_radius = { x: -1.0 * arc.i, y: -1.0 * arc.j };
+            // Center of arc
+            var center = { x: current_position.x - v_radius.x, y: current_position.y - v_radius.y };
+            // Z Travel Total
+            var travel_z = arc.z - current_position.z;
+            // Extruder Travel
+            var travel_e = arc.e - current_position.e;
+            // Radius Target Vector
+            var v_radius_target = { x: arc.x - center.x, y: arc.y - center.y };
+
+            var angular_travel_total = Math.atan2(
+                v_radius.x * v_radius_target.y - v_radius.y * v_radius_target.x,
+                v_radius.x * v_radius_target.x + v_radius.y * v_radius_target.y
+            );
+            // Having a positive angle is convenient here.  We will make it negative later
+            // if we need to.
+            if (angular_travel_total < 0) { angular_travel_total += 2.0 * Math.PI }
+
+            // Copy our mm_per_arc_segments var because we may be modifying it for this arc
+            var mm_per_arc_segment = self.mm_per_arc_segment;
+
+            // Enforce min_arc_segments if it is greater than 0
+            if (self.min_arc_segments > 0) {
+                mm_per_arc_segment = (radius * ((2.0 * Math.PI) / self.min_arc_segments));
+                // We will need to enforce our max segment length later, flag this
+            }
+
+            // Enforce the minimum segment length if it is set
+            if (self.min_mm_per_arc_segment > 0) {
+                if (mm_per_arc_segment < self.min_mm_per_arc_segment) {
+                    mm_per_arc_segment = self.min_mm_per_arc_segment;
+                }
+            }
+
+            // Enforce the maximum segment length
+            if (mm_per_arc_segment > self.mm_per_arc_segment) {
+                mm_per_arc_segment = self.mm_per_arc_segment;
+            }
+
+            // Adjust the angular travel if the direction is clockwise
+            if (arc.is_clockwise) { angular_travel_total -= (2.0 * Math.PI); }
+
+            // Compensate for a full circle, which would give us an angle of 0 here
+            // We want that to be 2Pi.  Note, full circles are bad in 3d printing, but they
+            // should still render correctly
+            if (current_position.x == arc.x && current_position.y == arc.y && angular_travel_total == 0) {
+                angular_travel_total += 2.0 * Math.PI;
+            }
+
+            // Now it's time to calculate the mm of total travel along the arc, making sure we take Z into account
+            var mm_of_travel_arc = Math.hypot(angular_travel_total * radius, Math.abs(travel_z));
+
+            // Get the number of segments total we will be generating
+            var num_segments = Math.ceil(mm_of_travel_arc / mm_per_arc_segment);
+
+            // Calculate xy_segment_theta, z_segment_theta, and e_segment_theta
+            // This is the distance we will be moving for each interpolated segment
+            var xy_segment_theta = angular_travel_total / num_segments;
+            var z_segment_theta = travel_z / num_segments;
+            var e_segment_theta = travel_e / num_segments;
+
+            // Time to interpolate!
+            if (num_segments > 1) {
+                // it's possible for num_segments to be zero.  If that's true, we just need to draw a line
+                // from the start to the end coordinates, and this isn't needed.
+
+                // I am NOT going to use the small angel approximation for sin and cos here, but it
+                // could be easily added if performance is a problem.  Here is code for this if it becomes
+                // necessary:
+                //var sq_theta_per_segment = theta_per_segment * theta_per_segment;
+                //var sin_T = theta_per_segment - sq_theta_per_segment * theta_per_segment / 6;
+                //var cos_T = 1 - 0.5f * sq_theta_per_segment; // Small angle approximation
+                var cos_t = Math.cos(xy_segment_theta);
+                var sin_t = Math.sin(xy_segment_theta);
+                var r_axisi;
+
+                // We are going to correct sin and cos only occasionally to reduce cpu usage
+                var count = 0;
+                // Loop through each interpolated segment, minus the endpoint which will be handled separately
+                for (var i = 1; i < num_segments; i++) {
+
+                    if (count < self.n_arc_correction) {
+                        // not time to recalculate X and Y.
+                        // Apply the rotational vector
+                        r_axisi = v_radius.x * sin_t + v_radius.y * cos_t;
+                        v_radius.x = v_radius.x * cos_t - v_radius.y * sin_t;
+                        v_radius.y = r_axisi;
+                        count++;
+                    }
+                    else {
+                        // Arc correction to radius vector. Computed only every N_ARC_CORRECTION increments.
+                        // Compute exact location by applying transformation matrix from initial radius vector(=-offset).
+                        var sin_ti = Math.sin(i * xy_segment_theta);
+                        var cos_ti = Math.cos(i * xy_segment_theta);
+                        v_radius.x = (-1.0 * arc.i) * cos_ti + arc.j * sin_ti;
+                        v_radius.y = (-1.0 * arc.i) * sin_ti - arc.j * cos_ti;
+                        count = 0;
+                    }
+
+                    // Draw the segment
+                    var line = {
+                        x: center.x + v_radius.x,
+                        y: center.y + v_radius.y,
+                        z: current_position.z + z_segment_theta,
+                        e: current_position.e + e_segment_theta,
+                        f: arc.f
+                    };
+                    /*console.debug(
+                        "Arc Segment " + i.toString() + ":" +
+                        " X" + line.x.toString() +
+                        " Y" + line.y.toString() +
+                        " Z" + line.z.toString() +
+                        " E" + line.e.toString() +
+                        " F" + line.f.toString()
+                    );*/
+                    interpolated_segments.push(line);
+
+                    // Update the current state
+                    current_position.x = line.x;
+                    current_position.y = line.y;
+                    current_position.z = line.z;
+                    current_position.e = line.e;
+                }
+            }
+            // Move to the target position
+            var line = {
+                x: arc.x,
+                y: arc.y,
+                z: arc.z,
+                e: arc.e,
+                f: arc.f
+            };
+            interpolated_segments.push(line);
+            //Done!!!
+            return interpolated_segments;
+        };
+
+        //used to animate the nozzle position in response to terminal messages
+        function PrintHeadSimulator() {
+            var buffer = [];
+            var HeadState = function () {
+                this.position = new THREE.Vector3(0, 0, 0);
+                this.rate = 5.0 * 60;
+                this.extrude = false;
+                this.relative = false;
+                //this.lastExtrudedZ=0;//used to better calc layer number
+                this.layerLineNumber = 0;
+                this.clone = function () {
+                    var newState = new HeadState();
+                    newState.position.copy(this.position);
+                    newState.rate = this.rate;
+                    newState.extrude = this.extrude;
+                    newState.relative = this.relative;
+                    //newState.lastExtrudedZ=this.lastExtrudedZ;
+                    newState.layerLineNumber = this.layerLineNumber;
+                    return (newState);
+                }
+            };
+            var curState = new HeadState();
+            var curEnd = new HeadState();
+            var parserCurState = new HeadState();
+
+            var observedLayerCount = 0;
+            var parserLayerLineNumber = 0;
+            var parserLastExtrudedZ = 0;
+
+            var curLastExtrudedZ = 0;
+
+            parserCurState.extrude = true;
+
+
+            this.getCurPosition = function () {
+                return ({ position: curState.position, layerZ: curLastExtrudedZ, lineNumber: curState.layerLineNumber });
+            }
+
+            this.getBufferStats = function () {
+                return (buffer.length);
+            }
+            //
+            //var currentFileOffset=0;
+
+            //add gcode command to the buffer
+            this.addCommand = function (cmd) {
+                //currentFileOffset+=cmd.length;
+                if (buffer.length > 1000) {
+                    console.log("PrintHeadSimulator buffer overflow")
+                    return;
+                }
+                var is_g0_g1 = cmd.indexOf(" G0") > -1 || cmd.indexOf(" G1") > -1;
+                var is_g2_g3 = !is_g0_g1 && cmd.indexOf(" G2") > -1 || cmd.indexOf(" G3") > -1;
+                if (is_g0_g1 || is_g2_g3) {
+                    var parserPreviousState = {};
+                    // If this is a g2/g3, we need to know the previous state to interpolate the arcs
+                    if (is_g2_g3) { parserPreviousState = Object.assign(parserPreviousState, parserCurState); }
+                    // Extract x, y, z, f and e
+                    var x = parseFloat(cmd.split("X")[1])
+                    if (!Number.isNaN(x)) {
+                        if (parserCurState.relative)
+                            parserCurState.position.x += x;
+                        else
+                            parserCurState.position.x = x;
+                    }
+                    var y = parseFloat(cmd.split("Y")[1])
+                    if (!Number.isNaN(y)) {
+                        if (parserCurState.relative)
+                            parserCurState.position.y += y;
+                        else
+                            parserCurState.position.y = y;
+                    }
+                    var z = parseFloat(cmd.split("Z")[1])
+                    if (!Number.isNaN(z)) {
+                        if (parserCurState.relative)
+                            parserCurState.position.z += z;
+                        else
+                            parserCurState.position.z = z;
+                    }
+                    var f = parseFloat(cmd.split("F")[1])
+                    if (!Number.isNaN(f)) {
+                        parserCurState.rate = f;
+                    }
+                    var e = parseFloat(cmd.split("E")[1])
+                    if (!Number.isNaN(e)) {
+                        parserCurState.extrude = true;
+                        if (parserLastExtrudedZ != parserCurState.position.z) {
+                            //new layer (probably)
+                            //observedLayerCount++
+                            //console.log("New layer Z."+parserCurState.position.z+" File offset:"+currentFileOffset)
+                            parserLayerLineNumber = 0;
+                            parserLastExtrudedZ = parserCurState.position.z;
+                        }
+                        else
+                            parserLayerLineNumber++;
+                    } else {
+                        parserCurState.extrude = false;
+                    }
+                    parserCurState.layerLineNumber = parserLayerLineNumber;
+
+                    // if this is a g0/g1, push the state to the buffer
+                    if (is_g0_g1) { buffer.push(parserCurState.clone()); }
+                    else {
+                        // This is a g2/g3, so we need to do things a bit differently.
+                        // Extract I and J, R, and is_clockwise
+                        var is_clockwise = cmd.indexOf(" G2") > -1;
+                        var i = parseFloat(cmd.split("I")[1]);
+                        var j = parseFloat(cmd.split("J")[1]);
+                        var r = parseFloat(cmd.split("R")[1]);
+                        var arc = {
+                            // Get X Y and Z from the previous state if it is not
+                            // provided
+                            x: this.getCurrentCoordinate(x, parserPreviousState.position.x),
+                            y: this.getCurrentCoordinate(y, parserPreviousState.position.y),
+                            z: this.getCurrentCoordinate(z, parserPreviousState.position.z),
+                            // Set I and J and R to 0 if they are not provided.
+                            i: this.getCurrentCoordinate(i, 0),
+                            j: this.getCurrentCoordinate(j, 0),
+                            r: this.getCurrentCoordinate(r, 0),
+                            // K omitted, not sure what that's supposed to do
+                            //k: k !== undefined ? k : 0,
+                            // Since the amount extruded doesn't really matter, set it to 1 if we are extruding,
+                            // We don't want undefined values going into the arc interpolation routine
+                            e: this.getCurrentCoordinate(e, parserPreviousState.extrude ? 1 : 0),
+                            f: this.getCurrentCoordinate(r, parserPreviousState.rate),
+                            is_clockwise: is_clockwise
+                        };
+                        // Need to handle R maybe
+                        var segments = self.interpolateArc(parserPreviousState, arc);
+                        for (var index = 1; index < segments.length; index++) {
+                            var cur_segment = segments[index];
+                            var cur_state = parserCurState.clone();
+                            cur_state.position = new THREE.Vector3(cur_segment.x, cur_segment.y, cur_segment.z);
+                            buffer.push(cur_state);
+                        }
+                    }
+                } else if (cmd.indexOf(" G90") > -1) {
+                    //G90: Set to Absolute Positioning
+                    parserCurState.relative = false;
+                } else if (cmd.indexOf(" G91") > -1) {
+                    //G91: Set to state.relative Positioning
+                    parserCurState.relative = true;
+                }
+
+            }
+            //window.myMaxRate=120.0; 
+            //window.fudge=7; 
+
+            // Handle undefined and NaN for current coordinates.
+            this.getCurrentCoordinate = function (cmdCoord, prevCoord) {
+                if (cmdCoord === undefined || isNaN(cmdCoord)) { cmdCoord = prevCoord; }
+                return cmdCoord;
+            }
+            //Update the printhead position based on time elapsed.
+            this.updatePosition = function (timeStep) {
+
+                //Convert the gcode feed rate (in MM/per min?) to rate per second.
+                var rate = curState.rate / 60.0;
+
+                //rate=rate/2;//todo. why still too fast?
+
+                //adapt rate to keep up with buffer.
+                //todo. Make dist based rather than just buffer size.
+                if (buffer.length > 10) {
+                    rate = rate * (buffer.length / 5.0);
+                    //console.log(["Too Slow ",rate,buffer.length])
+                }
+                if (buffer.length < 5) {
+                    rate = rate * (1.0 / (buffer.length * 5.0));
+                    //console.log(["Too fast ",rate,buffer.length])
+                }
+                //rate=Math.min(rate,window.myMaxRate);
+                //dist head needs to travel this frame
+                var dist = rate * timeStep
+                while (buffer.length > 0 && dist > 0)//while some place to go and some dist left.
+                {
+                    //direction
+                    var vectToCurEnd = curEnd.position.clone().sub(curState.position);
+                    var distToEnd = vectToCurEnd.length();
+                    if (dist < distToEnd)//Inside current line?
+                    {
+                        //move pos the distance along line
+                        vectToCurEnd.setLength(dist);
+                        curState.position.add(vectToCurEnd);
+                        dist = 0;//all done 
+                    } else {
+                        //move pos to end point.
+                        curState.position.copy(curEnd.position);
+                        curState.rate = curEnd.rate;
+                        //subract dist for next loop.
+                        dist = dist - distToEnd;
+
+                        //draw segment
+                        //todo.
+
+                        //update lastZ for display of layers. 
+                        if (curEnd.extrude && curEnd.position.z != curLastExtrudedZ) {
+                            curLastExtrudedZ = curEnd.position.z;
+                        }
+                        //console.log([curState.position.z,curState.layerLineNumber])
+
+                        //start on next buffer command
+                        buffer.shift();
+                        if (buffer.length > 0) {
+                            curEnd = buffer[0];
+                            curState.layerLineNumber = curEnd.layerLineNumber;
+                        }
+                    }
+                }
+            }
+        }
+
+        var printHeadSim = new PrintHeadSimulator();
+        var curPrinterState = null;
+        var curPrintFilePos = 0;
+        self.fromCurrentData = function (data) {
+
+            //Dont do anything if view not initalized
+            if (!viewInitialized)
+                return;
+
+            //update current loaded model.
+            updateJob(data.job);
+            if (curPrinterState && curPrinterState.text != data.state.text) {
+                //console.log(["Printer state changed: ",curPrinterState.text," -> ",data.state.text])
+                if (data.state.text.startsWith("Operational")) {
+                    //console.log("Resetting print simulation");
+                    printHeadSim = new PrintHeadSimulator();
+                }
+            }
+            curPrinterState = data.state;
+
+
+            curPrintFilePos = data.progress.filepos;
+
+            //parse logs position data for simulator
+            if (data.logs.length) {
+                data.logs.forEach(function (e, i) {
+                    if (e.startsWith("Send:")) {
+                        //console.log(["GCmd:",e]);
+                        if (printHeadSim)
+                            printHeadSim.addCommand(e);
+
+                        //Strip out the extra stuff in the terminal line.
+                        //match second space to * character. I hate regexp.
+                        if (terminalGcodeProxy) {
+                            var reg = new RegExp('(?<=\\s\\S*\\s).[^*]*', 'g');
+                            var matches = e.match(reg);
+                            if (matches && matches.length > 0)
+                                terminalGcodeProxy.parse(matches[0] + '\n');
+                        }
+                    }
+                    else if (e.startsWith("Recv: T:")) {
+                        //console.log(["GCmd:",e]);
+                        let parts = e.substr(6).split("@");//remove Recv: and checksum.
+                        let temps = parts[0];
+                        let statusStr = temps;//+" Buffer:"+printHeadSim.getBufferStats()
+                        $(".pgstatus").text(statusStr);
+
+                    }
+                })
+            }
+        };
+
+        self.updateCss = function (newCss) {
+            //alert(this)
+            var newCss = $("#pg_add_css").val();
+            console.log(["Update css:", newCss]);
+            localStorage.setItem('pg_add_css_val', newCss)
+            $("#pgcss").html(newCss);
+
+        }
+        self.onAfterBinding = function () {
+            console.log("onAfterBinding")
+
+            //var addCss=$("#add_css").val();
+            $("<style id='pgcss'>")
+                .prop("type", "text/css")
+                .html("")
+                .appendTo("head");
+
+            var css = localStorage.getItem('pg_add_css_val')
+            if (css) {
+                $("#pgcss").html(css);
+                $("#pg_add_css").val(css);
+            }
+        };
+        self.onEventFileSelected = function (payload) {
+            //console.log(["onEventFileSelected ",payload])
+        }
+
+        //Scene globals
+        var camera, cameraControls, cameraLight;
+        var scene, renderer;
+        var lightBackground, darkBackground;
+        var gcodeProxy;//used to display loaded gcode.
+
+        var terminalGcodeProxy;//todo remove(prob not used anymore). used to display gcode actualy sent to printer.
+        var cubeCamera;//todo make reflections optional.
+        var nozzleModel;
+
+        var clock;
+        var dimensionsGroup;
+        var sceneBounds = new THREE.Box3();
+        //todo. Are these needed?
+        var gcodeWid = 580;
+        var gcodeHei = 580;
+        var gui;
+
+        var forceNoSync = false;//used to override sync when user drags slider. Todo. Better way to handle this?
+
+        var currentLayerNumber = 0;
+
+        //settings that are saved between sessions
+        var PGSettings = function () {
+            this.showMirror = false;//default changed
+            this.fatLines = true;//default changed
+            //this.reflections=false;//remove this
+            this.darkMode = false;
+            this.syncToProgress = true;
+            this.orbitWhenIdle = false;
+            this.reloadGcode = function () {
+                if (gcodeProxy && curJobName != "")
+                    gcodeProxy.loadGcode('downloads/files/local/' + curJobName);
+            };
+            this.showState = true;
+            this.showWebcam = false;
+            this.showFiles = false;
+            this.showDash = false;
+            this.antialias = true;
+
+            this.showNozzle = true;
+            this.highlightCurrentLayer = true;
+        };
+        var pgSettings = new PGSettings();
+
+        function updateWindowStates() {
+            if (pgSettings.showState) {
+                $("#state_wrapper").removeClass("pghidden");
+            }
+            else {
+                $("#state_wrapper").addClass("pghidden");
+            }
+            if (pgSettings.showFiles) {
+                $("#files_wrapper").removeClass("pghidden");
+            }
+            else {
+                $("#files_wrapper").addClass("pghidden");
+            }
+            if (pgSettings.showWebcam) {
+                $(".gwin #webcam_rotator").removeClass("pghidden");
+            }
+            else {
+                $(".gwin #webcam_rotator").addClass("pghidden");
+            }
+            if (pgSettings.showDash) {
+                $("#tab_plugin_dashboard").removeClass("pghidden");
+            }
+            else {
+                $("#tab_plugin_dashboard").addClass("pghidden");
+            }
+        }
+
+        var bedVolume = {
+            depth: 0,
+            formFactor: "",
+            height: 0,
+            origin: "",
+            width: 0,
+        };
+        var viewInitialized = false;
+        self.onTabChange = function (current, previous) {
+
+            if (current == "#tab_plugin_prettygcode") {
+                if (!viewInitialized) {
+                    viewInitialized = true;
+
+                    //Watch for bed volume changes
+                    self.printerProfiles.currentProfileData.subscribe(
+                        function () {
+                            //get new build volume.
+                            updateBedVolume();
+                            //update scene if any
+                            updateGridMesh();
+
+                            //Needed in case center has changed.
+                            resetCamera();
+                        });
+
+
+                    //get current (possibly default) printer build volume.
+                    updateBedVolume();
+
+                    //console.log(["bedVolume",bedVolume]);
+
+                    if (true) {
+                        //simple gui
+                        dat.GUI.TEXT_OPEN = "View Options"
+                        dat.GUI.TEXT_CLOSED = "View Options"
+                        gui = new dat.GUI({ autoPlace: false, name: "View Options", closed: false, closeOnTop: true, useLocalStorage: true });
+
+                        //Override default storage location to fix bug with tabs.
+                        //Not working
+                        //gui.setLocalStorageHash("PrettyGCodeSettings");
+
+                        gui.useLocalStorage = true;
+                        // var guielem = $("<div id='mygui' style='position:absolute;right:95px;top:20px;opacity:0.8;z-index:5;'></div>");
+
+                        // $('.gwin').prepend(guielem)
+
+                        $('#mygui').append(gui.domElement);
+
+                        gui.remember(pgSettings);
+                        gui.add(pgSettings, 'syncToProgress').onFinishChange(function () {
+                            if (pgSettings.syncToProgress) {
+                                //                                syncLayerToZ();
+                            }
+                        });
+
+                        gui.add(pgSettings, 'darkMode').onFinishChange(function (checked) {
+                            var color = checked ? darkBackground : lightBackground;
+                            scene.background = new THREE.Color(color);
+
+                            // Apply dark mode CSS class
+                            if (checked) {
+                                $(".page-container").addClass("pgdarkmode");
+                            } else {
+                                $(".page-container").removeClass("pgdarkmode");
+                            }
+
+                            renderer.render(scene, camera);
+                        });
+
+                        gui.add(pgSettings, 'showMirror').onFinishChange(pgSettings.reloadGcode);
+                        gui.add(pgSettings, 'orbitWhenIdle');
+                        gui.add(pgSettings, 'fatLines').onFinishChange(pgSettings.reloadGcode);
+                        //gui.add(pgSettings, 'reflections');
+                        gui.add(pgSettings, 'antialias').onFinishChange(function () {
+                            new PNotify({
+                                title: "Reload page required",
+                                text: "Antialias chenges won't take effect until you refresh the page",
+                                type: "info"
+
+                            });
+                            //alert("Antialias chenges won't take effect until you refresh the page");
+                        });
+
+                        gui.add(pgSettings, 'showNozzle');
+
+                        //gui.add(pgSettings, 'reloadGcode');
+
+                        var folder = gui.addFolder('Windows');//hidden.
+                        folder.add(pgSettings, 'showState').onFinishChange(updateWindowStates).listen();
+                        folder.add(pgSettings, 'showWebcam').onFinishChange(updateWindowStates).listen();
+                        folder.add(pgSettings, 'showFiles').onFinishChange(updateWindowStates).listen();
+                        folder.add(pgSettings, 'showDash').onFinishChange(updateWindowStates).listen();
+
+                        //dont show Windows. Automatically handled by toggle buttons
+                        $(folder.domElement).attr("hidden", true);
+
+                        // Apply dark mode on initial load if enabled
+                        if (pgSettings.darkMode) {
+                            $(".page-container").addClass("pgdarkmode");
+                        }
+
+                    }
+
+                    initThree();
+
+                    //load Nozzle model.
+                    var objloader = new THREE.OBJLoader();
+                    objloader.load('plugin/prettygcode/static/js/models/ExtruderNozzle.obj', function (obj) {
+                        obj.quaternion.setFromEuler(new THREE.Euler(Math.PI / 2, 0, 0));
+                        obj.scale.setScalar(0.1)
+                        obj.position.set(0, 0, 10);
+                        obj.name = "nozzle";
+                        var nozzleMaterial = new THREE.MeshStandardMaterial({
+                            metalness: 1,   // between 0 and 1
+                            roughness: 0.5, // between 0 and 1
+                            envMap: cubeCamera.renderTarget.texture,
+                            color: new THREE.Color(0xba971b),
+                            //flatShading:false,
+                        });
+                        obj.children.forEach(function (e, i) {
+                            if (e instanceof THREE.Mesh) {
+                                e.material = nozzleMaterial;
+                                //e.geometry.computeVertexNormals();
+                            }
+                        })
+                        nozzleModel = obj;
+                        scene.add(obj);
+                    });
+
+                    //GCode loader.
+                    gcodeProxy = new GCodeParser();
+                    var gcodeObject = gcodeProxy.getObject();
+                    gcodeObject.position.set(-0, -0, 0);
+                    scene.add(gcodeObject);
+
+                    if (curJobName != "")
+                        gcodeProxy.loadGcode('downloads/files/local/' + curJobName);
+
+                    if (false) {
+                        //terminal parser
+                        terminalGcodeProxy = new GCodeParser();
+                        terminalGcodeProxy.addSegment = function (p1, p2) {
+                            //console.log(["addSegment",p1,p2])
+                            if (currentLayer === undefined) {
+                                newLayer(p1);
+                            }
+                        }
+                        var terminalGcodeObject = terminalGcodeProxy.getObject();
+                        terminalGcodeObject.position.set(100, -0, 0);
+                        scene.add(terminalGcodeObject);
+                    }
+
+                    //note this is an octoprint version of a bootstrap slider. not a jquery ui slider. 
+                    $('.gwin').append($('<div id="myslider-vertical" style=""></div>'));
+                    $("#myslider-vertical").slider({
+                        id: "myslider",
+                        orientation: "vertical",
+                        reversed: true,
+                        range: "min",
+                        min: 0,
+                        max: 100,
+                        value: 100,
+                    }).on("slide", function (event, ui) {
+                        currentLayerNumber = event.value;
+                        $("#myslider .slider-handle").text(currentLayerNumber);
+                    }).on("slideStart", function (event, ui) {
+                        //console.log("slideStart");
+                        forceNoSync = true;
+                    }).on("slideStop", function (event, ui) {
+                        //console.log("slideStop");
+                        forceNoSync = false;
+                    });
+                    $("#myslider").attr("style", "height:90%;position:absolute;top:5%;right:20px")
+
+
+
+                    //Create a web camera inset for the view. 
+                    var camView = $("#webcam_rotator").clone();
+                    let img = camView.find("#webcam_image")
+                    img.attr("id", "pg_webcam_image")
+                    $(".gwin").append(camView)
+
+                    //check url for fullscreen mode
+                    if (urlParam("fullscreen"))
+                        $(".page-container").addClass("pgfullscreen");
+
+                    //setup window toggle buttons
+                    $(".fstoggle").on("click", function () {
+                        $(".page-container").toggleClass("pgfullscreen");
+                    });
+                    $(".pgsettingstoggle").on("click", function () {
+                        $("#mygui").toggleClass("pghidden");
+                    });
+                    $(".pgstatetoggle").on("click", function () {
+                        pgSettings.showState = !pgSettings.showState;
+                        updateWindowStates();
+                    });
+                    $(".pgfilestoggle").on("click", function () {
+                        pgSettings.showFiles = !pgSettings.showFiles;
+                        updateWindowStates();
+                    });
+                    $(".pgcameratoggle").on("click", function () {
+                        pgSettings.showWebcam = !pgSettings.showWebcam;
+                        updateWindowStates();
+                    });
+                    $(".pgdashtoggle").on("click", function () {
+                        pgSettings.showDash = !pgSettings.showDash;;
+                        updateWindowStates();
+                    });
+                    updateWindowStates();
+                }
+
+                //Activate webcam view in window. 
+                // Use modern OctoPrint webcam API if available (OctoPrint 1.9+)
+                var webcamUrl = null;
+                if (self.settings && self.settings.webcam && self.settings.webcam.streamUrl) {
+                    webcamUrl = self.settings.webcam.streamUrl();
+                }
+
+                // Fallback to legacy API if modern API not available
+                if (!webcamUrl) {
+                    webcamUrl = "/webcam/?action=stream";
+                }
+
+                $(".gwin #pg_webcam_image").attr("src", webcamUrl + "&" + Math.random());
+
+                // Ensure webcam is enabled even if controlViewModel isn't ready yet
+                if (self.controlViewModel && typeof self.controlViewModel._enableWebcam === 'function') {
+                    self.controlViewModel._enableWebcam();
+                }
+
+            } else if (previous == "#tab_plugin_prettygcode") {
+                //todo. disable animation 
+
+                //Disable camera when tab isnt visible.
+                $(".gwin #pg_webcam_image").attr("src", "")
+                if (self.controlViewModel && typeof self.controlViewModel._disableWebcam === 'function') {
+                    self.controlViewModel._disableWebcam();
+                }
+            }
+
+            // Re-enable webcam for control tab if it exists
+            if (self.controlViewModel && typeof self.controlViewModel._enableWebcam === 'function') {
+                self.controlViewModel._enableWebcam();
+            }
+        };
+
+        //util function
+        String.prototype.hashCode = function () {
+            var hash = 0, i, chr;
+            if (this.length === 0) return hash;
+            for (i = 0; i < this.length; i++) {
+                chr = this.charCodeAt(i);
+                hash = ((hash << 5) - hash) + chr;
+                hash |= 0; // Convert to 32bit integer
+            }
+            return hash;
+        };
+
+        //util function
+        urlParam = function (name) {
+            var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
+            if (results == null) {
+                return null;
+            }
+            return decodeURI(results[1]) || 0;
+        }
+
+        //Handle "focus" url param. Not used anymore.
+        var focus = urlParam("focus");
+        if (focus != null) {
+            console.log("Focusing on:" + focus);
+            $("body").children().hide();
+            $("#webcam_container").hide();
+            if (!focus.startsWith("."))
+                focus = "#" + focus;
+            var el = $(focus)[0];
+            $("body").prepend(el);
+        }
+
+        function updateBedVolume() {
+            var currentProfileData = self.printerProfiles.currentProfileData();
+            if (!currentProfileData || !currentProfileData.volume) {
+                return;
+            }
+
+            var volume = currentProfileData.volume;
+            //console.log([arguments.callee.name,volume]);
+
+            if (typeof volume.custom_box === "function") //check for custom bounds.
+            {
+                bedVolume = {
+                    width: volume.width(),
+                    height: volume.height(),
+                    depth: volume.depth(),
+                    origin: volume.origin(),
+                    formFactor: volume.formFactor(),
+                };
+            }
+            else {
+                //console.log(["volume.custom_box",volume.custom_box]);
+                bedVolume = {
+                    width: volume.custom_box.x_max() - volume.custom_box.x_min(),
+                    height: volume.custom_box.z_max() - volume.custom_box.z_min(),
+                    depth: volume.custom_box.y_max() - volume.custom_box.y_min(),
+                    origin: volume.origin(),
+                    formFactor: volume.formFactor(),
+                };
+            }
+        }
+
+        function GCodeParser(data) {
+
+            var state = { x: 0, y: 0, z: 0, e: 0, f: 0, extruding: false, relative: false };
+            var layers = [];
+
+            var currentLayer = undefined;
+
+            var defaultColor = new THREE.Color('white');
+            var curColor = defaultColor;
+            var filePos = 0;//used for syncing when printing.
+
+            var previousPiece = "";//used for parsing gcode in chunks.
+
+            //material for fatlines
+            var curMaterial = new THREE.LineMaterial({
+                linewidth: 3, // in pixels
+                //transparent: true,
+                //opacity: 0.5,
+                //color: new THREE.Color(curColorHex),// rainbow.getColor(layers.length % 64).getHex()
+                vertexColors: THREE.VertexColors,
+            });
+            //todo. handle window resize
+            //            curMaterial.resolution.set(gcodeWid, gcodeHei);
+            curMaterial.resolution.set(500, 500);
+
+            //for plain lines
+            var curLineBasicMaterial = new THREE.LineBasicMaterial({
+                color: 0xffffff,
+                vertexColors: THREE.VertexColors
+            });
+
+            var gcodeGroup = new THREE.Group();
+            gcodeGroup.name = 'gcode';
+
+            //reset parser for another object.
+            this.reset = function () {
+                this.clearObject();
+                state = { x: 0, y: 0, z: 0, e: 0, f: 0, extruding: false, relative: false };
+                layers = [];
+                currentLayer = undefined;
+                curColor = defaultColor;
+                filePos = 0;
+                previousPiece = "";
+            }
+            this.getObject = function () {
+                return gcodeGroup;
+            }
+
+            this.clearObject = function () {
+                if (gcodeGroup) {
+                    for (var i = gcodeGroup.children.length - 1; i >= 0; i--) {
+                        gcodeGroup.remove(gcodeGroup.children[i]);
+                    }
+                }
+            }
+            easeOutBounce = function (t, b, c, d) {
+                if ((t /= d) < (1 / 2.75)) {
+                    return c * (7.5625 * t * t) + b;
+                } else if (t < (2 / 2.75)) {
+                    return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b;
+                } else if (t < (2.5 / 2.75)) {
+                    return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b;
+                } else {
+                    return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b;
+                }
+            }
+            easeInBounce = function (t, b, c, d) {
+                return c - easeOutBounce(d - t, 0, c, d) + b;
+            };
+            easeOutExpo = function (t, b, c, d) {
+                return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
+            }
+            easeOutFall = function (t, b, c, d) {
+                var dist = 0.5 * 9.8 * (t * t)
+                var per = (dist + b)
+                return dist;
+            }
+            this.animateLayers = function (curTime, deltaTime) {
+                if (true) {
+                    var startZ = 100;
+                    gcodeGroup.traverse(function (child) {
+                        if (child.name.startsWith("layer#")) {
+                            var udata = child.userData;
+
+                            var dist = (2.0 - (curTime / 2)) * 100.0;
+
+                            var newZ = Math.max(0, udata.layerNumber * dist);
+                            child.position.set(0, 0, newZ);
+
+
+                            // var startTime = udata.layerNumber/8.0;
+                            // if(curTime>=startTime)
+                            // {
+                            //     var myTime = curTime-startTime;
+                            //     var dist = 9.8*(myTime*myTime)
+                            //     var newZ = Math.max(0,startZ-dist);
+                            //     child.position.set(0,0,newZ);
+                            // }
+                            // else{
+                            //     child.position.set(0,0,startZ);
+                            // }
+                        }
+                    });
+                } else {
+                    gcodeGroup.traverse(function (child) {
+                        if (child.name.startsWith("layer#")) {
+                            var udata = child.userData;
+                            var endTime = udata.layerNumber / 4.0;
+                            if (curTime < endTime) {
+                                var z = easeOutExpo(curTime, 0, 100, endTime);
+                                //Math.sin(curTime+(udata.layerNumber*0.1))
+                                child.position.set(0, 0, 100 - z);
+                            }
+                            else
+                                child.position.set(0, 0, 0);
+                        }
+                    });
+
+                }
+            }
+            this.highlightLayer = function (layerNumber, highlightMaterial) {
+                var needUpdate = false;//only need update if visiblity changes
+                var defaultMat = curLineBasicMaterial;
+                if (pgSettings.fatLines) {
+                    defaultMat = curMaterial;
+                }
+
+                gcodeGroup.traverse(function (child) {
+                    if (child.name.startsWith("layer#")) {
+                        if (child.userData.layerNumber < layerNumber) {
+                            if (child.material.uuid != defaultMat.uuid) {
+                                child.material = defaultMat;
+                                needUpdate = true;
+                            }
+                        } else if (child.userData.layerNumber == layerNumber) {
+                            if (child.material.uuid != highlightMaterial.uuid) {
+                                child.material = highlightMaterial;
+                                needUpdate = true;
+                            }
+                        }
+                        else {
+                            if (child.material.uuid != defaultMat.uuid) {
+                                child.material = defaultMat;
+                                needUpdate = true;
+                            }
+                        }
+                    }
+                });
+                return (needUpdate);
+            }
+
+            this.syncGcodeObjToLayer = function (layerNumber, lineNumber = Infinity) {
+                var needUpdate = false;//only need update if visiblity changes
+
+                //hack comp for mirror.
+                //todo. better handle of mirror object so this isnt needed. 
+                if (pgSettings.showMirror && lineNumber != Infinity)
+                    lineNumber = lineNumber * 2;
+
+                gcodeGroup.traverse(function (child) {
+                    if (child.name.startsWith("layer#")) {
+                        if (child.userData.layerNumber < layerNumber) {
+
+                            if (!child.visible || child.geometry.maxInstancedCount != child.userData.numLines)
+                                needUpdate = true;
+
+                            child.visible = true;
+                            child.geometry.maxInstancedCount = child.userData.numLines;
+                        } else if (child.userData.layerNumber == layerNumber) {
+                            if (!child.visible || child.geometry.maxInstancedCount != Math.min(lineNumber, child.userData.numLines))
+                                needUpdate = true;
+
+                            child.visible = true;
+                            child.geometry.maxInstancedCount = Math.min(lineNumber, child.userData.numLines);
+                        }
+                        else {
+                            if (child.visible)
+                                needUpdate = true;
+
+                            child.visible = false;
+                        }
+                    }
+                });
+                return (needUpdate);
+            }
+            this.syncGcodeObjTo = function (layerZ, lineNumber = Infinity) {
+                //hack comp for mirror.
+                //todo. better handle of mirror object so this isnt needed. 
+                if (pgSettings.showMirror && lineNumber != Infinity)
+                    lineNumber = lineNumber * 2;
+
+                gcodeGroup.traverse(function (child) {
+                    if (child.name.startsWith("layer#")) {
+                        if (child.userData.layerZ < layerZ) {
+                            child.visible = true;
+
+                            child.geometry.maxInstancedCount = child.userData.numLines;
+                        } else if (child.userData.layerZ == layerZ) {
+                            child.visible = true;
+                            child.geometry.maxInstancedCount = Math.min(lineNumber, child.userData.numLines);
+                        }
+                        else {
+                            child.visible = false;
+                        }
+                    }
+                });
+            }
+            this.syncGcodeObjToFilePos = function (filePosition) {
+                let syncLayerNumber = 0;//derived layer number based on pos and user data.
+                gcodeGroup.traverse(function (child) {
+                    if (child.name.startsWith("layer#")) {
+                        var filePositions = child.userData.filePositions;
+                        var fpMin = filePositions[0];
+                        var fpMax = filePositions[filePositions.length];
+                        if (fpMax < filePosition) { //way before.
+                            child.visible = true;
+
+                            child.geometry.maxInstancedCount = child.userData.numLines;
+                        } else if (fpMin > filePosition) { //way after
+                            child.visible = false;
+                        } else //must be during. right?
+                        {
+                            child.visible = true;
+
+                            //count number of lines before filePos
+                            var count = 0;
+                            while (count < filePositions.length && filePositions[count] < filePosition)
+                                count++;
+
+                            //hack comp for mirror.
+                            //todo. better handle of mirror object so this isnt needed. 
+                            if (pgSettings.showMirror)
+                                count = count * 2;
+
+                            child.geometry.maxInstancedCount = Math.min(count, child.userData.numLines);
+
+                            syncLayerNumber = child.userData.layerNumber
+                        }
+                    }
+                });
+                return syncLayerNumber;//used to sync other elements.
+            }
+            this.currentUrl = "";
+            this.loadGcode = function (url) {
+                this.reset();
+
+                currentUrl = url;
+
+                var parserObject = this;
+                var file_url = url;//'downloads/files/local/xxx.gcode';
+                var myRequest = new Request(file_url);
+                fetch(myRequest)
+                    .then(function (response) {
+                        var contentLength = response.headers.get('Content-Length');
+                        if (!response.body || !window['TextDecoder']) {
+                            response.text().then(function (text) {
+                                parserObject.parse(text);
+                                parserObject.finishLoading();
+                            });
+                        } else {
+                            var myReader = response.body.getReader();
+                            var decoder = new TextDecoder();
+                            var buffer = '';
+                            var received = 0;
+                            myReader.read().then(function processResult(result) {
+                                if (result.done) {
+                                    parserObject.finishLoading();
+                                    return;
+                                }
+                                received += result.value.length;
+                                //                buffer += decoder.decode(result.value, {stream: true});
+                                /* process the buffer string */
+                                parserObject.parse(decoder.decode(result.value, { stream: true }));
+
+                                // read the next piece of the stream and process the result
+                                return myReader.read().then(processResult);
+                            })
+                        }
+                    })
+
+            }
+            this.finishLoading = function () {
+                if (currentLayer !== undefined) {
+                    addObject(currentLayer, true);
+                }
+
+                //update scene bounds.
+                var bsize = new THREE.Vector3();
+                sceneBounds.getSize(bsize);
+
+                //update ui slider
+                if ($("#myslider-vertical").length) {
+                    $("#myslider-vertical").slider("setMax", layers.length)
+                    $("#myslider-vertical").slider("setValue", layers.length, false, true)
+                    $("#myslider .slider-handle").text(layers.length);
+
+                    currentLayerNumber = layers.length;
+                }
+
+                console.log("Finished loading GCode object.")
+                console.log(["layers:", layers.length, "size:", filePos])
+
+                let totalLines = 0;
+                for (let layer of layers) {
+                    totalLines += layer.vertex.length / 6;
+                }
+                console.log(["lines:", totalLines])
+
+                //console.log([sceneBounds,layers])
+
+                //gcodeProxy.syncGcodeObjTo(Infinity);
+
+                //updateDimensions(bsize); 
+
+                //Move zoom camera to new bounds.
+                var dist = Math.max(Math.abs(bsize.x), Math.abs(bsize.y)) / 2;
+                dist = Math.max(20, dist);//min distance to model.
+                //console.log(dist)
+                cameraControls.dollyTo(dist * 2.0, true);
+            }
+
+            function addObject(layer, extruding) {
+
+                if (layer.vertex.length > 2) { //Something to draw?
+                    if (pgSettings.fatLines) {//fancy lines
+                        var geo = new THREE.LineGeometry();
+                        geo.setPositions(layer.vertex);
+                        geo.setColors(layer.colors)
+                        var line = new THREE.Line2(geo, curMaterial);
+                        line.name = 'layer#' + layers.length;
+                        line.userData = { layerZ: layer.z, layerNumber: layers.length, numLines: layer.vertex.length / 6, filePositions: layer.filePositions };// 6 because 2 x triplets
+                        gcodeGroup.add(line);
+                        //line.renderOrder = 2;
+                    } else {//plain lines
+                        var geo = new THREE.BufferGeometry();
+                        geo.addAttribute('position', new THREE.BufferAttribute(new Float32Array(layer.vertex), 3));
+                        geo.addAttribute('color', new THREE.BufferAttribute(new Float32Array(layer.colors), 3));
+                        var line = new THREE.LineSegments(geo, curLineBasicMaterial);
+                        line.name = 'layer#' + layers.length;
+                        line.userData = { layerZ: layer.z, layerNumber: layers.length, numLines: layer.vertex.length / 6, filePositions: layer.filePositions };
+                        gcodeGroup.add(line);
+
+                    }
+                }
+            }
+
+            function newLayer(line) {
+                if (currentLayer !== undefined) {
+                    addObject(currentLayer, true);
+                }
+
+                currentLayer = { vertex: [], pathVertex: [], z: line.z, colors: [], filePositions: [] };
+                layers.push(currentLayer);
+                //console.log("layer #" + layers.length + " z:" + line.z);
+
+            }
+            /*this.addArc= function (arc, material ) {
+                // let geometry = new THREE.Geometry();
+        
+                // let start  = new THREE.Vector3(arc.x1, arc.y1, arc.z1);
+                // let center = new THREE.Vector3(arc.i,  arc.j,  arc.k);
+                // let end    = new THREE.Vector3(arc.x2, arc.y2, arc.z2);
+        
+                let radius = Math.sqrt(
+                    Math.pow((arc.x1 - arc.i), 2) + Math.pow((arc.y1 - arc.j), 2)
+                );
+                let arcCurve = new THREE.ArcCurve(
+                    arc.i, // aX
+                    arc.j, // aY
+                    radius, // aRadius
+                    Math.atan2(arc.y1 - arc.j, arc.x1 - arc.i), // aStartAngle
+                    Math.atan2(arc.y2 - arc.j, arc.x2 - arc.i), // aEndAngle
+                    !!arc.isClockwise // isClockwise
+                );
+                let divisions = 10;
+                let vertices = arcCurve.getPoints(divisions);
+                let vectorthrees = [];
+                for (var i = 0; i < vertices.length; i++) {
+                    vectorthrees.push(new THREE.Vector3(vertices[i].x, vertices[i].y, arc.z1));
+                }
+                if (vectorthrees.length) {
+                    let geometry = new THREE.Geometry();
+                    geometry.vertices = vectorthrees;
+                    object.add(new THREE.Line(geometry, material));
+                }
+            }*/
+            this.addSegment = function (p1, p2) {
+                if (currentLayer === undefined) {
+                    newLayer(p1);
+                }
+                if (Number.isNaN(p1.x) || Number.isNaN(p1.y) || Number.isNaN(p1.z) || Number.isNaN(p2.x) || Number.isNaN(p2.y) || Number.isNaN(p2.z)) {
+                    console.log(["Bad line segment", p1, p2]);
+                    return;
+                }
+
+                currentLayer.vertex.push(p1.x, p1.y, p1.z);
+                currentLayer.vertex.push(p2.x, p2.y, p2.z);
+                currentLayer.filePositions.push(filePos);//save for syncing.
+
+                if (curColor != defaultColor) {
+                    sceneBounds.expandByPoint(p1);
+                    sceneBounds.expandByPoint(p2);
+                }
+
+                if (pgSettings.showMirror) {
+                    //add mirror version
+                    currentLayer.vertex.push(p1.x, p1.y, -p1.z);
+                    currentLayer.vertex.push(p2.x, p2.y, -p2.z);
+                }
+
+                if (true)//faux shading. Darken line color based on angle
+                {
+                    //var p1=new THREE.Vector3(10,10,0);
+                    //var p2=new THREE.Vector3(15,15,0);
+
+                    var per = 1.0;//bright
+                    if (true) {
+                        //var np2=new THREE.Vector3(p2.x,p2.y,p2.z);
+                        var vec = new THREE.Vector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z);
+                        vec.normalize();
+                        //                        per= Math.max(vec.dot(new THREE.Vector3(1,0,0)),0.0)
+                        //                        per= Math.abs(vec.dot(new THREE.Vector3(1,0,0)),0.0)
+                        per = (vec.dot(new THREE.Vector3(1, 0, 0)) / 2) + 0.5;
+                        per = (per / 5.0);
+                    } else {
+                        var deltaX = p2.x - p1.x;
+                        var deltaY = p2.y - p1.y;
+                        var rad = Math.atan2(deltaY, deltaX);
+
+                        rad = Math.abs(rad)
+                        per = (rad) / (2.0 * 3.1415);
+                        //console.log(rad + " " + per);
+                    }
+                    var drawColor = new THREE.Color(curColor)
+                    var hsl = {}
+                    drawColor.getHSL(hsl);
+
+                    //darken every other line to make the layers easier to see.
+                    if ((layers.length % 2) == 0)
+                        hsl.l = per + 0.25;
+                    else
+                        hsl.l = per + 0.30;
+
+                    drawColor.setHSL(hsl.h, hsl.s, hsl.l);
+                    //console.log(drawColor.r + " " + drawColor.g + " " + drawColor.b )
+                    currentLayer.colors.push(drawColor.r, drawColor.g, drawColor.b);
+                    currentLayer.colors.push(drawColor.r, drawColor.g, drawColor.b);
+
+                    if (pgSettings.showMirror) {
+                        //add mirror version
+                        drawColor.setHSL(hsl.h, hsl.s, hsl.l / 2);
+                        currentLayer.colors.push(drawColor.r, drawColor.g, drawColor.b);
+                        currentLayer.colors.push(drawColor.r, drawColor.g, drawColor.b);
+                    }
+                }
+                else {
+                    currentLayer.colors.push(curColor.r, curColor.g, curColor.b);
+                    currentLayer.colors.push(curColor.r, curColor.g, curColor.b);
+                }
+            }
+
+            function delta(v1, v2) {
+                return state.relative ? v2 : v2 - v1;
+            }
+
+            function absolute(v1, v2) {
+                return state.relative ? v1 + v2 : v2;
+            }
+
+            this.parse = function (chunk) {
+
+                //remove comments from chunk.
+                //var lines = chunk.replace(/;.+/g, '').split('\n');
+                //or not
+                var lines = chunk.split('\n');
+
+                //handle partial lines from previous chunk.
+                lines[0] = previousPiece + lines[0];
+                previousPiece = lines[lines.length - 1];
+
+                //note -1 so we dont process last line in case it is a partial.
+                //Todo process the last line. Probably not needed since last line is usually gcode cleanup and not extruded lines.
+                for (var i = 0; i < lines.length - 1; i++) {
+
+                    filePos += lines[i].length + 1;//+1 because of split \n. 
+
+                    //Process comments
+                    //figure out line color from comments.
+                    if (lines[i].indexOf(";") > -1) {
+                        var cmdLower = lines[i].toLowerCase();
+                        if (cmdLower.indexOf("inner") > -1) {
+                            curColor = new THREE.Color(0x00ff00);//green
+                        }
+                        else if (cmdLower.indexOf("outer") > -1) {
+                            curColor = new THREE.Color('red');
+                        }
+                        else if (cmdLower.indexOf("perimeter") > -1) {
+                            curColor = new THREE.Color('red');
+                        }
+                        else if (cmdLower.indexOf("fill") > -1) {
+                            curColor = new THREE.Color('orange');
+                        }
+                        else if (cmdLower.indexOf("skin") > -1) {
+                            curColor = new THREE.Color('yellow');
+                        }
+                        else if (cmdLower.indexOf("support") > -1) {
+                            curColor = new THREE.Color('skyblue');
+                        }
+                        else if (cmdLower.indexOf("skirt") > -1) {
+                            curColor = new THREE.Color('skyblue');
+                        }
+                        else {
+                            //var curColorHex = (Math.abs(cmd.hashCode()) & 0xffffff);
+                            //curColor = new THREE.Color(curColorHex);
+                            //console.log(cmd + ' ' + curColorHex.toString(16))
+                        }
+                        //console.log(lines[i])
+                    }
+
+
+                    //remove comments and process command part of line.
+                    var tokens = lines[i].replace(/;.+/g, '').split(' ');
+                    if (tokens.length < 1)
+                        continue; //nothing left to process.
+
+                    var cmd = tokens[0].toUpperCase();
+
+                    //Arguments
+                    var args = {};
+                    tokens.splice(1).forEach(function (token) {
+                        if (token[0] !== undefined) {
+                            var key = token[0].toLowerCase();
+                            var value = parseFloat(token.substring(1));
+                            args[key] = value;
+                        }
+                    });
+
+                    //G0/G1 - Linear Movement
+                    if (cmd === 'G0' || cmd === 'G1') {
+                        var line = {
+                            x: args.x !== undefined ? absolute(state.x, args.x) : state.x,
+                            y: args.y !== undefined ? absolute(state.y, args.y) : state.y,
+                            z: args.z !== undefined ? absolute(state.z, args.z) : state.z,
+                            e: args.e !== undefined ? absolute(state.e, args.e) : state.e,
+                            f: args.f !== undefined ? absolute(state.f, args.f) : state.f,
+                        };
+                        //Layer change detection is or made by watching Z, it's made by watching when we extrude at a new Z position
+                        if (delta(state.e, line.e) > 0) {
+                            var diff = delta(state.e, line.e);
+                            line.extruding = delta(state.e, line.e) > 0;
+                            if (currentLayer == undefined || line.z != currentLayer.z) {
+                                newLayer(line);
+                            }
+                        }
+
+                        //make sure extruding is updated. might not be needed.
+                        //line.extruding = delta(state.e, line.e) > 0;
+                        //if (line.extruding)
+                        //    addSegment(state, line);//only if extruding right now.
+
+                        //If E is defined in the args then extruding. Todo. is this right?
+                        if (args.e !== undefined)
+                            this.addSegment(state, line);//only if extruding right now.
+                        state = line;
+                    } else if (cmd === 'G2' || cmd === 'G3') {
+                        //G2/G3 - Arc Movement ( G2 clock wise and G3 counter clock wise )
+                        // Not supporting K ATM
+                        if (args.k !== undefined) {
+                            // I have no idea what K is for...
+                            console.warn('THREE.GCodeLoader: Arcs with K parameter not currently supported');
+                        }
+                        else if (args.r !== undefined) {
+                            console.warn('THREE.GCodeLoader: Arc in R form are not currently supported.');
+                        }
+                        else {
+                            var arc = {
+                                x: args.x !== undefined ? absolute(state.x, args.x) : state.x,
+                                y: args.y !== undefined ? absolute(state.y, args.y) : state.y,
+                                z: args.z !== undefined ? absolute(state.z, args.z) : state.z,
+                                i: args.i !== undefined ? args.i : 0,
+                                j: args.j !== undefined ? args.j : 0,
+                                r: args.r !== undefined ? args.r : null,
+                                // What is this K I'm seeing here, lol
+                                //k: args.k !== undefined ? absolute( state.k, args.k ) : state.k,
+                                e: args.e !== undefined ? absolute(state.e, args.e) : state.e,
+                                f: args.f !== undefined ? absolute(state.f, args.f) : state.f,
+                                is_clockwise: cmd === 'G2'
+                            };
+                            /*  If R format is working, this could be used.  I have no test code so I can't verify
+                            if ((arc.i || arc.j) && arc.r)
+                            {
+                                console.warn('THREE.GCodeLoader: Arc contains I/J and R, which is not allowed.  Removing R');
+                                arc.r = null;
+                            }
+                            else
+                            {
+                                var segments = self.interpolateArc(state, arc);
+                                for(var index = 1; index < segments.length; index++)
+                                {
+                                    this.addSegment(segments[index-1], segments[index]);
+                                }
+                            }*/
+                            var segments = self.interpolateArc(state, arc);
+                            for (var index = 1; index < segments.length; index++) {
+                                this.addSegment(segments[index - 1], segments[index]);
+                            }
+                            // Set the state to the last segment
+                            state = segments[segments.length - 1];
+                        }
+                    } else if (cmd === 'G90') {
+                        //G90: Set to Absolute Positioning
+                        state.relative = false;
+                    } else if (cmd === 'G91') {
+                        //G91: Set to state.relative Positioning
+                        state.relative = true;
+                    } else if (cmd === 'G92') {
+                        //G92: Set Position
+                        var line = state;
+                        line.x = args.x !== undefined ? args.x : line.x;
+                        line.y = args.y !== undefined ? args.y : line.y;
+                        line.z = args.z !== undefined ? args.z : line.z;
+                        line.e = args.e !== undefined ? args.e : line.e;
+                        state = line;
+                    } else {
+                        //console.warn( 'THREE.GCodeLoader: Command not supported:' + cmd );
+                    }
+                }
+            }
+
+        };
+
+        //todo move to new file or remove.
+        function updateDimensions(bsize) {
+
+            if (dimensionsGroup === undefined) {
+                dimensionsGroup = new THREE.Group();
+                dimensionsGroup.name = 'dimensions';
+                scene.add(dimensionsGroup);
+            }
+
+            var fontLoader = new THREE.FontLoader();
+            fontLoader.load('plugin/prettygcode/static/js/helvetiker_bold.typeface.json', function (font) {
+                var xMid, text;
+                var color = 0x006699;
+                var matDark = new THREE.LineBasicMaterial({
+                    color: color,
+                    side: THREE.DoubleSide
+                });
+                var matLite = new THREE.MeshBasicMaterial({
+                    color: color,
+                    transparent: true,
+                    opacity: 0.8,
+                    side: THREE.DoubleSide
+                });
+                var center = new THREE.Vector3(0, 0, 0);
+                sceneBounds.getCenter(center);
+                //console.log(["center",center]);
+                //clear out any old lines
+                for (var i = dimensionsGroup.children.length - 1; i >= 0; i--) {
+                    dimensionsGroup.remove(dimensionsGroup.children[i]);
+                }
+                var textHeight = 3;
+                var textZ = 0.2;
+                var message = bsize.x.toFixed(2) + " MM";
+                var shapes = font.generateShapes(message, textHeight);
+                var geometry = new THREE.ShapeBufferGeometry(shapes);
+                geometry.computeBoundingBox();
+                xMid = -0.5 * (geometry.boundingBox.max.x - geometry.boundingBox.min.x);
+                geometry.translate(xMid, 0, 0);
+                // make shape ( N.B. edge view not visible )
+                text = new THREE.Mesh(geometry, matLite);
+                text.position.set(center.x, sceneBounds.min.y - (textHeight * 2), textZ);
+                dimensionsGroup.add(text);
+                var lineMat = new THREE.LineMaterial({
+                    linewidth: 6,
+                    color: color
+                });
+                lineMat.resolution.set(gcodeWid, gcodeHei);
+                var lineGeo = new THREE.LineGeometry();
+                var lineVerts = [
+                    sceneBounds.min.x, sceneBounds.min.y - (textHeight * 0.8), textZ,
+                    sceneBounds.max.x, sceneBounds.min.y - (textHeight * 0.8), textZ,
+                    sceneBounds.min.x, sceneBounds.min.y - 1, textZ,
+                    sceneBounds.min.x, sceneBounds.min.y - (textHeight * 1.2), textZ,
+                    sceneBounds.max.x, sceneBounds.min.y - 1, textZ,
+                    sceneBounds.max.x, sceneBounds.min.y - (textHeight * 1.2), textZ,
+                ];
+                lineGeo.setPositions(lineVerts);
+                var line = new THREE.Line2(lineGeo, lineMat);
+                dimensionsGroup.add(line);
+                var textHeight = 3;
+                var message = bsize.y.toFixed(2) + " MM";
+                var shapes = font.generateShapes(message, textHeight);
+                var geometry = new THREE.ShapeBufferGeometry(shapes);
+                geometry.computeBoundingBox();
+                xMid = -0.5 * (geometry.boundingBox.max.x - geometry.boundingBox.min.x);
+                geometry.translate(xMid, 0, 0);
+                geometry.rotateZ(Math.PI / 2);
+                // make shape ( N.B. edge view not visible )
+                text = new THREE.Mesh(geometry, matLite);
+                text.position.set(sceneBounds.max.x + (textHeight * 2), center.y, textZ);
+                dimensionsGroup.add(text);
+                var lineGeo = new THREE.LineGeometry();
+                var lineVerts = [
+                    sceneBounds.max.x + (textHeight * 0.8), sceneBounds.min.y, textZ,
+                    sceneBounds.max.x + (textHeight * 0.8), sceneBounds.max.y, textZ,
+                    sceneBounds.max.x + 1, sceneBounds.min.y, textZ,
+                    sceneBounds.max.x + (textHeight * 1.2), sceneBounds.min.y, textZ,
+                    sceneBounds.max.x + 1, sceneBounds.max.y, textZ,
+                    sceneBounds.max.x + (textHeight * 1.2), sceneBounds.max.y, textZ,
+                ];
+                lineGeo.setPositions(lineVerts);
+                var line = new THREE.Line2(lineGeo, lineMat);
+                dimensionsGroup.add(line);
+                var textHeight = 3;
+                var message = bsize.z.toFixed(2) + " MM";
+                var shapes = font.generateShapes(message, textHeight);
+                var geometry = new THREE.ShapeBufferGeometry(shapes);
+                geometry.computeBoundingBox();
+                xMid = 0; // - 0.5 * ( geometry.boundingBox.max.x - geometry.boundingBox.min.x );
+                geometry.translate(xMid, 0, 0);
+                geometry.rotateX(Math.PI / 2);
+                // make shape ( N.B. edge view not visible )
+                text = new THREE.Mesh(geometry, matLite);
+                text.position.set(sceneBounds.max.x + (textHeight * 1), sceneBounds.max.y, center.z);
+                dimensionsGroup.add(text);
+                var lineGeo = new THREE.LineGeometry();
+                var lineVerts = [
+                    sceneBounds.max.x + (textHeight * 0.8), sceneBounds.max.y + (textHeight * 0.8), 0,
+                    sceneBounds.max.x + (textHeight * 0.8), sceneBounds.max.y + (textHeight * 0.8), bsize.z,
+                ];
+                lineGeo.setPositions(lineVerts);
+                var line = new THREE.Line2(lineGeo, lineMat);
+                dimensionsGroup.add(line);
+            });
+        }
+
+        function resizeCanvasToDisplaySize() {
+            const canvas = renderer.domElement;
+            // look up the size the canvas is being displayed
+            const width = canvas.clientWidth;
+            const height = canvas.clientHeight;
+
+            // adjust displayBuffer size to match
+            if (canvas.width !== width || canvas.height !== height) {
+                // you must pass false here or three.js sadly fights the browser
+                renderer.setSize(width, height, false);
+                camera.aspect = width / height;
+                camera.updateProjectionMatrix();
+                gcodeWid = width;
+                gcodeHei = height;
+                cameraControls.setViewport(0, 0, width, height);
+                return true;//update needed. 
+            }
+            return false;//no update needed
+        }
+
+        function initThree() {
+            renderer = new THREE.WebGLRenderer({ canvas: document.getElementById("mycanvas"), antialias: pgSettings.antialias });
+            //todo. is this right?
+            renderer.setPixelRatio(window.devicePixelRatio);
+
+            //renderer2 = new THREE.WebGLRenderer({ canvas: document.getElementById("pipcanvas") });
+            //todo. is this right?
+            //renderer2.setPixelRatio(window.devicePixelRatio*3.0);
+
+
+            //todo allow save/pos camera at start.
+            camera = new THREE.PerspectiveCamera(70, 2, 0.1, 10000);
+            camera.up.set(0, 0, 1);
+            camera.position.set(bedVolume.width, 0, 50);
+
+            CameraControls.install({ THREE: THREE });
+            clock = new THREE.Clock();
+
+            var canvas = $("#mycanvas");
+            cameraControls = new CameraControls(camera, canvas[0]);
+
+            //todo handle other than lowerleft
+            resetCamera();
+
+            lightBackground = 0xd0d0d0;
+            darkBackground = 0x000000;
+
+            //for debugging
+            window.myCameraControls = cameraControls;
+
+            //scene
+            scene = new THREE.Scene();
+            if (pgSettings.darkMode) {
+                scene.background = new THREE.Color(darkBackground);
+            } else {
+                scene.background = new THREE.Color(lightBackground);
+            }
+
+            //for debugging
+            window.myScene = scene;
+
+            //add a light. might not be needed.
+            var light = new THREE.PointLight(0xffffff);
+            light.position.set(0, 0, -bedVolume.height);
+            scene.add(light);
+
+            // light = new THREE.PointLight(0xffffff);
+            // light.position.set(bedVolume.width/2, bedVolume.depth/2,bedVolume.height);
+            // scene.add(light);
+
+            cameraLight = new THREE.PointLight(0xffffff);
+            cameraLight.position.copy(camera.position);
+            scene.add(cameraLight);
+
+            // light = new THREE.AmbientLight( 0xffffff ); // soft white light
+            // scene.add( light );
+
+            // light = new THREE.PointLight(0xffffff);
+            // light.position.copy(camera.position);
+            // scene.add(light);
+
+
+            //Semi-transparent plane to represent the bed. 
+            updateGridMesh();
+
+            cubeCamera = new THREE.CubeCamera(1, 100000, 128);
+            cubeCamera.position.set(bedVolume.width / 2, bedVolume.depth / 2, 10);
+            scene.add(cubeCamera);
+            cubeCamera.update(renderer, scene);
+
+            var syncSavedZ = 0;
+            var cameraIdleTime = 0;
+            var firstFrame = true;                 /*possible bug fix. this might not be needed.*/
+
+            //material for fatline highlighter
+            var highlightMaterial = undefined;
+
+            if (pgSettings.fatLines) {
+                highlightMaterial = new THREE.LineMaterial({
+                    linewidth: 4, // in pixels
+                    //transparent: true,
+                    //opacity: 0.5,
+                    //color: new THREE.Color(curColorHex),// rainbow.getColor(layers.length % 64).getHex()
+                    vertexColors: THREE.VertexColors,
+                });
+                highlightMaterial.resolution.set(500, 500);
+            } else {
+                //highlightMaterial=
+            }
+
+            function animate() {
+
+                const delta = clock.getDelta();
+                const elapsed = clock.getElapsedTime();
+
+                var needRender = false;
+
+                /*possible bug fix. this might not be needed.*/
+                if (firstFrame) {
+                    needRender = true;
+                    firstFrame = false;
+                }
+
+                if (printHeadSim) {
+                    printHeadSim.updatePosition(delta);
+                }
+                if (curPrinterState &&
+                    (curPrinterState.flags.printing || curPrinterState.flags.paused) &&
+                    pgSettings.syncToProgress && (!forceNoSync)) {
+                    if (nozzleModel && printHeadSim) {
+                        var curState = printHeadSim.getCurPosition();
+                        nozzleModel.position.copy(curState.position);
+                        needRender = true;
+                    }
+                    if (gcodeProxy) {
+                        var calculatedLayer = gcodeProxy.syncGcodeObjToFilePos(curPrintFilePos);
+                        if (highlightMaterial !== undefined) {
+                            gcodeProxy.highlightLayer(calculatedLayer, highlightMaterial);
+                        }
+
+                        $("#myslider-vertical").slider('setValue', calculatedLayer, false, true);
+                        $("#myslider .slider-handle").text(calculatedLayer);
+
+                        needRender = true;
+                        //    gcodeProxy.syncGcodeObjTo(curState.layerZ,curState.lineNumber-1/*-window.fudge*/);//todo. figure out why *2 is needed.
+                    }
+                } else {
+                    if (nozzleModel && nozzleModel.position.lengthSq()) {
+                        nozzleModel.position.set(0, 0, 0);//todo. hide instead/also?
+                        needRender = true;
+                    }
+
+                    if (gcodeProxy) {
+                        if (gcodeProxy.syncGcodeObjToLayer(currentLayerNumber)) {
+                            if (highlightMaterial !== undefined) {
+                                gcodeProxy.highlightLayer(currentLayerNumber, highlightMaterial);
+                            }
+                            needRender = true;
+                            //console.log("GCode Proxy needs update");
+                        }
+                    }
+
+                }
+
+                //show or hide nozzle based on settings.
+                if (nozzleModel && nozzleModel.visible != pgSettings.showNozzle) {
+                    nozzleModel.visible = pgSettings.showNozzle;
+                    needRender = true;
+                }
+
+                if (highlightMaterial !== undefined) {
+                    //fake a glow by ramping the diffuse color.
+                    let nv = 0.5 + ((Math.sin(elapsed * 4) + 1) / 4.0);
+                    //console.log(nv);
+                    //highlightMaterial.uniforms.linewidth.value=nv*15;
+                    nv = 0.5;
+                    highlightMaterial.uniforms.diffuse.value.r = nv;
+                    highlightMaterial.uniforms.diffuse.value.g = nv;
+                    highlightMaterial.uniforms.diffuse.value.b = nv;
+                }
+
+
+                //if(gcodeProxy)
+                //    gcodeProxy.animateLayers(elapsed)
+
+
+
+                cameraControls.dollyToCursor = true;//todo. needed every frame?
+                const updated = cameraControls.update(delta);//handle mouse/keyboard etc.
+                if (updated)//did user move the camera?
+                {
+                    cameraIdleTime = 0;
+                    needRender = true;
+                }
+                else {
+                    cameraIdleTime += delta;
+                    if (pgSettings.orbitWhenIdle && cameraIdleTime > 5) {
+                        cameraControls.rotate(delta / 5.0, 0, false);//auto orbit camera a bit.
+                        cameraControls.update(delta);//force update so it wont look like manual move next frame.
+                        needRender = true;
+                    }
+                }
+
+                if (cameraLight) {
+                    cameraLight.position.copy(camera.position);
+                }
+
+                if (resizeCanvasToDisplaySize())
+                    needRender = true;
+
+                if (needRender) {
+                    // //do real time reflections. Probably overkill. Certianly overkill.
+                    // if(pgSettings.reflections && cubeCamera && nozzleModel)
+                    // {
+                    //     cubeCamera.position.copy( nozzleModel.position );
+                    //     cubeCamera.position.z=cubeCamera.position.z+10;
+                    //     nozzleModel.visible=false;
+                    //     cubeCamera.update( renderer, scene );
+                    //     nozzleModel.visible=true;
+                    // }
+
+                    renderer.render(scene, camera);
+                } else {
+                    //console.log("idle");
+                }
+
+                //renderer2.render(scene, camera);
+                requestAnimationFrame(animate);
+            }
+
+            animate();
+        }
+
+        function resetCamera() {
+
+            if (!cameraControls)//Make sure controls exist. 
+                return;
+
+            if (bedVolume.origin == "lowerleft")
+                cameraControls.setTarget(bedVolume.width / 2, bedVolume.depth / 2, 0, false);
+            else
+                cameraControls.setTarget(0, 0, 0, false);
+        }
+
+        function updateGridMesh() {
+            //console.log("updateGridMesh");
+            console.log(arguments.callee.name);
+
+            if (!scene)//scene loaded yet?
+                return;
+
+            var existingPlane = scene.getObjectByName("plane");
+            if (existingPlane)
+                scene.remove(existingPlane);
+            var existingGrid = scene.getObjectByName("grid");
+            if (existingGrid)
+                scene.remove(existingGrid);
+
+            console.log([existingPlane, existingGrid]);
+
+            var planeGeometry = new THREE.PlaneGeometry(bedVolume.width, bedVolume.depth);
+            var planeMaterial = new THREE.MeshBasicMaterial({
+                color: 0x909090,
+                side: THREE.DoubleSide,
+                transparent: true,
+                opacity: 0.2,
+            });
+            var plane = new THREE.Mesh(planeGeometry, planeMaterial);
+            plane.name = "plane";
+            //todo handle other than lowerleft
+            if (bedVolume.origin == "lowerleft")
+                plane.position.set(bedVolume.width / 2, bedVolume.depth / 2, -0.1);
+            //plane.quaternion.setFromEuler(new THREE.Euler(- Math.PI / 2, 0, 0));
+            scene.add(plane);
+            //make bed sized grid. 
+            var grid = new THREE.GridHelper(bedVolume.width, bedVolume.width / 10, 0x000000, 0x888888);
+            grid.name = "grid";
+            //todo handle other than lowerleft
+            if (bedVolume.origin == "lowerleft")
+                grid.position.set(bedVolume.width / 2, bedVolume.depth / 2, 0);
+            //if (pgSettings.transparency){
+            grid.material.opacity = 0.6;
+            grid.material.transparent = true;
+            grid.quaternion.setFromEuler(new THREE.Euler(-Math.PI / 2, 0, 0));
+            scene.add(grid);
+        }
+    }
+
+    OCTOPRINT_VIEWMODELS.push({
+        construct: PrettyGCodeViewModel,
+        dependencies: ["settingsViewModel", "loginStateViewModel", "printerProfilesViewModel", "controlViewModel"],
+        elements: ["#tab_plugin_prettygcode"]
+    });
+
+
+});
+
+

+ 142 - 0
gcode_viewer/js/slider-shim.js

@@ -0,0 +1,142 @@
+/**
+ * slider-shim.js
+ * Minimal jQuery plugin shim for the bootstrap-slider API used by prettygcode.js.
+ *
+ * Supports the subset used by PrettyGCode:
+ *   $(el).slider(opts)              — init
+ *   $(el).slider("setValue", v)     — set value
+ *   $(el).slider("setMax", v)       — update max
+ *   $(el).on("slide", fn)           — fires with event.value
+ *   $(el).on("slideStart", fn)
+ *   $(el).on("slideStop", fn)
+ */
+(function ($) {
+    'use strict';
+
+    $.fn.slider = function (optsOrCmd, cmdArg1, cmdArg2, cmdArg3) {
+        return this.each(function () {
+            var $el = $(this);
+            var data = $el.data('_pgslider');
+
+            // ---------- init ----------
+            if (!data || typeof optsOrCmd === 'object') {
+                var opts = $.extend({
+                    id: null,
+                    orientation: 'horizontal',
+                    reversed: false,
+                    min: 0,
+                    max: 100,
+                    value: 0,
+                }, typeof optsOrCmd === 'object' ? optsOrCmd : {});
+
+                // Build the DOM
+                var isVertical = opts.orientation === 'vertical';
+                var trackHtml =
+                    '<div class="slider' + (isVertical ? ' slider-vertical' : '') + '"' +
+                    (opts.id ? ' id="' + opts.id + '"' : '') + '>' +
+                    '<div class="slider-track"><div class="slider-selection"></div></div>' +
+                    '<div class="slider-handle round">0</div>' +
+                    '</div>';
+                $el.html(trackHtml);
+
+                var $slider = $el.find('.slider');
+                var $handle = $el.find('.slider-handle');
+                var $selection = $el.find('.slider-selection');
+                var isDragging = false;
+
+                data = {
+                    opts: opts,
+                    $slider: $slider,
+                    $handle: $handle,
+                    $selection: $selection,
+                };
+                $el.data('_pgslider', data);
+
+                function pct(v) {
+                    var range = data.opts.max - data.opts.min;
+                    if (range === 0) return 0;
+                    var p = (v - data.opts.min) / range * 100;
+                    return opts.reversed ? 100 - p : p;
+                }
+
+                function updateUI(val) {
+                    var p = pct(val);
+                    $handle.text(val);
+                    if (isVertical) {
+                        $handle.css({ top: p + '%', bottom: '' });
+                        $selection.css({ height: (100 - p) + '%', top: p + '%' });
+                    } else {
+                        $handle.css({ left: p + '%' });
+                        $selection.css({ width: p + '%' });
+                    }
+                }
+
+                data.updateUI = updateUI;
+                updateUI(opts.value);
+                data.opts.value = opts.value;
+
+                // Mouse interaction
+                function getValueFromEvent(e) {
+                    var offset = $slider.offset();
+                    var range = data.opts.max - data.opts.min;
+                    var p;
+                    if (isVertical) {
+                        var h = $slider.height();
+                        p = (e.pageY - offset.top) / h;
+                    } else {
+                        var w = $slider.width();
+                        p = (e.pageX - offset.left) / w;
+                    }
+                    p = Math.max(0, Math.min(1, p));
+                    if (opts.reversed) p = 1 - p;
+                    return Math.round(data.opts.min + p * range);
+                }
+
+                $slider.on('mousedown', function (e) {
+                    isDragging = true;
+                    var val = getValueFromEvent(e);
+                    data.opts.value = val;
+                    updateUI(val);
+                    var ev = $.Event('slideStart'); ev.value = val;
+                    $el.trigger(ev);
+                    e.preventDefault();
+                });
+
+                $(document).on('mousemove.pgslider_' + $el.attr('id'), function (e) {
+                    if (!isDragging) return;
+                    var val = getValueFromEvent(e);
+                    data.opts.value = val;
+                    updateUI(val);
+                    var ev = $.Event('slide'); ev.value = val;
+                    $el.trigger(ev);
+                });
+
+                $(document).on('mouseup.pgslider_' + $el.attr('id'), function (e) {
+                    if (!isDragging) return;
+                    isDragging = false;
+                    var val = getValueFromEvent(e);
+                    data.opts.value = val;
+                    updateUI(val);
+                    var ev = $.Event('slideStop'); ev.value = val;
+                    $el.trigger(ev);
+                });
+
+                return;
+            }
+
+            // ---------- commands ----------
+            if (optsOrCmd === 'setValue') {
+                data.opts.value = cmdArg1;
+                data.updateUI(cmdArg1);
+                // prettygcode.js calls slider('setValue', N, false, true) after loading
+                // — the third arg means "trigger the slide event so listeners update state"
+                if (cmdArg3) {
+                    var ev = $.Event('slide'); ev.value = cmdArg1; $el.trigger(ev);
+                }
+            } else if (optsOrCmd === 'setMax') {
+                data.opts.max = cmdArg1;
+                data.updateUI(data.opts.value);
+            }
+        });
+    };
+}(jQuery));

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 576 - 0
gcode_viewer/js/three.min.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/index-BfEnlXcp.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-aHxaU9HU.js"></script>
+    <script type="module" crossorigin src="/assets/index-BfEnlXcp.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-BoxU3Y8Y.css">
     <link rel="stylesheet" crossorigin href="/assets/index-BoxU3Y8Y.css">
   </head>
   </head>
   <body>
   <body>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů