OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps commits / b1333af

docs: #120 L4 assets design (§15): live instances, asset blobs, room + personal libraries, browser
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b1333af25a66e30fbc3ac1977fe86dcb81e3052f
by plankatron <fleeky@gmail.com> · 2026-10-19T20:12:10.000Z

Changed files (1) +209 -0

docs/node-tree-and-stacks.md +209 -0
……@@ -1097,3 +1097,212 @@ flatten remaps both; deleting a cable keeps its path curve while another cable p
10971097it. Seen while gating: `set_curve_point` writing only `p` leaves a point's stored handles
10981098where they were, so a `vector` corner moved that way bends its spans (existing curve
10991099behaviour; the curve tool is not affected only if it re-derives — not checked here).
1100
1101## 15. L4 — assets (DESIGN, user answers 2026-10-19)
1102
1103#120 L4: "a collection + its stack saved as an asset; asset browser (bottom panel,
1104drag/drop)". Plugin shapes as data graphs (L4's second item) get their own design after
1105this ships (§15.9). Decided by the user:
1106
1107- **A placement is a LIVE INSTANCE** (Blender's Link): one record, expanded at fold time.
1108 Make Real turns one placement into ordinary editable records (Blender's Make Local).
1109- **An asset holds everything in the collection:** shapes with their params, modifiers and
1110 live cuts; curves (and the cables on them); brush objects (their planes); scene objects;
1111 sub-collections. All relative to a pivot.
1112- **Libraries: the room's AND a personal one** on this machine. Using a personal asset in
1113 a room publishes it to that room.
1114- **The browser: thumbnail tiles in the bottom area; drag a tile into the viewport** to
1115 place it on the surface under the pointer. Click a tile, then a key places it at the
1116 3D cursor.
1117
1118What exists (survey, 2026-10-19, HEAD 2b1a31a):
1119- Collections are organisation only. §2.1a's collection stack and path evaluation are
1120 designed, NOT built: no record targets a collection with a mod or op, and the brush
1121 fold never reads CollectionFold. So an asset cannot be "a collection's stack" yet; v1
1122 is the collection's CONTENTS, each carrying its own stack (§15.1).
1123- Models already travel the way assets must: bytes content-addressed and served by
1124 `ContentOverlay` (hyper-overlay v2 over the game mesh), named in the room by
1125 `AssetSync` (`world:assets:model:<name>` → `{oid, size, ext}`, newest wins), each
1126 placement pinning its oid (`object.set_prop asset`). Assets reuse all of it.
1127- `flatten_map` already writes "the minimum records that rebuild what is there now" with
1128 dots remapped (shapes, Alt+D link sets, curves, mods, carves, objects, collections).
1129 The asset writer is that, scoped to one collection subtree.
1130- The bottom area holds only the texture strip. No panel drags into the 3D view today
1131 (View3D is a MOUSE_FILTER_IGNORE Control; only the outliner Tree drags, into rows).
1132- Build mode keeps a visible pointer (GameMode.wants_pointer); right-drag flies.
1133
1134### 15.1 The asset document
1135
1136An asset is ONE immutable blob, `.hasset`, named by its content hash (the oid):
1137
1138```
1139{ "v": 1, "name": "Pillar", "pivot": "base-centre",
1140 "bounds": [min, max], # local, for placeholders and the drop ghost
1141 "records": [ … ], # ordinary journal payloads, LOCAL dots
1142 "assets": [oid, …] } # nested placements' oids (fetch these too)
1143```
1144
1145- `records` are the payloads `flatten_map` would write for the collection's subtree:
1146 `shape.create` (merged params, frame, size, tex, input, `link` re-rooted inside the
1147 asset), `mod.create`, live `op.create` (cutter and targets both inside — an op whose
1148 cutter or target is OUTSIDE the collection is dropped and reported), `curve.create`,
1149 `brush.create` for brush objects (current planes, group kept), `object.create` +
1150 props + parent (parents outside → none), `col.create` + `col.link` for
1151 sub-collections, and `asset.place` for placements inside it (nesting, §15.2).
1152- LOCAL dots: `L:<n>`, numbered in fold order. Every reference (link, target, cutter,
1153 curve, parent, member) is remapped to them. Nothing in the blob names the room.
1154- Positions are relative to the PIVOT, the collection's bounds' base centre (so a pillar
1155 sits ON the surface it is dropped on). Every frame / point / plane is moved by −pivot.
1156- CANONICAL bytes: keys sorted, floats written exactly (var_to_str of each value), so the
1157 same contents give the same oid on every peer and on every save.
1158- A nested asset is named by oid; a blob cannot contain its own oid (the hash depends
1159 on the contents), so cycles are impossible by construction. Depth is still capped (8).
1160
1161### 15.2 Placement records and the fold
1162
1163| record | payload |
1164|---|---|
1165| `asset.place` | `{asset: oid, name, frame}` — the placement's dot is its identity |
1166| `asset.xform` | `{target, frame}` (latest wins) |
1167| `asset.set` | `{target, asset: oid}` — re-point at another version (§15.4) |
1168| `asset.remove` | `{target}` |
1169
1170**AssetFold** (new, pure, beside ShapeFold) runs FIRST in `refold`: for each live
1171placement whose blob is in the cache, it emits the blob's records as SYNTHETIC records
1172(never journalled), so ShapeFold / ModFold / OpFold / CurveFold / the level see ordinary
1173shapes, curves, cuts and brushes:
1174
1175- Each local dot `L:<n>` becomes `<placement author>/A<n>:<placement seq>` — the shape
1176 pieces' naming scheme (§ shape_fold.gd header) one level up: deterministic, the same on
1177 every peer, unique per placement, sorting at the placement's create position.
1178- Frames are `placement frame × local frame`; curve points and brush planes are
1179 transformed the same way (ShapeFold.transform_planes). The whole result is RIGID
1180 (no scale in v1: a scaled convex piece stays convex, but cables and curves would
1181 need their radii scaled too — later).
1182- `<ver>` of each synthetic shape = max(its local ver, the placement's latest xform/set
1183 seq), so moving a placement renames its pieces (BrushCreate restores by name).
1184- Synthetic records carry `inst: <placement dot>`; group names are
1185 `<placement dot>/<local group>`. Selection maps any of them back to the placement.
1186- Nested `asset.place` inside a blob expands recursively (frames compose).
1187- **Blob not here yet** (a peer has not fetched it): the placement folds to ONE clearance
1188 -style wire box of `bounds` (no collision, csg4 ignores it), `ModelCache`-style fetch
1189 via `ContentOverlay.request(oid)`, and a refold when it arrives. Never an empty hole
1190 that looks like a delete.
1191- Followed curves: an instance's curves are inside it, so a curve edit there is not
1192 possible (read-only); no new follow rules.
1193
1194Outside the placement, the room can still CUT its pieces: an `op.create` whose targets are
1195instance groups works unchanged (the groups have stable names). Modifiers on a placement
1196(the §2.1a collection stack) are NOT in v1.
1197
1198### 15.3 What a placement is in the editor
1199
1200- One row in the outliner: `▣ Pillar` (the asset's name), under its collection like any
1201 member (new member kind `"a:<dot>"`; CollectionFold unchanged — members are strings).
1202 It opens to list its contents read-only (like a shape's pieces).
1203- Clicking any of its pieces selects the PLACEMENT (as a shape's pieces select the shape).
1204 G / R move and turn it (`asset.xform`); X deletes it (`asset.remove`, one gesture);
1205 Shift+D / Alt+D duplicate it (a new `asset.place`, same oid — every placement is
1206 already linked data).
1207- **Make Real** (the existing key) on a placement: writes the expanded records as ordinary
1208 records with fresh dots in ONE gesture (shapes stay parametric, cuts stay live), links
1209 them into the placement's collections, and `asset.remove`s it. Undo brings it back.
1210- Faces / edges of an instance are not editable (read-only pieces, as for shapes).
1211
1212### 15.4 Libraries: saving, the room's, personal
1213
1214- **Save as asset** (Object menu, and the outliner's context menu on a collection row):
1215 name it (prefilled with the collection's name), write the blob (§15.1), render the
1216 thumbnail (§15.6), then:
1217 - **room:** `ContentOverlay.publish` the blob and the thumbnail;
1218 `AssetSync.publish_asset("asset", name, oid, size, "hasset", {thumb})` (an optional
1219 extra-fields argument added to publish_asset; newest wins per name, as for models);
1220 - **personal:** copy both into `user://assets/<oid>.hasset` / `<oid>.png` and add
1221 `{name, oid, thumb, saved}` to `user://assets/index.json`.
1222 - Default: both. A checkbox in the save prompt.
1223- **Saving under a name that exists** makes a NEW oid (the content changed). Existing
1224 placements keep their pinned oid. The browser marks a tile "N placements on an older
1225 version" with **Update placements** (one gesture of `asset.set` on each). Never silent:
1226 a peer's re-save must not move your level under you.
1227- **Personal asset into a room:** dragging / placing one publishes it to the room first
1228 (blob + thumbnail + catalogue entry), then places it.
1229- Delete from a library: the catalogue entry only (room) / the index line + files
1230 (personal). Placements that pin the oid keep working while any peer still serves it;
1231 a placement whose blob no peer has folds to its wire box (§15.2).
1232
1233### 15.5 The asset browser (bottom area)
1234
1235- A panel `assets` in the bottom area, beside the texture strip: a filter box, a source
1236 switch (Room / Personal / Both), and a flow of tiles (thumbnail + name).
1237- **Drag a tile into the 3D view:** while a drag is in flight the View3D Control takes
1238 drops (its mouse filter flips to PASS on NOTIFICATION_DRAG_BEGIN and back on END, with
1239 drag forwarding). Under the pointer: a raycast from the camera through the pointer;
1240 the ghost is the asset's `bounds` box standing on the hit, its up = the hit normal
1241 snapped to the nearest world axis (walls and ceilings work; a slope uses its axis).
1242 Drop = ONE `asset.place` gesture, then the placement is selected so G / R adjust it
1243 (and Adjust Last offers rotation about up and offset).
1244- **Click a tile** selects it as the "current asset"; **Shift+A → Asset** (the Add menu)
1245 and its key place the current asset at the 3D cursor, as the Add menu does for shapes.
1246- Wheel scrolls; tile size follows the strip's height.
1247
1248### 15.6 Thumbnails
1249
1250- Rendered at save, from the room itself: a SubViewport (256²) sharing the world, its own
1251 camera framing the collection's bounds from the front-left-above, only the
1252 collection's brushes and objects visible (the others hidden for that one frame, the
1253 outliner's hide path). PNG, 128², published beside the blob.
1254- Headless (CI): the dummy renderer gives an empty image; the gate checks the file exists
1255 and has the size, not its pixels.
1256
1257### 15.7 Keys and menus
1258
1259| key | action | where |
1260|---|---|---|
1261| — | Save collection as asset… | Object menu; outliner context menu |
1262| Shift+A → Asset | place the current asset at the 3D cursor | Add menu |
1263| X / G / R / Shift+D / Alt+D | on a placement: as on any object | — |
1264| Make Real | the placement → ordinary records | Object menu (existing key) |
1265| — | Update placements | browser tile menu |
1266
1267Every command gets a menu entry; none needs a new key beyond the Add menu (a key is
1268offered in the questions below).
1269
1270### 15.8 Steps (each gated, one commit each)
1271
12721. **AssetDoc** (pure): write a collection subtree to a blob and read it back.
1273 `gate_asset_doc`: round trip byte-identical; the same contents → the same oid on two
1274 saves; an outside op is dropped and reported; local dots remapped everywhere.
12752. **AssetFold**: `asset.*` records, the expansion, nesting, the missing-blob box.
1276 `gate_asset_fold`: a collection saved and placed at its own pivot folds to the SAME
1277 geometry digest as the original (the oracle is the room's own fold — independent);
1278 two placements, one moved; a nested asset; a placement re-pointed (`asset.set`); undo
1279 of each; a missing blob → the wire box, then the real pieces when it arrives.
12803. **Libraries + Save as asset**: room (ContentOverlay + AssetSync kind `asset`, the
1281 extra field) and personal (user://assets). `gate_asset_library`: save → listed in
1282 both; re-save → new oid, old placements unchanged, Update placements moves them; a
1283 personal asset placed in a room is published first.
12844. **Editor**: outliner rows, selection mapping, G / R / X / Shift+D / Alt+D, Make Real.
1285 `gate_asset_edit`: each, plus Make Real = the same digest, then ordinary editable
1286 records (a shape param edit works on one).
12875. **Browser**: the bottom panel, tiles, click + Shift+A, drag into the view (the drop
1288 path called directly in the gate, as the outliner's drop_on is).
1289 `gate_asset_browser`.
12906. **Thumbnails** (SubViewport render at save).
12917. **Measure**: fold cost of N placements of a 50-shape asset (the _probe_fold_split
1292 method) — the follow-up perf issue is #128.
1293
1294### 15.9 Not in v1
1295
1296- Plugin shapes as DATA graphs (L4's second item) — after this; its own design.
1297- A stack on a placement or a collection (§2.1a path evaluation).
1298- Scaled placements.
1299- Editing an asset in place (Blender's "edit the library file"): v1 is Make Real a copy,
1300 edit it, Save as asset under the same name, Update placements.
1301
1302### 15.10 Questions for the user
1303
13041. A key for **Save collection as asset**? (Blender's Mark as Asset has none.)
13052. Should **Update placements** after a re-save be offered per tile (as designed), or
1306 automatic for placements YOU made and offered for others'?
13073. Drop orientation: **up = the hit normal snapped to an axis** (as designed), or always
1308 world up?