| … | … | @@ -1097,3 +1097,212 @@ flatten remaps both; deleting a cable keeps its path curve while another cable p |
| 1097 | 1097 | it. Seen while gating: `set_curve_point` writing only `p` leaves a point's stored handles |
| 1098 | 1098 | where they were, so a `vector` corner moved that way bends its spans (existing curve |
| 1099 | 1099 | behaviour; 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, |
| 1104 | drag/drop)". Plugin shapes as data graphs (L4's second item) get their own design after |
| 1105 | this 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 | |
| 1118 | What 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 | |
| 1136 | An 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 |
| 1171 | placement 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 |
| 1173 | shapes, 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 | |
| 1194 | Outside the placement, the room can still CUT its pieces: an `op.create` whose targets are |
| 1195 | instance 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 | |
| 1267 | Every command gets a menu entry; none needs a new key beyond the Add menu (a key is |
| 1268 | offered in the questions below). |
| 1269 | |
| 1270 | ### 15.8 Steps (each gated, one commit each) |
| 1271 | |
| 1272 | 1. **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. |
| 1275 | 2. **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. |
| 1280 | 3. **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. |
| 1284 | 4. **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). |
| 1287 | 5. **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`. |
| 1290 | 6. **Thumbnails** (SubViewport render at save). |
| 1291 | 7. **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 | |
| 1304 | 1. A key for **Save collection as asset**? (Blender's Mark as Asset has none.) |
| 1305 | 2. Should **Update placements** after a re-save be offered per tile (as designed), or |
| 1306 | automatic for placements YOU made and offered for others'? |
| 1307 | 3. Drop orientation: **up = the hit normal snapped to an axis** (as designed), or always |
| 1308 | world up? |