From 20804f70611a40e682eb4ed7289cde659f6bb82a Mon Sep 17 00:00:00 2001 From: Phoomparin Mano Date: Sun, 24 May 2026 14:40:29 +0700 Subject: [PATCH 01/12] add prototype for deckgl --- .../specs/147-deckgl-fbo-renderer.md | 46 ++ ui/bun.lock | 86 +++- ui/package.json | 4 + ui/src/lib/ai/object-descriptions-types.ts | 2 + ui/src/lib/ai/object-prompts/deckgl.ts | 28 ++ ui/src/lib/ai/object-prompts/index.ts | 2 + ui/src/lib/canvas/CanvasMouseHandler.ts | 26 +- ui/src/lib/canvas/SurfaceMouseForwarder.ts | 4 +- ui/src/lib/canvas/constants.ts | 20 + ui/src/lib/canvas/surfaceMouseForwarding.ts | 7 + ui/src/lib/codemirror/patchies-completions.ts | 10 +- ui/src/lib/components/nodes/DeckGLNode.svelte | 399 ++++++++++++++++++ .../object-browser/get-categorized-objects.ts | 3 +- ui/src/lib/extensions/object-packs.ts | 1 + ui/src/lib/nodes/defaultNodeData.ts | 8 + ui/src/lib/nodes/node-types.ts | 2 + ui/src/lib/objects/schemas/deckgl.ts | 36 ++ ui/src/lib/objects/schemas/index.ts | 3 + ui/src/lib/rendering/types.ts | 10 + ui/src/workers/rendering/deckglRenderer.ts | 340 +++++++++++++++ ui/src/workers/rendering/fboRenderer.ts | 56 ++- ui/src/workers/rendering/renderWorker.ts | 6 + ui/src/workers/rendering/workerDOMMocks.ts | 26 +- ui/static/content/objects/deckgl.md | 49 +++ 24 files changed, 1161 insertions(+), 13 deletions(-) create mode 100644 docs/design-docs/specs/147-deckgl-fbo-renderer.md create mode 100644 ui/src/lib/ai/object-prompts/deckgl.ts create mode 100644 ui/src/lib/components/nodes/DeckGLNode.svelte create mode 100644 ui/src/lib/objects/schemas/deckgl.ts create mode 100644 ui/src/workers/rendering/deckglRenderer.ts create mode 100644 ui/static/content/objects/deckgl.md diff --git a/docs/design-docs/specs/147-deckgl-fbo-renderer.md b/docs/design-docs/specs/147-deckgl-fbo-renderer.md new file mode 100644 index 000000000..b4e4646ab --- /dev/null +++ b/docs/design-docs/specs/147-deckgl-fbo-renderer.md @@ -0,0 +1,46 @@ +# 147. DeckGL FBO Renderer + +## Goal + +Prototype a `deckgl` visual object that renders deck.gl layers inside the worker FBO pipeline, so deck.gl output can be chained like other video-producing objects. + +## Approach + +- Use deck.gl's experimental `_framebuffer` render target support. +- Attach luma.gl to the render worker's existing WebGL2 context. +- Render deck.gl into a luma-owned framebuffer and blit that framebuffer into the Patchies regl framebuffer. +- Disable deck.gl's built-in controller path and drive `viewState` from Patchies mouse and wheel forwarding. +- Keep the first prototype focused on a single video outlet and a simple layer API. + +## User Code Shape + +The object exposes deck.gl classes and expects user code to define `getLayers`. + +```js +function getLayers({ time, viewState, mouse }) { + return [ + new ScatterplotLayer({ + id: 'points', + data, + getPosition: d => d.position, + getRadius: 400, + getFillColor: [255, 140, 40], + pickable: true + }) + ] +} +``` + +## Interaction + +- Drag forwards through the existing Shadertoy-style mouse state. +- Left-drag pans longitude/latitude. +- Wheel changes zoom. +- The surface object forwards pointer and wheel input to `deckgl` the same way it does for worker `three`. + +## Non-goals + +- No Mapbox basemap integration in this prototype. +- No WebGPU path in this prototype. +- No production-grade picking or tooltip UI yet. +- No direct rendering into a regl-owned framebuffer until the safer luma-owned target path is proven. diff --git a/ui/bun.lock b/ui/bun.lock index 415394beb..58c95a212 100644 --- a/ui/bun.lock +++ b/ui/bun.lock @@ -15,12 +15,16 @@ "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.38.1", "@csound/browser": "file:./packages/csound-browser", + "@deck.gl/core": "^9.3.0-beta.2", + "@deck.gl/layers": "^9.3.0-beta.2", "@elemaudio/core": "^4.0.1", "@elemaudio/web-renderer": "^4.0.3", "@google/genai": "^1.11.0", "@iizukak/codemirror-lang-wgsl": "^0.3.0", "@lezer/generator": "^1.8.0", "@lezer/highlight": "^1.2.1", + "@luma.gl/core": "^9.3.3", + "@luma.gl/webgl": "^9.3.3", "@mediapipe/tasks-vision": "0.10.33", "@replit/codemirror-vim": "^6.3.0", "@rollup/browser": "^4.50.1", @@ -402,6 +406,10 @@ "@davepagurek/bezier-path": ["@davepagurek/bezier-path@0.0.2", "", {}, "sha512-4L9ddgzZc9DRGyl1RrS3z5nwnVJoyjsAelVG4X1jh4tVxryEHr4H9QavhxW/my6Rn3669Qz6mhv8gd5O/WeFTA=="], + "@deck.gl/core": ["@deck.gl/core@9.3.2", "", { "dependencies": { "@loaders.gl/core": "^4.4.1", "@loaders.gl/images": "^4.4.1", "@luma.gl/core": "^9.3.3", "@luma.gl/engine": "^9.3.3", "@luma.gl/shadertools": "^9.3.3", "@luma.gl/webgl": "^9.3.3", "@math.gl/core": "^4.1.0", "@math.gl/sun": "^4.1.0", "@math.gl/types": "^4.1.0", "@math.gl/web-mercator": "^4.1.0", "@probe.gl/env": "^4.1.1", "@probe.gl/log": "^4.1.1", "@probe.gl/stats": "^4.1.1", "@types/offscreencanvas": "^2019.6.4", "gl-matrix": "^3.0.0", "mjolnir.js": "^3.0.0" } }, "sha512-32Va3np0Zdlz/LBNtDWCs4EkKqdHmXcbGmVp4+7i1Cpdza8y8CFmJs2VPOmSX1fwHvNCGkAZV/SFZOfDb2INsg=="], + + "@deck.gl/layers": ["@deck.gl/layers@9.3.2", "", { "dependencies": { "@loaders.gl/images": "^4.4.1", "@loaders.gl/schema": "^4.4.1", "@luma.gl/shadertools": "^9.3.3", "@mapbox/tiny-sdf": "^2.0.5", "@math.gl/core": "^4.1.0", "@math.gl/polygon": "^4.1.0", "@math.gl/web-mercator": "^4.1.0", "earcut": "^2.2.4" }, "peerDependencies": { "@deck.gl/core": "~9.3.0", "@loaders.gl/core": "^4.4.1", "@luma.gl/core": "~9.3.3", "@luma.gl/engine": "~9.3.3" } }, "sha512-TeVfhQ/cQU1oTlTn16mCp7268d1uBJ6dwfgmKXThe2TzW9hql3iJaxbYTKg2phDg5YSiGmeEOpXbeBh59jyUcA=="], + "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], "@elemaudio/core": ["@elemaudio/core@4.0.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "invariant": "^2.2.4", "shallowequal": "^1.1.0" } }, "sha512-O58CaR5ttxzPt1LwkJoXVipPLlZ8lZmOIHhJFiih3rWkfCZ3/H1X2XTG0HM2jAn3BokoP5jYSbnbEiQybyvRGw=="], @@ -598,10 +606,42 @@ "@lezer/python": ["@lezer/python@1.1.18", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg=="], + "@loaders.gl/core": ["@loaders.gl/core@4.4.2", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/schema": "4.4.2", "@loaders.gl/schema-utils": "4.4.2", "@loaders.gl/worker-utils": "4.4.2", "@probe.gl/log": "^4.1.1" } }, "sha512-DZmsTwxdKh3q+mS1vSOW2EXFgwxZ4nIBte4H5g6e4VyQoQ6jAOkk0M6V+Asgy/eqjGTNjhfBA1HIkyBl0A9hcA=="], + + "@loaders.gl/images": ["@loaders.gl/images@4.4.2", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.2" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-b+1keNvPlyLniWtX4ZaThz2dF2aohi8Q+OEsDF2hJNZYyZJOqP9b/72UhlVk+inxTJfTLRBNARs2TJ2ssBlelg=="], + + "@loaders.gl/loader-utils": ["@loaders.gl/loader-utils@4.4.2", "", { "dependencies": { "@loaders.gl/schema": "4.4.2", "@loaders.gl/worker-utils": "4.4.2", "@probe.gl/log": "^4.1.1", "@probe.gl/stats": "^4.1.1" } }, "sha512-kqwBbyRC7rrQVsnJyKeoaig9hxaa5oj91OKqWm27HPuVn4q2dD67SEhiG0ND62eRp0tLY6jTqEcI5kDzHBZ6MA=="], + + "@loaders.gl/schema": ["@loaders.gl/schema@4.4.2", "", { "dependencies": { "@types/geojson": "^7946.0.7", "apache-arrow": ">= 17.0.0" } }, "sha512-mJTZehTHIFl8ed+03nebuPAMnLP8Yp00DKTzCnKT2HNy/uV4+Sw+GrGIuhPHGU8tdQmtBXRURGM2ZxUAxMfGKg=="], + + "@loaders.gl/schema-utils": ["@loaders.gl/schema-utils@4.4.2", "", { "dependencies": { "@loaders.gl/schema": "4.4.2", "@types/geojson": "^7946.0.7", "apache-arrow": ">= 17.0.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-yYYRD/POBEO72rhIyLASrqKUUhfIOQuFk/fgInN6Td2qvFgsHbo5UaCM4sTqVUWwNxNvXDQi8ezpbnCa/yi+OQ=="], + + "@loaders.gl/worker-utils": ["@loaders.gl/worker-utils@4.4.2", "", { "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-oiZ0SoC1QKrOkhYPlVZ6Q06CtmuFRyZw2rwzmT08ZyaGtOArIJHDjlhxzwWiv+6fdws47Ub5uIGsdI1Ab1xYsA=="], + "@lucide/svelte": ["@lucide/svelte@0.575.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-FEFp/0McZwsjBqh1Dn8H+UBm1yHFQYk+utuVMFDw57155+wz2XMoc1pw027ylCPzs+bi14UEXYKbekFhuJKtnw=="], + "@luma.gl/core": ["@luma.gl/core@9.3.3", "", { "dependencies": { "@math.gl/types": "^4.1.0", "@probe.gl/env": "^4.1.1", "@probe.gl/log": "^4.1.1", "@probe.gl/stats": "^4.1.1", "@types/offscreencanvas": "^2019.7.3" } }, "sha512-jCFm2htvrVpcXIy85TBTF1ROgMfknKnfw2OH+Vydr41hiCFd6nqr79gM3f2uhaNkal0BghFNqF3qDioKiUWtew=="], + + "@luma.gl/engine": ["@luma.gl/engine@9.3.3", "", { "dependencies": { "@math.gl/core": "^4.1.0", "@math.gl/types": "^4.1.0", "@probe.gl/log": "^4.1.1", "@probe.gl/stats": "^4.1.1" }, "peerDependencies": { "@luma.gl/core": "~9.3.0", "@luma.gl/shadertools": "~9.3.0" } }, "sha512-StmMTzUcUlpKMU3wvWU48A6OQyphptD9zVGBsSkK6iHIBdtBKlOcmqRkyfvRouo8JHtlrnoJDHLVKhxorwhGAg=="], + + "@luma.gl/shadertools": ["@luma.gl/shadertools@9.3.3", "", { "dependencies": { "@math.gl/core": "^4.1.0", "@math.gl/types": "^4.1.0" }, "peerDependencies": { "@luma.gl/core": "~9.3.0" } }, "sha512-4ZfG4/Utix951vqyiG/JIx+Eg+GMNwOxgr/07/i0gf7bK1gJZIEQ5BxVcDw4MCQfdoVlGPGzl0cQKbdqBvaCAQ=="], + + "@luma.gl/webgl": ["@luma.gl/webgl@9.3.3", "", { "dependencies": { "@math.gl/types": "^4.1.0", "@probe.gl/env": "^4.1.1" }, "peerDependencies": { "@luma.gl/core": "~9.3.0" } }, "sha512-X+aavdP5o6VFHSA0es9gKZTT145jfcFbhKJt/gwJrptnKNoIW4+Y37ZEpCo1AzAnr+FQCxjgcM2kOCpoWMfSVA=="], + + "@mapbox/tiny-sdf": ["@mapbox/tiny-sdf@2.2.0", "", {}, "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg=="], + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], + "@math.gl/core": ["@math.gl/core@4.1.0", "", { "dependencies": { "@math.gl/types": "4.1.0" } }, "sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA=="], + + "@math.gl/polygon": ["@math.gl/polygon@4.1.0", "", { "dependencies": { "@math.gl/core": "4.1.0" } }, "sha512-YA/9PzaCRHbIP5/0E9uTYrqe+jsYTQoqoDWhf6/b0Ixz8bPZBaGDEafLg3z7ffBomZLacUty9U3TlPjqMtzPjA=="], + + "@math.gl/sun": ["@math.gl/sun@4.1.0", "", {}, "sha512-i3q6OCBLSZ5wgZVhXg+X7gsjY/TUtuFW/2KBiq/U1ypLso3S4sEykoU/MGjxUv1xiiGtr+v8TeMbO1OBIh/HmA=="], + + "@math.gl/types": ["@math.gl/types@4.1.0", "", {}, "sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA=="], + + "@math.gl/web-mercator": ["@math.gl/web-mercator@4.1.0", "", { "dependencies": { "@math.gl/core": "4.1.0" } }, "sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw=="], + "@mediapipe/face_detection": ["@mediapipe/face_detection@0.4.1646425229", "", {}, "sha512-aeCN+fRAojv9ch3NXorP6r5tcGVLR3/gC1HmtqB0WEZBRXrdP6/3W/sGR0dHr1iT6ueiK95G9PVjbzFosf/hrg=="], "@mediapipe/face_mesh": ["@mediapipe/face_mesh@0.4.1633559619", "", {}, "sha512-Vc8cdjxS5+O2gnjWH9KncYpUCVXT0h714KlWAsyqJvJbIgUJBqpppbIx8yWcAzBDxm/5cYSuBI5p5ySIPxzcEg=="], @@ -634,6 +674,12 @@ "@poppinss/exception": ["@poppinss/exception@1.2.2", "", {}, "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg=="], + "@probe.gl/env": ["@probe.gl/env@4.1.1", "", {}, "sha512-+68seNDMVsEegRB47pFA/Ws1Fjy8agcFYXxzorKToyPcD6zd+gZ5uhwoLd7TzsSw6Ydns//2KEszWn+EnNHTbA=="], + + "@probe.gl/log": ["@probe.gl/log@4.1.1", "", { "dependencies": { "@probe.gl/env": "4.1.1" } }, "sha512-kcZs9BT44pL7hS1OkRGKYRXI/SN9KejUlPD+BY40DguRLzdC5tLG/28WGMyfKdn/51GT4a0p+0P8xvDn1Ez+Kg=="], + + "@probe.gl/stats": ["@probe.gl/stats@4.1.1", "", {}, "sha512-4VpAyMHOqydSvPlEyHwXaE+AkIdR03nX+Qhlxsk2D/IW4OVmDZgIsvJB1cDzyEEtcfKcnaEbfXeiPgejBceT6g=="], + "@replit/codemirror-emacs": ["@replit/codemirror-emacs@6.1.0", "", { "peerDependencies": { "@codemirror/autocomplete": "^6.0.2", "@codemirror/commands": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.1", "@codemirror/view": "^6.3.0" } }, "sha512-74DITnht6Cs6sHg02PQ169IKb1XgtyhI9sLD0JeOFco6Ds18PT+dkD8+DgXBDokne9UIFKsBbKPnpFRAz60/Lw=="], "@replit/codemirror-vim": ["@replit/codemirror-vim@6.3.0", "", { "peerDependencies": { "@codemirror/commands": "6.x.x", "@codemirror/language": "6.x.x", "@codemirror/search": "6.x.x", "@codemirror/state": "6.x.x", "@codemirror/view": "6.x.x" } }, "sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ=="], @@ -928,6 +974,10 @@ "@types/clone": ["@types/clone@0.1.30", "", {}, "sha512-vcxBr+ybljeSiasmdke1cQ9ICxoEwaBgM1OQ/P5h4MPj/kRyLcDl5L8PrftlbyV1kBbJIs3M3x1A1+rcWd4mEA=="], + "@types/command-line-args": ["@types/command-line-args@5.2.3", "", {}, "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw=="], + + "@types/command-line-usage": ["@types/command-line-usage@5.0.4", "", {}, "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg=="], + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], @@ -962,6 +1012,8 @@ "@types/fast-json-stable-stringify": ["@types/fast-json-stable-stringify@2.1.2", "", { "dependencies": { "fast-json-stable-stringify": "*" } }, "sha512-vsxcbfLDdjytnCnHXtinE40Xl46Wr7l/VGRGt7ewJwCPMKEHOdEsTxXX8xwgoR7cbc+6dE8SB4jlMrOV2zAg7g=="], + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], @@ -986,7 +1038,7 @@ "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], - "@types/offscreencanvas": ["@types/offscreencanvas@2019.3.0", "", {}, "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q=="], + "@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="], "@types/p5": ["@types/p5@1.7.7", "", {}, "sha512-WFuP7jqc5CkkMtCK/NphgvMnJz1Qi9CMuK7t6xLu/tuXkRdGQA4q4AD0dUYcChC0Oibe8PE8gbKSFPNF0BqVNw=="], @@ -1152,6 +1204,8 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "apache-arrow": ["apache-arrow@21.1.0", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/command-line-args": "^5.2.3", "@types/command-line-usage": "^5.0.4", "@types/node": "^24.0.3", "command-line-args": "^6.0.1", "command-line-usage": "^7.0.1", "flatbuffers": "^25.1.24", "json-bignum": "^0.0.3", "tslib": "^2.6.2" }, "bin": { "arrow2csv": "bin/arrow2csv.js" } }, "sha512-kQrYLxhC+NTVVZ4CCzGF6L/uPVOzJmD1T3XgbiUnP7oTeVFOFgEUu6IKNwCDkpFoBVqDKQivlX4RUFqqnWFlEA=="], + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], @@ -1280,6 +1334,8 @@ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk-template": ["chalk-template@0.4.0", "", { "dependencies": { "chalk": "^4.1.2" } }, "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg=="], + "charenc": ["charenc@0.0.2", "", {}, "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="], "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], @@ -1482,6 +1538,8 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "earcut": ["earcut@2.2.4", "", {}, "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], @@ -1656,6 +1714,8 @@ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + "flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="], + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], @@ -1710,6 +1770,8 @@ "gifenc": ["gifenc@1.0.3", "", {}, "sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw=="], + "gl-matrix": ["gl-matrix@3.4.4", "", {}, "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ=="], + "glamor": ["glamor@2.20.40", "", { "dependencies": { "fbjs": "^0.8.12", "inline-style-prefixer": "^3.0.6", "object-assign": "^4.1.1", "prop-types": "^15.5.10", "through": "^2.3.8" } }, "sha512-DNXCd+c14N9QF8aAKrfl4xakPk5FdcFwmH7sD0qnC0Pr7xoZ5W9yovhUrY/dJc3psfGGXC58vqQyRtuskyUJxA=="], "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], @@ -1942,6 +2004,8 @@ "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "json-bignum": ["json-bignum@0.0.3", "", {}, "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], @@ -2132,6 +2196,8 @@ "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], + "mjolnir.js": ["mjolnir.js@3.0.0", "", {}, "sha512-siX3YCG7N2HnmN1xMH3cK4JkUZJhbkhRFJL+G5N1vH0mh1t5088rJknIoqDFWDIU6NPGvRRgLnYW3ZHjSMEBLA=="], + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], "mkdirp2": ["mkdirp2@1.0.5", "", {}, "sha512-xOE9xbICroUDmG1ye2h4bZ8WBie9EGmACaco8K8cx6RlkJJrxGIqjGqztAI+NMhexXBcdGbSEzI6N3EJPevxZw=="], @@ -3072,7 +3138,7 @@ "@tensorflow/tfjs/regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], - "@tensorflow/tfjs-core/@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="], + "@tensorflow/tfjs-backend-webgl/@types/offscreencanvas": ["@types/offscreencanvas@2019.3.0", "", {}, "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q=="], "@tensorflow/tfjs-core/node-fetch": ["node-fetch@2.6.13", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA=="], @@ -3140,6 +3206,10 @@ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "apache-arrow/command-line-args": ["command-line-args@6.0.2", "", { "dependencies": { "array-back": "^6.2.3", "find-replace": "^5.0.2", "lodash.camelcase": "^4.3.0", "typical": "^7.3.0" }, "peerDependencies": { "@75lb/nature": "latest" }, "optionalPeers": ["@75lb/nature"] }, "sha512-AIjYVxrV9X752LmPDLbVYv8aMCuHPSLZJXEo2qo/xJfv+NYhaZ4sMSF01rM+gHPaMgvPM0l5D/F+Qx+i2WfSmQ=="], + + "apache-arrow/command-line-usage": ["command-line-usage@7.0.4", "", { "dependencies": { "array-back": "^6.2.2", "chalk-template": "^0.4.0", "table-layout": "^4.1.1", "typical": "^7.3.0" } }, "sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg=="], + "automation-events/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -3650,6 +3720,16 @@ "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "apache-arrow/command-line-args/array-back": ["array-back@6.2.3", "", {}, "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw=="], + + "apache-arrow/command-line-args/typical": ["typical@7.3.0", "", {}, "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw=="], + + "apache-arrow/command-line-usage/array-back": ["array-back@6.2.3", "", {}, "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw=="], + + "apache-arrow/command-line-usage/table-layout": ["table-layout@4.1.1", "", { "dependencies": { "array-back": "^6.2.2", "wordwrapjs": "^5.1.0" } }, "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA=="], + + "apache-arrow/command-line-usage/typical": ["typical@7.3.0", "", {}, "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw=="], + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -3912,6 +3992,8 @@ "@tonaljs/progression/@tonaljs/chord/@tonaljs/interval/@tonaljs/pitch": ["@tonaljs/pitch@5.0.2", "", {}, "sha512-mxaXJPPe+LIJdjzpZEl8I8Wx3dEvlzkBbsr2Ltwc2dTAdnErAZ5R0TxVq2egF27lMvQN2QPQPWI9iDPPdVUmrg=="], + "apache-arrow/command-line-usage/table-layout/wordwrapjs": ["wordwrapjs@5.1.1", "", {}, "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg=="], + "mocha/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], diff --git a/ui/package.json b/ui/package.json index b66990f52..e4e47b89a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -88,12 +88,16 @@ "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.38.1", "@csound/browser": "file:./packages/csound-browser", + "@deck.gl/core": "^9.3.0-beta.2", + "@deck.gl/layers": "^9.3.0-beta.2", "@elemaudio/core": "^4.0.1", "@elemaudio/web-renderer": "^4.0.3", "@google/genai": "^1.11.0", "@iizukak/codemirror-lang-wgsl": "^0.3.0", "@lezer/generator": "^1.8.0", "@lezer/highlight": "^1.2.1", + "@luma.gl/core": "^9.3.3", + "@luma.gl/webgl": "^9.3.3", "@mediapipe/tasks-vision": "0.10.33", "@replit/codemirror-vim": "^6.3.0", "@rollup/browser": "^4.50.1", diff --git a/ui/src/lib/ai/object-descriptions-types.ts b/ui/src/lib/ai/object-descriptions-types.ts index 997750cff..725d65108 100644 --- a/ui/src/lib/ai/object-descriptions-types.ts +++ b/ui/src/lib/ai/object-descriptions-types.ts @@ -54,6 +54,7 @@ export const OBJECT_TYPE_LIST = `## Basic Control & UI - float.tex: Convert Float32Array or Float32Array[] channel data into a 32-bit float video texture for shaders - shaderpark: Shader Park SDF raymarching visuals for the video pipeline - three: Three.js 3D graphics (offscreen worker, video chaining, pointer drag/wheel interaction) +- deckgl: deck.gl data visualization layers rendered into the video pipeline - regl: GPU renderer with regl draw commands (custom vertices, buffers, multi-pass, blend modes) - three.dom: Three.js 3D graphics (main thread, for keyboard/DOM interaction) - projmap: Projection mapper — warp video textures onto N-point polygon surfaces with built-in point editor and expand mode @@ -150,6 +151,7 @@ export const SPARKS_OBJECT_LIST = `## Visuals - shaderpark: Shader Park — concise SDF/raymarched procedural 3D visuals - three: Three.js 3D graphics (offscreen worker) — meshes, lighting, cameras, 3D scenes, pointer drag/wheel interaction; for video chaining - three.dom: Three.js 3D graphics (main thread) — same as three but supports keyboard and DOM interaction +- deckgl: deck.gl data visualization — maps, points, lines, arcs, polygons, and geospatial layers for video chaining - regl: Low-level GPU rendering — custom vertices, multi-pass, blend modes - canvas.dom: HTML5 Canvas (main thread) — supports mouse/keyboard, lower overhead than p5 - projmap: Projection mapping — warp video onto surfaces with a built-in point editor diff --git a/ui/src/lib/ai/object-prompts/deckgl.ts b/ui/src/lib/ai/object-prompts/deckgl.ts new file mode 100644 index 000000000..352989f61 --- /dev/null +++ b/ui/src/lib/ai/object-prompts/deckgl.ts @@ -0,0 +1,28 @@ +export const deckglPrompt = `## deckgl Object Instructions + +deckgl renders deck.gl data visualization layers into the Patchies video pipeline. Use it for point maps, lines, arcs, polygons, geospatial visualization, and data-driven visual layers. + +Define a getLayers({ time, viewState, mouse }) function that returns deck.gl layers. Common layer classes are available as globals: ScatterplotLayer, GeoJsonLayer, LineLayer, ArcLayer, PolygonLayer, TextLayer, BitmapLayer. + +Example: + +\`\`\`js +const data = [ + { position: [-122.45, 37.78], color: [255, 90, 70] }, + { position: [-122.42, 37.76], color: [80, 180, 255] } +] + +function getLayers({ time }) { + return [ + new ScatterplotLayer({ + id: 'points', + data, + getPosition: d => d.position, + getRadius: 500 + Math.sin(time * 2) * 100, + getFillColor: d => d.color, + radiusUnits: 'meters' + }) + ] +} +\`\`\` +`; diff --git a/ui/src/lib/ai/object-prompts/index.ts b/ui/src/lib/ai/object-prompts/index.ts index d4348cfae..bf357ea6b 100644 --- a/ui/src/lib/ai/object-prompts/index.ts +++ b/ui/src/lib/ai/object-prompts/index.ts @@ -52,6 +52,7 @@ import { elemPrompt } from './elem~'; import { labelPrompt } from './label'; import { titlePrompt } from './title'; import { vuePrompt } from './vue'; +import { deckglPrompt } from './deckgl'; import { reglPrompt } from './regl'; import { threePrompt } from './three'; import { threeDomPrompt } from './three.dom'; @@ -121,6 +122,7 @@ export const objectPrompts: Record = { 'elem~': elemPrompt, label: labelPrompt, title: titlePrompt, + deckgl: deckglPrompt, regl: reglPrompt, three: threePrompt, 'three.dom': threeDomPrompt, diff --git a/ui/src/lib/canvas/CanvasMouseHandler.ts b/ui/src/lib/canvas/CanvasMouseHandler.ts index eb4d442e2..f07197396 100644 --- a/ui/src/lib/canvas/CanvasMouseHandler.ts +++ b/ui/src/lib/canvas/CanvasMouseHandler.ts @@ -19,7 +19,7 @@ export interface ShadertoyMouseConfig { outputWidth: number; outputHeight: number; wheelZoom?: boolean; - wheelTarget?: 'shaderparkOrbit' | 'threeInteraction'; + wheelTarget?: 'shaderparkOrbit' | 'threeInteraction' | 'deckglInteraction'; flipY?: boolean; } @@ -399,6 +399,19 @@ export class CanvasMouseHandler { deltaMode: event.deltaMode }); }) + .with('deckglInteraction', () => { + const rect = config.canvas.getBoundingClientRect(); + const screenX = event.clientX - rect.left; + const screenY = event.clientY - rect.top; + + this.glSystem.sendThreeWheelData(config.nodeId, { + x: (screenX / rect.width) * config.outputWidth, + y: this.mapY(screenY, rect.height, config), + deltaX: event.deltaX, + deltaY: event.deltaY, + deltaMode: event.deltaMode + }); + }) .with('shaderparkOrbit', () => this.glSystem.zoomShaderParkOrbit(config.nodeId, event.deltaY) ) @@ -499,6 +512,17 @@ export class CanvasMouseHandler { deltaMode: 0 }); }) + .with('deckglInteraction', () => { + const { x, y } = getFramebufferPosition(center.clientX, center.clientY); + + this.glSystem.sendThreeWheelData(config.nodeId, { + x, + y, + deltaX: 0, + deltaY, + deltaMode: 0 + }); + }) .with('shaderparkOrbit', () => this.glSystem.zoomShaderParkOrbit(config.nodeId, deltaY)) .exhaustive(); diff --git a/ui/src/lib/canvas/SurfaceMouseForwarder.ts b/ui/src/lib/canvas/SurfaceMouseForwarder.ts index b6ea0cac1..30b380c71 100644 --- a/ui/src/lib/canvas/SurfaceMouseForwarder.ts +++ b/ui/src/lib/canvas/SurfaceMouseForwarder.ts @@ -70,7 +70,7 @@ export class SurfaceMouseForwarder { const yFBShadertoy = (1 - y) * h; // Y-flip for GL origin (bottom-left) for (const node of renderNodes) { - if (node.type === 'three') { + if (node.type === 'three' || node.type === 'deckgl') { this.forwardShadertoy(node.nodeId, xFB, yFBSimple, type, _buttons); } else if (node.kind === 'shadertoy') { this.forwardShadertoy(node.nodeId, xFB, yFBShadertoy, type, _buttons); @@ -93,7 +93,7 @@ export class SurfaceMouseForwarder { const yFB = event.y * h; for (const target of this.wheelTargets) { - if (target.kind === 'three') { + if (target.kind === 'three' || target.kind === 'deckgl') { this.glSystem.sendThreeWheelData(target.nodeId, { x: xFB, y: yFB, diff --git a/ui/src/lib/canvas/constants.ts b/ui/src/lib/canvas/constants.ts index 2b52624c0..c89c3e42a 100644 --- a/ui/src/lib/canvas/constants.ts +++ b/ui/src/lib/canvas/constants.ts @@ -297,6 +297,26 @@ function render(time) { draw({ time }) }`; +export const DEFAULT_DECKGL_CODE = `const data = [ + { position: [-122.45, 37.78], color: [255, 90, 70] }, + { position: [-122.42, 37.76], color: [80, 180, 255] }, + { position: [-122.48, 37.74], color: [255, 210, 80] } +] + +function getLayers({ time }) { + return [ + new ScatterplotLayer({ + id: 'points', + data, + getPosition: d => d.position, + getRadius: 450 + Math.sin(time * 2) * 120, + getFillColor: d => d.color, + radiusUnits: 'meters', + pickable: true + }) + ] +}`; + export const DEFAULT_SHADERPARK_CODE = `let sizeBase = input(0.5, 0.1, 1.2); let pulse = 0.08 * nsin(time * 2.0); diff --git a/ui/src/lib/canvas/surfaceMouseForwarding.ts b/ui/src/lib/canvas/surfaceMouseForwarding.ts index f0cfa82b1..2b668959a 100644 --- a/ui/src/lib/canvas/surfaceMouseForwarding.ts +++ b/ui/src/lib/canvas/surfaceMouseForwarding.ts @@ -12,11 +12,13 @@ export type SurfaceMouseForwardingRules = { export type SurfaceMouseTarget = | { kind: 'three'; nodeId: string; type: string } + | { kind: 'deckgl'; nodeId: string; type: string } | { kind: 'shadertoy'; nodeId: string; type: string } | { kind: 'simple'; nodeId: string; type: string }; export type SurfaceWheelTarget = | { kind: 'three'; nodeId: string } + | { kind: 'deckgl'; nodeId: string } | { kind: 'shaderpark3d'; nodeId: string }; function matchesForwardingRules(nodeId: string, rules?: SurfaceMouseForwardingRules): boolean { @@ -33,6 +35,7 @@ function matchesForwardingRules(nodeId: string, rules?: SurfaceMouseForwardingRu export const isSurfaceMouseForwardingNode = (node: Node): boolean => match(node.type) .with('three', () => true) + .with('deckgl', () => true) .when( (type) => SHADERTOY_TYPES.has(type ?? ''), () => true @@ -60,6 +63,9 @@ export const getSurfaceMouseTargets = ( .with({ type: 'three' }, (): SurfaceMouseTarget[] => [ { kind: 'three', nodeId: node.id, type: 'three' } ]) + .with({ type: 'deckgl' }, (): SurfaceMouseTarget[] => [ + { kind: 'deckgl', nodeId: node.id, type: 'deckgl' } + ]) .with({ type: 'shaderpark', renderMode: '3d' }, (): SurfaceMouseTarget[] => [ { kind: 'shadertoy', nodeId: node.id, type: 'shaderpark' } ]) @@ -85,6 +91,7 @@ export const getSurfaceWheelTargets = ( return match({ type: node.type, renderMode: node.data?.renderMode }) .with({ type: 'three' }, (): SurfaceWheelTarget[] => [{ kind: 'three', nodeId: node.id }]) + .with({ type: 'deckgl' }, (): SurfaceWheelTarget[] => [{ kind: 'deckgl', nodeId: node.id }]) .with({ type: 'shaderpark', renderMode: '3d' }, (): SurfaceWheelTarget[] => [ { kind: 'shaderpark3d', nodeId: node.id } ]) diff --git a/ui/src/lib/codemirror/patchies-completions.ts b/ui/src/lib/codemirror/patchies-completions.ts index e4f6f6d4b..5367fa166 100644 --- a/ui/src/lib/codemirror/patchies-completions.ts +++ b/ui/src/lib/codemirror/patchies-completions.ts @@ -495,6 +495,7 @@ const MOUSE_INTERACTION_JS_NODES = [ 'textmode', 'textmode.dom', 'three', + 'deckgl', 'three.dom', 'vue', 'dom', @@ -521,6 +522,7 @@ const nodeSpecificFunctions: Record = { 'textmode', 'textmode.dom', 'three', + 'deckgl', 'three.dom', 'dom', 'vue', @@ -540,6 +542,7 @@ const nodeSpecificFunctions: Record = { 'textmode', 'textmode.dom', 'three', + 'deckgl', 'three.dom', 'tone~' ], @@ -556,6 +559,7 @@ const nodeSpecificFunctions: Record = { 'textmode', 'textmode.dom', 'three', + 'deckgl', 'three.dom', 'surface' ], @@ -581,6 +585,7 @@ const nodeSpecificFunctions: Record = { 'textmode', 'textmode.dom', 'three', + 'deckgl', 'three.dom' ], setKeepAlive: ['dsp~'], @@ -608,13 +613,14 @@ const nodeSpecificFunctions: Record = { 'textmode', 'textmode.dom', 'three', + 'deckgl', 'three.dom' ], 'htmlCanvas.videoOutput': ['dom', 'vue'], 'htmlCanvas.canvasLayer': ['dom', 'vue'], 'htmlCanvas.glslLayer': ['dom', 'vue'], - setTextureFormat: ['hydra', 'canvas', 'three', 'regl', 'swgl', 'textmode'], - setResolution: ['hydra', 'canvas', 'three', 'regl', 'swgl', 'textmode'], + setTextureFormat: ['hydra', 'canvas', 'three', 'deckgl', 'regl', 'swgl', 'textmode'], + setResolution: ['hydra', 'canvas', 'three', 'deckgl', 'regl', 'swgl', 'textmode'], setPrimaryButton: ['js', 'worker', 'p5', 'hydra', 'canvas', 'regl', 'swgl', 'textmode', 'three'], setVideoCount: ['hydra', 'regl', 'swgl', 'three', 'worker'], getTexture: ['hydra', 'regl', 'swgl', 'three'], diff --git a/ui/src/lib/components/nodes/DeckGLNode.svelte b/ui/src/lib/components/nodes/DeckGLNode.svelte new file mode 100644 index 000000000..e767196be --- /dev/null +++ b/ui/src/lib/components/nodes/DeckGLNode.svelte @@ -0,0 +1,399 @@ + + + { + settingsManager.setValue(key, value); + glSystem.sendSettingsValueChanged(nodeId, key, value); + }} + onSettingsRevertAll={() => { + settingsManager.revertAll(); + + for (const [key, value] of Object.entries(settingsManager.getAll())) { + glSystem.sendSettingsValueChanged(nodeId, key, value); + } + }} +> + {#snippet topHandle()} + {#each Array.from({ length: videoInletCount }, (_, index) => index) as index (index)} + + {/each} + + {#each Array.from({ length: messageInletCount }, (_, index) => index) as index (index)} + + {/each} + {/snippet} + + {#snippet bottomHandle()} + {#if videoOutputEnabled} + {#each Array.from({ length: videoOutletCount }, (_, index) => index) as index (index)} + + {/each} + {/if} + + {#each Array.from({ length: messageOutletCount }, (_, index) => index) as index (index)} + + {/each} + {/snippet} + + {#snippet codeEditor()} + { + updateNodeData(nodeId, { code: newCode }); + }} + onready={() => (editorReady = true)} + {lineErrors} + {nodeId} + /> + {/snippet} + + {#snippet console()} +
+ +
+ {/snippet} +
diff --git a/ui/src/lib/components/object-browser/get-categorized-objects.ts b/ui/src/lib/components/object-browser/get-categorized-objects.ts index c242fcfd4..6aadafe55 100644 --- a/ui/src/lib/components/object-browser/get-categorized-objects.ts +++ b/ui/src/lib/components/object-browser/get-categorized-objects.ts @@ -22,7 +22,8 @@ export interface CategoryGroup { * Fallback descriptions for objects that don't have schemas */ const FALLBACK_DESCRIPTIONS: Record = { - bchrn: 'Butterchurn milkdrop visualizer with audio reactivity' + bchrn: 'Butterchurn milkdrop visualizer with audio reactivity', + deckgl: 'deck.gl data visualization renderer for the video pipeline' }; /** diff --git a/ui/src/lib/extensions/object-packs.ts b/ui/src/lib/extensions/object-packs.ts index d528985ad..2db62680a 100644 --- a/ui/src/lib/extensions/object-packs.ts +++ b/ui/src/lib/extensions/object-packs.ts @@ -111,6 +111,7 @@ export const BUILT_IN_PACKS: ExtensionPack[] = [ 'projmap', 'shaderpark', 'float.tex', + 'deckgl', 'regl', 'swgl' ] diff --git a/ui/src/lib/nodes/defaultNodeData.ts b/ui/src/lib/nodes/defaultNodeData.ts index 2f0a8588f..54dc98ac6 100644 --- a/ui/src/lib/nodes/defaultNodeData.ts +++ b/ui/src/lib/nodes/defaultNodeData.ts @@ -18,6 +18,7 @@ import { DEFAULT_TEXTMODE_CODE, DEFAULT_THREE_CODE, DEFAULT_REGL_CODE, + DEFAULT_DECKGL_CODE, DEFAULT_SHADERPARK_CODE, DEFAULT_SURFACE_CODE, DEFAULT_DOM_CODE, @@ -110,6 +111,13 @@ export function getDefaultNodeData(nodeType: string): NodeData { videoInletCount: 1, videoOutletCount: 1 })) + .with('deckgl', () => ({ + code: DEFAULT_DECKGL_CODE, + messageInletCount: 1, + messageOutletCount: 0, + videoInletCount: 0, + videoOutletCount: 1 + })) .with('shaderpark', () => ({ code: DEFAULT_SHADERPARK_CODE, videoInletCount: 4, diff --git a/ui/src/lib/nodes/node-types.ts b/ui/src/lib/nodes/node-types.ts index 025727565..1ce6f2cdd 100644 --- a/ui/src/lib/nodes/node-types.ts +++ b/ui/src/lib/nodes/node-types.ts @@ -10,6 +10,7 @@ import ThreeDom from '$lib/components/nodes/ThreeDom.svelte'; import GLSLCanvasNode from '$lib/components/nodes/GLSLCanvasNode.svelte'; import SwissGLNode from '$lib/components/nodes/SwissGLNode.svelte'; import ReglNode from '$lib/components/nodes/ReglNode.svelte'; +import DeckGLNode from '$lib/components/nodes/DeckGLNode.svelte'; import ShaderParkNode from '$lib/components/nodes/ShaderParkNode.svelte'; import FloatTextureNode from '$lib/components/nodes/FloatTextureNode.svelte'; import StrudelNode from '$lib/components/nodes/StrudelNode.svelte'; @@ -129,6 +130,7 @@ export const nodeTypes: Record = { dom: DomNode, vue: VueNode, regl: ReglNode, + deckgl: DeckGLNode, shaderpark: ShaderParkNode, 'float.tex': FloatTextureNode, glsl: GLSLCanvasNode, diff --git a/ui/src/lib/objects/schemas/deckgl.ts b/ui/src/lib/objects/schemas/deckgl.ts new file mode 100644 index 000000000..4f9172103 --- /dev/null +++ b/ui/src/lib/objects/schemas/deckgl.ts @@ -0,0 +1,36 @@ +import type { ObjectSchema } from './types'; +import { Run, SetCode } from './common'; + +/** + * Schema for the deckgl data visualization object. + */ +export const deckglSchema: ObjectSchema = { + type: 'deckgl', + category: 'video', + description: 'Renders deck.gl data visualization layers into the video pipeline', + inlets: [ + { + id: 'message', + description: 'Control messages', + messages: [ + { schema: SetCode, description: 'Set the code in the editor' }, + { schema: Run, description: 'Evaluate code and update visuals' } + ] + } + ], + outlets: [], + tags: ['deckgl', 'data', 'visualization', 'map', 'geospatial', 'webgl'], + hasDynamicOutlets: true, + handlePatterns: { + inlet: { + template: 'video-in-{index}', + handleType: 'video', + description: 'Video inlets (0-indexed), message inlets use message-in-{index}' + }, + outlet: { + template: 'video-out-{index}', + handleType: 'video', + description: 'Video outlets (0-indexed), message outlets use message-out-{index}' + } + } +}; diff --git a/ui/src/lib/objects/schemas/index.ts b/ui/src/lib/objects/schemas/index.ts index 37f49f08d..6be83a000 100644 --- a/ui/src/lib/objects/schemas/index.ts +++ b/ui/src/lib/objects/schemas/index.ts @@ -7,6 +7,7 @@ export * from './p5'; export * from './hydra'; export * from './glsl'; export * from './canvas'; +export * from './deckgl'; export * from './surface'; export * from './shaderpark'; export * from './float-tex'; @@ -104,6 +105,7 @@ import { p5Schema } from './p5'; import { hydraSchema } from './hydra'; import { glslSchema } from './glsl'; import { canvasSchema, canvasDomSchema } from './canvas'; +import { deckglSchema } from './deckgl'; import { surfaceSchema } from './surface'; import { shaderparkSchema } from './shaderpark'; import { floatTexSchema } from './float-tex'; @@ -219,6 +221,7 @@ export const objectSchemas: ObjectSchemaRegistry = { glsl: glslSchema, canvas: canvasSchema, 'canvas.dom': canvasDomSchema, + deckgl: deckglSchema, surface: surfaceSchema, shaderpark: shaderparkSchema, 'float.tex': floatTexSchema, diff --git a/ui/src/lib/rendering/types.ts b/ui/src/lib/rendering/types.ts index 3a6405491..044bca8e8 100644 --- a/ui/src/lib/rendering/types.ts +++ b/ui/src/lib/rendering/types.ts @@ -82,6 +82,15 @@ export type RenderNode = { resolution?: FBOResolution; }; } + | { + type: 'deckgl'; + data: { + code: string; + fboFormat?: FBOFormat; + resolution?: FBOResolution; + _runRevision?: number; + }; + } | { type: 'projmap'; data: { surfaces: import('$objects/projmap/types').ProjMapSurface[] } } | { type: 'img'; data: unknown } | { type: 'float.tex'; data: unknown } @@ -371,6 +380,7 @@ export const FBO_COMPATIBLE_TYPES: RenderNode['type'][] = [ 'three', 'shaderpark', 'regl', + 'deckgl', 'projmap', 'img', 'float.tex', diff --git a/ui/src/workers/rendering/deckglRenderer.ts b/ui/src/workers/rendering/deckglRenderer.ts new file mode 100644 index 000000000..c0d253e59 --- /dev/null +++ b/ui/src/workers/rendering/deckglRenderer.ts @@ -0,0 +1,340 @@ +import type regl from 'regl'; +import type { Deck, Layer } from '@deck.gl/core'; +import type { Device, Framebuffer, Texture } from '@luma.gl/core'; +import type { FBORenderer } from './fboRenderer'; +import type { RenderParams } from '$lib/rendering/types'; +import { CANVAS_WRAPPER_OFFSET } from '$lib/constants/error-reporting-offsets'; +import { BaseWorkerRenderer, type BaseRendererConfig } from './BaseWorkerRenderer'; +import { getFramebuffer } from './utils'; +import { setupWorkerDOMMocks } from './workerDOMMocks'; + +type DeckGLRendererConfig = BaseRendererConfig & { + runRevision?: number; +}; + +type DeckViewState = { + longitude: number; + latitude: number; + zoom: number; + pitch: number; + bearing: number; +}; + +type WheelEventData = { + x?: number; + y?: number; + deltaX?: number; + deltaY: number; + deltaMode?: number; +}; + +type WorkerMouseObject = { + readonly x: number; + readonly y: number; +}; + +type UserGetLayers = (args: { + time: number; + viewState: DeckViewState; + mouse: WorkerMouseObject; +}) => Layer[]; + +type SizedFramebuffer = regl.Framebuffer2D & { + width: number; + height: number; +}; + +export class DeckGLRenderer extends BaseWorkerRenderer { + private DeckClass: typeof import('@deck.gl/core').Deck | null = null; + private layerClasses: typeof import('@deck.gl/layers') | null = null; + + private deck: Deck | null = null; + private device: Device | null = null; + private deckTexture: Texture | null = null; + private deckFramebuffer: Framebuffer | null = null; + private getLayers: UserGetLayers | null = null; + private previousPointer: { x: number; y: number; down: boolean } | null = null; + private pendingWheelDelta = 0; + + private viewState: DeckViewState = { + longitude: -122.44, + latitude: 37.76, + zoom: 11, + pitch: 45, + bearing: 0 + }; + + private constructor( + config: DeckGLRendererConfig, + framebuffer: regl.Framebuffer2D, + renderer: FBORenderer + ) { + super(config, framebuffer, renderer); + } + + static async create( + config: DeckGLRendererConfig, + framebuffer: regl.Framebuffer2D, + renderer: FBORenderer + ): Promise { + const instance = new DeckGLRenderer(config, framebuffer, renderer); + setupWorkerDOMMocks(); + + const deckModule = await import('@deck.gl/core'); + instance.layerClasses = await import('@deck.gl/layers'); + + instance.DeckClass = deckModule.Deck; + + await instance.createDeck(); + await instance.updateCode(); + + return instance; + } + + renderFrame(params: RenderParams): void { + if (!this.deck || !this.deckFramebuffer || !this.getLayers) return; + + const gl = this.renderer.gl; + if (!gl) return; + + this.mouseX = params.mouseX; + this.mouseY = params.mouseY; + + this.updateViewStateFromPointer(params); + this.updateViewStateFromWheel(); + this.ensureRenderTargetSize(); + + let layers: Layer[] = []; + + try { + layers = this.getLayers({ + time: params.transportTime, + viewState: this.viewState, + mouse: this.createMouseObject() + }); + } catch (error) { + this.handleRuntimeError(error, CANVAS_WRAPPER_OFFSET); + return; + } + + this.deck.setProps({ + viewState: this.viewState, + layers + }); + + this.deck.redraw('patchies'); + + this.blitToReglFramebuffer(); + this.renderer.regl._refresh(); + } + + async updateCode(): Promise { + this.resetState(); + this.setPortCount(1, 0); + + if (!this.DeckClass || !this.layerClasses) return; + + try { + const setViewState = (viewState: Partial) => { + this.viewState = { ...this.viewState, ...viewState }; + }; + + const extraContext = { + ...this.buildBaseExtraContext(), + Deck: this.DeckClass, + ...this.layerClasses, + viewState: this.viewState, + setViewState + }; + + const codeWithWrapper = ` + var getLayers; + + ${this.config.code} + + return { + getLayers: typeof getLayers === 'function' ? getLayers : null + }; + `; + + const result = await this.executeUserCode(codeWithWrapper, extraContext); + const maybeGetLayers = (result as { getLayers?: unknown } | null)?.getLayers; + + this.getLayers = + typeof maybeGetLayers === 'function' ? (maybeGetLayers as UserGetLayers) : null; + + if (!this.getLayers) { + throw new Error('Define getLayers({ time, viewState, mouse }) to render deck.gl layers.'); + } + } catch (error) { + this.handleRuntimeError(error, CANVAS_WRAPPER_OFFSET); + } + } + + async updateConfig(config: DeckGLRendererConfig, framebuffer: regl.Framebuffer2D) { + const previousFramebuffer = this.framebuffer as SizedFramebuffer | null; + const nextFramebuffer = framebuffer as SizedFramebuffer; + + this.config = config; + this.framebuffer = framebuffer; + + const sizeChanged = + previousFramebuffer?.width !== nextFramebuffer.width || + previousFramebuffer?.height !== nextFramebuffer.height; + + if (sizeChanged) { + this.ensureRenderTargetSize(); + } + + await this.updateCode(); + } + + handleWheelData(event: WheelEventData): void { + this.pendingWheelDelta += event.deltaY; + } + + destroy(): void { + this.deck?.finalize(); + this.deck = null; + + this.deckFramebuffer?.destroy(); + this.deckFramebuffer = null; + + this.deckTexture?.destroy(); + this.deckTexture = null; + } + + private async createDeck(): Promise { + const DeckClass = this.DeckClass; + if (!DeckClass) return; + + const gl = this.renderer.gl; + if (!gl) return; + + const canvas = gl.canvas as OffscreenCanvas & { + style?: Record; + isConnected?: boolean; + parentElement?: unknown; + }; + + canvas.style ??= {}; + canvas.isConnected ??= false; + canvas.parentElement ??= null; + + await new Promise((resolve) => { + this.deck = new DeckClass({ + controller: false, + gl, + width: null, + height: null, + useDevicePixels: false, + initialViewState: this.viewState, + viewState: this.viewState, + layers: [], + getTooltip: null, + parameters: { depthCompare: 'less-equal' }, + onDeviceInitialized: (device: Device) => { + this.device = device; + this.createRenderTarget(); + + this.deck?.setProps({ _framebuffer: this.deckFramebuffer }); + + resolve(); + }, + onError: (error) => { + this.handleRuntimeError(error, CANVAS_WRAPPER_OFFSET); + } + }); + }); + } + + private createRenderTarget(): void { + if (!this.device) return; + + const [width, height] = this.renderer.outputSize; + + this.deckTexture?.destroy(); + this.deckFramebuffer?.destroy(); + + this.deckTexture = this.device.createTexture({ + format: 'rgba8unorm', + width, + height, + sampler: { + minFilter: 'linear', + magFilter: 'linear', + addressModeU: 'clamp-to-edge', + addressModeV: 'clamp-to-edge' + } + }); + + this.deckFramebuffer = this.device.createFramebuffer({ + width, + height, + colorAttachments: [this.deckTexture], + depthStencilAttachment: 'depth16unorm' + }); + } + + private ensureRenderTargetSize(): void { + if (!this.device || !this.deckFramebuffer) return; + + const [width, height] = this.renderer.outputSize; + this.device.canvasContext?.setDrawingBufferSize(width, height); + + if (this.deckFramebuffer.width !== width || this.deckFramebuffer.height !== height) { + this.deckFramebuffer.resize({ width, height }); + } + } + + private updateViewStateFromPointer(params: RenderParams): void { + const down = params.mouseZ >= 0 && params.mouseW >= 0; + const pointer = { x: params.mouseX, y: params.mouseY, down }; + + if (down && this.previousPointer?.down) { + const dx = pointer.x - this.previousPointer.x; + const dy = pointer.y - this.previousPointer.y; + const zoomScale = Math.max(1, this.viewState.zoom); + + this.viewState = { + ...this.viewState, + longitude: this.viewState.longitude - dx / (80 * zoomScale), + latitude: Math.max(-85, Math.min(85, this.viewState.latitude + dy / (80 * zoomScale))) + }; + } + + this.previousPointer = pointer; + } + + private updateViewStateFromWheel(): void { + if (this.pendingWheelDelta === 0) return; + + const delta = this.pendingWheelDelta; + this.pendingWheelDelta = 0; + + this.viewState = { + ...this.viewState, + zoom: Math.max(0, Math.min(22, this.viewState.zoom - delta * 0.01)) + }; + } + + private blitToReglFramebuffer(): void { + if (!this.deckFramebuffer || !this.framebuffer) return; + + const gl = this.renderer.gl; + if (!gl) return; + + const [width, height] = this.renderer.outputSize; + const sourceFBO = (this.deckFramebuffer as Framebuffer & { handle?: WebGLFramebuffer }).handle; + const destFBO = getFramebuffer(this.framebuffer); + if (!sourceFBO) return; + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, sourceFBO); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, destFBO); + + gl.blitFramebuffer(0, 0, width, height, 0, 0, width, height, gl.COLOR_BUFFER_BIT, gl.LINEAR); + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); + } +} diff --git a/ui/src/workers/rendering/fboRenderer.ts b/ui/src/workers/rendering/fboRenderer.ts index 74eaa0b97..d290ccc3d 100644 --- a/ui/src/workers/rendering/fboRenderer.ts +++ b/ui/src/workers/rendering/fboRenderer.ts @@ -27,6 +27,7 @@ import { CanvasRenderer } from './canvasRenderer'; import { TextmodeRenderer } from './textmodeRenderer'; import { ThreeRenderer } from './threeRenderer'; import { ReglRenderer } from './reglRenderer'; +import { DeckGLRenderer } from './deckglRenderer'; import { SwissGLRenderer } from './swglRenderer'; import { createShaderParkDrawCommand, SHADERPARK_VIDEO_UNIFORM_COUNT } from './shaderParkRenderer'; import { ShaderParkThreeRenderer } from './shaderParkThreeRenderer'; @@ -115,6 +116,7 @@ export class FBORenderer { public threeByNode = new Map(); public shaderParkThreeByNode = new Map(); public reglByNode = new Map(); + public deckglByNode = new Map(); public projmapByNode = new Map(); public swglByNode = new Map(); @@ -496,8 +498,9 @@ export class FBORenderer { node.data.renderMode === '3d' && this.shaderParkThreeByNode.has(node.id); const isReusableThree = node.type === 'three' && this.threeByNode.has(node.id); + const isReusableDeckGL = node.type === 'deckgl' && this.deckglByNode.has(node.id); - if (!isHydra && !isShaderPark3D && !isReusableThree) { + if (!isHydra && !isShaderPark3D && !isReusableThree && !isReusableDeckGL) { existingFbo.cleanup?.(); } } else { @@ -512,8 +515,9 @@ export class FBORenderer { this.shaderParkThreeByNode.has(node.id); const isReusableThree = node.type === 'three' && this.threeByNode.has(node.id); + const isReusableDeckGL = node.type === 'deckgl' && this.deckglByNode.has(node.id); - if (!isHydra && !isShaderPark3D && !isReusableThree) { + if (!isHydra && !isShaderPark3D && !isReusableThree && !isReusableDeckGL) { existingFbo.cleanup?.(); } @@ -593,6 +597,7 @@ export class FBORenderer { .with({ type: 'three' }, (node) => this.createThreeRenderer(node, framebuffer)) .with({ type: 'shaderpark' }, (node) => this.createShaderParkRenderer(node, framebuffer)) .with({ type: 'regl' }, (node) => this.createReglRenderer(node, framebuffer)) + .with({ type: 'deckgl' }, (node) => this.createDeckGLRenderer(node, framebuffer)) .with({ type: 'projmap' }, (node) => this.createProjMapRenderer(node, framebuffer)) .with({ type: 'img' }, () => this.createEmptyRenderer()) .with({ type: 'float.tex' }, () => this.createEmptyRenderer()) @@ -1009,6 +1014,41 @@ export class FBORenderer { }; } + async createDeckGLRenderer( + node: RenderNode, + framebuffer: regl.Framebuffer2D + ): Promise<{ render: RenderFunction; cleanup: () => void } | null> { + if (node.type !== 'deckgl') return null; + + const existingRenderer = this.deckglByNode.get(node.id); + const runRevision = node.data._runRevision; + const config = { code: node.data.code, nodeId: node.id, runRevision }; + + if (existingRenderer) { + await existingRenderer.updateConfig(config, framebuffer); + + return { + render: existingRenderer.renderFrame.bind(existingRenderer), + cleanup: () => { + existingRenderer.destroy(); + this.deckglByNode.delete(node.id); + } + }; + } + + const deckglRenderer = await DeckGLRenderer.create(config, framebuffer, this); + + this.deckglByNode.set(node.id, deckglRenderer); + + return { + render: deckglRenderer.renderFrame.bind(deckglRenderer), + cleanup: () => { + deckglRenderer.destroy(); + this.deckglByNode.delete(node.id); + } + }; + } + async createProjMapRenderer( node: RenderNode, framebuffer: regl.Framebuffer2D @@ -1411,6 +1451,7 @@ export class FBORenderer { this.hydraByNode.get(nodeId), this.textmodeByNode.get(nodeId), this.threeByNode.get(nodeId), + this.deckglByNode.get(nodeId), this.swglByNode.get(nodeId) ]; @@ -1440,6 +1481,7 @@ export class FBORenderer { event: { x?: number; y?: number; deltaX?: number; deltaY: number; deltaMode?: number } ) { this.threeByNode.get(nodeId)?.handleWheelData(event); + this.deckglByNode.get(nodeId)?.handleWheelData(event); } resetThreeOrbitControls(nodeId: string) { @@ -1621,6 +1663,7 @@ export class FBORenderer { node.type === 'hydra' || node.type === 'three' || node.type === 'regl' || + node.type === 'deckgl' || node.type === 'swgl' || node.type === 'projmap' ) { @@ -2113,6 +2156,12 @@ export class FBORenderer { reglRenderer.handleMessage(message); }) + .with('deckgl', () => { + const deckglRenderer = this.deckglByNode.get(nodeId); + if (!deckglRenderer) return; + + deckglRenderer.handleMessage(message); + }) .with( P.union( 'glsl', @@ -2150,6 +2199,9 @@ export class FBORenderer { .with('regl', () => { this.reglByNode.get(nodeId)?.handleChannelMessage(channel, data, sourceNodeId); }) + .with('deckgl', () => { + this.deckglByNode.get(nodeId)?.handleChannelMessage(channel, data, sourceNodeId); + }) .with('swgl', () => { this.swglByNode.get(nodeId)?.handleChannelMessage(channel, data, sourceNodeId); }) diff --git a/ui/src/workers/rendering/renderWorker.ts b/ui/src/workers/rendering/renderWorker.ts index 345c83d3c..2acb56c75 100644 --- a/ui/src/workers/rendering/renderWorker.ts +++ b/ui/src/workers/rendering/renderWorker.ts @@ -332,6 +332,12 @@ function handleSetFFTData(payload: AudioAnalysisPayloadWithType) { reglRenderer.setFFTData(payload); }) + .with('deckgl', () => { + const deckglRenderer = fboRenderer.deckglByNode.get(nodeId); + if (!deckglRenderer) return; + + deckglRenderer.setFFTData(payload); + }) .with('swgl', () => { const swglRenderer = fboRenderer.swglByNode.get(nodeId); if (!swglRenderer) return; diff --git a/ui/src/workers/rendering/workerDOMMocks.ts b/ui/src/workers/rendering/workerDOMMocks.ts index a8870381f..2dc5b1e6f 100644 --- a/ui/src/workers/rendering/workerDOMMocks.ts +++ b/ui/src/workers/rendering/workerDOMMocks.ts @@ -50,15 +50,35 @@ function createMockElement(tagName: string): unknown { } // Generic mock element - return { + const children: unknown[] = []; + const element = { tagName: tagName.toUpperCase(), style: {}, className: '', - appendChild: () => {}, - removeChild: () => {}, + classList: { + add: () => {}, + remove: () => {}, + contains: () => false + }, + append: (child: unknown) => { + children.push(child); + }, + appendChild: (child: unknown) => { + children.push(child); + return child; + }, + remove: () => {}, + removeChild: (child: unknown) => { + const index = children.indexOf(child); + if (index >= 0) children.splice(index, 1); + return child; + }, + querySelector: () => null, addEventListener: () => {}, removeEventListener: () => {} }; + + return element; } // Mock document object diff --git a/ui/static/content/objects/deckgl.md b/ui/static/content/objects/deckgl.md new file mode 100644 index 000000000..0bf752e16 --- /dev/null +++ b/ui/static/content/objects/deckgl.md @@ -0,0 +1,49 @@ +The `deckgl` object renders [deck.gl](https://deck.gl) layers inside the worker +video pipeline. + +This is an experimental prototype. It renders deck.gl into an offscreen luma.gl +framebuffer, then blits the result into Patchies' FBO pipeline so it can connect +to `bg.out` and other video objects. + +## Basic Use + +Define a `getLayers()` function that returns deck.gl layers. + +```js +const data = [ + { position: [-122.45, 37.78], color: [255, 90, 70] }, + { position: [-122.42, 37.76], color: [80, 180, 255] } +] + +function getLayers({ time, viewState, mouse }) { + return [ + new ScatterplotLayer({ + id: 'points', + data, + getPosition: d => d.position, + getRadius: 500 + Math.sin(time * 2) * 100, + getFillColor: d => d.color, + radiusUnits: 'meters' + }) + ] +} +``` + +## Available Globals + +- `Deck` - deck.gl Deck class +- `ScatterplotLayer`, `GeoJsonLayer`, `LineLayer`, `ArcLayer`, `PolygonLayer`, `TextLayer`, `BitmapLayer` - common deck.gl layers +- `viewState` - current camera state +- `setViewState(value)` - replace camera state +- `mouse` - forwarded mouse position + +## Interaction + +Drag the preview or a fullscreen `surface` to pan. Use the mouse wheel or pinch +gesture to zoom. + +## See Also + +- [glsl](/docs/objects/glsl) +- [regl](/docs/objects/regl) +- [three](/docs/objects/three) From bb6c1cb74e2b1e6ca6a3b6e6f76f02b62f91f6cf Mon Sep 17 00:00:00 2001 From: Phoomparin Mano Date: Sun, 24 May 2026 15:00:43 +0700 Subject: [PATCH 02/12] add mocks for worker integration --- ui/bun.lock | 38 +++++++++---- ui/package.json | 16 ++++-- ui/src/lib/audio/AudioAnalysisSystem.ts | 2 +- ui/src/lib/components/nodes/DeckGLNode.svelte | 8 ++- ui/src/workers/rendering/deckglRenderer.ts | 53 +++++++++++++++---- ui/src/workers/rendering/workerDOMMocks.ts | 37 ++++++++++++- 6 files changed, 125 insertions(+), 29 deletions(-) diff --git a/ui/bun.lock b/ui/bun.lock index 58c95a212..bb131f62a 100644 --- a/ui/bun.lock +++ b/ui/bun.lock @@ -15,16 +15,18 @@ "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.38.1", "@csound/browser": "file:./packages/csound-browser", - "@deck.gl/core": "^9.3.0-beta.2", - "@deck.gl/layers": "^9.3.0-beta.2", + "@deck.gl/core": "9.0.0", + "@deck.gl/layers": "9.0.0", "@elemaudio/core": "^4.0.1", "@elemaudio/web-renderer": "^4.0.3", "@google/genai": "^1.11.0", "@iizukak/codemirror-lang-wgsl": "^0.3.0", "@lezer/generator": "^1.8.0", "@lezer/highlight": "^1.2.1", - "@luma.gl/core": "^9.3.3", - "@luma.gl/webgl": "^9.3.3", + "@luma.gl/core": "9.0.0", + "@luma.gl/engine": "9.0.0", + "@luma.gl/shadertools": "9.0.0", + "@luma.gl/webgl": "9.0.0", "@mediapipe/tasks-vision": "0.10.33", "@replit/codemirror-vim": "^6.3.0", "@rollup/browser": "^4.50.1", @@ -163,6 +165,12 @@ "patchedDependencies": { "superdough@1.2.6": "patches/superdough@1.2.6.patch", }, + "overrides": { + "@luma.gl/core": "9.0.0", + "@luma.gl/engine": "9.0.0", + "@luma.gl/shadertools": "9.0.0", + "@luma.gl/webgl": "9.0.0", + }, "packages": { "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], @@ -406,9 +414,9 @@ "@davepagurek/bezier-path": ["@davepagurek/bezier-path@0.0.2", "", {}, "sha512-4L9ddgzZc9DRGyl1RrS3z5nwnVJoyjsAelVG4X1jh4tVxryEHr4H9QavhxW/my6Rn3669Qz6mhv8gd5O/WeFTA=="], - "@deck.gl/core": ["@deck.gl/core@9.3.2", "", { "dependencies": { "@loaders.gl/core": "^4.4.1", "@loaders.gl/images": "^4.4.1", "@luma.gl/core": "^9.3.3", "@luma.gl/engine": "^9.3.3", "@luma.gl/shadertools": "^9.3.3", "@luma.gl/webgl": "^9.3.3", "@math.gl/core": "^4.1.0", "@math.gl/sun": "^4.1.0", "@math.gl/types": "^4.1.0", "@math.gl/web-mercator": "^4.1.0", "@probe.gl/env": "^4.1.1", "@probe.gl/log": "^4.1.1", "@probe.gl/stats": "^4.1.1", "@types/offscreencanvas": "^2019.6.4", "gl-matrix": "^3.0.0", "mjolnir.js": "^3.0.0" } }, "sha512-32Va3np0Zdlz/LBNtDWCs4EkKqdHmXcbGmVp4+7i1Cpdza8y8CFmJs2VPOmSX1fwHvNCGkAZV/SFZOfDb2INsg=="], + "@deck.gl/core": ["@deck.gl/core@9.0.0", "", { "dependencies": { "@loaders.gl/core": "^4.1.4", "@loaders.gl/images": "^4.1.4", "@luma.gl/constants": "^9.0.6", "@luma.gl/core": "^9.0.6", "@luma.gl/engine": "^9.0.6", "@luma.gl/shadertools": "^9.0.6", "@luma.gl/webgl": "^9.0.6", "@math.gl/core": "^4.0.0", "@math.gl/sun": "^4.0.0", "@math.gl/web-mercator": "^4.0.0", "@probe.gl/env": "^4.0.9", "@probe.gl/log": "^4.0.9", "@probe.gl/stats": "^4.0.9", "@types/offscreencanvas": "^2019.6.4", "gl-matrix": "^3.0.0", "mjolnir.js": "^2.7.0" } }, "sha512-o3Ica4ZUU+dHAJ3v5BXorzg90xy9THLWiZQ/v4kZHat050EfFT8ZAPWwL0xS9M0DBvY4ikcZjTtcBtsh+KGm0A=="], - "@deck.gl/layers": ["@deck.gl/layers@9.3.2", "", { "dependencies": { "@loaders.gl/images": "^4.4.1", "@loaders.gl/schema": "^4.4.1", "@luma.gl/shadertools": "^9.3.3", "@mapbox/tiny-sdf": "^2.0.5", "@math.gl/core": "^4.1.0", "@math.gl/polygon": "^4.1.0", "@math.gl/web-mercator": "^4.1.0", "earcut": "^2.2.4" }, "peerDependencies": { "@deck.gl/core": "~9.3.0", "@loaders.gl/core": "^4.4.1", "@luma.gl/core": "~9.3.3", "@luma.gl/engine": "~9.3.3" } }, "sha512-TeVfhQ/cQU1oTlTn16mCp7268d1uBJ6dwfgmKXThe2TzW9hql3iJaxbYTKg2phDg5YSiGmeEOpXbeBh59jyUcA=="], + "@deck.gl/layers": ["@deck.gl/layers@9.0.0", "", { "dependencies": { "@loaders.gl/images": "^4.1.4", "@loaders.gl/schema": "^4.1.4", "@mapbox/tiny-sdf": "^2.0.5", "@math.gl/core": "^4.0.0", "@math.gl/polygon": "^4.0.0", "@math.gl/web-mercator": "^4.0.0", "earcut": "^2.2.4" }, "peerDependencies": { "@deck.gl/core": "^9.0.0", "@loaders.gl/core": "^4.1.0", "@luma.gl/core": "^9.0.0", "@luma.gl/engine": "^9.0.0" } }, "sha512-ScWnvgyWEuJCritPshptbtp1DIqn9KTDCFlJyxKZFj1ATn9Hco5gXgPxVJmoY2w3j2WOwaAo93Mgk8GlUry0Pg=="], "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], @@ -620,13 +628,15 @@ "@lucide/svelte": ["@lucide/svelte@0.575.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-FEFp/0McZwsjBqh1Dn8H+UBm1yHFQYk+utuVMFDw57155+wz2XMoc1pw027ylCPzs+bi14UEXYKbekFhuJKtnw=="], - "@luma.gl/core": ["@luma.gl/core@9.3.3", "", { "dependencies": { "@math.gl/types": "^4.1.0", "@probe.gl/env": "^4.1.1", "@probe.gl/log": "^4.1.1", "@probe.gl/stats": "^4.1.1", "@types/offscreencanvas": "^2019.7.3" } }, "sha512-jCFm2htvrVpcXIy85TBTF1ROgMfknKnfw2OH+Vydr41hiCFd6nqr79gM3f2uhaNkal0BghFNqF3qDioKiUWtew=="], + "@luma.gl/constants": ["@luma.gl/constants@9.1.10", "", {}, "sha512-O4Nx8UbWmrHHZ7ihKB8WiscX1cz05l1KvKorYTgq+xeXwz2Beh3MkXBMnA46uuyEtimN945OEdYshZnbh80jyw=="], + + "@luma.gl/core": ["@luma.gl/core@9.0.0", "", { "dependencies": { "@math.gl/types": "^4.0.0", "@probe.gl/env": "^4.0.2", "@probe.gl/log": "^4.0.2", "@probe.gl/stats": "^4.0.2", "@types/offscreencanvas": "^2019.6.4" } }, "sha512-3ApetxOC5vcdViyQGyiyUiXxcpt6f+bsFE0LcJy32HA9ygBkojzcpmfTFRm9SO9FMhI2zn2jq7i+xh7xVmrl0Q=="], - "@luma.gl/engine": ["@luma.gl/engine@9.3.3", "", { "dependencies": { "@math.gl/core": "^4.1.0", "@math.gl/types": "^4.1.0", "@probe.gl/log": "^4.1.1", "@probe.gl/stats": "^4.1.1" }, "peerDependencies": { "@luma.gl/core": "~9.3.0", "@luma.gl/shadertools": "~9.3.0" } }, "sha512-StmMTzUcUlpKMU3wvWU48A6OQyphptD9zVGBsSkK6iHIBdtBKlOcmqRkyfvRouo8JHtlrnoJDHLVKhxorwhGAg=="], + "@luma.gl/engine": ["@luma.gl/engine@9.0.0", "", { "dependencies": { "@luma.gl/shadertools": "9.0.0", "@math.gl/core": "^4.0.0", "@probe.gl/log": "^4.0.2", "@probe.gl/stats": "^4.0.2" }, "peerDependencies": { "@luma.gl/core": "^9.0.0-beta" } }, "sha512-I5OUFvqkYlsz1zn0u9EcF+NDtWSCvtSA3Ki8dRcbhx3ff2f1UuMZ8w0H2f/0Dz4pfnRaSXFaxfx6EslbXK2TzQ=="], - "@luma.gl/shadertools": ["@luma.gl/shadertools@9.3.3", "", { "dependencies": { "@math.gl/core": "^4.1.0", "@math.gl/types": "^4.1.0" }, "peerDependencies": { "@luma.gl/core": "~9.3.0" } }, "sha512-4ZfG4/Utix951vqyiG/JIx+Eg+GMNwOxgr/07/i0gf7bK1gJZIEQ5BxVcDw4MCQfdoVlGPGzl0cQKbdqBvaCAQ=="], + "@luma.gl/shadertools": ["@luma.gl/shadertools@9.0.0", "", { "dependencies": { "@math.gl/core": "^4.0.0", "@math.gl/types": "^4.0.0" }, "peerDependencies": { "@luma.gl/core": "^9.0.0-beta" } }, "sha512-m8IdrG+bFz8P2onLDD0fdaH12ibMzShxZ4I/DDrz9HFq4CitdiMABQiqRmAzZNv0RxnyJ6xIYxvudgvUk18B3Q=="], - "@luma.gl/webgl": ["@luma.gl/webgl@9.3.3", "", { "dependencies": { "@math.gl/types": "^4.1.0", "@probe.gl/env": "^4.1.1" }, "peerDependencies": { "@luma.gl/core": "~9.3.0" } }, "sha512-X+aavdP5o6VFHSA0es9gKZTT145jfcFbhKJt/gwJrptnKNoIW4+Y37ZEpCo1AzAnr+FQCxjgcM2kOCpoWMfSVA=="], + "@luma.gl/webgl": ["@luma.gl/webgl@9.0.0", "", { "dependencies": { "@luma.gl/constants": "9.0.0", "@probe.gl/env": "^4.0.2" }, "peerDependencies": { "@luma.gl/core": "^9.0.0-beta" } }, "sha512-yAHvoFcSXepMk5mh3Z3K/P63fkkNYoivkUBhivz0rXFzkp5I1kXpyEvnDvREdan8GRZoKXM9SvlRSvHVxTdNIw=="], "@mapbox/tiny-sdf": ["@mapbox/tiny-sdf@2.2.0", "", {}, "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg=="], @@ -1014,6 +1024,8 @@ "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], @@ -1810,6 +1822,8 @@ "gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="], + "hammerjs": ["hammerjs@2.0.8", "", {}, "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ=="], + "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], @@ -2196,7 +2210,7 @@ "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], - "mjolnir.js": ["mjolnir.js@3.0.0", "", {}, "sha512-siX3YCG7N2HnmN1xMH3cK4JkUZJhbkhRFJL+G5N1vH0mh1t5088rJknIoqDFWDIU6NPGvRRgLnYW3ZHjSMEBLA=="], + "mjolnir.js": ["mjolnir.js@2.7.3", "", { "dependencies": { "@types/hammerjs": "^2.0.41", "hammerjs": "^2.0.8" } }, "sha512-Z5z/+FzZqOSO3juSVKV3zcm4R2eAlWwlKMcqHmyFEJAaLILNcDKnIbnb4/kbcGyIuhtdWrzu8WOIR7uM6I34aw=="], "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], @@ -3100,6 +3114,8 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@luma.gl/webgl/@luma.gl/constants": ["@luma.gl/constants@9.0.0", "", {}, "sha512-0oVddffzzkYeHs2CKwehezQ0R5IkqM9Jq2FVYdnOAa02rAtqytWwvmhn4zs0Q/kYtHVn51OnkqIcB1vk5MsLJA=="], + "@poppinss/dumper/supports-color": ["supports-color@10.0.0", "", {}, "sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ=="], "@rollup/plugin-node-resolve/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], diff --git a/ui/package.json b/ui/package.json index e4e47b89a..45bdb1576 100644 --- a/ui/package.json +++ b/ui/package.json @@ -88,16 +88,18 @@ "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.38.1", "@csound/browser": "file:./packages/csound-browser", - "@deck.gl/core": "^9.3.0-beta.2", - "@deck.gl/layers": "^9.3.0-beta.2", + "@deck.gl/core": "9.0.0", + "@deck.gl/layers": "9.0.0", "@elemaudio/core": "^4.0.1", "@elemaudio/web-renderer": "^4.0.3", "@google/genai": "^1.11.0", "@iizukak/codemirror-lang-wgsl": "^0.3.0", "@lezer/generator": "^1.8.0", "@lezer/highlight": "^1.2.1", - "@luma.gl/core": "^9.3.3", - "@luma.gl/webgl": "^9.3.3", + "@luma.gl/core": "9.0.0", + "@luma.gl/engine": "9.0.0", + "@luma.gl/shadertools": "9.0.0", + "@luma.gl/webgl": "9.0.0", "@mediapipe/tasks-vision": "0.10.33", "@replit/codemirror-vim": "^6.3.0", "@rollup/browser": "^4.50.1", @@ -178,6 +180,12 @@ "patchedDependencies": { "superdough@1.2.6": "patches/superdough@1.2.6.patch" }, + "overrides": { + "@luma.gl/core": "9.0.0", + "@luma.gl/engine": "9.0.0", + "@luma.gl/shadertools": "9.0.0", + "@luma.gl/webgl": "9.0.0" + }, "packageManager": "bun@1.2.0", "lint-staged": { "*.{js,ts,svelte,json,md,css}": "prettier --write" diff --git a/ui/src/lib/audio/AudioAnalysisSystem.ts b/ui/src/lib/audio/AudioAnalysisSystem.ts index 572290b0a..2ec08605f 100644 --- a/ui/src/lib/audio/AudioAnalysisSystem.ts +++ b/ui/src/lib/audio/AudioAnalysisSystem.ts @@ -43,7 +43,7 @@ export interface AudioAnalysisPayload { export type AudioAnalysisPayloadWithType = AudioAnalysisPayload & { type: 'setFFTData'; - nodeType: 'hydra' | 'glsl' | 'canvas' | 'textmode' | 'three' | 'regl' | 'swgl'; + nodeType: 'hydra' | 'glsl' | 'canvas' | 'textmode' | 'three' | 'deckgl' | 'regl' | 'swgl'; }; export type OnFFTReadyCallback = (data: AudioAnalysisPayload) => void; diff --git a/ui/src/lib/components/nodes/DeckGLNode.svelte b/ui/src/lib/components/nodes/DeckGLNode.svelte index e767196be..0278f57fe 100644 --- a/ui/src/lib/components/nodes/DeckGLNode.svelte +++ b/ui/src/lib/components/nodes/DeckGLNode.svelte @@ -55,9 +55,10 @@ selected?: boolean; } = $props(); + const eventBus = PatchiesEventBus.getInstance(); + let consoleRef: VirtualConsole | null = $state(null); let lineErrors = $state | undefined>(undefined); - const eventBus = PatchiesEventBus.getInstance(); function handleConsoleOutput(event: ConsoleOutputEvent) { if (event.nodeId !== nodeId) return; @@ -147,11 +148,13 @@ function handleTitleUpdate(e: NodeTitleUpdateEvent) { if (e.nodeId !== nodeId) return; + updateNodeData(nodeId, { title: e.title }); } function handleHidePortsUpdate(e: NodeHidePortsUpdateEvent) { if (e.nodeId !== nodeId) return; + updateNodeData(nodeId, { hidePorts: e.hidePorts }); } @@ -178,12 +181,15 @@ function handleVideoOutputEnabledUpdate(e: NodeVideoOutputEnabledUpdateEvent) { if (e.nodeId !== nodeId) return; + videoOutputEnabled = e.videoOutputEnabled; + updateNodeInternals(nodeId); } const setCodeAndUpdate = (newCode: string) => { updateNodeData(nodeId, { code: newCode }); + setTimeout(() => updateDeckGL()); }; diff --git a/ui/src/workers/rendering/deckglRenderer.ts b/ui/src/workers/rendering/deckglRenderer.ts index c0d253e59..0b93efddb 100644 --- a/ui/src/workers/rendering/deckglRenderer.ts +++ b/ui/src/workers/rendering/deckglRenderer.ts @@ -1,6 +1,5 @@ import type regl from 'regl'; import type { Deck, Layer } from '@deck.gl/core'; -import type { Device, Framebuffer, Texture } from '@luma.gl/core'; import type { FBORenderer } from './fboRenderer'; import type { RenderParams } from '$lib/rendering/types'; import { CANVAS_WRAPPER_OFFSET } from '$lib/constants/error-reporting-offsets'; @@ -44,14 +43,44 @@ type SizedFramebuffer = regl.Framebuffer2D & { height: number; }; +type DeckTexture = { + destroy(): void; +}; + +type DeckFramebuffer = { + width: number; + height: number; + handle?: WebGLFramebuffer; + resize(options: { width: number; height: number }): void; + destroy(): void; +}; + +type DeckDevice = { + canvasContext?: { + setDrawingBufferSize?: (width: number, height: number) => void; + }; + createTexture(options: { + format: string; + width: number; + height: number; + sampler?: Record; + }): DeckTexture; + createFramebuffer(options: { + width: number; + height: number; + colorAttachments: DeckTexture[]; + depthStencilAttachment?: string; + }): DeckFramebuffer; +}; + export class DeckGLRenderer extends BaseWorkerRenderer { private DeckClass: typeof import('@deck.gl/core').Deck | null = null; private layerClasses: typeof import('@deck.gl/layers') | null = null; private deck: Deck | null = null; - private device: Device | null = null; - private deckTexture: Texture | null = null; - private deckFramebuffer: Framebuffer | null = null; + private device: DeckDevice | null = null; + private deckTexture: DeckTexture | null = null; + private deckFramebuffer: DeckFramebuffer | null = null; private getLayers: UserGetLayers | null = null; private previousPointer: { x: number; y: number; down: boolean } | null = null; private pendingWheelDelta = 0; @@ -222,7 +251,9 @@ export class DeckGLRenderer extends BaseWorkerRenderer { canvas.parentElement ??= null; await new Promise((resolve) => { - this.deck = new DeckClass({ + const renderer = this; + + renderer.deck = new DeckClass({ controller: false, gl, width: null, @@ -233,11 +264,11 @@ export class DeckGLRenderer extends BaseWorkerRenderer { layers: [], getTooltip: null, parameters: { depthCompare: 'less-equal' }, - onDeviceInitialized: (device: Device) => { - this.device = device; - this.createRenderTarget(); + onDeviceInitialized: (device: unknown) => { + renderer.device = device as DeckDevice; + renderer.createRenderTarget(); - this.deck?.setProps({ _framebuffer: this.deckFramebuffer }); + renderer.deck?.setProps({ _framebuffer: renderer.deckFramebuffer } as never); resolve(); }, @@ -280,7 +311,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { if (!this.device || !this.deckFramebuffer) return; const [width, height] = this.renderer.outputSize; - this.device.canvasContext?.setDrawingBufferSize(width, height); + this.device.canvasContext?.setDrawingBufferSize?.(width, height); if (this.deckFramebuffer.width !== width || this.deckFramebuffer.height !== height) { this.deckFramebuffer.resize({ width, height }); @@ -325,7 +356,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { if (!gl) return; const [width, height] = this.renderer.outputSize; - const sourceFBO = (this.deckFramebuffer as Framebuffer & { handle?: WebGLFramebuffer }).handle; + const sourceFBO = this.deckFramebuffer.handle; const destFBO = getFramebuffer(this.framebuffer); if (!sourceFBO) return; diff --git a/ui/src/workers/rendering/workerDOMMocks.ts b/ui/src/workers/rendering/workerDOMMocks.ts index 2dc5b1e6f..2b60e082f 100644 --- a/ui/src/workers/rendering/workerDOMMocks.ts +++ b/ui/src/workers/rendering/workerDOMMocks.ts @@ -128,6 +128,11 @@ const mockWindow = { clearInterval: self.clearInterval.bind(self) }; +class MockHTMLElement {} +class MockHTMLCanvasElement extends MockHTMLElement {} +class MockHTMLImageElement extends MockHTMLElement {} +class MockHTMLVideoElement extends MockHTMLElement {} + let isSetup = false; /** @@ -150,7 +155,37 @@ export function setupWorkerDOMMocks(): void { // Also make HTMLElement available for instanceof checks // @ts-expect-error -- mock class for instanceof - self.HTMLElement = class HTMLElement {}; + self.HTMLElement = MockHTMLElement; + + // luma.gl checks `canvas instanceof HTMLCanvasElement` even when running with + // an OffscreenCanvas. The worker only needs the global constructor to exist. + // @ts-expect-error -- mock class for instanceof + self.HTMLCanvasElement = MockHTMLCanvasElement; + + // Texture/source checks in visualization libraries often reference these + // browser globals even when the current deck.gl layer does not use them. + // @ts-expect-error -- mock class for instanceof + self.HTMLImageElement = MockHTMLImageElement; + // @ts-expect-error -- mock class for instanceof + self.HTMLVideoElement = MockHTMLVideoElement; + + // @ts-expect-error -- injecting browser constructors into worker global + globalThis.HTMLElement = MockHTMLElement; + // @ts-expect-error -- injecting browser constructors into worker global + globalThis.HTMLCanvasElement = MockHTMLCanvasElement; + // @ts-expect-error -- injecting browser constructors into worker global + globalThis.HTMLImageElement = MockHTMLImageElement; + // @ts-expect-error -- injecting browser constructors into worker global + globalThis.HTMLVideoElement = MockHTMLVideoElement; + + // @ts-expect-error -- mock window shape + mockWindow.HTMLElement = MockHTMLElement; + // @ts-expect-error -- mock window shape + mockWindow.HTMLCanvasElement = MockHTMLCanvasElement; + // @ts-expect-error -- mock window shape + mockWindow.HTMLImageElement = MockHTMLImageElement; + // @ts-expect-error -- mock window shape + mockWindow.HTMLVideoElement = MockHTMLVideoElement; isSetup = true; } From 60eb0c6a5a54d6a5410310875ad2605501940249 Mon Sep 17 00:00:00 2001 From: Phoomparin Mano Date: Sun, 24 May 2026 15:33:52 +0700 Subject: [PATCH 03/12] add openstreetmap tile prototype --- .../specs/147-deckgl-fbo-renderer.md | 28 +++- ui/bun.lock | 125 +++++++++++++++++- ui/package.json | 5 + ui/src/lib/ai/object-prompts/deckgl.ts | 4 +- ui/src/lib/canvas/constants.ts | 4 +- ui/src/lib/components/nodes/DeckGLNode.svelte | 6 +- ui/src/lib/extensions/pack-icons.ts | 2 + ui/src/lib/extensions/preset-packs.ts | 8 ++ ui/src/lib/presets/builtin/deckgl.presets.ts | 62 +++++++++ ui/src/lib/presets/builtin/index.ts | 7 +- ui/src/workers/rendering/deckglRenderer.ts | 6 +- ui/src/workers/rendering/fboRenderer.ts | 5 +- ui/static/content/objects/deckgl.md | 26 +++- 13 files changed, 275 insertions(+), 13 deletions(-) create mode 100644 ui/src/lib/presets/builtin/deckgl.presets.ts diff --git a/docs/design-docs/specs/147-deckgl-fbo-renderer.md b/docs/design-docs/specs/147-deckgl-fbo-renderer.md index b4e4646ab..10bf8033b 100644 --- a/docs/design-docs/specs/147-deckgl-fbo-renderer.md +++ b/docs/design-docs/specs/147-deckgl-fbo-renderer.md @@ -14,7 +14,7 @@ Prototype a `deckgl` visual object that renders deck.gl layers inside the worker ## User Code Shape -The object exposes deck.gl classes and expects user code to define `getLayers`. +The object exposes deck.gl layer classes and expects user code to define `getLayers`. ```js function getLayers({ time, viewState, mouse }) { @@ -31,6 +31,32 @@ function getLayers({ time, viewState, mouse }) { } ``` +Raster tile layers are supported through `TileLayer` from `@deck.gl/geo-layers` +and `BitmapLayer` from `@deck.gl/layers`. The first built-in preset uses OSM +slippy-map raster tiles: + +```js +function getLayers() { + return [ + new TileLayer({ + data: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', + renderSubLayers: props => { + const { boundingBox } = props.tile; + return new BitmapLayer(props, { + image: props.data, + bounds: [ + boundingBox[0][0], + boundingBox[0][1], + boundingBox[1][0], + boundingBox[1][1] + ] + }); + } + }) + ]; +} +``` + ## Interaction - Drag forwards through the existing Shadertoy-style mouse state. diff --git a/ui/bun.lock b/ui/bun.lock index bb131f62a..65d7549b4 100644 --- a/ui/bun.lock +++ b/ui/bun.lock @@ -16,7 +16,10 @@ "@codemirror/view": "^6.38.1", "@csound/browser": "file:./packages/csound-browser", "@deck.gl/core": "9.0.0", + "@deck.gl/extensions": "9.0.0", + "@deck.gl/geo-layers": "9.0.0", "@deck.gl/layers": "9.0.0", + "@deck.gl/mesh-layers": "9.0.0", "@elemaudio/core": "^4.0.1", "@elemaudio/web-renderer": "^4.0.3", "@google/genai": "^1.11.0", @@ -25,6 +28,7 @@ "@lezer/highlight": "^1.2.1", "@luma.gl/core": "9.0.0", "@luma.gl/engine": "9.0.0", + "@luma.gl/gltf": "9.0.0", "@luma.gl/shadertools": "9.0.0", "@luma.gl/webgl": "9.0.0", "@mediapipe/tasks-vision": "0.10.33", @@ -168,6 +172,7 @@ "overrides": { "@luma.gl/core": "9.0.0", "@luma.gl/engine": "9.0.0", + "@luma.gl/gltf": "9.0.0", "@luma.gl/shadertools": "9.0.0", "@luma.gl/webgl": "9.0.0", }, @@ -416,8 +421,14 @@ "@deck.gl/core": ["@deck.gl/core@9.0.0", "", { "dependencies": { "@loaders.gl/core": "^4.1.4", "@loaders.gl/images": "^4.1.4", "@luma.gl/constants": "^9.0.6", "@luma.gl/core": "^9.0.6", "@luma.gl/engine": "^9.0.6", "@luma.gl/shadertools": "^9.0.6", "@luma.gl/webgl": "^9.0.6", "@math.gl/core": "^4.0.0", "@math.gl/sun": "^4.0.0", "@math.gl/web-mercator": "^4.0.0", "@probe.gl/env": "^4.0.9", "@probe.gl/log": "^4.0.9", "@probe.gl/stats": "^4.0.9", "@types/offscreencanvas": "^2019.6.4", "gl-matrix": "^3.0.0", "mjolnir.js": "^2.7.0" } }, "sha512-o3Ica4ZUU+dHAJ3v5BXorzg90xy9THLWiZQ/v4kZHat050EfFT8ZAPWwL0xS9M0DBvY4ikcZjTtcBtsh+KGm0A=="], + "@deck.gl/extensions": ["@deck.gl/extensions@9.0.0", "", { "dependencies": { "@luma.gl/constants": "^9.0.6", "@luma.gl/shadertools": "^9.0.6", "@math.gl/core": "^4.0.0" }, "peerDependencies": { "@deck.gl/core": "^9.0.0", "@luma.gl/core": "^9.0.0", "@luma.gl/engine": "^9.0.0" } }, "sha512-2QcWLXuQVdlOWCV/vqpZtIhAkYjv7Pvcd5bxI3b02mtkoICpryWSWYJDP6+vNxNvviwe9rVSvwTL6vqd/dwWDQ=="], + + "@deck.gl/geo-layers": ["@deck.gl/geo-layers@9.0.0", "", { "dependencies": { "@loaders.gl/3d-tiles": "^4.1.4", "@loaders.gl/gis": "^4.1.4", "@loaders.gl/loader-utils": "^4.1.4", "@loaders.gl/mvt": "^4.1.4", "@loaders.gl/schema": "^4.1.4", "@loaders.gl/terrain": "^4.1.4", "@loaders.gl/tiles": "^4.1.4", "@loaders.gl/wms": "^4.1.4", "@luma.gl/gltf": "^9.0.6", "@luma.gl/shadertools": "^9.0.6", "@math.gl/core": "^4.0.0", "@math.gl/culling": "^4.0.0", "@math.gl/web-mercator": "^4.0.0", "@types/geojson": "^7946.0.8", "h3-js": "^4.1.0", "long": "^3.2.0" }, "peerDependencies": { "@deck.gl/core": "^9.0.0", "@deck.gl/extensions": "^9.0.0", "@deck.gl/layers": "^9.0.0", "@deck.gl/mesh-layers": "^9.0.0", "@loaders.gl/core": "^4.1.0", "@luma.gl/core": "^9.0.0", "@luma.gl/engine": "^9.0.0" } }, "sha512-Qt5fwuFcff7Jbb7hbNcJbUMRkhcbPQ/wK1euvNhKMRm18Ng9Y+9OfDEggmU0QEfsHaJzUaaWotY34wgbI1P5Lw=="], + "@deck.gl/layers": ["@deck.gl/layers@9.0.0", "", { "dependencies": { "@loaders.gl/images": "^4.1.4", "@loaders.gl/schema": "^4.1.4", "@mapbox/tiny-sdf": "^2.0.5", "@math.gl/core": "^4.0.0", "@math.gl/polygon": "^4.0.0", "@math.gl/web-mercator": "^4.0.0", "earcut": "^2.2.4" }, "peerDependencies": { "@deck.gl/core": "^9.0.0", "@loaders.gl/core": "^4.1.0", "@luma.gl/core": "^9.0.0", "@luma.gl/engine": "^9.0.0" } }, "sha512-ScWnvgyWEuJCritPshptbtp1DIqn9KTDCFlJyxKZFj1ATn9Hco5gXgPxVJmoY2w3j2WOwaAo93Mgk8GlUry0Pg=="], + "@deck.gl/mesh-layers": ["@deck.gl/mesh-layers@9.0.0", "", { "dependencies": { "@loaders.gl/gltf": "^4.1.4", "@luma.gl/gltf": "^9.0.6", "@luma.gl/shadertools": "^9.0.6" }, "peerDependencies": { "@deck.gl/core": "^9.0.0", "@luma.gl/core": "^9.0.0", "@luma.gl/engine": "^9.0.0" } }, "sha512-SJbOCgnYbJ76uyXbteo+13z4QSWfFv/TFCFjZA7zHsUOxHYtMPVy7r9jN6HpSVqTmi/Y7jZFdfHMn2SfxCdxFQ=="], + "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], "@elemaudio/core": ["@elemaudio/core@4.0.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "invariant": "^2.2.4", "shallowequal": "^1.1.0" } }, "sha512-O58CaR5ttxzPt1LwkJoXVipPLlZ8lZmOIHhJFiih3rWkfCZ3/H1X2XTG0HM2jAn3BokoP5jYSbnbEiQybyvRGw=="], @@ -614,18 +625,48 @@ "@lezer/python": ["@lezer/python@1.1.18", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg=="], + "@loaders.gl/3d-tiles": ["@loaders.gl/3d-tiles@4.4.2", "", { "dependencies": { "@loaders.gl/compression": "4.4.2", "@loaders.gl/crypto": "4.4.2", "@loaders.gl/draco": "4.4.2", "@loaders.gl/gltf": "4.4.2", "@loaders.gl/images": "4.4.2", "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/math": "4.4.2", "@loaders.gl/tiles": "4.4.2", "@loaders.gl/zip": "4.4.2", "@math.gl/core": "^4.1.0", "@math.gl/culling": "^4.1.0", "@math.gl/geospatial": "^4.1.0", "@probe.gl/log": "^4.1.1", "long": "^5.2.1" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-rf2R7x/+t41hQpaQ3iKofooE6unZ0+sGlYUXBo7lYFEnoMmalzrOI6jCs+CV96TALMPQcpfPa566XWF74XkaBQ=="], + + "@loaders.gl/compression": ["@loaders.gl/compression@4.4.2", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/worker-utils": "4.4.2", "@types/pako": "^1.0.1", "fflate": "0.7.4", "pako": "1.0.11", "snappyjs": "^0.6.1" }, "optionalDependencies": { "@types/brotli": "^1.3.0", "brotli": "^1.3.2", "lz4js": "^0.2.0", "zstd-codec": "^0.1" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-/LzblCXn6wOg7ca2zkUOTO0zhjjaPAOqlLp4/kwd57v7KZU8M8aKUNlU1DAuWKW9p/+TpGsLKwDqOCO+hjrOnQ=="], + "@loaders.gl/core": ["@loaders.gl/core@4.4.2", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/schema": "4.4.2", "@loaders.gl/schema-utils": "4.4.2", "@loaders.gl/worker-utils": "4.4.2", "@probe.gl/log": "^4.1.1" } }, "sha512-DZmsTwxdKh3q+mS1vSOW2EXFgwxZ4nIBte4H5g6e4VyQoQ6jAOkk0M6V+Asgy/eqjGTNjhfBA1HIkyBl0A9hcA=="], + "@loaders.gl/crypto": ["@loaders.gl/crypto@4.4.2", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/worker-utils": "4.4.2", "@types/crypto-js": "^4.0.2" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-3QQxNFmCeznMIsY/ZD4pYO4SvS4i3nq5aJJ993DEIZVB1i0z19OmyJu4TSovvirXXrNWPQRJFPUUwdPxol9wLA=="], + + "@loaders.gl/draco": ["@loaders.gl/draco@4.4.2", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/schema": "4.4.2", "@loaders.gl/schema-utils": "4.4.2", "@loaders.gl/worker-utils": "4.4.2", "draco3d": "1.5.7" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-UByWIt/yhxMFBIlyoqJYaj0rnTz/wwDWbI4CVQc/MzbLZW7NtUkDyYDcjbjE2SBWpu6Ef4ryojGd/NWIA3Yknw=="], + + "@loaders.gl/geoarrow": ["@loaders.gl/geoarrow@4.4.2", "", { "dependencies": { "@math.gl/polygon": "^4.1.0", "apache-arrow": ">= 17.0.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-FGCtUsvTwdxiNqS8cEtys1FdM/m6pwMzvNEa32sHfrI4Si465Oa2iJkyu2q/XMgL/vhfE/G3EezpVKZARbCzkw=="], + + "@loaders.gl/gis": ["@loaders.gl/gis@4.4.2", "", { "dependencies": { "@loaders.gl/geoarrow": "4.4.2", "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/schema": "4.4.2", "@loaders.gl/schema-utils": "4.4.2", "@mapbox/vector-tile": "^1.3.1", "@math.gl/polygon": "^4.1.0", "pbf": "^3.2.1" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-VMs1XcqUCqLczfySsI9VA/L3WDuuNRPqVoHcXU3FZuSe49x++gtwmxF1TEhJCaQINk0CkCOnxBbKOz3I9M1e3w=="], + + "@loaders.gl/gltf": ["@loaders.gl/gltf@4.4.2", "", { "dependencies": { "@loaders.gl/draco": "4.4.2", "@loaders.gl/images": "4.4.2", "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/schema": "4.4.2", "@loaders.gl/textures": "4.4.2", "@math.gl/core": "^4.1.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-aBvI7P/1GxePdHIvuyTM4A8yt3F5ph4dq0mkyJHmEjBl1Cwh3mDZJI1JSlZAFVilTZ6NxJZOiHUYWe1pBloVvw=="], + "@loaders.gl/images": ["@loaders.gl/images@4.4.2", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.2" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-b+1keNvPlyLniWtX4ZaThz2dF2aohi8Q+OEsDF2hJNZYyZJOqP9b/72UhlVk+inxTJfTLRBNARs2TJ2ssBlelg=="], "@loaders.gl/loader-utils": ["@loaders.gl/loader-utils@4.4.2", "", { "dependencies": { "@loaders.gl/schema": "4.4.2", "@loaders.gl/worker-utils": "4.4.2", "@probe.gl/log": "^4.1.1", "@probe.gl/stats": "^4.1.1" } }, "sha512-kqwBbyRC7rrQVsnJyKeoaig9hxaa5oj91OKqWm27HPuVn4q2dD67SEhiG0ND62eRp0tLY6jTqEcI5kDzHBZ6MA=="], + "@loaders.gl/math": ["@loaders.gl/math@4.4.2", "", { "dependencies": { "@math.gl/core": "^4.1.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-Pcm1DKrzH3EqC5PkBxQX0oVjmXM3RIm2Gfj0cXQoqly+8c/NBtQrcBA9tl12h2ozZe8Ednue/kockbGsyKAx5A=="], + + "@loaders.gl/mvt": ["@loaders.gl/mvt@4.4.2", "", { "dependencies": { "@loaders.gl/gis": "4.4.2", "@loaders.gl/images": "4.4.2", "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/schema": "4.4.2", "@math.gl/polygon": "^4.1.0", "@probe.gl/stats": "^4.1.1", "pbf": "^3.2.1" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-yji3TAUofTA3GXvvyemwfrRwbd1ILpv2qe4mRduHdzjJdy2httH1cCKkesavFuLeRQqEuVyhhxYK+aSAbdt9Kg=="], + "@loaders.gl/schema": ["@loaders.gl/schema@4.4.2", "", { "dependencies": { "@types/geojson": "^7946.0.7", "apache-arrow": ">= 17.0.0" } }, "sha512-mJTZehTHIFl8ed+03nebuPAMnLP8Yp00DKTzCnKT2HNy/uV4+Sw+GrGIuhPHGU8tdQmtBXRURGM2ZxUAxMfGKg=="], "@loaders.gl/schema-utils": ["@loaders.gl/schema-utils@4.4.2", "", { "dependencies": { "@loaders.gl/schema": "4.4.2", "@types/geojson": "^7946.0.7", "apache-arrow": ">= 17.0.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-yYYRD/POBEO72rhIyLASrqKUUhfIOQuFk/fgInN6Td2qvFgsHbo5UaCM4sTqVUWwNxNvXDQi8ezpbnCa/yi+OQ=="], + "@loaders.gl/terrain": ["@loaders.gl/terrain@4.4.2", "", { "dependencies": { "@loaders.gl/images": "4.4.2", "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/schema": "4.4.2", "@mapbox/martini": "^0.2.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-ScE90mhUrIOOf+248+G8bxgg5xfLptE94gVxtYsLysyG8b4Ne2WEb6J2gpvQqmaLz3k9OqgPR7M8F1zI5BVO0w=="], + + "@loaders.gl/textures": ["@loaders.gl/textures@4.4.2", "", { "dependencies": { "@loaders.gl/images": "4.4.2", "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/schema": "4.4.2", "@loaders.gl/worker-utils": "4.4.2", "@math.gl/types": "^4.1.0", "ktx-parse": "^0.7.0", "texture-compressor": "^1.0.2" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-+GKcHEE0GjpuSJ6qbuRsB0CaOSUhJ1epUvhMP5GVK7I6+bwSvG8nqmRRGXFQNmYsbFANwG+wjwKf16wqJwP6vg=="], + + "@loaders.gl/tiles": ["@loaders.gl/tiles@4.4.2", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/math": "4.4.2", "@math.gl/core": "^4.1.0", "@math.gl/culling": "^4.1.0", "@math.gl/geospatial": "^4.1.0", "@math.gl/web-mercator": "^4.1.0", "@probe.gl/stats": "^4.1.1" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-DHqMC38e6IEzWEDc15GfIKsZT82VZH7ocF79xLRScey0eZfCb3qI5nDLeg5adSBBsHkG3Li0S0PEnwxQmKT3qw=="], + + "@loaders.gl/wms": ["@loaders.gl/wms@4.4.2", "", { "dependencies": { "@loaders.gl/images": "4.4.2", "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/schema": "4.4.2", "@loaders.gl/xml": "4.4.2", "@turf/rewind": "^5.1.5", "deep-strict-equal": "^0.2.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-7uhis6fOTHeqRLIU1EPoC1oqeXVk5p1pM136fSqMw0CbVSNj87b0FFMwlJwzwTfL8Vte8GyKrNcDa47PjaT19Q=="], + "@loaders.gl/worker-utils": ["@loaders.gl/worker-utils@4.4.2", "", { "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-oiZ0SoC1QKrOkhYPlVZ6Q06CtmuFRyZw2rwzmT08ZyaGtOArIJHDjlhxzwWiv+6fdws47Ub5uIGsdI1Ab1xYsA=="], + "@loaders.gl/xml": ["@loaders.gl/xml@4.4.2", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.2", "@loaders.gl/schema": "4.4.2", "fast-xml-parser": "^5.3.6" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-OOPqpYH1PK9szuzXh3Oy7aErMXTXB+aiKi79LPCI93Wsb8pdbQiDiRRW8X/op9qABhxpCAMXF5N89eDJv3XdtQ=="], + + "@loaders.gl/zip": ["@loaders.gl/zip@4.4.2", "", { "dependencies": { "@loaders.gl/compression": "4.4.2", "@loaders.gl/crypto": "4.4.2", "@loaders.gl/loader-utils": "4.4.2", "jszip": "^3.1.5", "md5": "^2.3.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-KdgmJRNra9+9jt2zzHUvFXnBqwzeN7dW4MEgTmH/NtraGy8bz5Tk5NrIUj8JXPxhx+vP2vxUWbeCEUoLGO/m9A=="], + "@lucide/svelte": ["@lucide/svelte@0.575.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-FEFp/0McZwsjBqh1Dn8H+UBm1yHFQYk+utuVMFDw57155+wz2XMoc1pw027ylCPzs+bi14UEXYKbekFhuJKtnw=="], "@luma.gl/constants": ["@luma.gl/constants@9.1.10", "", {}, "sha512-O4Nx8UbWmrHHZ7ihKB8WiscX1cz05l1KvKorYTgq+xeXwz2Beh3MkXBMnA46uuyEtimN945OEdYshZnbh80jyw=="], @@ -634,16 +675,28 @@ "@luma.gl/engine": ["@luma.gl/engine@9.0.0", "", { "dependencies": { "@luma.gl/shadertools": "9.0.0", "@math.gl/core": "^4.0.0", "@probe.gl/log": "^4.0.2", "@probe.gl/stats": "^4.0.2" }, "peerDependencies": { "@luma.gl/core": "^9.0.0-beta" } }, "sha512-I5OUFvqkYlsz1zn0u9EcF+NDtWSCvtSA3Ki8dRcbhx3ff2f1UuMZ8w0H2f/0Dz4pfnRaSXFaxfx6EslbXK2TzQ=="], + "@luma.gl/gltf": ["@luma.gl/gltf@9.0.0", "", { "dependencies": { "@loaders.gl/textures": "^4.1.0", "@luma.gl/shadertools": "9.0.0", "@math.gl/core": "^4.0.0" }, "peerDependencies": { "@luma.gl/core": "^9.0.0-beta", "@luma.gl/engine": "^9.0.0-beta" } }, "sha512-Wjpo/kJZm6R9Pr0Co4tWqxc+vBAGNOlpvFkdNLcvdtkJJg/pqOD4QOR1Wc6QRSfUhZh9yPwgYHwSNKfkBO9kAA=="], + "@luma.gl/shadertools": ["@luma.gl/shadertools@9.0.0", "", { "dependencies": { "@math.gl/core": "^4.0.0", "@math.gl/types": "^4.0.0" }, "peerDependencies": { "@luma.gl/core": "^9.0.0-beta" } }, "sha512-m8IdrG+bFz8P2onLDD0fdaH12ibMzShxZ4I/DDrz9HFq4CitdiMABQiqRmAzZNv0RxnyJ6xIYxvudgvUk18B3Q=="], "@luma.gl/webgl": ["@luma.gl/webgl@9.0.0", "", { "dependencies": { "@luma.gl/constants": "9.0.0", "@probe.gl/env": "^4.0.2" }, "peerDependencies": { "@luma.gl/core": "^9.0.0-beta" } }, "sha512-yAHvoFcSXepMk5mh3Z3K/P63fkkNYoivkUBhivz0rXFzkp5I1kXpyEvnDvREdan8GRZoKXM9SvlRSvHVxTdNIw=="], + "@mapbox/martini": ["@mapbox/martini@0.2.0", "", {}, "sha512-7hFhtkb0KTLEls+TRw/rWayq5EeHtTaErgm/NskVoXmtgAQu/9D299aeyj6mzAR/6XUnYRp2lU+4IcrYRFjVsQ=="], + + "@mapbox/point-geometry": ["@mapbox/point-geometry@0.1.0", "", {}, "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="], + "@mapbox/tiny-sdf": ["@mapbox/tiny-sdf@2.2.0", "", {}, "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg=="], + "@mapbox/vector-tile": ["@mapbox/vector-tile@1.3.1", "", { "dependencies": { "@mapbox/point-geometry": "~0.1.0" } }, "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw=="], + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], "@math.gl/core": ["@math.gl/core@4.1.0", "", { "dependencies": { "@math.gl/types": "4.1.0" } }, "sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA=="], + "@math.gl/culling": ["@math.gl/culling@4.1.0", "", { "dependencies": { "@math.gl/core": "4.1.0", "@math.gl/types": "4.1.0" } }, "sha512-jFmjFEACnP9kVl8qhZxFNhCyd47qPfSVmSvvjR0/dIL6R9oD5zhR1ub2gN16eKDO/UM7JF9OHKU3EBIfeR7gtg=="], + + "@math.gl/geospatial": ["@math.gl/geospatial@4.1.0", "", { "dependencies": { "@math.gl/core": "4.1.0", "@math.gl/types": "4.1.0" } }, "sha512-BzsUhpVvnmleyYF6qdqJIip6FtIzJmnWuPTGhlBuPzh7VBHLonCFSPtQpbkRuoyAlbSyaGXcVt6p6lm9eK2vtg=="], + "@math.gl/polygon": ["@math.gl/polygon@4.1.0", "", { "dependencies": { "@math.gl/core": "4.1.0" } }, "sha512-YA/9PzaCRHbIP5/0E9uTYrqe+jsYTQoqoDWhf6/b0Ixz8bPZBaGDEafLg3z7ffBomZLacUty9U3TlPjqMtzPjA=="], "@math.gl/sun": ["@math.gl/sun@4.1.0", "", {}, "sha512-i3q6OCBLSZ5wgZVhXg+X7gsjY/TUtuFW/2KBiq/U1ypLso3S4sEykoU/MGjxUv1xiiGtr+v8TeMbO1OBIh/HmA=="], @@ -668,6 +721,8 @@ "@noble/secp256k1": ["@noble/secp256k1@3.0.0", "", {}, "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg=="], + "@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -974,12 +1029,26 @@ "@trystero-p2p/nostr": ["@trystero-p2p/nostr@0.23.0", "", { "dependencies": { "@noble/secp256k1": "^3.0.0", "@trystero-p2p/core": "0.23.0" } }, "sha512-KSqUR2c1KVfv4zeErcntuegtyKzFTzNNiitIKGD0LiKA/4H3CeTF81ROk2h+X/PNvP4mv7Gp5eVxFYwfMu4Nrg=="], + "@turf/boolean-clockwise": ["@turf/boolean-clockwise@5.1.5", "", { "dependencies": { "@turf/helpers": "^5.1.5", "@turf/invariant": "^5.1.5" } }, "sha512-FqbmEEOJ4rU4/2t7FKx0HUWmjFEVqR+NJrFP7ymGSjja2SQ7Q91nnBihGuT+yuHHl6ElMjQ3ttsB/eTmyCycxA=="], + + "@turf/clone": ["@turf/clone@5.1.5", "", { "dependencies": { "@turf/helpers": "^5.1.5" } }, "sha512-//pITsQ8xUdcQ9pVb4JqXiSqG4dos5Q9N4sYFoWghX21tfOV2dhc5TGqYOhnHrQS7RiKQL1vQ48kIK34gQ5oRg=="], + + "@turf/helpers": ["@turf/helpers@5.1.5", "", {}, "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw=="], + + "@turf/invariant": ["@turf/invariant@5.2.0", "", { "dependencies": { "@turf/helpers": "^5.1.5" } }, "sha512-28RCBGvCYsajVkw2EydpzLdcYyhSA77LovuOvgCJplJWaNVyJYH6BOR3HR9w50MEkPqb/Vc/jdo6I6ermlRtQA=="], + + "@turf/meta": ["@turf/meta@5.2.0", "", { "dependencies": { "@turf/helpers": "^5.1.5" } }, "sha512-ZjQ3Ii62X9FjnK4hhdsbT+64AYRpaI8XMBMcyftEOGSmPMUVnkbvuv3C9geuElAXfQU7Zk1oWGOcrGOD9zr78Q=="], + + "@turf/rewind": ["@turf/rewind@5.1.5", "", { "dependencies": { "@turf/boolean-clockwise": "^5.1.5", "@turf/clone": "^5.1.5", "@turf/helpers": "^5.1.5", "@turf/invariant": "^5.1.5", "@turf/meta": "^5.1.5" } }, "sha512-Gdem7JXNu+G4hMllQHXRFRihJl3+pNl7qY+l4qhQFxq+hiU1cQoVFnyoleIqWKIrdK/i2YubaSwc3SCM7N5mMw=="], + "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], "@types/audioworklet": ["@types/audioworklet@0.0.97", "", {}, "sha512-z4RRI9147KXm3b1U4dR/XFfsxSR5yymEWEJT+Td0tCJyRkyGccIXEL09W7c5TcbDd51qKR/+LrO2SPC1SL2RLA=="], + "@types/brotli": ["@types/brotli@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-9xoNr+bcxT236/7ZgcWw/6Pb2RRetE13p4bFy1xYSckKwyOiRfmInay8baUWZgH7/284Wl6IPe7+nOI9+OQg/A=="], + "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], "@types/clone": ["@types/clone@0.1.30", "", {}, "sha512-vcxBr+ybljeSiasmdke1cQ9ICxoEwaBgM1OQ/P5h4MPj/kRyLcDl5L8PrftlbyV1kBbJIs3M3x1A1+rcWd4mEA=="], @@ -990,6 +1059,8 @@ "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + "@types/crypto-js": ["@types/crypto-js@4.2.2", "", {}, "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="], + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], @@ -1054,6 +1125,8 @@ "@types/p5": ["@types/p5@1.7.7", "", {}, "sha512-WFuP7jqc5CkkMtCK/NphgvMnJz1Qi9CMuK7t6xLu/tuXkRdGQA4q4AD0dUYcChC0Oibe8PE8gbKSFPNF0BqVNw=="], + "@types/pako": ["@types/pako@1.0.7", "", {}, "sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A=="], + "@types/readable-stream": ["@types/readable-stream@4.0.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig=="], "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], @@ -1296,12 +1369,16 @@ "broker-factory": ["broker-factory@3.1.13", "", { "dependencies": { "@babel/runtime": "^7.28.6", "fast-unique-numbers": "^9.0.26", "tslib": "^2.8.1", "worker-factory": "^7.0.48" } }, "sha512-H2VALe31mEtO/SRcNp4cUU5BAm1biwhc/JaF77AigUuni/1YT0FLCJfbUxwIEs9y6Kssjk2fmXgf+Y9ALvmKlw=="], + "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], + "browser-or-node": ["browser-or-node@2.1.1", "", {}, "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg=="], "browser-stdout": ["browser-stdout@1.3.1", "", {}, "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "buf-compare": ["buf-compare@1.0.1", "", {}, "sha512-Bvx4xH00qweepGc43xFvMs5BKASXTbHaHm6+kDYIK9p/4iFwjATQkmPKHQSgJZzKbAymhztRbXUf1Nqhzl73/Q=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "buffer-alloc": ["buffer-alloc@1.2.0", "", { "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" } }, "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow=="], @@ -1426,6 +1503,8 @@ "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + "core-assert": ["core-assert@0.2.1", "", { "dependencies": { "buf-compare": "^1.0.0", "is-error": "^2.2.0" } }, "sha512-IG97qShIP+nrJCXMCgkNZgH7jZQ4n8RpPyPeXX++T6avR/KhLhgLiHKoEn5Rc1KjfycSfA9DMa6m+4C4eguHhw=="], + "core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="], "core-js-compat": ["core-js-compat@3.48.0", "", { "dependencies": { "browserslist": "^4.28.1" } }, "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q=="], @@ -1514,6 +1593,8 @@ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "deep-strict-equal": ["deep-strict-equal@0.2.0", "", { "dependencies": { "core-assert": "^0.2.0" } }, "sha512-3daSWyvZ/zwJvuMGlzG1O+Ow0YSadGfb3jsh9xoCutv2tWyB9dA4YvR9L9/fSdDZa2dByYQe+TqapSGUrjnkoA=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], @@ -1548,6 +1629,8 @@ "dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="], + "draco3d": ["draco3d@1.5.7", "", {}, "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "earcut": ["earcut@2.2.4", "", {}, "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="], @@ -1694,6 +1777,10 @@ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="], + + "fast-xml-parser": ["fast-xml-parser@5.8.0", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.2.0", "path-expression-matcher": "^1.5.0", "strnum": "^2.3.0", "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg=="], + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fbjs": ["fbjs@0.8.18", "", { "dependencies": { "core-js": "^1.0.0", "isomorphic-fetch": "^2.1.1", "loose-envify": "^1.0.0", "object-assign": "^4.1.0", "promise": "^7.1.1", "setimmediate": "^1.0.5", "ua-parser-js": "^0.7.30" } }, "sha512-EQaWFK+fEPSoibjNy8IxUtaFOMXcWsY0JaVrQoZR9zC8N2Ygf9iDITPWjUTVIax95b6I742JFLqASHfsag/vKA=="], @@ -1822,6 +1909,8 @@ "gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="], + "h3-js": ["h3-js@4.4.0", "", {}, "sha512-DvJh07MhGgY2KcC4OeZc8SSyA+ZXpdvoh6uCzGpoKvWtZxJB+g6VXXC1+eWYkaMIsLz7J/ErhOalHCpcs1KYog=="], + "hammerjs": ["hammerjs@2.0.8", "", {}, "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ=="], "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], @@ -1872,6 +1961,8 @@ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "image-size": ["image-size@0.7.5", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g=="], + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -1922,6 +2013,8 @@ "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + "is-error": ["is-error@2.2.2", "", {}, "sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], @@ -2062,6 +2155,8 @@ "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], + "ktx-parse": ["ktx-parse@0.7.1", "", {}, "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ=="], + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -2136,7 +2231,7 @@ "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], - "long": ["long@4.0.0", "", {}, "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="], + "long": ["long@3.2.0", "", {}, "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -2148,6 +2243,8 @@ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "lz4js": ["lz4js@0.2.0", "", {}, "sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg=="], + "machine": ["machine@file:src/assets/vasm", {}], "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], @@ -2320,6 +2417,8 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -2334,6 +2433,8 @@ "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + "pbf": ["pbf@3.3.0", "", { "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q=="], + "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -2390,6 +2491,8 @@ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "protocol-buffers-schema": ["protocol-buffers-schema@3.6.1", "", {}, "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -2466,6 +2569,8 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], @@ -2564,6 +2669,8 @@ "smob": ["smob@1.5.0", "", {}, "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig=="], + "snappyjs": ["snappyjs@0.6.1", "", {}, "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg=="], + "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], "sort-array": ["sort-array@5.1.1", "", { "dependencies": { "array-back": "^6.2.2", "typical": "^7.1.1" }, "peerDependencies": { "@75lb/nature": "^0.1.1" }, "optionalPeers": ["@75lb/nature"] }, "sha512-EltS7AIsNlAFIM9cayrgKrM6XP94ATWwXP4LCL4IQbvbYhELSt2hZTrixg+AaQwnWFs/JGJgqU3rxMcNNWxGAA=="], @@ -2642,6 +2749,8 @@ "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], + "strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], + "style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="], "style-to-object": ["style-to-object@1.0.9", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw=="], @@ -2702,6 +2811,8 @@ "textmode.synth.js": ["textmode.synth.js@1.5.1", "", { "dependencies": { "textmode.js": ">=0.9.1" } }, "sha512-mNeoM3WiEc4Y6i13mlbhqQPgxpLTiGXUgQ9nTYWq4AxyajCl1vWuzaYtA7c6Waw+Xpp6xDbMruIUzw//NsPF4g=="], + "texture-compressor": ["texture-compressor@1.0.2", "", { "dependencies": { "argparse": "^1.0.10", "image-size": "^0.7.4" }, "bin": { "texture-compressor": "./bin/texture-compressor.js" } }, "sha512-dStVgoaQ11mA5htJ+RzZ51ZxIZqNOgWKAIvtjLrW1AliQQLCmrDqNzQZ8Jh91YealQ95DXt4MEduLzJmbs6lig=="], + "thingies": ["thingies@2.5.0", "", { "peerDependencies": { "tslib": "^2" } }, "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw=="], "three": ["three@0.182.0", "", {}, "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ=="], @@ -3030,6 +3141,8 @@ "xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="], + "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], + "xmlcreate": ["xmlcreate@2.0.4", "", {}, "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -3054,6 +3167,8 @@ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zstd-codec": ["zstd-codec@0.1.5", "", {}, "sha512-v3fyjpK8S/dpY/X5WxqTK3IoCnp/ZOLxn144GZVlNUjtwAchzrVo03h+oMATFhCIiJ5KTr4V3vDQQYz4RU684g=="], + "@apideck/better-ajv-errors/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], @@ -3114,6 +3229,12 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@loaders.gl/3d-tiles/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "@loaders.gl/compression/fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="], + + "@loaders.gl/compression/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "@luma.gl/webgl/@luma.gl/constants": ["@luma.gl/constants@9.0.0", "", {}, "sha512-0oVddffzzkYeHs2CKwehezQ0R5IkqM9Jq2FVYdnOAa02rAtqytWwvmhn4zs0Q/kYtHVn51OnkqIcB1vk5MsLJA=="], "@poppinss/dumper/supports-color": ["supports-color@10.0.0", "", {}, "sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ=="], @@ -3156,6 +3277,8 @@ "@tensorflow/tfjs-backend-webgl/@types/offscreencanvas": ["@types/offscreencanvas@2019.3.0", "", {}, "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q=="], + "@tensorflow/tfjs-core/long": ["long@4.0.0", "", {}, "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="], + "@tensorflow/tfjs-core/node-fetch": ["node-fetch@2.6.13", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA=="], "@tensorflow/tfjs-data/node-fetch": ["node-fetch@2.6.13", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA=="], diff --git a/ui/package.json b/ui/package.json index 45bdb1576..77b5be2b0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -89,7 +89,10 @@ "@codemirror/view": "^6.38.1", "@csound/browser": "file:./packages/csound-browser", "@deck.gl/core": "9.0.0", + "@deck.gl/extensions": "9.0.0", + "@deck.gl/geo-layers": "9.0.0", "@deck.gl/layers": "9.0.0", + "@deck.gl/mesh-layers": "9.0.0", "@elemaudio/core": "^4.0.1", "@elemaudio/web-renderer": "^4.0.3", "@google/genai": "^1.11.0", @@ -98,6 +101,7 @@ "@lezer/highlight": "^1.2.1", "@luma.gl/core": "9.0.0", "@luma.gl/engine": "9.0.0", + "@luma.gl/gltf": "9.0.0", "@luma.gl/shadertools": "9.0.0", "@luma.gl/webgl": "9.0.0", "@mediapipe/tasks-vision": "0.10.33", @@ -183,6 +187,7 @@ "overrides": { "@luma.gl/core": "9.0.0", "@luma.gl/engine": "9.0.0", + "@luma.gl/gltf": "9.0.0", "@luma.gl/shadertools": "9.0.0", "@luma.gl/webgl": "9.0.0" }, diff --git a/ui/src/lib/ai/object-prompts/deckgl.ts b/ui/src/lib/ai/object-prompts/deckgl.ts index 352989f61..6a0154d98 100644 --- a/ui/src/lib/ai/object-prompts/deckgl.ts +++ b/ui/src/lib/ai/object-prompts/deckgl.ts @@ -2,11 +2,13 @@ export const deckglPrompt = `## deckgl Object Instructions deckgl renders deck.gl data visualization layers into the Patchies video pipeline. Use it for point maps, lines, arcs, polygons, geospatial visualization, and data-driven visual layers. -Define a getLayers({ time, viewState, mouse }) function that returns deck.gl layers. Common layer classes are available as globals: ScatterplotLayer, GeoJsonLayer, LineLayer, ArcLayer, PolygonLayer, TextLayer, BitmapLayer. +Define a getLayers({ time, viewState, mouse }) function that returns deck.gl layers. Common layer classes are available as globals: ScatterplotLayer, GeoJsonLayer, LineLayer, ArcLayer, PolygonLayer, TextLayer, BitmapLayer, TileLayer. Example: \`\`\`js +noInteract() + const data = [ { position: [-122.45, 37.78], color: [255, 90, 70] }, { position: [-122.42, 37.76], color: [80, 180, 255] } diff --git a/ui/src/lib/canvas/constants.ts b/ui/src/lib/canvas/constants.ts index c89c3e42a..a1f234826 100644 --- a/ui/src/lib/canvas/constants.ts +++ b/ui/src/lib/canvas/constants.ts @@ -297,7 +297,9 @@ function render(time) { draw({ time }) }`; -export const DEFAULT_DECKGL_CODE = `const data = [ +export const DEFAULT_DECKGL_CODE = `noInteract() + +const data = [ { position: [-122.45, 37.78], color: [255, 90, 70] }, { position: [-122.42, 37.76], color: [80, 180, 255] }, { position: [-122.48, 37.74], color: [255, 210, 80] } diff --git a/ui/src/lib/components/nodes/DeckGLNode.svelte b/ui/src/lib/components/nodes/DeckGLNode.svelte index 0278f57fe..4453435c3 100644 --- a/ui/src/lib/components/nodes/DeckGLNode.svelte +++ b/ui/src/lib/components/nodes/DeckGLNode.svelte @@ -81,9 +81,9 @@ let mouseHandler: CanvasMouseHandler | null = null; let previewCanvas = $state(); let previewBitmapContext: ImageBitmapRenderingContext; - let dragEnabled = $state(true); - let panEnabled = $state(true); - let wheelEnabled = $state(true); + let dragEnabled = $state(false); + let panEnabled = $state(false); + let wheelEnabled = $state(false); let videoOutputEnabled = $state(true); let editorReady = $state(false); diff --git a/ui/src/lib/extensions/pack-icons.ts b/ui/src/lib/extensions/pack-icons.ts index 7ee8b0b9f..65605b6ef 100644 --- a/ui/src/lib/extensions/pack-icons.ts +++ b/ui/src/lib/extensions/pack-icons.ts @@ -24,6 +24,7 @@ import { Calculator, FileHeadphone, Grid3x3, + Map, Usb, Waypoints } from '@lucide/svelte/icons'; @@ -58,6 +59,7 @@ export function getPackIcon(iconName: string) { .with('Calculator', () => Calculator) .with('FileHeadphone', () => FileHeadphone) .with('Grid3x3', () => Grid3x3) + .with('Map', () => Map) .with('Usb', () => Usb) .with('Eye', () => Eye) .with('Waypoints', () => Waypoints) diff --git a/ui/src/lib/extensions/preset-packs.ts b/ui/src/lib/extensions/preset-packs.ts index 6bed0cc87..85d3d31d8 100644 --- a/ui/src/lib/extensions/preset-packs.ts +++ b/ui/src/lib/extensions/preset-packs.ts @@ -300,6 +300,14 @@ export const BUILT_IN_PRESET_PACKS: PresetPack[] = [ 'Torus Position Field' ] }, + { + id: 'deckgl-maps', + name: 'DeckGL Maps', + description: 'Geospatial visualization layers rendered into the video pipeline', + icon: 'Map', + requiredObjects: ['deckgl'], + presets: ['osm.deckgl'] + }, { id: 'tone-presets', name: 'Tone.js Presets', diff --git a/ui/src/lib/presets/builtin/deckgl.presets.ts b/ui/src/lib/presets/builtin/deckgl.presets.ts new file mode 100644 index 000000000..ba7825b76 --- /dev/null +++ b/ui/src/lib/presets/builtin/deckgl.presets.ts @@ -0,0 +1,62 @@ +const OSM_TILES_DECKGL = `noInteract() + +setTitle('OSM Tiles') +setViewState({ + longitude: -122.44, + latitude: 37.76, + zoom: 11, + pitch: 0, + bearing: 0 +}) + +function getTileBounds(tile) { + if (tile.boundingBox) { + const [[west, south], [east, north]] = tile.boundingBox + return [west, south, east, north] + } + + const { west, south, east, north } = tile.bbox + return [west, south, east, north] +} + +function getLayers() { + return [ + new TileLayer({ + id: 'osm-tiles', + data: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', + minZoom: 0, + maxZoom: 19, + tileSize: 256, + renderSubLayers: props => new BitmapLayer(props, { + data: null, + image: props.data, + bounds: getTileBounds(props.tile) + }) + }) + ] +}`; + +type DeckGLPresetData = { + code: string; + messageInletCount?: number; + messageOutletCount?: number; + videoInletCount?: number; + videoOutletCount?: number; +}; + +export const DECKGL_PRESETS: Record< + string, + { type: 'deckgl'; description?: string; data: DeckGLPresetData } +> = { + 'osm.deckgl': { + type: 'deckgl', + description: 'OpenStreetMap raster tiles rendered through deck.gl TileLayer', + data: { + code: OSM_TILES_DECKGL.trim(), + messageInletCount: 1, + messageOutletCount: 0, + videoInletCount: 0, + videoOutletCount: 1 + } + } +}; diff --git a/ui/src/lib/presets/builtin/index.ts b/ui/src/lib/presets/builtin/index.ts index fa4baa917..85c68d1c4 100644 --- a/ui/src/lib/presets/builtin/index.ts +++ b/ui/src/lib/presets/builtin/index.ts @@ -29,6 +29,7 @@ import { SWGL_PRESETS } from './swgl.presets'; import { SHADERPARK_PRESETS } from './shaderpark.presets'; import { FLOAT_TEXTURE_PRESETS } from './float-texture.presets'; import { SURFACE_PRESETS } from './surface.presets'; +import { DECKGL_PRESETS } from './deckgl.presets'; // Re-export individual preset collections export { @@ -59,7 +60,8 @@ export { SWGL_PRESETS, SHADERPARK_PRESETS, FLOAT_TEXTURE_PRESETS, - SURFACE_PRESETS + SURFACE_PRESETS, + DECKGL_PRESETS }; /** @@ -97,5 +99,6 @@ export const BUILTIN_PRESETS: Record< ...SWGL_PRESETS, ...SHADERPARK_PRESETS, ...FLOAT_TEXTURE_PRESETS, - ...SURFACE_PRESETS + ...SURFACE_PRESETS, + ...DECKGL_PRESETS }; diff --git a/ui/src/workers/rendering/deckglRenderer.ts b/ui/src/workers/rendering/deckglRenderer.ts index 0b93efddb..fefb13945 100644 --- a/ui/src/workers/rendering/deckglRenderer.ts +++ b/ui/src/workers/rendering/deckglRenderer.ts @@ -76,6 +76,7 @@ type DeckDevice = { export class DeckGLRenderer extends BaseWorkerRenderer { private DeckClass: typeof import('@deck.gl/core').Deck | null = null; private layerClasses: typeof import('@deck.gl/layers') | null = null; + private geoLayerClasses: typeof import('@deck.gl/geo-layers') | null = null; private deck: Deck | null = null; private device: DeckDevice | null = null; @@ -111,6 +112,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { const deckModule = await import('@deck.gl/core'); instance.layerClasses = await import('@deck.gl/layers'); + instance.geoLayerClasses = await import('@deck.gl/geo-layers'); instance.DeckClass = deckModule.Deck; @@ -159,9 +161,10 @@ export class DeckGLRenderer extends BaseWorkerRenderer { async updateCode(): Promise { this.resetState(); + this.setInteraction('interact', false); this.setPortCount(1, 0); - if (!this.DeckClass || !this.layerClasses) return; + if (!this.DeckClass || !this.layerClasses || !this.geoLayerClasses) return; try { const setViewState = (viewState: Partial) => { @@ -172,6 +175,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { ...this.buildBaseExtraContext(), Deck: this.DeckClass, ...this.layerClasses, + ...this.geoLayerClasses, viewState: this.viewState, setViewState }; diff --git a/ui/src/workers/rendering/fboRenderer.ts b/ui/src/workers/rendering/fboRenderer.ts index d290ccc3d..ed6313761 100644 --- a/ui/src/workers/rendering/fboRenderer.ts +++ b/ui/src/workers/rendering/fboRenderer.ts @@ -1677,8 +1677,9 @@ export class FBORenderer { userUniformParams = textureArray; } - // Get mouse data for this node (defaults to [0, 0, 0, 0]) - const mouseData = this.mouseDataByNode.get(node.id) ?? [0, 0, 0, 0]; + // Get mouse data for this node. Negative zw follows the Shadertoy-style + // "not pressed" convention used by CanvasMouseHandler. + const mouseData = this.mouseDataByNode.get(node.id) ?? [0, 0, -1, -1, 0]; // Render to FBO // Use transport time if available, otherwise fall back to local time diff --git a/ui/static/content/objects/deckgl.md b/ui/static/content/objects/deckgl.md index 0bf752e16..2f773260a 100644 --- a/ui/static/content/objects/deckgl.md +++ b/ui/static/content/objects/deckgl.md @@ -10,6 +10,8 @@ to `bg.out` and other video objects. Define a `getLayers()` function that returns deck.gl layers. ```js +noInteract() + const data = [ { position: [-122.45, 37.78], color: [255, 90, 70] }, { position: [-122.42, 37.76], color: [80, 180, 255] } @@ -33,6 +35,7 @@ function getLayers({ time, viewState, mouse }) { - `Deck` - deck.gl Deck class - `ScatterplotLayer`, `GeoJsonLayer`, `LineLayer`, `ArcLayer`, `PolygonLayer`, `TextLayer`, `BitmapLayer` - common deck.gl layers +- `TileLayer` - tiled raster/vector data, useful for OSM-style map tiles - `viewState` - current camera state - `setViewState(value)` - replace camera state - `mouse` - forwarded mouse position @@ -40,7 +43,28 @@ function getLayers({ time, viewState, mouse }) { ## Interaction Drag the preview or a fullscreen `surface` to pan. Use the mouse wheel or pinch -gesture to zoom. +gesture to zoom. `deckgl` disables Patchies canvas drag/pan/wheel interaction by +default so those gestures control the deck.gl view instead of moving the object. + +## OSM Tiles + +Use `TileLayer` with `BitmapLayer` to render OpenStreetMap raster tiles. + +```js +new TileLayer({ + id: 'osm-tiles', + data: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', + renderSubLayers: props => { + const [[west, south], [east, north]] = props.tile.boundingBox + + return new BitmapLayer(props, { + data: null, + image: props.data, + bounds: [west, south, east, north] + }) + } +}) +``` ## See Also From a8c613ed0e599afeffe91971df66a9590315419c Mon Sep 17 00:00:00 2001 From: Phoomparin Mano Date: Sun, 24 May 2026 15:47:58 +0700 Subject: [PATCH 04/12] feat(deckgl): add setViewState completions --- ui/src/lib/codemirror/patchies-completions.test.ts | 5 +++++ ui/src/lib/codemirror/patchies-completions.ts | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/ui/src/lib/codemirror/patchies-completions.test.ts b/ui/src/lib/codemirror/patchies-completions.test.ts index 45a5fdaff..0ac92e2b5 100644 --- a/ui/src/lib/codemirror/patchies-completions.test.ts +++ b/ui/src/lib/codemirror/patchies-completions.test.ts @@ -121,6 +121,11 @@ describe('patchies completions', () => { expect(getCompletionLabels('js', 'setS')).not.toContain('setSize'); }); + it('shows setViewState only for deckgl code', () => { + expect(getCompletionLabels('deckgl', 'setV')).toContain('setViewState'); + expect(getCompletionLabels('three', 'setV')).not.toContain('setViewState'); + }); + it('shows Shader Park completions only for shaderpark code', () => { expect(getShaderParkCompletionLabels('shaderpark', 'sp')).toContain('sphere'); expect(getShaderParkCompletionLabels('shaderpark', 'setSpace(getS')).toContain('getSpace'); diff --git a/ui/src/lib/codemirror/patchies-completions.ts b/ui/src/lib/codemirror/patchies-completions.ts index 5367fa166..83cc1ea57 100644 --- a/ui/src/lib/codemirror/patchies-completions.ts +++ b/ui/src/lib/codemirror/patchies-completions.ts @@ -171,6 +171,14 @@ const patchiesAPICompletions: Completion[] = [ info: "Set output FBO resolution. Pass a number for square (256), two numbers for rectangular (512, 256), or a string fraction ('1/2', '1/4').", apply: 'setResolution(256)' }, + { + label: 'setViewState', + type: 'function', + detail: '(viewState: Partial<{ longitude, latitude, zoom, pitch, bearing }>) => void', + info: 'Set the deck.gl camera view state for deckgl nodes.', + apply: + 'setViewState({\n longitude: -122.44,\n latitude: 37.76,\n zoom: 11,\n pitch: 0,\n bearing: 0\n})' + }, { label: 'setPrimaryButton', type: 'function', @@ -621,6 +629,7 @@ const nodeSpecificFunctions: Record = { 'htmlCanvas.glslLayer': ['dom', 'vue'], setTextureFormat: ['hydra', 'canvas', 'three', 'deckgl', 'regl', 'swgl', 'textmode'], setResolution: ['hydra', 'canvas', 'three', 'deckgl', 'regl', 'swgl', 'textmode'], + setViewState: ['deckgl'], setPrimaryButton: ['js', 'worker', 'p5', 'hydra', 'canvas', 'regl', 'swgl', 'textmode', 'three'], setVideoCount: ['hydra', 'regl', 'swgl', 'three', 'worker'], getTexture: ['hydra', 'regl', 'swgl', 'three'], From f786e7490fba3d93baa73e7dbad9e5b5e0c2e92f Mon Sep 17 00:00:00 2001 From: Phoomparin Mano Date: Sun, 24 May 2026 15:57:47 +0700 Subject: [PATCH 05/12] feat(deckgl): add setDeckInteraction --- .../specs/147-deckgl-fbo-renderer.md | 3 ++ ui/src/lib/ai/object-prompts/deckgl.ts | 10 +++++ ui/src/lib/codemirror/patchies-completions.ts | 8 ++++ ui/src/workers/rendering/deckglRenderer.ts | 37 ++++++++++++------- ui/static/content/objects/deckgl.md | 5 +++ 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/docs/design-docs/specs/147-deckgl-fbo-renderer.md b/docs/design-docs/specs/147-deckgl-fbo-renderer.md index 10bf8033b..81001ef2c 100644 --- a/docs/design-docs/specs/147-deckgl-fbo-renderer.md +++ b/docs/design-docs/specs/147-deckgl-fbo-renderer.md @@ -63,6 +63,9 @@ function getLayers() { - Left-drag pans longitude/latitude. - Wheel changes zoom. - The surface object forwards pointer and wheel input to `deckgl` the same way it does for worker `three`. +- User code can call `setDeckInteraction(false)` to disable Patchies' + built-in deck camera pan/zoom while still rendering layers from a fixed or + programmatically controlled `viewState`. ## Non-goals diff --git a/ui/src/lib/ai/object-prompts/deckgl.ts b/ui/src/lib/ai/object-prompts/deckgl.ts index 6a0154d98..9354a6221 100644 --- a/ui/src/lib/ai/object-prompts/deckgl.ts +++ b/ui/src/lib/ai/object-prompts/deckgl.ts @@ -4,6 +4,8 @@ deckgl renders deck.gl data visualization layers into the Patchies video pipelin Define a getLayers({ time, viewState, mouse }) function that returns deck.gl layers. Common layer classes are available as globals: ScatterplotLayer, GeoJsonLayer, LineLayer, ArcLayer, PolygonLayer, TextLayer, BitmapLayer, TileLayer. +Use setDeckInteraction(false) when the camera should stay fixed instead of responding to forwarded mouse drag/wheel input. Use setViewState({ longitude, latitude, zoom, pitch, bearing }) to set the camera explicitly. + Example: \`\`\`js @@ -14,6 +16,14 @@ const data = [ { position: [-122.42, 37.76], color: [80, 180, 255] } ] +setViewState({ + longitude: -122.44, + latitude: 37.76, + zoom: 11, + pitch: 45, + bearing: 0 +}) + function getLayers({ time }) { return [ new ScatterplotLayer({ diff --git a/ui/src/lib/codemirror/patchies-completions.ts b/ui/src/lib/codemirror/patchies-completions.ts index 83cc1ea57..552df35d3 100644 --- a/ui/src/lib/codemirror/patchies-completions.ts +++ b/ui/src/lib/codemirror/patchies-completions.ts @@ -179,6 +179,13 @@ const patchiesAPICompletions: Completion[] = [ apply: 'setViewState({\n longitude: -122.44,\n latitude: 37.76,\n zoom: 11,\n pitch: 0,\n bearing: 0\n})' }, + { + label: 'setDeckInteraction', + type: 'function', + detail: '(enabled: boolean) => void', + info: 'Enable or disable Patchies-provided deckgl camera pan and zoom controls.', + apply: 'setDeckInteraction(false)' + }, { label: 'setPrimaryButton', type: 'function', @@ -630,6 +637,7 @@ const nodeSpecificFunctions: Record = { setTextureFormat: ['hydra', 'canvas', 'three', 'deckgl', 'regl', 'swgl', 'textmode'], setResolution: ['hydra', 'canvas', 'three', 'deckgl', 'regl', 'swgl', 'textmode'], setViewState: ['deckgl'], + setDeckInteraction: ['deckgl'], setPrimaryButton: ['js', 'worker', 'p5', 'hydra', 'canvas', 'regl', 'swgl', 'textmode', 'three'], setVideoCount: ['hydra', 'regl', 'swgl', 'three', 'worker'], getTexture: ['hydra', 'regl', 'swgl', 'three'], diff --git a/ui/src/workers/rendering/deckglRenderer.ts b/ui/src/workers/rendering/deckglRenderer.ts index fefb13945..81e90d0f4 100644 --- a/ui/src/workers/rendering/deckglRenderer.ts +++ b/ui/src/workers/rendering/deckglRenderer.ts @@ -85,6 +85,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { private getLayers: UserGetLayers | null = null; private previousPointer: { x: number; y: number; down: boolean } | null = null; private pendingWheelDelta = 0; + private deckInteractionEnabled = true; private viewState: DeckViewState = { longitude: -122.44, @@ -131,8 +132,14 @@ export class DeckGLRenderer extends BaseWorkerRenderer { this.mouseX = params.mouseX; this.mouseY = params.mouseY; - this.updateViewStateFromPointer(params); - this.updateViewStateFromWheel(); + if (this.deckInteractionEnabled) { + this.updateViewStateFromPointer(params); + this.updateViewStateFromWheel(); + } else { + this.previousPointer = null; + this.pendingWheelDelta = 0; + } + this.ensureRenderTargetSize(); let layers: Layer[] = []; @@ -148,11 +155,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { return; } - this.deck.setProps({ - viewState: this.viewState, - layers - }); - + this.deck.setProps({ viewState: this.viewState, layers }); this.deck.redraw('patchies'); this.blitToReglFramebuffer(); @@ -161,7 +164,10 @@ export class DeckGLRenderer extends BaseWorkerRenderer { async updateCode(): Promise { this.resetState(); + this.setInteraction('interact', false); + this.deckInteractionEnabled = true; + this.setPortCount(1, 0); if (!this.DeckClass || !this.layerClasses || !this.geoLayerClasses) return; @@ -171,13 +177,18 @@ export class DeckGLRenderer extends BaseWorkerRenderer { this.viewState = { ...this.viewState, ...viewState }; }; + const setDeckInteraction = (enabled: boolean) => { + this.deckInteractionEnabled = enabled; + }; + const extraContext = { ...this.buildBaseExtraContext(), Deck: this.DeckClass, ...this.layerClasses, ...this.geoLayerClasses, viewState: this.viewState, - setViewState + setViewState, + setDeckInteraction }; const codeWithWrapper = ` @@ -255,9 +266,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { canvas.parentElement ??= null; await new Promise((resolve) => { - const renderer = this; - - renderer.deck = new DeckClass({ + this.deck = new DeckClass({ controller: false, gl, width: null, @@ -269,10 +278,10 @@ export class DeckGLRenderer extends BaseWorkerRenderer { getTooltip: null, parameters: { depthCompare: 'less-equal' }, onDeviceInitialized: (device: unknown) => { - renderer.device = device as DeckDevice; - renderer.createRenderTarget(); + this.device = device as DeckDevice; + this.createRenderTarget(); - renderer.deck?.setProps({ _framebuffer: renderer.deckFramebuffer } as never); + this.deck?.setProps({ _framebuffer: this.deckFramebuffer } as never); resolve(); }, diff --git a/ui/static/content/objects/deckgl.md b/ui/static/content/objects/deckgl.md index 2f773260a..572c1d4c4 100644 --- a/ui/static/content/objects/deckgl.md +++ b/ui/static/content/objects/deckgl.md @@ -38,6 +38,8 @@ function getLayers({ time, viewState, mouse }) { - `TileLayer` - tiled raster/vector data, useful for OSM-style map tiles - `viewState` - current camera state - `setViewState(value)` - replace camera state +- `setDeckInteraction(enabled)` - enable or disable Patchies' built-in deck + camera pan/zoom controls - `mouse` - forwarded mouse position ## Interaction @@ -46,6 +48,9 @@ Drag the preview or a fullscreen `surface` to pan. Use the mouse wheel or pinch gesture to zoom. `deckgl` disables Patchies canvas drag/pan/wheel interaction by default so those gestures control the deck.gl view instead of moving the object. +Call `setDeckInteraction(false)` to keep the deck camera fixed unless your code +changes it with `setViewState()`. + ## OSM Tiles Use `TileLayer` with `BitmapLayer` to render OpenStreetMap raster tiles. From 98604f1f60f352491a08eb90d9f2151a88a43d33 Mon Sep 17 00:00:00 2001 From: Phoomparin Mano Date: Sun, 24 May 2026 16:06:31 +0700 Subject: [PATCH 06/12] feat(deckgl): add completion layers for deck.gl --- ui/src/lib/codemirror/deckgl-completions.ts | 213 ++++++++++++++++++ ui/src/lib/codemirror/hover-hints.ts | 8 + ui/src/lib/codemirror/language.ts | 7 + .../codemirror/patchies-completions.test.ts | 38 ++++ 4 files changed, 266 insertions(+) create mode 100644 ui/src/lib/codemirror/deckgl-completions.ts diff --git a/ui/src/lib/codemirror/deckgl-completions.ts b/ui/src/lib/codemirror/deckgl-completions.ts new file mode 100644 index 000000000..a0f9810b5 --- /dev/null +++ b/ui/src/lib/codemirror/deckgl-completions.ts @@ -0,0 +1,213 @@ +import { + CompletionContext as CMCompletionContext, + type Completion +} from '@codemirror/autocomplete'; +import { isCompletionSuppressedByComment } from '$lib/codemirror/completion-utils'; +import { isJavaScriptStringCompletionContext } from '$lib/codemirror/glsl-in-js'; +import type { PatchiesContext } from '$lib/codemirror/patchies-completions'; + +const DECKGL_NODE_TYPE = 'deckgl'; + +type DeckGLLayerCompletion = { + label: string; + detail: string; + info: string; + apply?: string; +}; + +const layerCompletions: DeckGLLayerCompletion[] = [ + { + label: 'ArcLayer', + detail: 'new ArcLayer(props)', + info: 'Render arcs between source and target coordinates.', + apply: + "new ArcLayer({\n id: 'arcs',\n data,\n getSourcePosition: d => d.source,\n getTargetPosition: d => d.target,\n getSourceColor: [255, 120, 0],\n getTargetColor: [0, 160, 255],\n getWidth: 4\n})" + }, + { + label: 'BitmapLayer', + detail: 'new BitmapLayer(props)', + info: 'Render an image or texture into geographic bounds.', + apply: "new BitmapLayer({\n id: 'bitmap',\n image,\n bounds: [west, south, east, north]\n})" + }, + { + label: 'ColumnLayer', + detail: 'new ColumnLayer(props)', + info: 'Render extruded columns at coordinates.', + apply: + "new ColumnLayer({\n id: 'columns',\n data,\n getPosition: d => d.position,\n getElevation: d => d.value,\n getFillColor: [80, 180, 255]\n})" + }, + { + label: 'GeoJsonLayer', + detail: 'new GeoJsonLayer(props)', + info: 'Render GeoJSON features as points, lines, and polygons.', + apply: + "new GeoJsonLayer({\n id: 'geojson',\n data,\n filled: true,\n stroked: true,\n getFillColor: [80, 180, 255, 120],\n getLineColor: [255, 255, 255]\n})" + }, + { + label: 'GreatCircleLayer', + detail: 'new GreatCircleLayer(props)', + info: 'Render great-circle arcs between geographic points.', + apply: + "new GreatCircleLayer({\n id: 'great-circles',\n data,\n getSourcePosition: d => d.source,\n getTargetPosition: d => d.target,\n getWidth: 2\n})" + }, + { + label: 'GridCellLayer', + detail: 'new GridCellLayer(props)', + info: 'Render extruded grid cells from cell-center coordinates.', + apply: + "new GridCellLayer({\n id: 'grid-cells',\n data,\n getPosition: d => d.position,\n getElevation: d => d.value,\n getFillColor: [255, 180, 80]\n})" + }, + { + label: 'H3ClusterLayer', + detail: 'new H3ClusterLayer(props)', + info: 'Render H3 hexagon clusters.', + apply: + "new H3ClusterLayer({\n id: 'h3-clusters',\n data,\n getHexagons: d => d.hexagons,\n getFillColor: [80, 180, 255]\n})" + }, + { + label: 'H3HexagonLayer', + detail: 'new H3HexagonLayer(props)', + info: 'Render H3 hexagon cells.', + apply: + "new H3HexagonLayer({\n id: 'h3-hexagons',\n data,\n getHexagon: d => d.hex,\n getFillColor: [80, 180, 255, 160]\n})" + }, + { + label: 'IconLayer', + detail: 'new IconLayer(props)', + info: 'Render billboarded icons at coordinates.', + apply: + "new IconLayer({\n id: 'icons',\n data,\n getPosition: d => d.position,\n getIcon: d => d.icon,\n getSize: 32\n})" + }, + { + label: 'LineLayer', + detail: 'new LineLayer(props)', + info: 'Render line segments between source and target positions.', + apply: + "new LineLayer({\n id: 'lines',\n data,\n getSourcePosition: d => d.source,\n getTargetPosition: d => d.target,\n getColor: [255, 255, 255],\n getWidth: 2\n})" + }, + { + label: 'MVTLayer', + detail: 'new MVTLayer(props)', + info: 'Render Mapbox Vector Tile data.', + apply: + "new MVTLayer({\n id: 'mvt',\n data: 'https://example.com/{z}/{x}/{y}.mvt',\n getLineColor: [255, 255, 255],\n getFillColor: [80, 180, 255, 120]\n})" + }, + { + label: 'PathLayer', + detail: 'new PathLayer(props)', + info: 'Render paths from arrays of coordinates.', + apply: + "new PathLayer({\n id: 'paths',\n data,\n getPath: d => d.path,\n getColor: [255, 180, 80],\n getWidth: 4\n})" + }, + { + label: 'PointCloudLayer', + detail: 'new PointCloudLayer(props)', + info: 'Render 3D point clouds.', + apply: + "new PointCloudLayer({\n id: 'points3d',\n data,\n getPosition: d => d.position,\n getColor: d => d.color,\n pointSize: 2\n})" + }, + { + label: 'PolygonLayer', + detail: 'new PolygonLayer(props)', + info: 'Render filled and/or stroked polygons.', + apply: + "new PolygonLayer({\n id: 'polygons',\n data,\n getPolygon: d => d.polygon,\n getFillColor: [80, 180, 255, 120],\n getLineColor: [255, 255, 255]\n})" + }, + { + label: 'QuadkeyLayer', + detail: 'new QuadkeyLayer(props)', + info: 'Render cells identified by Bing Maps quadkeys.', + apply: + "new QuadkeyLayer({\n id: 'quadkeys',\n data,\n getQuadkey: d => d.quadkey,\n getFillColor: [80, 180, 255, 160]\n})" + }, + { + label: 'S2Layer', + detail: 'new S2Layer(props)', + info: 'Render S2 geometry cells.', + apply: + "new S2Layer({\n id: 's2',\n data,\n getS2Token: d => d.token,\n getFillColor: [80, 180, 255, 160]\n})" + }, + { + label: 'ScatterplotLayer', + detail: 'new ScatterplotLayer(props)', + info: 'Render circles at coordinates.', + apply: + "new ScatterplotLayer({\n id: 'points',\n data,\n getPosition: d => d.position,\n getRadius: 500,\n getFillColor: d => d.color,\n radiusUnits: 'meters'\n})" + }, + { + label: 'SolidPolygonLayer', + detail: 'new SolidPolygonLayer(props)', + info: 'Low-level filled polygon layer used by polygon-based layers.', + apply: + "new SolidPolygonLayer({\n id: 'solid-polygons',\n data,\n getPolygon: d => d.polygon,\n getFillColor: [80, 180, 255, 160]\n})" + }, + { + label: 'TerrainLayer', + detail: 'new TerrainLayer(props)', + info: 'Render terrain tiles from elevation and texture data.', + apply: + "new TerrainLayer({\n id: 'terrain',\n elevationData,\n texture,\n bounds: [west, south, east, north]\n})" + }, + { + label: 'TextLayer', + detail: 'new TextLayer(props)', + info: 'Render text labels at coordinates.', + apply: + "new TextLayer({\n id: 'labels',\n data,\n getPosition: d => d.position,\n getText: d => d.label,\n getSize: 16,\n getColor: [255, 255, 255]\n})" + }, + { + label: 'Tile3DLayer', + detail: 'new Tile3DLayer(props)', + info: 'Render 3D Tiles datasets.', + apply: "new Tile3DLayer({\n id: 'tiles-3d',\n data: 'https://example.com/tileset.json'\n})" + }, + { + label: 'TileLayer', + detail: 'new TileLayer(props)', + info: 'Render tiled raster or vector data, including OSM-style map tiles.', + apply: + "new TileLayer({\n id: 'tiles',\n data: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png',\n minZoom: 0,\n maxZoom: 19,\n renderSubLayers: props => {\n const [[west, south], [east, north]] = props.tile.boundingBox\n\n return new BitmapLayer(props, {\n data: null,\n image: props.data,\n bounds: [west, south, east, north]\n })\n }\n})" + }, + { + label: 'TripsLayer', + detail: 'new TripsLayer(props)', + info: 'Render animated paths with timestamps.', + apply: + "new TripsLayer({\n id: 'trips',\n data,\n getPath: d => d.path,\n getTimestamps: d => d.timestamps,\n currentTime: time\n})" + } +]; + +const deckglLayerCompletions: Completion[] = layerCompletions.map((completion) => ({ + type: 'class', + apply: `new ${completion.label}({\n id: '${completion.label}',\n data\n})`, + ...completion +})); + +export function createDeckGLCompletionSource(patchiesContext?: PatchiesContext) { + return (context: CMCompletionContext) => { + if (patchiesContext?.nodeType !== DECKGL_NODE_TYPE) return null; + if (isJavaScriptStringCompletionContext(context)) return null; + + const word = context.matchBefore(/\w*/); + if (!word) return null; + if (word.from === word.to && !context.explicit) return null; + if (isCompletionSuppressedByComment(context, word.from)) return null; + + const typedText = context.state.doc.sliceString(word.from, word.to).toLowerCase(); + const options = deckglLayerCompletions.filter( + (completion) => !typedText || completion.label.toLowerCase().startsWith(typedText) + ); + + return { + from: word.from, + options, + validFor: /^\w*$/ + }; + }; +} + +export const getDeckGLCompletionByLabel = (label: string): Completion | undefined => + deckglLayerCompletions.find((completion) => completion.label === label); + +export const deckglCompletionsSource = (context?: PatchiesContext) => + createDeckGLCompletionSource(context); diff --git a/ui/src/lib/codemirror/hover-hints.ts b/ui/src/lib/codemirror/hover-hints.ts index 48937f4ac..2f4ab2b2e 100644 --- a/ui/src/lib/codemirror/hover-hints.ts +++ b/ui/src/lib/codemirror/hover-hints.ts @@ -14,7 +14,11 @@ import { } from '$lib/codemirror/patchies-completions'; import { getHydraCompletionByLabel } from '$lib/codemirror/hydra-completions'; import { getShaderParkCompletionByLabel } from '$lib/codemirror/shaderpark-completions'; +<<<<<<< HEAD import { getPeppermintCompletionByLabel } from '$lib/codemirror/peppermint.codemirror'; +======= +import { getDeckGLCompletionByLabel } from '$lib/codemirror/deckgl-completions'; +>>>>>>> 31751603 (feat(deckgl): add completion layers for deck.gl) import type { SupportedLanguage } from '$lib/codemirror/types'; export interface CompletionHoverContext extends PatchiesContext { @@ -86,6 +90,10 @@ function getCompletionForWord( return getHydraCompletionByLabel(word) ?? getPatchiesCompletionByLabel(word, context); } + if (context.nodeType === 'deckgl') { + return getDeckGLCompletionByLabel(word) ?? getPatchiesCompletionByLabel(word, context); + } + return getPatchiesCompletionByLabel(word, context); }) .otherwise(() => undefined); diff --git a/ui/src/lib/codemirror/language.ts b/ui/src/lib/codemirror/language.ts index fcdbd7f7a..7bea82b95 100644 --- a/ui/src/lib/codemirror/language.ts +++ b/ui/src/lib/codemirror/language.ts @@ -50,8 +50,13 @@ export async function loadLanguageExtension( { patchiesCompletions }, { shaderParkCompletionsSource }, { hydraCompletionsSource }, +<<<<<<< HEAD { glslInJsCompletions }, { javascriptMixedWrap }, +======= + { deckglCompletionsSource }, + { glslInJsCompletions, glslInJsWrap }, +>>>>>>> 31751603 (feat(deckgl): add completion layers for deck.gl) { completionHoverHints }, { glslIncludeHighlighter } ] = await Promise.all([ @@ -61,6 +66,7 @@ export async function loadLanguageExtension( import('$lib/codemirror/patchies-completions'), import('$lib/codemirror/shaderpark-completions'), import('$lib/codemirror/hydra-completions'), + import('$lib/codemirror/deckgl-completions'), import('$lib/codemirror/glsl-in-js'), import('$lib/codemirror/javascript-mixed'), import('$lib/codemirror/hover-hints'), @@ -81,6 +87,7 @@ export async function loadLanguageExtension( glslInJsCompletions, shaderParkCompletionsSource(context), hydraCompletionsSource(context), + deckglCompletionsSource(context), patchiesCompletions(context) ] }) diff --git a/ui/src/lib/codemirror/patchies-completions.test.ts b/ui/src/lib/codemirror/patchies-completions.test.ts index 0ac92e2b5..a167a75e6 100644 --- a/ui/src/lib/codemirror/patchies-completions.test.ts +++ b/ui/src/lib/codemirror/patchies-completions.test.ts @@ -7,6 +7,7 @@ import { } from '$lib/codemirror/patchies-completions'; import { createHydraCompletionSource } from '$lib/codemirror/hydra-completions'; import { createShaderParkCompletionSource } from '$lib/codemirror/shaderpark-completions'; +import { createDeckGLCompletionSource } from '$lib/codemirror/deckgl-completions'; function getCompletionLabels(nodeType: string, doc: string) { const state = EditorState.create({ doc }); @@ -24,6 +25,14 @@ function getShaderParkCompletionLabels(nodeType: string, doc: string) { return result?.options.map((option) => option.label) ?? []; } +function getDeckGLCompletionLabels(nodeType: string, doc: string) { + const state = EditorState.create({ doc }); + const context = new CompletionContext(state, doc.length, true); + const result = createDeckGLCompletionSource({ nodeType })(context); + + return result?.options.map((option) => option.label) ?? []; +} + function getShaderParkCompletions(doc: string) { const state = EditorState.create({ doc }); const context = new CompletionContext(state, doc.length, true); @@ -126,6 +135,35 @@ describe('patchies completions', () => { expect(getCompletionLabels('three', 'setV')).not.toContain('setViewState'); }); + it('shows deckgl camera helper completions only for deckgl code', () => { + expect(getCompletionLabels('deckgl', 'setV')).toContain('setViewState'); + expect(getCompletionLabels('deckgl', 'setD')).toContain('setDeckInteraction'); + expect(getCompletionLabels('three', 'setV')).not.toContain('setViewState'); + expect(getCompletionLabels('three', 'setD')).not.toContain('setDeckInteraction'); + }); + + it('shows deck.gl layer completions only for deckgl code', () => { + expect(getDeckGLCompletionLabels('deckgl', 'Tile')).toEqual( + expect.arrayContaining(['TileLayer', 'Tile3DLayer']) + ); + expect(getDeckGLCompletionLabels('three', 'Tile')).toEqual([]); + }); + + it('includes common deck.gl layer completions', () => { + expect(getDeckGLCompletionLabels('deckgl', '')).toEqual( + expect.arrayContaining([ + 'ArcLayer', + 'BitmapLayer', + 'GeoJsonLayer', + 'LineLayer', + 'MVTLayer', + 'ScatterplotLayer', + 'TextLayer', + 'TileLayer' + ]) + ); + }); + it('shows Shader Park completions only for shaderpark code', () => { expect(getShaderParkCompletionLabels('shaderpark', 'sp')).toContain('sphere'); expect(getShaderParkCompletionLabels('shaderpark', 'setSpace(getS')).toContain('getSpace'); From d78bf0926a284ddf125eb514e7d5cb4571f6530c Mon Sep 17 00:00:00 2001 From: Phoomparin Mano Date: Sun, 24 May 2026 16:36:05 +0700 Subject: [PATCH 07/12] feat(deckgl): add onDeckHover and onDeckClick --- .../specs/147-deckgl-fbo-renderer.md | 29 +- ui/src/lib/ai/object-prompts/deckgl.ts | 2 + .../codemirror/patchies-completions.test.ts | 7 + ui/src/lib/codemirror/patchies-completions.ts | 16 + ui/src/workers/rendering/deckglRenderer.ts | 277 +++++++++++++++++- ui/static/content/objects/deckgl.md | 22 ++ 6 files changed, 331 insertions(+), 22 deletions(-) diff --git a/docs/design-docs/specs/147-deckgl-fbo-renderer.md b/docs/design-docs/specs/147-deckgl-fbo-renderer.md index 81001ef2c..9b317d57a 100644 --- a/docs/design-docs/specs/147-deckgl-fbo-renderer.md +++ b/docs/design-docs/specs/147-deckgl-fbo-renderer.md @@ -20,14 +20,14 @@ The object exposes deck.gl layer classes and expects user code to define `getLay function getLayers({ time, viewState, mouse }) { return [ new ScatterplotLayer({ - id: 'points', + id: "points", data, - getPosition: d => d.position, + getPosition: (d) => d.position, getRadius: 400, getFillColor: [255, 140, 40], - pickable: true - }) - ] + pickable: true, + }), + ]; } ``` @@ -39,8 +39,8 @@ slippy-map raster tiles: function getLayers() { return [ new TileLayer({ - data: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', - renderSubLayers: props => { + data: "https://c.tile.openstreetmap.org/{z}/{x}/{y}.png", + renderSubLayers: (props) => { const { boundingBox } = props.tile; return new BitmapLayer(props, { image: props.data, @@ -48,11 +48,11 @@ function getLayers() { boundingBox[0][0], boundingBox[0][1], boundingBox[1][0], - boundingBox[1][1] - ] + boundingBox[1][1], + ], }); - } - }) + }, + }), ]; } ``` @@ -66,6 +66,13 @@ function getLayers() { - User code can call `setDeckInteraction(false)` to disable Patchies' built-in deck camera pan/zoom while still rendering layers from a fixed or programmatically controlled `viewState`. +- User code can call `onDeckHover(callback)` and `onDeckClick(callback)` to + receive manual picking results from forwarded mouse input. Layers must opt in + with `pickable: true`. +- The worker patches deck.gl v9.0 picking blend parameters into legacy WebGL + `blendEquation`/`blendFunc` parameters because deck.gl's picking pass emits + `blendAlphaSrcFactor: 'constant-alpha'`, which the pinned luma.gl WebGL + adapter does not accept. ## Non-goals diff --git a/ui/src/lib/ai/object-prompts/deckgl.ts b/ui/src/lib/ai/object-prompts/deckgl.ts index 9354a6221..f7222113f 100644 --- a/ui/src/lib/ai/object-prompts/deckgl.ts +++ b/ui/src/lib/ai/object-prompts/deckgl.ts @@ -6,6 +6,8 @@ Define a getLayers({ time, viewState, mouse }) function that returns deck.gl lay Use setDeckInteraction(false) when the camera should stay fixed instead of responding to forwarded mouse drag/wheel input. Use setViewState({ longitude, latitude, zoom, pitch, bearing }) to set the camera explicitly. +Use onDeckHover(info => {}) and onDeckClick(info => {}) for picking callbacks. Layers must set pickable: true for picking to work. + Example: \`\`\`js diff --git a/ui/src/lib/codemirror/patchies-completions.test.ts b/ui/src/lib/codemirror/patchies-completions.test.ts index a167a75e6..518f3a2f6 100644 --- a/ui/src/lib/codemirror/patchies-completions.test.ts +++ b/ui/src/lib/codemirror/patchies-completions.test.ts @@ -142,6 +142,13 @@ describe('patchies completions', () => { expect(getCompletionLabels('three', 'setD')).not.toContain('setDeckInteraction'); }); + it('shows deckgl picking helper completions only for deckgl code', () => { + expect(getCompletionLabels('deckgl', 'onDeck')).toEqual( + expect.arrayContaining(['onDeckHover', 'onDeckClick']) + ); + expect(getCompletionLabels('three', 'onDeck')).toEqual([]); + }); + it('shows deck.gl layer completions only for deckgl code', () => { expect(getDeckGLCompletionLabels('deckgl', 'Tile')).toEqual( expect.arrayContaining(['TileLayer', 'Tile3DLayer']) diff --git a/ui/src/lib/codemirror/patchies-completions.ts b/ui/src/lib/codemirror/patchies-completions.ts index 552df35d3..23ee52455 100644 --- a/ui/src/lib/codemirror/patchies-completions.ts +++ b/ui/src/lib/codemirror/patchies-completions.ts @@ -82,6 +82,20 @@ const patchiesAPICompletions: Completion[] = [ info: 'Receive raw wheel events in worker three nodes. Event includes x, y, deltaX, deltaY, and deltaMode.', apply: 'onWheel(({ x, y, deltaY }) => {\n \n})' }, + { + label: 'onDeckHover', + type: 'function', + detail: '(callback: (info) => void) => () => void', + info: 'Register a callback for deckgl hover picking. Layers must set pickable: true. Receives null when leaving a picked object.', + apply: 'onDeckHover((info) => {\n \n})' + }, + { + label: 'onDeckClick', + type: 'function', + detail: '(callback: (info) => void) => () => void', + info: 'Register a callback for deckgl click picking. Layers must set pickable: true.', + apply: 'onDeckClick((info) => {\n \n})' + }, { label: 'onVideoFrame', type: 'function', @@ -644,6 +658,8 @@ const nodeSpecificFunctions: Record = { OrbitControls: ['three'], onPointerDrag: ['three'], onWheel: ['three'], + onDeckHover: ['deckgl'], + onDeckClick: ['deckgl'], onVideoFrame: ['worker'], getVideoFrames: ['worker'], getVfsUrl: [ diff --git a/ui/src/workers/rendering/deckglRenderer.ts b/ui/src/workers/rendering/deckglRenderer.ts index 81e90d0f4..f0543e453 100644 --- a/ui/src/workers/rendering/deckglRenderer.ts +++ b/ui/src/workers/rendering/deckglRenderer.ts @@ -1,5 +1,5 @@ import type regl from 'regl'; -import type { Deck, Layer } from '@deck.gl/core'; +import type { Deck, Layer, PickingInfo } from '@deck.gl/core'; import type { FBORenderer } from './fboRenderer'; import type { RenderParams } from '$lib/rendering/types'; import { CANVAS_WRAPPER_OFFSET } from '$lib/constants/error-reporting-offsets'; @@ -38,6 +38,42 @@ type UserGetLayers = (args: { mouse: WorkerMouseObject; }) => Layer[]; +type SerializablePickingInfo = { + picked: boolean; + index: number; + object: unknown; + x: number; + y: number; + coordinate?: number[]; + color?: number[]; + pixelRatio?: number; + layer?: { id: string } | null; + sourceLayer?: { id: string } | null; + viewport?: { id: string } | null; +}; + +type DeckPickingCallback = (info: SerializablePickingInfo | null) => void; + +type DeckPointerState = { + x: number; + y: number; + down: boolean; +}; + +type DeckPrivatePickingPass = { + getLayerParameters?: ( + layer: unknown, + layerIndex: number, + viewport: unknown + ) => Record; +}; + +type DeckWithPrivatePicker = Deck & { + deckPicker?: { + pickLayersPass?: DeckPrivatePickingPass; + }; +}; + type SizedFramebuffer = regl.Framebuffer2D & { width: number; height: number; @@ -83,7 +119,13 @@ export class DeckGLRenderer extends BaseWorkerRenderer { private deckTexture: DeckTexture | null = null; private deckFramebuffer: DeckFramebuffer | null = null; private getLayers: UserGetLayers | null = null; - private previousPointer: { x: number; y: number; down: boolean } | null = null; + private previousCameraPointer: DeckPointerState | null = null; + private previousPickingPointer: DeckPointerState | null = null; + private clickStartPointer: DeckPointerState | null = null; + private lastHoverKey: string | null = null; + private hoverCallbacks = new Set(); + private clickCallbacks = new Set(); + private patchedPickingPasses = new WeakSet(); private pendingWheelDelta = 0; private deckInteractionEnabled = true; @@ -136,7 +178,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { this.updateViewStateFromPointer(params); this.updateViewStateFromWheel(); } else { - this.previousPointer = null; + this.previousCameraPointer = null; this.pendingWheelDelta = 0; } @@ -157,6 +199,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { this.deck.setProps({ viewState: this.viewState, layers }); this.deck.redraw('patchies'); + this.updatePicking(params); this.blitToReglFramebuffer(); this.renderer.regl._refresh(); @@ -164,9 +207,14 @@ export class DeckGLRenderer extends BaseWorkerRenderer { async updateCode(): Promise { this.resetState(); - this.setInteraction('interact', false); + this.deckInteractionEnabled = true; + this.hoverCallbacks.clear(); + this.clickCallbacks.clear(); + this.previousPickingPointer = null; + this.clickStartPointer = null; + this.lastHoverKey = null; this.setPortCount(1, 0); @@ -181,6 +229,18 @@ export class DeckGLRenderer extends BaseWorkerRenderer { this.deckInteractionEnabled = enabled; }; + const onDeckHover = (callback: DeckPickingCallback) => { + this.hoverCallbacks.add(callback); + + return () => this.hoverCallbacks.delete(callback); + }; + + const onDeckClick = (callback: DeckPickingCallback) => { + this.clickCallbacks.add(callback); + + return () => this.clickCallbacks.delete(callback); + }; + const extraContext = { ...this.buildBaseExtraContext(), Deck: this.DeckClass, @@ -188,7 +248,9 @@ export class DeckGLRenderer extends BaseWorkerRenderer { ...this.geoLayerClasses, viewState: this.viewState, setViewState, - setDeckInteraction + setDeckInteraction, + onDeckHover, + onDeckClick }; const codeWithWrapper = ` @@ -282,6 +344,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { this.createRenderTarget(); this.deck?.setProps({ _framebuffer: this.deckFramebuffer } as never); + this.patchPickingBlendParameters(); resolve(); }, @@ -332,12 +395,11 @@ export class DeckGLRenderer extends BaseWorkerRenderer { } private updateViewStateFromPointer(params: RenderParams): void { - const down = params.mouseZ >= 0 && params.mouseW >= 0; - const pointer = { x: params.mouseX, y: params.mouseY, down }; + const pointer = this.getPointerState(params); - if (down && this.previousPointer?.down) { - const dx = pointer.x - this.previousPointer.x; - const dy = pointer.y - this.previousPointer.y; + if (pointer.down && this.previousCameraPointer?.down) { + const dx = pointer.x - this.previousCameraPointer.x; + const dy = pointer.y - this.previousCameraPointer.y; const zoomScale = Math.max(1, this.viewState.zoom); this.viewState = { @@ -347,7 +409,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { }; } - this.previousPointer = pointer; + this.previousCameraPointer = pointer; } private updateViewStateFromWheel(): void { @@ -362,6 +424,198 @@ export class DeckGLRenderer extends BaseWorkerRenderer { }; } + private patchPickingBlendParameters(): void { + const pickLayersPass = (this.deck as DeckWithPrivatePicker | null)?.deckPicker?.pickLayersPass; + const getLayerParameters = pickLayersPass?.getLayerParameters; + + if (!pickLayersPass || !getLayerParameters) return; + if (this.patchedPickingPasses.has(pickLayersPass)) return; + + this.patchedPickingPasses.add(pickLayersPass); + + pickLayersPass.getLayerParameters = (layer, layerIndex, viewport) => { + const parameters = getLayerParameters.call(pickLayersPass, layer, layerIndex, viewport); + + if (parameters.blendAlphaSrcFactor !== 'constant-alpha') return parameters; + + const legacyParameters = { ...parameters }; + delete legacyParameters.blendColorOperation; + delete legacyParameters.blendColorSrcFactor; + delete legacyParameters.blendColorDstFactor; + delete legacyParameters.blendAlphaOperation; + delete legacyParameters.blendAlphaSrcFactor; + delete legacyParameters.blendAlphaDstFactor; + + return { + ...legacyParameters, + blendEquation: [32774, 32774], + blendFunc: [1, 0, 32771, 0] + }; + }; + } + + private updatePicking(params: RenderParams): void { + if (!this.deck || (this.hoverCallbacks.size === 0 && this.clickCallbacks.size === 0)) { + this.previousPickingPointer = this.getPointerState(params); + return; + } + + const pointer = this.getPointerState(params); + const previousPointer = this.previousPickingPointer; + + if (pointer.down && !previousPointer?.down) { + this.clickStartPointer = pointer; + } + + if (!pointer.down) { + const hoverInfo = this.pickAt(pointer); + + this.emitHoverIfChanged(hoverInfo); + } + + if (!pointer.down && previousPointer?.down && this.clickStartPointer) { + const dx = pointer.x - this.clickStartPointer.x; + const dy = pointer.y - this.clickStartPointer.y; + + const movedDistance = Math.hypot(dx, dy); + + if (movedDistance <= 5) { + const clickInfo = this.pickAt(pointer); + + if (clickInfo?.object) { + this.emitPickingCallbacks(this.clickCallbacks, clickInfo); + } + } + + this.clickStartPointer = null; + } + + this.previousPickingPointer = pointer; + } + + private pickAt(pointer: DeckPointerState): PickingInfo | null { + if (!this.deck) return null; + + try { + this.patchPickingBlendParameters(); + + return this.deck.pickObject({ x: pointer.x, y: pointer.y, radius: 5 }); + } catch (error) { + this.handleRuntimeError(error, CANVAS_WRAPPER_OFFSET); + + return null; + } + } + + private emitHoverIfChanged(info: PickingInfo | null): void { + const hoverKey = info?.object ? `${info.layer?.id ?? 'unknown'}:${info.index}` : null; + if (hoverKey === this.lastHoverKey) return; + + this.lastHoverKey = hoverKey; + this.emitPickingCallbacks(this.hoverCallbacks, info?.object ? info : null); + } + + private emitPickingCallbacks( + callbacks: Set, + info: PickingInfo | null + ): void { + const serializableInfo = this.serializePickingInfo(info); + + for (const callback of callbacks) { + try { + callback(serializableInfo); + } catch (error) { + this.handleRuntimeError(error, CANVAS_WRAPPER_OFFSET); + } + } + } + + private serializePickingInfo(info: PickingInfo | null): SerializablePickingInfo | null { + if (!info) return null; + + return { + picked: info.picked, + index: info.index, + object: this.toCloneableValue(info.object), + x: info.x, + y: info.y, + coordinate: this.toNumberArray(info.coordinate), + color: this.toNumberArray(info.color), + pixelRatio: info.pixelRatio, + layer: this.serializeDeckItem(info.layer), + sourceLayer: this.serializeDeckItem(info.sourceLayer), + viewport: this.serializeDeckItem(info.viewport) + }; + } + + private serializeDeckItem(item: { id?: string } | null | undefined): { id: string } | null { + return item?.id ? { id: item.id } : null; + } + + private toNumberArray(value: unknown): number[] | undefined { + return Array.isArray(value) + ? value.filter((item): item is number => typeof item === 'number') + : undefined; + } + + private toCloneableValue(value: unknown): unknown { + if (value === null || value === undefined) return value; + + try { + return structuredClone(value); + } catch { + return this.toJsonLikeValue(value, new WeakSet()); + } + } + + private toJsonLikeValue(value: unknown, seen: WeakSet): unknown { + if ( + value === null || + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if (ArrayBuffer.isView(value)) { + const view = value as unknown as { length?: number; [index: number]: unknown }; + if (typeof view.length !== 'number') return undefined; + + return Array.from({ length: view.length }, (_, index) => + this.toJsonLikeValue(view[index], seen) + ); + } + + if (Array.isArray(value)) { + return value.map((item) => this.toJsonLikeValue(item, seen)); + } + + if (typeof value !== 'object' || seen.has(value)) return undefined; + + seen.add(value); + + const output: Record = {}; + + for (const [key, item] of Object.entries(value)) { + if (typeof item === 'function' || typeof item === 'symbol') continue; + + output[key] = this.toJsonLikeValue(item, seen); + } + + seen.delete(value); + + return output; + } + + private getPointerState(params: RenderParams): DeckPointerState { + return { + x: params.mouseX, + y: params.mouseY, + down: params.mouseZ >= 0 && params.mouseW >= 0 + }; + } + private blitToReglFramebuffer(): void { if (!this.deckFramebuffer || !this.framebuffer) return; @@ -370,6 +624,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { const [width, height] = this.renderer.outputSize; const sourceFBO = this.deckFramebuffer.handle; + const destFBO = getFramebuffer(this.framebuffer); if (!sourceFBO) return; diff --git a/ui/static/content/objects/deckgl.md b/ui/static/content/objects/deckgl.md index 572c1d4c4..97c837a5c 100644 --- a/ui/static/content/objects/deckgl.md +++ b/ui/static/content/objects/deckgl.md @@ -40,6 +40,10 @@ function getLayers({ time, viewState, mouse }) { - `setViewState(value)` - replace camera state - `setDeckInteraction(enabled)` - enable or disable Patchies' built-in deck camera pan/zoom controls +- `onDeckHover(callback)` - receive deck.gl picking info when hovering over + pickable layers +- `onDeckClick(callback)` - receive deck.gl picking info when clicking pickable + layers - `mouse` - forwarded mouse position ## Interaction @@ -51,6 +55,24 @@ default so those gestures control the deck.gl view instead of moving the object. Call `setDeckInteraction(false)` to keep the deck camera fixed unless your code changes it with `setViewState()`. +## Picking + +Set `pickable: true` on a layer, then register hover or click callbacks. +The callback receives clone-safe picking info with `object`, `index`, `x`, `y`, +`coordinate`, and lightweight `layer`/`sourceLayer`/`viewport` ids. + +```js +onDeckHover(info => { + if (info) { + console.log('hover', info.object) + } +}) + +onDeckClick(info => { + console.log('click', info?.object) +}) +``` + ## OSM Tiles Use `TileLayer` with `BitmapLayer` to render OpenStreetMap raster tiles. From 3926c2f4f00a4834a4df86522a822d820d0da2a5 Mon Sep 17 00:00:00 2001 From: Phoomparin Mano Date: Sun, 24 May 2026 16:47:05 +0700 Subject: [PATCH 08/12] presets(deckgl): add osm-points.deckgl preset --- .../specs/147-deckgl-fbo-renderer.md | 4 + ui/src/lib/extensions/preset-packs.ts | 2 +- ui/src/lib/presets/builtin/deckgl.presets.ts | 129 ++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/docs/design-docs/specs/147-deckgl-fbo-renderer.md b/docs/design-docs/specs/147-deckgl-fbo-renderer.md index 9b317d57a..8721d90c6 100644 --- a/docs/design-docs/specs/147-deckgl-fbo-renderer.md +++ b/docs/design-docs/specs/147-deckgl-fbo-renderer.md @@ -57,6 +57,10 @@ function getLayers() { } ``` +`osm-points.deckgl` extends the OSM tile pattern with a pickable +`ScatterplotLayer`, hover highlighting, click-driven color updates, and message +output for hover/click events. + ## Interaction - Drag forwards through the existing Shadertoy-style mouse state. diff --git a/ui/src/lib/extensions/preset-packs.ts b/ui/src/lib/extensions/preset-packs.ts index 85d3d31d8..ca868f6ab 100644 --- a/ui/src/lib/extensions/preset-packs.ts +++ b/ui/src/lib/extensions/preset-packs.ts @@ -306,7 +306,7 @@ export const BUILT_IN_PRESET_PACKS: PresetPack[] = [ description: 'Geospatial visualization layers rendered into the video pipeline', icon: 'Map', requiredObjects: ['deckgl'], - presets: ['osm.deckgl'] + presets: ['osm.deckgl', 'osm-points.deckgl'] }, { id: 'tone-presets', diff --git a/ui/src/lib/presets/builtin/deckgl.presets.ts b/ui/src/lib/presets/builtin/deckgl.presets.ts index ba7825b76..ccfd6e702 100644 --- a/ui/src/lib/presets/builtin/deckgl.presets.ts +++ b/ui/src/lib/presets/builtin/deckgl.presets.ts @@ -36,6 +36,124 @@ function getLayers() { ] }`; +const OSM_POINTS_DECKGL = `setTitle('Interactive OSM Points') +setPortCount(1, 1) + +let hoveredIndex = -1 +let updateVersion = 0 + +setViewState({ + longitude: 98.9853, + latitude: 18.7883, + zoom: 13, + pitch: 0, + bearing: 0 +}) + +function hslToRgb(h, s, l) { + s /= 100 + l /= 100 + + const k = n => (n + h / 30) % 12 + const a = s * Math.min(l, 1 - l) + const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1)) + + return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4))] +} + +const points = Array.from({ length: 50 }, (_, i) => ({ + id: i, + position: [ + 98.9853 + (Math.random() - 0.5) * 0.04, + 18.7883 + (Math.random() - 0.5) * 0.04 + ], + h: 210 + Math.random() * 40, + s: 80, + l: 60, + size: 50 + Math.random() * 50 +})) + +onDeckClick(info => { + if (info && info.index !== -1) { + const targetPoint = points[info.index] + + if (targetPoint) { + targetPoint.h = (targetPoint.h + 45) % 360 + + updateVersion++ + + send({ + type: 'click', + id: targetPoint.id, + newHue: targetPoint.h, + updateVersion + }) + } + } +}) + +onDeckHover(info => { + if (!info) { + hoveredIndex = -1 + return + } + + hoveredIndex = info.index !== undefined ? info.index : -1 + + if (hoveredIndex !== -1) { + send({ type: 'hover', index: hoveredIndex, id: info.object?.id }) + } +}) + +function getTileBounds(tile) { + if (tile.boundingBox) { + const [[west, south], [east, north]] = tile.boundingBox + + return [west, south, east, north] + } + + const { west, south, east, north } = tile.bbox + + return [west, south, east, north] +} + +function getLayers({ time }) { + const tile = new TileLayer({ + id: 'osm-tiles', + data: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', + minZoom: 0, + maxZoom: 19, + tileSize: 256, + renderSubLayers: props => new BitmapLayer(props, { + data: null, + image: props.data, + bounds: getTileBounds(props.tile) + }) + }) + + const scatterplot = new ScatterplotLayer({ + id: 'points-layer', + data: points, + getPosition: d => d.position, + getFillColor: (d, { index }) => { + const lightness = index === hoveredIndex ? d.l + 25 : d.l + + return hslToRgb(d.h, d.s, lightness) + }, + getRadius: d => d.size + Math.sin(time * 2 + d.id) * 20, + radiusUnits: 'meters', + stroked: true, + getLineColor: [255, 255, 255], + getLineWidth: 2, + lineWidthUnits: 'pixels', + opacity: 0.9, + pickable: true, + updateTriggers: { getFillColor: [hoveredIndex, updateVersion] } + }) + + return [tile, scatterplot] +}`; + type DeckGLPresetData = { code: string; messageInletCount?: number; @@ -58,5 +176,16 @@ export const DECKGL_PRESETS: Record< videoInletCount: 0, videoOutletCount: 1 } + }, + 'osm-points.deckgl': { + type: 'deckgl', + description: 'Interactive OpenStreetMap points with hover and click picking', + data: { + code: OSM_POINTS_DECKGL.trim(), + messageInletCount: 1, + messageOutletCount: 1, + videoInletCount: 0, + videoOutletCount: 1 + } } }; From eaf30f79b69b8e0d931671997900ee591c186a9b Mon Sep 17 00:00:00 2001 From: Phoomparin Mano Date: Sun, 24 May 2026 17:36:52 +0700 Subject: [PATCH 09/12] feat(deckgl): add setViewState to default code --- ui/src/lib/canvas/constants.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ui/src/lib/canvas/constants.ts b/ui/src/lib/canvas/constants.ts index a1f234826..0c5793a60 100644 --- a/ui/src/lib/canvas/constants.ts +++ b/ui/src/lib/canvas/constants.ts @@ -305,6 +305,14 @@ const data = [ { position: [-122.48, 37.74], color: [255, 210, 80] } ] +setViewState({ + longitude: -122.44, + latitude: 37.76, + zoom: 11, + pitch: 0, + bearing: 0 +}) + function getLayers({ time }) { return [ new ScatterplotLayer({ From 262393a27f61a224ec7282c1133407c68a17a62a Mon Sep 17 00:00:00 2001 From: Phoomparin Mano Date: Sun, 24 May 2026 17:50:54 +0700 Subject: [PATCH 10/12] feat(deckgl): add aggregation layers --- .../specs/147-deckgl-fbo-renderer.md | 2 + ui/bun.lock | 5 + ui/package.json | 1 + ui/src/lib/codemirror/deckgl-completions.ts | 7 ++ .../codemirror/patchies-completions.test.ts | 1 + ui/src/lib/extensions/preset-packs.ts | 2 +- ui/src/lib/presets/builtin/deckgl.presets.ts | 119 ++++++++++++++++++ ui/src/workers/rendering/deckglRenderer.ts | 12 +- ui/static/content/objects/deckgl.md | 1 + 9 files changed, 148 insertions(+), 2 deletions(-) diff --git a/docs/design-docs/specs/147-deckgl-fbo-renderer.md b/docs/design-docs/specs/147-deckgl-fbo-renderer.md index 8721d90c6..fa98ff480 100644 --- a/docs/design-docs/specs/147-deckgl-fbo-renderer.md +++ b/docs/design-docs/specs/147-deckgl-fbo-renderer.md @@ -60,6 +60,8 @@ function getLayers() { `osm-points.deckgl` extends the OSM tile pattern with a pickable `ScatterplotLayer`, hover highlighting, click-driven color updates, and message output for hover/click events. +`chiang-mai-hexagon.deckgl` uses `HexagonLayer` from +`@deck.gl/aggregation-layers` to render an extruded 3D heatmap over Chiang Mai. ## Interaction diff --git a/ui/bun.lock b/ui/bun.lock index 65d7549b4..de3626ef5 100644 --- a/ui/bun.lock +++ b/ui/bun.lock @@ -15,6 +15,7 @@ "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.38.1", "@csound/browser": "file:./packages/csound-browser", + "@deck.gl/aggregation-layers": "9.0.0", "@deck.gl/core": "9.0.0", "@deck.gl/extensions": "9.0.0", "@deck.gl/geo-layers": "9.0.0", @@ -419,6 +420,8 @@ "@davepagurek/bezier-path": ["@davepagurek/bezier-path@0.0.2", "", {}, "sha512-4L9ddgzZc9DRGyl1RrS3z5nwnVJoyjsAelVG4X1jh4tVxryEHr4H9QavhxW/my6Rn3669Qz6mhv8gd5O/WeFTA=="], + "@deck.gl/aggregation-layers": ["@deck.gl/aggregation-layers@9.0.0", "", { "dependencies": { "@luma.gl/constants": "^9.0.6", "@luma.gl/shadertools": "^9.0.6", "@math.gl/web-mercator": "^4.0.0", "d3-hexbin": "^0.2.1" }, "peerDependencies": { "@deck.gl/core": "^9.0.0", "@deck.gl/layers": "^9.0.0", "@luma.gl/core": "^9.0.0", "@luma.gl/engine": "^9.0.0" } }, "sha512-u4rS/vwV6LcsuExkJsjs2tOGApTSLEO/P7JL3EJQM2ZL2O80nP3CnWH0CKXEgK7xOHKZXIccxBNS7S6XPgEpMA=="], + "@deck.gl/core": ["@deck.gl/core@9.0.0", "", { "dependencies": { "@loaders.gl/core": "^4.1.4", "@loaders.gl/images": "^4.1.4", "@luma.gl/constants": "^9.0.6", "@luma.gl/core": "^9.0.6", "@luma.gl/engine": "^9.0.6", "@luma.gl/shadertools": "^9.0.6", "@luma.gl/webgl": "^9.0.6", "@math.gl/core": "^4.0.0", "@math.gl/sun": "^4.0.0", "@math.gl/web-mercator": "^4.0.0", "@probe.gl/env": "^4.0.9", "@probe.gl/log": "^4.0.9", "@probe.gl/stats": "^4.0.9", "@types/offscreencanvas": "^2019.6.4", "gl-matrix": "^3.0.0", "mjolnir.js": "^2.7.0" } }, "sha512-o3Ica4ZUU+dHAJ3v5BXorzg90xy9THLWiZQ/v4kZHat050EfFT8ZAPWwL0xS9M0DBvY4ikcZjTtcBtsh+KGm0A=="], "@deck.gl/extensions": ["@deck.gl/extensions@9.0.0", "", { "dependencies": { "@luma.gl/constants": "^9.0.6", "@luma.gl/shadertools": "^9.0.6", "@math.gl/core": "^4.0.0" }, "peerDependencies": { "@deck.gl/core": "^9.0.0", "@luma.gl/core": "^9.0.0", "@luma.gl/engine": "^9.0.0" } }, "sha512-2QcWLXuQVdlOWCV/vqpZtIhAkYjv7Pvcd5bxI3b02mtkoICpryWSWYJDP6+vNxNvviwe9rVSvwTL6vqd/dwWDQ=="], @@ -1549,6 +1552,8 @@ "d3-geo-projection": ["d3-geo-projection@3.0.0", "", { "dependencies": { "commander": "2", "d3-array": "1 - 2", "d3-geo": "1.12.0 - 2", "resolve": "^1.1.10" }, "bin": { "geo2svg": "bin/geo2svg", "geograticule": "bin/geograticule", "geoproject": "bin/geoproject", "geoquantize": "bin/geoquantize", "geostitch": "bin/geostitch" } }, "sha512-1JE+filVbkEX2bT25dJdQ05iA4QHvUwev6o0nIQHOSrNlHCAKfVss/U10vEM3pA4j5v7uQoFdQ4KLbx9BlEbWA=="], + "d3-hexbin": ["d3-hexbin@0.2.2", "", {}, "sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w=="], + "d3-hierarchy": ["d3-hierarchy@2.0.0", "", {}, "sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw=="], "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], diff --git a/ui/package.json b/ui/package.json index 77b5be2b0..11ebb8f8b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -88,6 +88,7 @@ "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.38.1", "@csound/browser": "file:./packages/csound-browser", + "@deck.gl/aggregation-layers": "9.0.0", "@deck.gl/core": "9.0.0", "@deck.gl/extensions": "9.0.0", "@deck.gl/geo-layers": "9.0.0", diff --git a/ui/src/lib/codemirror/deckgl-completions.ts b/ui/src/lib/codemirror/deckgl-completions.ts index a0f9810b5..e4076657a 100644 --- a/ui/src/lib/codemirror/deckgl-completions.ts +++ b/ui/src/lib/codemirror/deckgl-completions.ts @@ -71,6 +71,13 @@ const layerCompletions: DeckGLLayerCompletion[] = [ apply: "new H3HexagonLayer({\n id: 'h3-hexagons',\n data,\n getHexagon: d => d.hex,\n getFillColor: [80, 180, 255, 160]\n})" }, + { + label: 'HexagonLayer', + detail: 'new HexagonLayer(props)', + info: 'Aggregate points into extruded 3D hexagonal bins.', + apply: + "new HexagonLayer({\n id: 'hexagons',\n data,\n getPosition: d => d.position,\n radius: 200,\n extruded: true,\n elevationScale: 40,\n pickable: true\n})" + }, { label: 'IconLayer', detail: 'new IconLayer(props)', diff --git a/ui/src/lib/codemirror/patchies-completions.test.ts b/ui/src/lib/codemirror/patchies-completions.test.ts index 518f3a2f6..2652ccbc9 100644 --- a/ui/src/lib/codemirror/patchies-completions.test.ts +++ b/ui/src/lib/codemirror/patchies-completions.test.ts @@ -162,6 +162,7 @@ describe('patchies completions', () => { 'ArcLayer', 'BitmapLayer', 'GeoJsonLayer', + 'HexagonLayer', 'LineLayer', 'MVTLayer', 'ScatterplotLayer', diff --git a/ui/src/lib/extensions/preset-packs.ts b/ui/src/lib/extensions/preset-packs.ts index ca868f6ab..7a4aec7ac 100644 --- a/ui/src/lib/extensions/preset-packs.ts +++ b/ui/src/lib/extensions/preset-packs.ts @@ -306,7 +306,7 @@ export const BUILT_IN_PRESET_PACKS: PresetPack[] = [ description: 'Geospatial visualization layers rendered into the video pipeline', icon: 'Map', requiredObjects: ['deckgl'], - presets: ['osm.deckgl', 'osm-points.deckgl'] + presets: ['osm.deckgl', 'osm-points.deckgl', 'hexagon.deckgl'] }, { id: 'tone-presets', diff --git a/ui/src/lib/presets/builtin/deckgl.presets.ts b/ui/src/lib/presets/builtin/deckgl.presets.ts index ccfd6e702..01e6821d5 100644 --- a/ui/src/lib/presets/builtin/deckgl.presets.ts +++ b/ui/src/lib/presets/builtin/deckgl.presets.ts @@ -154,6 +154,114 @@ function getLayers({ time }) { return [tile, scatterplot] }`; +const HEXAGON_DECKGL = `setTitle('Hexagon') +setPortCount(1, 1) + +setViewState({ + longitude: 98.9853, + latitude: 18.7883, + zoom: 12, + pitch: 52, + bearing: -24 +}) + +const colorRange = [ + [28, 120, 170], + [55, 174, 170], + [123, 204, 146], + [205, 230, 122], + [247, 196, 89], + [225, 90, 72] +] + +const centers = [ + { name: 'Old City', position: [98.9853, 18.7883], weight: 1.2 }, + { name: 'Nimman', position: [98.9677, 18.7991], weight: 1.0 }, + { name: 'Night Bazaar', position: [99.0006, 18.7833], weight: 0.9 }, + { name: 'Riverside', position: [99.0089, 18.7896], weight: 0.75 }, + { name: 'Doi Suthep Road', position: [98.9495, 18.7965], weight: 0.65 } +] + +function makePoint(id, center) { + const spread = 0.008 + (1.3 - center.weight) * 0.006 + const angle = Math.random() * Math.PI * 2 + const distance = Math.pow(Math.random(), 1.7) * spread + + return { + id, + area: center.name, + position: [ + center.position[0] + Math.cos(angle) * distance, + center.position[1] + Math.sin(angle) * distance + ], + value: Math.round(8 + Math.random() * 18 * center.weight) + } +} + +const points = Array.from({ length: 1800 }, (_, i) => { + const center = centers[Math.floor(Math.random() * centers.length)] + + return makePoint(i, center) +}) + +let hoveredHexagon = null + +function getPointPosition(point) { + return point.position +} + +function getPointValue(point) { + return point.value +} + +onDeckHover(info => { + hoveredHexagon = info?.object ?? null + + if (hoveredHexagon) { + send({ + type: 'hover', + count: hoveredHexagon.points?.length ?? 0, + elevation: hoveredHexagon.elevationValue ?? 0 + }) + } +}) + +function getTileBounds(tile) { + if (tile.boundingBox) { + const [[west, south], [east, north]] = tile.boundingBox + + return [west, south, east, north] + } + + const { west, south, east, north } = tile.bbox + + return [west, south, east, north] +} + +function getLayers() { + return [ + new HexagonLayer({ + id: 'hexagons', + data: points, + getPosition: getPointPosition, + getColorWeight: getPointValue, + getElevationWeight: getPointValue, + colorRange, + radius: 260, + coverage: 0.82, + elevationRange: [0, 900], + elevationScale: 36, + extruded: true, + material: { + ambient: 0.45, + diffuse: 0.65, + shininess: 18, + specularColor: [180, 180, 180] + } + }) + ] +}`; + type DeckGLPresetData = { code: string; messageInletCount?: number; @@ -187,5 +295,16 @@ export const DECKGL_PRESETS: Record< videoInletCount: 0, videoOutletCount: 1 } + }, + 'hexagon.deckgl': { + type: 'deckgl', + description: 'Extruded HexagonLayer heatmap over Chiang Mai', + data: { + code: HEXAGON_DECKGL.trim(), + messageInletCount: 1, + messageOutletCount: 1, + videoInletCount: 0, + videoOutletCount: 1 + } } }; diff --git a/ui/src/workers/rendering/deckglRenderer.ts b/ui/src/workers/rendering/deckglRenderer.ts index f0543e453..ab909e2bf 100644 --- a/ui/src/workers/rendering/deckglRenderer.ts +++ b/ui/src/workers/rendering/deckglRenderer.ts @@ -113,6 +113,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { private DeckClass: typeof import('@deck.gl/core').Deck | null = null; private layerClasses: typeof import('@deck.gl/layers') | null = null; private geoLayerClasses: typeof import('@deck.gl/geo-layers') | null = null; + private aggregationLayerClasses: typeof import('@deck.gl/aggregation-layers') | null = null; private deck: Deck | null = null; private device: DeckDevice | null = null; @@ -156,6 +157,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { const deckModule = await import('@deck.gl/core'); instance.layerClasses = await import('@deck.gl/layers'); instance.geoLayerClasses = await import('@deck.gl/geo-layers'); + instance.aggregationLayerClasses = await import('@deck.gl/aggregation-layers'); instance.DeckClass = deckModule.Deck; @@ -218,7 +220,14 @@ export class DeckGLRenderer extends BaseWorkerRenderer { this.setPortCount(1, 0); - if (!this.DeckClass || !this.layerClasses || !this.geoLayerClasses) return; + if ( + !this.DeckClass || + !this.layerClasses || + !this.geoLayerClasses || + !this.aggregationLayerClasses + ) { + return; + } try { const setViewState = (viewState: Partial) => { @@ -246,6 +255,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { Deck: this.DeckClass, ...this.layerClasses, ...this.geoLayerClasses, + ...this.aggregationLayerClasses, viewState: this.viewState, setViewState, setDeckInteraction, diff --git a/ui/static/content/objects/deckgl.md b/ui/static/content/objects/deckgl.md index 97c837a5c..743fb0be4 100644 --- a/ui/static/content/objects/deckgl.md +++ b/ui/static/content/objects/deckgl.md @@ -36,6 +36,7 @@ function getLayers({ time, viewState, mouse }) { - `Deck` - deck.gl Deck class - `ScatterplotLayer`, `GeoJsonLayer`, `LineLayer`, `ArcLayer`, `PolygonLayer`, `TextLayer`, `BitmapLayer` - common deck.gl layers - `TileLayer` - tiled raster/vector data, useful for OSM-style map tiles +- `HexagonLayer` - aggregate points into extruded 3D hexagonal bins - `viewState` - current camera state - `setViewState(value)` - replace camera state - `setDeckInteraction(enabled)` - enable or disable Patchies' built-in deck From 2f01da0325f18a89f4c745ee9ca71f3e01744c23 Mon Sep 17 00:00:00 2001 From: Phoomparin Mano Date: Sun, 24 May 2026 19:33:59 +0700 Subject: [PATCH 11/12] presets(deckgl): add hexagon-flat and column presets --- .../specs/147-deckgl-fbo-renderer.md | 19 + ui/src/lib/ai/object-prompts/deckgl.ts | 2 + .../codemirror/patchies-completions.test.ts | 6 +- ui/src/lib/codemirror/patchies-completions.ts | 16 + ui/src/lib/extensions/preset-packs.ts | 8 +- ui/src/lib/presets/builtin/deckgl.presets.ts | 169 +++++++ ui/src/workers/rendering/deckglRenderer.ts | 476 +++++++++++++++++- ui/static/content/objects/deckgl.md | 8 + 8 files changed, 695 insertions(+), 9 deletions(-) diff --git a/docs/design-docs/specs/147-deckgl-fbo-renderer.md b/docs/design-docs/specs/147-deckgl-fbo-renderer.md index fa98ff480..101c2c29d 100644 --- a/docs/design-docs/specs/147-deckgl-fbo-renderer.md +++ b/docs/design-docs/specs/147-deckgl-fbo-renderer.md @@ -62,6 +62,12 @@ function getLayers() { output for hover/click events. `chiang-mai-hexagon.deckgl` uses `HexagonLayer` from `@deck.gl/aggregation-layers` to render an extruded 3D heatmap over Chiang Mai. +The installed `@deck.gl/aggregation-layers@9.0.0` HexagonLayer path is +CPU-backed and renders an aggregated `ColumnLayer` sublayer, so debugging should +inspect sublayer data, picking, and GL state across frame boundaries. +`hexagon-flat.deckgl` and `column.deckgl` are diagnostic presets used to isolate +whether failures come from HexagonLayer aggregation, ColumnLayer rendering, or +extruded/depth/lighting state. ## Interaction @@ -75,10 +81,23 @@ output for hover/click events. - User code can call `onDeckHover(callback)` and `onDeckClick(callback)` to receive manual picking results from forwarded mouse input. Layers must opt in with `pickable: true`. +- User code can call `setDeckPicking(false)` to skip Patchies' manual + `pickObject()` pass while keeping camera interaction enabled. This is useful + for render-only demos and for isolating whether deck.gl's picking pass is + involved in a rendering issue. +- The worker also skips `pickObject()` when the flattened deck.gl layer tree has + no `pickable` layers, even if hover/click callbacks are registered. +- User code can call `setDeckDebug(true)` to log throttled worker diagnostics + for deck.gl frame stages, flattened layer ids, framebuffer size, and WebGL + errors. - The worker patches deck.gl v9.0 picking blend parameters into legacy WebGL `blendEquation`/`blendFunc` parameters because deck.gl's picking pass emits `blendAlphaSrcFactor: 'constant-alpha'`, which the pinned luma.gl WebGL adapter does not accept. +- The worker sets deck.gl `width`/`height` from Patchies' + `renderer.outputSize` instead of relying on DOM/canvas auto-size inference. + Worker `OffscreenCanvas` and the shared regl WebGL context do not provide the + same reliable `clientWidth`/`clientHeight` path as a DOM canvas. ## Non-goals diff --git a/ui/src/lib/ai/object-prompts/deckgl.ts b/ui/src/lib/ai/object-prompts/deckgl.ts index f7222113f..7ffad5fcf 100644 --- a/ui/src/lib/ai/object-prompts/deckgl.ts +++ b/ui/src/lib/ai/object-prompts/deckgl.ts @@ -8,6 +8,8 @@ Use setDeckInteraction(false) when the camera should stay fixed instead of respo Use onDeckHover(info => {}) and onDeckClick(info => {}) for picking callbacks. Layers must set pickable: true for picking to work. +Use setDeckPicking(false) to isolate render-only layers from Patchies' manual hover/click picking pass. Use setDeckDebug(true) only while debugging renderer issues. + Example: \`\`\`js diff --git a/ui/src/lib/codemirror/patchies-completions.test.ts b/ui/src/lib/codemirror/patchies-completions.test.ts index 2652ccbc9..a9e95da7a 100644 --- a/ui/src/lib/codemirror/patchies-completions.test.ts +++ b/ui/src/lib/codemirror/patchies-completions.test.ts @@ -137,9 +137,13 @@ describe('patchies completions', () => { it('shows deckgl camera helper completions only for deckgl code', () => { expect(getCompletionLabels('deckgl', 'setV')).toContain('setViewState'); - expect(getCompletionLabels('deckgl', 'setD')).toContain('setDeckInteraction'); + expect(getCompletionLabels('deckgl', 'setD')).toEqual( + expect.arrayContaining(['setDeckDebug', 'setDeckInteraction', 'setDeckPicking']) + ); expect(getCompletionLabels('three', 'setV')).not.toContain('setViewState'); expect(getCompletionLabels('three', 'setD')).not.toContain('setDeckInteraction'); + expect(getCompletionLabels('three', 'setD')).not.toContain('setDeckPicking'); + expect(getCompletionLabels('three', 'setD')).not.toContain('setDeckDebug'); }); it('shows deckgl picking helper completions only for deckgl code', () => { diff --git a/ui/src/lib/codemirror/patchies-completions.ts b/ui/src/lib/codemirror/patchies-completions.ts index 23ee52455..4c64c0061 100644 --- a/ui/src/lib/codemirror/patchies-completions.ts +++ b/ui/src/lib/codemirror/patchies-completions.ts @@ -200,6 +200,20 @@ const patchiesAPICompletions: Completion[] = [ info: 'Enable or disable Patchies-provided deckgl camera pan and zoom controls.', apply: 'setDeckInteraction(false)' }, + { + label: 'setDeckPicking', + type: 'function', + detail: '(enabled: boolean) => void', + info: 'Enable or disable Patchies-provided deckgl hover/click picking.', + apply: 'setDeckPicking(false)' + }, + { + label: 'setDeckDebug', + type: 'function', + detail: '(enabled?: boolean) => void', + info: 'Enable throttled deckgl worker diagnostics for frame stages, layer summaries, framebuffer size, and WebGL errors.', + apply: 'setDeckDebug(true)' + }, { label: 'setPrimaryButton', type: 'function', @@ -652,6 +666,8 @@ const nodeSpecificFunctions: Record = { setResolution: ['hydra', 'canvas', 'three', 'deckgl', 'regl', 'swgl', 'textmode'], setViewState: ['deckgl'], setDeckInteraction: ['deckgl'], + setDeckPicking: ['deckgl'], + setDeckDebug: ['deckgl'], setPrimaryButton: ['js', 'worker', 'p5', 'hydra', 'canvas', 'regl', 'swgl', 'textmode', 'three'], setVideoCount: ['hydra', 'regl', 'swgl', 'three', 'worker'], getTexture: ['hydra', 'regl', 'swgl', 'three'], diff --git a/ui/src/lib/extensions/preset-packs.ts b/ui/src/lib/extensions/preset-packs.ts index 7a4aec7ac..b031e8f53 100644 --- a/ui/src/lib/extensions/preset-packs.ts +++ b/ui/src/lib/extensions/preset-packs.ts @@ -306,7 +306,13 @@ export const BUILT_IN_PRESET_PACKS: PresetPack[] = [ description: 'Geospatial visualization layers rendered into the video pipeline', icon: 'Map', requiredObjects: ['deckgl'], - presets: ['osm.deckgl', 'osm-points.deckgl', 'hexagon.deckgl'] + presets: [ + 'osm.deckgl', + 'osm-points.deckgl', + 'hexagon.deckgl', + 'hexagon-flat.deckgl', + 'column.deckgl' + ] }, { id: 'tone-presets', diff --git a/ui/src/lib/presets/builtin/deckgl.presets.ts b/ui/src/lib/presets/builtin/deckgl.presets.ts index 01e6821d5..df1751bd9 100644 --- a/ui/src/lib/presets/builtin/deckgl.presets.ts +++ b/ui/src/lib/presets/builtin/deckgl.presets.ts @@ -156,6 +156,7 @@ function getLayers({ time }) { const HEXAGON_DECKGL = `setTitle('Hexagon') setPortCount(1, 1) +setDeckDebug(true) setViewState({ longitude: 98.9853, @@ -262,6 +263,152 @@ function getLayers() { ] }`; +const HEXAGON_FLAT_DECKGL = `setTitle('Hexagon Flat Probe') +setPortCount(1, 0) +setDeckPicking(false) +setDeckDebug(true) + +setViewState({ + longitude: 98.9853, + latitude: 18.7883, + zoom: 12, + pitch: 0, + bearing: 0 +}) + +const colorRange = [ + [28, 120, 170], + [55, 174, 170], + [123, 204, 146], + [205, 230, 122], + [247, 196, 89], + [225, 90, 72] +] + +const centers = [ + { name: 'Old City', position: [98.9853, 18.7883], weight: 1.2 }, + { name: 'Nimman', position: [98.9677, 18.7991], weight: 1.0 }, + { name: 'Night Bazaar', position: [99.0006, 18.7833], weight: 0.9 }, + { name: 'Riverside', position: [99.0089, 18.7896], weight: 0.75 }, + { name: 'Doi Suthep Road', position: [98.9495, 18.7965], weight: 0.65 } +] + +function makePoint(id, center) { + const spread = 0.008 + (1.3 - center.weight) * 0.006 + const angle = Math.random() * Math.PI * 2 + const distance = Math.pow(Math.random(), 1.7) * spread + + return { + id, + area: center.name, + position: [ + center.position[0] + Math.cos(angle) * distance, + center.position[1] + Math.sin(angle) * distance + ], + value: Math.round(8 + Math.random() * 18 * center.weight) + } +} + +const points = Array.from({ length: 1800 }, (_, i) => { + const center = centers[Math.floor(Math.random() * centers.length)] + + return makePoint(i, center) +}) + +function getLayers() { + return [ + new HexagonLayer({ + id: 'hexagons-flat', + data: points, + getPosition: d => d.position, + getColorWeight: d => d.value, + colorRange, + radius: 260, + coverage: 0.82, + extruded: false, + stroked: true, + getLineColor: [255, 255, 255, 160], + lineWidthUnits: 'pixels', + getLineWidth: 1 + }) + ] +}`; + +const COLUMN_DECKGL = `setTitle('Column Probe') +setPortCount(1, 0) +setDeckPicking(false) +setDeckDebug(true) + +setViewState({ + longitude: 98.9853, + latitude: 18.7883, + zoom: 12, + pitch: 52, + bearing: -24 +}) + +const colors = [ + [28, 120, 170, 255], + [55, 174, 170, 255], + [123, 204, 146, 255], + [205, 230, 122, 255], + [247, 196, 89, 255], + [225, 90, 72, 255] +] + +const centers = [ + { name: 'Old City', position: [98.9853, 18.7883], weight: 1.2 }, + { name: 'Nimman', position: [98.9677, 18.7991], weight: 1.0 }, + { name: 'Night Bazaar', position: [99.0006, 18.7833], weight: 0.9 }, + { name: 'Riverside', position: [99.0089, 18.7896], weight: 0.75 }, + { name: 'Doi Suthep Road', position: [98.9495, 18.7965], weight: 0.65 } +] + +function makeColumn(id, center) { + const spread = 0.012 + (1.3 - center.weight) * 0.008 + const angle = Math.random() * Math.PI * 2 + const distance = Math.pow(Math.random(), 1.3) * spread + const bucket = Math.min(colors.length - 1, Math.floor(Math.random() * colors.length)) + + return { + id, + position: [ + center.position[0] + Math.cos(angle) * distance, + center.position[1] + Math.sin(angle) * distance + ], + elevation: 80 + bucket * 120 * center.weight, + color: colors[bucket] + } +} + +const columns = Array.from({ length: 120 }, (_, i) => { + const center = centers[Math.floor(Math.random() * centers.length)] + + return makeColumn(i, center) +}) + +function getLayers() { + return [ + new ColumnLayer({ + id: 'columns', + data: columns, + diskResolution: 6, + radius: 90, + coverage: 0.9, + extruded: true, + getPosition: d => d.position, + getElevation: d => d.elevation, + getFillColor: d => d.color, + material: { + ambient: 0.45, + diffuse: 0.65, + shininess: 18, + specularColor: [180, 180, 180] + } + }) + ] +}`; + type DeckGLPresetData = { code: string; messageInletCount?: number; @@ -306,5 +453,27 @@ export const DECKGL_PRESETS: Record< videoInletCount: 0, videoOutletCount: 1 } + }, + 'hexagon-flat.deckgl': { + type: 'deckgl', + description: 'Diagnostic flat HexagonLayer over Chiang Mai', + data: { + code: HEXAGON_FLAT_DECKGL.trim(), + messageInletCount: 1, + messageOutletCount: 0, + videoInletCount: 0, + videoOutletCount: 1 + } + }, + 'column.deckgl': { + type: 'deckgl', + description: 'Diagnostic direct ColumnLayer over Chiang Mai', + data: { + code: COLUMN_DECKGL.trim(), + messageInletCount: 1, + messageOutletCount: 0, + videoInletCount: 0, + videoOutletCount: 1 + } } }; diff --git a/ui/src/workers/rendering/deckglRenderer.ts b/ui/src/workers/rendering/deckglRenderer.ts index ab909e2bf..62e328375 100644 --- a/ui/src/workers/rendering/deckglRenderer.ts +++ b/ui/src/workers/rendering/deckglRenderer.ts @@ -74,6 +74,46 @@ type DeckWithPrivatePicker = Deck & { }; }; +type DeckWithPrivateLayers = Deck & { + layerManager?: { + getLayers?: () => unknown[]; + }; + viewManager?: { + getViewports?: () => Array<{ + id?: unknown; + x?: unknown; + y?: unknown; + width?: unknown; + height?: unknown; + longitude?: unknown; + latitude?: unknown; + zoom?: unknown; + pitch?: unknown; + bearing?: unknown; + }>; + }; +}; + +type DeckDebugModel = { + id?: unknown; + vertexCount?: unknown; + instanceCount?: unknown; + bindings?: Record; + uniforms?: Record; + pipeline?: { + id?: unknown; + shaderLayout?: { + bindings?: Array<{ name?: unknown; type?: unknown }>; + attributes?: Array<{ name?: unknown }>; + }; + }; +}; + +type DeckDebugAttribute = { + size?: number; + value?: unknown; +}; + type SizedFramebuffer = regl.Framebuffer2D & { width: number; height: number; @@ -92,6 +132,7 @@ type DeckFramebuffer = { }; type DeckDevice = { + _lumaData?: Record; canvasContext?: { setDrawingBufferSize?: (width: number, height: number) => void; }; @@ -129,6 +170,10 @@ export class DeckGLRenderer extends BaseWorkerRenderer { private patchedPickingPasses = new WeakSet(); private pendingWheelDelta = 0; private deckInteractionEnabled = true; + private deckPickingEnabled = true; + private deckDebugEnabled = false; + private deckDebugFrame = 0; + private deckDebugStageState = new Map(); private viewState: DeckViewState = { longitude: -122.44, @@ -173,6 +218,8 @@ export class DeckGLRenderer extends BaseWorkerRenderer { const gl = this.renderer.gl; if (!gl) return; + this.deckDebugFrame += 1; + this.mouseX = params.mouseX; this.mouseY = params.mouseY; @@ -199,11 +246,67 @@ export class DeckGLRenderer extends BaseWorkerRenderer { return; } - this.deck.setProps({ viewState: this.viewState, layers }); - this.deck.redraw('patchies'); - this.updatePicking(params); + this.logDeckDebug( + 'layers', + { + input: this.summarizeLayers(layers), + outputSize: this.renderer.outputSize, + viewState: this.viewState, + pickingEnabled: this.deckPickingEnabled, + hoverCallbacks: this.hoverCallbacks.size, + clickCallbacks: this.clickCallbacks.size + }, + this.shouldLogFrameDebug() + ); + + try { + const [width, height] = this.renderer.outputSize; + + this.deck.setProps({ width, height, viewState: this.viewState, layers }); + this.readGlError('setProps'); + + this.updatePicking(params); + this.readGlError('picking'); + + this.deck.redraw('patchies'); + this.readGlError('redraw'); + + this.logDeckDebug( + 'post-redraw', + { + flattened: this.summarizeFlattenedLayers(), + framebuffer: this.summarizeDeckFramebuffer(), + viewports: this.summarizeDeckViewports(), + pixels: this.sampleFramebufferPixels(this.deckFramebuffer.handle) + }, + this.shouldLogFrameDebug() + ); + } catch (error) { + this.logDeckDebug( + 'render-error', + { + message: error instanceof Error ? error.message : String(error), + flattened: this.summarizeFlattenedLayers(), + framebuffer: this.summarizeDeckFramebuffer() + }, + true + ); + this.handleRuntimeError(error, CANVAS_WRAPPER_OFFSET); + return; + } this.blitToReglFramebuffer(); + this.readGlError('blit'); + this.logDeckDebug( + 'post-blit', + { + framebuffer: this.summarizeReglFramebuffer(), + pixels: this.sampleFramebufferPixels( + this.framebuffer ? getFramebuffer(this.framebuffer) : null + ) + }, + this.shouldLogFrameDebug() + ); this.renderer.regl._refresh(); } @@ -212,6 +315,10 @@ export class DeckGLRenderer extends BaseWorkerRenderer { this.setInteraction('interact', false); this.deckInteractionEnabled = true; + this.deckPickingEnabled = true; + this.deckDebugEnabled = false; + this.deckDebugFrame = 0; + this.deckDebugStageState.clear(); this.hoverCallbacks.clear(); this.clickCallbacks.clear(); this.previousPickingPointer = null; @@ -238,6 +345,16 @@ export class DeckGLRenderer extends BaseWorkerRenderer { this.deckInteractionEnabled = enabled; }; + const setDeckPicking = (enabled: boolean) => { + this.deckPickingEnabled = enabled; + }; + + const setDeckDebug = (enabled = true) => { + this.deckDebugEnabled = enabled; + this.deckDebugFrame = 0; + this.deckDebugStageState.clear(); + }; + const onDeckHover = (callback: DeckPickingCallback) => { this.hoverCallbacks.add(callback); @@ -259,6 +376,8 @@ export class DeckGLRenderer extends BaseWorkerRenderer { viewState: this.viewState, setViewState, setDeckInteraction, + setDeckPicking, + setDeckDebug, onDeckHover, onDeckClick }; @@ -338,13 +457,14 @@ export class DeckGLRenderer extends BaseWorkerRenderer { canvas.parentElement ??= null; await new Promise((resolve) => { + const [width, height] = this.renderer.outputSize; + this.deck = new DeckClass({ controller: false, gl, - width: null, - height: null, + width, + height, useDevicePixels: false, - initialViewState: this.viewState, viewState: this.viewState, layers: [], getTooltip: null, @@ -400,6 +520,14 @@ export class DeckGLRenderer extends BaseWorkerRenderer { this.device.canvasContext?.setDrawingBufferSize?.(width, height); if (this.deckFramebuffer.width !== width || this.deckFramebuffer.height !== height) { + this.logDeckDebug( + 'resize-target', + { + from: [this.deckFramebuffer.width, this.deckFramebuffer.height], + to: [width, height] + }, + true + ); this.deckFramebuffer.resize({ width, height }); } } @@ -465,8 +593,28 @@ export class DeckGLRenderer extends BaseWorkerRenderer { } private updatePicking(params: RenderParams): void { - if (!this.deck || (this.hoverCallbacks.size === 0 && this.clickCallbacks.size === 0)) { + const hasPickableLayer = this.hasPickableLayer(); + + if ( + !this.deck || + !this.deckPickingEnabled || + !hasPickableLayer || + (this.hoverCallbacks.size === 0 && this.clickCallbacks.size === 0) + ) { + this.logDeckDebug( + 'picking-skipped', + { + deckReady: Boolean(this.deck), + pickingEnabled: this.deckPickingEnabled, + hasPickableLayer, + hoverCallbacks: this.hoverCallbacks.size, + clickCallbacks: this.clickCallbacks.size + }, + this.shouldLogFrameDebug() + ); this.previousPickingPointer = this.getPointerState(params); + this.clickStartPointer = null; + this.lastHoverKey = null; return; } @@ -503,6 +651,14 @@ export class DeckGLRenderer extends BaseWorkerRenderer { this.previousPickingPointer = pointer; } + private hasPickableLayer(): boolean { + const layers = (this.deck as DeckWithPrivateLayers | null)?.layerManager?.getLayers?.() ?? []; + + return layers.some((layer) => + Boolean((layer as { props?: { pickable?: unknown } }).props?.pickable) + ); + } + private pickAt(pointer: DeckPointerState): PickingInfo | null { if (!this.deck) return null; @@ -517,6 +673,312 @@ export class DeckGLRenderer extends BaseWorkerRenderer { } } + private shouldLogFrameDebug(): boolean { + if (!this.deckDebugEnabled) return false; + + return this.deckDebugFrame === 1 || this.deckDebugFrame % 120 === 0; + } + + private logDeckDebug(stage: string, details: Record, force = false): void { + if (!this.deckDebugEnabled) return; + + const now = performance.now(); + const signature = JSON.stringify(details); + const previous = this.deckDebugStageState.get(stage); + const isSame = previous?.signature === signature; + const elapsed = previous ? now - previous.time : Number.POSITIVE_INFINITY; + + if (!force) { + if (isSame) return; + if (elapsed < 1000) return; + } + + this.deckDebugStageState.set(stage, { signature, time: now }); + + self.postMessage({ + type: 'consoleOutput', + nodeId: this.config.nodeId, + level: 'log', + args: [`[deckgl debug] frame ${this.deckDebugFrame} ${stage}`, details] + }); + } + + private readGlError(stage: string): void { + if (!this.deckDebugEnabled) return; + + const gl = this.renderer.gl; + if (!gl) return; + + const errors: number[] = []; + let error = gl.getError(); + + while (error !== gl.NO_ERROR && errors.length < 8) { + errors.push(error); + error = gl.getError(); + } + + if (errors.length === 0) return; + + this.logDeckDebug(`${stage}:gl-error`, { errors }, true); + } + + private summarizeDeckFramebuffer(): Record | null { + if (!this.deckFramebuffer) return null; + + return { + width: this.deckFramebuffer.width, + height: this.deckFramebuffer.height, + hasHandle: Boolean(this.deckFramebuffer.handle) + }; + } + + private summarizeReglFramebuffer(): Record | null { + const framebuffer = this.framebuffer as SizedFramebuffer | null; + if (!framebuffer) return null; + + return { + width: framebuffer.width, + height: framebuffer.height, + hasHandle: Boolean(getFramebuffer(framebuffer)) + }; + } + + private sampleFramebufferPixels( + framebuffer: WebGLFramebuffer | null | undefined + ): Record | null { + if (!this.deckDebugEnabled || !this.shouldLogFrameDebug()) return null; + + const gl = this.renderer.gl; + if (!gl || !framebuffer) return null; + + const [width, height] = this.renderer.outputSize; + if (width <= 0 || height <= 0) return null; + + const previousReadFramebuffer = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING); + const pixel = new Uint8Array(4); + const samples = [ + [0.5, 0.5], + [0.4, 0.5], + [0.6, 0.5], + [0.5, 0.4], + [0.5, 0.6], + [0.35, 0.35], + [0.65, 0.35], + [0.35, 0.65], + [0.65, 0.65] + ]; + + let maxAlpha = 0; + let nonTransparentSamples = 0; + let colorSum = 0; + + try { + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer); + + for (const [xRatio, yRatio] of samples) { + const x = Math.max(0, Math.min(width - 1, Math.floor(width * xRatio))); + const y = Math.max(0, Math.min(height - 1, Math.floor(height * yRatio))); + + gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel); + + maxAlpha = Math.max(maxAlpha, pixel[3]); + colorSum += pixel[0] + pixel[1] + pixel[2] + pixel[3]; + + if (pixel[3] > 0) { + nonTransparentSamples += 1; + } + } + } catch (error) { + return { + error: error instanceof Error ? error.message : String(error) + }; + } finally { + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousReadFramebuffer); + } + + return { + samples: samples.length, + nonTransparentSamples, + maxAlpha, + colorSum + }; + } + + private summarizeFlattenedLayers(): unknown[] { + const layers = (this.deck as DeckWithPrivateLayers | null)?.layerManager?.getLayers?.() ?? []; + + return this.summarizeLayers(layers); + } + + private summarizeDeckViewports(): unknown[] { + const viewports = + (this.deck as DeckWithPrivateLayers | null)?.viewManager?.getViewports?.() ?? []; + + return viewports.map((viewport) => ({ + id: viewport.id, + x: viewport.x, + y: viewport.y, + width: viewport.width, + height: viewport.height, + longitude: viewport.longitude, + latitude: viewport.latitude, + zoom: viewport.zoom, + pitch: viewport.pitch, + bearing: viewport.bearing + })); + } + + private summarizeLayers(layers: unknown[]): unknown[] { + return layers.map((layer) => { + const layerLike = layer as { + id?: unknown; + constructor?: { name?: string }; + props?: { data?: unknown; pickable?: unknown; visible?: unknown }; + state?: { aggregatorState?: { layerData?: { data?: unknown } } }; + getModels?: () => DeckDebugModel[]; + getAttributeManager?: () => { + getAttributes?: () => Record; + } | null; + }; + const aggregatedData = layerLike.state?.aggregatorState?.layerData?.data; + + return { + id: layerLike.id, + type: layerLike.constructor?.name, + visible: layerLike.props?.visible, + pickable: layerLike.props?.pickable, + dataLength: this.getDataLength(layerLike.props?.data), + aggregatedDataLength: this.getDataLength(aggregatedData), + attributes: this.summarizeLayerAttributes(layerLike), + models: this.summarizeLayerModels(layerLike) + }; + }); + } + + private summarizeLayerAttributes(layer: { + getAttributeManager?: () => { + getAttributes?: () => Record; + } | null; + }): Record { + let attributes: Record = {}; + + try { + attributes = layer.getAttributeManager?.()?.getAttributes?.() ?? {}; + } catch { + return {}; + } + + const summary: Record = {}; + + for (const [name, attribute] of Object.entries(attributes)) { + summary[name] = this.summarizeAttribute(attribute); + } + + return summary; + } + + private summarizeAttribute(attribute: DeckDebugAttribute): Record { + const value = attribute.value; + const size = attribute.size ?? null; + + if (!ArrayBuffer.isView(value) && !Array.isArray(value)) { + return { + size, + length: this.getDataLength(value) + }; + } + + const array = value as ArrayLike; + const length = array.length; + const tupleCount = size && size > 0 ? Math.floor(length / size) : null; + const first = Array.from({ length: Math.min(size ?? 4, length) }, (_, index) => array[index]); + const stats = this.getAttributeStats(array, size); + + return { + size, + length, + tupleCount, + first, + ...stats + }; + } + + private getAttributeStats( + array: ArrayLike, + size: number | null + ): Record { + if (!size || size <= 0 || array.length === 0) { + return {}; + } + + const sampleTuples = Math.min(256, Math.floor(array.length / size)); + const mins = Array.from({ length: size }, () => Number.POSITIVE_INFINITY); + const maxs = Array.from({ length: size }, () => Number.NEGATIVE_INFINITY); + const zeroCounts = Array.from({ length: size }, () => 0); + + for (let tuple = 0; tuple < sampleTuples; tuple += 1) { + for (let component = 0; component < size; component += 1) { + const value = array[tuple * size + component]; + mins[component] = Math.min(mins[component], value); + maxs[component] = Math.max(maxs[component], value); + + if (value === 0) { + zeroCounts[component] += 1; + } + } + } + + return { + sampleTuples, + min: mins, + max: maxs, + zeroCounts + }; + } + + private summarizeLayerModels(layer: { getModels?: () => DeckDebugModel[] }): unknown[] { + let models: DeckDebugModel[] = []; + + try { + models = layer.getModels?.() ?? []; + } catch { + return []; + } + + return models.map((model) => ({ + id: model.id, + vertexCount: model.vertexCount, + instanceCount: model.instanceCount, + bindingKeys: Object.keys(model.bindings ?? {}), + uniformKeys: Object.keys(model.uniforms ?? {}), + pipelineId: model.pipeline?.id, + pipelineBindings: + model.pipeline?.shaderLayout?.bindings?.map((binding) => ({ + name: binding.name, + type: binding.type + })) ?? [], + pipelineAttributes: + model.pipeline?.shaderLayout?.attributes?.map((attribute) => attribute.name) ?? [] + })); + } + + private getDataLength(data: unknown): number | null { + if (!data) return null; + if (Array.isArray(data)) return data.length; + if (ArrayBuffer.isView(data)) { + const viewLength = (data as { length?: unknown; byteLength?: unknown }).length; + const byteLength = (data as { length?: unknown; byteLength?: unknown }).byteLength; + + if (typeof viewLength === 'number') return viewLength; + + return typeof byteLength === 'number' ? byteLength : null; + } + + const maybeLength = (data as { length?: unknown }).length; + + return typeof maybeLength === 'number' ? maybeLength : null; + } + private emitHoverIfChanged(info: PickingInfo | null): void { const hoverKey = info?.object ? `${info.layer?.id ?? 'unknown'}:${info.index}` : null; if (hoverKey === this.lastHoverKey) return; diff --git a/ui/static/content/objects/deckgl.md b/ui/static/content/objects/deckgl.md index 743fb0be4..a59c7d490 100644 --- a/ui/static/content/objects/deckgl.md +++ b/ui/static/content/objects/deckgl.md @@ -41,6 +41,10 @@ function getLayers({ time, viewState, mouse }) { - `setViewState(value)` - replace camera state - `setDeckInteraction(enabled)` - enable or disable Patchies' built-in deck camera pan/zoom controls +- `setDeckPicking(enabled)` - enable or disable Patchies' manual hover/click + picking pass +- `setDeckDebug(enabled)` - enable throttled worker diagnostics for render + stages, layer summaries, framebuffer size, and WebGL errors - `onDeckHover(callback)` - receive deck.gl picking info when hovering over pickable layers - `onDeckClick(callback)` - receive deck.gl picking info when clicking pickable @@ -56,6 +60,10 @@ default so those gestures control the deck.gl view instead of moving the object. Call `setDeckInteraction(false)` to keep the deck camera fixed unless your code changes it with `setViewState()`. +Call `setDeckDebug(true)` while investigating renderer issues. It logs the first +few frames, then periodic snapshots, including flattened deck.gl layer ids and +WebGL errors after `setProps`, picking, redraw, and blit. + ## Picking Set `pickable: true` on a layer, then register hover or click callbacks. From ffdbcb2c05e6cb5b4a37847b470a070b2197d406 Mon Sep 17 00:00:00 2001 From: Phoomparin Mano Date: Sun, 24 May 2026 19:39:25 +0700 Subject: [PATCH 12/12] presets(deckgl): add column-common preset --- .../specs/147-deckgl-fbo-renderer.md | 8 +- ui/src/lib/extensions/preset-packs.ts | 3 +- ui/src/lib/presets/builtin/deckgl.presets.ts | 74 ++++++++++++++++++- ui/src/workers/rendering/deckglRenderer.ts | 54 ++++++++++---- 4 files changed, 122 insertions(+), 17 deletions(-) diff --git a/docs/design-docs/specs/147-deckgl-fbo-renderer.md b/docs/design-docs/specs/147-deckgl-fbo-renderer.md index 101c2c29d..f0e8b0ed6 100644 --- a/docs/design-docs/specs/147-deckgl-fbo-renderer.md +++ b/docs/design-docs/specs/147-deckgl-fbo-renderer.md @@ -65,9 +65,13 @@ output for hover/click events. The installed `@deck.gl/aggregation-layers@9.0.0` HexagonLayer path is CPU-backed and renders an aggregated `ColumnLayer` sublayer, so debugging should inspect sublayer data, picking, and GL state across frame boundaries. -`hexagon-flat.deckgl` and `column.deckgl` are diagnostic presets used to isolate -whether failures come from HexagonLayer aggregation, ColumnLayer rendering, or +`hexagon-flat.deckgl`, `column.deckgl`, and `column-common.deckgl` are +diagnostic presets used to isolate whether failures come from HexagonLayer +aggregation, ColumnLayer rendering, common-unit radii, or extruded/depth/lighting state. +The extruded Chiang Mai HexagonLayer preset keeps `elevationScale` conservative; +large values can push aggregated columns outside the camera frustum after the +aggregation domain settles, making a valid layer render transparent. ## Interaction diff --git a/ui/src/lib/extensions/preset-packs.ts b/ui/src/lib/extensions/preset-packs.ts index b031e8f53..921b51030 100644 --- a/ui/src/lib/extensions/preset-packs.ts +++ b/ui/src/lib/extensions/preset-packs.ts @@ -311,7 +311,8 @@ export const BUILT_IN_PRESET_PACKS: PresetPack[] = [ 'osm-points.deckgl', 'hexagon.deckgl', 'hexagon-flat.deckgl', - 'column.deckgl' + 'column.deckgl', + 'column-common.deckgl' ] }, { diff --git a/ui/src/lib/presets/builtin/deckgl.presets.ts b/ui/src/lib/presets/builtin/deckgl.presets.ts index df1751bd9..97a1bd685 100644 --- a/ui/src/lib/presets/builtin/deckgl.presets.ts +++ b/ui/src/lib/presets/builtin/deckgl.presets.ts @@ -251,7 +251,7 @@ function getLayers() { radius: 260, coverage: 0.82, elevationRange: [0, 900], - elevationScale: 36, + elevationScale: 4, extruded: true, material: { ambient: 0.45, @@ -409,6 +409,67 @@ function getLayers() { ] }`; +const COLUMN_COMMON_DECKGL = `setTitle('Column Common-Unit Probe') +setPortCount(1, 0) +setDeckPicking(false) +setDeckDebug(true) + +setViewState({ + longitude: 98.9853, + latitude: 18.7883, + zoom: 12, + pitch: 52, + bearing: -24 +}) + +const colors = [ + [28, 120, 170, 255], + [55, 174, 170, 255], + [123, 204, 146, 255], + [205, 230, 122, 255], + [247, 196, 89, 255], + [225, 90, 72, 255] +] + +const columns = Array.from({ length: 120 }, (_, i) => { + const angle = Math.random() * Math.PI * 2 + const distance = Math.pow(Math.random(), 1.3) * 0.04 + const bucket = i % colors.length + + return { + id: i, + position: [ + 98.9853 + Math.cos(angle) * distance, + 18.7883 + Math.sin(angle) * distance + ], + elevation: 80 + bucket * 120, + color: colors[bucket] + } +}) + +function getLayers() { + return [ + new ColumnLayer({ + id: 'columns-common', + data: columns, + diskResolution: 6, + radius: 0.0025, + radiusUnits: 'common', + coverage: 0.9, + extruded: true, + getPosition: d => d.position, + getElevation: d => d.elevation, + getFillColor: d => d.color, + material: { + ambient: 0.45, + diffuse: 0.65, + shininess: 18, + specularColor: [180, 180, 180] + } + }) + ] +}`; + type DeckGLPresetData = { code: string; messageInletCount?: number; @@ -475,5 +536,16 @@ export const DECKGL_PRESETS: Record< videoInletCount: 0, videoOutletCount: 1 } + }, + 'column-common.deckgl': { + type: 'deckgl', + description: 'Diagnostic ColumnLayer with common-unit radius over Chiang Mai', + data: { + code: COLUMN_COMMON_DECKGL.trim(), + messageInletCount: 1, + messageOutletCount: 0, + videoInletCount: 0, + videoOutletCount: 1 + } } }; diff --git a/ui/src/workers/rendering/deckglRenderer.ts b/ui/src/workers/rendering/deckglRenderer.ts index 62e328375..256c2bd8f 100644 --- a/ui/src/workers/rendering/deckglRenderer.ts +++ b/ui/src/workers/rendering/deckglRenderer.ts @@ -814,18 +814,28 @@ export class DeckGLRenderer extends BaseWorkerRenderer { const viewports = (this.deck as DeckWithPrivateLayers | null)?.viewManager?.getViewports?.() ?? []; - return viewports.map((viewport) => ({ - id: viewport.id, - x: viewport.x, - y: viewport.y, - width: viewport.width, - height: viewport.height, - longitude: viewport.longitude, - latitude: viewport.latitude, - zoom: viewport.zoom, - pitch: viewport.pitch, - bearing: viewport.bearing - })); + return viewports.map((viewport) => { + const viewState = viewport as { + longitude?: unknown; + latitude?: unknown; + zoom?: unknown; + pitch?: unknown; + bearing?: unknown; + }; + + return { + id: viewport.id, + x: viewport.x, + y: viewport.y, + width: viewport.width, + height: viewport.height, + longitude: viewState.longitude, + latitude: viewState.latitude, + zoom: viewState.zoom, + pitch: viewState.pitch, + bearing: viewState.bearing + }; + }); } private summarizeLayers(layers: unknown[]): unknown[] { @@ -833,7 +843,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { const layerLike = layer as { id?: unknown; constructor?: { name?: string }; - props?: { data?: unknown; pickable?: unknown; visible?: unknown }; + props?: Record & { data?: unknown; pickable?: unknown; visible?: unknown }; state?: { aggregatorState?: { layerData?: { data?: unknown } } }; getModels?: () => DeckDebugModel[]; getAttributeManager?: () => { @@ -847,6 +857,7 @@ export class DeckGLRenderer extends BaseWorkerRenderer { type: layerLike.constructor?.name, visible: layerLike.props?.visible, pickable: layerLike.props?.pickable, + props: this.summarizeLayerProps(layerLike.props), dataLength: this.getDataLength(layerLike.props?.data), aggregatedDataLength: this.getDataLength(aggregatedData), attributes: this.summarizeLayerAttributes(layerLike), @@ -855,6 +866,23 @@ export class DeckGLRenderer extends BaseWorkerRenderer { }); } + private summarizeLayerProps(props: Record | undefined): Record { + if (!props) return {}; + + return { + operation: props.operation, + opacity: props.opacity, + radius: props.radius, + radiusUnits: props.radiusUnits, + elevationScale: props.elevationScale, + extruded: props.extruded, + filled: props.filled, + stroked: props.stroked, + coverage: props.coverage, + material: props.material + }; + } + private summarizeLayerAttributes(layer: { getAttributeManager?: () => { getAttributes?: () => Record;