@jgengine/core 0.11.0 → 0.12.0

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.
Files changed (219) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/ai/populationDirector.d.ts +103 -0
  3. package/dist/ai/populationDirector.js +190 -0
  4. package/dist/ai/pursuit.d.ts +56 -0
  5. package/dist/ai/pursuit.js +53 -0
  6. package/dist/authoring.d.ts +1 -1
  7. package/dist/authoring.js +1 -1
  8. package/dist/combat.d.ts +1 -0
  9. package/dist/combat.js +1 -0
  10. package/dist/devtools/controls.d.ts +38 -0
  11. package/dist/devtools/controls.js +181 -0
  12. package/dist/devtools/devtools.d.ts +5 -197
  13. package/dist/devtools/devtools.js +38 -774
  14. package/dist/devtools/discover.d.ts +32 -0
  15. package/dist/devtools/discover.js +240 -0
  16. package/dist/devtools/fallbackSeams.d.ts +50 -0
  17. package/dist/devtools/fallbackSeams.js +111 -0
  18. package/dist/devtools/frame.d.ts +15 -0
  19. package/dist/devtools/frame.js +247 -0
  20. package/dist/devtools/logs.d.ts +15 -0
  21. package/dist/devtools/logs.js +86 -0
  22. package/dist/devtools/overrides.d.ts +16 -0
  23. package/dist/devtools/overrides.js +66 -0
  24. package/dist/devtools/types.d.ts +212 -0
  25. package/dist/devtools/types.js +1 -0
  26. package/dist/economy/auctionBook.d.ts +122 -0
  27. package/dist/economy/auctionBook.js +213 -0
  28. package/dist/economy/priceHistory.d.ts +44 -0
  29. package/dist/economy/priceHistory.js +89 -0
  30. package/dist/economy/unlockPoints.d.ts +92 -0
  31. package/dist/economy/unlockPoints.js +103 -0
  32. package/dist/editor/commandApply.d.ts +5 -0
  33. package/dist/editor/commandApply.js +755 -0
  34. package/dist/editor/commands.d.ts +48 -3
  35. package/dist/editor/commands.js +40 -754
  36. package/dist/editor/directives.d.ts +94 -0
  37. package/dist/editor/directives.js +206 -0
  38. package/dist/editor/document.d.ts +37 -2
  39. package/dist/editor/document.js +422 -6
  40. package/dist/editor/entityCatalog.d.ts +42 -0
  41. package/dist/editor/entityCatalog.js +101 -0
  42. package/dist/editor/environment.d.ts +74 -0
  43. package/dist/editor/environment.js +169 -0
  44. package/dist/editor/index.d.ts +26 -1
  45. package/dist/editor/index.js +8 -1
  46. package/dist/editor/minimap.d.ts +31 -0
  47. package/dist/editor/minimap.js +73 -0
  48. package/dist/editor/streamer.d.ts +66 -0
  49. package/dist/editor/streamer.js +68 -0
  50. package/dist/editor/types.d.ts +143 -0
  51. package/dist/editor/world.d.ts +136 -0
  52. package/dist/editor/world.js +303 -0
  53. package/dist/game/breeding.d.ts +161 -0
  54. package/dist/game/breeding.js +181 -0
  55. package/dist/game/cameraConfig.d.ts +451 -0
  56. package/dist/game/cameraConfig.js +1 -0
  57. package/dist/game/defineGame.d.ts +16 -9
  58. package/dist/game/defineGame.js +14 -6
  59. package/dist/game/defineSystem.d.ts +2 -0
  60. package/dist/game/defineSystem.js +2 -0
  61. package/dist/game/events.d.ts +28 -0
  62. package/dist/game/playableGame.d.ts +34 -435
  63. package/dist/game/playableGame.js +1 -1
  64. package/dist/game/progression.d.ts +35 -0
  65. package/dist/game/progression.js +19 -0
  66. package/dist/game/quest.js +1 -3
  67. package/dist/game/tribe.d.ts +184 -0
  68. package/dist/game/tribe.js +432 -0
  69. package/dist/gameplay.d.ts +3 -1
  70. package/dist/gameplay.js +3 -1
  71. package/dist/inventory/encumbrance.d.ts +69 -0
  72. package/dist/inventory/encumbrance.js +68 -0
  73. package/dist/item/durability.js +1 -3
  74. package/dist/meta/changelog.d.ts +1 -1
  75. package/dist/meta/changelog.js +24 -1
  76. package/dist/movement/movementModel.d.ts +15 -1
  77. package/dist/movement/movementModel.js +33 -15
  78. package/dist/movement/playerMovement.js +102 -2
  79. package/dist/multiplayer/combatSnapshot.js +13 -8
  80. package/dist/multiplayer/presenceModel.d.ts +2 -0
  81. package/dist/multiplayer/presenceModel.js +8 -0
  82. package/dist/physics/drivableVehicle.d.ts +2 -0
  83. package/dist/physics/drivableVehicle.js +1 -1
  84. package/dist/physics/flightDynamics.d.ts +82 -0
  85. package/dist/physics/flightDynamics.js +165 -0
  86. package/dist/physics/gravityField.d.ts +25 -0
  87. package/dist/physics/gravityField.js +41 -0
  88. package/dist/physics/kinematicVehicle.d.ts +87 -0
  89. package/dist/physics/kinematicVehicle.js +193 -11
  90. package/dist/physics/physicsWorld.d.ts +26 -0
  91. package/dist/physics/physicsWorld.js +109 -117
  92. package/dist/physics/vehicleObstacles.d.ts +44 -0
  93. package/dist/physics/vehicleObstacles.js +115 -0
  94. package/dist/random/rng.d.ts +4 -1
  95. package/dist/random/rng.js +4 -1
  96. package/dist/runtime/adapter.d.ts +13 -0
  97. package/dist/runtime/adapter.js +13 -0
  98. package/dist/runtime/context/combat.d.ts +51 -0
  99. package/dist/runtime/context/combat.js +130 -0
  100. package/dist/runtime/context/deathLoot.d.ts +30 -0
  101. package/dist/runtime/context/deathLoot.js +39 -0
  102. package/dist/runtime/context/player.d.ts +48 -0
  103. package/dist/runtime/context/player.js +125 -0
  104. package/dist/runtime/context/scene.d.ts +59 -0
  105. package/dist/runtime/context/scene.js +291 -0
  106. package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
  107. package/dist/runtime/context/snapshotCodecs.js +26 -0
  108. package/dist/runtime/descriptors/baseline.js +26 -3
  109. package/dist/runtime/descriptors/features.d.ts +5 -0
  110. package/dist/runtime/descriptors/features.js +20 -2
  111. package/dist/runtime/gameContext.d.ts +2 -428
  112. package/dist/runtime/gameContext.js +88 -501
  113. package/dist/runtime/gameContextTypes.d.ts +467 -0
  114. package/dist/runtime/gameContextTypes.js +1 -0
  115. package/dist/runtime/headlessRunner.d.ts +4 -2
  116. package/dist/runtime/headlessRunner.js +1 -0
  117. package/dist/runtime/hostPolicy.d.ts +51 -0
  118. package/dist/runtime/hostPolicy.js +68 -0
  119. package/dist/runtime/hostedGameRunner.d.ts +3 -1
  120. package/dist/runtime/hostedGameRunner.js +1 -0
  121. package/dist/runtime/hostedWorldSession.d.ts +3 -1
  122. package/dist/runtime/hostedWorldSession.js +1 -0
  123. package/dist/runtime/worldSnapshot.d.ts +13 -1
  124. package/dist/runtime/worldSnapshot.js +15 -3
  125. package/dist/scene/assetCatalog.d.ts +5 -0
  126. package/dist/scene/assetSpace.js +1 -3
  127. package/dist/scene/authoredTriggers.d.ts +50 -0
  128. package/dist/scene/authoredTriggers.js +114 -0
  129. package/dist/scene/builtinSceneKinds.js +8 -3
  130. package/dist/scene/colliders.d.ts +58 -0
  131. package/dist/scene/colliders.js +108 -0
  132. package/dist/scene/collisionMesh.d.ts +94 -0
  133. package/dist/scene/collisionMesh.js +699 -0
  134. package/dist/scene/companion.d.ts +147 -0
  135. package/dist/scene/companion.js +193 -0
  136. package/dist/scene/entityStats.d.ts +7 -9
  137. package/dist/scene/entityStats.js +10 -14
  138. package/dist/scene/sceneKinds.d.ts +16 -0
  139. package/dist/scene/sceneKinds.js +4 -0
  140. package/dist/scene/sceneRaycast.js +15 -0
  141. package/dist/stats/spawnLevelStats.d.ts +110 -0
  142. package/dist/stats/spawnLevelStats.js +140 -0
  143. package/dist/stats/statPool.d.ts +70 -0
  144. package/dist/stats/statPool.js +59 -0
  145. package/dist/store/changeSignal.d.ts +3 -0
  146. package/dist/store/changeSignal.js +2 -0
  147. package/dist/store/observableKeyedStore.d.ts +2 -0
  148. package/dist/store/observableKeyedStore.js +1 -0
  149. package/dist/survival/decayMeter.js +1 -3
  150. package/dist/survival/regionHealth.js +1 -3
  151. package/dist/ui/hudLayout.d.ts +5 -0
  152. package/dist/ui/hudLayout.js +17 -1
  153. package/dist/world/authoredEntities.d.ts +29 -0
  154. package/dist/world/authoredEntities.js +29 -0
  155. package/dist/world/authoredObjects.d.ts +15 -1
  156. package/dist/world/authoredObjects.js +13 -1
  157. package/dist/world/authoredSpawn.d.ts +39 -0
  158. package/dist/world/authoredSpawn.js +37 -0
  159. package/dist/world/buildSockets.d.ts +152 -0
  160. package/dist/world/buildSockets.js +188 -0
  161. package/dist/world/cityBlocks.d.ts +152 -0
  162. package/dist/world/cityBlocks.js +670 -0
  163. package/dist/world/cityContent.d.ts +77 -0
  164. package/dist/world/cityContent.js +261 -0
  165. package/dist/world/cityGeometry.d.ts +114 -0
  166. package/dist/world/cityGeometry.js +573 -0
  167. package/dist/world/cityKind.d.ts +314 -0
  168. package/dist/world/cityKind.js +1578 -0
  169. package/dist/world/features.d.ts +18 -3
  170. package/dist/world/features.js +10 -1
  171. package/dist/world/grassKind.js +2 -2
  172. package/dist/world/markers.d.ts +50 -4
  173. package/dist/world/markers.js +49 -0
  174. package/dist/world/massing.js +1 -1
  175. package/dist/world/minimapBake.d.ts +78 -0
  176. package/dist/world/minimapBake.js +208 -0
  177. package/dist/world/minimapTrack.d.ts +17 -0
  178. package/dist/world/minimapTrack.js +22 -0
  179. package/dist/world/pathNetwork.d.ts +159 -0
  180. package/dist/world/pathNetwork.js +684 -0
  181. package/dist/world/place.d.ts +166 -0
  182. package/dist/world/place.js +96 -0
  183. package/dist/world/placeAsset.js +3 -1
  184. package/dist/world/poleLineKind.d.ts +40 -0
  185. package/dist/world/poleLineKind.js +97 -0
  186. package/dist/world/polyline.js +9 -2
  187. package/dist/world/resourceNode.d.ts +109 -0
  188. package/dist/world/resourceNode.js +140 -0
  189. package/dist/world/scatter.js +2 -20
  190. package/dist/world/streets.d.ts +29 -0
  191. package/dist/world/streets.js +55 -0
  192. package/dist/world/terraform.js +58 -88
  193. package/dist/world/terrain.d.ts +45 -0
  194. package/dist/world/terrain.js +78 -0
  195. package/dist/world.d.ts +14 -5
  196. package/dist/world.js +12 -3
  197. package/package.json +1 -1
  198. package/skills/game-design/SKILL.md +28 -0
  199. package/skills/game-design/agents/openai.yaml +4 -0
  200. package/skills/game-design/references/application-playbook.md +170 -0
  201. package/skills/game-design/references/field-guide.md +288 -0
  202. package/skills/jgengine/SKILL.md +7 -4
  203. package/skills/jgengine/api.md +172 -79
  204. package/skills/jgengine/capabilities.md +104 -2
  205. package/skills/jgengine/recipes/minimal-game.md +88 -0
  206. package/skills/jgengine-combat/SKILL.md +5 -1
  207. package/skills/jgengine-combat/api.md +36 -0
  208. package/skills/jgengine-combat/capabilities.md +44 -0
  209. package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
  210. package/skills/jgengine-combat/reference.md +12 -2
  211. package/skills/jgengine-gameplay/api.md +152 -26
  212. package/skills/jgengine-gameplay/capabilities.md +78 -2
  213. package/skills/jgengine-gameplay/reference-systems.md +1 -5
  214. package/skills/jgengine-world/SKILL.md +13 -0
  215. package/skills/jgengine-world/api.md +419 -15
  216. package/skills/jgengine-world/capabilities.md +108 -2
  217. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  218. package/skills/jgengine-world/reference.md +43 -4
  219. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -1,690 +1,27 @@
