-
2026-09-19 -- one legal-name rule, explained rather than repaired (
str_utils/_str_utils.py,core_utils/engines/shots/shot_model.py). New onStrUtils:LEGAL_NAME_PATTERN([A-Za-z0-9_]+) andLEGAL_NAME_RULE,is_legal_name(name),illegal_name_chars(name)(the distinct offending characters, in order),name_error(name, subject=, reason=)(the sentence a field or a store shows -- it never rewrites the name) andlegal_name_matcher(legal_name)(the inverse ofto_legal_name: every original that converts to a legal name).to_legal_namestill converts; it is for identities two sides derive the same way, never for what a user typed.ShotStore.NAME_PATTERN/NAME_RULEare now these constants andShotStore.name_errorbuilds its character refusal fromStrUtils.name_error, so a UI field validating a shot name (uitk'sset_validator("name")) enforces the same set the store refuses. Pinned intest_str.py. -
2026-09-19 -- 0.11.0 retires what 0.10 deprecated (
core_utils/git.py,net_utils/preview_server.py,file_utils/_file_utils.py,core_utils/engines/textures/map_factory/conversions.py). Removed after a release of warnings, none with a caller in any ecosystem package:Git(no callers, no tests); the JSON key-value clusterFileUtils.set_json_file/get_json_file/set_json/get_json(a process-global path,assertvalidation, a truncate-then-write -- useFileUtils.write_json/read_json);ConversionRegistry.register_from_class(defineregister_conversions(registry)and useadd_plugin); and thepythontk.net_utils.preview_serverimport path (ptk.PreviewServer/PreviewDeliverer/PreviewBridgeare unchanged).test_file.RetiredSurfaceTestpins that the freed names stay unused, and the non-atomic-JSON-writer guard now expects none. -
2026-09-18 -- A shot name is refused at entry instead of respelled on export (
core_utils/engines/shots/shot_model.py,shot_transfer.py,shot_detection.py). A shot's name is its clip name on every carrier (FBX take, Unity AnimationClip, glTF animation, theshot_metadatajoin key), and the export used to rewrite whatever the tools accepted --"Step 9.1"shipped asStep_9_1, a second"Shot 1"asShot_1_1, with nothing said. New onShotStore:NAME_PATTERN([A-Za-z0-9_]+) andNAME_RULE,name_error(name, shot_id=None)(the reason a name cannot be used: empty, a character outside the pattern, or another shot's name ignoring case -- Unity joins clips case-insensitively; compared by EXPORTED spelling, soShot_1is refused beside a legacy"Shot 1"it would otherwise have shipped asShot_1_1),unique_name(base, first=None), the pureunique_among(name, taken, first=None)(puts name into its clip spelling first, so its result is always legal) anddefault_name(wanted=None)(a tool-proposed name kept when free, else the next freeShot_<n>).name_erroralso refuses a name another shot EXPORTS as after de-duplication (intro_1, beside legacy case twinsIntro/intro). Behaviour change:define_shotand a renamingupdate_shotraiseValueErrorfor a namename_errorrefuses (checked before any field changes). A legal name now exports VERBATIM -- the legacy respelling collapsedA__BtoA_Band stripped a trailing_; it now applies only to a held name the rule REFUSES ("Step 9.1"), andto_export_viewwarns with eachold -> newwhen it does. A legacy scene's LEGAL names the old rule respelled (A__B,Shot_) therefore ship under a new clip name on their next export, with no warning -- nothing distinguishes one from a name typed today. Clip de-duplication ignores case. Detection names its candidatesShot_<n>(wasShot <n>) anddetect_and_definenames each throughdefault_name(besideShot_1/Shot_2the next areShot_3/Shot_4, notShot_1_2), andShotTransfer.mergenumbers an incoming name the scene already has -- legally, so a legacy"Shot 1"merges asShot_1_2, never"Shot 1_2".ShotManifest.syncchecks a new step's id against the store after its removals and reports one the store refuses as the new action"refused"(logged, left unbuilt) instead of raising part-way through the batch. A boundary restore puts names back without re-validating (undoing a swap). Descriptions are unchanged: JSON carries any text, now guarded by a round-trip test.test_shots_core+test_shot_transfer+test_shots_manifest_core244. -
2026-09-18 --
SceneRecords: every tool-authored scene record is declared ONCE, and the export publishes them in one assembly with its decisions as input (core_utils/scene_records.py,core_utils/engines/shots/shot_model.py,core_utils/export_profile.py,file_utils/mesh_convert/_mesh_convert.py,export_verify.py). Before this, a channel's name lived in five places (the DCC carrier classes, each producer,MeshConvert.*_KEY, the handoff descriptions, unitytk's settings) and the owner doc was seven records short. New:Scope(PRIVATE / DELIVERABLE),Kind(AUTHORED / DERIVED),RecordSpec(key, scope, version, owner, description, kind,after, consumers;make/load/save/clear; a falsy payload CLEARS and never creates; a record newer than its declaration is refused with a warning),Record, theSceneRecordsregistry (15 records;ordered,check_producers,describe,handoff_block, anddeclared_takes(read)-- THE reader of the take list: theshot_metadataclips' own ranges, else the legacyfbx_takes),SceneStoreBase(the whole DCC storage contract:read/write/valuesper scope, plus the inheriteddump/format_dump;_dumpedholds dump's value rules for a store that dumps one carrier of several),ExportContext(clip mode, clip span, provenance, and every record produced so far -- how a reader gets another producer's output) andExportSnapshot(assembleruns producers in dependency order, each isolated -- a raise or a bare-payload return fails that record only -- on a context it starts empty, so a reused context never feeds a producer an earlier assembly's records;commitwrites once, each write isolated the same way, and stamps the handoff from what the carrier holds;publishis the authoring-time form and refuses a PRIVATE record, whose key can be a deliverable one's (the emissive registry and manifest share one);summarylists only what shipped).RecordSpec.version_keynames the payload key an enveloped record versions under:emissive_groupsusesschema, the key its readers read, so it never carries two versions. A HANDOFF context refreshes only DERIVED records, so a bridge can no longer clear an authored manifest it is not the authority on. The deliverable is leaner (data_export): eachshot_metadataclip carries its ownstart/end-- the take list IS the clips, one record and one join key -- and an emptydescription/sectionis left out rather than written as""(objectsstays, an array Unity reads).fbx_takesis no longer written:ShotStore.to_export_view()returns{"shot_metadata": ...}only (it lost itsfbx_takeskey) andexport_recordsthe one shot record; a scene that still holds the legacy list has it cleared by its next shots publish, andFBX_TAKESstays declared (noremove_in) so a file written before 0.11.0 still reads.MeshConvert.build_visibility_tracksrounds every float to 6 places (VISIBILITY_TRACK_DIGITS): float32 attributes read back as doubles published their noise (0.49952034551044694). The handoff instructions are tighter and name no container format -- the same records are read out of a GLB, a sidecar or a dump, where "the FBX" misled (the shot record's description dropped it too) -- and their claim that the manifest names no folder is now true: the DCC lightmap records stop publishing absolutedir/dirs(the GLB embeds its maps, so a build-time folder hint had no business in the deliverable; the host hands the build its folders throughlightmap_dirs), and a manifest written before still reads.ShotStore.export_records(ctx)/produce_export_records/CLIP_MODE_KEY;ExportRun.animation_clips_mode;ExportVerifier,GlbPipelineand every GLB reader of the takes (the declared-takes-but-no-animations gate,apply_glb_clips, the take windows the visibility gates and the clip manifest are cut against) read them throughdeclared_takes, so none reads only the legacy list;fbx_takeslistsglbamong its consumers.MeshConvert.SHADOW_METADATA_VERSIONderives from the registry like its siblings.MeshConvert.FBX_HANDOFF_*,*_METADATA_KEY,VISIBILITY_TRACKS_*now derive from the registry, andbuild_fbx_handoffdelegates toSceneRecords.handoff_block, which describes only the carrier's STRING channels (a keyable float weight was listed as a JSON channel).test_scene_records48;test_shots_core,test_mesh_convertupdated.