carla-canvas 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- carla_canvas-0.1.1/.github/actions/wheel/action.yml +50 -0
- carla_canvas-0.1.1/.github/workflows/ci.yml +51 -0
- carla_canvas-0.1.1/.github/workflows/release-label.yml +45 -0
- carla_canvas-0.1.1/.github/workflows/release.yml +280 -0
- carla_canvas-0.1.1/.gitignore +7 -0
- carla_canvas-0.1.1/CLAUDE.md +54 -0
- carla_canvas-0.1.1/CMakeLists.txt +155 -0
- carla_canvas-0.1.1/LICENSE +21 -0
- carla_canvas-0.1.1/PKG-INFO +202 -0
- carla_canvas-0.1.1/README.md +177 -0
- carla_canvas-0.1.1/cmake/FetchDeps.cmake +104 -0
- carla_canvas-0.1.1/docs/configuration.md +119 -0
- carla_canvas-0.1.1/docs/design.md +189 -0
- carla_canvas-0.1.1/docs/renderers.md +112 -0
- carla_canvas-0.1.1/docs/ros2.md +43 -0
- carla_canvas-0.1.1/examples/camera_lidar.py +50 -0
- carla_canvas-0.1.1/examples/gaussian_splatting.yaml +28 -0
- carla_canvas-0.1.1/examples/gaussian_splatting_demo.yaml +17 -0
- carla_canvas-0.1.1/examples/my_renderer.py +44 -0
- carla_canvas-0.1.1/examples/primitives.yaml +13 -0
- carla_canvas-0.1.1/examples/splatsim.yaml +22 -0
- carla_canvas-0.1.1/native/src/config.cpp +134 -0
- carla_canvas-0.1.1/native/src/config.hpp +41 -0
- carla_canvas-0.1.1/native/src/exports.map +7 -0
- carla_canvas-0.1.1/native/src/plugin.cpp +743 -0
- carla_canvas-0.1.1/native/src/tsc_api.hpp +95 -0
- carla_canvas-0.1.1/native/tests/test_config.cpp +78 -0
- carla_canvas-0.1.1/proto/carla_canvas/v1/renderer.proto +206 -0
- carla_canvas-0.1.1/pyproject.toml +68 -0
- carla_canvas-0.1.1/python/carla_canvas/__init__.py +37 -0
- carla_canvas-0.1.1/python/carla_canvas/cli.py +140 -0
- carla_canvas-0.1.1/python/carla_canvas/config.py +137 -0
- carla_canvas-0.1.1/python/carla_canvas/encodings.py +57 -0
- carla_canvas-0.1.1/python/carla_canvas/geometry.py +154 -0
- carla_canvas-0.1.1/python/carla_canvas/lidar.py +220 -0
- carla_canvas-0.1.1/python/carla_canvas/plugin.py +53 -0
- carla_canvas-0.1.1/python/carla_canvas/renderer.py +166 -0
- carla_canvas-0.1.1/python/carla_canvas/renderers/__init__.py +1 -0
- carla_canvas-0.1.1/python/carla_canvas/renderers/demo_assets.py +62 -0
- carla_canvas-0.1.1/python/carla_canvas/renderers/gaussian_splatting.py +390 -0
- carla_canvas-0.1.1/python/carla_canvas/renderers/gs_ply.py +192 -0
- carla_canvas-0.1.1/python/carla_canvas/renderers/primitives.py +141 -0
- carla_canvas-0.1.1/python/carla_canvas/ros/__init__.py +17 -0
- carla_canvas-0.1.1/python/carla_canvas/ros/bridge.py +214 -0
- carla_canvas-0.1.1/python/carla_canvas/ros/dds_types.py +118 -0
- carla_canvas-0.1.1/python/carla_canvas/ros/messages.py +142 -0
- carla_canvas-0.1.1/python/carla_canvas/ros/transports.py +214 -0
- carla_canvas-0.1.1/python/carla_canvas/scene.py +185 -0
- carla_canvas-0.1.1/python/carla_canvas/sensors.py +88 -0
- carla_canvas-0.1.1/python/carla_canvas/server/__init__.py +113 -0
- carla_canvas-0.1.1/python/carla_canvas/server/service.py +235 -0
- carla_canvas-0.1.1/python/carla_canvas/v1/__init__.py +1 -0
- carla_canvas-0.1.1/python/carla_canvas/v1/renderer_pb2.py +89 -0
- carla_canvas-0.1.1/python/carla_canvas/v1/renderer_pb2.pyi +244 -0
- carla_canvas-0.1.1/python/carla_canvas/v1/renderer_pb2_grpc.py +328 -0
- carla_canvas-0.1.1/tests/conftest.py +63 -0
- carla_canvas-0.1.1/tests/programs/batch_destroy.py +29 -0
- carla_canvas-0.1.1/tests/programs/callbacks_ros.py +60 -0
- carla_canvas-0.1.1/tests/test_end_to_end.py +143 -0
- carla_canvas-0.1.1/tests/test_geometry_and_config.py +88 -0
- carla_canvas-0.1.1/tests/test_packaging.py +61 -0
- carla_canvas-0.1.1/tests/test_renderers.py +175 -0
- carla_canvas-0.1.1/tests/test_service.py +323 -0
- carla_canvas-0.1.1/tools/bump_version.py +63 -0
- carla_canvas-0.1.1/tools/check_wheel.py +49 -0
- carla_canvas-0.1.1/tools/gen_proto.py +52 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# One carla-canvas wheel for an architecture, on a runner of that architecture:
|
|
2
|
+
# the sensor plugin with gRPC, protobuf and yaml-cpp built from source and linked
|
|
3
|
+
# in statically (CARLA_CANVAS_FETCH_DEPS), checked as installed. Shared by CI and
|
|
4
|
+
# the release workflow; the dependencies' build is cached between runs.
|
|
5
|
+
name: carla-canvas wheel
|
|
6
|
+
description: Builds and checks one carla-canvas manylinux wheel for an architecture
|
|
7
|
+
inputs:
|
|
8
|
+
arch:
|
|
9
|
+
description: x86_64 or aarch64 (the runner's)
|
|
10
|
+
required: true
|
|
11
|
+
runs:
|
|
12
|
+
using: composite
|
|
13
|
+
steps:
|
|
14
|
+
- name: Restore the dependencies' build
|
|
15
|
+
uses: actions/cache@v4
|
|
16
|
+
with:
|
|
17
|
+
path: ${{ runner.temp }}/wheel-build
|
|
18
|
+
# Keep the cibuildwheel version in step with `uses:` below.
|
|
19
|
+
# One entry (~1 GB) per dependency set: cmake/FetchDeps.cmake pins them.
|
|
20
|
+
key: wheel-manylinux_2_28-${{ inputs.arch }}-cibuildwheel-v3.1.4-${{ hashFiles('cmake/**') }}
|
|
21
|
+
- uses: pypa/cibuildwheel@v3.1.4
|
|
22
|
+
env:
|
|
23
|
+
CIBW_BUILD: cp312-manylinux_${{ inputs.arch }}
|
|
24
|
+
CIBW_ARCHS: ${{ inputs.arch }}
|
|
25
|
+
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
|
|
26
|
+
CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28
|
|
27
|
+
# FetchDeps needs CMake 3.28+: scikit-build-core fetches it if the image's is older.
|
|
28
|
+
CIBW_ENVIRONMENT: SKBUILD_CMAKE_VERSION=">=3.28"
|
|
29
|
+
CIBW_CONTAINER_ENGINE: "docker; create_args: --volume=${{ runner.temp }}/wheel-build:/wheel-build"
|
|
30
|
+
CIBW_CONFIG_SETTINGS: >-
|
|
31
|
+
build-dir=/wheel-build/build
|
|
32
|
+
cmake.define.CARLA_CANVAS_FETCH_DEPS=ON
|
|
33
|
+
cmake.define.CARLA_CANVAS_REQUIRE_PLUGIN=ON
|
|
34
|
+
# Checked in the next step, as installed outside the source tree.
|
|
35
|
+
CIBW_TEST_SKIP: "*"
|
|
36
|
+
with:
|
|
37
|
+
output-dir: dist
|
|
38
|
+
# The container wrote the build directory as root; actions/cache saves it
|
|
39
|
+
# as the runner user.
|
|
40
|
+
- name: Hand the build back to the runner user
|
|
41
|
+
shell: bash
|
|
42
|
+
run: sudo chown -R "$(id -u):$(id -g)" "$RUNNER_TEMP/wheel-build"
|
|
43
|
+
- uses: actions/setup-python@v5
|
|
44
|
+
with: { python-version: "3.12" }
|
|
45
|
+
- name: Check the wheel (plugin, exports, no system gRPC/protobuf/yaml-cpp)
|
|
46
|
+
shell: bash
|
|
47
|
+
run: |
|
|
48
|
+
python -m venv /tmp/check
|
|
49
|
+
/tmp/check/bin/pip install dist/*.whl
|
|
50
|
+
cd /tmp && /tmp/check/bin/python "$GITHUB_WORKSPACE/tools/check_wheel.py"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
env:
|
|
8
|
+
# typesafe_carla with the sensor source C ABI (5.3); mock backend: no CARLA server.
|
|
9
|
+
TYPESAFE_CARLA_REPO: hakuturu583/typesafe_carla
|
|
10
|
+
TYPESAFE_CARLA_REF: main
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-24.04
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
repository: ${{ env.TYPESAFE_CARLA_REPO }}
|
|
20
|
+
ref: ${{ env.TYPESAFE_CARLA_REF }}
|
|
21
|
+
path: typesafe_carla
|
|
22
|
+
- name: System packages
|
|
23
|
+
run: |
|
|
24
|
+
sudo apt-get update
|
|
25
|
+
sudo apt-get install -y g++ cmake zlib1g-dev libgrpc++-dev protobuf-compiler-grpc \
|
|
26
|
+
libprotobuf-dev protobuf-compiler libyaml-cpp-dev
|
|
27
|
+
- uses: astral-sh/setup-uv@v5
|
|
28
|
+
- name: Environment
|
|
29
|
+
run: |
|
|
30
|
+
uv venv
|
|
31
|
+
uv pip install scikit-build-core grpcio grpcio-tools protobuf numpy pyyaml pytest cyclonedds
|
|
32
|
+
TSC_BACKEND=mock uv pip install ./typesafe_carla
|
|
33
|
+
uv pip install --no-build-isolation --no-deps -e .
|
|
34
|
+
- name: Native tests
|
|
35
|
+
run: |
|
|
36
|
+
cmake -S . -B build -DCARLA_CANVAS_REQUIRE_PLUGIN=ON \
|
|
37
|
+
-DTYPESAFE_CARLA_INCLUDE_DIR=$PWD/typesafe_carla/native/include
|
|
38
|
+
cmake --build build -j
|
|
39
|
+
ctest --test-dir build --output-on-failure
|
|
40
|
+
- name: Python tests
|
|
41
|
+
run: PATH=$PWD/.venv/bin:$PATH .venv/bin/python -m pytest -v
|
|
42
|
+
|
|
43
|
+
wheel:
|
|
44
|
+
# The release wheel (x86_64): the plugin with gRPC, protobuf and yaml-cpp
|
|
45
|
+
# built from source, no system packages (.github/actions/wheel).
|
|
46
|
+
runs-on: ubuntu-24.04
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- uses: ./.github/actions/wheel
|
|
50
|
+
with:
|
|
51
|
+
arch: x86_64
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# The release labels of the auto-release (release.yml): a PR merged to main with
|
|
2
|
+
# release:major, release:minor or release:patch is released; with none, it is
|
|
3
|
+
# not. This creates the labels if they are missing and fails a PR that carries
|
|
4
|
+
# more than one.
|
|
5
|
+
name: Release label
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
pull_request:
|
|
9
|
+
types: [opened, reopened, labeled, unlabeled, synchronize]
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
label:
|
|
16
|
+
runs-on: ubuntu-24.04
|
|
17
|
+
permissions:
|
|
18
|
+
issues: write # creating the labels
|
|
19
|
+
steps:
|
|
20
|
+
- name: Create the release labels
|
|
21
|
+
# A fork's PR gets a read-only token: the labels are created by the next
|
|
22
|
+
# PR from this repository instead.
|
|
23
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
24
|
+
env:
|
|
25
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
26
|
+
REPO: ${{ github.repository }}
|
|
27
|
+
run: |
|
|
28
|
+
gh label create release:major --repo "$REPO" --color b60205 --force \
|
|
29
|
+
--description "Merging releases a new major version (X.0.0)"
|
|
30
|
+
gh label create release:minor --repo "$REPO" --color fbca04 --force \
|
|
31
|
+
--description "Merging releases a new minor version (x.Y.0)"
|
|
32
|
+
gh label create release:patch --repo "$REPO" --color 0e8a16 --force \
|
|
33
|
+
--description "Merging releases a new patch version (x.y.Z)"
|
|
34
|
+
- name: At most one release label
|
|
35
|
+
env:
|
|
36
|
+
LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
|
|
37
|
+
run: |
|
|
38
|
+
count=$(echo "$LABELS" | jq '[.[] | select(test("^release:(major|minor|patch)$"))] | length')
|
|
39
|
+
if [ "$count" -gt 1 ]; then
|
|
40
|
+
echo "::error::More than one release label: pick one of release:major, release:minor, release:patch"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
if [ "$count" -eq 0 ]; then
|
|
44
|
+
echo "No release label: merging does not release"
|
|
45
|
+
fi
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# Cuts releases of carla-canvas and publishes its wheels and sdist to PyPI.
|
|
2
|
+
#
|
|
3
|
+
# The wheels carry the sensor plugin with gRPC, protobuf and yaml-cpp built
|
|
4
|
+
# from source and linked in statically (CARLA_CANVAS_FETCH_DEPS), so
|
|
5
|
+
# `pip install 'carla-canvas[client]'` needs no system packages: one
|
|
6
|
+
# py3-none-manylinux_2_28 wheel per architecture.
|
|
7
|
+
#
|
|
8
|
+
# There are three ways in, and the first is the everyday one:
|
|
9
|
+
#
|
|
10
|
+
# push to main the auto-release path. When the merged PR carries exactly one
|
|
11
|
+
# `release:major|minor|patch` label, the `release` job bumps the
|
|
12
|
+
# version (tools/bump_version.py), commits it to main, tags
|
|
13
|
+
# v<version> and drafts a GitHub release; the tag is built and
|
|
14
|
+
# published to PyPI in this same run, and the GitHub release is
|
|
15
|
+
# published last. No label, no release.
|
|
16
|
+
# push a tag v<version> a hand-cut release: built and published directly
|
|
17
|
+
# (the tag must match python/carla_canvas/__init__.py).
|
|
18
|
+
# workflow_dispatch a dry run: builds and checks; with publish=true it
|
|
19
|
+
# uploads to TestPyPI.
|
|
20
|
+
#
|
|
21
|
+
# It is one workflow rather than a separate `on: push: tags` one on purpose: the
|
|
22
|
+
# auto-release tag is pushed with GITHUB_TOKEN, and a tag pushed with
|
|
23
|
+
# GITHUB_TOKEN starts no new workflow runs, so the follow-up jobs of the run
|
|
24
|
+
# that cut it build and publish it.
|
|
25
|
+
#
|
|
26
|
+
# Publishing uses PyPI Trusted Publishing (OIDC) from the "pypi" / "testpypi"
|
|
27
|
+
# environments: register this workflow as the project's trusted publisher on
|
|
28
|
+
# both indexes. No API tokens are stored.
|
|
29
|
+
name: Release
|
|
30
|
+
|
|
31
|
+
on:
|
|
32
|
+
push:
|
|
33
|
+
branches: [main]
|
|
34
|
+
tags: ["v*"]
|
|
35
|
+
workflow_dispatch:
|
|
36
|
+
inputs:
|
|
37
|
+
publish:
|
|
38
|
+
description: "Upload to TestPyPI"
|
|
39
|
+
type: boolean
|
|
40
|
+
default: false
|
|
41
|
+
|
|
42
|
+
permissions:
|
|
43
|
+
contents: read
|
|
44
|
+
|
|
45
|
+
jobs:
|
|
46
|
+
# Reads the release label off the merged PR and, if there is one, bumps the
|
|
47
|
+
# version and pushes the tag. Only on a push to main.
|
|
48
|
+
release:
|
|
49
|
+
name: cut the release tag
|
|
50
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
51
|
+
runs-on: ubuntu-24.04
|
|
52
|
+
permissions:
|
|
53
|
+
contents: write
|
|
54
|
+
pull-requests: read
|
|
55
|
+
# Never two release cuts at once; a queued one runs after (and fails on the
|
|
56
|
+
# non-fast-forward push if main moved).
|
|
57
|
+
concurrency:
|
|
58
|
+
group: release-main
|
|
59
|
+
cancel-in-progress: false
|
|
60
|
+
outputs:
|
|
61
|
+
bump: ${{ steps.pr.outputs.bump }}
|
|
62
|
+
tag: ${{ steps.bump.outputs.tag }}
|
|
63
|
+
steps:
|
|
64
|
+
- name: Read the release label off the merged PR
|
|
65
|
+
id: pr
|
|
66
|
+
env:
|
|
67
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
68
|
+
run: |
|
|
69
|
+
set -euo pipefail
|
|
70
|
+
# An API error fails the job; only an empty list means "no PR".
|
|
71
|
+
pr_json=$(gh api "repos/${{ github.repository }}/commits/${{ github.sha }}/pulls" \
|
|
72
|
+
--jq '.[0] // empty')
|
|
73
|
+
if [ -z "$pr_json" ]; then
|
|
74
|
+
echo "No PR associated with this push: nothing to release"
|
|
75
|
+
echo "bump=none" >> "$GITHUB_OUTPUT"
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
labels=$(echo "$pr_json" | jq -r '[.labels[].name] | join(",")')
|
|
79
|
+
bump=none
|
|
80
|
+
count=0
|
|
81
|
+
for lvl in major minor patch; do
|
|
82
|
+
if echo ",$labels," | grep -q ",release:${lvl},"; then
|
|
83
|
+
bump=$lvl
|
|
84
|
+
count=$((count + 1))
|
|
85
|
+
fi
|
|
86
|
+
done
|
|
87
|
+
if [ "$count" -gt 1 ]; then
|
|
88
|
+
echo "::error::More than one release label on the PR: pick exactly one"
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
echo "bump=${bump}" >> "$GITHUB_OUTPUT"
|
|
92
|
+
echo "Resolved bump level: ${bump}"
|
|
93
|
+
|
|
94
|
+
# The pushed commit, not whatever main is now: the bump is pushed to main
|
|
95
|
+
# without force, so if main has moved since, the push fails loudly.
|
|
96
|
+
- uses: actions/checkout@v4
|
|
97
|
+
if: steps.pr.outputs.bump != 'none'
|
|
98
|
+
with:
|
|
99
|
+
ref: ${{ github.sha }}
|
|
100
|
+
fetch-depth: 0
|
|
101
|
+
|
|
102
|
+
- name: Bump the version
|
|
103
|
+
if: steps.pr.outputs.bump != 'none'
|
|
104
|
+
id: bump
|
|
105
|
+
env:
|
|
106
|
+
BUMP: ${{ steps.pr.outputs.bump }}
|
|
107
|
+
run: |
|
|
108
|
+
set -euo pipefail
|
|
109
|
+
next=$(python3 tools/bump_version.py "$BUMP")
|
|
110
|
+
echo "version=${next}" >> "$GITHUB_OUTPUT"
|
|
111
|
+
echo "tag=v${next}" >> "$GITHUB_OUTPUT"
|
|
112
|
+
echo "Bumped to ${next} (${BUMP})"
|
|
113
|
+
|
|
114
|
+
- name: Fail if the version is already tagged or on PyPI
|
|
115
|
+
if: steps.pr.outputs.bump != 'none'
|
|
116
|
+
env:
|
|
117
|
+
VERSION: ${{ steps.bump.outputs.version }}
|
|
118
|
+
TAG: ${{ steps.bump.outputs.tag }}
|
|
119
|
+
run: |
|
|
120
|
+
set -euo pipefail
|
|
121
|
+
if git rev-parse "refs/tags/${TAG}" >/dev/null 2>&1; then
|
|
122
|
+
echo "::error::Tag ${TAG} already exists"
|
|
123
|
+
exit 1
|
|
124
|
+
fi
|
|
125
|
+
code=$(curl -s -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/carla-canvas/${VERSION}/json")
|
|
126
|
+
if [ "$code" = 200 ]; then
|
|
127
|
+
echo "::error::carla-canvas ${VERSION} is already on PyPI"
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
- name: Commit, tag and push
|
|
132
|
+
if: steps.pr.outputs.bump != 'none'
|
|
133
|
+
env:
|
|
134
|
+
TAG: ${{ steps.bump.outputs.tag }}
|
|
135
|
+
run: |
|
|
136
|
+
set -euo pipefail
|
|
137
|
+
git config user.name "github-actions[bot]"
|
|
138
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
139
|
+
git add python/carla_canvas/__init__.py
|
|
140
|
+
git commit -m "chore(release): ${TAG}"
|
|
141
|
+
git tag -a "${TAG}" -m "Release ${TAG}"
|
|
142
|
+
# Both or neither: a main bump without its tag could not be re-run.
|
|
143
|
+
git push --atomic origin "HEAD:main" "refs/tags/${TAG}"
|
|
144
|
+
|
|
145
|
+
# A draft until `github-release` publishes it, after PyPI.
|
|
146
|
+
- name: Draft the GitHub release with generated notes
|
|
147
|
+
if: steps.pr.outputs.bump != 'none'
|
|
148
|
+
env:
|
|
149
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
150
|
+
TAG: ${{ steps.bump.outputs.tag }}
|
|
151
|
+
run: gh release create "${TAG}" --draft --verify-tag --title "${TAG}" --generate-notes
|
|
152
|
+
|
|
153
|
+
# For all three entry points: whether to build, which commit, and where to
|
|
154
|
+
# publish. Runs even when `release` is skipped (tag push, dispatch).
|
|
155
|
+
resolve:
|
|
156
|
+
name: resolve what to build
|
|
157
|
+
needs: [release]
|
|
158
|
+
if: always() && needs.release.result != 'failure' && needs.release.result != 'cancelled'
|
|
159
|
+
runs-on: ubuntu-24.04
|
|
160
|
+
outputs:
|
|
161
|
+
proceed: ${{ steps.r.outputs.proceed }}
|
|
162
|
+
ref: ${{ steps.r.outputs.ref }}
|
|
163
|
+
tag: ${{ steps.r.outputs.tag }}
|
|
164
|
+
index: ${{ steps.r.outputs.index }}
|
|
165
|
+
steps:
|
|
166
|
+
- id: r
|
|
167
|
+
env:
|
|
168
|
+
EVENT: ${{ github.event_name }}
|
|
169
|
+
REF_TYPE: ${{ github.ref_type }}
|
|
170
|
+
REF_NAME: ${{ github.ref_name }}
|
|
171
|
+
SHA: ${{ github.sha }}
|
|
172
|
+
BUMP: ${{ needs.release.outputs.bump }}
|
|
173
|
+
CUT_TAG: ${{ needs.release.outputs.tag }}
|
|
174
|
+
INPUT_PUBLISH: ${{ inputs.publish }}
|
|
175
|
+
run: |
|
|
176
|
+
set -euo pipefail
|
|
177
|
+
proceed=true; ref="$SHA"; tag=""; index=none
|
|
178
|
+
if [ "$EVENT" = push ] && [ "$REF_TYPE" = branch ]; then
|
|
179
|
+
if [ "${BUMP:-none}" = none ]; then
|
|
180
|
+
proceed=false
|
|
181
|
+
else
|
|
182
|
+
ref="refs/tags/${CUT_TAG}"; tag="$CUT_TAG"; index=pypi
|
|
183
|
+
fi
|
|
184
|
+
elif [ "$EVENT" = push ]; then
|
|
185
|
+
ref="refs/tags/${REF_NAME}"; tag="$REF_NAME"; index=pypi
|
|
186
|
+
elif [ "$INPUT_PUBLISH" = true ]; then
|
|
187
|
+
index=testpypi
|
|
188
|
+
fi
|
|
189
|
+
{
|
|
190
|
+
echo "proceed=$proceed"; echo "ref=$ref"; echo "tag=$tag"; echo "index=$index"
|
|
191
|
+
} >> "$GITHUB_OUTPUT"
|
|
192
|
+
echo "proceed=$proceed ref=$ref tag=$tag index=$index"
|
|
193
|
+
|
|
194
|
+
version:
|
|
195
|
+
needs: resolve
|
|
196
|
+
if: always() && needs.resolve.outputs.proceed == 'true'
|
|
197
|
+
runs-on: ubuntu-24.04
|
|
198
|
+
steps:
|
|
199
|
+
- uses: actions/checkout@v4
|
|
200
|
+
with: { ref: "${{ needs.resolve.outputs.ref }}" }
|
|
201
|
+
- name: The tag matches the package version
|
|
202
|
+
env:
|
|
203
|
+
TAG: ${{ needs.resolve.outputs.tag }}
|
|
204
|
+
run: |
|
|
205
|
+
version=$(python3 tools/bump_version.py --current)
|
|
206
|
+
echo "carla-canvas ${version}"
|
|
207
|
+
if [ -n "$TAG" ] && [ "$TAG" != "v$version" ]; then
|
|
208
|
+
echo "::error::tag $TAG does not match the package version $version"
|
|
209
|
+
exit 1
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
wheel:
|
|
213
|
+
name: wheel (${{ matrix.arch.name }})
|
|
214
|
+
needs: [resolve, version]
|
|
215
|
+
if: always() && needs.version.result == 'success'
|
|
216
|
+
strategy:
|
|
217
|
+
fail-fast: false
|
|
218
|
+
matrix:
|
|
219
|
+
arch:
|
|
220
|
+
- { name: x86_64, runner: ubuntu-24.04 }
|
|
221
|
+
- { name: aarch64, runner: ubuntu-24.04-arm }
|
|
222
|
+
runs-on: ${{ matrix.arch.runner }}
|
|
223
|
+
steps:
|
|
224
|
+
- uses: actions/checkout@v4
|
|
225
|
+
with: { ref: "${{ needs.resolve.outputs.ref }}" }
|
|
226
|
+
- uses: ./.github/actions/wheel
|
|
227
|
+
with:
|
|
228
|
+
arch: ${{ matrix.arch.name }}
|
|
229
|
+
- uses: actions/upload-artifact@v4
|
|
230
|
+
with: { name: "dist-wheel-${{ matrix.arch.name }}", path: dist/*.whl }
|
|
231
|
+
|
|
232
|
+
sdist:
|
|
233
|
+
needs: [resolve, version]
|
|
234
|
+
if: always() && needs.version.result == 'success'
|
|
235
|
+
runs-on: ubuntu-24.04
|
|
236
|
+
steps:
|
|
237
|
+
- uses: actions/checkout@v4
|
|
238
|
+
with: { ref: "${{ needs.resolve.outputs.ref }}" }
|
|
239
|
+
- uses: actions/setup-python@v5
|
|
240
|
+
with: { python-version: "3.12" }
|
|
241
|
+
- run: pipx run build --sdist
|
|
242
|
+
- uses: actions/upload-artifact@v4
|
|
243
|
+
with: { name: dist-sdist, path: dist/*.tar.gz }
|
|
244
|
+
|
|
245
|
+
publish:
|
|
246
|
+
needs: [resolve, wheel, sdist]
|
|
247
|
+
if: >-
|
|
248
|
+
always() && needs.wheel.result == 'success' && needs.sdist.result == 'success'
|
|
249
|
+
&& needs.resolve.outputs.index != 'none'
|
|
250
|
+
runs-on: ubuntu-24.04
|
|
251
|
+
environment: ${{ needs.resolve.outputs.index }}
|
|
252
|
+
permissions:
|
|
253
|
+
id-token: write # Trusted Publishing
|
|
254
|
+
steps:
|
|
255
|
+
- uses: actions/download-artifact@v4
|
|
256
|
+
with: { pattern: "dist-*", path: dist, merge-multiple: true }
|
|
257
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
258
|
+
with:
|
|
259
|
+
repository-url: ${{ needs.resolve.outputs.index == 'pypi' && 'https://upload.pypi.org/legacy/' || 'https://test.pypi.org/legacy/' }}
|
|
260
|
+
|
|
261
|
+
# Publishes the GitHub release once PyPI has the version: the auto-release
|
|
262
|
+
# path drafted it in `release`; a hand-cut tag gets one here.
|
|
263
|
+
github-release:
|
|
264
|
+
needs: [resolve, publish]
|
|
265
|
+
if: always() && needs.publish.result == 'success' && needs.resolve.outputs.tag != ''
|
|
266
|
+
runs-on: ubuntu-24.04
|
|
267
|
+
permissions:
|
|
268
|
+
contents: write
|
|
269
|
+
steps:
|
|
270
|
+
- env:
|
|
271
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
272
|
+
TAG: ${{ needs.resolve.outputs.tag }}
|
|
273
|
+
REPO: ${{ github.repository }}
|
|
274
|
+
run: |
|
|
275
|
+
set -euo pipefail
|
|
276
|
+
if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
|
|
277
|
+
gh release edit "$TAG" --repo "$REPO" --draft=false
|
|
278
|
+
else
|
|
279
|
+
gh release create "$TAG" --repo "$REPO" --verify-tag --title "$TAG" --generate-notes
|
|
280
|
+
fi
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# carla_canvas
|
|
2
|
+
|
|
3
|
+
Swaps CARLA's camera/LiDAR rendering (3D Gaussian splatting or any renderer)
|
|
4
|
+
behind the CARLA API for typesafe_carla programs: a typesafe_carla sensor
|
|
5
|
+
plugin (C++, `native/`) ↔ gRPC (`proto/carla_canvas/v1/renderer.proto`) ↔
|
|
6
|
+
renderer service (Python, `python/carla_canvas/`). Design: `docs/design.md`.
|
|
7
|
+
|
|
8
|
+
## Commands
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
uv venv && TSC_BACKEND=mock uv pip install -e ../typesafe_carla # typesafe_carla with ABI >= 5.3
|
|
12
|
+
uv pip install scikit-build-core grpcio grpcio-tools protobuf numpy pyyaml pytest cyclonedds
|
|
13
|
+
uv pip install --no-build-isolation -e . # builds libcarla_canvas_plugin.so
|
|
14
|
+
cmake -S . -B build && cmake --build build -j && ctest --test-dir build
|
|
15
|
+
PATH=$PWD/.venv/bin:$PATH .venv/bin/python -m pytest # e2e tests run Codon programs (mock CARLA)
|
|
16
|
+
.venv/bin/python tools/gen_proto.py # after editing proto/ (commit the output)
|
|
17
|
+
CARLA_CANVAS_CONFIG=examples/primitives.yaml typesafe-codon run examples/camera_lidar.py
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The plugin uses the system's gRPC C++, protobuf and yaml-cpp (`libgrpc++-dev
|
|
21
|
+
protobuf-compiler-grpc libprotobuf-dev protobuf-compiler libyaml-cpp-dev`), or
|
|
22
|
+
builds them from source with `-DCARLA_CANVAS_FETCH_DEPS=ON` (`cmake/FetchDeps.cmake`,
|
|
23
|
+
pinned versions; what the release wheels do: `.github/actions/wheel`,
|
|
24
|
+
`tools/check_wheel.py`).
|
|
25
|
+
|
|
26
|
+
Releases: a PR merged to main with one `release:major|minor|patch` label is
|
|
27
|
+
released by `.github/workflows/release.yml` (bumps `__version__` with
|
|
28
|
+
`tools/bump_version.py`, tags `v<version>`, publishes to PyPI). Do not bump the
|
|
29
|
+
version or tag by hand.
|
|
30
|
+
|
|
31
|
+
## Rules
|
|
32
|
+
|
|
33
|
+
- The user's program never changes: everything is chosen by the file in
|
|
34
|
+
`CARLA_CANVAS_CONFIG`. Do not add carla_canvas imports to examples or tests
|
|
35
|
+
of programs (`examples/camera_lidar.py` is typesafe_carla's example).
|
|
36
|
+
- The protocol mirrors the CARLA API (names, units, Unreal's left-handed
|
|
37
|
+
frame, world transforms, CARLA's raw measurement layouts). A protocol
|
|
38
|
+
change bumps `kProtocolVersion` (plugin.cpp) and `PROTOCOL_VERSION`
|
|
39
|
+
(server/service.py) together, and regenerates `python/carla_canvas/v1/`.
|
|
40
|
+
- CARLA semantics live in the service, not in renderers: encodings
|
|
41
|
+
(`encodings.py`), the LiDAR model (`lidar.py`, mirrors RayCastLidar), sensor
|
|
42
|
+
attributes and defaults (`sensors.py`), ROS 2 (`ros/`). A renderer only
|
|
43
|
+
implements `load_world` / `render_camera` (and optionally `cast_rays`).
|
|
44
|
+
- ROS 2 output mirrors CARLA's publishers (`LibCarla/source/carla/ros2`):
|
|
45
|
+
topic names, types, frame ids, y negation, TransformFromCarlaRotation.
|
|
46
|
+
Renderers never implement DDS.
|
|
47
|
+
- The plugin talks to typesafe_carla only through `get_proc` (`tsc_api.hpp`);
|
|
48
|
+
it must not link against the library. It needs typesafe_carla's C ABI 5.3+.
|
|
49
|
+
- 3D Gaussian splatting for real use is splatsim's backend (its repository
|
|
50
|
+
depends on carla_canvas). `renderers/gaussian_splatting.py` and
|
|
51
|
+
`primitives.py` are reference and test renderers: keep them numpy-only and
|
|
52
|
+
covered by the CPU tests.
|
|
53
|
+
- `ros/dds_types.py` must not use `from __future__ import annotations`
|
|
54
|
+
(cyclonedds resolves field types from the module's globals).
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.20)
|
|
2
|
+
project(carla_canvas LANGUAGES C CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
7
|
+
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
|
8
|
+
set(CMAKE_BUILD_TYPE Release)
|
|
9
|
+
endif()
|
|
10
|
+
|
|
11
|
+
# The sensor plugin needs gRPC C++, protobuf, yaml-cpp and typesafe_carla's
|
|
12
|
+
# header. Without them (a renderer-only install: the Python SDK is all a
|
|
13
|
+
# renderer host needs) it is skipped, unless CARLA_CANVAS_REQUIRE_PLUGIN is ON.
|
|
14
|
+
# With CARLA_CANVAS_FETCH_DEPS, the missing ones are built from source (the
|
|
15
|
+
# published wheels; a source build without system packages).
|
|
16
|
+
option(CARLA_CANVAS_REQUIRE_PLUGIN "Fail when the sensor plugin cannot be built" OFF)
|
|
17
|
+
option(CARLA_CANVAS_FETCH_DEPS
|
|
18
|
+
"Build missing gRPC, protobuf and yaml-cpp from source, and download typesafe_carla's header"
|
|
19
|
+
OFF)
|
|
20
|
+
find_package(Threads REQUIRED)
|
|
21
|
+
find_package(Protobuf QUIET)
|
|
22
|
+
find_package(gRPC CONFIG QUIET)
|
|
23
|
+
find_package(yaml-cpp QUIET)
|
|
24
|
+
find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin)
|
|
25
|
+
if(CARLA_CANVAS_FETCH_DEPS)
|
|
26
|
+
include(cmake/FetchDeps.cmake)
|
|
27
|
+
endif()
|
|
28
|
+
# gRPC and protobuf: the system's, or both from gRPC's source tree.
|
|
29
|
+
if(Protobuf_FOUND AND gRPC_FOUND AND GRPC_CPP_PLUGIN)
|
|
30
|
+
set(_protoc protobuf::protoc)
|
|
31
|
+
set(_grpc_cpp_plugin "${GRPC_CPP_PLUGIN}")
|
|
32
|
+
set(_protobuf_lib protobuf::libprotobuf)
|
|
33
|
+
set(_grpc_lib gRPC::grpc++)
|
|
34
|
+
elseif(CARLA_CANVAS_FETCH_DEPS)
|
|
35
|
+
carla_canvas_fetch_grpc()
|
|
36
|
+
set(_protoc protoc)
|
|
37
|
+
set(_grpc_cpp_plugin "$<TARGET_FILE:grpc_cpp_plugin>")
|
|
38
|
+
set(_protobuf_lib libprotobuf)
|
|
39
|
+
set(_grpc_lib grpc++)
|
|
40
|
+
# protobuf's well-known types (google/protobuf/struct.proto)
|
|
41
|
+
set(_proto_include --proto_path "${grpc_SOURCE_DIR}/third_party/protobuf/src")
|
|
42
|
+
endif()
|
|
43
|
+
if(NOT yaml-cpp_FOUND AND CARLA_CANVAS_FETCH_DEPS)
|
|
44
|
+
carla_canvas_fetch_yaml_cpp()
|
|
45
|
+
endif()
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# typesafe_carla's C ABI header (typesafe_carla/ffi.h, ABI 5.3 or later).
|
|
48
|
+
# The plugin does not link against the library (it gets the functions through
|
|
49
|
+
# get_proc at load time), it only needs the declarations. Found, in order:
|
|
50
|
+
# TYPESAFE_CARLA_INCLUDE_DIR, the installed typesafe_carla Python package
|
|
51
|
+
# (typesafe_carla.paths.include_dir()), a sibling checkout ../typesafe_carla.
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
set(TYPESAFE_CARLA_INCLUDE_DIR "$ENV{TYPESAFE_CARLA_INCLUDE_DIR}" CACHE PATH
|
|
54
|
+
"Directory containing typesafe_carla/ffi.h")
|
|
55
|
+
if(NOT TYPESAFE_CARLA_INCLUDE_DIR)
|
|
56
|
+
find_package(Python COMPONENTS Interpreter QUIET)
|
|
57
|
+
if(Python_EXECUTABLE)
|
|
58
|
+
execute_process(
|
|
59
|
+
COMMAND "${Python_EXECUTABLE}" -c
|
|
60
|
+
"from typesafe_carla import paths; print(paths.include_dir())"
|
|
61
|
+
OUTPUT_VARIABLE _tsc_include OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET
|
|
62
|
+
RESULT_VARIABLE _tsc_result)
|
|
63
|
+
if(_tsc_result EQUAL 0 AND EXISTS "${_tsc_include}/typesafe_carla/ffi.h")
|
|
64
|
+
set(TYPESAFE_CARLA_INCLUDE_DIR "${_tsc_include}")
|
|
65
|
+
endif()
|
|
66
|
+
endif()
|
|
67
|
+
endif()
|
|
68
|
+
if(NOT TYPESAFE_CARLA_INCLUDE_DIR AND
|
|
69
|
+
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../typesafe_carla/native/include/typesafe_carla/ffi.h")
|
|
70
|
+
set(TYPESAFE_CARLA_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../typesafe_carla/native/include")
|
|
71
|
+
endif()
|
|
72
|
+
if(NOT TYPESAFE_CARLA_INCLUDE_DIR AND CARLA_CANVAS_FETCH_DEPS)
|
|
73
|
+
carla_canvas_fetch_tsc_header(TYPESAFE_CARLA_INCLUDE_DIR)
|
|
74
|
+
endif()
|
|
75
|
+
set(_missing "")
|
|
76
|
+
if(NOT EXISTS "${TYPESAFE_CARLA_INCLUDE_DIR}/typesafe_carla/ffi.h")
|
|
77
|
+
list(APPEND _missing "typesafe_carla/ffi.h (install typesafe-carla or set TYPESAFE_CARLA_INCLUDE_DIR)")
|
|
78
|
+
endif()
|
|
79
|
+
if(NOT _grpc_lib)
|
|
80
|
+
list(APPEND _missing "gRPC C++ and protobuf")
|
|
81
|
+
endif()
|
|
82
|
+
if(NOT TARGET yaml-cpp::yaml-cpp)
|
|
83
|
+
list(APPEND _missing "yaml-cpp")
|
|
84
|
+
endif()
|
|
85
|
+
if(_missing AND NOT CARLA_CANVAS_FETCH_DEPS)
|
|
86
|
+
list(APPEND _missing "(or -DCARLA_CANVAS_FETCH_DEPS=ON to build them from source)")
|
|
87
|
+
endif()
|
|
88
|
+
if(_missing)
|
|
89
|
+
if(CARLA_CANVAS_REQUIRE_PLUGIN)
|
|
90
|
+
message(FATAL_ERROR "carla_canvas: cannot build the sensor plugin, missing: ${_missing}")
|
|
91
|
+
endif()
|
|
92
|
+
message(WARNING "carla_canvas: not building the sensor plugin (renderer-only install), "
|
|
93
|
+
"missing: ${_missing}")
|
|
94
|
+
return()
|
|
95
|
+
endif()
|
|
96
|
+
message(STATUS "carla_canvas: typesafe_carla header from ${TYPESAFE_CARLA_INCLUDE_DIR}")
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# gRPC / protobuf / yaml-cpp (Ubuntu: libgrpc++-dev protobuf-compiler-grpc
|
|
100
|
+
# libprotobuf-dev protobuf-compiler libyaml-cpp-dev)
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
set(_proto_dir "${CMAKE_CURRENT_SOURCE_DIR}/proto")
|
|
104
|
+
set(_proto "${_proto_dir}/carla_canvas/v1/renderer.proto")
|
|
105
|
+
set(_gen "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
|
106
|
+
set(_gen_srcs
|
|
107
|
+
"${_gen}/carla_canvas/v1/renderer.pb.cc"
|
|
108
|
+
"${_gen}/carla_canvas/v1/renderer.grpc.pb.cc")
|
|
109
|
+
add_custom_command(
|
|
110
|
+
OUTPUT ${_gen_srcs}
|
|
111
|
+
"${_gen}/carla_canvas/v1/renderer.pb.h"
|
|
112
|
+
"${_gen}/carla_canvas/v1/renderer.grpc.pb.h"
|
|
113
|
+
COMMAND "${CMAKE_COMMAND}" -E make_directory "${_gen}"
|
|
114
|
+
COMMAND ${_protoc}
|
|
115
|
+
--proto_path "${_proto_dir}" ${_proto_include}
|
|
116
|
+
--cpp_out "${_gen}"
|
|
117
|
+
--grpc_out "${_gen}"
|
|
118
|
+
--plugin=protoc-gen-grpc=${_grpc_cpp_plugin}
|
|
119
|
+
"${_proto}"
|
|
120
|
+
DEPENDS "${_proto}" ${_protoc} $<$<TARGET_EXISTS:grpc_cpp_plugin>:grpc_cpp_plugin>
|
|
121
|
+
COMMENT "Generating the renderer protocol (C++)")
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# The sensor plugin (loaded by typesafe_carla: TYPESAFE_CARLA_SENSOR_PLUGIN)
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
add_library(carla_canvas_plugin MODULE
|
|
127
|
+
native/src/plugin.cpp
|
|
128
|
+
native/src/config.cpp
|
|
129
|
+
${_gen_srcs})
|
|
130
|
+
target_include_directories(carla_canvas_plugin PRIVATE
|
|
131
|
+
"${TYPESAFE_CARLA_INCLUDE_DIR}" "${_gen}" native/src)
|
|
132
|
+
target_link_libraries(carla_canvas_plugin PRIVATE
|
|
133
|
+
${_grpc_lib} ${_protobuf_lib} yaml-cpp::yaml-cpp Threads::Threads)
|
|
134
|
+
set_target_properties(carla_canvas_plugin PROPERTIES
|
|
135
|
+
OUTPUT_NAME carla_canvas_plugin
|
|
136
|
+
CXX_VISIBILITY_PRESET hidden
|
|
137
|
+
VISIBILITY_INLINES_HIDDEN ON)
|
|
138
|
+
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
|
139
|
+
target_compile_options(carla_canvas_plugin PRIVATE -Wall -Wextra)
|
|
140
|
+
# Export only tsc_sensor_plugin_init.
|
|
141
|
+
target_link_options(carla_canvas_plugin PRIVATE
|
|
142
|
+
"-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/native/src/exports.map")
|
|
143
|
+
endif()
|
|
144
|
+
|
|
145
|
+
install(TARGETS carla_canvas_plugin LIBRARY DESTINATION carla_canvas/_native
|
|
146
|
+
COMPONENT carla_canvas)
|
|
147
|
+
|
|
148
|
+
option(CARLA_CANVAS_BUILD_TESTS "Build native tests" ON)
|
|
149
|
+
if(CARLA_CANVAS_BUILD_TESTS)
|
|
150
|
+
enable_testing()
|
|
151
|
+
add_executable(test_config native/tests/test_config.cpp native/src/config.cpp)
|
|
152
|
+
target_include_directories(test_config PRIVATE native/src)
|
|
153
|
+
target_link_libraries(test_config PRIVATE ${_protobuf_lib} yaml-cpp::yaml-cpp)
|
|
154
|
+
add_test(NAME test_config COMMAND test_config "${CMAKE_CURRENT_SOURCE_DIR}/native/tests")
|
|
155
|
+
endif()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 carla_canvas contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|