1
1
  import { applyDeltaToSnapshot, applySurfaceDeltaToSnapshot, applyWeightDeltaToSnapshot, revertDeltaFromSnapshot, revertSurfaceDeltaFromSnapshot, revertWeightDeltaFromSnapshot, } from "../world/terraform.js";
2
- import { patchUiPanel, removeUiPanel, } from "../ui/hudDocument.js";
3
- import { cloneEditorDocument, collectDescendants, createPrefabFragment, editorDocumentExtras, ensureUniqueEditorId, extractEditorFragment, findEditorCollection, findEditorMarker, findEditorPrefab, findEditorVolume, importEditorDocumentJson, isEditorObjectLocked, wouldCreateCycle, } from "./document.js";
4
- import { fillGridRect, floodFillGrid, migrateGridLayer, paintGridCells, resizeGridLayer, } from "./grid.js";
2
+ import { cloneEditorDocument, findEditorMarker, findEditorVolume, } from "./document.js";
3
+ import { applyMutating, isStructural } from "./commandApply.js";
5
4
  function snapshotState(state) {
6
5
  return {
7
6
  document: cloneEditorDocument(state.document),
8
7
  selection: [...state.selection],
9
8
  };
10
9
  }
11
- function removeByIds(doc, ids) {
12
- return {
13
- version: 1,
14
- markers: doc.markers.filter((marker) => !ids.has(marker.id)),
15
- volumes: doc.volumes.filter((volume) => !ids.has(volume.id)),
16
- paths: doc.paths.filter((path) => !ids.has(path.id)),
17
- annotations: doc.annotations.filter((note) => !ids.has(note.id)),
18
- ...editorDocumentExtras(doc),
19
- collections: doc.collections.map((collection) => ({
20
- ...collection,
21
- memberIds: collection.memberIds.filter((id) => !ids.has(id)),
22
- })),
23
- };
24
- }
25
- function shifted(point, delta) {
26
- return { x: point.x + delta.x, y: point.y + delta.y, z: point.z + delta.z };
27
- }
28
- function translateByIds(doc, ids, delta) {
29
- return {
30
- version: 1,
31
- markers: doc.markers.map((marker) => ids.has(marker.id) ? { ...marker, position: shifted(marker.position, delta) } : marker),
32
- volumes: doc.volumes.map((volume) => ids.has(volume.id) ? { ...volume, center: shifted(volume.center, delta) } : volume),
33
- paths: doc.paths.map((path) => ids.has(path.id) ? { ...path, points: path.points.map((point) => shifted(point, delta)) } : path),
34
- annotations: doc.annotations.map((note) => ids.has(note.id) ? { ...note, position: shifted(note.position, delta) } : note),
35
- ...editorDocumentExtras(doc),
36
- };
37
- }
38
- function collectIds(doc) {
39
- const all = new Set();
40
- for (const marker of doc.markers)
41
- all.add(marker.id);
42
- for (const volume of doc.volumes)
43
- all.add(volume.id);
44
- for (const path of doc.paths)
45
- all.add(path.id);
46
- for (const note of doc.annotations)
47
- all.add(note.id);
48
- return all;
49
- }
50
- /**
51
- * Adding to a collection replaces a same-id member in that same collection (the existing
52
- * upsert behavior); anything else collides with the document-global id space shared by markers,
53
- * volumes, paths, and annotations, so it gets re-idded instead of shadowing whatever already
54
- * holds that id in a different collection.
55
- */
56
- function ownIdOrGloballyUnique(doc, item, ownCollection) {
57
- if (ownCollection.some((entry) => entry.id === item.id))
58
- return item;
59
- const id = ensureUniqueEditorId(item.id, collectIds(doc));
60
- return id === item.id ? item : { ...item, id };
61
- }
62
- function copyId(base, taken) {
63
- let candidate = `${base}_copy`;
64
- let n = 2;
65
- while (taken.has(candidate)) {
66
- candidate = `${base}_copy${n}`;
67
- n += 1;
68
- }
69
- taken.add(candidate);
70
- return candidate;
71
- }
72
- function insertFragment(state, fragment, offset, renameAll) {
73
- const doc = state.document;
74
- const taken = collectIds(doc);
75
- const clones = cloneEditorDocument(fragment);
76
- const newIds = [];
77
- const withId = (item) => {
78
- const id = renameAll || taken.has(item.id) ? copyId(item.id, taken) : item.id;
79
- taken.add(id);
80
- newIds.push(id);
81
- return { ...item, id };
82
- };
83
- const markers = clones.markers.map((marker) => withId({ ...marker, position: shifted(marker.position, offset) }));
84
- const volumes = clones.volumes.map((volume) => withId({ ...volume, center: shifted(volume.center, offset) }));
85
- const paths = clones.paths.map((path) => withId({ ...path, points: path.points.map((point) => shifted(point, offset)) }));
86
- const annotations = clones.annotations.map((note) => withId({ ...note, position: shifted(note.position, offset) }));
87
- if (newIds.length === 0)
88
- return state;
89
- return {
90
- document: {
91
- version: 1,
92
- markers: [...doc.markers, ...markers],
93
- volumes: [...doc.volumes, ...volumes],
94
- paths: [...doc.paths, ...paths],
95
- annotations: [...doc.annotations, ...annotations],
96
- ...editorDocumentExtras(doc),
97
- },
98
- selection: newIds,
99
- };
100
- }
101
- function fragmentAsDocument(fragment) {
102
- return {
103
- version: 1,
104
- markers: [...fragment.markers],
105
- volumes: [...fragment.volumes],
106
- paths: [...fragment.paths],
107
- annotations: [...fragment.annotations],
108
- prefabs: [],
109
- collections: [],
110
- catalogs: [],
111
- };
112
- }
113
- function tagWithInstance(item, prefabId, instanceId) {
114
- return { ...item, meta: { ...item.meta, prefabId, prefabInstanceId: instanceId } };
115
- }
116
10
  /**
117
- * Replaces one grid layer in place via `transform`, returning `null` when the layer is missing and
118
- * the same state when the transform is a no-op (so history skips it).
11
+ * Per-brush apply/revert pair over the heightfield snapshot. One registry drives every terrain
12
+ * command, push, and undo/redo path, so the three brushes never fork into parallel code.
119
13
  */
