diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 6f8a16f9d..000000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,101 +0,0 @@ -name: test - -# On every pull request, but only on push to master -on: - push: - branches: - - master - pull_request: - -permissions: - contents: read - -jobs: - test-node: - runs-on: ubuntu-latest - - # Keep env simple. You don't need --openssl-legacy-provider for Node 20. - env: - NODE_ENV: test - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: '20' # ✅ match what Vite/Vitest expect - - # ✅ Enable Corepack so the `packageManager` field is honored - - name: Enable Corepack / Yarn 4 - run: | - corepack enable - corepack prepare yarn@4.10.3 --activate - yarn -v - - # ✅ Cache Yarn 4's compressed archives - - name: Cache Yarn 4 artifacts - id: yarn-cache - uses: actions/cache@v4 - with: - path: | - .yarn/cache - .pnp.* # if you use Plug’n’Play - .yarn/install-state.gz - key: yarn-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - yarn-cache-${{ runner.os }}- - - - name: Print versions - run: | - node -v - yarn -v - node -p "process.versions" - - - name: Install deps - # Always run to materialize node_modules; the cache only restores tarballs. - run: yarn --frozen-lockfile - - # Sanity-check that Vite sees the root config (this MUST be true) - - name: Verify vite.config.ts presence - run: | - test -f vite.config.ts || (echo "vite.config.ts missing at repo root" && exit 1) - node -e "console.log('vite.config.ts exists, good')" - - - name: Build packages - run: yarn build - - - name: Run node tests - run: yarn test-node - - - name: Cache Playwright browsers - id: playwright-cache - uses: actions/cache@v4 - with: - path: ~/.cache/ms-playwright - key: pw-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} - restore-keys: pw-${{ runner.os }}- - - - name: Install Playwright Chromium (only) - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: npx playwright install --with-deps chromium - - - name: Run headless browser tests - run: yarn test-headless --reporter=verbose - - - name: Cache website Yarn artifacts - id: website-yarn-cache - uses: actions/cache@v4 - with: - path: | - website/.yarn/cache - website/.pnp.* - website/.yarn/install-state.gz - key: website-yarn-cache-${{ runner.os }}-${{ hashFiles('website/yarn.lock') }} - restore-keys: | - website-yarn-cache-${{ runner.os }}- - - - name: Build website - run: | - cd website - yarn - yarn build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..549e1e622 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,242 @@ +name: test + +on: + push: + branches: + - master + pull_request: + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Enable Corepack / Yarn 4 + run: | + corepack enable + corepack prepare yarn@4.10.3 --activate + + - name: Cache Yarn 4 artifacts + uses: actions/cache@v4 + with: + path: | + .yarn/cache + .pnp.* + .yarn/install-state.gz + key: yarn-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn-cache-${{ runner.os }}- + + - name: Install dependencies + run: yarn + + - name: Lint + run: yarn lint + + test-node: + runs-on: ubuntu-22.04 + permissions: + contents: read + actions: read + pull-requests: write + checks: write + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Enable Corepack / Yarn 4 + run: | + corepack enable + corepack prepare yarn@4.10.3 --activate + + - name: Cache Yarn 4 artifacts + uses: actions/cache@v4 + with: + path: | + .yarn/cache + .pnp.* + .yarn/install-state.gz + key: yarn-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn-cache-${{ runner.os }}- + + - name: Install dependencies + run: yarn + + - name: Build packages + run: yarn build + + - name: Run node tests with coverage + run: yarn test-ci 2>&1 | tee /tmp/coverage.txt; exit ${PIPESTATUS[0]} + + - name: Save coverage summary + if: always() + run: grep -m1 "^All files" /tmp/coverage.txt > /tmp/coverage-summary.txt || true + + - name: Upload coverage baseline + if: github.ref == 'refs/heads/master' + uses: actions/upload-artifact@v4 + with: + name: coverage-baseline + path: /tmp/coverage-summary.txt + retention-days: 90 + + - name: Find latest master run with baseline + if: github.event_name == 'pull_request' + id: find-baseline + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + const runs = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'test.yml', + branch: 'master', + status: 'success', + per_page: 5, + }); + for (const run of runs.data.workflow_runs) { + const {data} = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: run.id, + }); + if (data.artifacts.find(a => a.name === 'coverage-baseline')) { + return String(run.id); + } + } + return ''; + + - name: Download coverage baseline + if: github.event_name == 'pull_request' && steps.find-baseline.outputs.result != '' + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: coverage-baseline + path: /tmp/baseline + run-id: ${{ steps.find-baseline.outputs.result }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Post coverage summary to PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const raw = fs.readFileSync('/tmp/coverage.txt', 'utf8'); + const lines = raw.split('\n'); + + const parseAllFiles = text => { + const line = text.split('\n').find(l => l.match(/^All files\s*\|/)); + if (!line) return null; + const [, stmts, branch, funcs, ln] = line.split('|').map(s => s.trim()); + return {stmts: parseFloat(stmts), branch: parseFloat(branch), funcs: parseFloat(funcs), ln: parseFloat(ln)}; + }; + + const current = parseAllFiles(raw); + let baseline = null; + try { baseline = parseAllFiles(fs.readFileSync('/tmp/baseline/coverage-summary.txt', 'utf8')); } catch {} + + const fmt = (val, baseVal) => { + const str = `**${val.toFixed(2)}%**`; + if (baseline === null || isNaN(baseVal)) return str; + const d = (val - baseVal).toFixed(2); + const sign = d >= 0 ? '+' : ''; + const color = d > 0 ? '▲' : d < 0 ? '▼' : '●'; + return `${str} ${color}${sign}${d}%`; + }; + + let coverageLine = '_Coverage data not found_'; + if (current) { + const b = baseline || {}; + coverageLine = [ + `Stmts: ${fmt(current.stmts, b.stmts)}`, + `Branch: ${fmt(current.branch, b.branch)}`, + `Funcs: ${fmt(current.funcs, b.funcs)}`, + `Lines: ${fmt(current.ln, b.ln)}`, + ].join(' | '); + } + + const testLine = lines.find(l => l.includes('Test Files')) || ''; + const body = [ + '## Coverage', + coverageLine, + testLine.trim() ? `> ${testLine.trim()}` : '', + ].filter(Boolean).join('\n'); + + const {data: comments} = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body.startsWith('## Coverage')); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + + test-headless: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Enable Corepack / Yarn 4 + run: | + corepack enable + corepack prepare yarn@4.10.3 --activate + + - name: Cache Yarn 4 artifacts + uses: actions/cache@v4 + with: + path: | + .yarn/cache + .pnp.* + .yarn/install-state.gz + key: yarn-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn-cache-${{ runner.os }}- + + - name: Install dependencies + run: yarn + + - name: Build packages + run: yarn build + + - name: Install Playwright browsers + run: npx playwright install chromium --with-deps + + - name: Run headless browser tests + run: yarn test-headless diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 000000000..e8d88f4de --- /dev/null +++ b/dev/README.md @@ -0,0 +1,10 @@ +# dev/ + +Work-in-progress modules and examples. Not published to npm or indexed by the docs site. + +| Directory | Type | Status | +|---|---|---| +| `timeline-layers/` | module + examples | in progress — see #517, #379 | +| `arrow-layers/` | examples | arrow-layers WIP examples | + +Design proposals and RFCs belong in [GitHub Issues/Discussions](https://github.com/visgl/deck.gl-community/issues). diff --git a/examples/arrow-layers-wip/linestring/README.md b/dev/arrow-layers/linestring/README.md similarity index 100% rename from examples/arrow-layers-wip/linestring/README.md rename to dev/arrow-layers/linestring/README.md diff --git a/examples/arrow-layers-wip/linestring/app.tsx b/dev/arrow-layers/linestring/app.tsx similarity index 100% rename from examples/arrow-layers-wip/linestring/app.tsx rename to dev/arrow-layers/linestring/app.tsx diff --git a/examples/arrow-layers-wip/linestring/index.html b/dev/arrow-layers/linestring/index.html similarity index 100% rename from examples/arrow-layers-wip/linestring/index.html rename to dev/arrow-layers/linestring/index.html diff --git a/examples/arrow-layers-wip/linestring/package.json b/dev/arrow-layers/linestring/package.json similarity index 100% rename from examples/arrow-layers-wip/linestring/package.json rename to dev/arrow-layers/linestring/package.json diff --git a/examples/arrow-layers-wip/multilinestring/app.tsx b/dev/arrow-layers/multilinestring/app.tsx similarity index 100% rename from examples/arrow-layers-wip/multilinestring/app.tsx rename to dev/arrow-layers/multilinestring/app.tsx diff --git a/examples/arrow-layers-wip/multilinestring/index.html b/dev/arrow-layers/multilinestring/index.html similarity index 100% rename from examples/arrow-layers-wip/multilinestring/index.html rename to dev/arrow-layers/multilinestring/index.html diff --git a/examples/arrow-layers-wip/multilinestring/package.json b/dev/arrow-layers/multilinestring/package.json similarity index 100% rename from examples/arrow-layers-wip/multilinestring/package.json rename to dev/arrow-layers/multilinestring/package.json diff --git a/examples/arrow-layers-wip/multipoint/app.tsx b/dev/arrow-layers/multipoint/app.tsx similarity index 100% rename from examples/arrow-layers-wip/multipoint/app.tsx rename to dev/arrow-layers/multipoint/app.tsx diff --git a/examples/arrow-layers-wip/multipoint/index.html b/dev/arrow-layers/multipoint/index.html similarity index 100% rename from examples/arrow-layers-wip/multipoint/index.html rename to dev/arrow-layers/multipoint/index.html diff --git a/examples/arrow-layers-wip/multipoint/package.json b/dev/arrow-layers/multipoint/package.json similarity index 100% rename from examples/arrow-layers-wip/multipoint/package.json rename to dev/arrow-layers/multipoint/package.json diff --git a/examples/arrow-layers-wip/multipolygon/README.md b/dev/arrow-layers/multipolygon/README.md similarity index 100% rename from examples/arrow-layers-wip/multipolygon/README.md rename to dev/arrow-layers/multipolygon/README.md diff --git a/examples/arrow-layers-wip/multipolygon/app.tsx b/dev/arrow-layers/multipolygon/app.tsx similarity index 100% rename from examples/arrow-layers-wip/multipolygon/app.tsx rename to dev/arrow-layers/multipolygon/app.tsx diff --git a/examples/arrow-layers-wip/multipolygon/generate_data.py b/dev/arrow-layers/multipolygon/generate_data.py similarity index 100% rename from examples/arrow-layers-wip/multipolygon/generate_data.py rename to dev/arrow-layers/multipolygon/generate_data.py diff --git a/examples/arrow-layers-wip/multipolygon/index.html b/dev/arrow-layers/multipolygon/index.html similarity index 100% rename from examples/arrow-layers-wip/multipolygon/index.html rename to dev/arrow-layers/multipolygon/index.html diff --git a/examples/arrow-layers-wip/multipolygon/package.json b/dev/arrow-layers/multipolygon/package.json similarity index 100% rename from examples/arrow-layers-wip/multipolygon/package.json rename to dev/arrow-layers/multipolygon/package.json diff --git a/examples/arrow-layers-wip/multipolygon/poetry.lock.disable-dependabot b/dev/arrow-layers/multipolygon/poetry.lock.disable-dependabot similarity index 100% rename from examples/arrow-layers-wip/multipolygon/poetry.lock.disable-dependabot rename to dev/arrow-layers/multipolygon/poetry.lock.disable-dependabot diff --git a/examples/arrow-layers-wip/multipolygon/pyproject.toml b/dev/arrow-layers/multipolygon/pyproject.toml similarity index 100% rename from examples/arrow-layers-wip/multipolygon/pyproject.toml rename to dev/arrow-layers/multipolygon/pyproject.toml diff --git a/examples/arrow-layers-wip/point/README.md b/dev/arrow-layers/point/README.md similarity index 100% rename from examples/arrow-layers-wip/point/README.md rename to dev/arrow-layers/point/README.md diff --git a/examples/arrow-layers-wip/point/app.tsx b/dev/arrow-layers/point/app.tsx similarity index 100% rename from examples/arrow-layers-wip/point/app.tsx rename to dev/arrow-layers/point/app.tsx diff --git a/examples/arrow-layers-wip/point/generate_data.py b/dev/arrow-layers/point/generate_data.py similarity index 100% rename from examples/arrow-layers-wip/point/generate_data.py rename to dev/arrow-layers/point/generate_data.py diff --git a/examples/arrow-layers-wip/point/index.html b/dev/arrow-layers/point/index.html similarity index 100% rename from examples/arrow-layers-wip/point/index.html rename to dev/arrow-layers/point/index.html diff --git a/examples/arrow-layers-wip/point/package.json b/dev/arrow-layers/point/package.json similarity index 100% rename from examples/arrow-layers-wip/point/package.json rename to dev/arrow-layers/point/package.json diff --git a/examples/arrow-layers-wip/point/poetry.lock.disable-dependabot b/dev/arrow-layers/point/poetry.lock.disable-dependabot similarity index 100% rename from examples/arrow-layers-wip/point/poetry.lock.disable-dependabot rename to dev/arrow-layers/point/poetry.lock.disable-dependabot diff --git a/examples/arrow-layers-wip/point/pyproject.toml b/dev/arrow-layers/point/pyproject.toml similarity index 100% rename from examples/arrow-layers-wip/point/pyproject.toml rename to dev/arrow-layers/point/pyproject.toml diff --git a/examples/arrow-layers-wip/polygon/README.md b/dev/arrow-layers/polygon/README.md similarity index 100% rename from examples/arrow-layers-wip/polygon/README.md rename to dev/arrow-layers/polygon/README.md diff --git a/examples/arrow-layers-wip/polygon/app.tsx b/dev/arrow-layers/polygon/app.tsx similarity index 100% rename from examples/arrow-layers-wip/polygon/app.tsx rename to dev/arrow-layers/polygon/app.tsx diff --git a/examples/arrow-layers-wip/polygon/generate_data.py b/dev/arrow-layers/polygon/generate_data.py similarity index 100% rename from examples/arrow-layers-wip/polygon/generate_data.py rename to dev/arrow-layers/polygon/generate_data.py diff --git a/examples/arrow-layers-wip/polygon/index.html b/dev/arrow-layers/polygon/index.html similarity index 100% rename from examples/arrow-layers-wip/polygon/index.html rename to dev/arrow-layers/polygon/index.html diff --git a/examples/arrow-layers-wip/polygon/package.json b/dev/arrow-layers/polygon/package.json similarity index 100% rename from examples/arrow-layers-wip/polygon/package.json rename to dev/arrow-layers/polygon/package.json diff --git a/examples/arrow-layers-wip/polygon/poetry.lock.disable-dependabot b/dev/arrow-layers/polygon/poetry.lock.disable-dependabot similarity index 100% rename from examples/arrow-layers-wip/polygon/poetry.lock.disable-dependabot rename to dev/arrow-layers/polygon/poetry.lock.disable-dependabot diff --git a/examples/arrow-layers-wip/polygon/pyproject.toml b/dev/arrow-layers/polygon/pyproject.toml similarity index 100% rename from examples/arrow-layers-wip/polygon/pyproject.toml rename to dev/arrow-layers/polygon/pyproject.toml diff --git a/examples/arrow-layers-wip/text/app.tsx b/dev/arrow-layers/text/app.tsx similarity index 100% rename from examples/arrow-layers-wip/text/app.tsx rename to dev/arrow-layers/text/app.tsx diff --git a/examples/arrow-layers-wip/text/generate_data.py b/dev/arrow-layers/text/generate_data.py similarity index 100% rename from examples/arrow-layers-wip/text/generate_data.py rename to dev/arrow-layers/text/generate_data.py diff --git a/examples/arrow-layers-wip/text/index.html b/dev/arrow-layers/text/index.html similarity index 100% rename from examples/arrow-layers-wip/text/index.html rename to dev/arrow-layers/text/index.html diff --git a/examples/arrow-layers-wip/text/package.json b/dev/arrow-layers/text/package.json similarity index 100% rename from examples/arrow-layers-wip/text/package.json rename to dev/arrow-layers/text/package.json diff --git a/examples/arrow-layers-wip/text/poetry.lock.disable-dependabot b/dev/arrow-layers/text/poetry.lock.disable-dependabot similarity index 100% rename from examples/arrow-layers-wip/text/poetry.lock.disable-dependabot rename to dev/arrow-layers/text/poetry.lock.disable-dependabot diff --git a/examples/arrow-layers-wip/text/pyproject.toml b/dev/arrow-layers/text/pyproject.toml similarity index 100% rename from examples/arrow-layers-wip/text/pyproject.toml rename to dev/arrow-layers/text/pyproject.toml diff --git a/examples/arrow-layers-wip/trips/README.md b/dev/arrow-layers/trips/README.md similarity index 100% rename from examples/arrow-layers-wip/trips/README.md rename to dev/arrow-layers/trips/README.md diff --git a/examples/arrow-layers-wip/trips/app.tsx b/dev/arrow-layers/trips/app.tsx similarity index 100% rename from examples/arrow-layers-wip/trips/app.tsx rename to dev/arrow-layers/trips/app.tsx diff --git a/examples/arrow-layers-wip/trips/generate_data.py b/dev/arrow-layers/trips/generate_data.py similarity index 100% rename from examples/arrow-layers-wip/trips/generate_data.py rename to dev/arrow-layers/trips/generate_data.py diff --git a/examples/arrow-layers-wip/trips/index.html b/dev/arrow-layers/trips/index.html similarity index 100% rename from examples/arrow-layers-wip/trips/index.html rename to dev/arrow-layers/trips/index.html diff --git a/examples/arrow-layers-wip/trips/package.json b/dev/arrow-layers/trips/package.json similarity index 100% rename from examples/arrow-layers-wip/trips/package.json rename to dev/arrow-layers/trips/package.json diff --git a/examples/arrow-layers-wip/trips/poetry.lock.disable-dependabot b/dev/arrow-layers/trips/poetry.lock.disable-dependabot similarity index 100% rename from examples/arrow-layers-wip/trips/poetry.lock.disable-dependabot rename to dev/arrow-layers/trips/poetry.lock.disable-dependabot diff --git a/examples/arrow-layers-wip/trips/pyproject.toml b/dev/arrow-layers/trips/pyproject.toml similarity index 100% rename from examples/arrow-layers-wip/trips/pyproject.toml rename to dev/arrow-layers/trips/pyproject.toml diff --git a/modules/timeline-layers/LICENSE b/dev/timeline-layers/LICENSE similarity index 100% rename from modules/timeline-layers/LICENSE rename to dev/timeline-layers/LICENSE diff --git a/modules/timeline-layers/README.md b/dev/timeline-layers/README.md similarity index 100% rename from modules/timeline-layers/README.md rename to dev/timeline-layers/README.md diff --git a/examples/timeline-layers/horizon-graph-layer/app.tsx b/dev/timeline-layers/examples/horizon-graph-layer/app.tsx similarity index 100% rename from examples/timeline-layers/horizon-graph-layer/app.tsx rename to dev/timeline-layers/examples/horizon-graph-layer/app.tsx diff --git a/examples/timeline-layers/horizon-graph-layer/index.html b/dev/timeline-layers/examples/horizon-graph-layer/index.html similarity index 100% rename from examples/timeline-layers/horizon-graph-layer/index.html rename to dev/timeline-layers/examples/horizon-graph-layer/index.html diff --git a/examples/timeline-layers/horizon-graph-layer/index.tsx b/dev/timeline-layers/examples/horizon-graph-layer/index.tsx similarity index 100% rename from examples/timeline-layers/horizon-graph-layer/index.tsx rename to dev/timeline-layers/examples/horizon-graph-layer/index.tsx diff --git a/examples/timeline-layers/horizon-graph-layer/package.json b/dev/timeline-layers/examples/horizon-graph-layer/package.json similarity index 100% rename from examples/timeline-layers/horizon-graph-layer/package.json rename to dev/timeline-layers/examples/horizon-graph-layer/package.json diff --git a/examples/timeline-layers/horizon-graph-layer/tsconfig.json b/dev/timeline-layers/examples/horizon-graph-layer/tsconfig.json similarity index 100% rename from examples/timeline-layers/horizon-graph-layer/tsconfig.json rename to dev/timeline-layers/examples/horizon-graph-layer/tsconfig.json diff --git a/dev/timeline-layers/examples/timeline-layer/README.md b/dev/timeline-layers/examples/timeline-layer/README.md new file mode 100644 index 000000000..d303681e4 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/README.md @@ -0,0 +1,52 @@ +# Timeline Layer Example + +An interactive GPU-accelerated timeline built with `@deck.gl-community/timeline-layers` — a deck.gl `CompositeLayer` that replaces DOM-driven timelines and handles hundreds of thousands of datapoints without lag. + +### Prototype + +https://github.com/user-attachments/assets/95006924-913f-4025-a0ef-6d74b336ca67 + +### End Goal + +Timeline end goal + +## Features + +- Multi-track timeline with clip visualization +- Collision detection for overlapping clips (automatic subtrack assignment) +- Interactive playhead scrubber with drag support +- Zoom (mouse wheel) and pan (click + drag when zoomed) +- Clip and track selection with hover highlighting +- Multiple time formatting options + +## Usage + +```bash +# Against installed packages +yarn start + +# Against local source (dev) +yarn start-local +``` + +## API + +```ts +import {TimelineLayer} from '@deck.gl-community/timeline-layers'; + +new TimelineLayer({ + data: tracks, // TimelineTrack[] + timelineStart: 0, // start of full range (ms) + timelineEnd: 60_000, // end of full range (ms) + currentTimeMs: 5_000, // current playhead position + onClipClick: ({clip, track}) => console.log('clicked', clip.label), + onViewportChange: (startMs, endMs) => setViewport({startMs, endMs}), + onZoomChange: (zoom) => setZoom(zoom) +}); +``` + +See [`TimelineLayerProps`](../../src/layers/timeline-layer/timeline-types.ts) for the full API reference. + +## Context + +Originally prototyped in PR [#379](https://github.com/visgl/deck.gl-community/pull/379), refined in PR [#517](https://github.com/visgl/deck.gl-community/pull/517). Lives in `dev/timeline-layers` until promoted to `modules/` — tracked in [#38](https://github.com/visgl/deck.gl-community/issues/38). diff --git a/dev/timeline-layers/examples/timeline-layer/app.tsx b/dev/timeline-layers/examples/timeline-layer/app.tsx new file mode 100644 index 000000000..d3765d9c6 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/app.tsx @@ -0,0 +1,174 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import React, {ReactElement, useMemo, useEffect} from 'react'; +import DeckGL from '@deck.gl/react'; +import {OrthographicView, type OrthographicViewState} from '@deck.gl/core'; +import {TimelineLayer} from '@deck.gl-community/timeline-layers'; +import {useTimelineControls, TimelineControls} from './demo-controls'; +import { + useTimelineInteractionState, + useTimelineRefs, + useGlobalMouseUpCleanup, + useWheelZoom, + useTimelineCallbacks, + useContainerHandlers, + useDeckGLHandlers, + useCursorGetter, + useTimelineResize +} from './timeline-hooks'; + +const ORTHOGRAPHIC_VIEW = new OrthographicView(); + +const CONTROLLER_CONFIG = { + scrollZoom: false, + doubleClickZoom: false, + touchZoom: false, + dragPan: false, + dragRotate: false, + keyboard: false +} as const; + +export default function App(): ReactElement { + const {state, controls, trackCount, clipsPerTrack, labelFormatterType} = useTimelineControls(); + const {state: interactionState, setState} = useTimelineInteractionState(); + const {timelineLayerRef, containerRef} = useTimelineRefs(); + + const initialViewState = useMemo((): OrthographicViewState => { + const w = (typeof window !== 'undefined' ? window.innerWidth : 1280) - 320; + const h = typeof window !== 'undefined' ? window.innerHeight : 720; + return {target: [w / 2, h / 2, 0], zoom: 0}; + }, []); + + useGlobalMouseUpCleanup( + setState.setIsDraggingScrubber, + setState.setIsPanning, + setState.setPanStartViewport + ); + useWheelZoom(containerRef, timelineLayerRef, state.zoomLevel); + useTimelineResize(controls.setTimelineWidth); + + const timelineCallbacks = useTimelineCallbacks(controls); + + const containerHandlers = useContainerHandlers( + interactionState, + setState, + timelineLayerRef, + controls, + state + ); + + const deckGLHandlers = useDeckGLHandlers(setState, timelineLayerRef, controls, state); + const getCursor = useCursorGetter(interactionState, state.zoomLevel); + + const viewport = useMemo( + () => ({startMs: state.viewportStartMs, endMs: state.viewportEndMs}), + [state.viewportStartMs, state.viewportEndMs] + ); + + const selectionStyle = useMemo( + () => ({ + selectedClipColor: [255, 200, 0, 255] as [number, number, number, number], + hoveredClipColor: [200, 200, 200, 255] as [number, number, number, number], + selectedTrackColor: [80, 80, 80, 255] as [number, number, number, number], + hoveredTrackColor: [70, 70, 70, 255] as [number, number, number, number], + selectedLineWidth: state.selectedLineWidth, + hoveredLineWidth: state.hoveredLineWidth + }), + [state.selectedLineWidth, state.hoveredLineWidth] + ); + + const timelineLayer = useMemo( + () => + new TimelineLayer({ + id: 'timeline', + data: state.tracks, + timelineStart: state.timelineStart, + timelineEnd: state.timelineEnd, + x: state.timelineX, + y: state.timelineY, + width: state.timelineWidth, + trackHeight: state.trackHeight, + trackSpacing: state.trackSpacing, + currentTimeMs: state.currentTimeMs, + viewport, + timeFormatter: state.labelFormatter, + selectedClipId: state.selectedClip?.id, + hoveredClipId: state.hoveredClip?.id, + selectedTrackId: state.selectedTrack?.id, + hoveredTrackId: state.hoveredTrack?.id, + showTrackLabels: true, + showClipLabels: true, + showScrubber: true, + showAxis: true, + showSubtrackSeparators: state.showSubtrackSeparators, + selectionStyle, + onClipClick: timelineCallbacks.handleClipClick, + onClipHover: timelineCallbacks.handleClipHover, + onTrackClick: timelineCallbacks.handleTrackClick, + onTrackHover: timelineCallbacks.handleTrackHover, + onScrubberDrag: timelineCallbacks.handleScrubberDrag, + onTimelineClick: timelineCallbacks.handleTimelineClick, + onViewportChange: timelineCallbacks.handleViewportChange, + onZoomChange: timelineCallbacks.handleZoomChange + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + state.tracks, + state.timelineStart, + state.timelineEnd, + state.timelineX, + state.timelineY, + state.timelineWidth, + state.trackHeight, + state.trackSpacing, + state.currentTimeMs, + viewport, + state.labelFormatter, + state.selectedClip?.id, + state.hoveredClip?.id, + state.selectedTrack?.id, + state.hoveredTrack?.id, + state.showSubtrackSeparators, + selectionStyle, + timelineCallbacks + ] + ); + + useEffect(() => { + timelineLayerRef.current = timelineLayer; + }, [timelineLayer, timelineLayerRef]); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/dev/timeline-layers/examples/timeline-layer/demo-controls.tsx b/dev/timeline-layers/examples/timeline-layer/demo-controls.tsx new file mode 100644 index 000000000..212907cb8 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/demo-controls.tsx @@ -0,0 +1,520 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import React, {ReactElement, useMemo, useState} from 'react'; +import { + timeAxisFormatters, + type TimelineTrack, + type TimelineClip +} from '@deck.gl-community/timeline-layers'; +import {generateRandomTracks} from './demo-utils'; + +const PANEL_WIDTH = 320; +const INITIAL_CANVAS_WIDTH = (typeof window !== 'undefined' ? window.innerWidth : 1280) - PANEL_WIDTH; + +export type TimelineControlsState = { + tracks: TimelineTrack[]; + timelineStart: number; + timelineEnd: number; + currentTimeMs: number; + zoomLevel: number; + trackHeight: number; + trackSpacing: number; + timelineX: number; + timelineY: number; + timelineWidth: number; + viewportStartMs?: number; + viewportEndMs?: number; + labelFormatter: (timeMs: number) => string; + selectedClip: TimelineClip | null; + hoveredClip: TimelineClip | null; + selectedTrack: TimelineTrack | null; + hoveredTrack: TimelineTrack | null; + selectedLineWidth: number; + hoveredLineWidth: number; + showSubtrackSeparators: boolean; +}; + +export function useTimelineControls() { + const [trackCount, setTrackCount] = useState(4); + const [clipsPerTrack, setClipsPerTrack] = useState(3); + const [timelineStart] = useState(0); + const [timelineEnd, setTimelineEnd] = useState(60000); + const [currentTimeMs, setCurrentTimeMs] = useState(0); + const [zoomLevel, setZoomLevel] = useState(1); + + const [trackHeight, setTrackHeight] = useState(40); + const [trackSpacing, setTrackSpacing] = useState(8); + const [timelineX] = useState(150); + const [timelineY] = useState(80); + const [timelineWidth, setTimelineWidth] = useState(INITIAL_CANVAS_WIDTH - 200); + + const [viewportStartMs, setViewportStartMs] = useState(undefined); + const [viewportEndMs, setViewportEndMs] = useState(undefined); + + const [selectedClip, setSelectedClip] = useState(null); + const [hoveredClip, setHoveredClip] = useState(null); + const [selectedTrack, setSelectedTrack] = useState(null); + const [hoveredTrack, setHoveredTrack] = useState(null); + + const [labelFormatterType, setLabelFormatterType] = + useState('seconds'); + const labelFormatter = timeAxisFormatters[labelFormatterType]; + + const [selectedLineWidth, setSelectedLineWidth] = useState(3); + const [hoveredLineWidth, setHoveredLineWidth] = useState(2); + const [showSubtrackSeparators, setShowSubtrackSeparators] = useState(true); + + const tracks = useMemo( + () => generateRandomTracks(trackCount, clipsPerTrack, timelineStart, timelineEnd), + [trackCount, clipsPerTrack, timelineStart, timelineEnd] + ); + + const state: TimelineControlsState = useMemo( + () => ({ + tracks, + timelineStart, + timelineEnd, + currentTimeMs, + zoomLevel, + trackHeight, + trackSpacing, + timelineX, + timelineY, + timelineWidth, + viewportStartMs, + viewportEndMs, + labelFormatter, + selectedClip, + hoveredClip, + selectedTrack, + hoveredTrack, + selectedLineWidth, + hoveredLineWidth, + showSubtrackSeparators + }), + [ + tracks, + timelineStart, + timelineEnd, + currentTimeMs, + zoomLevel, + trackHeight, + trackSpacing, + timelineX, + timelineY, + timelineWidth, + viewportStartMs, + viewportEndMs, + labelFormatter, + selectedClip, + hoveredClip, + selectedTrack, + hoveredTrack, + selectedLineWidth, + hoveredLineWidth, + showSubtrackSeparators + ] + ); + + const controls = useMemo( + () => ({ + setTrackCount, + setClipsPerTrack, + setTimelineEnd, + setCurrentTimeMs, + setZoomLevel, + setTrackHeight, + setTrackSpacing, + setTimelineWidth, + setViewportStartMs, + setViewportEndMs, + setLabelFormatterType, + setSelectedClip, + setHoveredClip, + setSelectedTrack, + setHoveredTrack, + setSelectedLineWidth, + setHoveredLineWidth, + setShowSubtrackSeparators + }), + [] + ); + + return {state, controls, trackCount, clipsPerTrack, labelFormatterType}; +} + +export function TimelineControls({ + state, + controls, + trackCount, + clipsPerTrack, + labelFormatterType +}: { + state: TimelineControlsState; + controls: ReturnType['controls']; + trackCount: number; + clipsPerTrack: number; + labelFormatterType: string; +}): ReactElement { + return ( +
+
+

