ncel 0.2.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.
- ncel-0.2.1/PKG-INFO +122 -0
- ncel-0.2.1/README.md +97 -0
- ncel-0.2.1/ncel/__init__.py +18 -0
- ncel-0.2.1/ncel/__main__.py +10 -0
- ncel-0.2.1/ncel/bundle.py +84 -0
- ncel-0.2.1/ncel/cli.py +189 -0
- ncel-0.2.1/ncel/data/AGENT-GUIDE.md +92 -0
- ncel-0.2.1/ncel/data/lantern_film.ncel +14 -0
- ncel-0.2.1/ncel/data/pins.json +52 -0
- ncel-0.2.1/ncel/data/shower_f0001.ncel +11 -0
- ncel-0.2.1/ncel/data/starter.ncel +10 -0
- ncel-0.2.1/ncel/doctor.py +221 -0
- ncel-0.2.1/ncel/fallback.py +177 -0
- ncel-0.2.1/ncel/native.py +67 -0
- ncel-0.2.1/ncel/platform_info.py +151 -0
- ncel-0.2.1/ncel/toolchain.py +197 -0
- ncel-0.2.1/ncel/verify.py +168 -0
- ncel-0.2.1/ncel.egg-info/PKG-INFO +122 -0
- ncel-0.2.1/ncel.egg-info/SOURCES.txt +23 -0
- ncel-0.2.1/ncel.egg-info/dependency_links.txt +1 -0
- ncel-0.2.1/ncel.egg-info/entry_points.txt +2 -0
- ncel-0.2.1/ncel.egg-info/requires.txt +4 -0
- ncel-0.2.1/ncel.egg-info/top_level.txt +1 -0
- ncel-0.2.1/pyproject.toml +53 -0
- ncel-0.2.1/setup.cfg +4 -0
ncel-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ncel
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: NorthCel (.ncel) one-command CLI: runs the native MoonBit + Bend/HVM2 + GLSL runtime when the platform supports it, and says so plainly when it does not.
|
|
5
|
+
Author: North Noir
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://northnoir.com/northcel
|
|
8
|
+
Project-URL: Documentation, https://northnoir.com/northcel
|
|
9
|
+
Project-URL: Repository, https://github.com/syver777/NorthNoir/tree/main/packaging
|
|
10
|
+
Keywords: ncel,northcel,ai-drawn-video,animation,hand-drawn,moonbit,bend,glsl
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
20
|
+
Classifier: Topic :: Multimedia :: Video
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: northcel==0.2.1
|
|
24
|
+
Requires-Dist: ncel-native==0.2.1; sys_platform == "linux" and platform_machine == "x86_64"
|
|
25
|
+
|
|
26
|
+
# ncel — NorthCel, drawn frame by frame by a native runtime
|
|
27
|
+
|
|
28
|
+
`ncel` is the one-command NorthCel (`.ncel`) CLI. It runs the **native runtime**:
|
|
29
|
+
|
|
30
|
+
* **MoonBit** parses the typed scene/film AST, decodes every contour chain, expands
|
|
31
|
+
`def`/`use`, applies frame deltas and the deterministic boil hash;
|
|
32
|
+
* **Bend 0.2.38 / HVM 2.0.22** evaluates every `bend_swarm` agent;
|
|
33
|
+
* **GLSL 4.30**, compiled by the **Mesa/EGL** driver (llvmpipe, no GPU), rasterises
|
|
34
|
+
the normative `aa×aa` even-odd integration.
|
|
35
|
+
|
|
36
|
+
The Python reference implementation (`northcel`) ships alongside as the
|
|
37
|
+
conformance oracle and as a **clearly labelled** fallback where the native runtime
|
|
38
|
+
cannot run. Nothing is ever silently substituted.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
pip install ncel # Linux x86_64
|
|
44
|
+
ncel doctor # stages, exact versions, platform verdict
|
|
45
|
+
ncel verify # real renders + determinism, from bundled fixtures
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Supported today: **Linux x86_64**, glibc >= 2.39, Mesa `libegl1` + `libgl1` +
|
|
49
|
+
`mesa-libgallium` (Ubuntu 24.04+, Debian 13, Fedora 40+, RHEL 10).
|
|
50
|
+
|
|
51
|
+
Not supported yet, and it says so: macOS (Apple's OpenGL caps at 4.1; the 4.30
|
|
52
|
+
shader cannot compile — macOS needs Vulkan/MoltenVK or a tested GL 4.1 path),
|
|
53
|
+
Windows, and non-x86_64 Linux. On those platforms `pip install ncel` still
|
|
54
|
+
installs the CLI, `ncel doctor` prints the exact reason, and `render`/`film`/`check`
|
|
55
|
+
use the Python reference renderer, marked `python-reference` — never "native".
|
|
56
|
+
|
|
57
|
+
`pip install ncel` brings `ncel-native` (the ~3 MB runtime bundle, no download at
|
|
58
|
+
first run) and `northcel` (the reference parser/validator/formatter).
|
|
59
|
+
|
|
60
|
+
## Use
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
ncel render scene.ncel -o frame.png --frame 0 --json
|
|
64
|
+
ncel film film.ncel -o out/ --frames 0..5 --mp4 out.mp4 --json
|
|
65
|
+
ncel check scene.ncel --json
|
|
66
|
+
ncel info scene.ncel --json
|
|
67
|
+
ncel fmt scene.ncel --check
|
|
68
|
+
ncel guide # offline authoring guide
|
|
69
|
+
ncel doctor --json # machine-readable stage report
|
|
70
|
+
ncel toolchain status|install # MoonBit compiler, only needed to rebuild
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`--json` receipts come from the native runtime and include `language`,
|
|
74
|
+
`northcel_version`, `spec_version`, size/fps/frames, source bytes, per-frame
|
|
75
|
+
`sha256`, stage timings, geometry, swarm counts and `"stage": "MoonBit+Bend+GLSL"`.
|
|
76
|
+
|
|
77
|
+
## What `doctor` proves
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
ncel 0.2.1 (NorthCel spec 0.2.0-draft)
|
|
81
|
+
platform linux-x86_64 glibc 2.39
|
|
82
|
+
supported yes
|
|
83
|
+
native 0.2.0 (bundle checksums 13 verified, stage MoonBit+Bend+GLSL)
|
|
84
|
+
MoonBit stage present: yes binary ncel-ast compiled with MoonBit 0.1.20261102 (ea07287 2026-11-02)
|
|
85
|
+
Bend bend-lang 0.2.38 …/bin/bend
|
|
86
|
+
HVM2 hvm 2.0.22 …/bin/hvm
|
|
87
|
+
GL runtime 4.5 (Core Profile) Mesa 25.2.8 llvmpipe (LLVM 20.1.2, 256 bits) GLSL 4.50
|
|
88
|
+
GLSL draw430 14829a3cbb6c… compiles draw410 fe4d0c4fe4e0… compiles
|
|
89
|
+
ffmpeg present (optional, --mp4)
|
|
90
|
+
reference northcel 0.2.1 — fallback only — non-native reference renderer
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## What `verify` proves
|
|
94
|
+
|
|
95
|
+
* a starter scene renders to a 320×180 PNG;
|
|
96
|
+
* the shower `f0001` frame reproduces the pinned conformance hash
|
|
97
|
+
(`be0eb036…`), which the Python reference reproduces pixel-for-pixel;
|
|
98
|
+
* a 6-frame film renders 6 distinct frames;
|
|
99
|
+
* both renders repeat with identical sha256 (determinism).
|
|
100
|
+
|
|
101
|
+
## Toolchain notes
|
|
102
|
+
|
|
103
|
+
A runtime needs no downloads: the MoonBit-compiled stage, Bend and HVM2 are inside
|
|
104
|
+
`ncel-native`, with every file sha256-pinned in `bundle-manifest.json` and checked
|
|
105
|
+
against `native/ncel/toolchain.lock`. The **MoonBit compiler** (101 MB) is only
|
|
106
|
+
needed to rebuild the native tree from source — `ncel toolchain install` fetches
|
|
107
|
+
and verifies it into `~/.local/share/ncel/toolchain`.
|
|
108
|
+
|
|
109
|
+
GLSL is not installed; it is compiled by the GL driver. That is why the installer
|
|
110
|
+
provisions the **Mesa/EGL runtime** and why `doctor` compiles and links both
|
|
111
|
+
shaders as proof.
|
|
112
|
+
|
|
113
|
+
## Uninstall
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
pip uninstall ncel ncel-native northcel
|
|
117
|
+
rm -rf ~/.local/share/ncel # only if you ran `ncel toolchain install`
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Licence
|
|
121
|
+
|
|
122
|
+
Proprietary — North Noir. See `LICENSE` in the NorthCel distribution.
|
ncel-0.2.1/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# ncel — NorthCel, drawn frame by frame by a native runtime
|
|
2
|
+
|
|
3
|
+
`ncel` is the one-command NorthCel (`.ncel`) CLI. It runs the **native runtime**:
|
|
4
|
+
|
|
5
|
+
* **MoonBit** parses the typed scene/film AST, decodes every contour chain, expands
|
|
6
|
+
`def`/`use`, applies frame deltas and the deterministic boil hash;
|
|
7
|
+
* **Bend 0.2.38 / HVM 2.0.22** evaluates every `bend_swarm` agent;
|
|
8
|
+
* **GLSL 4.30**, compiled by the **Mesa/EGL** driver (llvmpipe, no GPU), rasterises
|
|
9
|
+
the normative `aa×aa` even-odd integration.
|
|
10
|
+
|
|
11
|
+
The Python reference implementation (`northcel`) ships alongside as the
|
|
12
|
+
conformance oracle and as a **clearly labelled** fallback where the native runtime
|
|
13
|
+
cannot run. Nothing is ever silently substituted.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
pip install ncel # Linux x86_64
|
|
19
|
+
ncel doctor # stages, exact versions, platform verdict
|
|
20
|
+
ncel verify # real renders + determinism, from bundled fixtures
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Supported today: **Linux x86_64**, glibc >= 2.39, Mesa `libegl1` + `libgl1` +
|
|
24
|
+
`mesa-libgallium` (Ubuntu 24.04+, Debian 13, Fedora 40+, RHEL 10).
|
|
25
|
+
|
|
26
|
+
Not supported yet, and it says so: macOS (Apple's OpenGL caps at 4.1; the 4.30
|
|
27
|
+
shader cannot compile — macOS needs Vulkan/MoltenVK or a tested GL 4.1 path),
|
|
28
|
+
Windows, and non-x86_64 Linux. On those platforms `pip install ncel` still
|
|
29
|
+
installs the CLI, `ncel doctor` prints the exact reason, and `render`/`film`/`check`
|
|
30
|
+
use the Python reference renderer, marked `python-reference` — never "native".
|
|
31
|
+
|
|
32
|
+
`pip install ncel` brings `ncel-native` (the ~3 MB runtime bundle, no download at
|
|
33
|
+
first run) and `northcel` (the reference parser/validator/formatter).
|
|
34
|
+
|
|
35
|
+
## Use
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
ncel render scene.ncel -o frame.png --frame 0 --json
|
|
39
|
+
ncel film film.ncel -o out/ --frames 0..5 --mp4 out.mp4 --json
|
|
40
|
+
ncel check scene.ncel --json
|
|
41
|
+
ncel info scene.ncel --json
|
|
42
|
+
ncel fmt scene.ncel --check
|
|
43
|
+
ncel guide # offline authoring guide
|
|
44
|
+
ncel doctor --json # machine-readable stage report
|
|
45
|
+
ncel toolchain status|install # MoonBit compiler, only needed to rebuild
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`--json` receipts come from the native runtime and include `language`,
|
|
49
|
+
`northcel_version`, `spec_version`, size/fps/frames, source bytes, per-frame
|
|
50
|
+
`sha256`, stage timings, geometry, swarm counts and `"stage": "MoonBit+Bend+GLSL"`.
|
|
51
|
+
|
|
52
|
+
## What `doctor` proves
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
ncel 0.2.1 (NorthCel spec 0.2.0-draft)
|
|
56
|
+
platform linux-x86_64 glibc 2.39
|
|
57
|
+
supported yes
|
|
58
|
+
native 0.2.0 (bundle checksums 13 verified, stage MoonBit+Bend+GLSL)
|
|
59
|
+
MoonBit stage present: yes binary ncel-ast compiled with MoonBit 0.1.20261102 (ea07287 2026-11-02)
|
|
60
|
+
Bend bend-lang 0.2.38 …/bin/bend
|
|
61
|
+
HVM2 hvm 2.0.22 …/bin/hvm
|
|
62
|
+
GL runtime 4.5 (Core Profile) Mesa 25.2.8 llvmpipe (LLVM 20.1.2, 256 bits) GLSL 4.50
|
|
63
|
+
GLSL draw430 14829a3cbb6c… compiles draw410 fe4d0c4fe4e0… compiles
|
|
64
|
+
ffmpeg present (optional, --mp4)
|
|
65
|
+
reference northcel 0.2.1 — fallback only — non-native reference renderer
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## What `verify` proves
|
|
69
|
+
|
|
70
|
+
* a starter scene renders to a 320×180 PNG;
|
|
71
|
+
* the shower `f0001` frame reproduces the pinned conformance hash
|
|
72
|
+
(`be0eb036…`), which the Python reference reproduces pixel-for-pixel;
|
|
73
|
+
* a 6-frame film renders 6 distinct frames;
|
|
74
|
+
* both renders repeat with identical sha256 (determinism).
|
|
75
|
+
|
|
76
|
+
## Toolchain notes
|
|
77
|
+
|
|
78
|
+
A runtime needs no downloads: the MoonBit-compiled stage, Bend and HVM2 are inside
|
|
79
|
+
`ncel-native`, with every file sha256-pinned in `bundle-manifest.json` and checked
|
|
80
|
+
against `native/ncel/toolchain.lock`. The **MoonBit compiler** (101 MB) is only
|
|
81
|
+
needed to rebuild the native tree from source — `ncel toolchain install` fetches
|
|
82
|
+
and verifies it into `~/.local/share/ncel/toolchain`.
|
|
83
|
+
|
|
84
|
+
GLSL is not installed; it is compiled by the GL driver. That is why the installer
|
|
85
|
+
provisions the **Mesa/EGL runtime** and why `doctor` compiles and links both
|
|
86
|
+
shaders as proof.
|
|
87
|
+
|
|
88
|
+
## Uninstall
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
pip uninstall ncel ncel-native northcel
|
|
92
|
+
rm -rf ~/.local/share/ncel # only if you ran `ncel toolchain install`
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Licence
|
|
96
|
+
|
|
97
|
+
Proprietary — North Noir. See `LICENSE` in the NorthCel distribution.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""`ncel` — the one-command NorthCel CLI.
|
|
2
|
+
|
|
3
|
+
The native runtime (MoonBit typed AST + Bend/HVM2 swarms + GLSL via Mesa/EGL) is
|
|
4
|
+
the implementation this CLI calls whenever it is available. The Python reference
|
|
5
|
+
renderer (`northcel`) is used only when the native stages genuinely cannot run on
|
|
6
|
+
the current platform, and then the tool says so and labels the result.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.2.1"
|
|
10
|
+
|
|
11
|
+
#: Version of the bundled native runtime build (native/ncel, commit 3ad5f7e).
|
|
12
|
+
NATIVE_RUNTIME_VERSION = "0.2.0"
|
|
13
|
+
|
|
14
|
+
#: NorthCel language specification implemented by both implementations.
|
|
15
|
+
SPEC_VERSION = "0.2.0-draft"
|
|
16
|
+
|
|
17
|
+
#: Exit code used when the native runtime is unavailable on this platform.
|
|
18
|
+
EXIT_NO_NATIVE = 3
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Locate and verify the bundled native runtime (`ncel-native` wheel)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def bundle_dir() -> Path | None:
|
|
11
|
+
"""Directory of the installed native bundle, or None when it is not installed."""
|
|
12
|
+
try:
|
|
13
|
+
import ncel_native_bundle # noqa: PLC0415 - optional platform dependency
|
|
14
|
+
except Exception:
|
|
15
|
+
return None
|
|
16
|
+
path = Path(ncel_native_bundle.__file__).resolve().parent
|
|
17
|
+
return path if (path / "bundle-manifest.json").is_file() else path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def manifest_path() -> Path | None:
|
|
21
|
+
root = bundle_dir()
|
|
22
|
+
if not root:
|
|
23
|
+
return None
|
|
24
|
+
manifest = root / "bundle-manifest.json"
|
|
25
|
+
return manifest if manifest.is_file() else None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_manifest() -> dict | None:
|
|
29
|
+
path = manifest_path()
|
|
30
|
+
if not path:
|
|
31
|
+
return None
|
|
32
|
+
try:
|
|
33
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
34
|
+
except Exception:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def binary() -> Path | None:
|
|
39
|
+
root = bundle_dir()
|
|
40
|
+
if not root:
|
|
41
|
+
return None
|
|
42
|
+
exe = root / "bin" / "ncel-native"
|
|
43
|
+
return exe if exe.is_file() else None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def sha256_file(path: Path) -> str:
|
|
47
|
+
digest = hashlib.sha256()
|
|
48
|
+
with path.open("rb") as handle:
|
|
49
|
+
for chunk in iter(lambda: handle.read(1 << 20), b""):
|
|
50
|
+
digest.update(chunk)
|
|
51
|
+
return digest.hexdigest()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def verify_checksums(manifest: dict | None = None) -> dict:
|
|
55
|
+
"""Hash every file named in the bundle manifest.
|
|
56
|
+
|
|
57
|
+
Returns {"checked": n, "failed": [{"path", "expected", "actual"}], "missing": [...]}.
|
|
58
|
+
"""
|
|
59
|
+
root = bundle_dir()
|
|
60
|
+
manifest = manifest if manifest is not None else load_manifest()
|
|
61
|
+
if not root or not manifest:
|
|
62
|
+
return {"checked": 0, "failed": [], "missing": [], "available": False}
|
|
63
|
+
|
|
64
|
+
checked = 0
|
|
65
|
+
failed: list[dict] = []
|
|
66
|
+
missing: list[str] = []
|
|
67
|
+
for entry in manifest.get("files", []):
|
|
68
|
+
rel = entry.get("path", "")
|
|
69
|
+
target = root / rel
|
|
70
|
+
if not target.is_file():
|
|
71
|
+
missing.append(rel)
|
|
72
|
+
continue
|
|
73
|
+
actual = sha256_file(target)
|
|
74
|
+
checked += 1
|
|
75
|
+
if actual != entry.get("sha256"):
|
|
76
|
+
failed.append(
|
|
77
|
+
{"path": rel, "expected": entry.get("sha256"), "actual": actual}
|
|
78
|
+
)
|
|
79
|
+
return {
|
|
80
|
+
"checked": checked,
|
|
81
|
+
"failed": failed,
|
|
82
|
+
"missing": missing,
|
|
83
|
+
"available": True,
|
|
84
|
+
}
|
ncel-0.2.1/ncel/cli.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""`ncel` command-line entry point.
|
|
2
|
+
|
|
3
|
+
Dispatch rule (this is the whole product promise): if the native runtime can run
|
|
4
|
+
here, run it. If it cannot, say why, use the Python reference and label the result.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from . import EXIT_NO_NATIVE, NATIVE_RUNTIME_VERSION, SPEC_VERSION, __version__, fallback, native
|
|
13
|
+
from . import doctor as doctor_mod
|
|
14
|
+
from . import toolchain as toolchain_mod
|
|
15
|
+
from . import verify as verify_mod
|
|
16
|
+
|
|
17
|
+
USAGE = """ncel — NorthCel (.ncel) command line
|
|
18
|
+
|
|
19
|
+
usage: ncel <command> [options]
|
|
20
|
+
|
|
21
|
+
doctor [--json] stage availability, exact versions, platform verdict
|
|
22
|
+
verify [--json] [--keep DIR] render the bundled fixtures with the native runtime
|
|
23
|
+
check FILE [--json] parse + validate (native when available)
|
|
24
|
+
info FILE [--json] scene/film metadata, layers, rings, vertices
|
|
25
|
+
render FILE [-o OUT.png] [--frame N] [--seed N] [--json]
|
|
26
|
+
film FILE -o OUTDIR [--frames 0..5] [--mp4 OUT.mp4] [--json]
|
|
27
|
+
fmt FILE [-w|--write] [--check] canonical formatting (Python reference)
|
|
28
|
+
guide offline authoring guide
|
|
29
|
+
toolchain <status|install> [--json] [--dry-run]
|
|
30
|
+
version, --version versions (launcher, native runtime, spec)
|
|
31
|
+
|
|
32
|
+
Global: --no-native force the labelled Python reference path (never called "native")
|
|
33
|
+
Platform: Linux x86_64, glibc >= 2.39, Mesa EGL/llvmpipe. macOS/Windows are not
|
|
34
|
+
supported in this release; `ncel doctor` explains exactly what is missing.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _version_line() -> str:
|
|
39
|
+
native_version = NATIVE_RUNTIME_VERSION
|
|
40
|
+
usable, _ = native.available()
|
|
41
|
+
if usable:
|
|
42
|
+
doc = native.doctor_json() or {}
|
|
43
|
+
if doc.get("version"):
|
|
44
|
+
native_version = doc["version"]
|
|
45
|
+
state = "native" if usable else "python-reference only"
|
|
46
|
+
return f"ncel {__version__} (native runtime {native_version}, spec {SPEC_VERSION}, {state})"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _pop_flag(args: list[str], *names: str) -> bool:
|
|
50
|
+
hit = False
|
|
51
|
+
for name in names:
|
|
52
|
+
while name in args:
|
|
53
|
+
args.remove(name)
|
|
54
|
+
hit = True
|
|
55
|
+
return hit
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _pop_option(args: list[str], *names: str) -> str | None:
|
|
59
|
+
for name in names:
|
|
60
|
+
for index, token in enumerate(args):
|
|
61
|
+
if token == name and index + 1 < len(args):
|
|
62
|
+
value = args[index + 1]
|
|
63
|
+
del args[index : index + 2]
|
|
64
|
+
return value
|
|
65
|
+
if token.startswith(name + "="):
|
|
66
|
+
value = token.split("=", 1)[1]
|
|
67
|
+
del args[index]
|
|
68
|
+
return value
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _native_or_reason(args: list[str], command: str) -> tuple[bool, str | None]:
|
|
73
|
+
if _pop_flag(args, "--no-native", "--python"):
|
|
74
|
+
return False, "explicitly requested with --no-native"
|
|
75
|
+
return native.available() if command != "" else (False, None)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _passthrough(command: str, args: list[str], strip_native_only: bool = True) -> int:
|
|
79
|
+
usable, reason = _native_or_reason(args, command)
|
|
80
|
+
if strip_native_only:
|
|
81
|
+
_pop_flag(args, "--require-full")
|
|
82
|
+
if not usable:
|
|
83
|
+
fallback.notify(reason)
|
|
84
|
+
return fallback.delegate([command, *args])
|
|
85
|
+
return native.run([command, *args]).returncode
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _cmd_render(args: list[str]) -> int:
|
|
89
|
+
usable, reason = _native_or_reason(args, "render")
|
|
90
|
+
if usable:
|
|
91
|
+
return native.run(["render", *args]).returncode
|
|
92
|
+
fallback.notify(reason)
|
|
93
|
+
as_json = _pop_flag(args, "--json")
|
|
94
|
+
out = _pop_option(args, "-o", "--out")
|
|
95
|
+
frame_raw = _pop_option(args, "-f", "--frame") or "0"
|
|
96
|
+
seed_raw = _pop_option(args, "--seed")
|
|
97
|
+
files = [a for a in args if not a.startswith("-")]
|
|
98
|
+
if not files:
|
|
99
|
+
print("usage: ncel render FILE [-o OUT.png] [--frame N] [--seed N]", file=sys.stderr)
|
|
100
|
+
return 2
|
|
101
|
+
try:
|
|
102
|
+
frame = int(frame_raw)
|
|
103
|
+
seed = int(seed_raw) if seed_raw is not None else None
|
|
104
|
+
except ValueError:
|
|
105
|
+
print("ncel: --frame/--seed must be integers", file=sys.stderr)
|
|
106
|
+
return 2
|
|
107
|
+
return fallback.render(files[0], out, frame, seed, as_json)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _cmd_film(args: list[str]) -> int:
|
|
111
|
+
usable, reason = _native_or_reason(args, "film")
|
|
112
|
+
if usable:
|
|
113
|
+
return native.run(["film", *args]).returncode
|
|
114
|
+
fallback.notify(reason)
|
|
115
|
+
as_json = _pop_flag(args, "--json")
|
|
116
|
+
out_dir = _pop_option(args, "-o", "--out-dir")
|
|
117
|
+
frames = _pop_option(args, "--frames")
|
|
118
|
+
mp4 = _pop_option(args, "--mp4")
|
|
119
|
+
files = [a for a in args if not a.startswith("-")]
|
|
120
|
+
if not files or not out_dir:
|
|
121
|
+
print("usage: ncel film FILE -o OUTDIR [--frames 0..5] [--mp4 OUT.mp4]", file=sys.stderr)
|
|
122
|
+
return 2
|
|
123
|
+
return fallback.film(files[0], out_dir, frames, mp4, as_json)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _cmd_guide(args: list[str]) -> int:
|
|
127
|
+
path = Path(__file__).with_name("data") / "AGENT-GUIDE.md"
|
|
128
|
+
if path.is_file():
|
|
129
|
+
sys.stdout.write(path.read_text(encoding="utf-8"))
|
|
130
|
+
return 0
|
|
131
|
+
return fallback.delegate(["guide", *args])
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _cmd_toolchain(args: list[str]) -> int:
|
|
135
|
+
as_json = _pop_flag(args, "--json")
|
|
136
|
+
dry_run = _pop_flag(args, "--dry-run")
|
|
137
|
+
action = next((a for a in args if not a.startswith("-")), "status")
|
|
138
|
+
return toolchain_mod.cmd_toolchain(action, as_json, dry_run)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main(argv: list[str] | None = None) -> int:
|
|
142
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
143
|
+
if not args or args[0] in {"-h", "--help", "help"}:
|
|
144
|
+
print(USAGE, end="")
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
command, rest = args[0], args[1:]
|
|
148
|
+
|
|
149
|
+
if command in {"--version", "version", "-V"}:
|
|
150
|
+
print(_version_line())
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
if command == "doctor":
|
|
154
|
+
return doctor_mod.cmd_doctor(_pop_flag(rest, "--json"))
|
|
155
|
+
|
|
156
|
+
if command == "verify":
|
|
157
|
+
as_json = _pop_flag(rest, "--json")
|
|
158
|
+
keep = _pop_option(rest, "--keep")
|
|
159
|
+
return verify_mod.cmd_verify(as_json, keep)
|
|
160
|
+
|
|
161
|
+
if command == "check":
|
|
162
|
+
return _passthrough("check", rest)
|
|
163
|
+
if command == "info":
|
|
164
|
+
return _passthrough("info", rest)
|
|
165
|
+
|
|
166
|
+
if command == "fmt":
|
|
167
|
+
# The native runtime has no formatter; formatting is the Python reference.
|
|
168
|
+
return fallback.delegate(["fmt", *rest])
|
|
169
|
+
|
|
170
|
+
if command == "guide":
|
|
171
|
+
return _cmd_guide(rest)
|
|
172
|
+
|
|
173
|
+
if command == "render":
|
|
174
|
+
return _cmd_render(rest)
|
|
175
|
+
if command == "film":
|
|
176
|
+
return _cmd_film(rest)
|
|
177
|
+
|
|
178
|
+
if command == "toolchain":
|
|
179
|
+
return _cmd_toolchain(rest)
|
|
180
|
+
|
|
181
|
+
if command in {"--json-doctor"}:
|
|
182
|
+
return doctor_mod.cmd_doctor(True)
|
|
183
|
+
|
|
184
|
+
print(f"ncel: unknown command {command!r}\n", file=sys.stderr)
|
|
185
|
+
print(USAGE, end="", file=sys.stderr)
|
|
186
|
+
return 2
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
__all__ = ["main", "EXIT_NO_NATIVE"]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# NorthCel quick agent guide — tools 0.2.1, spec 0.2.0-draft
|
|
2
|
+
|
|
3
|
+
Discover this guide offline with `npx northcel guide`. The npm package is a
|
|
4
|
+
zero-dependency **partial JS pre-check and Python CLI bridge**, not a renderer.
|
|
5
|
+
|
|
6
|
+
## Start here (a complete 3-second film)
|
|
7
|
+
|
|
8
|
+
```ncel
|
|
9
|
+
northcel "0.2.0-draft"
|
|
10
|
+
film Kite(width: 320, height: 180, fps: 12, frames: 36, precision: 0.25px, seed: 42) {
|
|
11
|
+
palette { ink: #202830, red: #D65C52 }
|
|
12
|
+
paper { color: #FFF8EC, antialias_grid: 4 }
|
|
13
|
+
cel kite(color: $red, outline: ($ink, 2px), boil: 0.3px,
|
|
14
|
+
translate: (lerp(0, 100, t), sin(t * 6.28318) * 10)) {
|
|
15
|
+
"240,120 100,80 -100,140 -100,-140"
|
|
16
|
+
}
|
|
17
|
+
stroke string(color: $ink, taper: (1.5, 0.5)) {
|
|
18
|
+
(60 + 100 * t, 85 + sin(t * 6.28318) * 10), (110, 160)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Coordinates are pixels, origin top left. Quoted rings are **relative integer
|
|
24
|
+
quarter-pixel pairs**, NOT absolute pixel vertices: multiply vertices by 4,
|
|
25
|
+
write the first vertex then deltas; at least three vertices per ring. `|`
|
|
26
|
+
separates rings (even-odd holes). `precision: 1px` and `0.25px-b63` instead use
|
|
27
|
+
`x0,y0:ABCD...` Base-63 pairs; prefer plain quarter-pixel deltas for hand authoring.
|
|
28
|
+
Never put expressions inside a quoted ring: move it with `translate: (x, y)`.
|
|
29
|
+
|
|
30
|
+
## Five language features to combine
|
|
31
|
+
|
|
32
|
+
1. **Reusable shapes:** `def Mark(dx: 0, dy: 0) { stroke line(width: 2) { (0, 0), (8, 4) } }`
|
|
33
|
+
then `use Mark(dx: 20, dy: 30)`. Only primitives inside `def`, not nested `use`.
|
|
34
|
+
`dx/dy` auto-translate expanded geometry; don't also add them to coordinates.
|
|
35
|
+
Same-named cels in repeated uses replace one another; use distinct named defs
|
|
36
|
+
for independent cel instances (stroke/ribbon uses append).
|
|
37
|
+
2. **Film/time:** `frames` belongs in the film **header**, never `film X { frames: 36 }`.
|
|
38
|
+
Duration = frames / fps. `t = f / max(1, frames - 1)`; `f` is zero-based.
|
|
39
|
+
Expressions: numbers, parameters, `t`, `f`, `seed`, `+ - * /`, `sin(x)`,
|
|
40
|
+
`cos(x)`, `lerp(a, b, t)`, `step(edge, x)`. Use spaces around minus.
|
|
41
|
+
`frame 12 { ... }` affects exactly frame 12, not all subsequent frames.
|
|
42
|
+
3. **Deterministic line boil:** `boil: 0.3px` or `boil: (0.3, 1)`; keep it subtle.
|
|
43
|
+
No randomness, wall clock or cross-frame state.
|
|
44
|
+
4. **Colour/cels:** `palette { ink: #202830 }` before `$ink` references; hex, `rgb(r,g,b)`
|
|
45
|
+
or `tone: 0..255`. `cel name(color: $ink, outline: (#000, 2px)) { "..." }`
|
|
46
|
+
couples fill and outline. `layer`/`wash` fills; `bend_swarm`/`swarm` adds detail rings.
|
|
47
|
+
5. **Tapered ink:** `stroke name(color: $ink, taper: (3, 1)) { (10, 10), (40, 20) }`;
|
|
48
|
+
`ribbon name(color: $ink) { (10, 10, 3), (40, 20, 1) }` uses per-point widths.
|
|
49
|
+
At least two points. Give every primitive a distinct name in each block.
|
|
50
|
+
|
|
51
|
+
## Honest validation and look-and-fix loop
|
|
52
|
+
|
|
53
|
+
1. Run `npx northcel check film.ncel --json`. Without Python it reports
|
|
54
|
+
`partial: true`, `validator: "js"`, a reason and next command. `ok: true` and
|
|
55
|
+
exit 0 mean **only that the JS pre-check found no error**, not full validity.
|
|
56
|
+
Text says **PARTIAL**, never OK. Syntax/header/ring/palette checks help, but
|
|
57
|
+
numeric ranges, expression evaluation, def resolution and render quality remain unchecked.
|
|
58
|
+
2. Run `npx northcel check film.ncel --require-full --json` before claiming full
|
|
59
|
+
validation. Exit 2 = no full validator (clean partial); exit 1 = detected error;
|
|
60
|
+
exit 0 = Python full check succeeded. Install `pipx install northcel` (or
|
|
61
|
+
`pip install northcel` in a venv), expose its Python CLI on PATH, then repeat.
|
|
62
|
+
The npm bridge finds importable Python modules or pipx-style Python entrypoints.
|
|
63
|
+
Never suppress exit 2 or call a partial check fully validated.
|
|
64
|
+
3. Python: `northcel render film.ncel -o frame0.png --frame 0`, inspect the PNG;
|
|
65
|
+
`northcel film film.ncel -o frames/ --mp4 preview.mp4`, inspect first/middle/last
|
|
66
|
+
and transition frames. ffmpeg is required for MP4; verify the MP4 exists.
|
|
67
|
+
Render samples in reverse order and compare native RGB hashes for determinism.
|
|
68
|
+
4. Fix the named block that is wrong, preserve good parts, render again; up to
|
|
69
|
+
three look-and-fix rounds. `northcel fmt -w film.ncel` canonicalizes it.
|
|
70
|
+
Full validation is not a visual-quality or runtime-expression guarantee.
|
|
71
|
+
|
|
72
|
+
Alternatively install `pip install "northnoir[ncel]"` and use
|
|
73
|
+
`northnoir init ink --language ncel --duration 3 --fps 24`,
|
|
74
|
+
`northnoir check ink/scene.ncel`, `northnoir render ink/scene.ncel -o ink/video.mp4`.
|
|
75
|
+
This is an explicit optional local path; JavaScript `scene.html` stays the default.
|
|
76
|
+
NorthCel films are silent; there is no native audio engine or hosted NorthCel API.
|
|
77
|
+
|
|
78
|
+
## Common mistakes
|
|
79
|
+
|
|
80
|
+
- No root, unclosed quote/brace, unknown statement: fix the reported line:column.
|
|
81
|
+
- `frames:` in the body: move it to `film Name(..., frames: N) { ... }`.
|
|
82
|
+
- Missing primitive attributes: `cel`, `layer`, `swarm`, `stroke` need `(...)`,
|
|
83
|
+
even `()`. `ribbon`, `def` and `use` may omit it. Palette takes `{ key: value }`
|
|
84
|
+
or `(key: value)`, not a name. `use Name(...)` has **no body**.
|
|
85
|
+
- Invalid/odd Base-63 tail: use only `0-9A-Za-z_`, two characters per delta.
|
|
86
|
+
Quarter-pixel payloads need whole integer pairs, not decimals or suffixes.
|
|
87
|
+
- Undefined `$colour`: declare it earlier, including before `def` bodies.
|
|
88
|
+
- Duplicate names: intentional layer/cel replacement is legal (JS warns, not errors).
|
|
89
|
+
Frame overrides replace named layers/cels; same-named strokes/ribbons append,
|
|
90
|
+
leaving stale ink. For moving ink use cels or time expressions instead.
|
|
91
|
+
- Undefined def, bad sizes/fps, expression variable errors: JS may miss these;
|
|
92
|
+
use Python full check **and real renders**. No JS success authorizes shipping.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
northcel "0.1.0-draft"
|
|
2
|
+
|
|
3
|
+
film LanternFilm(width: 512, height: 512, fps: 12, frames: 6, precision: 0.25px, seed: 108) {
|
|
4
|
+
paper { color: #101824, antialias_grid: 4, grain: 0.012 }
|
|
5
|
+
def SeaWave(dx: 0, dy: 0) {
|
|
6
|
+
stroke crest(color: #6CCFF6, taper: (4.5, 1.5), boil: 0.75px) { (40 + dx, 410 + dy), (180 + dx, 398 + dy), (330 + dx, 412 + dy), (470 + dx, 402 + dy) }
|
|
7
|
+
}
|
|
8
|
+
layer sea_base(color: #1B3B5A, rings: 1) { "0,1520 2048,0 0,528 -2048,0" }
|
|
9
|
+
layer lantern_halo(color: #F4A261, rings: 1, boil: 0.6px, translate: (sin(t * 6.28318) * 14, 0)) { "920,640 208,0 40,120 -40,120 -208,0 -40,-120" }
|
|
10
|
+
ribbon post_ink(color: #F8F9FA, boil: 0.4px) { (256, 480, 12), (256 + sin(t * 6.28318) * 6, 320, 9), (256 + sin(t * 6.28318) * 14, 190, 6) }
|
|
11
|
+
bend_swarm rain_streaks(agents: 4, color: #E0FBFC, boil: 0.35px, translate: (lerp(-18, 18, t), lerp(0, 48, t))) { "320,240 24,72 -8,4 -24,-72|840,300 24,72 -8,4 -24,-72|1420,220 24,72 -8,4 -24,-72|1760,340 24,72 -8,4 -24,-72" }
|
|
12
|
+
use SeaWave(dx: sin(t * 6.28318) * 10, dy: 0)
|
|
13
|
+
use SeaWave(dx: cos(t * 6.28318) * -12, dy: 42)
|
|
14
|
+
}
|