120
- function updateGridLayer(state, id, transform) {
121
- const grids = state.document.grids;
122
- if (grids === undefined)
123
- return null;
124
- const index = grids.findIndex((layer) => layer.id === id);
125
- if (index < 0)
126
- return null;
127
- const next = transform(grids[index]);
128
- if (next === grids[index])
129
- return state;
130
- const updated = grids.map((layer, i) => (i === index ? next : layer));
131
- return { ...state, document: { ...state.document, grids: updated } };
132
- }
133
- function applyMutating(state, command) {
134
- switch (command.type) {
135
- case "select":
136
- return { ...state, selection: [...command.ids] };
137
- case "clearSelection":
138
- return { ...state, selection: [] };
139
- case "setTransform": {
140
- if (isEditorObjectLocked(state.document, command.id))
141
- return null;
142
- const marker = findEditorMarker(state.document, command.id);
143
- const volume = findEditorVolume(state.document, command.id);
144
- const note = state.document.annotations.find((n) => n.id === command.id);
145
- const previous = marker?.position ?? volume?.center ?? note?.position;
146
- // Moving a parent to a new position carries its whole subtree by the same delta.
147
- const childDelta = command.position === undefined || previous === undefined
148
- ? null
149
- : {
150
- x: command.position.x - previous.x,
151
- y: command.position.y - previous.y,
152
- z: command.position.z - previous.z,
153
- };
154
- const descendants = childDelta === null ? new Set() : collectDescendants(state.document, [command.id]);
155
- const shift = (point) => childDelta === null ? point : shifted(point, childDelta);
156
- const markers = state.document.markers.map((entry) => {
157
- if (entry.id === command.id) {
158
- return {
159
- ...entry,
160
- position: command.position === undefined ? entry.position : { ...command.position },
161
- ...(command.rotationY === undefined ? {} : { rotationY: command.rotationY }),
162
- };
163
- }
164
- return descendants.has(entry.id) ? { ...entry, position: shift(entry.position) } : entry;
165
- });
166
- const volumes = state.document.volumes.map((entry) => {
167
- if (entry.id === command.id)
168
- return command.position === undefined ? entry : { ...entry, center: { ...command.position } };
169
- return descendants.has(entry.id) ? { ...entry, center: shift(entry.center) } : entry;
170
- });
171
- const paths = state.document.paths.map((entry) => descendants.has(entry.id) ? { ...entry, points: entry.points.map(shift) } : entry);
172
- const annotations = state.document.annotations.map((entry) => {
173
- if (entry.id === command.id)
174
- return command.position === undefined ? entry : { ...entry, position: { ...command.position } };
175
- return descendants.has(entry.id) ? { ...entry, position: shift(entry.position) } : entry;
176
- });
177
- return {
178
- ...state,
179
- document: { ...state.document, markers, volumes, paths, annotations },
180
- };
181
- }
182
- case "translate": {
183
- const movable = command.ids.filter((id) => !isEditorObjectLocked(state.document, id));
184
- if (movable.length === 0)
185
- return null;
186
- const ids = new Set(movable);
187
- for (const descendant of collectDescendants(state.document, movable))
188
- ids.add(descendant);
189
- return {
190
- ...state,
191
- document: translateByIds(state.document, ids, command.delta),
192
- };
193
- }
194
- case "setParent": {
195
- const targets = command.ids.filter((id) => !wouldCreateCycle(state.document, id, command.parentId));
196
- if (targets.length === 0)
197
- return state;
198
- const wanted = new Set(targets);
199
- const reparent = (entry) => {
200
- if (!wanted.has(entry.id))
201
- return entry;
202
- if (command.parentId === null) {
203
- if (entry.parentId === undefined)
204
- return entry;
205
- const next = { ...entry };
206
- delete next.parentId;
207
- return next;
208
- }
209
- return { ...entry, parentId: command.parentId };
210
- };
211
- return {
212
- ...state,
213
- document: {
214
- ...state.document,
215
- markers: state.document.markers.map(reparent),
216
- volumes: state.document.volumes.map(reparent),
217
- paths: state.document.paths.map(reparent),
218
- annotations: state.document.annotations.map(reparent),
219
- },
220
- };
221
- }
222
- case "addMarker": {
223
- const marker = ownIdOrGloballyUnique(state.document, command.marker, state.document.markers);
224
- return {
225
- ...state,
226
- document: {
227
- ...state.document,
228
- markers: [...state.document.markers.filter((m) => m.id !== marker.id), marker],
229
- },
230
- selection: [marker.id],
231
- };
232
- }
233
- case "addVolume": {
234
- const volume = ownIdOrGloballyUnique(state.document, command.volume, state.document.volumes);
235
- return {
236
- ...state,
237
- document: {
238
- ...state.document,
239
- volumes: [...state.document.volumes.filter((v) => v.id !== volume.id), volume],
240
- },
241
- selection: [volume.id],
242
- };
243
- }
244
- case "addPath": {
245
- const path = ownIdOrGloballyUnique(state.document, command.path, state.document.paths);
246
- return {
247
- ...state,
248
- document: {
249
- ...state.document,
250
- paths: [...state.document.paths.filter((p) => p.id !== path.id), path],
251
- },
252
- selection: [path.id],
253
- };
254
- }
255
- case "addNote": {
256
- const note = ownIdOrGloballyUnique(state.document, command.note, state.document.annotations);
257
- return {
258
- ...state,
259
- document: {
260
- ...state.document,
261
- annotations: [...state.document.annotations.filter((n) => n.id !== note.id), note],
262
- },
263
- selection: [note.id],
264
- };
265
- }
266
- case "setMarker": {
267
- const markers = state.document.markers.map((marker) => {
268
- if (marker.id !== command.id)
269
- return marker;
270
- return {
271
- ...marker,
272
- ...command.patch,
273
- position: command.patch.position === undefined ? marker.position : { ...command.patch.position },
274
- meta: command.patch.meta === undefined ? marker.meta : { ...command.patch.meta },
275
- };
276
- });
277
- return { ...state, document: { ...state.document, markers } };
278
- }
279
- case "setVolume": {
280
- const volumes = state.document.volumes.map((volume) => {
281
- if (volume.id !== command.id)
282
- return volume;
283
- return {
284
- ...volume,
285
- ...command.patch,
286
- center: command.patch.center === undefined ? volume.center : { ...command.patch.center },
287
- halfExtents: command.patch.halfExtents === undefined
288
- ? volume.halfExtents
289
- : { ...command.patch.halfExtents },
290
- meta: command.patch.meta === undefined ? volume.meta : { ...command.patch.meta },
291
- };
292
- });
293
- return { ...state, document: { ...state.document, volumes } };
294
- }
295
- case "setPath": {
296
- const paths = state.document.paths.map((path) => {
297
- if (path.id !== command.id)
298
- return path;
299
- return {
300
- ...path,
301
- ...command.patch,
302
- points: command.patch.points === undefined
303
- ? path.points
304
- : command.patch.points.map((point) => ({ ...point })),
305
- meta: command.patch.meta === undefined ? path.meta : { ...command.patch.meta },
306
- };
307
- });
308
- return { ...state, document: { ...state.document, paths } };
309
- }
310
- case "setNote": {
311
- const annotations = state.document.annotations.map((note) => {
312
- if (note.id !== command.id)
313
- return note;
314
- return {
315
- ...note,
316
- ...command.patch,
317
- position: command.patch.position === undefined ? note.position : { ...command.patch.position },
318
- meta: command.patch.meta === undefined ? note.meta : { ...command.patch.meta },
319
- };
320
- });
321
- return { ...state, document: { ...state.document, annotations } };
322
- }
323
- case "setCatalogEntry": {
324
- const catalog = state.document.catalogs.find((entry) => entry.id === command.catalogId);
325
- if (catalog === undefined)
326
- return null;
327
- if (!catalog.entries.some((entry) => entry.id === command.entryId))
328
- return null;
329
- const catalogs = state.document.catalogs.map((row) => {
330
- if (row.id !== command.catalogId)
331
- return row;
332
- return {
333
- ...row,
334
- entries: row.entries.map((entry) => {
335
- if (entry.id !== command.entryId)
336
- return entry;
337
- const next = { id: entry.id };
338
- const label = command.patch.label ?? entry.label;
339
- if (label !== undefined)
340
- next.label = label;
341
- if (command.patch.meta !== undefined)
342
- next.meta = { ...entry.meta, ...command.patch.meta };
343
- else if (entry.meta !== undefined)
344
- next.meta = { ...entry.meta };
345
- return next;
346
- }),
347
- };
348
- });
349
- return { ...state, document: { ...state.document, catalogs } };
350
- }
351
- case "remove":
352
- if (isEditorObjectLocked(state.document, command.id))
353
- return null;
354
- return {
355
- document: removeByIds(state.document, new Set([command.id])),
356
- selection: state.selection.filter((id) => id !== command.id),
357
- };
358
- case "removeMany": {
359
- const gone = new Set(command.ids.filter((id) => !isEditorObjectLocked(state.document, id)));
360
- if (gone.size === 0)
361
- return null;
362
- return {
363
- document: removeByIds(state.document, gone),
364
- selection: state.selection.filter((id) => !gone.has(id)),
365
- };
366
- }
367
- case "duplicate":
368
- return insertFragment(state, extractEditorFragment(state.document, command.ids), command.offset ?? { x: 2, y: 0, z: 2 }, true);
369
- case "addFragment":
370
- return insertFragment(state, command.fragment, command.offset ?? { x: 0, y: 0, z: 0 }, false);
371
- case "importDocument":
372
- case "replaceDocument":
373
- return {
374
- document: cloneEditorDocument(command.document),
375
- selection: [],
376
- };
377
- case "importJson":
378
- return {
379
- document: importEditorDocumentJson(command.json),
380
- selection: [],
381
- };
382
- case "setTerrain":
383
- return { ...state, document: { ...state.document, terrain: command.terrain } };
384
- case "clearTerrain": {
385
- const nextDoc = {
386
- version: 1,
387
- markers: state.document.markers,
388
- volumes: state.document.volumes,
389
- paths: state.document.paths,
390
- annotations: state.document.annotations,
391
- prefabs: state.document.prefabs,
392
- collections: state.document.collections,
393
- catalogs: state.document.catalogs,
394
- ...(state.document.grids === undefined ? {} : { grids: state.document.grids }),
395
- ...(state.document.ui === undefined ? {} : { ui: state.document.ui }),
396
- };
397
- return { ...state, document: nextDoc };
398
- }
399
- case "setUiPanel": {
400
- const ui = patchUiPanel(state.document.ui, command.id, command.patch);
401
- return { ...state, document: { ...state.document, ui } };
402
- }
403
- case "removeUiPanel": {
404
- const ui = removeUiPanel(state.document.ui, command.id);
405
- if (ui === undefined) {
406
- const { ui: _removed, ...rest } = state.document;
407
- void _removed;
408
- return { ...state, document: rest };
409
- }
410
- return { ...state, document: { ...state.document, ui } };
411
- }
412
- case "setUi": {
413
- if (command.ui === undefined) {
414
- const { ui: _removed, ...rest } = state.document;
415
- void _removed;
416
- return { ...state, document: rest };
417
- }
418
- return { ...state, document: { ...state.document, ui: command.ui } };
419
- }
420
- case "sculptTerrain": {
421
- if (state.document.terrain === undefined)
422
- return state;
423
- return {
424
- ...state,
425
- document: { ...state.document, terrain: applyDeltaToSnapshot(state.document.terrain, command.delta) },
426
- };
427
- }
428
- case "paintTerrain": {
429
- if (state.document.terrain === undefined)
430
- return state;
431
- return {
432
- ...state,
433
- document: { ...state.document, terrain: applySurfaceDeltaToSnapshot(state.document.terrain, command.delta) },
434
- };
435
- }
436
- case "blendTerrain": {
437
- if (state.document.terrain === undefined)
438
- return state;
439
- return {
440
- ...state,
441
- document: { ...state.document, terrain: applyWeightDeltaToSnapshot(state.document.terrain, command.delta) },
442
- };
443
- }
444
- case "setTerrainLayers": {
445
- if (state.document.terrain === undefined)
446
- return state;
447
- const terrain = state.document.terrain;
448
- const layers = command.layers.map((layer) => ({ ...layer }));
449
- const before = terrain.layers ?? [];
450
- // Keep hand-painted weights only when the layer id sequence is unchanged (a params-only edit).
451
- const sameSequence = before.length === layers.length && before.every((layer, i) => layer.id === layers[i].id);
452
- const next = sameSequence
453
- ? { ...terrain, layers }
454
- : (() => {
455
- const copy = { ...terrain, layers };
456
- delete copy.weights;
457
- return copy;
458
- })();
459
- return { ...state, document: { ...state.document, terrain: next } };
460
- }
461
- case "convertScatterToObjects": {
462
- const path = state.document.paths.find((entry) => entry.id === command.pathId);
463
- if (path === undefined)
464
- return state;
465
- const taken = collectIds(state.document);
466
- const markers = command.markers.map((marker) => {
467
- let id = marker.id;
468
- while (taken.has(id))
469
- id = copyId(marker.id, taken);
470
- taken.add(id);
471
- return { ...marker, id };
472
- });
473
- return {
474
- document: {
475
- ...state.document,
476
- markers: [...state.document.markers, ...markers],
477
- paths: state.document.paths.filter((entry) => entry.id !== command.pathId),
478
- },
479
- selection: markers.map((marker) => marker.id),
480
- };
481
- }
482
- case "createPrefab": {
483
- if (command.ids.length === 0)
484
- return state;
485
- const fragment = createPrefabFragment(state.document, command.ids);
486
- const prefab = { id: command.id, name: command.name, fragment };
487
- return {
488
- ...state,
489
- document: {
490
- ...state.document,
491
- prefabs: [...state.document.prefabs.filter((entry) => entry.id !== command.id), prefab],
492
- },
493
- };
494
- }
495
- case "deletePrefab":
496
- return {
497
- ...state,
498
- document: {
499
- ...state.document,
500
- prefabs: state.document.prefabs.filter((entry) => entry.id !== command.prefabId),
501
- },
502
- };
503
- case "insertPrefab": {
504
- const prefab = findEditorPrefab(state.document, command.prefabId);
505
- if (prefab === undefined)
506
- return state;
507
- const instanceId = command.instanceId ??
508
- `${prefab.id}_inst_${Date.now().toString(36)}_${Math.floor(Math.random() * 1_296_000).toString(36)}`;
509
- const tagged = fragmentAsDocument({
510
- markers: prefab.fragment.markers.map((marker) => tagWithInstance(marker, prefab.id, instanceId)),
511
- volumes: prefab.fragment.volumes.map((volume) => tagWithInstance(volume, prefab.id, instanceId)),
512
- paths: prefab.fragment.paths.map((path) => tagWithInstance(path, prefab.id, instanceId)),
513
- annotations: prefab.fragment.annotations.map((note) => tagWithInstance(note, prefab.id, instanceId)),
514
- });
515
- return insertFragment(state, tagged, command.at, true);
516
- }
517
- case "detachPrefabInstance": {
518
- const strip = (item) => {
519
- if (item.meta?.prefabInstanceId !== command.instanceId)
520
- return item;
521
- const meta = { ...item.meta };
522
- delete meta.prefabId;
523
- delete meta.prefabInstanceId;
524
- return { ...item, meta };
525
- };
526
- return {
527
- ...state,
528
- document: {
529
- ...state.document,
530
- markers: state.document.markers.map(strip),
531
- volumes: state.document.volumes.map(strip),
532
- paths: state.document.paths.map(strip),
533
- annotations: state.document.annotations.map(strip),
534
- },
535
- };
536
- }
537
- case "createCollection": {
538
- const collection = {
539
- id: command.id,
540
- name: command.name,
541
- memberIds: [...(command.memberIds ?? [])],
542
- };
543
- return {
544
- ...state,
545
- document: {
546
- ...state.document,
547
- collections: [...state.document.collections.filter((entry) => entry.id !== command.id), collection],
548
- },
549
- };
550
- }
551
- case "renameCollection": {
552
- const collections = state.document.collections.map((collection) => collection.id === command.id ? { ...collection, name: command.name } : collection);
553
- return { ...state, document: { ...state.document, collections } };
554
- }
555
- case "deleteCollection":
556
- return {
557
- ...state,
558
- document: {
559
- ...state.document,
560
- collections: state.document.collections.filter((entry) => entry.id !== command.id),
561
- },
562
- };
563
- case "setCollectionMembers": {
564
- const collections = state.document.collections.map((collection) => collection.id === command.id ? { ...collection, memberIds: [...command.memberIds] } : collection);
565
- return { ...state, document: { ...state.document, collections } };
566
- }
567
- case "addToCollection": {
568
- const adding = new Set(command.ids);
569
- const collections = state.document.collections.map((collection) => {
570
- if (collection.id !== command.id)
571
- return collection;
572
- const merged = new Set(collection.memberIds);
573
- for (const id of adding)
574
- merged.add(id);
575
- return { ...collection, memberIds: [...merged] };
576
- });
577
- return { ...state, document: { ...state.document, collections } };
578
- }
579
- case "removeFromCollection": {
580
- const removing = new Set(command.ids);
581
- const collections = state.document.collections.map((collection) => collection.id === command.id
582
- ? { ...collection, memberIds: collection.memberIds.filter((id) => !removing.has(id)) }
583
- : collection);
584
- return { ...state, document: { ...state.document, collections } };
585
- }
586
- case "setCollectionFlags": {
587
- const collections = state.document.collections.map((collection) => collection.id === command.id ? { ...collection, ...command.patch } : collection);
588
- return { ...state, document: { ...state.document, collections } };
589
- }
590
- case "selectCollection": {
591
- const collection = findEditorCollection(state.document, command.id);
592
- if (collection === undefined)
593
- return null;
594
- return { ...state, selection: [...collection.memberIds] };
595
- }
596
- case "batchSetProperties": {
597
- const ids = new Set(command.ids);
598
- const patch = command.patch;
599
- const markers = state.document.markers.map((marker) => ids.has(marker.id)
600
- ? {
601
- ...marker,
602
- ...(patch.color === undefined ? {} : { color: patch.color }),
603
- ...(patch.label === undefined ? {} : { label: patch.label }),
604
- ...(patch.meta === undefined ? {} : { meta: { ...marker.meta, ...patch.meta } }),
605
- }
606
- : marker);
607
- const volumes = state.document.volumes.map((volume) => ids.has(volume.id)
608
- ? {
609
- ...volume,
610
- ...(patch.color === undefined ? {} : { color: patch.color }),
611
- ...(patch.label === undefined ? {} : { label: patch.label }),
612
- ...(patch.meta === undefined ? {} : { meta: { ...volume.meta, ...patch.meta } }),
613
- }
614
- : volume);
615
- const paths = state.document.paths.map((path) => ids.has(path.id)
616
- ? {
617
- ...path,
618
- ...(patch.color === undefined ? {} : { color: patch.color }),
619
- ...(patch.label === undefined ? {} : { label: patch.label }),
620
- ...(patch.meta === undefined ? {} : { meta: { ...path.meta, ...patch.meta } }),
621
- }
622
- : path);
623
- const annotations = state.document.annotations.map((note) => ids.has(note.id)
624
- ? {
625
- ...note,
626
- ...(patch.color === undefined ? {} : { color: patch.color }),
627
- ...(patch.meta === undefined ? {} : { meta: { ...note.meta, ...patch.meta } }),
628
- }
629
- : note);
630
- return { ...state, document: { ...state.document, markers, volumes, paths, annotations } };
631
- }
632
- case "addGridLayer": {
633
- const layer = migrateGridLayer(command.layer);
634
- const grids = state.document.grids ?? [];
635
- return {
636
- ...state,
637
- document: {
638
- ...state.document,
639
- grids: [...grids.filter((entry) => entry.id !== layer.id), layer],
640
- },
641
- };
642
- }
643
- case "removeGridLayer": {
644
- const grids = state.document.grids;
645
- if (grids === undefined)
646
- return null;
647
- const next = grids.filter((layer) => layer.id !== command.id);
648
- if (next.length === grids.length)
649
- return null;
650
- return { ...state, document: { ...state.document, grids: next } };
651
- }
652
- case "setGridLayer":
653
- return updateGridLayer(state, command.id, (layer) => migrateGridLayer({ ...layer, ...command.patch }));
654
- case "paintGridCells":
655
- return updateGridLayer(state, command.id, (layer) => paintGridCells(layer, command.cells));
656
- case "fillGridRect":
657
- return updateGridLayer(state, command.id, (layer) => fillGridRect(layer, command.col0, command.row0, command.col1, command.row1, command.value));
658
- case "floodFillGrid":
659
- return updateGridLayer(state, command.id, (layer) => floodFillGrid(layer, command.col, command.row, command.value));
660
- case "resizeGridLayer":
661
- return updateGridLayer(state, command.id, (layer) => resizeGridLayer(layer, command.cols, command.rows));
662
- case "assignMaterial": {
663
- const ids = new Set(command.ids);
664
- const stamp = (meta) => ({ ...meta, materialId: command.materialId });
665
- return {
666
- ...state,
667
- document: {
668
- ...state.document,
669
- markers: state.document.markers.map((marker) => (ids.has(marker.id) ? { ...marker, meta: stamp(marker.meta) } : marker)),
670
- volumes: state.document.volumes.map((volume) => (ids.has(volume.id) ? { ...volume, meta: stamp(volume.meta) } : volume)),
671
- paths: state.document.paths.map((path) => (ids.has(path.id) ? { ...path, meta: stamp(path.meta) } : path)),
672
- annotations: state.document.annotations.map((note) => (ids.has(note.id) ? { ...note, meta: stamp(note.meta) } : note)),
673
- },
674
- };
675
- }
676
- case "undo":
677
- case "redo":
678
- return null;
679
- }
680
- }
681
- function isStructural(command) {
682
- return (command.type !== "select" &&
683
- command.type !== "clearSelection" &&
684
- command.type !== "selectCollection" &&
685
- command.type !== "undo" &&
686
- command.type !== "redo");
687
- }
14
+ const terrainStrokes = {
15
+ sculpt: { apply: applyDeltaToSnapshot, revert: revertDeltaFromSnapshot },
16
+ paint: { apply: applySurfaceDeltaToSnapshot, revert: revertSurfaceDeltaFromSnapshot },
17
+ blend: { apply: applyWeightDeltaToSnapshot, revert: revertWeightDeltaFromSnapshot },
18
+ };
19
+ /** Maps a terrain-brush command type to its stroke kind. */
20
+ const terrainStrokeKind = {
21
+ sculptTerrain: "sculpt",
22
+ paintTerrain: "paint",
23
+ blendTerrain: "blend",
24
+ };
688
25
  /** Creates an editor session with undo/redo history seeded from an initial document.
689
26
  * @internal
690
27
  */