Timeline Controls

+
+ +
+
+ + + controls.setTimelineEnd(state.timelineStart + v)} + /> +
+ +
+
+ + +
+
+ +
+ + +
+ +
+ +
+ +
+ +
+ + + + {state.selectedClip && ( + controls.setSelectedClip(null)} /> + )} + + {state.selectedTrack && ( + controls.setSelectedTrack(null)} /> + )} +
+
+ ); +} + +function Section({title, children}: {title: string; children: React.ReactNode}): ReactElement { + return ( +
+

{title}

+ {children} +
+ ); +} + +function RangeField({ + label, + min, + max, + step = 1, + value, + onChange +}: { + label: string; + min: number; + max: number; + step?: number; + value: number; + onChange: (v: number) => void; +}): ReactElement { + return ( +
+ + onChange(Number(e.target.value))} + style={{width: '100%'}} + /> +
+ ); +} + +function SelectField({ + label, + value, + options, + onChange +}: { + label: string; + value: number; + options: number[]; + onChange: (v: number) => void; +}): ReactElement { + return ( +
+ + +
+ ); +} + +function AdvancedSettings({ + state, + controls +}: { + state: TimelineControlsState; + controls: ReturnType['controls']; +}): ReactElement { + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
+
setIsExpanded(!isExpanded)} + style={{ + cursor: 'pointer', + padding: '10px', + background: '#f5f5f5', + border: '1px solid #ddd', + borderRadius: '4px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: isExpanded ? '10px' : '0' + }} + > +

Advanced Settings

+ {isExpanded ? '▼' : '▶'} +
+ + {isExpanded && ( +
+ + + +
+ )} +
+ ); +} + +function ClipDetails({ + clip, + formatter, + onClear +}: { + clip: TimelineClip; + formatter: (timeMs: number) => string; + onClear: () => void; +}): ReactElement { + return ( +
+

+ Selected Clip +

+
+
+ Label: {clip.label} +
+
+ Start: {formatter(clip.startMs)} +
+
+ Duration: {formatter(clip.endMs - clip.startMs)} +
+ +
+
+ ); +} + +function TrackDetails({ + track, + onClear +}: { + track: TimelineTrack; + onClear: () => void; +}): ReactElement { + return ( +
+

+ Selected Track +

+
+
+ Name: {track.name} +
+
+ Clips: {track.clips.length} +
+ +
+
+ ); +} diff --git a/dev/timeline-layers/examples/timeline-layer/demo-utils.ts b/dev/timeline-layers/examples/timeline-layer/demo-utils.ts new file mode 100644 index 000000000..012dcd143 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/demo-utils.ts @@ -0,0 +1,64 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import type {TimelineClip, TimelineTrack} from '@deck.gl-community/timeline-layers'; + +/** + * Generate random tracks with clips for demo purposes. + */ +export function generateRandomTracks( + trackCount: number, + clipsPerTrack: number, + timelineStart: number, + timelineEnd: number +): TimelineTrack[] { + const colors: [number, number, number, number][] = [ + [80, 120, 160, 220], + [120, 160, 180, 220], + [100, 140, 120, 220], + [140, 120, 140, 220], + [160, 140, 100, 220], + [120, 100, 140, 220], + [100, 120, 120, 220], + [140, 120, 100, 220] + ]; + + const tracks: TimelineTrack[] = []; + + for (let trackIdx = 0; trackIdx < trackCount; trackIdx++) { + const clips: TimelineClip[] = []; + + for (let clipIdx = 0; clipIdx < clipsPerTrack; clipIdx++) { + const duration = Math.random() * (timelineEnd - timelineStart) * 0.1; + let startMs: number; + + // 30% chance of an overlapping clip (tests collision detection) + if (clipIdx > 0 && Math.random() < 0.3) { + const prevClip = clips[clips.length - 1]; + const overlapPoint = + prevClip.startMs + (prevClip.endMs - prevClip.startMs) * (0.3 + Math.random() * 0.4); + startMs = overlapPoint; + } else { + startMs = Math.random() * (timelineEnd - timelineStart - duration) + timelineStart; + } + + clips.push({ + id: `track-${trackIdx}-clip-${clipIdx}`, + startMs, + endMs: startMs + duration, + label: `Clip ${clipIdx + 1}`, + color: colors[clipIdx % colors.length] + }); + } + + tracks.push({ + id: trackIdx, + name: `Track ${trackIdx + 1}`, + visible: true, + clips + }); + } + + return tracks; +} diff --git a/dev/timeline-layers/examples/timeline-layer/index.html b/dev/timeline-layers/examples/timeline-layer/index.html new file mode 100644 index 000000000..550c87aba --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/index.html @@ -0,0 +1,17 @@ + + + + + Timeline Layer Example + + + + + + diff --git a/dev/timeline-layers/examples/timeline-layer/index.tsx b/dev/timeline-layers/examples/timeline-layer/index.tsx new file mode 100644 index 000000000..b884876b3 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/index.tsx @@ -0,0 +1,10 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import React from 'react'; +import {createRoot} from 'react-dom/client'; +import App from './app'; + +const root = createRoot(document.body.appendChild(document.createElement('div'))); +root.render(); diff --git a/dev/timeline-layers/examples/timeline-layer/package.json b/dev/timeline-layers/examples/timeline-layer/package.json new file mode 100644 index 000000000..0307a6900 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/package.json @@ -0,0 +1,19 @@ +{ + "license": "MIT", + "scripts": { + "start": "vite --open", + "start-local": "vite --config ../../vite.config.local.mjs" + }, + "dependencies": { + "@deck.gl-community/timeline-layers": "workspace:*", + "@deck.gl/core": "~9.2.1", + "@deck.gl/layers": "~9.2.1", + "@deck.gl/react": "~9.2.1", + "@luma.gl/core": "~9.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "vite": "7.1.1" + } +} diff --git a/dev/timeline-layers/examples/timeline-layer/timeline-hooks.ts b/dev/timeline-layers/examples/timeline-layer/timeline-hooks.ts new file mode 100644 index 000000000..82e866dc2 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/timeline-hooks.ts @@ -0,0 +1,467 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {useState, useRef, useEffect, useCallback, useMemo} from 'react'; +import type {TimelineLayer, TimelineClipInfo, TimelineTrackInfo} from '@deck.gl-community/timeline-layers'; +import {positionToTime} from '@deck.gl-community/timeline-layers'; +import type {TimelineControlsState} from './demo-controls'; + +const DRAG_THRESHOLD = 5; + +interface TimelineInteractionState { + isDraggingScrubber: boolean; + isPanning: boolean; + panStartX: number; + panStartViewport: {start: number; end: number} | null; + mouseDownPos: {x: number; y: number} | null; + hoveredObjectId: string | null; +} + +interface TimelineRefs { + timelineLayerRef: React.RefObject; + containerRef: React.RefObject; +} + +interface TimelineControls { + setSelectedClip: (clip: unknown) => void; + setHoveredClip: (clip: unknown) => void; + setSelectedTrack: (track: unknown) => void; + setHoveredTrack: (track: unknown) => void; + setCurrentTimeMs: (timeMs: number) => void; + setViewportStartMs: (startMs: number | undefined) => void; + setViewportEndMs: (endMs: number | undefined) => void; + setZoomLevel: (zoomLevel: number) => void; +} + +/** Hook for managing timeline interaction state */ +export function useTimelineInteractionState(): { + state: TimelineInteractionState; + setState: { + setIsDraggingScrubber: (value: boolean) => void; + setIsPanning: (value: boolean) => void; + setPanStartX: (value: number) => void; + setPanStartViewport: (value: {start: number; end: number} | null) => void; + setMouseDownPos: (value: {x: number; y: number} | null) => void; + setHoveredObjectId: (value: string | null) => void; + }; +} { + const [isDraggingScrubber, setIsDraggingScrubber] = useState(false); + const [isPanning, setIsPanning] = useState(false); + const [panStartX, setPanStartX] = useState(0); + const [panStartViewport, setPanStartViewport] = useState<{start: number; end: number} | null>( + null + ); + const [mouseDownPos, setMouseDownPos] = useState<{x: number; y: number} | null>(null); + const [hoveredObjectId, setHoveredObjectId] = useState(null); + + const state = useMemo( + () => ({isDraggingScrubber, isPanning, panStartX, panStartViewport, mouseDownPos, hoveredObjectId}), + [isDraggingScrubber, isPanning, panStartX, panStartViewport, mouseDownPos, hoveredObjectId] + ); + + const setState = useMemo( + () => ({ + setIsDraggingScrubber, + setIsPanning, + setPanStartX, + setPanStartViewport, + setMouseDownPos, + setHoveredObjectId + }), + [] + ); + + return {state, setState}; +} + +/** Hook for creating timeline refs */ +export function useTimelineRefs(): TimelineRefs { + const timelineLayerRef = useRef(null); + const containerRef = useRef(null); + return useMemo(() => ({timelineLayerRef, containerRef}), []); +} + +/** Hook for global mouse-up cleanup */ +export function useGlobalMouseUpCleanup( + setIsDraggingScrubber: (value: boolean) => void, + setIsPanning: (value: boolean) => void, + setPanStartViewport: (value: {start: number; end: number} | null) => void +): void { + useEffect(() => { + const handleGlobalMouseUp = () => { + setIsDraggingScrubber(false); + setIsPanning(false); + setPanStartViewport(null); + }; + document.addEventListener('mouseup', handleGlobalMouseUp); + return () => document.removeEventListener('mouseup', handleGlobalMouseUp); + }, [setIsDraggingScrubber, setIsPanning, setPanStartViewport]); +} + +/** Hook for mouse-wheel zoom */ +export function useWheelZoom( + containerRef: React.RefObject, + timelineLayerRef: React.RefObject, + zoomLevel: number +): void { + useEffect(() => { + const container = containerRef.current; + if (!container) return undefined; + + const handleWheel = (e: WheelEvent) => { + if (!timelineLayerRef.current) return; + e.preventDefault(); + const rect = container.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const zoomFactor = e.deltaY > 0 ? 0.8 : 1.2; + timelineLayerRef.current.zoomToPoint(zoomFactor, mouseX, zoomLevel); + }; + + container.addEventListener('wheel', handleWheel, {passive: false}); + return () => container.removeEventListener('wheel', handleWheel); + }, [containerRef, timelineLayerRef, zoomLevel]); +} + +/** Hook for timeline layer event callbacks */ +export function useTimelineCallbacks(controls: TimelineControls): { + handleClipClick: (info: TimelineClipInfo) => void; + handleClipHover: (info: TimelineClipInfo | null) => void; + handleTrackClick: (info: TimelineTrackInfo) => void; + handleTrackHover: (info: TimelineTrackInfo | null) => void; + handleScrubberDrag: (timeMs: number) => void; + handleTimelineClick: (timeMs: number) => void; + handleViewportChange: (startMs: number, endMs: number) => void; + handleZoomChange: (zoomLevel: number) => void; +} { + const { + setSelectedClip, + setHoveredClip, + setSelectedTrack, + setHoveredTrack, + setCurrentTimeMs, + setViewportStartMs, + setViewportEndMs, + setZoomLevel + } = controls; + + const handleClipClick = useCallback((info: TimelineClipInfo) => setSelectedClip(info.clip), [setSelectedClip]); + const handleClipHover = useCallback((info: TimelineClipInfo | null) => setHoveredClip(info?.clip ?? null), [setHoveredClip]); + const handleTrackClick = useCallback((info: TimelineTrackInfo) => setSelectedTrack(info.track), [setSelectedTrack]); + const handleTrackHover = useCallback((info: TimelineTrackInfo | null) => setHoveredTrack(info?.track ?? null), [setHoveredTrack]); + const handleScrubberDrag = useCallback((timeMs: number) => setCurrentTimeMs(timeMs), [setCurrentTimeMs]); + const handleTimelineClick = useCallback((timeMs: number) => setCurrentTimeMs(timeMs), [setCurrentTimeMs]); + + const handleViewportChange = useCallback( + (startMs: number, endMs: number) => { + setViewportStartMs(startMs); + setViewportEndMs(endMs); + }, + [setViewportStartMs, setViewportEndMs] + ); + + const handleZoomChange = useCallback((zoomLevel: number) => setZoomLevel(zoomLevel), [setZoomLevel]); + + return useMemo( + () => ({ + handleClipClick, + handleClipHover, + handleTrackClick, + handleTrackHover, + handleScrubberDrag, + handleTimelineClick, + handleViewportChange, + handleZoomChange + }), + [ + handleClipClick, + handleClipHover, + handleTrackClick, + handleTrackHover, + handleScrubberDrag, + handleTimelineClick, + handleViewportChange, + handleZoomChange + ] + ); +} + +/** Hook for container mouse event handlers (scrubber drag + pan) */ +export function useContainerHandlers( + interactionState: TimelineInteractionState, + setState: ReturnType['setState'], + _timelineLayerRef: React.RefObject, + controls: TimelineControls, + state: TimelineControlsState +): { + handleContainerMouseDown: (e: React.MouseEvent) => void; + handleContainerMouseMove: (e: React.MouseEvent) => void; + handleContainerMouseUp: () => void; +} { + const {setCurrentTimeMs, setViewportStartMs, setViewportEndMs} = controls; + const {setIsDraggingScrubber, setMouseDownPos, setIsPanning, setPanStartX, setPanStartViewport} = + setState; + + const handleContainerMouseDown = useCallback( + (e: React.MouseEvent) => { + if ((e.target as HTMLElement).tagName !== 'CANVAS') return; + + if (interactionState.hoveredObjectId === 'scrubber-handle') { + setIsDraggingScrubber(true); + return; + } + + if (state.zoomLevel > 1.0) { + const rect = e.currentTarget.getBoundingClientRect(); + setMouseDownPos({x: e.clientX - rect.left, y: e.clientY - rect.top}); + } + }, + [interactionState.hoveredObjectId, state.zoomLevel, setIsDraggingScrubber, setMouseDownPos] + ); + + const handleContainerMouseMove = useCallback( + (e: React.MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + // Scrubber dragging + if (interactionState.isDraggingScrubber) { + const effectiveStartMs = state.viewportStartMs ?? state.timelineStart; + const effectiveEndMs = state.viewportEndMs ?? state.timelineEnd; + const timeMs = positionToTime( + mouseX, + state.timelineX, + state.timelineWidth, + effectiveStartMs, + effectiveEndMs + ); + setCurrentTimeMs(timeMs); + return; + } + + // Pan start detection + if ( + interactionState.mouseDownPos && + !interactionState.isPanning && + !interactionState.isDraggingScrubber + ) { + const dx = mouseX - interactionState.mouseDownPos.x; + const dy = mouseY - interactionState.mouseDownPos.y; + if (Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD && state.zoomLevel > 1.0) { + setIsPanning(true); + setPanStartX(interactionState.mouseDownPos.x); + setPanStartViewport({ + start: state.viewportStartMs ?? state.timelineStart, + end: state.viewportEndMs ?? state.timelineEnd + }); + setMouseDownPos(null); + } + } + + // Pan update + if (interactionState.isPanning && interactionState.panStartViewport) { + e.preventDefault(); + const deltaX = mouseX - interactionState.panStartX; + const currentRange = interactionState.panStartViewport.end - interactionState.panStartViewport.start; + const timeDelta = -(deltaX / state.timelineWidth) * currentRange; + + let newStart = interactionState.panStartViewport.start + timeDelta; + let newEnd = interactionState.panStartViewport.end + timeDelta; + + if (newStart < state.timelineStart) { + newStart = state.timelineStart; + newEnd = state.timelineStart + currentRange; + } else if (newEnd > state.timelineEnd) { + newEnd = state.timelineEnd; + newStart = state.timelineEnd - currentRange; + } + + setViewportStartMs(newStart); + setViewportEndMs(newEnd); + } + }, + [ + interactionState, + state.zoomLevel, + state.timelineStart, + state.timelineEnd, + state.timelineWidth, + state.timelineX, + state.viewportStartMs, + state.viewportEndMs, + setCurrentTimeMs, + setIsPanning, + setPanStartX, + setPanStartViewport, + setMouseDownPos, + setViewportStartMs, + setViewportEndMs + ] + ); + + const handleContainerMouseUp = useCallback(() => { + setMouseDownPos(null); + if (interactionState.isDraggingScrubber) setIsDraggingScrubber(false); + if (interactionState.isPanning) { + setIsPanning(false); + setPanStartViewport(null); + } + }, [ + interactionState.isDraggingScrubber, + interactionState.isPanning, + setMouseDownPos, + setIsDraggingScrubber, + setIsPanning, + setPanStartViewport + ]); + + return useMemo( + () => ({handleContainerMouseDown, handleContainerMouseMove, handleContainerMouseUp}), + [handleContainerMouseDown, handleContainerMouseMove, handleContainerMouseUp] + ); +} + +/** Hook for DeckGL hover/click handlers */ +export function useDeckGLHandlers( + setState: ReturnType['setState'], + _timelineLayerRef: React.RefObject, + controls: TimelineControls, + state: TimelineControlsState +): { + handleDeckGLHover: (info: any) => void; + handleDeckGLClick: (info: any) => void; +} { + const {setCurrentTimeMs, setSelectedClip, setSelectedTrack, setHoveredClip, setHoveredTrack} = + controls; + const {setHoveredObjectId, setIsDraggingScrubber, setMouseDownPos} = setState; + + const handleDeckGLHover = useCallback( + (info: any) => { + if (!info.object) { + setHoveredObjectId(null); + setHoveredClip(null); + setHoveredTrack(null); + return; + } + + const objectId = String(info.object.id); + setHoveredObjectId(objectId); + + if (objectId.startsWith('track-') && objectId.includes('-clip-')) { + if (info.object.clip) setHoveredClip(info.object.clip); + setHoveredTrack(null); + } else if (objectId.startsWith('track-bg-')) { + setHoveredClip(null); + if (info.object.track) setHoveredTrack(info.object.track); + } else { + setHoveredClip(null); + setHoveredTrack(null); + } + }, + [setHoveredObjectId, setHoveredClip, setHoveredTrack] + ); + + const handleDeckGLClick = useCallback( + (info: any) => { + if (!info.object) { + if (info.coordinate) { + const effectiveStartMs = state.viewportStartMs ?? state.timelineStart; + const effectiveEndMs = state.viewportEndMs ?? state.timelineEnd; + const timeMs = positionToTime( + info.coordinate[0] || 0, + state.timelineX, + state.timelineWidth, + effectiveStartMs, + effectiveEndMs + ); + setCurrentTimeMs(timeMs); + } + setSelectedClip(null); + setSelectedTrack(null); + return; + } + + const objectId = String(info.object.id); + + if (objectId.startsWith('track-') && objectId.includes('-clip-')) { + if (info.object.clip) setSelectedClip(info.object.clip); + setSelectedTrack(null); + } else if (objectId === 'scrubber-handle') { + setIsDraggingScrubber(true); + setMouseDownPos(null); + setSelectedClip(null); + setSelectedTrack(null); + } else if (objectId.startsWith('track-bg-')) { + if (info.coordinate) { + const effectiveStartMs = state.viewportStartMs ?? state.timelineStart; + const effectiveEndMs = state.viewportEndMs ?? state.timelineEnd; + const timeMs = positionToTime( + info.coordinate[0] || 0, + state.timelineX, + state.timelineWidth, + effectiveStartMs, + effectiveEndMs + ); + setCurrentTimeMs(timeMs); + } + setSelectedClip(null); + if (info.object.track) setSelectedTrack(info.object.track); + } else { + setSelectedClip(null); + setSelectedTrack(null); + } + }, + [ + state.timelineX, + state.timelineWidth, + state.viewportStartMs, + state.viewportEndMs, + state.timelineStart, + state.timelineEnd, + setCurrentTimeMs, + setSelectedClip, + setSelectedTrack, + setIsDraggingScrubber, + setMouseDownPos + ] + ); + + return useMemo( + () => ({handleDeckGLHover, handleDeckGLClick}), + [handleDeckGLHover, handleDeckGLClick] + ); +} + +/** Hook for cursor style */ +export function useCursorGetter( + interactionState: TimelineInteractionState, + zoomLevel: number +): ({isHovering}: {isHovering: boolean}) => string { + return useCallback( + ({isHovering}: {isHovering: boolean}) => { + if (interactionState.isDraggingScrubber || interactionState.isPanning) return 'grabbing'; + if (interactionState.hoveredObjectId === 'scrubber-handle') return 'grab'; + if (isHovering) return 'pointer'; + if (zoomLevel > 1.0) return 'grab'; + return 'default'; + }, + [ + interactionState.isDraggingScrubber, + interactionState.isPanning, + interactionState.hoveredObjectId, + zoomLevel + ] + ); +} + +/** Hook to update timeline width on window resize */ +export function useTimelineResize(setTimelineWidth: (width: number) => void): void { + useEffect(() => { + const handleResize = () => { + setTimelineWidth(window.innerWidth - 320 - 200); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [setTimelineWidth]); +} diff --git a/dev/timeline-layers/examples/timeline-layer/tsconfig.json b/dev/timeline-layers/examples/timeline-layer/tsconfig.json new file mode 100644 index 000000000..3fd35f2e6 --- /dev/null +++ b/dev/timeline-layers/examples/timeline-layer/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../tsconfig.json", + "include": ["./*.tsx", "./*.ts"] +} diff --git a/modules/timeline-layers/package.json b/dev/timeline-layers/package.json similarity index 100% rename from modules/timeline-layers/package.json rename to dev/timeline-layers/package.json diff --git a/dev/timeline-layers/src/index.ts b/dev/timeline-layers/src/index.ts new file mode 100644 index 000000000..31f419259 --- /dev/null +++ b/dev/timeline-layers/src/index.ts @@ -0,0 +1,55 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +// HORIZON GRAPH LAYERS +export type {HorizonGraphLayerProps} from './layers/horizon-graph-layer/horizon-graph-layer'; +export {HorizonGraphLayer} from './layers/horizon-graph-layer/horizon-graph-layer'; +export type {MultiHorizonGraphLayerProps} from './layers/horizon-graph-layer/multi-horizon-graph-layer'; +export {MultiHorizonGraphLayer} from './layers/horizon-graph-layer/multi-horizon-graph-layer'; + +// AXIS LAYERS +export type {TimeAxisLayerProps} from './layers/time-axis-layer'; +export {TimeAxisLayer} from './layers/time-axis-layer'; +export type {VerticalGridLayerProps} from './layers/vertical-grid-layer'; +export {VerticalGridLayer} from './layers/vertical-grid-layer'; +export {formatTimeMs, formatTimeRangeMs} from './utils/format-utils'; + +// TIMELINE LAYER +export type {TimelineLayerProps} from './layers/timeline-layer/timeline-layer'; +export {TimelineLayer} from './layers/timeline-layer/timeline-layer'; + +export type { + TimelineClip, + ClipWithSubtrack, + TimelineTrack, + TrackWithSubtracks, + TrackPosition, + TrackBackgroundData, + TrackLabelData, + ClipPolygonData, + ClipLabelData, + SeparatorLineData, + AxisLineData, + AxisLabelData, + ScrubberLineData, + ScrubberHandleData, + ScrubberLabelData, + TimelineTick, + TimelineViewport, + TimelineClipInfo, + TimelineTrackInfo, + TimelineScrubberInfo, + TimelineViewportInfo, + TimeAxisLabelFormatter +} from './layers/timeline-layer/timeline-types'; + +export type {SelectionStyle, TimelineLayout} from './layers/timeline-layer/timeline-layout'; +export {DEFAULT_TIMELINE_LAYOUT} from './layers/timeline-layer/timeline-layout'; + +export { + positionToTime, + timeToPosition, + timeAxisFormatters, + type GenerateTicksOptions +} from './layers/timeline-layer/timeline-utils'; diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts similarity index 100% rename from modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts rename to dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts similarity index 100% rename from modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts rename to dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts similarity index 100% rename from modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts rename to dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts similarity index 100% rename from modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts rename to dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts similarity index 100% rename from modules/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts rename to dev/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts diff --git a/modules/timeline-layers/src/layers/time-axis-layer.ts b/dev/timeline-layers/src/layers/time-axis-layer.ts similarity index 100% rename from modules/timeline-layers/src/layers/time-axis-layer.ts rename to dev/timeline-layers/src/layers/time-axis-layer.ts diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts new file mode 100644 index 000000000..2062d139e --- /dev/null +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts @@ -0,0 +1,51 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import type {TimelineClip, ClipWithSubtrack} from './timeline-types'; + +export type {ClipWithSubtrack}; +/** + * Detects overlapping clips and assigns them to subtracks. + * Uses a greedy algorithm to minimize the number of subtracks needed. + */ +export function assignClipsToSubtracks(clips: TimelineClip[]): ClipWithSubtrack[] { + if (clips.length === 0) return []; + + // Sort clips by start time + const sortedClips = [...clips].sort((a, b) => a.startMs - b.startMs); + + // Track the end time of the last clip in each subtrack + const subtrackEndTimes: number[] = []; + const result: ClipWithSubtrack[] = []; + + for (const clip of sortedClips) { + let assignedSubtrack = -1; + + for (let i = 0; i < subtrackEndTimes.length; i++) { + if (clip.startMs >= subtrackEndTimes[i]) { + assignedSubtrack = i; + subtrackEndTimes[i] = clip.endMs; + break; + } + } + + if (assignedSubtrack === -1) { + assignedSubtrack = subtrackEndTimes.length; + subtrackEndTimes.push(clip.endMs); + } + + result.push({...clip, subtrackIndex: assignedSubtrack}); + } + + return result; +} + +/** + * Calculate the number of subtracks needed for a set of clips. + */ +export function calculateSubtrackCount(clips: TimelineClip[]): number { + if (clips.length === 0) return 1; + const clipsWithSubtracks = assignClipsToSubtracks(clips); + return Math.max(...clipsWithSubtracks.map((c) => c.subtrackIndex)) + 1; +} diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts new file mode 100644 index 000000000..8348e93c1 --- /dev/null +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts @@ -0,0 +1,868 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {CompositeLayer, COORDINATE_SYSTEM, type PickingInfo, type Layer} from '@deck.gl/core'; +import {SolidPolygonLayer, LineLayer, TextLayer} from '@deck.gl/layers'; +import type {CompositeLayerProps} from '@deck.gl/core'; +import type {LineLayerProps, SolidPolygonLayerProps, TextLayerProps} from '@deck.gl/layers'; + +import type { + TimelineClipInfo, + TimelineTrackInfo, + TimelineTrack, + TrackWithSubtracks, + TrackPosition, + TrackBackgroundData, + TrackLabelData, + ClipPolygonData, + ClipLabelData, + ClipWithSubtrack, + SeparatorLineData, + AxisLineData, + AxisLabelData, + ScrubberLineData, + ScrubberHandleData, + ScrubberLabelData, + TimeAxisLabelFormatter +} from './timeline-types'; + +import type {SelectionStyle} from './timeline-layout'; + +import { + timeAxisFormatters, + generateTimelineTicks, + timeToPosition, + positionToTime +} from './timeline-utils'; +import {assignClipsToSubtracks, calculateSubtrackCount} from './timeline-collision'; + +function lightenColor( + color: [number, number, number, number], + amount: number = 30 +): [number, number, number, number] { + return [ + Math.min(255, color[0] + amount), + Math.min(255, color[1] + amount), + Math.min(255, color[2] + amount), + color[3] + ]; +} + +const defaultProps = { + x: 150, + y: 100, + width: 800, + trackHeight: 40, + trackSpacing: 10, + currentTimeMs: 0, + showScrubber: true, + showClipLabels: true, + showTrackLabels: true, + showAxis: true, + showSubtrackSeparators: true, + timeFormatter: timeAxisFormatters.seconds, + selectionStyle: { + selectedClipColor: [255, 200, 0, 255] as [number, number, number, number], + hoveredClipColor: [200, 200, 200, 255] as [number, number, number, number], + selectedTrackColor: [80, 80, 80, 255] as [number, number, number, number], + hoveredTrackColor: [70, 70, 70, 255] as [number, number, number, number], + selectedLineWidth: 3, + hoveredLineWidth: 2 + } +}; + +export type TimelineLayerProps = CompositeLayerProps & { + /** Array of timeline tracks, each containing clips */ + data: TimelineTrack[]; + /** Start of the full timeline range in milliseconds */ + timelineStart: number; + /** End of the full timeline range in milliseconds */ + timelineEnd: number; + + /** X offset of the timeline in canvas coordinates */ + x?: number; + /** Y offset of the timeline in canvas coordinates */ + y?: number; + /** Width of the timeline in canvas coordinates */ + width?: number; + /** Height of each track row in canvas coordinates */ + trackHeight?: number; + /** Spacing between tracks in canvas coordinates */ + trackSpacing?: number; + + /** Current playhead time in milliseconds */ + currentTimeMs?: number; + /** Optional zoomed viewport range */ + viewport?: {startMs?: number; endMs?: number}; + /** Formatter for time axis labels */ + timeFormatter?: TimeAxisLabelFormatter; + + /** ID of the currently selected clip */ + selectedClipId?: string | number | null; + /** ID of the currently hovered clip */ + hoveredClipId?: string | number | null; + /** ID of the currently selected track */ + selectedTrackId?: string | number | null; + /** ID of the currently hovered track */ + hoveredTrackId?: string | number | null; + /** Colors and line widths for selected/hovered states */ + selectionStyle?: SelectionStyle; + + /** Whether to show the playhead scrubber */ + showScrubber?: boolean; + /** Whether to show labels on clips */ + showClipLabels?: boolean; + /** Whether to show labels on tracks */ + showTrackLabels?: boolean; + /** Whether to show the time axis */ + showAxis?: boolean; + /** Whether to show separators between collision subtracks */ + showSubtrackSeparators?: boolean; + + /** Override props for the clip polygon sub-layer */ + clipProps?: Partial>; + /** Override props for the track background sub-layer */ + trackProps?: Partial>; + /** Override props for the track label sub-layer */ + trackLabelProps?: Partial>; + /** Override props for the clip label sub-layer */ + clipLabelProps?: Partial>; + /** Override props for the axis line sub-layer */ + axisLineProps?: Partial>; + /** Override props for the axis label sub-layer */ + axisLabelProps?: Partial>; + /** Override props for the scrubber line sub-layer */ + scrubberLineProps?: Partial>; + + /** Callback when a clip is clicked */ + onClipClick?: (info: TimelineClipInfo, event: PickingInfo) => void; + /** Callback when a clip is hovered */ + onClipHover?: (info: TimelineClipInfo | null, event: PickingInfo) => void; + /** Callback when a track is clicked */ + onTrackClick?: (info: TimelineTrackInfo, event: PickingInfo) => void; + /** Callback when a track is hovered */ + onTrackHover?: (info: TimelineTrackInfo | null, event: PickingInfo) => void; + /** Callback when the scrubber handle is hovered */ + onScrubberHover?: (isHovering: boolean, event: PickingInfo) => void; + /** Callback when a scrubber drag begins */ + onScrubberDragStart?: (event: PickingInfo) => void; + /** Callback when the scrubber is dragged to a new time */ + onScrubberDrag?: (timeMs: number, event: PickingInfo) => void; + /** Callback when the timeline background is clicked */ + onTimelineClick?: (timeMs: number, event: PickingInfo) => void; + + /** Callback when the current time changes */ + onCurrentTimeChange?: (timeMs: number) => void; + /** Callback when the viewport (zoom/pan) changes */ + onViewportChange?: (startMs: number, endMs: number) => void; + /** Callback when the zoom level changes */ + onZoomChange?: (zoomLevel: number) => void; +}; + +export class TimelineLayer extends CompositeLayer { + static layerName = 'TimelineLayer'; + static defaultProps = defaultProps; + + /** Convert a canvas X coordinate to a time in milliseconds */ + getTimeFromPosition(x: number): number { + const {timelineStart, timelineEnd, viewport, x: timelineX = 150, width = 800} = this.props; + const effectiveStartMs = viewport?.startMs ?? timelineStart; + const effectiveEndMs = viewport?.endMs ?? timelineEnd; + return positionToTime(x, timelineX, width, effectiveStartMs, effectiveEndMs); + } + + /** Zoom the timeline viewport around a canvas X coordinate */ + zoomToPoint(zoomFactor: number, mouseX: number, currentZoomLevel: number): void { + const { + timelineStart, + timelineEnd, + viewport, + x: timelineX = 150, + width = 800, + onViewportChange, + onZoomChange + } = this.props; + + const newZoomLevel = Math.max(1.0, Math.min(100, currentZoomLevel * zoomFactor)); + const mouseRatio = Math.max(0, Math.min(1, (mouseX - timelineX) / width)); + const currentStartMs = viewport?.startMs ?? timelineStart; + const currentEndMs = viewport?.endMs ?? timelineEnd; + const mouseTimeMs = currentStartMs + mouseRatio * (currentEndMs - currentStartMs); + + const fullTimeRange = timelineEnd - timelineStart; + const newViewportRange = fullTimeRange / newZoomLevel; + + let newStartMs = mouseTimeMs - mouseRatio * newViewportRange; + let newEndMs = newStartMs + newViewportRange; + + if (newStartMs < timelineStart) { + newStartMs = timelineStart; + newEndMs = timelineStart + newViewportRange; + } else if (newEndMs > timelineEnd) { + newEndMs = timelineEnd; + newStartMs = timelineEnd - newViewportRange; + } + + if (newZoomLevel > 1.0) { + onViewportChange?.(newStartMs, newEndMs); + } else { + onViewportChange?.(timelineStart, timelineEnd); + } + + onZoomChange?.(newZoomLevel); + } + + // ===== LAYOUT CALCULATION ===== + + private _calculateTrackPositions( + tracksWithSubtracks: TrackWithSubtracks[], + y: number, + trackHeight: number, + trackSpacing: number + ): {trackPositions: TrackPosition[]; totalTimelineHeight: number} { + let currentY = y; + const trackPositions: TrackPosition[] = []; + const subtrackSpacing = 2; + + for (const {subtrackCount} of tracksWithSubtracks) { + const trackTotalHeight = subtrackCount * trackHeight + (subtrackCount - 1) * subtrackSpacing; + trackPositions.push({y: currentY, height: trackTotalHeight, subtrackCount}); + currentY += trackTotalHeight + trackSpacing; + } + + const totalTimelineHeight = currentY - y - trackSpacing; + return {trackPositions, totalTimelineHeight}; + } + + // ===== DATA GENERATION ===== + + private _generateTrackBackgrounds( + tracksWithSubtracks: TrackWithSubtracks[], + trackPositions: TrackPosition[] + ): TrackBackgroundData[] { + const { + x = 150, + width = 800, + selectedTrackId, + hoveredTrackId, + selectionStyle = defaultProps.selectionStyle + } = this.props; + + return tracksWithSubtracks.map(({track, trackIndex}, i) => { + const {y: trackY, height} = trackPositions[i]; + const isSelected = selectedTrackId === track.id; + const isHovered = hoveredTrackId === track.id; + + let color: [number, number, number, number] = [60, 60, 60, 255]; + if (isSelected) { + color = selectionStyle.selectedTrackColor!; + } else if (isHovered) { + color = lightenColor([60, 60, 60, 255], 20); + } + + return { + id: `track-bg-${track.id}`, + track, + trackIndex, + polygon: [ + [x, trackY], + [x + width, trackY], + [x + width, trackY + height], + [x, trackY + height] + ], + color + }; + }); + } + + private _generateTrackLabels( + tracksWithSubtracks: TrackWithSubtracks[], + trackPositions: TrackPosition[] + ): TrackLabelData[] { + const {x = 150, showTrackLabels = true} = this.props; + if (!showTrackLabels) return []; + + return tracksWithSubtracks.map(({track}, i) => { + const label = track.name || `Track ${track.id}`; + const {y: trackY, height} = trackPositions[i]; + return {text: label, position: [x - 10, trackY + height / 2, 0]}; + }); + } + + private _buildClipPolygon( + clip: ClipWithSubtrack, + opts: { + track: TimelineTrack; + trackIndex: number; + clipIndex: number; + subtrackHeight: number; + baseTrackY: number; + x: number; + width: number; + effectiveStartMs: number; + effectiveEndMs: number; + selectedClipId: string | number | null | undefined; + hoveredClipId: string | number | null | undefined; + selectionStyle: SelectionStyle; + } + ): ClipPolygonData | null { + const {id: clipId, startMs, endMs, subtrackIndex = 0} = clip; + const { + track, + trackIndex, + clipIndex, + subtrackHeight, + baseTrackY, + x, + width, + effectiveStartMs, + effectiveEndMs, + selectedClipId, + hoveredClipId, + selectionStyle + } = opts; + + if (endMs <= effectiveStartMs || startMs >= effectiveEndMs) return null; + + const clipPadding = 2; + const subtrackSpacing = 2; + const clipTrackY = baseTrackY + subtrackIndex * (subtrackHeight + subtrackSpacing); + const clipStartRatio = (startMs - effectiveStartMs) / (effectiveEndMs - effectiveStartMs); + const clipEndRatio = (endMs - effectiveStartMs) / (effectiveEndMs - effectiveStartMs); + const clipStartX = x + Math.max(0, clipStartRatio) * width; + const clipEndX = x + Math.min(1, clipEndRatio) * width; + + const baseColor = clip.color || ([80, 120, 160, 220] as [number, number, number, number]); + const isSelected = selectedClipId !== null && String(selectedClipId) === String(clipId); + const isHovered = hoveredClipId !== null && String(hoveredClipId) === String(clipId); + + let color = baseColor; + if (isSelected) { + color = selectionStyle.selectedClipColor!; + } else if (isHovered) { + color = lightenColor(baseColor, 40); + } + + return { + id: clipId, + clip, + track, + clipIndex, + trackIndex, + subtrackIndex, + polygon: [ + [clipStartX, clipTrackY + clipPadding], + [clipEndX, clipTrackY + clipPadding], + [clipEndX, clipTrackY + subtrackHeight - clipPadding], + [clipStartX, clipTrackY + subtrackHeight - clipPadding] + ], + color, + label: clip.label || '', + labelPosition: [clipStartX + (clipEndX - clipStartX) / 2, clipTrackY + subtrackHeight / 2, 0] + }; + } + + private _generateClipPolygons( + tracksWithSubtracks: TrackWithSubtracks[], + trackPositions: TrackPosition[], + effectiveStartMs: number, + effectiveEndMs: number + ): ClipPolygonData[] { + const { + x = 150, + width = 800, + selectedClipId, + hoveredClipId, + selectionStyle = defaultProps.selectionStyle + } = this.props; + + const subtrackSpacing = 2; + const clipPolygons: ClipPolygonData[] = []; + + for (let i = 0; i < tracksWithSubtracks.length; i++) { + const {track, trackIndex, clips, subtrackCount} = tracksWithSubtracks[i]; + const {y: baseTrackY, height: trackTotalHeight} = trackPositions[i]; + const subtrackHeight = + (trackTotalHeight - (subtrackCount - 1) * subtrackSpacing) / subtrackCount; + + for (let clipIndex = 0; clipIndex < clips.length; clipIndex++) { + const polygon = this._buildClipPolygon(clips[clipIndex], { + track, + trackIndex, + clipIndex, + subtrackHeight, + baseTrackY, + x, + width, + effectiveStartMs, + effectiveEndMs, + selectedClipId, + hoveredClipId, + selectionStyle + }); + if (polygon) { + clipPolygons.push(polygon); + } + } + } + + return clipPolygons; + } + + private _generateSubtrackSeparators( + tracksWithSubtracks: TrackWithSubtracks[], + trackPositions: TrackPosition[] + ): SeparatorLineData[] { + const {x = 150, width = 800, showSubtrackSeparators = true} = this.props; + if (!showSubtrackSeparators) return []; + + const subtrackSpacing = 2; + const separatorLines: SeparatorLineData[] = []; + + for (let i = 0; i < tracksWithSubtracks.length; i++) { + const {subtrackCount} = tracksWithSubtracks[i]; + if (subtrackCount <= 1) { + // No separators needed for single-subtrack rows + } else { + const {y: baseTrackY, height: trackTotalHeight} = trackPositions[i]; + const subtrackHeight = + (trackTotalHeight - (subtrackCount - 1) * subtrackSpacing) / subtrackCount; + + for (let j = 1; j < subtrackCount; j++) { + const separatorY = + baseTrackY + j * (subtrackHeight + subtrackSpacing) - subtrackSpacing / 2; + separatorLines.push({ + sourcePosition: [x, separatorY], + targetPosition: [x + width, separatorY] + }); + } + } + } + + return separatorLines; + } + + private _generateAxis( + totalTimelineHeight: number, + effectiveStartMs: number, + effectiveEndMs: number + ): {axisLines: AxisLineData[]; axisLabels: AxisLabelData[]} { + const { + x = 150, + y = 100, + width = 800, + showAxis = true, + timeFormatter = timeAxisFormatters.seconds + } = this.props; + + const axisLines: AxisLineData[] = []; + const axisLabels: AxisLabelData[] = []; + + if (!showAxis) return {axisLines, axisLabels}; + + const axisHeight = 30; + const tickCount = Math.max(4, Math.min(10, Math.floor(width / 80))); + + const timelineTicks = generateTimelineTicks({ + startMs: effectiveStartMs, + endMs: effectiveEndMs, + timelineX: x, + timelineWidth: width, + tickCount, + formatter: timeFormatter + }); + + const axisY = y + totalTimelineHeight + axisHeight; + + axisLines.push({sourcePosition: [x, axisY], targetPosition: [x + width, axisY]}); + + for (const tick of timelineTicks) { + axisLines.push({ + sourcePosition: [tick.position, axisY - 5], + targetPosition: [tick.position, axisY + 5] + }); + axisLabels.push({text: tick.label, position: [tick.position, axisY + 15, 0]}); + } + + return {axisLines, axisLabels}; + } + + private _generateScrubber( + totalTimelineHeight: number, + effectiveStartMs: number, + effectiveEndMs: number + ): { + scrubberLine: ScrubberLineData[]; + scrubberHandle: ScrubberHandleData[]; + scrubberLabel: ScrubberLabelData[]; + } { + const { + x = 150, + y = 100, + width = 800, + showScrubber = true, + currentTimeMs = 0, + timeFormatter = timeAxisFormatters.seconds + } = this.props; + + if (!showScrubber) return {scrubberLine: [], scrubberHandle: [], scrubberLabel: []}; + + const scrubberPosition = timeToPosition( + currentTimeMs, + x, + width, + effectiveStartMs, + effectiveEndMs + ); + + const scrubberLine: ScrubberLineData[] = [ + { + sourcePosition: [scrubberPosition, y - 30], + targetPosition: [scrubberPosition, y + totalTimelineHeight + 30] + } + ]; + + const scrubberHandle: ScrubberHandleData[] = [ + { + id: 'scrubber-handle', + polygon: [ + [scrubberPosition - 8, y - 35], + [scrubberPosition + 8, y - 35], + [scrubberPosition + 8, y - 20], + [scrubberPosition - 8, y - 20] + ], + color: [255, 100, 100, 255] + } + ]; + + const scrubberLabel: ScrubberLabelData[] = [ + {text: timeFormatter(currentTimeMs), position: [scrubberPosition, y - 40, 0]} + ]; + + return {scrubberLine, scrubberHandle, scrubberLabel}; + } + + // ===== LAYER CREATION ===== + + private _createTrackLayers( + trackBackgrounds: TrackBackgroundData[], + trackLabels: TrackLabelData[] + ): Layer[] { + const {trackProps, showTrackLabels = true, onTrackClick, onTrackHover} = this.props; + const layers: Layer[] = []; + + layers.push( + new SolidPolygonLayer( + this.getSubLayerProps({ + ...trackProps, + id: 'tracks', + data: trackBackgrounds, + getPolygon: (d: TrackBackgroundData) => d.polygon, + getFillColor: (d: TrackBackgroundData) => d.color, + stroked: true, + getLineColor: [100, 100, 100, 255], + getLineWidth: 1, + pickable: Boolean(onTrackClick) || Boolean(onTrackHover), + onClick: (info: PickingInfo) => { + if (info.object && onTrackClick) { + const obj = info.object as TrackBackgroundData; + onTrackClick({track: obj.track, index: obj.trackIndex}, info); + } + }, + onHover: (info: PickingInfo) => { + if (onTrackHover) { + const obj = info.object as TrackBackgroundData | undefined; + onTrackHover(obj ? {track: obj.track, index: obj.trackIndex} : null, info); + } + } + }) + ) + ); + + if (showTrackLabels) { + layers.push( + new TextLayer({ + id: `${this.props.id}-track-labels`, + data: trackLabels, + getText: (d: TrackLabelData) => d.text, + getPosition: (d: TrackLabelData) => d.position, + getSize: 12, + getColor: [60, 60, 60, 255], + getTextAnchor: 'end', + getAlignmentBaseline: 'center', + fontFamily: 'Arial, sans-serif', + fontWeight: 'bold', + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN + }) + ); + } + + return layers; + } + + private _createClipLayers( + clipPolygons: ClipPolygonData[], + clipLabelsData: ClipLabelData[] + ): Layer[] { + const { + clipProps, + showClipLabels = true, + selectedClipId, + selectionStyle = defaultProps.selectionStyle, + onClipClick, + onClipHover + } = this.props; + const layers: Layer[] = []; + + layers.push( + new SolidPolygonLayer( + this.getSubLayerProps({ + ...clipProps, + id: 'clips', + data: clipPolygons, + getPolygon: (d: ClipPolygonData) => d.polygon, + getFillColor: (d: ClipPolygonData) => d.color, + stroked: true, + getLineColor: [255, 255, 255, 200], + getLineWidth: (d: ClipPolygonData) => { + const isSelected = selectedClipId !== null && String(selectedClipId) === String(d.id); + return isSelected ? selectionStyle.selectedLineWidth || 3 : 2; + }, + pickable: Boolean(onClipClick) || Boolean(onClipHover), + autoHighlight: true, + onClick: (info: PickingInfo) => { + if (info.object && onClipClick) { + const obj = info.object as ClipPolygonData; + onClipClick( + { + clip: obj.clip, + track: obj.track, + clipIndex: obj.clipIndex, + trackIndex: obj.trackIndex, + subtrackIndex: obj.subtrackIndex + }, + info + ); + } + }, + onHover: (info: PickingInfo) => { + if (onClipHover) { + const obj = info.object as ClipPolygonData | undefined; + onClipHover( + obj + ? { + clip: obj.clip, + track: obj.track, + clipIndex: obj.clipIndex, + trackIndex: obj.trackIndex, + subtrackIndex: obj.subtrackIndex + } + : null, + info + ); + } + } + }) + ) + ); + + if (showClipLabels) { + layers.push( + new TextLayer({ + id: `${this.props.id}-clip-labels`, + data: clipLabelsData, + getText: (d: ClipLabelData) => d.text, + getPosition: (d: ClipLabelData) => d.position, + getSize: 10, + getColor: [255, 255, 255, 255], + getTextAnchor: 'middle', + getAlignmentBaseline: 'center', + fontFamily: 'Arial, sans-serif', + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN + }) + ); + } + + return layers; + } + + private _createSubtrackSeparatorLayer(separators: SeparatorLineData[]): Layer | null { + const {showSubtrackSeparators = true} = this.props; + if (!showSubtrackSeparators) return null; + + return new LineLayer({ + id: `${this.props.id}-subtrack-separators`, + data: separators, + getSourcePosition: (d: SeparatorLineData) => d.sourcePosition, + getTargetPosition: (d: SeparatorLineData) => d.targetPosition, + getColor: [180, 180, 180, 128], + getWidth: 1, + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN + }); + } + + private _createAxisLayers(axisLines: AxisLineData[], axisLabels: AxisLabelData[]): Layer[] { + const {axisLineProps, axisLabelProps, showAxis = true} = this.props; + if (!showAxis) return []; + + return [ + new LineLayer( + this.getSubLayerProps({ + ...axisLineProps, + id: 'axis-lines', + data: axisLines, + getSourcePosition: (d: AxisLineData) => d.sourcePosition, + getTargetPosition: (d: AxisLineData) => d.targetPosition, + getColor: [150, 150, 150, 255], + getWidth: 2 + }) + ), + new TextLayer( + this.getSubLayerProps({ + ...axisLabelProps, + id: 'axis-labels', + data: axisLabels, + getText: (d: AxisLabelData) => d.text, + getPosition: (d: AxisLabelData) => d.position, + getSize: 11, + getColor: [150, 150, 150, 255], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top' + }) + ) + ]; + } + + private _createScrubberLayers( + scrubberLine: ScrubberLineData[], + scrubberHandle: ScrubberHandleData[], + scrubberLabel: ScrubberLabelData[] + ): Layer[] { + const { + scrubberLineProps, + showScrubber = true, + onScrubberDragStart, + onScrubberHover + } = this.props; + if (!showScrubber) return []; + + return [ + new LineLayer( + this.getSubLayerProps({ + ...scrubberLineProps, + id: 'scrubber-line', + data: scrubberLine, + getSourcePosition: (d: ScrubberLineData) => d.sourcePosition, + getTargetPosition: (d: ScrubberLineData) => d.targetPosition, + getColor: [255, 100, 100, 255], + getWidth: 2 + }) + ), + new SolidPolygonLayer( + this.getSubLayerProps({ + id: 'scrubber-handle', + data: scrubberHandle, + getPolygon: (d: ScrubberHandleData) => d.polygon, + getFillColor: (d: ScrubberHandleData) => d.color, + stroked: true, + getLineColor: [255, 255, 255, 255], + getLineWidth: 2, + pickable: true, + onClick: (info: PickingInfo) => { + if (onScrubberDragStart && info.object) { + onScrubberDragStart(info); + } + }, + onHover: (info: PickingInfo) => { + if (onScrubberHover) { + onScrubberHover(Boolean(info.object), info); + } + } + }) + ), + new TextLayer( + this.getSubLayerProps({ + id: 'scrubber-label', + data: scrubberLabel, + getText: (d: ScrubberLabelData) => d.text, + getPosition: (d: ScrubberLabelData) => d.position, + getSize: 11, + getColor: [255, 100, 100, 255], + getTextAnchor: 'middle', + getAlignmentBaseline: 'bottom' + }) + ) + ]; + } + + // ===== MAIN RENDER ===== + + renderLayers(): Layer[] { + const { + data: tracks, + timelineStart, + timelineEnd, + viewport, + y = 100, + trackHeight = 40, + trackSpacing = 10 + } = this.props; + + const effectiveStartMs = viewport?.startMs ?? timelineStart; + const effectiveEndMs = viewport?.endMs ?? timelineEnd; + + const visibleTracks = tracks.filter((track) => track.visible !== false); + + const tracksWithSubtracks: TrackWithSubtracks[] = visibleTracks.map((track, trackIndex) => { + const clips = track.clips || []; + const clipsWithSubtracks = assignClipsToSubtracks(clips); + const subtrackCount = Math.max(1, calculateSubtrackCount(clips)); + return {track, trackIndex, clips: clipsWithSubtracks, subtrackCount}; + }); + + const {trackPositions, totalTimelineHeight} = this._calculateTrackPositions( + tracksWithSubtracks, + y, + trackHeight, + trackSpacing + ); + + const trackBackgrounds = this._generateTrackBackgrounds(tracksWithSubtracks, trackPositions); + const trackLabels = this._generateTrackLabels(tracksWithSubtracks, trackPositions); + const clipPolygons = this._generateClipPolygons( + tracksWithSubtracks, + trackPositions, + effectiveStartMs, + effectiveEndMs + ); + const clipLabelsData = clipPolygons.map((clip) => ({ + text: clip.label, + position: clip.labelPosition + })); + const subtrackSeparators = this._generateSubtrackSeparators( + tracksWithSubtracks, + trackPositions + ); + const {axisLines, axisLabels} = this._generateAxis( + totalTimelineHeight, + effectiveStartMs, + effectiveEndMs + ); + const {scrubberLine, scrubberHandle, scrubberLabel} = this._generateScrubber( + totalTimelineHeight, + effectiveStartMs, + effectiveEndMs + ); + + const layers = [ + ...this._createTrackLayers(trackBackgrounds, trackLabels), + ...this._createClipLayers(clipPolygons, clipLabelsData), + this._createSubtrackSeparatorLayer(subtrackSeparators), + ...this._createAxisLayers(axisLines, axisLabels), + ...this._createScrubberLayers(scrubberLine, scrubberHandle, scrubberLabel) + ].filter((layer): layer is Layer => layer !== null); + + return layers; + } +} diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-layout.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-layout.ts new file mode 100644 index 000000000..554ac513f --- /dev/null +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-layout.ts @@ -0,0 +1,80 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +// ===== STYLE TYPES ===== + +export type SelectionStyle = { + selectedClipColor?: [number, number, number, number]; + hoveredClipColor?: [number, number, number, number]; + selectedTrackColor?: [number, number, number, number]; + hoveredTrackColor?: [number, number, number, number]; + selectedLineWidth?: number; + hoveredLineWidth?: number; +}; + +export type TimelineLayout = { + x?: number; + y?: number; + width?: number; + trackHeight?: number; + trackSpacing?: number; + showTopAxis?: boolean; + showBottomAxis?: boolean; + axisHeight?: number; + axisTickHeight?: number; + axisLabelSize?: number; + showVerticalGrid?: boolean; + showTrackLabels?: boolean; + trackLabelWidth?: number; + scrubberWidth?: number; + scrubberHandleSize?: number; + scrubberLabelSize?: number; + clipPadding?: number; + clipLabelSize?: number; + showClipLabels?: boolean; + clipLabelsMinZoom?: number; + maxClipLabels?: number; + backgroundColor?: [number, number, number, number]; + trackBackgroundColor?: [number, number, number, number]; + trackBorderColor?: [number, number, number, number]; + scrubberColor?: [number, number, number, number]; + axisColor?: [number, number, number, number]; + gridColor?: [number, number, number, number]; + selectedClipColor?: [number, number, number, number]; + hoveredClipColor?: [number, number, number, number]; +}; + +// ===== CONSTANTS ===== + +export const DEFAULT_TIMELINE_LAYOUT: Required = { + x: 50, + y: 100, + width: 800, + trackHeight: 60, + trackSpacing: 10, + showTopAxis: true, + showBottomAxis: false, + axisHeight: 30, + axisTickHeight: 10, + axisLabelSize: 11, + showVerticalGrid: true, + showTrackLabels: true, + trackLabelWidth: 100, + scrubberWidth: 3, + scrubberHandleSize: 16, + scrubberLabelSize: 10, + clipPadding: 5, + clipLabelSize: 10, + showClipLabels: true, + clipLabelsMinZoom: 1.5, + maxClipLabels: 1000, + backgroundColor: [255, 255, 255, 0], + trackBackgroundColor: [240, 240, 240, 100], + trackBorderColor: [200, 200, 200, 255], + scrubberColor: [255, 69, 0, 255], + axisColor: [50, 50, 50, 255], + gridColor: [200, 200, 200, 128], + selectedClipColor: [255, 255, 0, 220], + hoveredClipColor: [255, 255, 255, 255] +}; diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts new file mode 100644 index 000000000..161d91da2 --- /dev/null +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts @@ -0,0 +1,146 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +// ===== CORE DATA TYPES ===== + +export type TimelineClip = { + id: string | number; + startMs: number; + endMs: number; + color?: [number, number, number, number]; + label?: string; + subtrackIndex?: number; + [key: string]: unknown; +}; + +export type TimelineTrack = { + id: string | number; + clips: TimelineClip[]; + visible?: boolean; + name?: string; + [key: string]: unknown; +}; + +// ===== INTERNAL DATA TYPES ===== + +/** A clip that has been assigned to a specific subtrack by the collision-detection algorithm */ +export type ClipWithSubtrack = TimelineClip & { + subtrackIndex: number; +}; + +export type TrackWithSubtracks = { + track: TimelineTrack; + trackIndex: number; + clips: ClipWithSubtrack[]; + subtrackCount: number; +}; + +export type TrackPosition = { + y: number; + height: number; + subtrackCount: number; +}; + +export type TrackBackgroundData = { + id: string; + track: TimelineTrack; + trackIndex: number; + polygon: [number, number][]; + color: [number, number, number, number]; +}; + +export type TrackLabelData = { + text: string; + position: [number, number, number]; +}; + +export type ClipPolygonData = { + id: string | number; + clip: TimelineClip; + track: TimelineTrack; + clipIndex: number; + trackIndex: number; + subtrackIndex: number; + polygon: [number, number][]; + color: [number, number, number, number]; + label: string; + labelPosition: [number, number, number]; +}; + +export type ClipLabelData = { + text: string; + position: [number, number, number]; +}; + +export type SeparatorLineData = { + sourcePosition: [number, number]; + targetPosition: [number, number]; +}; + +export type AxisLineData = { + sourcePosition: [number, number]; + targetPosition: [number, number]; +}; + +export type AxisLabelData = { + text: string; + position: [number, number, number]; +}; + +export type ScrubberLineData = { + sourcePosition: [number, number]; + targetPosition: [number, number]; +}; + +export type ScrubberHandleData = { + id: string; + polygon: [number, number][]; + color: [number, number, number, number]; +}; + +export type ScrubberLabelData = { + text: string; + position: [number, number, number]; +}; + +export type TimelineTick = { + position: number; + timeMs: number; + label: string; +}; + +// ===== CALLBACK INFO TYPES ===== + +export type TimelineViewport = { + startMs?: number; + endMs?: number; +}; + +export type TimelineClipInfo = { + clip: TimelineClip; + track: TimelineTrack; + clipIndex: number; + trackIndex: number; + subtrackIndex: number; +}; + +export type TimelineTrackInfo = { + track: TimelineTrack; + index: number; +}; + +export type TimelineScrubberInfo = { + timeMs: number; + isDragging: boolean; +}; + +export type TimelineViewportInfo = { + startMs: number; + endMs: number; + zoomLevel: number; +}; + +// ===== FORMATTER TYPE ===== + +export type TimeAxisLabelFormatter = (timeMs: number) => string; diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-utils.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-utils.ts new file mode 100644 index 000000000..2b1becc09 --- /dev/null +++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-utils.ts @@ -0,0 +1,85 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import type {TimeAxisLabelFormatter, TimelineTick} from './timeline-types'; + +// ===== TIME FORMATTERS ===== + +export const timeAxisFormatters = { + seconds: (timeMs: number): string => `${(timeMs / 1000).toFixed(1)}s`, + timestamp: (timeMs: number): string => new Date(timeMs).toLocaleTimeString(), + minutesSeconds: (timeMs: number): string => { + const totalSeconds = Math.floor(timeMs / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + }, + hoursMinutesSeconds: (timeMs: number): string => { + const totalSeconds = Math.floor(timeMs / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } +}; + +// ===== TIMELINE HELPERS ===== + +export type GenerateTicksOptions = { + startMs: number; + endMs: number; + timelineX: number; + timelineWidth: number; + tickCount: number; + formatter: TimeAxisLabelFormatter; +}; + +/** + * Generate timeline axis ticks + */ +export function generateTimelineTicks(opts: GenerateTicksOptions): TimelineTick[] { + const {startMs, endMs, timelineX, timelineWidth, tickCount, formatter} = opts; + const ticks: TimelineTick[] = []; + const timeRange = endMs - startMs; + const step = timeRange / (tickCount - 1); + + for (let i = 0; i < tickCount; i++) { + const timeMs = startMs + i * step; + const position = timelineX + (i / (tickCount - 1)) * timelineWidth; + const label = formatter(timeMs); + ticks.push({position, timeMs, label}); + } + + return ticks; +} + +/** + * Convert canvas/pixel position to time + */ +export function positionToTime( + x: number, + timelineX: number, + timelineWidth: number, + startMs: number, + endMs: number +): number { + const ratio = (x - timelineX) / timelineWidth; + const clampedRatio = Math.max(0, Math.min(1, ratio)); + return startMs + clampedRatio * (endMs - startMs); +} + +/** + * Convert time to canvas/pixel position + */ +export function timeToPosition( + timeMs: number, + timelineX: number, + timelineWidth: number, + startMs: number, + endMs: number +): number { + const timeRatio = (timeMs - startMs) / (endMs - startMs); + const clampedRatio = Math.max(0, Math.min(1, timeRatio)); + return timelineX + clampedRatio * timelineWidth; +} diff --git a/modules/timeline-layers/src/layers/vertical-grid-layer.ts b/dev/timeline-layers/src/layers/vertical-grid-layer.ts similarity index 100% rename from modules/timeline-layers/src/layers/vertical-grid-layer.ts rename to dev/timeline-layers/src/layers/vertical-grid-layer.ts diff --git a/modules/timeline-layers/src/utils/format-utils.ts b/dev/timeline-layers/src/utils/format-utils.ts similarity index 100% rename from modules/timeline-layers/src/utils/format-utils.ts rename to dev/timeline-layers/src/utils/format-utils.ts diff --git a/modules/timeline-layers/src/utils/tick-utils.ts b/dev/timeline-layers/src/utils/tick-utils.ts similarity index 100% rename from modules/timeline-layers/src/utils/tick-utils.ts rename to dev/timeline-layers/src/utils/tick-utils.ts diff --git a/dev/timeline-layers/test/index.spec.ts b/dev/timeline-layers/test/index.spec.ts new file mode 100644 index 000000000..712c4c1ce --- /dev/null +++ b/dev/timeline-layers/test/index.spec.ts @@ -0,0 +1,50 @@ +import {describe, it, expect} from 'vitest'; +import { + HorizonGraphLayer, + MultiHorizonGraphLayer, + TimeAxisLayer, + VerticalGridLayer, + TimelineLayer, + formatTimeMs, + timeAxisFormatters, + positionToTime, + timeToPosition +} from '../src'; + +describe('@deck.gl-community/timeline-layers', () => { + it('exports HorizonGraph layers', () => { + expect(HorizonGraphLayer).toBeDefined(); + expect(MultiHorizonGraphLayer).toBeDefined(); + }); + + it('exports timeline utilities', () => { + expect(TimeAxisLayer).toBeDefined(); + expect(VerticalGridLayer).toBeDefined(); + expect(formatTimeMs).toBeTypeOf('function'); + }); + + it('exports TimelineLayer', () => { + expect(TimelineLayer).toBeDefined(); + }); + + it('exports time axis formatters', () => { + expect(timeAxisFormatters.seconds).toBeTypeOf('function'); + expect(timeAxisFormatters.seconds(5000)).toBe('5.0s'); + expect(timeAxisFormatters.minutesSeconds(90000)).toBe('1:30'); + }); + + it('exports position/time conversion utilities', () => { + expect(positionToTime).toBeTypeOf('function'); + expect(timeToPosition).toBeTypeOf('function'); + + // positionToTime: x=0 maps to startMs, x=width maps to endMs + expect(positionToTime(0, 0, 100, 0, 1000)).toBe(0); + expect(positionToTime(100, 0, 100, 0, 1000)).toBe(1000); + expect(positionToTime(50, 0, 100, 0, 1000)).toBe(500); + + // timeToPosition: startMs maps to x=0, endMs maps to x=width + expect(timeToPosition(0, 0, 100, 0, 1000)).toBe(0); + expect(timeToPosition(1000, 0, 100, 0, 1000)).toBe(100); + expect(timeToPosition(500, 0, 100, 0, 1000)).toBe(50); + }); +}); diff --git a/dev/timeline-layers/test/timeline-layer.spec.ts b/dev/timeline-layers/test/timeline-layer.spec.ts new file mode 100644 index 000000000..057d603bb --- /dev/null +++ b/dev/timeline-layers/test/timeline-layer.spec.ts @@ -0,0 +1,391 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {describe, it, expect, vi} from 'vitest'; +import { + assignClipsToSubtracks, + calculateSubtrackCount +} from '../src/layers/timeline-layer/timeline-collision'; +import { + positionToTime, + timeToPosition, + timeAxisFormatters, + generateTimelineTicks +} from '../src/layers/timeline-layer/timeline-utils'; +import {TimelineLayer} from '../src/layers/timeline-layer/timeline-layer'; +import type {TimelineClip, TimelineTrack} from '../src/layers/timeline-layer/timeline-types'; + +// ===== COLLISION DETECTION ===== + +describe('assignClipsToSubtracks', () => { + it('returns empty array for no clips', () => { + expect(assignClipsToSubtracks([])).toEqual([]); + }); + + it('assigns a single clip to subtrack 0', () => { + const clips: TimelineClip[] = [{id: 'a', startMs: 0, endMs: 1000}]; + const result = assignClipsToSubtracks(clips); + expect(result).toHaveLength(1); + expect(result[0].subtrackIndex).toBe(0); + }); + + it('places non-overlapping clips on the same subtrack', () => { + const clips: TimelineClip[] = [ + {id: 'a', startMs: 0, endMs: 500}, + {id: 'b', startMs: 500, endMs: 1000} + ]; + const result = assignClipsToSubtracks(clips); + const byId = Object.fromEntries(result.map((c) => [c.id, c.subtrackIndex])); + expect(byId.a).toBe(0); + expect(byId.b).toBe(0); + }); + + it('places overlapping clips on different subtracks', () => { + const clips: TimelineClip[] = [ + {id: 'a', startMs: 0, endMs: 1000}, + {id: 'b', startMs: 500, endMs: 1500} + ]; + const result = assignClipsToSubtracks(clips); + const byId = Object.fromEntries(result.map((c) => [c.id, c.subtrackIndex])); + expect(byId.a).not.toBe(byId.b); + }); + + it('packs three clips into two subtracks when third fits in first lane', () => { + // a: 0-500, b: 0-500 (overlaps a), c: 600-1000 (fits after a) + const clips: TimelineClip[] = [ + {id: 'a', startMs: 0, endMs: 500}, + {id: 'b', startMs: 0, endMs: 500}, + {id: 'c', startMs: 600, endMs: 1000} + ]; + const result = assignClipsToSubtracks(clips); + const subtracks = result.map((c) => c.subtrackIndex); + const uniqueSubtracks = new Set(subtracks); + expect(uniqueSubtracks.size).toBe(2); + }); + + it('sorts clips by startMs before assigning subtracks', () => { + // Provide clips in reverse order; the algorithm must still work correctly + const clips: TimelineClip[] = [ + {id: 'b', startMs: 1000, endMs: 2000}, + {id: 'a', startMs: 0, endMs: 500} + ]; + const result = assignClipsToSubtracks(clips); + const byId = Object.fromEntries(result.map((c) => [c.id, c.subtrackIndex])); + // Both fit on subtrack 0 since they don't overlap + expect(byId.a).toBe(0); + expect(byId.b).toBe(0); + }); + + it('preserves original clip properties', () => { + const clip: TimelineClip = { + id: 'x', + startMs: 100, + endMs: 200, + label: 'hello', + color: [1, 2, 3, 4] + }; + const [result] = assignClipsToSubtracks([clip]); + expect(result.id).toBe('x'); + expect(result.label).toBe('hello'); + expect(result.color).toEqual([1, 2, 3, 4]); + expect(result.subtrackIndex).toBeTypeOf('number'); + }); +}); + +describe('calculateSubtrackCount', () => { + it('returns 1 for empty clips array', () => { + expect(calculateSubtrackCount([])).toBe(1); + }); + + it('returns 1 for non-overlapping clips', () => { + const clips: TimelineClip[] = [ + {id: 'a', startMs: 0, endMs: 500}, + {id: 'b', startMs: 500, endMs: 1000} + ]; + expect(calculateSubtrackCount(clips)).toBe(1); + }); + + it('returns 2 for two fully overlapping clips', () => { + const clips: TimelineClip[] = [ + {id: 'a', startMs: 0, endMs: 1000}, + {id: 'b', startMs: 0, endMs: 1000} + ]; + expect(calculateSubtrackCount(clips)).toBe(2); + }); + + it('returns 3 for three mutually overlapping clips', () => { + const clips: TimelineClip[] = [ + {id: 'a', startMs: 0, endMs: 1000}, + {id: 'b', startMs: 0, endMs: 1000}, + {id: 'c', startMs: 0, endMs: 1000} + ]; + expect(calculateSubtrackCount(clips)).toBe(3); + }); +}); + +// ===== TIME / POSITION MATH ===== + +describe('positionToTime', () => { + it('maps left edge to startMs', () => { + expect(positionToTime(0, 0, 100, 0, 1000)).toBe(0); + }); + + it('maps right edge to endMs', () => { + expect(positionToTime(100, 0, 100, 0, 1000)).toBe(1000); + }); + + it('maps midpoint correctly', () => { + expect(positionToTime(50, 0, 100, 0, 1000)).toBe(500); + }); + + it('clamps to startMs when x is before timeline', () => { + expect(positionToTime(-10, 0, 100, 0, 1000)).toBe(0); + }); + + it('clamps to endMs when x is beyond timeline', () => { + expect(positionToTime(200, 0, 100, 0, 1000)).toBe(1000); + }); + + it('respects non-zero timelineX offset', () => { + // Timeline starts at x=50, width=100 → x=100 is midpoint + expect(positionToTime(100, 50, 100, 0, 1000)).toBe(500); + }); + + it('works with non-zero startMs', () => { + expect(positionToTime(50, 0, 100, 5000, 6000)).toBe(5500); + }); +}); + +describe('timeToPosition', () => { + it('maps startMs to left edge', () => { + expect(timeToPosition(0, 0, 100, 0, 1000)).toBe(0); + }); + + it('maps endMs to right edge', () => { + expect(timeToPosition(1000, 0, 100, 0, 1000)).toBe(100); + }); + + it('maps midpoint correctly', () => { + expect(timeToPosition(500, 0, 100, 0, 1000)).toBe(50); + }); + + it('clamps when time is before startMs', () => { + expect(timeToPosition(-100, 0, 100, 0, 1000)).toBe(0); + }); + + it('clamps when time is after endMs', () => { + expect(timeToPosition(9000, 0, 100, 0, 1000)).toBe(100); + }); + + it('round-trips with positionToTime', () => { + const timeMs = 750; + const x = timeToPosition(timeMs, 0, 800, 0, 60000); + expect(positionToTime(x, 0, 800, 0, 60000)).toBeCloseTo(timeMs); + }); +}); + +// ===== TIME AXIS FORMATTERS ===== + +describe('timeAxisFormatters', () => { + describe('seconds', () => { + it('formats 0ms as 0.0s', () => { + expect(timeAxisFormatters.seconds(0)).toBe('0.0s'); + }); + + it('formats 5000ms as 5.0s', () => { + expect(timeAxisFormatters.seconds(5000)).toBe('5.0s'); + }); + + it('formats 1500ms as 1.5s', () => { + expect(timeAxisFormatters.seconds(1500)).toBe('1.5s'); + }); + }); + + describe('minutesSeconds', () => { + it('formats 0ms as 0:00', () => { + expect(timeAxisFormatters.minutesSeconds(0)).toBe('0:00'); + }); + + it('formats 90000ms as 1:30', () => { + expect(timeAxisFormatters.minutesSeconds(90000)).toBe('1:30'); + }); + + it('formats 3600000ms as 60:00', () => { + expect(timeAxisFormatters.minutesSeconds(3600000)).toBe('60:00'); + }); + + it('pads seconds with leading zero', () => { + expect(timeAxisFormatters.minutesSeconds(65000)).toBe('1:05'); + }); + }); + + describe('hoursMinutesSeconds', () => { + it('formats 0ms as 0:00:00', () => { + expect(timeAxisFormatters.hoursMinutesSeconds(0)).toBe('0:00:00'); + }); + + it('formats 3661000ms as 1:01:01', () => { + expect(timeAxisFormatters.hoursMinutesSeconds(3661000)).toBe('1:01:01'); + }); + + it('pads minutes and seconds with leading zeros', () => { + expect(timeAxisFormatters.hoursMinutesSeconds(3600000)).toBe('1:00:00'); + }); + }); +}); + +// ===== TICK GENERATION ===== + +describe('generateTimelineTicks', () => { + it('generates the requested number of ticks', () => { + const ticks = generateTimelineTicks({ + startMs: 0, + endMs: 1000, + timelineX: 0, + timelineWidth: 100, + tickCount: 5, + formatter: timeAxisFormatters.seconds + }); + expect(ticks).toHaveLength(5); + }); + + it('first tick is at timelineX with startMs', () => { + const ticks = generateTimelineTicks({ + startMs: 0, + endMs: 1000, + timelineX: 50, + timelineWidth: 200, + tickCount: 3, + formatter: timeAxisFormatters.seconds + }); + expect(ticks[0].position).toBe(50); + expect(ticks[0].timeMs).toBe(0); + }); + + it('last tick is at timelineX + width with endMs', () => { + const ticks = generateTimelineTicks({ + startMs: 0, + endMs: 1000, + timelineX: 50, + timelineWidth: 200, + tickCount: 3, + formatter: timeAxisFormatters.seconds + }); + expect(ticks[2].position).toBe(250); + expect(ticks[2].timeMs).toBe(1000); + }); + + it('applies the formatter to produce labels', () => { + const fmt = (ms: number) => `t=${ms}`; + const ticks = generateTimelineTicks({ + startMs: 0, + endMs: 1000, + timelineX: 0, + timelineWidth: 100, + tickCount: 3, + formatter: fmt + }); + expect(ticks.map((t) => t.label)).toEqual(['t=0', 't=500', 't=1000']); + }); +}); + +// ===== TIMELINE LAYER ===== + +describe('TimelineLayer', () => { + it('has correct static layerName', () => { + expect(TimelineLayer.layerName).toBe('TimelineLayer'); + }); + + it('has default props', () => { + expect(TimelineLayer.defaultProps).toBeDefined(); + expect(TimelineLayer.defaultProps.x).toBe(150); + expect(TimelineLayer.defaultProps.y).toBe(100); + expect(TimelineLayer.defaultProps.width).toBe(800); + expect(TimelineLayer.defaultProps.trackHeight).toBe(40); + expect(TimelineLayer.defaultProps.trackSpacing).toBe(10); + expect(TimelineLayer.defaultProps.showScrubber).toBe(true); + expect(TimelineLayer.defaultProps.showAxis).toBe(true); + expect(TimelineLayer.defaultProps.showClipLabels).toBe(true); + expect(TimelineLayer.defaultProps.showTrackLabels).toBe(true); + expect(TimelineLayer.defaultProps.showSubtrackSeparators).toBe(true); + }); +}); + +describe('TimelineLayer.zoomToPoint', () => { + function makeLayer( + overrides: Partial> = {} + ): { + layer: TimelineLayer; + viewportSpy: ReturnType; + zoomSpy: ReturnType; + } { + const viewportSpy = vi.fn(); + const zoomSpy = vi.fn(); + + const tracks: TimelineTrack[] = []; + const layer = new TimelineLayer({ + id: 'test', + data: tracks, + timelineStart: 0, + timelineEnd: 60000, + x: 0, + width: 600, + onViewportChange: viewportSpy, + onZoomChange: zoomSpy + }); + + return {layer, viewportSpy, zoomSpy}; + } + + it('fires onZoomChange with new zoom level', () => { + const {layer, zoomSpy} = makeLayer(); + layer.zoomToPoint(2.0, 300, 1.0); // zoom in 2× from centre + expect(zoomSpy).toHaveBeenCalledOnce(); + expect(zoomSpy.mock.calls[0][0]).toBeCloseTo(2.0); + }); + + it('clamps zoom level to a maximum of 100', () => { + const {layer, zoomSpy} = makeLayer(); + layer.zoomToPoint(1000, 300, 50.0); + expect(zoomSpy.mock.calls[0][0]).toBe(100); + }); + + it('clamps zoom level to a minimum of 1.0', () => { + const {layer, zoomSpy} = makeLayer(); + layer.zoomToPoint(0.0001, 300, 1.0); + expect(zoomSpy.mock.calls[0][0]).toBe(1.0); + }); + + it('fires onViewportChange when zooming in', () => { + const {layer, viewportSpy} = makeLayer(); + layer.zoomToPoint(2.0, 300, 1.0); + expect(viewportSpy).toHaveBeenCalledOnce(); + const [startMs, endMs] = viewportSpy.mock.calls[0]; + expect(endMs - startMs).toBeCloseTo(30000); // half the range + }); + + it('fires onViewportChange with full range when zooming back to 1×', () => { + const {layer, viewportSpy} = makeLayer(); + // Force zoom back to exactly 1.0 (factor 0.5 × 2.0 = 1.0) + layer.zoomToPoint(0.5, 300, 2.0); + expect(viewportSpy).toHaveBeenCalledOnce(); + const [startMs, endMs] = viewportSpy.mock.calls[0]; + expect(startMs).toBe(0); + expect(endMs).toBe(60000); + }); + + it('viewport stays within timeline bounds when zooming near left edge', () => { + const {layer, viewportSpy} = makeLayer(); + layer.zoomToPoint(4.0, 0, 1.0); // zoom in 4× at left edge + const [startMs] = viewportSpy.mock.calls[0]; + expect(startMs).toBeGreaterThanOrEqual(0); + }); + + it('viewport stays within timeline bounds when zooming near right edge', () => { + const {layer, viewportSpy} = makeLayer(); + layer.zoomToPoint(4.0, 600, 1.0); // zoom in 4× at right edge + const [, endMs] = viewportSpy.mock.calls[0]; + expect(endMs).toBeLessThanOrEqual(60000); + }); +}); diff --git a/modules/timeline-layers/tsconfig.json b/dev/timeline-layers/tsconfig.json similarity index 100% rename from modules/timeline-layers/tsconfig.json rename to dev/timeline-layers/tsconfig.json diff --git a/dev/timeline-layers/vite.config.local.mjs b/dev/timeline-layers/vite.config.local.mjs new file mode 100644 index 000000000..e8fda4224 --- /dev/null +++ b/dev/timeline-layers/vite.config.local.mjs @@ -0,0 +1,35 @@ +// Allows examples in dev/timeline-layers/examples/ to be built against +// the source code in this repo instead of installed node_modules. + +import {defineConfig} from 'vite'; +import {getOcularConfig} from '@vis.gl/dev-tools'; +import {join} from 'path'; + +const rootDir = join(__dirname, '../..'); + +/** https://vitejs.dev/config/ */ +export default defineConfig(async () => { + const {aliases} = await getOcularConfig({root: rootDir}); + + return { + resolve: { + alias: { + ...aliases, + '@deck.gl-community/timeline-layers': join(rootDir, './dev/timeline-layers/src'), + '@deck.gl': join(rootDir, './node_modules/@deck.gl'), + '@luma.gl': join(rootDir, './node_modules/@luma.gl'), + '@math.gl': join(rootDir, './node_modules/@math.gl'), + '@loaders.gl': join(rootDir, './node_modules/@loaders.gl'), + 'react': join(rootDir, './node_modules/react'), + 'react-dom': join(rootDir, './node_modules/react-dom'), + } + }, + server: { + open: true, + port: 8080 + }, + optimizeDeps: { + esbuildOptions: {target: 'es2020'} + } + }; +}); diff --git a/lerna.json b/lerna.json index 19063139e..a2519130e 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,8 @@ "version": "9.2.8", "npmClient": "yarn", "packages": [ - "modules/*" + "modules/*", + "dev/*" ], "publishConfig": { "access": "public" diff --git a/modules/editable-layers/test/immutable-feature-collection.node.spec.ts b/modules/editable-layers/test/immutable-feature-collection.node.spec.ts new file mode 100644 index 000000000..fa9c6f413 --- /dev/null +++ b/modules/editable-layers/test/immutable-feature-collection.node.spec.ts @@ -0,0 +1,272 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {describe, it, expect} from 'vitest'; +import {ImmutableFeatureCollection} from '../src/edit-modes/immutable-feature-collection'; +import type {SimpleFeatureCollection} from '../src/utils/geojson-types'; + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +function makeCollection(features: SimpleFeatureCollection['features']): ImmutableFeatureCollection { + return new ImmutableFeatureCollection({type: 'FeatureCollection', features}); +} + +const POINT_FEATURE = { + type: 'Feature' as const, + geometry: {type: 'Point' as const, coordinates: [1, 2]}, + properties: {} +}; + +const LINE_FEATURE = { + type: 'Feature' as const, + geometry: { + type: 'LineString' as const, + coordinates: [ + [0, 0], + [1, 1], + [2, 2] + ] + }, + properties: {} +}; + +const POLYGON_FEATURE = { + type: 'Feature' as const, + geometry: { + type: 'Polygon' as const, + // outer ring (closed) + coordinates: [ + [ + [0, 0], + [1, 0], + [1, 1], + [0, 1], + [0, 0] + ] + ] + }, + properties: {} +}; + +// --------------------------------------------------------------------------- +// addFeature / addFeatures +// --------------------------------------------------------------------------- + +describe('addFeature', () => { + it('appends a feature and returns a new collection', () => { + const fc = makeCollection([POINT_FEATURE]); + const updated = fc.addFeature(LINE_FEATURE); + + expect(updated.featureCollection.features).toHaveLength(2); + // original is unchanged + expect(fc.featureCollection.features).toHaveLength(1); + }); + + it('addFeatures appends multiple features', () => { + const fc = makeCollection([]); + const updated = fc.addFeatures([POINT_FEATURE, LINE_FEATURE]); + + expect(updated.featureCollection.features).toHaveLength(2); + }); +}); + +// --------------------------------------------------------------------------- +// deleteFeature / deleteFeatures +// --------------------------------------------------------------------------- + +describe('deleteFeature', () => { + it('removes a feature by index', () => { + const fc = makeCollection([POINT_FEATURE, LINE_FEATURE, POLYGON_FEATURE]); + const updated = fc.deleteFeature(1); + + expect(updated.featureCollection.features).toHaveLength(2); + expect(updated.featureCollection.features[0].geometry.type).toBe('Point'); + expect(updated.featureCollection.features[1].geometry.type).toBe('Polygon'); + }); + + it('deleteFeatures removes multiple features in correct order', () => { + const fc = makeCollection([POINT_FEATURE, LINE_FEATURE, POLYGON_FEATURE]); + const updated = fc.deleteFeatures([0, 2]); + + expect(updated.featureCollection.features).toHaveLength(1); + expect(updated.featureCollection.features[0].geometry.type).toBe('LineString'); + }); + + it('ignores out-of-range indexes', () => { + const fc = makeCollection([POINT_FEATURE]); + const updated = fc.deleteFeature(99); + + expect(updated.featureCollection.features).toHaveLength(1); + }); +}); + +// --------------------------------------------------------------------------- +// replaceGeometry +// --------------------------------------------------------------------------- + +describe('replaceGeometry', () => { + it('replaces the geometry of a feature', () => { + const fc = makeCollection([POINT_FEATURE, LINE_FEATURE]); + const newGeom = {type: 'Point' as const, coordinates: [99, 99]}; + const updated = fc.replaceGeometry(0, newGeom); + + expect((updated.featureCollection.features[0].geometry as any).coordinates).toEqual([99, 99]); + // feature 1 is unchanged + expect(updated.featureCollection.features[1].geometry.type).toBe('LineString'); + // original is unchanged + expect((fc.featureCollection.features[0].geometry as any).coordinates).toEqual([1, 2]); + }); +}); + +// --------------------------------------------------------------------------- +// replacePosition — LineString +// --------------------------------------------------------------------------- + +describe('replacePosition (LineString)', () => { + it('replaces a coordinate at a given index', () => { + const fc = makeCollection([LINE_FEATURE]); + const updated = fc.replacePosition(0, [1], [9, 9]); + + const coords = (updated.featureCollection.features[0].geometry as any).coordinates; + expect(coords[1]).toEqual([9, 9]); + // other positions unchanged + expect(coords[0]).toEqual([0, 0]); + expect(coords[2]).toEqual([2, 2]); + }); + + it('preserves elevation when updating 2D position over a 3D point', () => { + const feature3D = { + type: 'Feature' as const, + geometry: { + type: 'LineString' as const, + coordinates: [ + [0, 0, 10], + [1, 1, 20] + ] + }, + properties: {} + }; + const fc = makeCollection([feature3D]); + const updated = fc.replacePosition(0, [0], [5, 5]); // no elevation given + + const coords = (updated.featureCollection.features[0].geometry as any).coordinates; + expect(coords[0]).toEqual([5, 5, 10]); // elevation copied from original + }); + + it('is immutable — original collection unchanged', () => { + const fc = makeCollection([LINE_FEATURE]); + fc.replacePosition(0, [0], [99, 99]); + + const coords = (fc.featureCollection.features[0].geometry as any).coordinates; + expect(coords[0]).toEqual([0, 0]); + }); +}); + +// --------------------------------------------------------------------------- +// replacePosition — Polygon (wraps first/last) +// --------------------------------------------------------------------------- + +describe('replacePosition (Polygon)', () => { + it('updates both first and last coordinate when replacing index 0', () => { + const fc = makeCollection([POLYGON_FEATURE]); + const updated = fc.replacePosition(0, [0, 0], [5, 5]); + + const ring = (updated.featureCollection.features[0].geometry as any).coordinates[0]; + expect(ring[0]).toEqual([5, 5]); + expect(ring[ring.length - 1]).toEqual([5, 5]); + }); +}); + +// --------------------------------------------------------------------------- +// addPosition +// --------------------------------------------------------------------------- + +describe('addPosition (LineString)', () => { + it('inserts a position at the given index', () => { + const fc = makeCollection([LINE_FEATURE]); + const updated = fc.addPosition(0, [1], [0.5, 0.5]); + + const coords = (updated.featureCollection.features[0].geometry as any).coordinates; + expect(coords).toHaveLength(4); + expect(coords[1]).toEqual([0.5, 0.5]); + expect(coords[2]).toEqual([1, 1]); + }); + + it('throws when adding a position to a Point', () => { + const fc = makeCollection([POINT_FEATURE]); + expect(() => fc.addPosition(0, [0], [1, 1])).toThrow(); + }); +}); + +// --------------------------------------------------------------------------- +// removePosition +// --------------------------------------------------------------------------- + +describe('removePosition (LineString)', () => { + it('removes a position from a LineString', () => { + const fc = makeCollection([LINE_FEATURE]); + const updated = fc.removePosition(0, [1]); + + const coords = (updated.featureCollection.features[0].geometry as any).coordinates; + expect(coords).toHaveLength(2); + expect(coords[0]).toEqual([0, 0]); + expect(coords[1]).toEqual([2, 2]); + }); + + it('throws when removing from a Point', () => { + const fc = makeCollection([POINT_FEATURE]); + expect(() => fc.removePosition(0, [0])).toThrow(); + }); + + it('throws when LineString would have fewer than 2 positions', () => { + const twoPointLine = { + type: 'Feature' as const, + geometry: { + type: 'LineString' as const, + coordinates: [ + [0, 0], + [1, 1] + ] + }, + properties: {} + }; + const fc = makeCollection([twoPointLine]); + expect(() => fc.removePosition(0, [0])).toThrow(); + }); + + it('throws when Polygon outer ring would have fewer than 4 positions', () => { + const triangle = { + type: 'Feature' as const, + geometry: { + type: 'Polygon' as const, + coordinates: [ + [ + [0, 0], + [1, 0], + [0.5, 1], + [0, 0] + ] + ] + }, + properties: {} + }; + const fc = makeCollection([triangle]); + expect(() => fc.removePosition(0, [0, 0])).toThrow(); + }); +}); + +// --------------------------------------------------------------------------- +// getObject +// --------------------------------------------------------------------------- + +describe('getObject', () => { + it('returns the underlying feature collection', () => { + const raw = {type: 'FeatureCollection' as const, features: [POINT_FEATURE]}; + const fc = new ImmutableFeatureCollection(raw); + + expect(fc.getObject()).toBe(raw); + }); +}); diff --git a/modules/editable-layers/test/imports.node.spec.ts b/modules/editable-layers/test/imports.node.spec.ts new file mode 100644 index 000000000..c9fdff2cc --- /dev/null +++ b/modules/editable-layers/test/imports.node.spec.ts @@ -0,0 +1,61 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +// Smoke test: verify the public API surface exports resolve without errors. +// This runs in Node (no WebGL) to catch broken re-exports early. + +import {it, expect} from 'vitest'; +import * as EditableLayers from '../src/index'; + +it('exports ImmutableFeatureCollection', () => { + expect(EditableLayers.ImmutableFeatureCollection).toBeDefined(); +}); + +it('exports GeoJsonEditMode', () => { + expect(EditableLayers.GeoJsonEditMode).toBeDefined(); +}); + +it('exports draw modes', () => { + const drawModes = [ + 'DrawPointMode', + 'DrawLineStringMode', + 'DrawPolygonMode', + 'DrawRectangleMode', + 'DrawCircleByDiameterMode', + 'DrawCircleFromCenterMode' + ] as const; + + for (const name of drawModes) { + expect(EditableLayers[name], `${name} should be exported`).toBeDefined(); + } +}); + +it('exports alter modes', () => { + const alterModes = [ + 'ModifyMode', + 'TranslateMode', + 'ScaleMode', + 'RotateMode', + 'DeleteMode', + 'DuplicateMode', + 'SplitPolygonMode', + 'TransformMode' + ] as const; + + for (const name of alterModes) { + expect(EditableLayers[name], `${name} should be exported`).toBeDefined(); + } +}); + +it('exports measurement modes', () => { + expect(EditableLayers.MeasureDistanceMode).toBeDefined(); + expect(EditableLayers.MeasureAreaMode).toBeDefined(); + expect(EditableLayers.MeasureAngleMode).toBeDefined(); +}); + +it('exports composite modes', () => { + expect(EditableLayers.CompositeMode).toBeDefined(); + expect(EditableLayers.SnappableMode).toBeDefined(); + expect(EditableLayers.ViewMode).toBeDefined(); +}); diff --git a/modules/geo-layers/test/imports.node.spec.ts b/modules/geo-layers/test/imports.node.spec.ts new file mode 100644 index 000000000..16c0e3e66 --- /dev/null +++ b/modules/geo-layers/test/imports.node.spec.ts @@ -0,0 +1,21 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {it, expect} from 'vitest'; +import * as GeoLayers from '../src/index'; + +it('exports TileSourceLayer', () => { + expect(GeoLayers.TileSourceLayer).toBeDefined(); +}); + +it('exports GlobalGridLayer', () => { + expect(GeoLayers.GlobalGridLayer).toBeDefined(); +}); + +it('exports grid systems', () => { + expect(GeoLayers.H3Grid).toBeDefined(); + expect(GeoLayers.S2Grid).toBeDefined(); + expect(GeoLayers.GeohashGrid).toBeDefined(); + expect(GeoLayers.QuadkeyGrid).toBeDefined(); +}); diff --git a/modules/layers/test/imports.node.spec.ts b/modules/layers/test/imports.node.spec.ts new file mode 100644 index 000000000..0feba5afc --- /dev/null +++ b/modules/layers/test/imports.node.spec.ts @@ -0,0 +1,14 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {it, expect} from 'vitest'; +import * as Layers from '../src/index'; + +it('exports PathOutlineLayer', () => { + expect(Layers.PathOutlineLayer).toBeDefined(); +}); + +it('exports PathMarkerLayer', () => { + expect(Layers.PathMarkerLayer).toBeDefined(); +}); diff --git a/modules/timeline-layers/src/index.ts b/modules/timeline-layers/src/index.ts deleted file mode 100644 index d65c9b9fe..000000000 --- a/modules/timeline-layers/src/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -// deck.gl-community -// SPDX-License-Identifier: MIT -// Copyright (c) vis.gl contributors - -export type {HorizonGraphLayerProps} from './layers/horizon-graph-layer/horizon-graph-layer'; -export {HorizonGraphLayer} from './layers/horizon-graph-layer/horizon-graph-layer'; -export type {MultiHorizonGraphLayerProps} from './layers/horizon-graph-layer/multi-horizon-graph-layer'; -export {MultiHorizonGraphLayer} from './layers/horizon-graph-layer/multi-horizon-graph-layer'; -export type {TimeAxisLayerProps} from './layers/time-axis-layer'; -export {TimeAxisLayer} from './layers/time-axis-layer'; -export type {VerticalGridLayerProps} from './layers/vertical-grid-layer'; -export {VerticalGridLayer} from './layers/vertical-grid-layer'; -export {formatTimeMs, formatTimeRangeMs} from './utils/format-utils'; diff --git a/modules/timeline-layers/test/index.spec.ts b/modules/timeline-layers/test/index.spec.ts deleted file mode 100644 index 62b62609f..000000000 --- a/modules/timeline-layers/test/index.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {describe, it, expect} from 'vitest'; -import { - HorizonGraphLayer, - MultiHorizonGraphLayer, - TimeAxisLayer, - VerticalGridLayer, - formatTimeMs -} from '../src'; - -describe('@deck.gl-community/timeline-layers', () => { - it('exports HorizonGraph layers', () => { - expect(HorizonGraphLayer).toBeDefined(); - expect(MultiHorizonGraphLayer).toBeDefined(); - }); - - it('exports timeline utilities', () => { - expect(TimeAxisLayer).toBeDefined(); - expect(VerticalGridLayer).toBeDefined(); - expect(formatTimeMs).toBeTypeOf('function'); - }); -}); diff --git a/modules/widgets/test/imports.node.spec.ts b/modules/widgets/test/imports.node.spec.ts new file mode 100644 index 000000000..6d8c2410d --- /dev/null +++ b/modules/widgets/test/imports.node.spec.ts @@ -0,0 +1,22 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {it, expect} from 'vitest'; +import * as Widgets from '../src/index'; + +it('exports PanWidget', () => { + expect(Widgets.PanWidget).toBeDefined(); +}); + +it('exports ZoomRangeWidget', () => { + expect(Widgets.ZoomRangeWidget).toBeDefined(); +}); + +it('exports HtmlOverlayWidget', () => { + expect(Widgets.HtmlOverlayWidget).toBeDefined(); +}); + +it('exports HtmlTooltipWidget', () => { + expect(Widgets.HtmlTooltipWidget).toBeDefined(); +}); diff --git a/package.json b/package.json index 817d91417..a9dc30855 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "type": "module", "workspaces": [ "modules/*", + "dev/*", + "dev/timeline-layers/examples/*", "examples/**" ], "scripts": { @@ -26,6 +28,7 @@ "publish-prod": "ocular-publish version-only-prod", "test": "yarn test-node", "test-node": "vitest --project node", + "test-ci": "vitest run --project node --coverage", "test-browser": "vitest --project browser", "test-headless": "vitest run --project headless", "metrics": "ocular-metrics", @@ -49,6 +52,7 @@ "@vitejs/plugin-react": "^5.1.4", "@vitest/browser": "^4.0.18", "@vitest/browser-playwright": "^4.0.18", + "@vitest/coverage-v8": "^4.0.18", "@vitest/eslint-plugin": "^1.6.9", "bin-pack": "^1.0.2", "datauri": "^4.1.0", diff --git a/tsconfig.json b/tsconfig.json index dcc5733fe..2bf349b8d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,7 +34,8 @@ "@deck.gl-community/react": ["./modules/react/src"], "@deck.gl-community/template": ["./modules/template/src"], "@deck.gl-community/graph-layers": ["./modules/graph-layers/src"], - "@deck.gl-community/three": ["./modules/three/src"] + "@deck.gl-community/three": ["./modules/three/src"], + "@deck.gl-community/timeline-layers": ["./dev/timeline-layers/src"] }, "typeRoots": ["./node_modules/@types"], "plugins": [ @@ -64,6 +65,7 @@ }, "include": [ "modules", + "dev", "test", "examples", "scripts" diff --git a/vitest.config.ts b/vitest.config.ts index 1b923c0ce..f4d2fa2b7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,14 +10,21 @@ const CONFIG = defineConfig({ conditions: ['node'] // prefer node resolution }, test: { + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['modules/*/src/**/*.{ts,tsx,js,jsx}', 'dev/*/src/**/*.{ts,tsx,js,jsx}'], + exclude: ['modules/template/**', 'modules/*/src/**/*.d.ts', 'dev/*/src/**/*.d.ts'] + }, projects: [ { test: { name: 'node', environment: 'node', - include: ['modules/**/*.{test,spec}.{js,ts}'], + include: ['modules/**/*.{test,spec}.{js,ts}', 'dev/**/*.{test,spec}.{js,ts}'], exclude: [ 'modules/**/*.browser.{test,spec}.{js,ts}', + 'dev/**/*.browser.{test,spec}.{js,ts}', 'modules/basemap-props/**' ], browser: { @@ -32,9 +39,10 @@ const CONFIG = defineConfig({ environment: 'node', include: [ 'modules/**/*.browser.{test,spec}.{js,ts,jsx,tsx}', - 'modules/**/*.{test,spec}.{jsx,tsx}' + 'modules/**/*.{test,spec}.{jsx,tsx}', + 'dev/**/*.browser.{test,spec}.{js,ts,jsx,tsx}', + 'dev/**/*.{test,spec}.{jsx,tsx}' ], - exclude: ['modules/basemap-props/**'], browser: { enabled: true, provider: playwright(), @@ -49,9 +57,10 @@ const CONFIG = defineConfig({ environment: 'node', include: [ 'modules/**/*.browser.{test,spec}.{js,ts,jsx,tsx}', - 'modules/**/*.{test,spec}.{jsx,tsx}' + 'modules/**/*.{test,spec}.{jsx,tsx}', + 'dev/**/*.browser.{test,spec}.{js,ts,jsx,tsx}', + 'dev/**/*.{test,spec}.{jsx,tsx}' ], - exclude: ['modules/basemap-props/**'], browser: { enabled: true, headless: true, diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index ce51f43b0..83ae83fa9 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -88,7 +88,7 @@ const config = { '@deck.gl-community/leaflet': resolve('../modules/leaflet/src'), '@deck.gl-community/graph-layers': resolve('../modules/graph-layers/src'), '@deck.gl-community/infovis-layers': resolve('../modules/infovis-layers/src'), - '@deck.gl-community/timeline-layers': resolve('../modules/timeline-layers/src'), + '@deck.gl-community/timeline-layers': resolve('../dev/timeline-layers/src'), '@deck.gl-community/react': resolve('../modules/react/src'), '@deck.gl-community/layers': resolve('../modules/layers/src'), '@deck.gl-community/arrow-layers': resolve('../modules/arrow-layers/src'), diff --git a/website/src/examples-sidebar.js b/website/src/examples-sidebar.js index 2cd284d8c..c073f9b6c 100644 --- a/website/src/examples-sidebar.js +++ b/website/src/examples-sidebar.js @@ -81,14 +81,6 @@ const sidebars = { // ] // }, - { - type: 'category', - label: '@deck.gl-community/three', - items: [ - "three/wild-forest" - ] - }, - // TODO - need BING map key // { // type: 'category', diff --git a/website/src/examples/timeline-layers/horizon-graph-layer.tsx b/website/src/examples/timeline-layers/horizon-graph-layer.tsx index 47700c297..2fbb44b2b 100644 --- a/website/src/examples/timeline-layers/horizon-graph-layer.tsx +++ b/website/src/examples/timeline-layers/horizon-graph-layer.tsx @@ -8,7 +8,7 @@ import {makeExample} from '../../components'; class HorizonDemo extends Component { static title = 'Horizon Graph Layer Demo'; - static code = `${GITHUB_TREE}/examples/timeline-layers/horizon-graph-layer`; + static code = `${GITHUB_TREE}/dev/timeline-layers/examples/horizon-graph-layer`; static renderInfo(meta) { return <>; @@ -20,7 +20,7 @@ class HorizonDemo extends Component { return ( {() => { - const App = require('../../../../examples/timeline-layers/horizon-graph-layer/app').default; + const App = require('../../../../dev/timeline-layers/examples/horizon-graph-layer/app').default; return ; }} diff --git a/yarn.lock b/yarn.lock index 66046489e..be128eb71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -195,13 +195,6 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.0": - version: 7.28.6 - resolution: "@babel/runtime@npm:7.28.6" - checksum: 10c0/358cf2429992ac1c466df1a21c1601d595c46930a13c1d4662fde908d44ee78ec3c183aaff513ecb01ef8c55c3624afe0309eeeb34715672dbfadb7feedb2c0d - languageName: node - linkType: hard - "@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.12.5": version: 7.28.4 resolution: "@babel/runtime@npm:7.28.4" @@ -262,18 +255,10 @@ __metadata: languageName: node linkType: hard -"@carto/api-client@npm:^0.5.19": - version: 0.5.24 - resolution: "@carto/api-client@npm:0.5.24" - dependencies: - "@loaders.gl/schema": "npm:^4.3.3" - "@types/geojson": "npm:^7946.0.16" - d3-format: "npm:^3.1.0" - d3-scale: "npm:^4.0.2" - h3-js: "npm:^4.1.0" - jsep: "npm:^1.4.0" - quadbin: "npm:^0.4.1-alpha.0" - checksum: 10c0/1a110955a9c0eefb4f322b1241d8a858d4cafd292ad988973c56765cad62c2f002a056f4692442e59129e483bc2814e2731bdd92492f213a7ebda2a241af76cf +"@bcoe/v8-coverage@npm:^1.0.2": + version: 1.0.2 + resolution: "@bcoe/v8-coverage@npm:1.0.2" + checksum: 10c0/1eb1dc93cc17fb7abdcef21a6e7b867d6aa99a7ec88ec8207402b23d9083ab22a8011213f04b2cf26d535f1d22dc26139b7929e6c2134c254bd1e14ba5e678c3 languageName: node linkType: hard @@ -286,7 +271,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl-community/arrow-layers@workspace:*, @deck.gl-community/arrow-layers@workspace:modules/arrow-layers": +"@deck.gl-community/arrow-layers@workspace:modules/arrow-layers": version: 0.0.0-use.local resolution: "@deck.gl-community/arrow-layers@workspace:modules/arrow-layers" dependencies: @@ -525,9 +510,9 @@ __metadata: languageName: unknown linkType: soft -"@deck.gl-community/timeline-layers@workspace:*, @deck.gl-community/timeline-layers@workspace:modules/timeline-layers": +"@deck.gl-community/timeline-layers@workspace:*, @deck.gl-community/timeline-layers@workspace:dev/timeline-layers": version: 0.0.0-use.local - resolution: "@deck.gl-community/timeline-layers@workspace:modules/timeline-layers" + resolution: "@deck.gl-community/timeline-layers@workspace:dev/timeline-layers" dependencies: "@deck.gl/core": "npm:~9.2.8" "@deck.gl/layers": "npm:~9.2.8" @@ -551,7 +536,7 @@ __metadata: languageName: unknown linkType: soft -"@deck.gl/aggregation-layers@npm:9.2.8, @deck.gl/aggregation-layers@npm:~9.2.8": +"@deck.gl/aggregation-layers@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/aggregation-layers@npm:9.2.8" dependencies: @@ -569,62 +554,32 @@ __metadata: languageName: node linkType: hard -"@deck.gl/arcgis@npm:9.2.8": - version: 9.2.8 - resolution: "@deck.gl/arcgis@npm:9.2.8" +"@deck.gl/core@npm:~9.2.1": + version: 9.2.10 + resolution: "@deck.gl/core@npm:9.2.10" dependencies: + "@loaders.gl/core": "npm:^4.3.4" + "@loaders.gl/images": "npm:^4.3.4" "@luma.gl/constants": "npm:^9.2.6" - esri-loader: "npm:^3.7.0" - peerDependencies: - "@arcgis/core": ^4.0.0 - "@deck.gl/core": ~9.2.0 - "@luma.gl/core": ~9.2.6 - "@luma.gl/engine": ~9.2.6 - "@luma.gl/webgl": ~9.2.6 - checksum: 10c0/2083b2dc5cb25982d0eab06b443be4a90f8315aca9b048c8578d9d2e1c43d9aa772db8ed2fcb726fb3c323a2e1af4f8abbe3c2189e13ede1f7346d88804f2d6b - languageName: node - linkType: hard - -"@deck.gl/carto@npm:9.2.8": - version: 9.2.8 - resolution: "@deck.gl/carto@npm:9.2.8" - dependencies: - "@carto/api-client": "npm:^0.5.19" - "@loaders.gl/compression": "npm:^4.3.4" - "@loaders.gl/gis": "npm:^4.3.4" - "@loaders.gl/loader-utils": "npm:^4.3.4" - "@loaders.gl/mvt": "npm:^4.3.4" - "@loaders.gl/schema": "npm:^4.3.4" - "@loaders.gl/tiles": "npm:^4.3.4" "@luma.gl/core": "npm:^9.2.6" + "@luma.gl/engine": "npm:^9.2.6" "@luma.gl/shadertools": "npm:^9.2.6" + "@luma.gl/webgl": "npm:^9.2.6" + "@math.gl/core": "npm:^4.1.0" + "@math.gl/sun": "npm:^4.1.0" + "@math.gl/types": "npm:^4.1.0" "@math.gl/web-mercator": "npm:^4.1.0" - "@types/d3-array": "npm:^3.0.2" - "@types/d3-color": "npm:^1.4.2" - "@types/d3-scale": "npm:^3.0.0" - cartocolor: "npm:^5.0.2" - d3-array: "npm:^3.2.0" - d3-color: "npm:^3.1.0" - d3-format: "npm:^3.1.0" - d3-scale: "npm:^4.0.0" - earcut: "npm:^2.2.4" - h3-js: "npm:^4.1.0" - moment-timezone: "npm:^0.6.0" - pbf: "npm:^3.2.1" - quadbin: "npm:^0.4.0" - peerDependencies: - "@deck.gl/aggregation-layers": ~9.2.0 - "@deck.gl/core": ~9.2.0 - "@deck.gl/extensions": ~9.2.0 - "@deck.gl/geo-layers": ~9.2.0 - "@deck.gl/layers": ~9.2.0 - "@loaders.gl/core": ^4.3.4 - "@luma.gl/core": ~9.2.6 - checksum: 10c0/6d76eb6ba48ce2ba9791888af08c0439db81f27c916fec645fec327082bf625f9f63056b45b756a71d62a28a8f639777ba255a50d36b427bec68e4e62f0973cd + "@probe.gl/env": "npm:^4.1.1" + "@probe.gl/log": "npm:^4.1.1" + "@probe.gl/stats": "npm:^4.1.1" + "@types/offscreencanvas": "npm:^2019.6.4" + gl-matrix: "npm:^3.0.0" + mjolnir.js: "npm:^3.0.0" + checksum: 10c0/195a5a2ae4216bfef0c7d280fd8b28a3be14c423ee4d42e4a0c8c6d960b9845ce33eba72390b61d5f57fa9be676672fe7f92013ff5c02989f85792c309480c8e languageName: node linkType: hard -"@deck.gl/core@npm:9.2.8, @deck.gl/core@npm:~9.2.8": +"@deck.gl/core@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/core@npm:9.2.8" dependencies: @@ -649,7 +604,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/extensions@npm:9.2.8, @deck.gl/extensions@npm:~9.2.8": +"@deck.gl/extensions@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/extensions@npm:9.2.8" dependencies: @@ -664,7 +619,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/geo-layers@npm:9.2.8, @deck.gl/geo-layers@npm:~9.2.8": +"@deck.gl/geo-layers@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/geo-layers@npm:9.2.8" dependencies: @@ -697,35 +652,28 @@ __metadata: languageName: node linkType: hard -"@deck.gl/google-maps@npm:9.2.8": - version: 9.2.8 - resolution: "@deck.gl/google-maps@npm:9.2.8" +"@deck.gl/layers@npm:~9.2.1": + version: 9.2.10 + resolution: "@deck.gl/layers@npm:9.2.10" dependencies: - "@luma.gl/constants": "npm:^9.2.6" - "@luma.gl/webgl": "npm:^9.2.6" + "@loaders.gl/images": "npm:^4.3.4" + "@loaders.gl/schema": "npm:^4.3.4" + "@luma.gl/shadertools": "npm:^9.2.6" + "@mapbox/tiny-sdf": "npm:^2.0.5" "@math.gl/core": "npm:^4.1.0" - "@types/google.maps": "npm:^3.48.6" + "@math.gl/polygon": "npm:^4.1.0" + "@math.gl/web-mercator": "npm:^4.1.0" + earcut: "npm:^2.2.4" peerDependencies: "@deck.gl/core": ~9.2.0 - "@luma.gl/constants": ~9.2.6 + "@loaders.gl/core": ^4.3.4 "@luma.gl/core": ~9.2.6 - "@luma.gl/webgl": ~9.2.6 - checksum: 10c0/bdf7d26c29b7ae73c9f955ef2966fdcfa99fdfb94908e26e35be0e16850de4a7e84770e5eff1418acdac9eed1018a6478a42286ce36a26815fdeeabe5ca66376 - languageName: node - linkType: hard - -"@deck.gl/json@npm:9.2.8": - version: 9.2.8 - resolution: "@deck.gl/json@npm:9.2.8" - dependencies: - jsep: "npm:^0.3.0" - peerDependencies: - "@deck.gl/core": ~9.2.0 - checksum: 10c0/f46cd6aeb25bd09c43a5da17a39d539ab1e9e70225423fced353317160b0fbef59c628152c546212e11c50582593099d25898e5bbe98d00012a4ec077a4585ab + "@luma.gl/engine": ~9.2.6 + checksum: 10c0/78e55822e06981725784b4d876a1aa2cc51a607c5e53bd5cf319abb6134305b83f2d07699932b6b0143cb4a14c8b3befedccb575cde62a24211e3c2052dc5a13 languageName: node linkType: hard -"@deck.gl/layers@npm:9.2.8, @deck.gl/layers@npm:~9.2.8": +"@deck.gl/layers@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/layers@npm:9.2.8" dependencies: @@ -746,22 +694,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/mapbox@npm:9.2.8": - version: 9.2.8 - resolution: "@deck.gl/mapbox@npm:9.2.8" - dependencies: - "@luma.gl/constants": "npm:^9.2.6" - "@math.gl/web-mercator": "npm:^4.1.0" - peerDependencies: - "@deck.gl/core": ~9.2.0 - "@luma.gl/constants": ~9.2.6 - "@luma.gl/core": ~9.2.6 - "@math.gl/web-mercator": ^4.1.0 - checksum: 10c0/2846a382b48bcf59c9c1d1246f7914597a9a2179ebb5246b71799b0c2ccdca5f84cadaae4a2fd9918671383fe1b22347f66ad7ecce1b1523632621ed94e77f47 - languageName: node - linkType: hard - -"@deck.gl/mesh-layers@npm:9.2.8, @deck.gl/mesh-layers@npm:~9.2.8": +"@deck.gl/mesh-layers@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/mesh-layers@npm:9.2.8" dependencies: @@ -779,7 +712,19 @@ __metadata: languageName: node linkType: hard -"@deck.gl/react@npm:9.2.8, @deck.gl/react@npm:~9.2.8": +"@deck.gl/react@npm:~9.2.1": + version: 9.2.10 + resolution: "@deck.gl/react@npm:9.2.10" + peerDependencies: + "@deck.gl/core": ~9.2.0 + "@deck.gl/widgets": ~9.2.0 + react: ">=16.3.0" + react-dom: ">=16.3.0" + checksum: 10c0/87b79ad77e494fb16cb0c0ac0d00d9911478a95f9e686e681687fc3e395e469380f1f79429bbbdc67c9e8adac49d8c2311aba991222e30620ca5a540e72578ed + languageName: node + linkType: hard + +"@deck.gl/react@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/react@npm:9.2.8" peerDependencies: @@ -806,7 +751,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/widgets@npm:9.2.8, @deck.gl/widgets@npm:~9.2.8": +"@deck.gl/widgets@npm:~9.2.8": version: 9.2.8 resolution: "@deck.gl/widgets@npm:9.2.8" dependencies: @@ -1781,7 +1726,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28, @jridgewell/trace-mapping@npm:^0.3.31": version: 0.3.31 resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: @@ -1910,7 +1855,7 @@ __metadata: languageName: node linkType: hard -"@loaders.gl/compression@npm:4.3.4, @loaders.gl/compression@npm:^4.3.4": +"@loaders.gl/compression@npm:4.3.4": version: 4.3.4 resolution: "@loaders.gl/compression@npm:4.3.4" dependencies: @@ -2086,7 +2031,7 @@ __metadata: languageName: node linkType: hard -"@loaders.gl/schema@npm:4.3.4, @loaders.gl/schema@npm:^4.2.0, @loaders.gl/schema@npm:^4.3.3, @loaders.gl/schema@npm:^4.3.4": +"@loaders.gl/schema@npm:4.3.4, @loaders.gl/schema@npm:^4.2.0, @loaders.gl/schema@npm:^4.3.4": version: 4.3.4 resolution: "@loaders.gl/schema@npm:4.3.4" dependencies: @@ -2225,7 +2170,7 @@ __metadata: languageName: node linkType: hard -"@luma.gl/core@npm:^9.2.6, @luma.gl/core@npm:~9.2.6": +"@luma.gl/core@npm:^9.2.6, @luma.gl/core@npm:~9.2.0, @luma.gl/core@npm:~9.2.6": version: 9.2.6 resolution: "@luma.gl/core@npm:9.2.6" dependencies: @@ -2336,13 +2281,6 @@ __metadata: languageName: node linkType: hard -"@mapbox/geojson-types@npm:^1.0.2": - version: 1.0.2 - resolution: "@mapbox/geojson-types@npm:1.0.2" - checksum: 10c0/aa0a2cb95a358d8756ab5aa70356bcbd6f554a4571703a88a09e7db6580061d6ef4054db5fe3ecb2817c383b8b5433746a8f46712dc606b32063f73b154f99fc - languageName: node - linkType: hard - "@mapbox/jsonlint-lines-primitives@npm:^2.0.2, @mapbox/jsonlint-lines-primitives@npm:~2.0.2": version: 2.0.2 resolution: "@mapbox/jsonlint-lines-primitives@npm:2.0.2" @@ -2371,15 +2309,6 @@ __metadata: languageName: node linkType: hard -"@mapbox/mapbox-gl-supported@npm:^1.5.0": - version: 1.5.0 - resolution: "@mapbox/mapbox-gl-supported@npm:1.5.0" - peerDependencies: - mapbox-gl: ">=0.32.1 <2.0.0" - checksum: 10c0/5b7712e8b546e598dc5152632504cad53081211b64ddc4447825840ddca703275bc36599167b9550ab906ca8a9554936bcdae562073fdef24b8d38d78ee262fb - languageName: node - linkType: hard - "@mapbox/martini@npm:^0.2.0": version: 0.2.0 resolution: "@mapbox/martini@npm:0.2.0" @@ -2394,13 +2323,6 @@ __metadata: languageName: node linkType: hard -"@mapbox/tiny-sdf@npm:^1.1.1": - version: 1.2.5 - resolution: "@mapbox/tiny-sdf@npm:1.2.5" - checksum: 10c0/de0252388a628ddb491c986c715f0b63ca6a74f5dac16d3e51eb75a21935a31e34fba5db47c81cc59a462d782021fc68b2e3cc119b2d6cabe15d31e311674d6c - languageName: node - linkType: hard - "@mapbox/tiny-sdf@npm:^2.0.5, @mapbox/tiny-sdf@npm:^2.0.6": version: 2.0.7 resolution: "@mapbox/tiny-sdf@npm:2.0.7" @@ -2548,16 +2470,6 @@ __metadata: languageName: node linkType: hard -"@math.gl/web-mercator@npm:^3.5.5": - version: 3.6.3 - resolution: "@math.gl/web-mercator@npm:3.6.3" - dependencies: - "@babel/runtime": "npm:^7.12.0" - gl-matrix: "npm:^3.4.0" - checksum: 10c0/95a1267261e03fa0194ec5ac5f3f8c3a13e1182cf3865d4d3bbd669706d42bd067335d1812df2e844c63c1f0f21c50e08af2b90168f2db1d3fa3ba2087f59703 - languageName: node - linkType: hard - "@monaco-editor/loader@npm:^1.5.0": version: 1.6.1 resolution: "@monaco-editor/loader@npm:1.6.1" @@ -3085,6 +2997,13 @@ __metadata: languageName: node linkType: hard +"@probe.gl/env@npm:4.1.1, @probe.gl/env@npm:^4.1.1": + version: 4.1.1 + resolution: "@probe.gl/env@npm:4.1.1" + checksum: 10c0/431f53d95936ba1ee08e8d4c155cc3c49d80d528acaff933ae8f32a4725f04d88ee634707e20baa885a505d6f1401f895bdfcfa42c259b3220dd26463d2cfb2d + languageName: node + linkType: hard + "@probe.gl/log@npm:4.1.0, @probe.gl/log@npm:^4.0.2, @probe.gl/log@npm:^4.0.4, @probe.gl/log@npm:^4.0.8, @probe.gl/log@npm:^4.1.0": version: 4.1.0 resolution: "@probe.gl/log@npm:4.1.0" @@ -3094,6 +3013,15 @@ __metadata: languageName: node linkType: hard +"@probe.gl/log@npm:^4.1.1": + version: 4.1.1 + resolution: "@probe.gl/log@npm:4.1.1" + dependencies: + "@probe.gl/env": "npm:4.1.1" + checksum: 10c0/713043aea095dbe6e97525a5d254f2563e9e0b6c64fdb888603f41342ed0f0a76a64e6996d2d461c7f1f9c6271a931b2e922d9e47f8c43f3d4116867f4cd5d0a + languageName: node + linkType: hard + "@probe.gl/stats@npm:^4.0.0, @probe.gl/stats@npm:^4.0.2, @probe.gl/stats@npm:^4.0.8, @probe.gl/stats@npm:^4.1.0": version: 4.1.0 resolution: "@probe.gl/stats@npm:4.1.0" @@ -3101,6 +3029,13 @@ __metadata: languageName: node linkType: hard +"@probe.gl/stats@npm:^4.1.1": + version: 4.1.1 + resolution: "@probe.gl/stats@npm:4.1.1" + checksum: 10c0/7fd49d891e5ecfc82330c0db9180cc6a3fae0f78062570fabdc1860f79f09b612c53af08b9d6e2bb701b79fb7156ec2c9c0ddf53e5dba12b0560c7bbc957afc7 + languageName: node + linkType: hard + "@probe.gl/test-utils@npm:^4.0.4, @probe.gl/test-utils@npm:^4.0.6": version: 4.1.0 resolution: "@probe.gl/test-utils@npm:4.1.0" @@ -4223,36 +4158,6 @@ __metadata: languageName: node linkType: hard -"@types/d3-array@npm:^3.0.2": - version: 3.2.2 - resolution: "@types/d3-array@npm:3.2.2" - checksum: 10c0/6137cb97302f8a4f18ca22c0560c585cfcb823f276b23d89f2c0c005d72697ec13bca671c08e68b4b0cabd622e3f0e91782ee221580d6774074050be96dd7028 - languageName: node - linkType: hard - -"@types/d3-color@npm:^1.4.2": - version: 1.4.5 - resolution: "@types/d3-color@npm:1.4.5" - checksum: 10c0/d84233fdfb3ea81710a2aa7eb8f332a0199dfbefb10cd81a68e504dfb2a00b78d309ebe0b1b452c97d7b9de193a301b8ce3cec0842ec300277caef34ef2dd8ed - languageName: node - linkType: hard - -"@types/d3-scale@npm:^3.0.0": - version: 3.3.5 - resolution: "@types/d3-scale@npm:3.3.5" - dependencies: - "@types/d3-time": "npm:^2" - checksum: 10c0/2689ab13092e3fded22cdd1b888afd91aa60190be40c8eddc12b2d42de59b00917778340f90317c68c5ffc3a1bee68f5ca155434cd466bc7804f400f3f9e7529 - languageName: node - linkType: hard - -"@types/d3-time@npm:^2": - version: 2.1.4 - resolution: "@types/d3-time@npm:2.1.4" - checksum: 10c0/b597bfa51a163d4231e953d6903b06fd6341d0f11a28222a79fafaddb46155d7f458a67c814de53df84926a47dd535897228a475679d228576b0cda87351e534 - languageName: node - linkType: hard - "@types/deep-eql@npm:*": version: 4.0.2 resolution: "@types/deep-eql@npm:4.0.2" @@ -4290,20 +4195,6 @@ __metadata: languageName: node linkType: hard -"@types/google.maps@npm:^3.48.6": - version: 3.58.1 - resolution: "@types/google.maps@npm:3.58.1" - checksum: 10c0/0247c61d2aeb216d04ab9392602ef41d58328c6336ed4e9a4e9e69834002e82c116a34def2bf4ceb31c4292f14899815058da4bb8d803147295c6b47c1162621 - languageName: node - linkType: hard - -"@types/hammerjs@npm:^2.0.41": - version: 2.0.46 - resolution: "@types/hammerjs@npm:2.0.46" - checksum: 10c0/f3c1cb20dc2f0523f7b8c76065078544d50d8ae9b0edc1f62fed657210ed814266ff2dfa835d2c157a075991001eec3b64c88bf92e3e6e895c0db78d05711d06 - languageName: node - linkType: hard - "@types/hoist-non-react-statics@npm:*": version: 3.3.7 resolution: "@types/hoist-non-react-statics@npm:3.3.7" @@ -4347,15 +4238,6 @@ __metadata: languageName: node linkType: hard -"@types/mapbox-gl@npm:^2.0.3": - version: 2.7.21 - resolution: "@types/mapbox-gl@npm:2.7.21" - dependencies: - "@types/geojson": "npm:*" - checksum: 10c0/51bcf2a0481a53a8f8148af9b7f0f9e4d1be2ebdd9390194e4e4a0f8c8375446004c8624e67b4a255384cf0a0ae0c5b9eb99fa865acadca0ac0b0f5853f3f957 - languageName: node - linkType: hard - "@types/mapbox__point-geometry@npm:*, @types/mapbox__point-geometry@npm:^0.1.4": version: 0.1.4 resolution: "@types/mapbox__point-geometry@npm:0.1.4" @@ -4493,16 +4375,6 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:^18.0.0": - version: 18.3.28 - resolution: "@types/react@npm:18.3.28" - dependencies: - "@types/prop-types": "npm:*" - csstype: "npm:^3.2.2" - checksum: 10c0/683e19cd12b5c691215529af2e32b5ffbaccae3bf0ba93bfafa0e460e8dfee18423afed568be2b8eadf4b837c3749dd296a4f64e2d79f68fa66962c05f5af661 - languageName: node - linkType: hard - "@types/react@npm:^18.3.3": version: 18.3.26 resolution: "@types/react@npm:18.3.26" @@ -4964,6 +4836,30 @@ __metadata: languageName: node linkType: hard +"@vitest/coverage-v8@npm:^4.0.18": + version: 4.0.18 + resolution: "@vitest/coverage-v8@npm:4.0.18" + dependencies: + "@bcoe/v8-coverage": "npm:^1.0.2" + "@vitest/utils": "npm:4.0.18" + ast-v8-to-istanbul: "npm:^0.3.10" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-reports: "npm:^3.2.0" + magicast: "npm:^0.5.1" + obug: "npm:^2.1.1" + std-env: "npm:^3.10.0" + tinyrainbow: "npm:^3.0.3" + peerDependencies: + "@vitest/browser": 4.0.18 + vitest: 4.0.18 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10c0/e23e0da86f0b2a020c51562bc40ebdc7fc7553c24f8071dfb39a6df0161badbd5eaf2eebbf8ceaef18933a18c1934ff52d1c0c4bde77bb87e0c1feb0c8cbee4d + languageName: node + linkType: hard + "@vitest/eslint-plugin@npm:^1.6.9": version: 1.6.9 resolution: "@vitest/eslint-plugin@npm:1.6.9" @@ -5277,7 +5173,7 @@ __metadata: languageName: node linkType: hard -"apache-arrow@npm:>=14, apache-arrow@npm:>=15": +"apache-arrow@npm:>=15": version: 21.1.0 resolution: "apache-arrow@npm:21.1.0" dependencies: @@ -5561,6 +5457,17 @@ __metadata: languageName: node linkType: hard +"ast-v8-to-istanbul@npm:^0.3.10": + version: 0.3.12 + resolution: "ast-v8-to-istanbul@npm:0.3.12" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.31" + estree-walker: "npm:^3.0.3" + js-tokens: "npm:^10.0.0" + checksum: 10c0/bad6ba222b1073c165c8d65dbf366193d4a90536dabe37f93a3df162269b1c9473975756e4c048f708c235efccc26f8e5321c547b7e9563b64b21b2e0f27cbc9 + languageName: node + linkType: hard + "async-function@npm:^1.0.0": version: 1.0.0 resolution: "async-function@npm:1.0.0" @@ -6081,15 +5988,6 @@ __metadata: languageName: node linkType: hard -"cartocolor@npm:^5.0.2": - version: 5.0.2 - resolution: "cartocolor@npm:5.0.2" - dependencies: - colorbrewer: "npm:1.5.6" - checksum: 10c0/305412d0354ed90b7db050f055a56fd68a95c2e9659d55c1f93272cd30d6c1148519e0b55378be1a1ad2c61147f63c3426c317f8ce5d7359f47266dc2d92f663 - languageName: node - linkType: hard - "caseless@npm:~0.12.0": version: 0.12.0 resolution: "caseless@npm:0.12.0" @@ -6352,13 +6250,6 @@ __metadata: languageName: node linkType: hard -"colorbrewer@npm:1.5.6": - version: 1.5.6 - resolution: "colorbrewer@npm:1.5.6" - checksum: 10c0/da8045058698e2df21df1b676184d9db6651b5c4c2d4775d24bfb60c761150c4e102b54db4a85f4b3019726b5a16053b040f026ee0214cc0ee6ba8612d8a54cd - languageName: node - linkType: hard - "columnify@npm:1.6.0": version: 1.6.0 resolution: "columnify@npm:1.6.0" @@ -6702,7 +6593,7 @@ __metadata: languageName: node linkType: hard -"csscolorparser@npm:~1.0.2, csscolorparser@npm:~1.0.3": +"csscolorparser@npm:~1.0.2": version: 1.0.3 resolution: "csscolorparser@npm:1.0.3" checksum: 10c0/57b30e1dd3e639fb74d63d3ee5a078ae6d0aaba26bc731fdec1a0f50fd77c2531a1fca2bbe07c18e0569255f92064cda0747e0115955fdb8c037332798ca634f @@ -6725,13 +6616,6 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^3.2.2": - version: 3.2.3 - resolution: "csstype@npm:3.2.3" - checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce - languageName: node - linkType: hard - "cwise-compiler@npm:^1.0.0": version: 1.1.3 resolution: "cwise-compiler@npm:1.1.3" @@ -6748,7 +6632,7 @@ __metadata: languageName: node linkType: hard -"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.2.0, d3-array@npm:^3.2.2, d3-array@npm:^3.2.4": +"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.2.2, d3-array@npm:^3.2.4": version: 3.2.4 resolution: "d3-array@npm:3.2.4" dependencies: @@ -6757,7 +6641,7 @@ __metadata: languageName: node linkType: hard -"d3-color@npm:1 - 3, d3-color@npm:^3.1.0": +"d3-color@npm:1 - 3": version: 3.1.0 resolution: "d3-color@npm:3.1.0" checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c @@ -6833,7 +6717,7 @@ __metadata: languageName: node linkType: hard -"d3-scale@npm:^4.0.0, d3-scale@npm:^4.0.2": +"d3-scale@npm:^4.0.2": version: 4.0.2 resolution: "d3-scale@npm:4.0.2" dependencies: @@ -7003,6 +6887,7 @@ __metadata: "@vitejs/plugin-react": "npm:^5.1.4" "@vitest/browser": "npm:^4.0.18" "@vitest/browser-playwright": "npm:^4.0.18" + "@vitest/coverage-v8": "npm:^4.0.18" "@vitest/eslint-plugin": "npm:^1.6.9" bin-pack: "npm:^1.0.2" datauri: "npm:^4.1.0" @@ -7019,166 +6904,6 @@ __metadata: languageName: unknown linkType: soft -"deck.gl@npm:^9.1.0": - version: 9.2.8 - resolution: "deck.gl@npm:9.2.8" - dependencies: - "@deck.gl/aggregation-layers": "npm:9.2.8" - "@deck.gl/arcgis": "npm:9.2.8" - "@deck.gl/carto": "npm:9.2.8" - "@deck.gl/core": "npm:9.2.8" - "@deck.gl/extensions": "npm:9.2.8" - "@deck.gl/geo-layers": "npm:9.2.8" - "@deck.gl/google-maps": "npm:9.2.8" - "@deck.gl/json": "npm:9.2.8" - "@deck.gl/layers": "npm:9.2.8" - "@deck.gl/mapbox": "npm:9.2.8" - "@deck.gl/mesh-layers": "npm:9.2.8" - "@deck.gl/react": "npm:9.2.8" - "@deck.gl/widgets": "npm:9.2.8" - "@loaders.gl/core": "npm:^4.3.4" - "@luma.gl/core": "npm:^9.2.6" - "@luma.gl/engine": "npm:^9.2.6" - peerDependencies: - "@arcgis/core": ^4.0.0 - react: ">=16.3.0" - react-dom: ">=16.3.0" - peerDependenciesMeta: - "@arcgis/core": - optional: true - react: - optional: true - react-dom: - optional: true - checksum: 10c0/1a442f0fd2f3cdc419b84ebea12acd525581ab6d61d9903342f4f3e05d55722369f9379d40325df12acdd968947fc5f75e931740f926ba6441b7ccf01a694a9d - languageName: node - linkType: hard - -"deckgl-example-geoarrow-linestring-layer@workspace:examples/arrow-layers-wip/linestring": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-linestring-layer@workspace:examples/arrow-layers-wip/linestring" - dependencies: - "@deck.gl-community/arrow-layers": "workspace:*" - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-multilinestring-layer@workspace:examples/arrow-layers-wip/multilinestring": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-multilinestring-layer@workspace:examples/arrow-layers-wip/multilinestring" - dependencies: - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-multipoint-layer@workspace:examples/arrow-layers-wip/multipoint": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-multipoint-layer@workspace:examples/arrow-layers-wip/multipoint" - dependencies: - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-multipolygon-layer@workspace:examples/arrow-layers-wip/multipolygon": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-multipolygon-layer@workspace:examples/arrow-layers-wip/multipolygon" - dependencies: - "@deck.gl-community/arrow-layers": "workspace:*" - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-point-layer@workspace:examples/arrow-layers-wip/point": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-point-layer@workspace:examples/arrow-layers-wip/point" - dependencies: - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-polygon-layer@workspace:examples/arrow-layers-wip/polygon": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-polygon-layer@workspace:examples/arrow-layers-wip/polygon" - dependencies: - "@deck.gl-community/arrow-layers": "workspace:*" - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-text-layer@workspace:examples/arrow-layers-wip/text": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-text-layer@workspace:examples/arrow-layers-wip/text" - dependencies: - "@deck.gl-community/arrow-layers": "workspace:*" - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - -"deckgl-example-geoarrow-trips-layer@workspace:examples/arrow-layers-wip/trips": - version: 0.0.0-use.local - resolution: "deckgl-example-geoarrow-trips-layer@workspace:examples/arrow-layers-wip/trips" - dependencies: - "@deck.gl-community/arrow-layers": "workspace:*" - "@types/react": "npm:^18.0.0" - "@types/react-dom": "npm:^18.0.0" - apache-arrow: "npm:>=14" - deck.gl: "npm:^9.1.0" - react: "npm:^18.0.0" - react-dom: "npm:^18.0.0" - react-map-gl: "npm:^5.3.0" - vite: "npm:^7.3.1" - languageName: unknown - linkType: soft - "decompress-response@npm:^6.0.0": version: 6.0.0 resolution: "decompress-response@npm:6.0.0" @@ -7506,7 +7231,7 @@ __metadata: languageName: node linkType: hard -"earcut@npm:^2.2.2, earcut@npm:^2.2.4": +"earcut@npm:^2.2.4": version: 2.2.4 resolution: "earcut@npm:2.2.4" checksum: 10c0/01ca51830edd2787819f904ae580087d37351f6048b4565e7add4b3da8a86b7bc19262ab2aa7fdc64129ab03af2d9cec8cccee4d230c82275f97ef285c79aafb @@ -8537,13 +8262,6 @@ __metadata: languageName: node linkType: hard -"esri-loader@npm:^3.7.0": - version: 3.7.0 - resolution: "esri-loader@npm:3.7.0" - checksum: 10c0/56c8febebb8698cb9884b9dd5d6c5800da0f4f3b66a10c0959892564621fbf976d73a3e65c5e940f86f7498897d50dff7354e17862041e9698a92a15efc60f8b - languageName: node - linkType: hard - "estraverse@npm:^5.1.0, estraverse@npm:^5.2.0, estraverse@npm:^5.3.0": version: 5.3.0 resolution: "estraverse@npm:5.3.0" @@ -8729,7 +8447,7 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.3, fdir@npm:^6.5.0": +"fdir@npm:^6.4.3, fdir@npm:^6.4.6, fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" peerDependencies: @@ -9103,13 +8821,6 @@ __metadata: languageName: node linkType: hard -"geojson-vt@npm:^3.2.1": - version: 3.2.1 - resolution: "geojson-vt@npm:3.2.1" - checksum: 10c0/db2fc1a452067ee8436fa86e5a138f6ebd3d64893e0af097bc1cc960ec63d67c0ce77444711e9583036192d6bf9ce754bf9b56a76789684fc0fea4d52321fffc - languageName: node - linkType: hard - "geojson-vt@npm:^4.0.2": version: 4.0.2 resolution: "geojson-vt@npm:4.0.2" @@ -9313,7 +9024,7 @@ __metadata: languageName: node linkType: hard -"gl-matrix@npm:^3.0.0, gl-matrix@npm:^3.2.1, gl-matrix@npm:^3.4.0, gl-matrix@npm:^3.4.3": +"gl-matrix@npm:^3.0.0, gl-matrix@npm:^3.4.3": version: 3.4.4 resolution: "gl-matrix@npm:3.4.4" checksum: 10c0/9aa022ffac0d158212ad0cd29939864ad919ac31cd5dc5a5d35e9d66bb62679ddf152ff7b2173ded20131045e40572b87f31b26a920be2a7583a1516b13b5b4b @@ -9484,13 +9195,6 @@ __metadata: languageName: node linkType: hard -"grid-index@npm:^1.1.0": - version: 1.1.0 - resolution: "grid-index@npm:1.1.0" - checksum: 10c0/0ba2a622a52badc86642a002abee79b48c207092347e869528253e634573b9b55494db649fbd1779122d797aaa3b59e284023a96a7a5c666c7375f0e320f8f57 - languageName: node - linkType: hard - "h3-js@npm:^4.1.0, h3-js@npm:^4.2.1": version: 4.3.0 resolution: "h3-js@npm:4.3.0" @@ -9498,13 +9202,6 @@ __metadata: languageName: node linkType: hard -"hammerjs@npm:^2.0.8": - version: 2.0.8 - resolution: "hammerjs@npm:2.0.8" - checksum: 10c0/5c95e5774b5ea49492cb3fa8f1949aea67048a0b84af33acb555e7139abfcf3c83aca2b83e0c5008755bc230166df7b5e469d1e3eb6746c48f215f3672609fed - languageName: node - linkType: hard - "handlebars@npm:^4.7.7": version: 4.7.8 resolution: "handlebars@npm:4.7.8" @@ -9657,9 +9354,9 @@ __metadata: languageName: node linkType: hard -"horizon-graph-layer-16cc55@workspace:examples/timeline-layers/horizon-graph-layer": +"horizon-graph-layer-289f4b@workspace:dev/timeline-layers/examples/horizon-graph-layer": version: 0.0.0-use.local - resolution: "horizon-graph-layer-16cc55@workspace:examples/timeline-layers/horizon-graph-layer" + resolution: "horizon-graph-layer-289f4b@workspace:dev/timeline-layers/examples/horizon-graph-layer" dependencies: "@deck.gl-community/timeline-layers": "workspace:*" "@deck.gl/core": "npm:~9.2.8" @@ -10505,14 +10202,14 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0, istanbul-lib-coverage@npm:^3.2.2": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b languageName: node linkType: hard -"istanbul-lib-report@npm:^3.0.0": +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": version: 3.0.1 resolution: "istanbul-lib-report@npm:3.0.1" dependencies: @@ -10523,7 +10220,7 @@ __metadata: languageName: node linkType: hard -"istanbul-reports@npm:^3.1.4": +"istanbul-reports@npm:^3.1.4, istanbul-reports@npm:^3.2.0": version: 3.2.0 resolution: "istanbul-reports@npm:3.2.0" dependencies: @@ -10608,6 +10305,13 @@ __metadata: languageName: node linkType: hard +"js-tokens@npm:^10.0.0": + version: 10.0.0 + resolution: "js-tokens@npm:10.0.0" + checksum: 10c0/a93498747812ba3e0c8626f95f75ab29319f2a13613a0de9e610700405760931624433a0de59eb7c27ff8836e526768fb20783861b86ef89be96676f2c996b64 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -10645,20 +10349,6 @@ __metadata: languageName: node linkType: hard -"jsep@npm:^0.3.0": - version: 0.3.5 - resolution: "jsep@npm:0.3.5" - checksum: 10c0/fb5def7a4ba1cee41d144ebdd0d477785dc84b6bc1fed6cf5169f106de980dbe363bf99cb36a450435d7fd952d22b1d76e1609aeb5c7e7cbbbdb6d15fad03614 - languageName: node - linkType: hard - -"jsep@npm:^1.4.0": - version: 1.4.0 - resolution: "jsep@npm:1.4.0" - checksum: 10c0/fe60adf47e050e22eadced42514a51a15a3cf0e2d147896584486acd8ee670fc16641101b9aeb81f4aaba382043d29744b7aac41171e8106515b14f27e0c7116 - languageName: node - linkType: hard - "jsesc@npm:^3.0.2": version: 3.1.0 resolution: "jsesc@npm:3.1.0" @@ -10863,13 +10553,6 @@ __metadata: languageName: node linkType: hard -"kdbush@npm:^3.0.0": - version: 3.0.0 - resolution: "kdbush@npm:3.0.0" - checksum: 10c0/3fc8795870bd04f60627e7345b26fd0644beb91bc4164912c9d9378b39c674ba01c31db68ecaf6266d51c9ad81bf5b770b7effa51eeee37553d38293a094a686 - languageName: node - linkType: hard - "kdbush@npm:^4.0.2": version: 4.0.2 resolution: "kdbush@npm:4.0.2" @@ -11316,6 +10999,17 @@ __metadata: languageName: node linkType: hard +"magicast@npm:^0.5.1": + version: 0.5.2 + resolution: "magicast@npm:0.5.2" + dependencies: + "@babel/parser": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" + source-map-js: "npm:^1.2.1" + checksum: 10c0/924af677643c5a0a7d6cdb3247c0eb96fa7611b2ba6a5e720d35d81c503d3d9f5948eb5227f80f90f82ea3e7d38cffd10bb988f3fc09020db428e14f26e960d7 + languageName: node + linkType: hard + "make-dir@npm:4.0.0, make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -11395,36 +11089,6 @@ __metadata: languageName: node linkType: hard -"mapbox-gl@npm:^1.0.0": - version: 1.13.3 - resolution: "mapbox-gl@npm:1.13.3" - dependencies: - "@mapbox/geojson-rewind": "npm:^0.5.2" - "@mapbox/geojson-types": "npm:^1.0.2" - "@mapbox/jsonlint-lines-primitives": "npm:^2.0.2" - "@mapbox/mapbox-gl-supported": "npm:^1.5.0" - "@mapbox/point-geometry": "npm:^0.1.0" - "@mapbox/tiny-sdf": "npm:^1.1.1" - "@mapbox/unitbezier": "npm:^0.0.0" - "@mapbox/vector-tile": "npm:^1.3.1" - "@mapbox/whoots-js": "npm:^3.1.0" - csscolorparser: "npm:~1.0.3" - earcut: "npm:^2.2.2" - geojson-vt: "npm:^3.2.1" - gl-matrix: "npm:^3.2.1" - grid-index: "npm:^1.1.0" - murmurhash-js: "npm:^1.0.0" - pbf: "npm:^3.2.1" - potpack: "npm:^1.0.1" - quickselect: "npm:^2.0.0" - rw: "npm:^1.3.3" - supercluster: "npm:^7.1.0" - tinyqueue: "npm:^2.0.3" - vt-pbf: "npm:^3.1.1" - checksum: 10c0/82bf86daa69e068d4138c4e5a1dbfc2a28c6d0e2cc142ef96d3ce671770e8173d3ffd62e6614b3606f932f99b40eb4dfc52d5f44c48dabd393ba27b61d9e50c4 - languageName: node - linkType: hard - "maplibre-gl@npm:^4.1.3": version: 4.7.1 resolution: "maplibre-gl@npm:4.7.1" @@ -11832,16 +11496,6 @@ __metadata: languageName: node linkType: hard -"mjolnir.js@npm:^2.5.0": - version: 2.7.3 - resolution: "mjolnir.js@npm:2.7.3" - dependencies: - "@types/hammerjs": "npm:^2.0.41" - hammerjs: "npm:^2.0.8" - checksum: 10c0/82f86dedfd410a640e5d0ef6f04e1d72ab292e61d42434bc56f103670ada45b2aacf130527f434d7d4bd0b8d2d5bc82bd36850e7586f1b70ebb72da2fe2c72b2 - languageName: node - linkType: hard - "mjolnir.js@npm:^3.0.0": version: 3.0.0 resolution: "mjolnir.js@npm:3.0.0" @@ -11886,22 +11540,6 @@ __metadata: languageName: node linkType: hard -"moment-timezone@npm:^0.6.0": - version: 0.6.0 - resolution: "moment-timezone@npm:0.6.0" - dependencies: - moment: "npm:^2.29.4" - checksum: 10c0/16164cf321d8be0bf7d43855286b426c94c8200e0634f2e42cf469f591c6a230ac43f37d3826d76b05ac221f69a571400323fb8625e3d4e8669f4d9ab00fe779 - languageName: node - linkType: hard - -"moment@npm:^2.29.4": - version: 2.30.1 - resolution: "moment@npm:2.30.1" - checksum: 10c0/865e4279418c6de666fca7786607705fd0189d8a7b7624e2e56be99290ac846f90878a6f602e34b4e0455c549b85385b1baf9966845962b313699e7cb847543a - languageName: node - linkType: hard - "monaco-editor@npm:^0.53.0": version: 0.53.0 resolution: "monaco-editor@npm:0.53.0" @@ -13239,13 +12877,6 @@ __metadata: languageName: node linkType: hard -"potpack@npm:^1.0.1": - version: 1.0.2 - resolution: "potpack@npm:1.0.2" - checksum: 10c0/670c23898a4257130858b960c2e654d3327c0f6a7e7091ff5846f213e65af8f9476320b995b8ad561a47a4d1c359c7ef347de57d22e7b02597051abb52bc85c4 - languageName: node - linkType: hard - "potpack@npm:^2.0.0": version: 2.1.0 resolution: "potpack@npm:2.1.0" @@ -13506,15 +13137,6 @@ __metadata: languageName: node linkType: hard -"quadbin@npm:^0.4.0, quadbin@npm:^0.4.1-alpha.0": - version: 0.4.2 - resolution: "quadbin@npm:0.4.2" - dependencies: - "@math.gl/web-mercator": "npm:^4.1.0" - checksum: 10c0/40ad1efa7146762e3cf6d9abfe47d452e69cceb0a72b5caec27e8bdf66d1e4522d6a1bde7e643015d9651eaa9f12611208f073856662fb6266263dcbf72d6dd8 - languageName: node - linkType: hard - "quadprog@npm:^1.6.1": version: 1.6.1 resolution: "quadprog@npm:1.6.1" @@ -13594,7 +13216,7 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^18.0.0, react-dom@npm:^18.3.1": +"react-dom@npm:^18.3.1": version: 18.3.1 resolution: "react-dom@npm:18.3.1" dependencies: @@ -13653,24 +13275,6 @@ __metadata: languageName: node linkType: hard -"react-map-gl@npm:^5.3.0": - version: 5.3.21 - resolution: "react-map-gl@npm:5.3.21" - dependencies: - "@babel/runtime": "npm:^7.0.0" - "@types/geojson": "npm:^7946.0.7" - "@types/mapbox-gl": "npm:^2.0.3" - mapbox-gl: "npm:^1.0.0" - mjolnir.js: "npm:^2.5.0" - prop-types: "npm:^15.7.2" - resize-observer-polyfill: "npm:^1.5.1" - viewport-mercator-project: "npm:^7.0.4" - peerDependencies: - react: ">=16.3.0" - checksum: 10c0/fa7bf984b3c5af19dfdaf12018753bfe3cfd7a040bf94cb3ad669f5f1aa42a7ae8c32a8b4523554b16ba1c532197800a4656ecd7c0e2f8e5818b16b1c00ae0a3 - languageName: node - linkType: hard - "react-map-gl@npm:^7.1.7": version: 7.1.9 resolution: "react-map-gl@npm:7.1.9" @@ -13742,7 +13346,7 @@ __metadata: languageName: node linkType: hard -"react@npm:^18.0.0, react@npm:^18.3.1": +"react@npm:^18.3.1": version: 18.3.1 resolution: "react@npm:18.3.1" dependencies: @@ -13958,13 +13562,6 @@ __metadata: languageName: node linkType: hard -"resize-observer-polyfill@npm:^1.5.1": - version: 1.5.1 - resolution: "resize-observer-polyfill@npm:1.5.1" - checksum: 10c0/5e882475067f0b97dc07e0f37c3e335ac5bc3520d463f777cec7e894bb273eddbfecb857ae668e6fb6881fd6f6bb7148246967172139302da50fa12ea3a15d95 - languageName: node - linkType: hard - "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -15214,15 +14811,6 @@ __metadata: languageName: node linkType: hard -"supercluster@npm:^7.1.0": - version: 7.1.5 - resolution: "supercluster@npm:7.1.5" - dependencies: - kdbush: "npm:^3.0.0" - checksum: 10c0/bbebf45927d0019831731c94b78d1c9a1f3e2da0be9875d7ea75c6f261487e0f15d3f1822a9a49256e3c1672bdfb9138f9a5e44e2de99edb06cd1e758083e12d - languageName: node - linkType: hard - "supercluster@npm:^8.0.1": version: 8.0.1 resolution: "supercluster@npm:8.0.1" @@ -15516,6 +15104,21 @@ __metadata: languageName: node linkType: hard +"timeline-layer-a0fda1@workspace:dev/timeline-layers/examples/timeline-layer": + version: 0.0.0-use.local + resolution: "timeline-layer-a0fda1@workspace:dev/timeline-layers/examples/timeline-layer" + dependencies: + "@deck.gl-community/timeline-layers": "workspace:*" + "@deck.gl/core": "npm:~9.2.1" + "@deck.gl/layers": "npm:~9.2.1" + "@deck.gl/react": "npm:~9.2.1" + "@luma.gl/core": "npm:~9.2.0" + react: "npm:^18.3.1" + react-dom: "npm:^18.3.1" + vite: "npm:7.1.1" + languageName: unknown + linkType: soft + "tiny-invariant@npm:^1.0.2": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3" @@ -15563,7 +15166,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -15573,7 +15176,7 @@ __metadata: languageName: node linkType: hard -"tinyqueue@npm:^2.0.0, tinyqueue@npm:^2.0.3": +"tinyqueue@npm:^2.0.0": version: 2.0.3 resolution: "tinyqueue@npm:2.0.3" checksum: 10c0/d7b590088f015a94a17132fa209c2f2a80c45158259af5474901fdf5932e95ea13ff6f034bcc725a6d5f66d3e5b888b048c310229beb25ad5bebb4f0a635abf2 @@ -16197,12 +15800,58 @@ __metadata: languageName: node linkType: hard -"viewport-mercator-project@npm:^7.0.4": - version: 7.0.4 - resolution: "viewport-mercator-project@npm:7.0.4" +"vite@npm:7.1.1": + version: 7.1.1 + resolution: "vite@npm:7.1.1" dependencies: - "@math.gl/web-mercator": "npm:^3.5.5" - checksum: 10c0/9a8a12f4bd68355a8162a23d4b161133b45aee71b2453fd8d57f54c55466007428cf502d0c02f58620d4c92ba934c29608613bc3176dd8c1d348f8a9deb34e32 + esbuild: "npm:^0.25.0" + fdir: "npm:^6.4.6" + fsevents: "npm:~2.3.3" + picomatch: "npm:^4.0.3" + postcss: "npm:^8.5.6" + rollup: "npm:^4.43.0" + tinyglobby: "npm:^0.2.14" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/391a5c8b8f287b7b1653dedbe952fb4cb93bf7c99b19dab915cf63497892427198fef637e943a3391eacfecf7f2e8f55c40d0fa065fabdd885641430d0b74af7 languageName: node linkType: hard @@ -16433,7 +16082,7 @@ __metadata: languageName: node linkType: hard -"vt-pbf@npm:^3.1.1, vt-pbf@npm:^3.1.3": +"vt-pbf@npm:^3.1.3": version: 3.1.3 resolution: "vt-pbf@npm:3.1.3" dependencies: