@jgengine/core 0.11.0 → 0.13.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.
- package/CHANGELOG.md +52 -0
- package/dist/ai/populationDirector.d.ts +103 -0
- package/dist/ai/populationDirector.js +190 -0
- package/dist/ai/pursuit.d.ts +56 -0
- package/dist/ai/pursuit.js +53 -0
- package/dist/authoring.d.ts +1 -1
- package/dist/authoring.js +1 -1
- package/dist/combat/effects.d.ts +11 -4
- package/dist/combat/effects.js +24 -17
- package/dist/combat/magazine.d.ts +13 -0
- package/dist/combat/magazine.js +27 -0
- package/dist/combat/vfxPresets.d.ts +176 -0
- package/dist/combat/vfxPresets.js +74 -0
- package/dist/combat/weaponFire.d.ts +114 -0
- package/dist/combat/weaponFire.js +96 -0
- package/dist/combat.d.ts +5 -2
- package/dist/combat.js +4 -1
- package/dist/devtools/controls.d.ts +38 -0
- package/dist/devtools/controls.js +181 -0
- package/dist/devtools/devtools.d.ts +5 -197
- package/dist/devtools/devtools.js +38 -774
- package/dist/devtools/discover.d.ts +32 -0
- package/dist/devtools/discover.js +240 -0
- package/dist/devtools/fallbackSeams.d.ts +50 -0
- package/dist/devtools/fallbackSeams.js +111 -0
- package/dist/devtools/frame.d.ts +15 -0
- package/dist/devtools/frame.js +247 -0
- package/dist/devtools/logs.d.ts +15 -0
- package/dist/devtools/logs.js +86 -0
- package/dist/devtools/overrides.d.ts +16 -0
- package/dist/devtools/overrides.js +66 -0
- package/dist/devtools/types.d.ts +212 -0
- package/dist/devtools/types.js +1 -0
- package/dist/devtools/urlFlags.d.ts +22 -0
- package/dist/devtools/urlFlags.js +62 -0
- package/dist/economy/auctionBook.d.ts +122 -0
- package/dist/economy/auctionBook.js +213 -0
- package/dist/economy/priceHistory.d.ts +44 -0
- package/dist/economy/priceHistory.js +89 -0
- package/dist/economy/unlockPoints.d.ts +92 -0
- package/dist/economy/unlockPoints.js +103 -0
- package/dist/editor/commandApply.d.ts +5 -0
- package/dist/editor/commandApply.js +755 -0
- package/dist/editor/commands.d.ts +48 -3
- package/dist/editor/commands.js +40 -754
- package/dist/editor/directives.d.ts +94 -0
- package/dist/editor/directives.js +206 -0
- package/dist/editor/document.d.ts +37 -2
- package/dist/editor/document.js +422 -6
- package/dist/editor/entityCatalog.d.ts +42 -0
- package/dist/editor/entityCatalog.js +101 -0
- package/dist/editor/environment.d.ts +74 -0
- package/dist/editor/environment.js +169 -0
- package/dist/editor/index.d.ts +26 -1
- package/dist/editor/index.js +8 -1
- package/dist/editor/minimap.d.ts +31 -0
- package/dist/editor/minimap.js +73 -0
- package/dist/editor/streamer.d.ts +66 -0
- package/dist/editor/streamer.js +68 -0
- package/dist/editor/types.d.ts +143 -0
- package/dist/editor/world.d.ts +136 -0
- package/dist/editor/world.js +303 -0
- package/dist/game/breeding.d.ts +161 -0
- package/dist/game/breeding.js +181 -0
- package/dist/game/cameraConfig.d.ts +451 -0
- package/dist/game/cameraConfig.js +1 -0
- package/dist/game/clipRoles.d.ts +51 -0
- package/dist/game/clipRoles.js +142 -0
- package/dist/game/defineGame.d.ts +16 -9
- package/dist/game/defineGame.js +14 -6
- package/dist/game/defineSystem.d.ts +2 -0
- package/dist/game/defineSystem.js +2 -0
- package/dist/game/events.d.ts +28 -0
- package/dist/game/partAnimation.d.ts +78 -0
- package/dist/game/partAnimation.js +147 -0
- package/dist/game/playableGame.d.ts +72 -440
- package/dist/game/playableGame.js +1 -1
- package/dist/game/progression.d.ts +35 -0
- package/dist/game/progression.js +22 -1
- package/dist/game/quest.js +1 -3
- package/dist/game/tribe.d.ts +184 -0
- package/dist/game/tribe.js +432 -0
- package/dist/gameplay.d.ts +3 -1
- package/dist/gameplay.js +3 -1
- package/dist/inventory/encumbrance.d.ts +69 -0
- package/dist/inventory/encumbrance.js +68 -0
- package/dist/inventory/inventoryModel.d.ts +24 -8
- package/dist/inventory/inventoryModel.js +47 -8
- package/dist/item/durability.js +1 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +48 -1
- package/dist/movement/movementModel.d.ts +15 -1
- package/dist/movement/movementModel.js +33 -15
- package/dist/movement/playerMovement.js +102 -2
- package/dist/multiplayer/combatSnapshot.js +13 -8
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +8 -0
- package/dist/physics/drivableVehicle.d.ts +2 -0
- package/dist/physics/drivableVehicle.js +1 -1
- package/dist/physics/flightDynamics.d.ts +82 -0
- package/dist/physics/flightDynamics.js +165 -0
- package/dist/physics/gravityField.d.ts +25 -0
- package/dist/physics/gravityField.js +41 -0
- package/dist/physics/kinematicVehicle.d.ts +87 -0
- package/dist/physics/kinematicVehicle.js +193 -11
- package/dist/physics/physicsWorld.d.ts +26 -0
- package/dist/physics/physicsWorld.js +109 -117
- package/dist/physics/vehicleObstacles.d.ts +44 -0
- package/dist/physics/vehicleObstacles.js +115 -0
- package/dist/random/rng.d.ts +4 -1
- package/dist/random/rng.js +4 -1
- package/dist/runtime/adapter.d.ts +13 -0
- package/dist/runtime/adapter.js +13 -0
- package/dist/runtime/context/combat.d.ts +51 -0
- package/dist/runtime/context/combat.js +131 -0
- package/dist/runtime/context/combatFx.js +15 -5
- package/dist/runtime/context/deathLoot.d.ts +30 -0
- package/dist/runtime/context/deathLoot.js +39 -0
- package/dist/runtime/context/player.d.ts +48 -0
- package/dist/runtime/context/player.js +125 -0
- package/dist/runtime/context/scene.d.ts +60 -0
- package/dist/runtime/context/scene.js +329 -0
- package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
- package/dist/runtime/context/snapshotCodecs.js +26 -0
- package/dist/runtime/descriptors/baseline.js +26 -3
- package/dist/runtime/descriptors/features.d.ts +5 -0
- package/dist/runtime/descriptors/features.js +20 -2
- package/dist/runtime/gameContext.d.ts +2 -428
- package/dist/runtime/gameContext.js +133 -500
- package/dist/runtime/gameContextTypes.d.ts +500 -0
- package/dist/runtime/gameContextTypes.js +1 -0
- package/dist/runtime/headlessRunner.d.ts +17 -2
- package/dist/runtime/headlessRunner.js +15 -0
- package/dist/runtime/hostPolicy.d.ts +51 -0
- package/dist/runtime/hostPolicy.js +68 -0
- package/dist/runtime/hostedGameRunner.d.ts +3 -1
- package/dist/runtime/hostedGameRunner.js +1 -0
- package/dist/runtime/hostedWorldSession.d.ts +3 -1
- package/dist/runtime/hostedWorldSession.js +1 -0
- package/dist/runtime/worldSnapshot.d.ts +13 -1
- package/dist/runtime/worldSnapshot.js +15 -3
- package/dist/scene/assetCatalog.d.ts +7 -0
- package/dist/scene/assetSpace.js +1 -3
- package/dist/scene/authoredTriggers.d.ts +50 -0
- package/dist/scene/authoredTriggers.js +114 -0
- package/dist/scene/builtinSceneKinds.js +8 -3
- package/dist/scene/colliders.d.ts +79 -0
- package/dist/scene/colliders.js +170 -0
- package/dist/scene/collisionMesh.d.ts +94 -0
- package/dist/scene/collisionMesh.js +699 -0
- package/dist/scene/companion.d.ts +147 -0
- package/dist/scene/companion.js +193 -0
- package/dist/scene/entityStats.d.ts +7 -9
- package/dist/scene/entityStats.js +19 -15
- package/dist/scene/sceneKinds.d.ts +16 -0
- package/dist/scene/sceneKinds.js +4 -0
- package/dist/scene/sceneRaycast.js +15 -0
- package/dist/stats/spawnLevelStats.d.ts +110 -0
- package/dist/stats/spawnLevelStats.js +140 -0
- package/dist/stats/statModifiers.d.ts +16 -0
- package/dist/stats/statModifiers.js +30 -1
- package/dist/stats/statPool.d.ts +70 -0
- package/dist/stats/statPool.js +62 -0
- package/dist/store/changeSignal.d.ts +3 -0
- package/dist/store/changeSignal.js +2 -0
- package/dist/store/observableKeyedStore.d.ts +2 -0
- package/dist/store/observableKeyedStore.js +1 -0
- package/dist/survival/decayMeter.js +1 -3
- package/dist/survival/regionHealth.js +1 -3
- package/dist/ui/hudLayout.d.ts +5 -0
- package/dist/ui/hudLayout.js +17 -1
- package/dist/ui/panelModel.d.ts +120 -0
- package/dist/ui/panelModel.js +168 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/dist/world/authoredEntities.d.ts +29 -0
- package/dist/world/authoredEntities.js +29 -0
- package/dist/world/authoredObjects.d.ts +15 -1
- package/dist/world/authoredObjects.js +13 -1
- package/dist/world/authoredSpawn.d.ts +39 -0
- package/dist/world/authoredSpawn.js +37 -0
- package/dist/world/buildSockets.d.ts +152 -0
- package/dist/world/buildSockets.js +188 -0
- package/dist/world/buildingLots.d.ts +84 -0
- package/dist/world/buildingLots.js +119 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/cityBlocks.d.ts +152 -0
- package/dist/world/cityBlocks.js +670 -0
- package/dist/world/cityContent.d.ts +77 -0
- package/dist/world/cityContent.js +261 -0
- package/dist/world/cityGenerator.d.ts +41 -0
- package/dist/world/cityGenerator.js +58 -0
- package/dist/world/cityGeometry.d.ts +114 -0
- package/dist/world/cityGeometry.js +573 -0
- package/dist/world/cityKind.d.ts +314 -0
- package/dist/world/cityKind.js +1578 -0
- package/dist/world/environmentSummary.js +57 -1
- package/dist/world/features.d.ts +53 -5
- package/dist/world/features.js +15 -3
- package/dist/world/grassKind.js +2 -2
- package/dist/world/markers.d.ts +50 -4
- package/dist/world/markers.js +49 -0
- package/dist/world/massing.js +1 -1
- package/dist/world/minimapBake.d.ts +78 -0
- package/dist/world/minimapBake.js +208 -0
- package/dist/world/minimapTrack.d.ts +17 -0
- package/dist/world/minimapTrack.js +22 -0
- package/dist/world/place.d.ts +166 -0
- package/dist/world/place.js +96 -0
- package/dist/world/placeAsset.js +3 -1
- package/dist/world/poleLineKind.d.ts +40 -0
- package/dist/world/poleLineKind.js +97 -0
- package/dist/world/polyline.js +9 -2
- package/dist/world/resourceNode.d.ts +109 -0
- package/dist/world/resourceNode.js +140 -0
- package/dist/world/roads.d.ts +19 -2
- package/dist/world/roads.js +60 -3
- package/dist/world/scatter.js +2 -20
- package/dist/world/streetGenerator.d.ts +159 -0
- package/dist/world/streetGenerator.js +684 -0
- package/dist/world/streets.d.ts +103 -1
- package/dist/world/streets.js +273 -0
- package/dist/world/terraform.js +58 -88
- package/dist/world/terrain.d.ts +65 -1
- package/dist/world/terrain.js +119 -2
- package/dist/world.d.ts +14 -5
- package/dist/world.js +12 -3
- package/package.json +1 -1
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-combat/SKILL.md +19 -1
- package/skills/jgengine-combat/api.md +75 -2
- package/skills/jgengine-combat/capabilities.md +61 -1
- package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
- package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
- package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
- package/skills/jgengine-combat/reference.md +33 -4
- package/skills/jgengine-gameplay/SKILL.md +8 -0
- package/skills/jgengine-gameplay/api.md +182 -26
- package/skills/jgengine-gameplay/capabilities.md +96 -2
- package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
- package/skills/jgengine-gameplay/reference-systems.md +10 -5
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +453 -21
- package/skills/jgengine-world/capabilities.md +112 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/jgengine/reference-cartridge.md +0 -72
package/dist/world/streets.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RoadEnvironmentDescriptor } from "./features.js";
|
|
2
|
-
import { type RoadPoint } from "./roads.js";
|
|
2
|
+
import { type DashExclusion, type RoadPoint } from "./roads.js";
|
|
3
3
|
/**
|
|
4
4
|
* Offset a centerline sideways by a signed distance along its local perpendicular — the
|
|
5
5
|
* building block for lanes, curb lines, and sidewalk paths. Positive offsets fall on the
|
|
@@ -73,8 +73,110 @@ export interface ParkingSpot {
|
|
|
73
73
|
* coordinates in the middle of the carriageway.
|
|
74
74
|
*/
|
|
75
75
|
export declare function parkingSpots(road: RoadEnvironmentDescriptor, options?: ParkingSpotOptions): readonly ParkingSpot[];
|
|
76
|
+
/**
|
|
77
|
+
* Signed planar distance from `(x, z)` to the nearest road *surface edge* across every segment of
|
|
78
|
+
* every road (#1051): negative inside the asphalt (magnitude = how far in from the closest edge, so
|
|
79
|
+
* `-width/2` on the centerline), `~0` right at the curb, and positive off the road (the gap to the
|
|
80
|
+
* nearest edge). Point-to-segment distance minus the road's half width, minimised over all segments —
|
|
81
|
+
* the "am I on the road, and by how much" seam driving grip, tyre audio, and off-road penalties.
|
|
82
|
+
*
|
|
83
|
+
* Allocation-free per call (only scalars — no arrays or objects) so a per-tick car sim can sample it
|
|
84
|
+
* on the hot path; degenerate roads (fewer than two path points) contribute nothing, and with no road
|
|
85
|
+
* surface at all it returns {@link Number.POSITIVE_INFINITY} (everywhere is off-road).
|
|
86
|
+
*/
|
|
87
|
+
export declare function distanceToRoadEdge(roads: readonly RoadEnvironmentDescriptor[], x: number, z: number): number;
|
|
88
|
+
/** Grip levels and blend width for {@link roadSurfaceSampler}; each field is optional and defaulted. */
|
|
89
|
+
export interface RoadSurfaceOptions {
|
|
90
|
+
/** Grip multiplier on the asphalt. Default 1. */
|
|
91
|
+
onRoad?: number;
|
|
92
|
+
/** Grip multiplier once fully off the road past the shoulder. Default 0.72. */
|
|
93
|
+
offRoad?: number;
|
|
94
|
+
/** Width (world units) of the blend band outside the curb from full to off-road grip. Default 3. */
|
|
95
|
+
shoulder?: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A `surfaceFriction`-compatible sampler (the `(x, z) => grip` hook a kinematic car reads each tick)
|
|
99
|
+
* that turns road geometry into grip (#1051): full `onRoad` grip on the asphalt, a monotonic linear ramp
|
|
100
|
+
* down across a `shoulder`-wide band at the curb, and flat `offRoad` grip beyond it — so a car that
|
|
101
|
+
* clips onto the grass loses bite progressively instead of snapping. Closes over {@link
|
|
102
|
+
* distanceToRoadEdge}, so the returned sampler is itself allocation-free per call.
|
|
103
|
+
*/
|
|
104
|
+
export declare function roadSurfaceSampler(roads: readonly RoadEnvironmentDescriptor[], options?: RoadSurfaceOptions): (x: number, z: number) => number;
|
|
76
105
|
/**
|
|
77
106
|
* A deterministic point on one of a road's sidewalks at a normalized position — `side` picks the
|
|
78
107
|
* band, `fraction` (0..1) picks how far along. The canonical pedestrian spawn helper.
|
|
79
108
|
*/
|
|
80
109
|
export declare function sidewalkPoint(road: RoadEnvironmentDescriptor, side: "left" | "right", fraction: number): RoadPoint | null;
|
|
110
|
+
/**
|
|
111
|
+
* The two curb lines of a road — offset polylines running along the outer edge of the asphalt on
|
|
112
|
+
* each side. Mesh these with a thin darker ribbon to give the road a curb/edge strip instead of a
|
|
113
|
+
* hard asphalt-to-terrain seam. `inset` (default 0) nudges the line inward off the raw edge so the
|
|
114
|
+
* strip straddles the border.
|
|
115
|
+
*/
|
|
116
|
+
export declare function curbPaths(road: RoadEnvironmentDescriptor, inset?: number): readonly [readonly RoadPoint[], readonly RoadPoint[]];
|
|
117
|
+
/** A single arm of a junction: the road width plus the outward direction the road leaves the center. */
|
|
118
|
+
export interface RoadApproach {
|
|
119
|
+
/** Unit vector pointing from the junction center outward along the road arm. */
|
|
120
|
+
direction: readonly [number, number];
|
|
121
|
+
/** Drivable width of the road on this arm. */
|
|
122
|
+
width: number;
|
|
123
|
+
}
|
|
124
|
+
/** A welded road intersection: a patch center/radius plus the arms that feed it. */
|
|
125
|
+
export interface RoadJunction {
|
|
126
|
+
/** World XZ center of the intersection. */
|
|
127
|
+
center: RoadPoint;
|
|
128
|
+
/** Radius of the covering patch (covers the crossing ribbons). */
|
|
129
|
+
radius: number;
|
|
130
|
+
/** Representative surface elevation (max of the participating roads). */
|
|
131
|
+
elevation: number;
|
|
132
|
+
/** Representative asphalt color (the widest participating road's color). */
|
|
133
|
+
color: string;
|
|
134
|
+
/** Outward arms leaving the junction — one per road direction that continues past the center. */
|
|
135
|
+
approaches: readonly RoadApproach[];
|
|
136
|
+
}
|
|
137
|
+
/** Options for {@link findRoadJunctions}. */
|
|
138
|
+
export interface RoadJunctionOptions {
|
|
139
|
+
/** Roads weld only when their elevations differ by at most this much. Default 0.12. */
|
|
140
|
+
elevationBand?: number;
|
|
141
|
+
/** Crossing points closer than this merge into a single junction. Default 6. */
|
|
142
|
+
mergeDistance?: number;
|
|
143
|
+
/** Hard cap on junctions returned so detection stays bounded. Default 256. */
|
|
144
|
+
maxJunctions?: number;
|
|
145
|
+
/** Extra scale on the patch radius past the widest arm's half-width. Default 1.15. */
|
|
146
|
+
radiusScale?: number;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Detect welded intersections across a set of roads: wherever two road centerlines cross (or a road
|
|
150
|
+
* terminates on another) within the same elevation band, emit a {@link RoadJunction} whose patch
|
|
151
|
+
* covers the crossing ribbons. Nearby crossings merge into one junction. Deterministic and bounded —
|
|
152
|
+
* O(roads² · segments²) with a `maxJunctions` cap — so a scene builds its intersections once.
|
|
153
|
+
*/
|
|
154
|
+
export declare function findRoadJunctions(roads: readonly RoadEnvironmentDescriptor[], options?: RoadJunctionOptions): readonly RoadJunction[];
|
|
155
|
+
/** Circular marking-exclusion zones for the junctions, sized to hide the center line under each patch. */
|
|
156
|
+
export declare function junctionExclusions(junctions: readonly RoadJunction[], pad?: number): readonly DashExclusion[];
|
|
157
|
+
/** Options for {@link junctionMarkings}. */
|
|
158
|
+
export interface JunctionMarkingOptions {
|
|
159
|
+
/** Emit a stop-line bar across each approach. Default true. */
|
|
160
|
+
stopLines?: boolean;
|
|
161
|
+
/** Emit crosswalk bars at each approach. Default true. */
|
|
162
|
+
crosswalks?: boolean;
|
|
163
|
+
/** Number of zebra bars per crosswalk. Default 5. */
|
|
164
|
+
crosswalkBars?: number;
|
|
165
|
+
/** Clear gap outside the patch before markings start. Default 0.6. */
|
|
166
|
+
gap?: number;
|
|
167
|
+
/** Depth (along travel) of the crosswalk band. Default 2.4. */
|
|
168
|
+
crosswalkDepth?: number;
|
|
169
|
+
}
|
|
170
|
+
/** Thin marking polylines for a junction: stop-line bars and crosswalk bars, ready for {@link buildRoadRibbon}. */
|
|
171
|
+
export interface JunctionMarkings {
|
|
172
|
+
/** One bar per approach, perpendicular to travel just outside the patch. */
|
|
173
|
+
stopLines: readonly (readonly RoadPoint[])[];
|
|
174
|
+
/** Continental (parallel-to-travel) zebra bars, `crosswalkBars` per approach. */
|
|
175
|
+
crosswalkBars: readonly (readonly RoadPoint[])[];
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Data-driven junction entry markings: a stop-line bar across each approach and a band of crosswalk
|
|
179
|
+
* bars just outside the welded patch. Each returned polyline is a thin centerline meant to be meshed
|
|
180
|
+
* with {@link buildRoadRibbon} at a small width. Fully skippable per-feature via the options.
|
|
181
|
+
*/
|
|
182
|
+
export declare function junctionMarkings(junction: RoadJunction, options?: JunctionMarkingOptions): JunctionMarkings;
|
package/dist/world/streets.js
CHANGED
|
@@ -130,6 +130,61 @@ export function parkingSpots(road, options = {}) {
|
|
|
130
130
|
}
|
|
131
131
|
return spots;
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Signed planar distance from `(x, z)` to the nearest road *surface edge* across every segment of
|
|
135
|
+
* every road (#1051): negative inside the asphalt (magnitude = how far in from the closest edge, so
|
|
136
|
+
* `-width/2` on the centerline), `~0` right at the curb, and positive off the road (the gap to the
|
|
137
|
+
* nearest edge). Point-to-segment distance minus the road's half width, minimised over all segments —
|
|
138
|
+
* the "am I on the road, and by how much" seam driving grip, tyre audio, and off-road penalties.
|
|
139
|
+
*
|
|
140
|
+
* Allocation-free per call (only scalars — no arrays or objects) so a per-tick car sim can sample it
|
|
141
|
+
* on the hot path; degenerate roads (fewer than two path points) contribute nothing, and with no road
|
|
142
|
+
* surface at all it returns {@link Number.POSITIVE_INFINITY} (everywhere is off-road).
|
|
143
|
+
*/
|
|
144
|
+
export function distanceToRoadEdge(roads, x, z) {
|
|
145
|
+
let best = Number.POSITIVE_INFINITY;
|
|
146
|
+
for (let r = 0; r < roads.length; r += 1) {
|
|
147
|
+
const road = roads[r];
|
|
148
|
+
const path = road.path;
|
|
149
|
+
if (path.length < 2)
|
|
150
|
+
continue;
|
|
151
|
+
const half = road.width / 2;
|
|
152
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
153
|
+
const a = path[i];
|
|
154
|
+
const b = path[i + 1];
|
|
155
|
+
const abx = b[0] - a[0];
|
|
156
|
+
const abz = b[1] - a[1];
|
|
157
|
+
const lengthSq = abx * abx + abz * abz;
|
|
158
|
+
const t = lengthSq === 0 ? 0 : Math.max(0, Math.min(1, ((x - a[0]) * abx + (z - a[1]) * abz) / lengthSq));
|
|
159
|
+
const px = a[0] + abx * t;
|
|
160
|
+
const pz = a[1] + abz * t;
|
|
161
|
+
const edge = Math.hypot(x - px, z - pz) - half;
|
|
162
|
+
if (edge < best)
|
|
163
|
+
best = edge;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return best;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* A `surfaceFriction`-compatible sampler (the `(x, z) => grip` hook a kinematic car reads each tick)
|
|
170
|
+
* that turns road geometry into grip (#1051): full `onRoad` grip on the asphalt, a monotonic linear ramp
|
|
171
|
+
* down across a `shoulder`-wide band at the curb, and flat `offRoad` grip beyond it — so a car that
|
|
172
|
+
* clips onto the grass loses bite progressively instead of snapping. Closes over {@link
|
|
173
|
+
* distanceToRoadEdge}, so the returned sampler is itself allocation-free per call.
|
|
174
|
+
*/
|
|
175
|
+
export function roadSurfaceSampler(roads, options = {}) {
|
|
176
|
+
const onRoad = options.onRoad ?? 1;
|
|
177
|
+
const offRoad = options.offRoad ?? 0.72;
|
|
178
|
+
const shoulder = options.shoulder ?? 3;
|
|
179
|
+
return (x, z) => {
|
|
180
|
+
const edge = distanceToRoadEdge(roads, x, z);
|
|
181
|
+
if (edge <= 0)
|
|
182
|
+
return onRoad;
|
|
183
|
+
if (shoulder <= 0 || edge >= shoulder)
|
|
184
|
+
return offRoad;
|
|
185
|
+
return onRoad + (offRoad - onRoad) * (edge / shoulder);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
133
188
|
/**
|
|
134
189
|
* A deterministic point on one of a road's sidewalks at a normalized position — `side` picks the
|
|
135
190
|
* band, `fraction` (0..1) picks how far along. The canonical pedestrian spawn helper.
|
|
@@ -142,3 +197,221 @@ export function sidewalkPoint(road, side, fraction) {
|
|
|
142
197
|
const along = Math.max(0, Math.min(1, fraction)) * pathLength(path);
|
|
143
198
|
return pointAlong(path, along).point;
|
|
144
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* The two curb lines of a road — offset polylines running along the outer edge of the asphalt on
|
|
202
|
+
* each side. Mesh these with a thin darker ribbon to give the road a curb/edge strip instead of a
|
|
203
|
+
* hard asphalt-to-terrain seam. `inset` (default 0) nudges the line inward off the raw edge so the
|
|
204
|
+
* strip straddles the border.
|
|
205
|
+
*/
|
|
206
|
+
export function curbPaths(road, inset = 0) {
|
|
207
|
+
const edge = road.width / 2 - inset;
|
|
208
|
+
return [offsetPath(road.path, edge), offsetPath(road.path, -edge)];
|
|
209
|
+
}
|
|
210
|
+
const EPS = 1e-6;
|
|
211
|
+
/** Intersection point of segments a→b and c→d, or null when they don't cross (endpoints included). */
|
|
212
|
+
function segmentIntersection(a, b, c, d) {
|
|
213
|
+
const r0 = b[0] - a[0];
|
|
214
|
+
const r1 = b[1] - a[1];
|
|
215
|
+
const s0 = d[0] - c[0];
|
|
216
|
+
const s1 = d[1] - c[1];
|
|
217
|
+
const denom = r0 * s1 - r1 * s0;
|
|
218
|
+
if (Math.abs(denom) < EPS)
|
|
219
|
+
return null; // parallel or degenerate
|
|
220
|
+
const t = ((c[0] - a[0]) * s1 - (c[1] - a[1]) * s0) / denom;
|
|
221
|
+
const u = ((c[0] - a[0]) * r1 - (c[1] - a[1]) * r0) / denom;
|
|
222
|
+
if (t < -EPS || t > 1 + EPS || u < -EPS || u > 1 + EPS)
|
|
223
|
+
return null;
|
|
224
|
+
return [a[0] + r0 * t, a[1] + r1 * t];
|
|
225
|
+
}
|
|
226
|
+
function unit(dx, dz) {
|
|
227
|
+
const len = Math.hypot(dx, dz) || 1;
|
|
228
|
+
return [dx / len, dz / len];
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Add the arms a road contributes at junction point `p`: the road continues past `p` in each
|
|
232
|
+
* direction that still has centerline beyond it, so a mid-path crossing yields two opposing arms
|
|
233
|
+
* and a road that terminates at `p` (a T-junction) yields only the one inward arm.
|
|
234
|
+
*/
|
|
235
|
+
function armsAt(path, p, width, roadIndex) {
|
|
236
|
+
const arms = [];
|
|
237
|
+
// Nearest vertex "before" (toward path start) and "after" (toward path end) of p.
|
|
238
|
+
// Walk the path measuring where p falls; use the two adjacent tangents.
|
|
239
|
+
let hitSeg = -1;
|
|
240
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
241
|
+
const a = path[i];
|
|
242
|
+
const b = path[i + 1];
|
|
243
|
+
const abx = b[0] - a[0];
|
|
244
|
+
const abz = b[1] - a[1];
|
|
245
|
+
const lengthSq = abx * abx + abz * abz;
|
|
246
|
+
const t = lengthSq === 0 ? 0 : ((p[0] - a[0]) * abx + (p[1] - a[1]) * abz) / lengthSq;
|
|
247
|
+
const px = a[0] + abx * Math.max(0, Math.min(1, t));
|
|
248
|
+
const pz = a[1] + abz * Math.max(0, Math.min(1, t));
|
|
249
|
+
if (Math.hypot(p[0] - px, p[1] - pz) < 1e-3) {
|
|
250
|
+
hitSeg = i;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (hitSeg < 0)
|
|
255
|
+
return arms;
|
|
256
|
+
const a = path[hitSeg];
|
|
257
|
+
const b = path[hitSeg + 1];
|
|
258
|
+
const forward = unit(b[0] - a[0], b[1] - a[1]);
|
|
259
|
+
const atStart = Math.hypot(p[0] - path[0][0], p[1] - path[0][1]) < 1e-3;
|
|
260
|
+
const atEnd = Math.hypot(p[0] - path[path.length - 1][0], p[1] - path[path.length - 1][1]) < 1e-3;
|
|
261
|
+
// Arm toward path end exists unless p sits exactly at the end.
|
|
262
|
+
if (!atEnd)
|
|
263
|
+
arms.push({ roadIndex, direction: forward, width });
|
|
264
|
+
// Arm toward path start exists unless p sits exactly at the start.
|
|
265
|
+
if (!atStart)
|
|
266
|
+
arms.push({ roadIndex, direction: [-forward[0], -forward[1]], width });
|
|
267
|
+
return arms;
|
|
268
|
+
}
|
|
269
|
+
function mergeArm(arms, arm) {
|
|
270
|
+
for (const existing of arms) {
|
|
271
|
+
if (existing.roadIndex === arm.roadIndex) {
|
|
272
|
+
const dot = existing.direction[0] * arm.direction[0] + existing.direction[1] * arm.direction[1];
|
|
273
|
+
if (dot > 0.985)
|
|
274
|
+
return; // same road, near-identical direction — already have this arm
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
arms.push(arm);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Detect welded intersections across a set of roads: wherever two road centerlines cross (or a road
|
|
281
|
+
* terminates on another) within the same elevation band, emit a {@link RoadJunction} whose patch
|
|
282
|
+
* covers the crossing ribbons. Nearby crossings merge into one junction. Deterministic and bounded —
|
|
283
|
+
* O(roads² · segments²) with a `maxJunctions` cap — so a scene builds its intersections once.
|
|
284
|
+
*/
|
|
285
|
+
export function findRoadJunctions(roads, options = {}) {
|
|
286
|
+
const elevationBand = options.elevationBand ?? 0.12;
|
|
287
|
+
const mergeDistance = options.mergeDistance ?? 6;
|
|
288
|
+
const maxJunctions = options.maxJunctions ?? 256;
|
|
289
|
+
const radiusScale = options.radiusScale ?? 1.15;
|
|
290
|
+
const mergeSq = mergeDistance * mergeDistance;
|
|
291
|
+
const candidates = [];
|
|
292
|
+
const emit = (p, ri, ci) => {
|
|
293
|
+
if (candidates.length >= maxJunctions && findNear(candidates, p, mergeSq) === null)
|
|
294
|
+
return;
|
|
295
|
+
const roadA = roads[ri];
|
|
296
|
+
const roadB = roads[ci];
|
|
297
|
+
const half = Math.max(roadA.width, roadB.width) / 2;
|
|
298
|
+
const radius = half * radiusScale;
|
|
299
|
+
const elevation = Math.max(roadA.elevation, roadB.elevation);
|
|
300
|
+
const wider = roadA.width >= roadB.width ? roadA : roadB;
|
|
301
|
+
const near = findNear(candidates, p, mergeSq);
|
|
302
|
+
const arms = [
|
|
303
|
+
...armsAt(roadA.path, p, roadA.width, ri),
|
|
304
|
+
...armsAt(roadB.path, p, roadB.width, ci),
|
|
305
|
+
];
|
|
306
|
+
if (near === null) {
|
|
307
|
+
const merged = [];
|
|
308
|
+
for (const arm of arms)
|
|
309
|
+
mergeArm(merged, arm);
|
|
310
|
+
candidates.push({
|
|
311
|
+
x: p[0],
|
|
312
|
+
z: p[1],
|
|
313
|
+
radius,
|
|
314
|
+
elevation,
|
|
315
|
+
color: wider.color,
|
|
316
|
+
colorWidth: wider.width,
|
|
317
|
+
arms: merged,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
near.radius = Math.max(near.radius, radius);
|
|
322
|
+
near.elevation = Math.max(near.elevation, elevation);
|
|
323
|
+
if (wider.width > near.colorWidth) {
|
|
324
|
+
near.color = wider.color;
|
|
325
|
+
near.colorWidth = wider.width;
|
|
326
|
+
}
|
|
327
|
+
for (const arm of arms)
|
|
328
|
+
mergeArm(near.arms, arm);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
for (let i = 0; i < roads.length; i += 1) {
|
|
332
|
+
const roadA = roads[i];
|
|
333
|
+
if (roadA.path.length < 2)
|
|
334
|
+
continue;
|
|
335
|
+
for (let j = i + 1; j < roads.length; j += 1) {
|
|
336
|
+
const roadB = roads[j];
|
|
337
|
+
if (roadB.path.length < 2)
|
|
338
|
+
continue;
|
|
339
|
+
if (Math.abs(roadA.elevation - roadB.elevation) > elevationBand)
|
|
340
|
+
continue;
|
|
341
|
+
for (let si = 0; si < roadA.path.length - 1; si += 1) {
|
|
342
|
+
for (let sj = 0; sj < roadB.path.length - 1; sj += 1) {
|
|
343
|
+
const hit = segmentIntersection(roadA.path[si], roadA.path[si + 1], roadB.path[sj], roadB.path[sj + 1]);
|
|
344
|
+
if (hit !== null)
|
|
345
|
+
emit(hit, i, j);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return candidates.map((c) => ({
|
|
351
|
+
center: [c.x, c.z],
|
|
352
|
+
radius: c.radius,
|
|
353
|
+
elevation: c.elevation,
|
|
354
|
+
color: c.color,
|
|
355
|
+
approaches: c.arms.map((arm) => ({ direction: arm.direction, width: arm.width })),
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
function findNear(candidates, p, mergeSq) {
|
|
359
|
+
for (const c of candidates) {
|
|
360
|
+
const dx = c.x - p[0];
|
|
361
|
+
const dz = c.z - p[1];
|
|
362
|
+
if (dx * dx + dz * dz <= mergeSq)
|
|
363
|
+
return c;
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
/** Circular marking-exclusion zones for the junctions, sized to hide the center line under each patch. */
|
|
368
|
+
export function junctionExclusions(junctions, pad = 0.5) {
|
|
369
|
+
return junctions.map((j) => ({ center: j.center, radius: j.radius + pad }));
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Data-driven junction entry markings: a stop-line bar across each approach and a band of crosswalk
|
|
373
|
+
* bars just outside the welded patch. Each returned polyline is a thin centerline meant to be meshed
|
|
374
|
+
* with {@link buildRoadRibbon} at a small width. Fully skippable per-feature via the options.
|
|
375
|
+
*/
|
|
376
|
+
export function junctionMarkings(junction, options = {}) {
|
|
377
|
+
const wantStop = options.stopLines ?? true;
|
|
378
|
+
const wantCross = options.crosswalks ?? true;
|
|
379
|
+
const barCount = Math.max(1, Math.floor(options.crosswalkBars ?? 5));
|
|
380
|
+
const gap = options.gap ?? 0.6;
|
|
381
|
+
const depth = options.crosswalkDepth ?? 2.4;
|
|
382
|
+
const [cx, cz] = junction.center;
|
|
383
|
+
const stopLines = [];
|
|
384
|
+
const crosswalkBars = [];
|
|
385
|
+
for (const approach of junction.approaches) {
|
|
386
|
+
const [dx, dz] = approach.direction;
|
|
387
|
+
// Across-the-road axis, perpendicular to travel.
|
|
388
|
+
const ax = -dz;
|
|
389
|
+
const az = dx;
|
|
390
|
+
const half = approach.width / 2;
|
|
391
|
+
const stopDist = junction.radius + gap;
|
|
392
|
+
if (wantStop) {
|
|
393
|
+
const px = cx + dx * stopDist;
|
|
394
|
+
const pz = cz + dz * stopDist;
|
|
395
|
+
stopLines.push([
|
|
396
|
+
[px + ax * half, pz + az * half],
|
|
397
|
+
[px - ax * half, pz - az * half],
|
|
398
|
+
]);
|
|
399
|
+
}
|
|
400
|
+
if (wantCross) {
|
|
401
|
+
const near = stopDist + 0.4;
|
|
402
|
+
const barWidth = half * 2 * 0.16;
|
|
403
|
+
const usable = half * 2 - barWidth;
|
|
404
|
+
for (let b = 0; b < barCount; b += 1) {
|
|
405
|
+
const frac = barCount === 1 ? 0 : b / (barCount - 1) - 0.5;
|
|
406
|
+
const offset = frac * usable;
|
|
407
|
+
const bx = cx + ax * offset;
|
|
408
|
+
const bz = cz + az * offset;
|
|
409
|
+
crosswalkBars.push([
|
|
410
|
+
[bx + dx * near, bz + dz * near],
|
|
411
|
+
[bx + dx * (near + depth), bz + dz * (near + depth)],
|
|
412
|
+
]);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { stopLines, crosswalkBars };
|
|
417
|
+
}
|
package/dist/world/terraform.js
CHANGED
|
@@ -218,13 +218,18 @@ export function createEditableTerrain(config) {
|
|
|
218
218
|
record?.(index, before, surface);
|
|
219
219
|
return true;
|
|
220
220
|
}
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Visits every cell in a brush footprint on the CELL grid, yielding each cell's flat `index`, its
|
|
223
|
+
* {@link brushWeight} `weight`, world center (`wx`,`wz`), and raw `dist` from the brush center.
|
|
224
|
+
* Shared by the cell-grid brushes ({@link paintSurface}, {@link blendPaint}); each applies its own
|
|
225
|
+
* inclusion test on the yielded values (paint uses `dist <= radius`, blend uses `weight > 0`).
|
|
226
|
+
*/
|
|
227
|
+
function forEachBrushCell(edit, visit) {
|
|
228
|
+
const falloff = edit.falloff ?? "smooth";
|
|
223
229
|
const shape = edit.shape ?? "circle";
|
|
224
230
|
const rCells = Math.ceil(edit.radius / cellSize) + 1;
|
|
225
231
|
const cx = gridX(edit.center[0]);
|
|
226
232
|
const cz = gridZ(edit.center[1]);
|
|
227
|
-
let painted = 0;
|
|
228
233
|
for (let iz = -rCells; iz <= rCells; iz += 1) {
|
|
229
234
|
const gz = Math.floor(cz) + iz;
|
|
230
235
|
if (gz < 0 || gz >= rows)
|
|
@@ -233,15 +238,25 @@ export function createEditableTerrain(config) {
|
|
|
233
238
|
const gx = Math.floor(cx) + ix;
|
|
234
239
|
if (gx < 0 || gx >= cols)
|
|
235
240
|
continue;
|
|
236
|
-
const
|
|
237
|
-
const
|
|
241
|
+
const wx = cellWorldX(gx);
|
|
242
|
+
const wz = cellWorldZ(gz);
|
|
243
|
+
const dx = wx - edit.center[0];
|
|
244
|
+
const dz = wz - edit.center[1];
|
|
238
245
|
const dist = shape === "square" ? Math.max(Math.abs(dx), Math.abs(dz)) : Math.hypot(dx, dz);
|
|
239
|
-
|
|
240
|
-
continue;
|
|
241
|
-
if (writeSurface(gz * cols + gx, surface, record))
|
|
242
|
-
painted += 1;
|
|
246
|
+
visit(gz * cols + gx, brushWeight(dist, edit.radius, falloff), wx, wz, dist);
|
|
243
247
|
}
|
|
244
248
|
}
|
|
249
|
+
}
|
|
250
|
+
function paintSurface(edit, record) {
|
|
251
|
+
const surface = edit.surface ?? null;
|
|
252
|
+
let painted = 0;
|
|
253
|
+
// Binary inclusion by raw distance (includes dist == radius), independent of falloff shaping.
|
|
254
|
+
forEachBrushCell(edit, (index, _weight, _wx, _wz, dist) => {
|
|
255
|
+
if (dist > edit.radius)
|
|
256
|
+
return;
|
|
257
|
+
if (writeSurface(index, surface, record))
|
|
258
|
+
painted += 1;
|
|
259
|
+
});
|
|
245
260
|
return painted;
|
|
246
261
|
}
|
|
247
262
|
function localNormal(wx, wz) {
|
|
@@ -371,31 +386,14 @@ export function createEditableTerrain(config) {
|
|
|
371
386
|
const target = layers.findIndex((entry) => entry.surface === surface);
|
|
372
387
|
if (target < 0)
|
|
373
388
|
return 0;
|
|
374
|
-
const falloff = edit.falloff ?? "smooth";
|
|
375
|
-
const shape = edit.shape ?? "circle";
|
|
376
389
|
const strength = edit.strength ?? 1;
|
|
377
|
-
const rCells = Math.ceil(edit.radius / cellSize) + 1;
|
|
378
|
-
const cx = gridX(edit.center[0]);
|
|
379
|
-
const cz = gridZ(edit.center[1]);
|
|
380
390
|
let painted = 0;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (gx < 0 || gx >= cols)
|
|
388
|
-
continue;
|
|
389
|
-
const dx = cellWorldX(gx) - edit.center[0];
|
|
390
|
-
const dz = cellWorldZ(gz) - edit.center[1];
|
|
391
|
-
const dist = shape === "square" ? Math.max(Math.abs(dx), Math.abs(dz)) : Math.hypot(dx, dz);
|
|
392
|
-
const weight = brushWeight(dist, edit.radius, falloff);
|
|
393
|
-
if (weight <= 0)
|
|
394
|
-
continue;
|
|
395
|
-
if (blendCell(gz * cols + gx, target, weight * strength, record))
|
|
396
|
-
painted += 1;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
391
|
+
forEachBrushCell(edit, (index, weight) => {
|
|
392
|
+
if (weight <= 0)
|
|
393
|
+
return;
|
|
394
|
+
if (blendCell(index, target, weight * strength, record))
|
|
395
|
+
painted += 1;
|
|
396
|
+
});
|
|
399
397
|
return painted;
|
|
400
398
|
}
|
|
401
399
|
function runHeightEdit(edit, record) {
|
|
@@ -545,23 +543,29 @@ export function createEditableTerrain(config) {
|
|
|
545
543
|
};
|
|
546
544
|
return field;
|
|
547
545
|
}
|
|
548
|
-
/**
|
|
549
|
-
|
|
546
|
+
/**
|
|
547
|
+
* The shared first/latest Map accumulator behind every `begin*Stroke`. Records each key's first
|
|
548
|
+
* `before` and latest `after` across many stamps, then assembles the net changed entries (skipping
|
|
549
|
+
* keys whose latest equals their first). Each stroke supplies its own recording call via `recordStamp`
|
|
550
|
+
* and wraps {@link StrokeChanges} into its concrete delta shape.
|
|
551
|
+
*/
|
|
552
|
+
function createStroke(recordStamp) {
|
|
550
553
|
const first = new Map();
|
|
551
554
|
const latest = new Map();
|
|
552
555
|
return {
|
|
553
556
|
stamp(edit) {
|
|
554
|
-
return
|
|
557
|
+
return recordStamp(edit, (index, before, after) => {
|
|
555
558
|
if (!first.has(index))
|
|
556
559
|
first.set(index, before);
|
|
557
560
|
latest.set(index, after);
|
|
558
561
|
});
|
|
559
562
|
},
|
|
560
|
-
|
|
563
|
+
changes() {
|
|
561
564
|
const indices = [];
|
|
562
565
|
const before = [];
|
|
563
566
|
const after = [];
|
|
564
567
|
for (const [index, next] of latest) {
|
|
568
|
+
// Every latest key was set in `first` on the same callback, so this is always present.
|
|
565
569
|
const prev = first.get(index);
|
|
566
570
|
if (next === prev)
|
|
567
571
|
continue;
|
|
@@ -576,35 +580,22 @@ export function beginTerraformStroke(terrain) {
|
|
|
576
580
|
},
|
|
577
581
|
};
|
|
578
582
|
}
|
|
583
|
+
/** Opens a stroke recorder over `terrain`; stamp edits into it, then read one net delta. */
|
|
584
|
+
export function beginTerraformStroke(terrain) {
|
|
585
|
+
const stroke = createStroke((edit, record) => terrain.applyRecording(edit, record));
|
|
586
|
+
return {
|
|
587
|
+
stamp: stroke.stamp,
|
|
588
|
+
delta: () => stroke.changes(),
|
|
589
|
+
isEmpty: stroke.isEmpty,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
579
592
|
/** Opens a paint-stroke recorder over `terrain`; stamp paint edits into it, then read one net delta. */
|
|
580
593
|
export function beginSurfaceStroke(terrain) {
|
|
581
|
-
const
|
|
582
|
-
const latest = new Map();
|
|
594
|
+
const stroke = createStroke((edit, record) => terrain.paintRecording(edit, record));
|
|
583
595
|
return {
|
|
584
|
-
stamp
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
first.set(index, before);
|
|
588
|
-
latest.set(index, after);
|
|
589
|
-
});
|
|
590
|
-
},
|
|
591
|
-
delta() {
|
|
592
|
-
const indices = [];
|
|
593
|
-
const before = [];
|
|
594
|
-
const after = [];
|
|
595
|
-
for (const [index, next] of latest) {
|
|
596
|
-
const prev = first.get(index) ?? null;
|
|
597
|
-
if (next === prev)
|
|
598
|
-
continue;
|
|
599
|
-
indices.push(index);
|
|
600
|
-
before.push(prev);
|
|
601
|
-
after.push(next);
|
|
602
|
-
}
|
|
603
|
-
return { indices, before, after };
|
|
604
|
-
},
|
|
605
|
-
isEmpty() {
|
|
606
|
-
return latest.size === 0;
|
|
607
|
-
},
|
|
596
|
+
stamp: stroke.stamp,
|
|
597
|
+
delta: () => stroke.changes(),
|
|
598
|
+
isEmpty: stroke.isEmpty,
|
|
608
599
|
};
|
|
609
600
|
}
|
|
610
601
|
/** Returns a new snapshot with a surface delta's `after` ids applied (copy-on-write). */
|
|
@@ -700,33 +691,12 @@ export function revertWeightDeltaFromSnapshot(snapshot, delta) {
|
|
|
700
691
|
* @internal — the stroke recorder behind the blend brush and the `blendTerrain` command.
|
|
701
692
|
*/
|
|
702
693
|
export function beginBlendStroke(terrain) {
|
|
703
|
-
const
|
|
704
|
-
const latest = new Map();
|
|
694
|
+
const stroke = createStroke((edit, record) => terrain.blendRecording(edit, record));
|
|
705
695
|
return {
|
|
706
|
-
stamp
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
latest.set(index, after);
|
|
711
|
-
});
|
|
712
|
-
},
|
|
713
|
-
delta() {
|
|
714
|
-
const indices = [];
|
|
715
|
-
const before = [];
|
|
716
|
-
const after = [];
|
|
717
|
-
for (const [index, next] of latest) {
|
|
718
|
-
const prev = first.get(index);
|
|
719
|
-
if (next === prev)
|
|
720
|
-
continue;
|
|
721
|
-
indices.push(index);
|
|
722
|
-
before.push(prev);
|
|
723
|
-
after.push(next);
|
|
724
|
-
}
|
|
725
|
-
return { layerCount: terrain.layers.length, indices, before, after };
|
|
726
|
-
},
|
|
727
|
-
isEmpty() {
|
|
728
|
-
return latest.size === 0;
|
|
729
|
-
},
|
|
696
|
+
stamp: stroke.stamp,
|
|
697
|
+
// layerCount is read at delta() time, guarding replay after the layer stack changed shape.
|
|
698
|
+
delta: () => ({ layerCount: terrain.layers.length, ...stroke.changes() }),
|
|
699
|
+
isEmpty: stroke.isEmpty,
|
|
730
700
|
};
|
|
731
701
|
}
|
|
732
702
|
/**
|