@@ -701,30 +38,20 @@ export function createEditorSession(initial, historyLimit = 100) {
701
38
  for (const listener of listeners)
702
39
  listener(state);
703
40
  };
704
- const sculptWith = (delta, direction, selection) => {
41
+ const applyStroke = (entry, direction, selection) => {
705
42
  const terrain = state.document.terrain;
706
43
  if (terrain === undefined)
707
44
  return state;
708
- const next = direction === "apply" ? applyDeltaToSnapshot(terrain, delta) : revertDeltaFromSnapshot(terrain, delta);
45
+ const next = terrainStrokes[entry.kind][direction](terrain, entry.delta);
709
46
  return { document: { ...state.document, terrain: next }, selection };
710
47
  };
711
- const paintWith = (delta, direction, selection) => {
712
- const terrain = state.document.terrain;
713
- if (terrain === undefined)
714
- return state;
715
- const next = direction === "apply"
716
- ? applySurfaceDeltaToSnapshot(terrain, delta)
717
- : revertSurfaceDeltaFromSnapshot(terrain, delta);
718
- return { document: { ...state.document, terrain: next }, selection };
719
- };
720
- const blendWith = (delta, direction, selection) => {
721
- const terrain = state.document.terrain;
722
- if (terrain === undefined)
723
- return state;
724
- const next = direction === "apply"
725
- ? applyWeightDeltaToSnapshot(terrain, delta)
726
- : revertWeightDeltaFromSnapshot(terrain, delta);
727
- return { document: { ...state.document, terrain: next }, selection };
48
+ const pushTerrainStroke = (entry) => {
49
+ past.push(entry);
50
+ if (past.length > historyLimit)
51
+ past.shift();
52
+ future.length = 0;
53
+ lastCoalesce = null;
54
+ state = applyStroke(entry, "apply", entry.selection);
728
55
  };
729
56
  return {
730
57
  getState: () => state,
@@ -741,22 +68,14 @@ export function createEditorSession(initial, historyLimit = 100) {
741
68
  const entry = past.pop();
742
69
  if (entry === undefined)
743
70
  return state;
744
- if (entry.kind === "sculpt") {
745
- future.push({ kind: "sculpt", delta: entry.delta, selection: state.selection });
746
- state = sculptWith(entry.delta, "revert", entry.selection);
747
- }
748
- else if (entry.kind === "paint") {
749
- future.push({ kind: "paint", delta: entry.delta, selection: state.selection });
750
- state = paintWith(entry.delta, "revert", entry.selection);
751
- }
752
- else if (entry.kind === "blend") {
753
- future.push({ kind: "blend", delta: entry.delta, selection: state.selection });
754
- state = blendWith(entry.delta, "revert", entry.selection);
755
- }
756
- else {
71
+ if (entry.kind === "snapshot") {
757
72
  future.push({ kind: "snapshot", state: snapshotState(state) });
758
73
  state = entry.state;
759
74
  }
75
+ else {
76
+ future.push({ ...entry, selection: [...state.selection] });
77
+ state = applyStroke(entry, "revert", entry.selection);
78
+ }
760
79
  lastCoalesce = null;
761
80
  emit();
762
81
  return state;
@@ -765,59 +84,26 @@ export function createEditorSession(initial, historyLimit = 100) {
765
84
  const entry = future.pop();
766
85
  if (entry === undefined)
767
86
  return state;
768
- if (entry.kind === "sculpt") {
769
- past.push({ kind: "sculpt", delta: entry.delta, selection: state.selection });
770
- state = sculptWith(entry.delta, "apply", entry.selection);
771
- }
772
- else if (entry.kind === "paint") {
773
- past.push({ kind: "paint", delta: entry.delta, selection: state.selection });
774
- state = paintWith(entry.delta, "apply", entry.selection);
775
- }
776
- else if (entry.kind === "blend") {
777
- past.push({ kind: "blend", delta: entry.delta, selection: state.selection });
778
- state = blendWith(entry.delta, "apply", entry.selection);
779
- }
780
- else {
87
+ if (entry.kind === "snapshot") {
781
88
  past.push({ kind: "snapshot", state: snapshotState(state) });
782
89
  state = entry.state;
783
90
  }
91
+ else {
92
+ past.push({ ...entry, selection: [...state.selection] });
93
+ state = applyStroke(entry, "apply", entry.selection);
94
+ }
784
95
  lastCoalesce = null;
785
96
  emit();
786
97
  return state;
787
98
  }
788
- if (command.type === "sculptTerrain") {
789
- if (state.document.terrain === undefined || command.delta.indices.length === 0)
790
- return state;
791
- past.push({ kind: "sculpt", delta: command.delta, selection: [...state.selection] });
792
- if (past.length > historyLimit)
793
- past.shift();
794
- future.length = 0;
795
- lastCoalesce = null;
796
- state = sculptWith(command.delta, "apply", state.selection);
797
- emit();
798
- return state;
799
- }
800
- if (command.type === "paintTerrain") {
801
- if (state.document.terrain === undefined || command.delta.indices.length === 0)
802
- return state;
803
- past.push({ kind: "paint", delta: command.delta, selection: [...state.selection] });
804
- if (past.length > historyLimit)
805
- past.shift();
806
- future.length = 0;
807
- lastCoalesce = null;
808
- state = paintWith(command.delta, "apply", state.selection);
809
- emit();
810
- return state;
811
- }
812
- if (command.type === "blendTerrain") {
99
+ if (command.type === "sculptTerrain" || command.type === "paintTerrain" || command.type === "blendTerrain") {
813
100
  if (state.document.terrain === undefined || command.delta.indices.length === 0)
814
101
  return state;
815
- past.push({ kind: "blend", delta: command.delta, selection: [...state.selection] });
816
- if (past.length > historyLimit)
817
- past.shift();
818
- future.length = 0;
819
- lastCoalesce = null;
820
- state = blendWith(command.delta, "apply", state.selection);
102
+ pushTerrainStroke({
103
+ kind: terrainStrokeKind[command.type],
104
+ delta: command.delta,
105
+ selection: [...state.selection],
106
+ });
821
107
  emit();
822
108
  return state;
823
109
  }