Until 2026-08-26 this repository was five large single-file scripts
(29,967 lines in total; moho2svg.py alone was 10,913). They are now a
package. This document is the map, and the record of what the split found.
mohokit/
paths.py repo/schema/corpus directory locations (one owner)
container.py read/write .mohoproj + .moho ZIP, preserving formatting
integrity.py check_integrity: the gate every write passes
geometry.py Vec2, Mat2D
bezier.py pure cubic-Bezier maths
channel.py Channel: animated values and their evaluation
style.py Color, ResolvedStyle, masking/blend enums
curve.py Bezier reconstruction from Moho's own representation
pathtrace.py re-tracing a shape's edges into walk order
outline.py tapered strokes and brush stamping
skin.py bone skinning
svgutil.py small helpers
document/ the document model (kinds, skeleton, mesh, layer, document)
render/ settings, exporter, walk, shapes (the SVG render path)
lottie/ constants, transform, clipping, exporter, validate
svgin/ SVG -> Moho importer
lottiein/ Lottie -> Moho importer
build/ Moho-JSON writing shared by both importers
edit/ errors, locator, schema, fields, rename, lifecycle,
create, move, keyframes, actions, enums, batch
cli/ one module per command-line entry point
64 modules. Largest file: 2,341 lines, down from 10,913.
The reverse-engineering notebook that was moho2svg.py's module docstring
(1,453 lines) now lives in
moho-svg-internals.md; code throughout the
package cites it by section name.
moho2svg.py, mohoedit.py, moho2lottie.py, svg2moho.py and
lottie2moho.py remain, as re-export facades. Every make target, every
script under tools/, and any of the author's own one-off scripts keep
working unchanged. make check-package fails if a facade stops exporting
something its consumers import.
New code should import from mohokit directly. The facades exist because
breaking a working checkout to tidy an import path is a bad trade — not
because the indirection earns its keep.
pyproject.toml is provided for installing the package (and defines the
five console scripts), but nothing requires installation: the package sits
at the repository root, so import mohokit works from a checkout as-is.
This repository has no unit tests; correctness is defined empirically. So the split was done as a verbatim move — source slices relocated, not rewritten — and checked three ways:
- Byte-identical output. Five documents exported to SVG and to Lottie before the refactor; after every step, re-exported and compared byte for byte. All ten matched at every checkpoint. The two importers mint a fresh UUID per layer, so their comparison masks UUIDs first — verified that two runs of unmodified code agree exactly once masked.
- Source-text comparison. Every top-level definition's source in the
new package was compared against its source in
git HEAD. Result: nothing missing, and exactly four deliberate differences — two function-level imports that break a cycle, one path re-anchoring, and one type annotation quoted. - The existing gates. All twelve editing gates plus
check-integrity,check-roundtripandcheck-lottiepass.
make check-package is new, and guards the structure itself.
Restructuring surfaced things reading would not have.
| Cycle | Cause | Resolution |
|---|---|---|
curve ↔ pathtrace |
both needed the other's helpers | pure Bezier maths extracted to bezier.py |
skin ↔ document.skeleton |
_wrap_angle/KeyedWorldState sat in the skinning section but are used only by Skeleton |
moved to document/skeleton.py |
render.shapes ↔ render.exporter |
Exporter used for one type annotation |
typing.TYPE_CHECKING |
A fourth, edit.fields ↔ edit.schema, was avoided by giving every
exception type its own dependency-free module, edit/errors.py.
svg2moho.py and lottie2moho.py shared 20 symbol names. Comparing the
code (ignoring docstrings):
- 13 are safely shareable and now live once, in
mohokit/build/common.py. - 2 look identical but are not shareable:
fit_curve_pointandpixel_to_mohoeach call something that genuinely differs (forward_blend_direction,V2). Sharing them would have silently bound one importer to the other's behaviour. - 5 differ outright:
V2,WarningCounter,build_document,forward_blend_direction,main.
That middle category is the point worth remembering: identical code is only shareable when everything it depends on is shareable too. It was found by an import cycle, not by reading, and a naive deduplication would have changed one importer's output without any test noticing — neither importer has a regression gate of its own.
Whether the five real divergences are intentional is not settled here. Someone who knows both importers should decide; nothing in this refactor establishes which version is right.
_REPO_ROOT and SCHEMA_DIR were computed by counting dirname() levels
from a file's own location. Moving those files made both wrong, and the
failure mode was an empty corpus walk rather than an error — a check
that passed while walking zero documents. mohokit/paths.py now owns these
locations so no other module counts levels.
- Splitting
Exporter(2,252 lines) andLottieExporter(2,341). Each is one cohesive class. Breaking one apart means extracting collaborator objects and rewriting call sites — a design change, not a move, in the most empirically-calibrated code in the repository. The natural seams are visible (Exporterhas a 7-method brush cluster and a 6-method mask cluster), so this is a well-scoped follow-up; it just should not ride along with a move-only refactor. document/skeleton.py(1,710 lines).Skeleton.world_matricesis calibrated against Moho's own renders and CLAUDE.md warns against touching it without new reference evidence. Left whole.- Rewriting anything for style. Correctness here rests on byte-comparison against Moho's own output; "improving" code that no test pins down is how such a codebase silently breaks.
- Migrating
tools/to importmohokitdirectly. The facades make it unnecessary, and 25 edits with no offsetting benefit is churn.