This commit is contained in:
Rainer Leit
2026-09-25 17:02:24 +03:00
parent cc43ed8dc8
commit 9597629951
2149 changed files with 460234 additions and 1770 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
{
"enabledPlugins": {
"unreal-engine-skills-for-claude-code@claude-plugins-official": true
"unreal-engine-skills-for-claude-code@claude-plugins-official": true,
"impeccable@impeccable": true
}
}
+2
View File
@@ -3,6 +3,8 @@
*.umap filter=lfs diff=lfs merge=lfs -text
*.fbx filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
+26
View File
@@ -90,3 +90,29 @@ __pycache__/
# The terrain generator is a Go program; its binary and preview output are rebuilt from a seed.
Tools/Terrain/bin/
RawContent/World/Preview/
# A planet bake is gigabytes of PNG rebuilt from a template and a seed in an hour or two, and the plan
# beside it is rewritten every run. Neither is worth archiving; Templates/ is the input and is tracked.
RawContent/World/Bake_*/
RawContent/World/Plan/
# The region tiles are 208 MB of PNG regenerated in two minutes from Region.json and the source map in
# Orogen Gens/, which is tracked because it came out of a browser session and cannot be regenerated here.
RawContent/World/RegionTiles/
# The raw Fab/Quixel downloads: 800 MB of 4K JPGs, nine maps per set where the landscape material samples
# three, and re-downloadable from Fab by anyone with the account. What is tracked instead is the 2K extract
# `mapart substances` makes from them, which is what actually gets imported - about a tenth of the size and
# the only part any pipeline reads. Delete Source/ and the substances still rebuild from Substances/.
RawContent/Terrain/Source/
# The biome masks are rendered from the painting and the climate export by `mapart biomes` in a few seconds.
# Their inputs are tracked (Templates/ and Orogen Gens/) and the masks themselves are a product; biomes.json
# beside them is the report of the run that made them.
RawContent/World/Biomes/
# The map art is rendered from the same Orogen Gens/ images in about six seconds by Tools/MapArt, so only its
# manifest is worth tracking. What the map view actually reads is the imported texture in Content/World/Maps/,
# which goes through LFS like every other asset.
RawContent/World/MapArt/*.png
RawContent/World/MapArt/mapart.json
Tools/MapArt/bin/
+14
View File
@@ -0,0 +1,14 @@
{
"detector": {
"ignoreRules": [],
"ignoreFiles": [],
"ignoreValues": [
{
"rule": "design-system-font",
"value": "apple sd gothic neo",
"createdAt": "2026-09-18T15:47:39.710Z",
"reason": "user accepted the Cyworld Minihompy departure (live session 9131d1ff, variant 3); DESIGN.md still describes the superseded iron-and-brass world and needs regenerating"
}
]
}
}
+266
View File
@@ -0,0 +1,266 @@
{
"schemaVersion": 2,
"generatedAt": "2026-09-18T00:00:00Z",
"title": "Design System: terrain studio",
"extensions": {
"colorMeta": {
"hot": {
"role": "primary",
"displayName": "Brass",
"canonical": "#d6a03a",
"tonalRamp": ["oklch(0.15 0.045 78)", "oklch(0.26 0.070 78)", "oklch(0.37 0.090 78)", "oklch(0.49 0.105 78)", "oklch(0.61 0.115 78)", "oklch(0.73 0.115 78)", "oklch(0.85 0.080 78)", "oklch(0.95 0.035 78)"]
},
"hot-ink": {
"role": "primary",
"displayName": "Brass Ink",
"canonical": "#17181a"
},
"ok": {
"role": "secondary",
"displayName": "Verdigris",
"canonical": "#6fbf73",
"tonalRamp": ["oklch(0.15 0.045 145)", "oklch(0.26 0.075 145)", "oklch(0.37 0.100 145)", "oklch(0.49 0.120 145)", "oklch(0.61 0.130 145)", "oklch(0.73 0.130 145)", "oklch(0.85 0.090 145)", "oklch(0.95 0.040 145)"]
},
"ok-ink": {
"role": "secondary",
"displayName": "Verdigris Light",
"canonical": "#bfe6c1"
},
"warn": {
"role": "tertiary",
"displayName": "Rust",
"canonical": "#b4553f",
"tonalRamp": ["oklch(0.15 0.050 37)", "oklch(0.26 0.080 37)", "oklch(0.37 0.110 37)", "oklch(0.49 0.128 37)", "oklch(0.61 0.130 37)", "oklch(0.73 0.115 37)", "oklch(0.85 0.080 37)", "oklch(0.95 0.035 37)"]
},
"warn-ink": {
"role": "tertiary",
"displayName": "Rust Light",
"canonical": "#f0b4a4"
},
"void": {
"role": "neutral",
"displayName": "Void",
"canonical": "#0b0e11"
},
"well": {
"role": "neutral",
"displayName": "Well",
"canonical": "#11151a"
},
"bg": {
"role": "neutral",
"displayName": "Cold Iron",
"canonical": "#14181d"
},
"panel": {
"role": "neutral",
"displayName": "Panel Iron",
"canonical": "#1b2027",
"tonalRamp": ["#0b0e11", "#11151a", "#14181d", "#1b2027", "#222831", "#252b34", "#262d36", "#2b323b"]
},
"row-hover": {
"role": "neutral",
"displayName": "Iron Hover",
"canonical": "#222831"
},
"row-live": {
"role": "neutral",
"displayName": "Iron Live",
"canonical": "#252b34"
},
"raised": {
"role": "neutral",
"displayName": "Raised Iron",
"canonical": "#262d36"
},
"line": {
"role": "neutral",
"displayName": "Hairline",
"canonical": "#2b323b"
},
"ink": {
"role": "neutral",
"displayName": "Cold Ink",
"canonical": "#d8dee6",
"tonalRamp": ["oklch(0.15 0.010 250)", "oklch(0.26 0.012 250)", "oklch(0.37 0.013 250)", "oklch(0.49 0.014 250)", "oklch(0.61 0.014 250)", "oklch(0.73 0.014 250)", "oklch(0.85 0.013 250)", "oklch(0.95 0.008 250)"]
},
"dim": {
"role": "neutral",
"displayName": "Graphite",
"canonical": "#8b95a1",
"tonalRamp": ["oklch(0.15 0.012 245)", "oklch(0.26 0.016 245)", "oklch(0.37 0.019 245)", "oklch(0.49 0.020 245)", "oklch(0.61 0.020 245)", "oklch(0.73 0.019 245)", "oklch(0.85 0.015 245)", "oklch(0.95 0.008 245)"]
},
"scrim": {
"role": "neutral",
"displayName": "Scrim",
"canonical": "#0d1014ee"
},
"scrim-hud": {
"role": "neutral",
"displayName": "Scrim HUD",
"canonical": "#0d1014dd"
},
"chip-edge": {
"role": "neutral",
"displayName": "Chip Edge",
"canonical": "#0006"
}
},
"typographyMeta": {
"body": {
"displayName": "Body",
"purpose": "Everything the author typed or chose: class names, button labels, input values, toast messages. Every control carries font: inherit, so a number in a field is the same size as the same number in a sentence."
},
"data": {
"displayName": "Data",
"purpose": "Plan reports, bake status and map-key items, set white-space: pre in graphite with <b> in cold ink for the one word that matters."
},
"note": {
"displayName": "Note",
"purpose": "The 11px graphite annotation under a control, saying why rather than what. A first-class role in this system, not a caption."
},
"label": {
"displayName": "Label",
"purpose": "Uppercased, tracked section headings in the rail and map-key titles. Lowercase in the source, uppercased by CSS."
}
},
"shadows": [],
"motion": [
{
"name": "progress-width",
"value": "width .3s",
"purpose": "The only animation in the product. It exists because a bake is a two-hour process and the bar measures something real. No other state change in this system transitions."
}
],
"breakpoints": []
},
"components": [
{
"name": "Primary Button",
"kind": "button",
"refersTo": "button-primary",
"description": "The one action in a group: Plan, Bake. Filled brass with near-black text. Hover is deliberately a no-op here, because the shared button hover only moves the border to brass and this border is brass already.",
"html": "<button class=\"ds-btn-primary\">Plan (4 s)</button>",
"css": ".ds-btn-primary { background: var(--hot, #d6a03a); color: var(--hot-ink, #17181a); border: 1px solid var(--hot, #d6a03a); border-radius: 4px; padding: 6px 10px; font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; font-weight: 600; cursor: pointer; } .ds-btn-primary:disabled { opacity: .5; cursor: default; }"
},
{
"name": "Button",
"kind": "button",
"refersTo": "button",
"description": "Everything that saves, toggles or opens. A raised iron plate with a hairline that turns brass under the cursor; the surface itself never changes and nothing moves.",
"html": "<button class=\"ds-btn\">Save legend</button>",
"css": ".ds-btn { background: var(--raised, #262d36); color: var(--ink, #d8dee6); border: 1px solid var(--line, #2b323b); border-radius: 4px; padding: 6px 10px; font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; cursor: pointer; } .ds-btn:hover { border-color: var(--hot, #d6a03a); } .ds-btn:disabled { opacity: .5; cursor: default; }"
},
{
"name": "Danger Button",
"kind": "button",
"refersTo": "button-warn",
"description": "Rust border and rust-light text on the ordinary raised plate. Exactly one exists in the product, Cancel on a running bake, and it is only rendered while there is something to cancel.",
"html": "<button class=\"ds-btn-warn\">Cancel</button>",
"css": ".ds-btn-warn { background: var(--raised, #262d36); color: var(--warn-ink, #f0b4a4); border: 1px solid var(--warn, #b4553f); border-radius: 4px; padding: 6px 10px; font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; cursor: pointer; } .ds-btn-warn:hover { border-color: var(--hot, #d6a03a); }"
},
{
"name": "Layer Tabs",
"kind": "nav",
"refersTo": "button-toggle-on",
"description": "Two full-width tabs switching the brush between the class painting and the overlay. The active one takes the outlined-brass treatment: filled means act, outlined means live. A tab that cannot apply is disabled with a title rather than hidden.",
"html": "<div class=\"ds-tabs\"><button class=\"ds-tab ds-on\">classes</button><button class=\"ds-tab\">overlay</button></div>",
"css": ".ds-tabs { display: flex; gap: 4px; } .ds-tab { flex: 1; background: var(--raised, #262d36); color: var(--ink, #d8dee6); border: 1px solid var(--line, #2b323b); border-radius: 4px; padding: 4px 6px; font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; cursor: pointer; } .ds-tab:hover { border-color: var(--hot, #d6a03a); } .ds-tab.ds-on { border-color: var(--hot, #d6a03a); color: var(--hot, #d6a03a); font-weight: 600; } .ds-tab:disabled { opacity: .5; cursor: default; }"
},
{
"name": "Class Swatch Row",
"kind": "custom",
"refersTo": "swatch-row",
"description": "The signature control of the rail and a gauge as much as a picker: chip, class name, and a right-aligned reading of what the class does to the ground. The second row shows the selected state; the third is the eraser, whose hatched chip draws absence rather than leaving it blank.",
"html": "<div class=\"ds-sw-list\"><div class=\"ds-sw\"><span class=\"ds-chip\" style=\"background:#5f7d4c\"></span><span class=\"ds-sw-nm\">lowland</span><span class=\"ds-sw-ang\">0.6&deg; plain</span></div><div class=\"ds-sw ds-sw-live\"><span class=\"ds-chip\" style=\"background:#8a6b4a\"></span><span class=\"ds-sw-nm\">upland</span><span class=\"ds-sw-ang\">3.4&deg; rolling</span></div><div class=\"ds-sw\"><span class=\"ds-chip ds-chip-erase\"></span><span class=\"ds-sw-nm\">erase</span><span class=\"ds-sw-ang\"></span></div></div>",
"css": ".ds-sw-list { font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; color: var(--ink, #d8dee6); } .ds-sw { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 4px; border: 1px solid transparent; cursor: pointer; } .ds-sw:hover { background: var(--row-hover, #222831); } .ds-sw.ds-sw-live { border-color: var(--hot, #d6a03a); background: var(--row-live, #252b34); } .ds-chip { width: 18px; height: 18px; border-radius: 3px; border: 1px solid #0006; flex: none; } .ds-chip-erase { background: repeating-linear-gradient(45deg, #3a424c 0 4px, #232931 4px 8px); } .ds-sw-nm { flex: 1; } .ds-sw-ang { color: var(--dim, #8b95a1); font-size: 11px; }"
},
{
"name": "Number Row",
"kind": "input",
"refersTo": "input-number",
"description": "The rail's parameter grammar: a graphite label at flex 1 on the left, a fixed 78px recessed field on the right, and a note underneath saying what the number does to the ground. Nested parameters are indented with two leading spaces in the label text, which is reliable because the font is monospace.",
"html": "<div class=\"ds-num-block\"><div class=\"ds-row\"><label class=\"ds-row-label\">uplift mm/yr</label><input class=\"ds-num\" type=\"number\" step=\"0.005\" value=\"0.12\"></div><div class=\"ds-row\"><label class=\"ds-row-label\">&nbsp;&nbsp;floor mm/yr</label><input class=\"ds-num\" type=\"number\" step=\"0.002\" value=\"0.017\"></div><p class=\"ds-note\">massif <b>3.4&deg;</b> rolling &nbsp;<span style=\"opacity:.6\">(10.2&deg; at a divide)</span></p></div>",
"css": ".ds-num-block { font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; color: var(--ink, #d8dee6); } .ds-row { display: flex; align-items: center; gap: 6px; margin: 3px 0; } .ds-row-label { flex: 1; color: var(--dim, #8b95a1); white-space: pre; } .ds-num { width: 78px; background: var(--well, #11151a); color: var(--ink, #d8dee6); border: 1px solid var(--line, #2b323b); border-radius: 3px; padding: 2px 5px; font: inherit; } .ds-note { color: var(--dim, #8b95a1); font-size: 11px; margin: 4px 0 0; } .ds-note b { color: var(--ink, #d8dee6); font-weight: 600; }"
},
{
"name": "Message Toast",
"kind": "custom",
"refersTo": "toast-error",
"description": "Floats at the stage's top-left over the canvas, inset 10px, at max-width 70%. Darker than any inline surface and translucent, so a message never hides what it is describing. Errors persist until replaced; the ok variant swaps to a verdigris border and removes itself after four seconds.",
"html": "<div class=\"ds-toast ds-toast-err\">This page is served by the studio, not opened from disk.\n\n Tools/Terrain/bin/terrain.exe studio\n\nthen open http://127.0.0.1:8099 in a browser.</div>",
"css": ".ds-toast { background: #0d1014ee; border: 1px solid var(--line, #2b323b); border-radius: 4px; padding: 6px 9px; max-width: 70%; white-space: pre-wrap; font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; color: var(--ink, #d8dee6); } .ds-toast.ds-toast-err { border-color: var(--warn, #b4553f); color: var(--warn-ink, #f0b4a4); } .ds-toast.ds-toast-ok { border-color: var(--ok, #6fbf73); color: var(--ok-ink, #bfe6c1); }"
},
{
"name": "Bake Progress",
"kind": "custom",
"refersTo": "progress-track",
"description": "A 6px well with a brass fill, and the only transition in the entire system. Below it the status block in Data type, with the state word bolded to cold ink and a dimmed tail of the last log lines at 65% opacity.",
"html": "<div class=\"ds-bake\"><div class=\"ds-prog\"><div style=\"width:42%\"></div></div><div class=\"ds-bake-status\"><b>baking</b> 11/26 regions 48.3 min\n 11 -12..1904 m 0.01% clipped 214 s\n 13 0..47 m 0.00% clipped 96 s\n<span style=\"opacity:.65\">region 14: 340 of 900 steps</span></div></div>",
"css": ".ds-bake { font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; } .ds-prog { height: 6px; background: var(--well, #11151a); border-radius: 3px; overflow: hidden; margin: 6px 0; } .ds-prog > div { height: 100%; background: var(--hot, #d6a03a); transition: width .3s; } .ds-bake-status { white-space: pre-wrap; font-size: 11.5px; color: var(--dim, #8b95a1); margin-top: 6px; } .ds-bake-status b { color: var(--ink, #d8dee6); font-weight: 600; }"
},
{
"name": "Map Key",
"kind": "custom",
"refersTo": "ramp",
"description": "Published with every map view and generated by the code that drew that map, so the key cannot drift from the picture. Two shapes: a 12px gradient ramp with its end values beneath for continuous fields, and a two-column chip grid for categorical ones. Under either, a note saying what the map does not mean.",
"html": "<div class=\"ds-key\"><div class=\"ds-key-title\">uplift, mm/yr</div><div class=\"ds-ramp\"></div><div class=\"ds-key-ends\"><span>0</span><span>0.412</span></div><p class=\"ds-note\">The field everything else is a consequence of, massifs included. The cyan lines are fault traces: a rate difference across a line, which erosion carves into a scarp.</p></div>",
"css": ".ds-key { font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; } .ds-key-title { color: var(--dim, #8b95a1); font-size: 11px; text-transform: uppercase; letter-spacing: .09em; margin-bottom: 4px; } .ds-ramp { height: 12px; border-radius: 2px; border: 1px solid #0006; background: linear-gradient(to right, #141a24, #2f4a52, #5d7a4e, #a8974f, #d8c9b4); } .ds-key-ends { display: flex; justify-content: space-between; color: var(--dim, #8b95a1); font-size: 11px; } .ds-key .ds-note { color: var(--dim, #8b95a1); font-size: 11px; margin: 4px 0 0; }"
},
{
"name": "Monospace Report",
"kind": "custom",
"refersTo": "typography.data",
"description": "The four-second answer the whole product is built around. Columns are padded in the code that generates it, so the alignment is the layout and the monospace font is structural. Mixes a warning, a table and prose in one flow, meant to be read whole.",
"html": "<div class=\"ds-report\">planned in 4.1 s\n\nclass share at a divide\n lowland 61.4% 1.8&deg; plain\n upland 22.7% 10.2&deg; rolling\n 16% massif, rest 0.017 mm/yr 1.4&deg; plain\n range 9.1% 28.6&deg; mountain\n\nthe seed re-rolls, the painting does not\n rock 5 types over 180.0 km provinces\n faults 38 traces on a 60 km grain\n\n26 regions, 118 min of solve estimated</div>",
"css": ".ds-report { white-space: pre; font: 11.5px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; color: var(--dim, #8b95a1); }"
}
],
"narrative": {
"northStar": "The Instrument Panel",
"overview": "terrain studio is a gauge with a world in it. The left rail is calibrated measurement — a rate typed in mm/yr comes back as the hillslope angle it buys, a brush size comes back as metres on the ground, a bake comes back as minutes — and the rest of the window is the specimen those numbers describe. Everything in the chrome is either a reading, a control, or the hairline that separates the two. There is no third category, and nothing in the frame exists to be looked at.\n\nThe room is dark because the work is coloured. Every hue that means anything in this product belongs to the painting: a class colour is an uplift rate, a ramp is a field in mm/yr, a region colour is a landmass. So the instrument around it is made of iron and ink, with exactly one warm colour — brass — reserved for the two things the machine can tell you about itself: what you can act on, and what is currently live. A second hue in the chrome would be a colour that looks like data and isn't.\n\nIt is patient, and it explains itself. Under a control that takes a number nobody has an intuition for sits an 11px note in graphite saying what that number does to the ground, and the notes say why rather than what — \"a mark you cannot see is one you will paint over\". That voice is part of the system, not commentary bolted to it. The tool assumes a reader who will read, and it is written for someone who opens it every few weeks rather than every day.",
"keyCharacteristics": [
"One monospace family, three sizes (13px / 11.5px / 11px), no display type anywhere.",
"A fixed 300px rail against a fluid canvas; the canvas absorbs every pixel of a resize.",
"Flat by doctrine: not one box-shadow in the system. Depth is tonal value plus a 1px hairline.",
"Brass (#d6a03a) is the only warm colour and never describes data.",
"State changes are instant. The single animation in the product measures something that actually takes time.",
"Monospace alignment is load-bearing — reports are white-space: pre and the columns are the layout."
],
"rules": [
{ "name": "The One Warm Thing Rule", "body": "Brass is the only warm colour on screen. It marks what you can act on (filled) or what is currently live (outlined), and it never describes data. If a new element needs attention and is neither of those two things, it does not get brass — it gets a note.", "section": "colors" },
{ "name": "The Data Owns Colour Rule", "body": "Hue inside the canvas belongs to the painting, the legend and the map ramps. The chrome is iron, ink, and one accent. Nothing in the frame may be a colour that could be mistaken for something the solve will read.", "section": "colors" },
{ "name": "The Ladder Rule", "body": "The neutrals are ordered by depth, not by taste. A new surface picks its value by answering whether it sits below the panel (a well), on it (a row), or above it (a control). There is no free choice of grey.", "section": "colors" },
{ "name": "The Alignment Is Layout Rule", "body": "Reports are white-space: pre, and their columns come from padEnd/padStart in the code that generates them. Never rebuild one as a table, a grid, or flex rows. The monospace font is structural here, and switching families would silently break every report in the product.", "section": "typography" },
{ "name": "The Lowercase Rule", "body": "Headings are lowercase in the source and uppercased by CSS; labels and buttons are sentence case at most. Nothing in this interface is Title Case. The one exception is a proper noun the domain already capitalises.", "section": "typography" },
{ "name": "The Rail Is Fixed Rule", "body": "300px, never a percentage, and number inputs are 78px. A label and its value must not reflow when the window changes size; the canvas absorbs the entire delta. An author resizing the window is looking at the world, not at the panel.", "section": "layout" },
{ "name": "The Canvas Is Never Chrome Rule", "body": "Nothing in the stage has a background, a border or a panel of its own except the two floating readouts, and both are translucent. The world goes edge to edge.", "section": "layout" },
{ "name": "The No-Shadow Rule", "body": "No box-shadow, no filter: drop-shadow, no glow. If something needs to separate, it changes value or gains a hairline.", "section": "elevation" },
{ "name": "The Two-Direction Rule", "body": "A surface declares its role by which way it moves off the panel: down means you type into it, up means you press it, level means you select it. A new surface that is the same value as the panel is undecided, and undecided is a bug.", "section": "elevation" },
{ "name": "The Scrim Rule", "body": "Anything floating over the canvas is darker than the panel and translucent (#0d1014dd–ee). It never becomes opaque and it never gets lighter, because the point of a floating readout is that you can still see what it is describing.", "section": "elevation" },
{ "name": "The Reserved Border Rule", "body": "Anything that can become selected carries border: 1px solid transparent at rest, so selection changes colour and never geometry. Nothing in this rail is allowed to shift, grow or reflow when you click it.", "section": "shapes" },
{ "name": "The Square Chip Rule", "body": "A colour sample is a 3px-radius square with its own dark edge, at 18px in the rail and 12px in a key. Never a circle, never borderless, never a swatch of the class colour used as a background behind text.", "section": "shapes" }
],
"dos": [
"Do use filled brass for the one action in a group and outlined brass for the one item that is live. Never both in the same role.",
"Do reserve border: 1px solid transparent on anything selectable, so selection never changes geometry.",
"Do put an 11px graphite note under any control whose raw value has no obvious consequence, and write it to say why. The note is part of the control.",
"Do report a number back in the unit the author thinks in — degrees, metres, kilometres, minutes — beside the field that takes it.",
"Do publish a key with every map, generated by the code that drew the map, so the two cannot drift apart.",
"Do pick a new surface's value off the ladder by asking whether it sinks, sits or rises relative to the panel (#11151a / #1b2027 / #262d36), and give it the 1px hairline.",
"Do keep reports in white-space: pre with their columns padded in code.",
"Do disable a control that cannot apply and give it a title saying what would enable it, rather than hiding it."
],
"donts": [
"Don't add a box-shadow, a glow or a drop-shadow anywhere, at any elevation.",
"Don't add a transition to a state change. Hover and selection are instant. The only animation in this system is the bake progress width (.3s), and it earns that because it measures a two-hour process.",
"Don't introduce a second font family or a fourth type size. 13px / 11.5px / 11px in one monospace stack is the entire scale, and a family change would break every column-aligned report.",
"Don't put a hue in the chrome. Colour in this product means an uplift rate, a depth, a rock type or a region; a decorative hue would be indistinguishable from one of those.",
"Don't use brass on anything that is neither actionable nor live — not on a heading, not on a value, not to emphasise a word in a note.",
"Don't let a control change size, padding or position between states.",
"Don't round anything past 4px, and don't make a colour sample a circle.",
"Don't leave a control with only the browser's default focus ring. Number inputs currently do, and it is the one gap in the hairline system: a keyboard user gets a focus treatment this design never specified.",
"Don't ship a second letter-spacing for the label role. The map-key title is at 0.08em against the section heading's 0.09em; that is drift, and 0.09em is the value."
]
}
}
+317 -9
View File
@@ -38,7 +38,23 @@ The editor serves the engine's MCP plugin on `127.0.0.1:8000/mcp` while it is op
Claude Code at it. Reconnect with `/mcp` once the editor is up. Close the editor and its Live Coding console
before a shell build.
Off the ladder: `Content/Maps/L_World`, a ~200 km² world-partitioned landscape, is a product of three inputs and
Off the ladder, and **[`Docs/World-Pipeline.md`](Docs/World-Pipeline.md) is the map of it**: what order a
painted world map becomes ground in Unreal, which of the three routes is actually live, what every artefact is
and whether it is tracked, and the traps. Read it before running any of this; the rest of this section is the
reasoning behind it.
**`Content/Maps/L_World` is the one world level, and it is the planet-map region (D-72).**
There is no `L_Region` any more: a second world level was a second thing to keep dressed, lit and in step,
and the region always was the world. `Region.json` says which level it builds, the tile PNGs are named after
it, and `build_region.sh` reads the name out of the manifest rather than carrying one of its own.
What follows describes the numpy pipeline, which now builds `L_Canvas_Proto` rather than `L_World` — legacy
by D-47 and kept only because it is still the one path that carries the erosion pass's flow, wear and deposit
maps into Unreal. `World.json`'s `level` is named apart from the real world deliberately: `create_world.py`
empties whatever level it is handed, so a manifest still pointing at `L_World` would replace 98 landscapes
with a 14 km square on one run, without a prompt.
`Content/Maps/L_Canvas_Proto`, a ~200 km² world-partitioned landscape, is a product of three inputs and
nothing else: the manifest `RawContent/World/World.json` (size, what a heightmap value means in metres, the
height source, the paint-layer rules), the PNGs `Scripts/Authoring/generate_heightmap.py` writes from it into
`RawContent/World/Heightmaps/`, and `Scripts/Authoring/create_world.py`, which imports them through
@@ -55,19 +71,292 @@ it before touching the generator, and the numpy pipeline is not to be extended.
The Go core now exists and works: `Tools/Terrain/` builds an uplift *rate* field and lets a Braun-Willett
stream-power solve produce the terrain from it, with a Roering nonlinear hillslope law and per-uplift-class
statistics. The coast is a pass of its own now (D-51, `internal/coast`), running after the solve: a continental
statistics. The coast is a pass of its own (D-51, `internal/coast`), running after the solve: a continental
shelf whose width follows the relief behind the shore, a surf that planes a shore platform and leaves a cliff
where its reach ends, and a sediment budget carried along the shore into the bays and out of the river mouths.
It stops at the geology grid, so the detail passes are unbuilt and `L_World` still comes from the numpy
pipeline. **[`Docs/Terrain-Next.md`](Docs/Terrain-Next.md) is the working brief** — how to run it, what
each output map is for, what still looks wrong, and what has already been measured and rejected. Start there.
Where it is going: finish the detail passes so there is a full-resolution output at all, then make the source
a **painted map** — an author paints the uplift, the mask and the erodibility, never the height, and the solve
turns that intent into terrain with real drainage. Worlds are far larger than today's 14 km canvas, so the
geology solve stays whole (it cannot be tiled: drainage area is global) and only the detail passes tile. That
makes two rules binding on anything written now: index every noise and hash by **absolute world coordinates**
rather than grid index, and give every tiled pass an overlap margin. Composition tuning is parked.
**The source is a painted map now (D-53).** An author paints a flat cylindrical world map; a JSON legend
beside it says what each colour means in uplift mm/yr and erodibility; the simulation makes the terrain.
**And a class is two rates, not one (D-55).** `n` is 1, so a class's uplift rate *is* its hillslope angle, and
one rate over a painted blob is therefore one landscape over it - the first planet's continents came out
uniformly dissected from the waterline to the summit with no flat ground on them anywhere. A class now carries
`massif: {floor_mm_yr, fraction}` and cuts one planet-wide upland fabric, so a painted lowland is a plain with
hill masses standing out of it. The threshold is a quantile of the **planet**, never of the region.
**And there is a tool for the painting now (D-56):** `terrain studio` serves a painter on `127.0.0.1:8099`
where the brushes *are* the legend's classes, the panel shows the hillslope angle each rate buys as you type
it, the canvas wraps at the seam, and `plan` is a button. It saves by patching the *text* of the legend and
the manifest, so their commentary survives. **Its canvas is a GPU texture (D-61)**, because the 2D one put
all twenty-nine million cells through `putImageData` and a whole-image downsample on every pointer event -
105 ms an event whatever the brush touched, against 6 ms flat now. A stroke uploads only its own rectangle
(`UNPACK_ROW_LENGTH`, no copy), drawing is one quad, the seam is `REPEAT`, and one rAF loop owns the screen.
Do not put per-event work back on the input path, and do not reintroduce a second copy of a sheet in a
canvas: `full` and `ovFull` are the authority and are encoded only when pushed. **`ctrl+z` undoes a stroke**,
which is copy-on-write over a 256 px tile grid recorded at the top of `stamp` - the only writer, which is
what makes it complete. Anything new that writes to a sheet goes through `stamp` or records its own tiles
first, and wraps a rectangle into runs *before* cutting it into tiles: the grid does not line up with itself
across the seam, because 7738 is not a multiple of 256. The painted waterline is also roughened before projection now -
`coast_jitter_px` had been in the manifest since D-53 with nothing reading it - by adding fractal noise to the
signed distance from the shore rather than by warping the painting.
`RawContent/World/Templates/` holds the first one and its README is how to paint another;
`RawContent/World/Planet.json` is the planet's own manifest and `World.json` beside it — the square 14.28 km
canvas the numpy pipeline still builds — is untouched by any of it. `terrain plan` reads the painting and cuts
the planet into regions in four seconds without eroding anything; `terrain bake` solves it, about two hours at
100 km round. **Paint the uplift, never the height:** a solve handed a painted surface erodes it into
something else and throws the drainage network away, which is the reason the generator exists.
**And the overlay can be proposed from a bake now (D-68).** `terrain overlay` reads a finished bake and
fills the annotation layer in: woodland, settlements and the roads between them, which are all consequences
of ground an author cannot see while painting. **It never touches a painted pixel** - it fills blanks, so
generate, move the towns, regenerate is a loop that runs both ways - and it is **opt-in per mark** through a
`generate` block, so a legend without one produces the blank sheet it always did. Roads are a minimum
spanning tree over least-cost paths with water impassable, so each landmass gets its own network; forest
takes its treeline from a quantile of the land's own heights, because metres mean nothing until a world is
baked; `not_classes` keeps marks off the ice caps. The `coast` kind is supported and deliberately not
shipped enabled, because `coast_jitter` is the one overlay property a pass reads and generating it would
move the next bake's coastline everywhere. It proposes and does not decide: there is no economy, no history
and no climate in the Go tool, so "where would a city be" is answered with drainage, slope and distance to
the sea.
**And generating it is a button now (D-70).** The studio's overlay tab has **Generate marks**, and because a
button is pressed repeatedly rather than once, every press is a fresh seed and each draft *replaces* the last
rather than piling on it: the server remembers exactly which pixels the previous generation put down and
clears those alone, so hand-painted work is never in the set. It runs before the first bake too, on the
painting alone - flat height, no slope, drainage nil rather than zero - and says which of the two ran, because
a sketch that knows nothing about rivers must not be mistaken for one that does. A bake made from a
*different painting* is ignored with its reason printed: `CheckBake` compares only how a heightmap is
encoded, and two paintings of one planet agree on every one of those numbers, so nothing else would catch it.
**And there is a second painting now (D-57), which is not geology.** An *overlay* the same size as the
template, registered to it, painted in the studio's other tab (`o`), carries **marks** rather than classes:
forests, settlements, roads, and stretches of coast. Blank on it is alpha, never a reserved colour, and an
opaque pixel matching no mark is dropped and counted rather than snapped to the nearest — the class legend's
rule inverted, because most of an overlay is nothing. Exactly one mark property is read by any pass:
`coast_jitter` scales the waterline roughening per pixel, so a coastline drawn deliberately is pinned with a
brush stroke (`0`) while the rest of the world is still roughened, and a fjord coast is the same knob above 1.
Everything else is **inert and travels**: an 8-bit mark index beside every detail tile, and `overlay.json` with
every feature in world metres — a centre, area and extent per blob, an ordered polyline per path, because what
UE builds from a road is a spline. Nothing in the generator reads any of it back. `RawContent/World/Templates/
Map3.overlay.json` is the starter legend.
**A painting is a composition and the seed re-rolls what it does not fix (D-58).** One painting is many
worlds: the same seed change that moves the massifs, the swell, the initial relief and the coastline detail
now also moves the **rock** and the **faults**, neither of which existed on the painted path before. Lithology
is one planet-wide rock field cut into `pipeline.lithology`'s types at a quantile of the *planet*, multiplying
each class's own `k_mult` by its `lithology_mix`; before it, `map_erodibility.png` was a recolour of
`map_class.png`. Faults are a per-class `faults: {per_1000km2, throw_m, length_km}` - a rate difference across
a line, steep one side and gentle the other, which erosion carves into a scarp - drawn once for the planet in
world metres and filtered per region, so a fault crossing a region boundary is one fault. Neither could be
ported from `uplift.Build`: its lithology takes a percentile of the grid it is handed and its fault centres
are fractions of that grid, which is what D-53's decomposition forbids. `--seed` is on `plan`, `bake` and
`tiles`, and the studio has a Re-roll button. Measured: seed 7 against 9342 on one painting moves 13.8 % of
the uplift map and 24.9 % of the erodibility map.
**And a fault was a welt with a cliff down the middle (D-62).** Raised as "each fault line makes a rough line
of mountains that just doesn't look realistic", and it was not a legend number set wrong: `per_1000km2`,
`throw_m` and `length_km` say how many, how long and how much, and nothing said what *shape* a fault is. That
was two constants with a step between them - the whole throw one side of the trace, the whole throw negated
the other, one 8 m cell apart, which is **89 degrees**, inside an upthrown flank only 600 m wide. Both halves
are unsolvable and for the same reason the painted path exists. A discontinuity in the *rate* is a painted
cliff, so the trace facets at any throw and turning `throw_m` down only lowers the artefact. And 600 m is
narrower than one hillslope - the drainage density puts a divide 1.1 km from its channel - so no valley can
cut it and the profile is **printed** on the surface instead of being eroded into a landform, which is why
every fault in a bake read as a smooth ruled ridge through terrain dissected everywhere else. The profile is
continuous now, zero *on* the trace, with a 6 km footwall and a 4 km hanging wall, and the along-strike throw
is a bell rather than a flat top over the middle two thirds, so the cross-section is never extruded.
**`throw_m` is the step across the fault now, not a full throw on each side**, so a legend written before it
asks for half what it used to get. Measured on a 400 m throw: the steepest cell in the rate field went from
89 to **17.4 degrees**, and solving the same landscape for a thousand steps, the old profile left a
ruler-straight cliff with a dead apron at 43 m of relief - *less than the 45 m with no fault at all* -
against a dissected range front at 97 m.
**And widening it made a *set* of them stack (D-63).** D-62 was verified on a region two traces reach, which
is the wrong place to verify a width change: `FaultDelta` accumulates with `+=`, harmless at 600 m because
two faults almost never met, and at 6 km they meet constantly - and a fault set is sub-parallel *by
construction*, since traces inside one cell of the orientation grain share a strike. On a 22 km landmass
crossed by 13 traces, **75 % of the faulted ground had two or more faults on it**, the sum was a median
1.77x the largest single contribution, and the repose clamp fired on 4.1 % of the region against 0.15 %
planet-wide before - which is how the mountains came to read as "streaking horizontally like someone just
cut them apart with a knife". The bound is a soft knee per cell keyed to the **largest single contribution
there** (a planet-wide throw would not bite: 744 m biggest throw against a 209 m biggest contribution):
identity below it so a lone fault is untouched, bending onto 1.6x above it. Continuous and
frame-independent, and it runs unconditionally or a cell's value would depend on which frame asked.
**The other half was the initial relief reading the finished rate**: `painted.go` scaled the
symmetry-breaking noise by `rate/maxClassRate` with the fault delta in it and unbounded, so a 6 km footwall
stamped ~166 m of ridged fBm on a landmass whose whole relief was 221 m - and initial relief the size of
the landscape is not a symmetry-breaker, it is the landscape. Pre-fault rate now, bounded at one. Measured,
same region, same seed, same steps: max **221 -> 116 m**, repose-clamped cells **160 289 -> 2 865**, median
slope **6.3 -> 1.8 deg**, slope-area exponent **-0.94 -> -0.60** against a theoretical -0.50. The corduroy
is gone; the residual flank ribbing is Terrain-Next 4.B3, predates D-62 and is now diagnosed there.
**`terrain plan` prints two angles per class now, and the second is the one to read.** `divide` is
`U/(K·A^m)` at one cell — exact, and the *steepest* ground a rate can make; `typical` is the median over the
class, measured at a third of it in tangent. Almost none of a map is divide, so the old single column is how a
legend gets set two or three times too hot. **And `preview.png` lies about scale the same way**: its
hypsometric ramp tops out at a percentile of the world being drawn, so a 47 m lowland continent gets the
bare rock and snow a 2800 m range would. `palette.land_top_m` is an absolute ceiling in metres and every
run now prints which ceiling its preview used. Measured: the real 45.9 x 19.8 km lowland bakes to
**0..47 m with a 0.61° median slope**, 4.4 % of it over three degrees and nothing over eight - a plain,
and it always was one. What makes one read as hill country in the *numbers* is its massif `fraction` and
the rate the massifs reach, both of which are the author's.
Two rules govern anything written here. **The geology solve is decomposed per landmass, exactly** — ocean
cells are fixed at sea level and nothing in the solve can move them, so no flow path crosses open water and a
landmass in a box of its own gets the same answer as the whole planet would. Everything else, the coastal pass
included, runs once over the whole cylinder: it costs 26 ns a cell against 80 ns a cell *per step* for the
solve, and cutting it up would truncate the fetch across every strait and split the sediment budget.
*Decompose the solve, not the map.* And **index every noise and hash by absolute world coordinates** rather
than by grid index, through `noise.WorldUV` and a `world.Frame` — done for the router and the painted path,
still to do for the detail passes, which are the only thing that tiles.
The detail passes are built and tiled: `terrain tiles` runs passes 8 to 12 and 14 over a bake and writes 5 km
tiles of 2500 samples at 2 m, about twelve seconds each, with a hillshade and the flow, wear and deposit maps
beside each one. Their margin is measured rather than reasoned - three droplet lifetimes - and the droplets
spawn from a hash of world position so a tile's interior is what it would have been in one whole run.
**And a planet can be judged now (D-59).** `internal/stats` sorted a copy of every land cell, so a bake
printed its elevation range and nothing else - no slopes, no per-uplift-class breakdown, no drainage density.
Fixed-bin histograms replace the sorts and `field.LocalRelief` the O(radius²) window, but the property that
matters is that a histogram **adds**: a planet's statistics are pooled from its regions, exactly, where a
median of medians would not be. Each region accumulates while its grid is alive and they merge in region
order; the *extent* is measured once on the composited cylinder, because regions carry overlapping ocean
margins. A partial bake says PARTIAL rather than letting the whole world's extent be read against three
islands' ground. About 120 ns a cell, so a planet is a few seconds at the end of a two-hour bake.
**And a planet has a shore now (D-60).** `internal/coast` ran on a flat grid, so a bake laid the painted sea
floor and stopped: the land met the painted ocean depth in one cell, with no shelf, no surf platform, no beach
and no exposure anywhere. Four primitives wrap now - `boxBlur`'s running sum, `fetch`'s ray march,
`shelfWidth`'s inland march and the distance-field gradient both marches steer by - and the test for it is
that the same island in two places is the same island, not that the seam looks right. **The abyss is a field**,
because a painted planet's sea classes carry their own depths and one global number would step to the painting
wherever they disagreed; it also means the break can never be deeper than the water it is a break in. Memory
was the other half: `Geometry.Ref` holds a waterline *slot* rather than a cell index, so the sediment supply is
a few hundred thousand entries instead of 608 MB, and `Measure` holds one distance transform at a time. 7.9 s
over the whole 76 M cell cylinder; the seam step in the sea floor went from a mean of 9.1 m to 0.32 m, which is
what an ordinary column is. The pass is now the memory peak of a bake, about 8.3 GB against the solve's 3.6.
**And the ocean was thirty metres deep (D-64).** Reported as "it is just a landmass and no oceans really",
against the exported heightmap. The legend paints `ocean` at 512 m over 56 % of the planet and 17 % of it got
there; 40 % of the world was water between 0 and 30 m, which at the manifest's `-1024..2048` encoding is 1 %
of the 16-bit ramp from sea level, so the shelf and the land came out the same grey and every landmass's
shelf halo fused it to its neighbours. The depth at the shelf break is one number, and both call sites took
it from `pipeline.continent.sea_floor_m` - the **square canvas's** block, whose own comment says −30 m is not
a shelf break because a real one does not fit on a 14.28 km canvas. It is `pipeline.coast.break_m` now, 130 m
on a planet and the old reading on the square canvas, which is unchanged. What hid it is arithmetic: the
derived margin reaches `shelf_km.hi() + slope_km` = 4.6 km from every shore, and 1069 km of shoreline against
a 3111 km² sea is more margin than there is ocean, so no strait ever reached the painting. **The margin's
width is right and was never the problem** - 512 m over 4.6 km is a 6.3° continental slope. The profile is
monotone at any break depth, so every profile test passed; this had to be found in a histogram, and a bake
prints its sea floor's three numbers now. It also prints **how much of the 16-bit ramp the world used** - 28 %,
with land at 7 % - because too *wide* an `elevation_m` clips nothing and `clip_fraction` only ever catches one
too narrow. **The whole-planet re-bake was killed by memory pressure and its numbers are still owed**; what is
measured is the mechanism at unit scale, 33 % → 8 % of a painted 512 m sea left shallower than 50 m.
**And the mountain flanks were laser-carved, which was D8 (D-65).** Reported as "streaks going down the
side of the mountains", and `Terrain-Next.md` 4.B3 had the wrong cause: it blamed the ridged-fBm initial
relief and dismissed grid locking because the ribs are *oblique*, which is not a counter-indication - D8's
parallel-flow grooves run in whatever direction the slope faces. The grooves are **channels**: in
`map_flow.png`, absent from `map_uplift.png`, and the network is **pinnate** - ruler-straight parallel trunks
with barbs at a fixed angle - with 25 sources and **no confluences** in a 12.8 km window. What settles it is
that the pitch is **18 cells in both the 32 m bake and the 8 m one**, and everything fixed in metres would
have changed by four. Isolated with no erosion at all, on a planar ramp where the correct specific catchment
area is the same along a contour: D8 gives the most-drained cell **769x the median** and leaves **29.5 % of
the grid draining nothing**, because every cell picks the same steepest neighbour and the flow lines never
converge. Drainage area is **Freeman MFD** now - `Accumulate` only, D8 receivers kept because Braun-Willett
walks one receiver chain - ordered by **Kahn over the flow graph, not by elevation**: exact, O(n), and a
bucket sort would leak every lake bed's area, since the queue quantises to a centimetre and the flood's
epsilon ladder is a millimetre a cell. 769 -> **1.34**, 29.5 % -> **0.4 %**, float32 is enough (2.4e-9 over a
closed basin), and it costs **101 ns a cell against D8's 17**. `pipeline.fluvial.mfd_exponent` is 1; 0 is the
old behaviour, so the A/B is one flag. Three repairs alongside: the repose clamp jitters its pop order and
allowance (the residual octagon on a clamped cone is the octile metric, not the order, and is irreducible);
`DiffuseNonlinear` is the isotropic **nine-point** stencil, because the clamp cuts across eight faces and a
five-point smoother cannot transport across a diagonal one at all; and `field.SmoothEdgePreserving` is ported
from the World Orogen browser generator, **off by default**, and measured against its own acceptance gates
it fails them: two passes at `slope_ref` 0.3 move the slope-area exponent from -1.02 to +0.36 and the
mountain median slope by 3.7 degrees, and one gentle pass at 0.02 still lands at +0.16. It is a filter, not a
polish, and it is for somebody who decides in a file that they want the look more than the statistic. That generator routes single-receiver too; what keeps its flanks clean is that it solves on an
irregular Voronoi mesh with no lattice directions, which a raster pipeline cannot adopt. **Do not fold a
pass's precondition into another pass**: the Kahn counts were first computed in `ComputeReceivers` for free,
and because the walk consumes them a second call returned a silently wrong area that only a benchmark caught.
**And the painting has a browser twin (D-66).** `Tools/Orogen/` is World Orogen, the GPL browser planet
generator D-65 borrowed its smooth from, and its import page now has a **Painted Map** source that reads the
*same* painting and legend `terrain plan` does and solves them on its 204K-region sphere mesh in about
twelve seconds: the same classify, vote, stroke, coast, massif, rock and swell steps, then a Braun-Willett
solve with the ocean fixed, then Orogen's own climate, satellite view and exports - plus class, uplift,
erodibility, drainage, slope and basin layers. It is for looking at a painting's rivers on a globe in half a
minute and tuning the legend's numbers in a table before a two-hour bake; it is not ground a player can
stand on, and it carries no faults, craters, plates or detail passes, because a 44 km cell cannot
show them. The legend is the contract between the two tools: new keys optional, nothing renamed. Serve the
folder with any static server (`npx serve Tools/Orogen`) and open `/import`.
**And the twin reads the planet's own files now (D-67).** Three things carried across, each one a question an
author has *while typing a number*. The class table prints the **typical** hillslope angle a rate makes and
what it reads as, with the divide angle in the tooltip - `plan.go`'s functions ported exactly and checked
against `plan.json` to 1e-9 - because the divide is the steepest ground a rate can make, almost none of a map
is divide, and reading it as the landscape is how a legend gets set two or three times too hot. The
**overlay** is carried as a *texture* over the globe and the map rather than voted onto the mesh, since a road
is 8 px and a region there is 44 km; marks are voted for one thing only, `coast_jitter`. And `Planet.json` is
read directly, outranking a legend's own `planet` block because it is the file the bake reads. `terrain
studio` serves all of it: `share.go` sets the CORS header on **GET and HEAD alone** and answers no preflight,
so **Load from studio** brings the whole planet in four seconds, including strokes made since the last save,
and no browser tab can ever paint, save, plan or bake. The two tools are not interchangeable and the twin is
not on a path to replace the bake: the 8 m geology grid, the coast pass, the faults, the craters and the
detail tiles are the Go tool's, and only they produce ground a player can stand on.
**And there is ground in Unreal now, 900 km² of it (D-69).** `L_Region` is a *window cut out of a finished planet heightmap* and laid out as a grid of landscapes - 6 x 6 tiles of 2551 vertices at 200 cm, 10 x 10 components of 255 quads each, **30.60 km a side, 936 km² of map holding 905 km² of land**, built one batch at a time by `Scripts/Authoring/build_region.sh`. Its contract is `RawContent/World/Region.json` and `RawContent/World/README.md` is how to move the window. Today's source is the Orogen twin's export, and three things about it are the whole design. **The scale is a choice, not the planet's**: the export carries none, and at `Planet.json`'s 100 km circumference the planet is 31.8 km across, so a flat 30 km square does not fit on it at all; 22.583 m a pixel is a 185 km circumference, the finest reading whose best window still clears 900 km² of land. **The map is read flat**, which at latitude -24 stretches the ground 9.6 % east-west - cheaper than any projection over a patch two thirds the width of the planet. **The heights are Orogen's normalised metres, not the generator's**: `terrain bake` makes this same continent a 116 m plain and the export makes it 2972 m, so the relief is art. The source resolves about 200 m, so below that the ground is smooth and no upsample can invent what is not there - **the detail is still the Go tool's**, and when `terrain tiles` replaces the window as the source, `generate_region_tiles.py` is the only thing that changes. Seams are exact rather than blended, because every vertex is sampled from its global position in the window. Do not run a landscape import for the whole grid in one process: a hundred components cost about a gigabyte the editor never gives back, and thirty-six tiles in one run reached 14.7 GB by the ninth. Do not pass a script's arguments after `-script=` either - they go inside the quoted value, and the engine silently eats them otherwise.
**And Orogen exports that tile set itself now (D-71).** The Python cutter exists because Orogen gives two
things Unreal can import neither of - a picture, and one flat 8192 x 4096 PNG with **no scale on it**, which
is what forced `metres_per_pixel` to be a number somebody chose. The browser twin's export card now has
**Unreal Landscape…**: it renders a window of the planet straight into the tiles the importer wants - a
16-bit height at 255*N+1 vertices and three 8-bit weightmaps per tile, plus the `Region.json` describing
them - into a folder picked through the File System Access API, so the files land in `RegionTiles/` and
`create_region_world.py` reads them unchanged. **The window is sampled once and then cut**: every tile is
resampled out of one float raster by its *global* vertex position, which is what makes a shared column
bit-identical (measured: 0 of 1021 vertices differ on either seam, heights and paint). A tile rendered under
its own camera would put a rasteriser's luck on every seam, where one 16-bit step is 11 cm of crack. The
paint closes only because tiles carry a one-vertex margin for the slope's central difference - D-69's numpy
defect, met again in JavaScript. **The panel is for the number you have not typed yet**: it re-plans on the
keystroke and prints ground, components, files, sample spacing and what the flat reading costs, and it
immediately said what this project had not: **936 km² on a 100 km planet is 29 % of the entire globe**, a
window 110 degrees on a side stretched 74.7 % at its edge, because a 100 km circumference is a 3183 km²
world. The projection is cosine-corrected at the centre latitude so the error splits between the two edges;
the defaults are 4 x 2 tiles, 208 km² at 5.4 %. **It does not make ground finer** - the mesh resolves a
couple of hundred metres and no sampling invents what is not there - so `generate_region_tiles.py` stays,
because it is the path that will read `terrain tiles`. Two traps worth keeping: never `<input type="number">`
for a decimal (it parses in the browser's locale, so a comma-decimal machine turns 0.17 into NaN and writes
a tile set of nothing), and a build now **refuses to start when the editor holds the level** (D-71a) -
`--rebuild` empties first and saves last, so a lock detected late is indistinguishable from data loss.
**And the ground is collected in one place now (D-69a).** `Content/Terrain/` is the project's own ground, copied out of the asset packs by `Scripts/Authoring/collect_terrain_assets.py` from the list in `RawContent/Terrain/ground.json`; adding a substance is a line in that file and a rerun. A copy is not ownership - a duplicated material function still samples the pack's textures, because the reference is inside the graph - so the copies are repointed at each other and the asset registry is then asked what still points outside the folder. The packs are never modified. Elite_RockyMeadows' sky kit stays separate in `Scripts/Authoring/rocky_meadows.py`, which both `create_world.py` and `create_region_world.py` now dress from.
**And there is a map of it now, in the game and in the editor (D-73).** The three candidate sources were one:
`Region.json` imports the **whole** Orogen export with no crop, so `L_World` *is* the planet map and
world-to-map is `u = (X/100 + 35700) / 71400` — one multiply and one add per axis. There is **no scene capture
and no render target**, and a capture would have been wrong rather than merely expensive: the level is
world-partitioned, so a capture only ever sees the streamed-in region, and a map is the thing that must show
ground nobody is standing on. `FWorldMapProjection` in `SaltyCore` is the whole transform, pure and tested;
`SWorldMap` is **one Slate widget with three ways in** — `UWorldMapWidget` for UMG, the editor's **Window →
World Map** tab, and `bs.WorldMap` straight over the viewport — because the editor tab has no `UWorld` at all
and anything needing one could not have been shared. A whole cylinder **wraps**, so a view across the seam is
two draws of two parts of one image and `WrapU` uses floor rather than `FMath::Fmod`, which keeps its
argument's sign and would leave a westward pan sampling nothing. The art is `Tools/MapArt`, a Go tool, because
the engine's Python cannot decode 33 megapixels of RGB — `heightmap_io.py` is greyscale-only and unfilters a
byte at a time — and it renders four 4096 x 2048 layers in six seconds from `RawContent/World/MapArt/layers.json`.
The default layer, `relief`, is **derived rather than exported**: hillshade and a hypsometric tint off the same
heightmap the landscape was cut from, so the map and the ground are the same shape by construction, where a
colormap shows biome and a player needs landform. Nothing inside a PNG says which planet it is, so `mapart
check` measures land/sea agreement against the heightmap instead — the pasted `orogen-colormap-14733759` scores
97.5 % against `orogen-heightmap-7945` and is the same world; Orogen numbers each *export*, not each planet.
The definition's projection is copied out of `Region.json` and never typed, because a map that disagrees with
the landscape about how big the world is is the one bug here that still looks like a plausible map. Overlay
marks are not drawn yet and transfer by the same normalised coordinates when they are.
Where it is going: the coastal detail pass, which the shelf and the shore platform have just unblocked.
Composition tuning is parked, and on a painted world most of it is the author's job now.
Next: step 3, the body, the camera, the input map and the gym. Step 1's PIE proof (two clients, dedicated
server, two pawns in the gym) is still a person's to tick. Update this section when a step closes.
@@ -80,9 +369,16 @@ The `.uproject` sits at the repository root, which is what the `.gitignore` assu
| --- | --- |
| `Salty.uproject`, `Source/Salty.Target.cs`, `SaltyEditor.Target.cs`, `SaltyServer.Target.cs` | The project and its three build targets. The server target needs a source-built engine (OD-04) |
| `Source/SaltyCore/` | Rules, data types, tags, the telemetry contract. Knows no `AActor`. Tests in `Tests/` |
| `Source/Salty/` | Gameplay: `Core/`, `Stats/`, `Movement/`, `Interaction/`, `Combat/`, `Crafting/`, `UI/`, one folder per feature. `Variant_*` is template reference (D-41) |
| `Source/Salty/` | Gameplay: `Core/`, `Stats/`, `Movement/`, `Interaction/`, `Combat/`, `Crafting/`, `UI/`, `World/`, one folder per feature. `Variant_*` is template reference (D-41) |
| `Source/SaltyEditor/` | Editor-only tools: `Authoring/` (the landscape library the Python scripts call), `WorldMap/` (the dockable map tab). May depend on `Salty`; never the reverse |
| `Content/<Feature>/` | Assets per feature: `Definitions/` for data assets, Blueprints, meshes, montages |
| `Content/Maps/L_Gym` | The movement test level, authored by `Scripts/Authoring/create_gym.py`. Never leaves the project |
| `Content/Terrain/` | The project's own ground: textures, layer functions, the landscape material and its layer infos, collected out of the packs (D-69a). Built by `collect_terrain_assets.py` from `RawContent/Terrain/ground.json` |
| `Content/Maps/L_World` | **The world.** Ground from a planet map, one landscape a tile (D-69, D-72). Built by `Scripts/Authoring/build_region.sh`; the contract is `RawContent/World/Region.json`, written by hand or by Orogen's Unreal export (D-71). Never run a build with the editor holding the level |
| `Content/Maps/L_Canvas_Proto` | The numpy pipeline's square 14.28 km canvas, legacy (D-47). Built by `create_world.py` from `World.json`. Named apart from `L_World` on purpose: that script empties whatever level it is handed |
| `Content/World/` | World dressing the project owns rather than borrows. `M_Sea_Proto`, the placeholder grey the sea plane wears; authored on demand by `rocky_meadows.sea_grey_material` |
| `Content/World/Maps/` | The world map's textures and `DA_WorldMap_L_World` (D-73). Built by `Scripts/Authoring/build_world_map.sh`; the contract is `RawContent/World/MapArt/layers.json` and the projection is copied out of `Region.json` |
| `Tools/MapArt/` | Renders the map's layers from the planet images: downsamples what is already coloured, shades relief from the heightmap. Go, because the engine's Python cannot decode 33 megapixels of RGB |
| `Content/Tests/` | Functional test maps |
| `Config/Tags/<Feature>.ini` | Gameplay tag source of truth, one file per top-level namespace |
| `Scripts/` | `run-tests.sh`, `build.sh`, and `Authoring/` for editor Python scripts. Run by hand; there is no CI. `UE_ROOT` overrides the engine path |
@@ -135,8 +431,20 @@ Scripts/run-tests.sh Core # a filter: Salty.Core.*
Scripts/build.sh Win64 Development # UAT BuildCookRun for the game target; add -server for the server target once the engine is a source build
D:/UE_5.8/Engine/Build/BatchFiles/Build.bat SaltyEditor Win64 Development -Project="<abs>\Salty.uproject" -WaitMutex # compile the editor from a shell
UnrealEditor-Cmd.exe Salty.uproject -run=pythonscript -script=Scripts/Authoring/create_gym.py # rerun an authoring script
Scripts/Authoring/build_world_map.sh # the world map: render the art, import it, write the definition asset
cd Tools/MapArt && go run . check # is every map layer the same planet L_World was cut from?
```
In the editor the map is **Window → World Map**; in game it is **`M`**, with **`N`** to switch layers - both
`DebugExecBindings` in `Config/DefaultInput.ini` onto `bs.WorldMap` and `bs.WorldMapLayer`, which is the
engine's own mechanism for binding a key to a console command and exists in development builds only. It needs
no Input Action, no mapping context and no asset, so it does not pre-empt step 3, where the real input map is
designed; when the map gets a HUD it gets an `Input.*` action and those two lines go away. Drag to pan, wheel
to zoom about the cursor, right-double-click to fit, **left-double-click to go back to the player and follow
them again** (`bs.WorldMapFollow` does the same). The map opens centred on you with an arrow, haloed so it is
findable at whole-world zoom, pointing where you are looking; panning by hand drops the follow deliberately,
because a map that snaps back as you let go of it cannot be read.
There is no CI, deliberately, and there will not be until there is a reason. Run the tests yourself before saying
a step is done. Every replicated step is also played in the editor with two clients, "Run Dedicated Server" on, and
network emulation at 100 ms and 5 % loss with `p.NetShowCorrections 1`.
+6
View File
@@ -7,6 +7,12 @@ CommonUI.CheckKeyboardFocusAndParentage=1
CommonUI.DisallowUserFocusedWidgetForPendingFocusRecipient=1
CommonUI.FallbackToDesiredOnAutoRestoreFailure=1
[/Script/Engine.AssetManagerSettings]
; Definition types the asset manager scans, one line per type as the step that defines it lands.
; WorldMap: UWorldMapSubsystem asks for these by primary asset id to find the map of the level it is in.
; Scoped to /Game/World/Maps rather than /Game so the scan does not walk the asset packs.
+PrimaryAssetTypesToScan=(PrimaryAssetType="WorldMap",AssetBaseClass="/Script/Salty.WorldMapDefinition",bHasBlueprintClasses=False,bIsEditorOnly=False,Directories=((Path="/Game/World/Maps")),SpecificAssets=,Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True,CookRule=AlwaysCook))
[/Script/GameplayAbilities.AbilitySystemGlobals]
; Gameplay cue notifies live with the feature that owns them; scanning all of /Game would walk the asset packs.
+GameplayCueNotifyPaths=/Game/Combat
+8
View File
@@ -84,3 +84,11 @@ DefaultTouchInterface=None
-ConsoleKeys=Tilde
+ConsoleKeys=Tilde
; Debug keys. The engine's own mechanism for binding a key straight to a console command, and it is the right
; home for a bs.* command: it exists only in development builds, it needs no Input Action, no mapping context
; and no asset, and it therefore does not pre-empt step 3, which is where the real input map is designed.
; When the world map gets a HUD it gets a proper Input.* action and this line goes away (Docs/Spec/UI.md).
[/Script/Engine.PlayerInput]
+DebugExecBindings=(Key=M,Command="bs.WorldMap")
+DebugExecBindings=(Key=N,Command="bs.WorldMapLayer")
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:56a9bd64c79ca30efbdb9ae4bc14cc0fdc3905f1be1cc8b88aa44b9a9f9a4bbd
size 1703
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d9109cf8374df0e1faf27a2565d22e182b8c40189ba4f45d26556d99ae00b366
size 3032
+1 -1
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3727f0a1a2026a9079672e697074883485e776b0a3b91d4ee5d884bccc455f8c
oid sha256:de51bf6cacf004286b8659b7fb5f80a8594a4bc5530d065cb7e480fa71550fb9
size 13642
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f315f8cd1aded77e306eccdf029e51b4179828a413bed5ac77dd3d68d39da21
size 1582
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2ac788957d1d1ee3631be52751a3fd41b7afe73983f340638aa4d5e7f29313a2
size 1318
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d09ef0ea04fd351cc9e6df6208b9113f0b293dc3c7e48f86dfb087afdf130e60
size 1306
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97b4dbef87cda9e7abeb8ec967fffdd0fc49a86a1683ff0dc954733ab9495f3e
size 1570
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72a6beb10eeea9ff93b207b38916bf3604b2f0a7ba7caac5e039d0a6cd1912d6
size 1570
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba718cad4c918442ebcfdb99f8e896228fd57794067b5a7e971afce8aee84809
size 1336
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d788e2bddb079dbfa74be145e1236ac801744777f611e452218f9f3881e7bcde
size 1312
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:75786263237558f4ee9c451fb24ca4589478c4a9576f0d69c21e82ffc30ac411
size 26016
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d677c726cc6fb446258e4038f76a6d26374b3fbb5c31ae9cc15e0f421b2f971d
size 25992
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ef0e9aea4f4f158cf449b9ea6e920883cbfae68f81f4b32d4c4b1e168e5b8dce
size 21520
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7b2360bec0c91ee8c08e4351f3168366ec988c4b443418dfac4b70d7f05cfe25
size 26052
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6dfdaffa2974ec8c9c153ac47ba3cd5d792a4966c5c4ecfb949e673e4393209f
size 21561
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:441ff125160d51a63ad63261f8602e3caef0807233912bdec22efce7ff2397c2
size 21511
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a6d9ca0739a806c91d06b1a6d34de38cef383d7b361b834726bc60bebf6e6ba
size 26004
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e45ba71e7e1207587159577ac5a6b26ae8a36de005e78cd6dcb1447f1aa0723c
size 29724
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:47bbce99fc0c4950c6f32995cb9bdb68d4b3d3b21cd07388a2e0595aca050205
size 78488
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eccd4fe0ac9a29f621bd7ed9223dedb99ae07488b65a58c6ffeec69b7b18bf90
size 43177917
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6dfec65b18b4ebb1acd43b5e53fec3d0ac2770af2d725453beaa8595223ed765
size 44875954
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e866258b60ad86dd2ab13a424d475f8cfc23be96a2e7ab97f7fcb316a4bd64e5
size 1543592
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:232b9578ef41bd94ad6c265e1cf942b1f656f84159f2f2b3f463c3d1489a6120
size 12063102
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:419e6d311bb7f1a2a8399b2ccf3d52ace5f30380b2d774a1f3e7451147e98e85
size 707175
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e2606a4414cd2067b1f090c13262e46a330066b11ab68a5577c2f96864dd7460
size 14444906
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5f182ace5ccb3282b110c182c499e3020670905d5ea62cddf8ff5e6df009b0c9
size 34620256
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d86c7aa316425405c20b1c4bb25597da000513f94d714273df0ceeb6af31710d
size 17414785
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0f7fd328156e27e873919e93331031afa04dba8d14bc060933b9285a15edcc96
size 1503513
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:52c03304d2df7539b16905575e93a323031c002dee39bf20cdcda6b43e65ac8d
size 11825994
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6d7729d789dc41940824621835092d5044cb862ebd91e3040ad5eb456d069caf
size 494132
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4bb8787c039d21061d6720aeff337eca3692dbabb4a2ba41c93efd939d66b5ed
size 4454815
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a89c3492805aab27199af8952fbcb6ab1bdb22d05dcfad2510c2a8c6068f0f86
size 1739485
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e62cbf73b5e49411b3a127a7ed89de00e838e4e894a3a631bb06b06e0b49cdb2
size 11585053
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:790f8a44374725073fdd03494d7724356f1067584cdcde56176bd9a0e5a2c386
size 601272
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:73e7bb8e01b10e1916b05a87582a7c51fae8afd324fc9ec754a3b4fda83138ed
size 1678961
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d92becc3fd0c701752f929fee095a7df0929fd1ab19ac86016deb3fff48f2be0
size 9587846
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d7ceefddd3f27dc689831373d289e41c4708a583e12d107c354b684c38c8eeb
size 727374
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b4998bef06ca02671d20d868fd56f04f6d7534a1f7a397307d6f7cbfd005159f
size 715986
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:163361ac93bf35896317eb24b257e88651d1abe36079245d71144bd45989f3b6
size 6537543
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:436318bd465f0d5627990c8f82b75227899b41e32c8798da4bdc6f0ad58a6606
size 1670999
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf40b80dddf703f87312b5abe659ca5ca7f2b676a599b80c93271f4e1d136e6a
size 55533
oid sha256:042d319942794510fd171a64f355926093f62941f91e49d4906e22a385f84bc8
size 55299
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c60c6d46acc2d20e7a025e6217756a555c916e76380369d5bae109b31778be7d
size 5240
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d9aa0779bb5889bbc5649e2390f966a745219f69881aa19579e72ec42309e82
size 3905
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3e72572ee58f9d2c08beaa8101f4465d17cfd2c1d173fca9fae8609c5b676241
size 544617
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c462acd5d7b13e8f59b530d75039ff8bd6dcd0d904b53769347d1cdb79b4c50d
size 2242096
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:78ce557bdb565926ddf7332e458a86fe51e18409640dbf41eb5fb12e6d5078a2
size 5661273
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c548ec3af6698e64ce8eaa4ce16a7409f6f527fbc1c35e13d6e295d66aa893f
size 2137305
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fff49bd4586003ffa4b79bc20f59a2eecac2fd8e770ef74c93d89e4a20e1c775
size 8496872
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:caf284c0ffd3184c2e6583235b55d14212538910e6c1421ac76361b745212f0d
size 376929
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:81e5a110cdf8aa9e1e6a4c4c598cceee6db0860a481cc8436f9d9e43a6f8f8f3
size 283713
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:814a2bca7be8211635f8d037448483ca85a232e1d813b71b693755e428ac4ce1
size 8250
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a7e56ee44963e29b972c646b18b5b1fa961903eb6a3497e42470d88d632ee8a7
size 502633
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a14c5d679fda651edde7b01cad398816555d3d71dfe455ebe70c9f0870a97a4
size 8241
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e7eda60da3a8b83fee6479e37fc48d53cee9df7f2540d9b47f74cdf6208c0195
size 6563825
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:91235e7a28bdfda2c43b6d0823b891c6d73b2745d6175a3864c78e9ed0ffb2cc
size 12610733
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:20af7d512e4e4e61f3c8e2e2068a201b7207134a269565c1721afe76fc802e48
size 10654236
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0299e50b7126f678a2ee5f1386cda5736dcc5b9436becebc2628f9933c76b6aa
size 570501
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c028d9d8265a67b778f7049eb6d5cabba5bdf8c23997939d7d65a6f63f08cb0a
size 6174
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6e7857fe93d3b039b49ae58514a45dad845590b20f8956c229e69666f269cb46
size 7954073
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:412144c32b128918ba90dea5fd3dc40b1228ea2d19fa4ffc97ec648b11aece32
size 5315080
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:69f7907ace75e4380ebabf1f437293a98f55012c6250f8e177ec0b0e92ec4e7b
size 5177400
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b11e2fc18acee89016351465083bf4c42399cb79bc76c6e006cd51c1b0488d09
size 5525151
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c9d293e2bad2f88566d8ef4adcda79c9f21c9a652c59af9dd86be1262aea6a66
size 596319
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1fccb4161943a8f7d8a1c1f2a6f43b0398d1802961309bd9d32a2f5aff134849
size 5520811
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c3880455deacbb84985ed2fc6caa7ecefbe15058010cd00766c2f10a24c93e31
size 7023383
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c288a24211d016952bc1221bf802bca095b9af4689b1141b1c940b5e37549423
size 10774480
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:81782aa8820dda0c7a678b776c22b1befdd0d8af7edacf77fcae8799909819b2
size 5175088
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eebebadb435ac4f0893d331f06809c2acb82e23543502614a9cf54525b353990
size 11144
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e5543f72d0e5a54b9ed771ef0b545d99903d2852fac0d0746c0b17458d0474b5
size 13170180
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43186e521051e7cdebc0da44792ae4d7e0f245ff7ebea36f2ba606dcee35d467
size 11672088
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f44139066b59019eac431281681d5aed33edebee6b027e52851dc8e25b84a6cf
size 10670195
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:86dac7325381b18d513f84b7809d3b61a9a1db6aff5284f40e1c0503398d754a
size 6174253
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:41393ec2cba36804a6cf5344d05a005eef6ba2f059e5608b7e4279bfe085fbc4
size 6174
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b49e5490c58a04aba73e99b0d3d4b78ee87f1e3d1e40475b5d8bb04ce4600536
size 6174
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:401fa96953d92887bfc3ed0172131c1f4ef9fb2c08a35230e4f17d39bc1c1e6f
size 432242
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:814915005c78e2031a4b4c454991e479cbc7575ddd4232fcff735f9bd98d3440
size 12123237
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:933ba1857855556357a3d5c3a7df259ab21bd6ad95a2fd1b6f6df22f591ac51d
size 8596636
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43563de4e0fb3262826812dd0e3273977917e65be59d4c2b1d0644a2370ac19f
size 8237
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f6e3d187a993637dd92fc58ab4a52ae92f66586853da2ad41bc3983100921724
size 12914
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5e22d17577a7551375d41469d4ce7ab046e74cab3c1c5e82639bcb95079bf626
size 10025
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0bf2a6dddeea48c33d7d8fadafde4abdfd093d0d08f0bae8c334aad3da3d7c6e
size 7632169
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:24d3980c5202b093a16e8b2fd1cb41bbd0f56f222447f7b65a566b31b39ac848
size 677932
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2da6895fc9819642fa235a3c35a7ac1e7ab92d9afd2d6d61234d584bdac59996
size 9752
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a49b8bdbf4c5040a0ee3571154b73f219e5adaee6739610e18f70d17cbdf27f
size 10019
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a19c8d13f0b0fd2b1ef07b803684e98b03ee615c29b42a64eea383c853379a7f
size 11155
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:650640385e112941701d58a62620288d7820bcdbef5dc8b3d3b1703b5f10b5e3
size 376113
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf9f28de8c7a990a28ea948bbbf9dc97eaf54b179545f54a7a5661ac9471bcae
size 582529
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4fa21103ce433b256959db7da39cc37b10c00ad344c0d79b8df1db2178c7e98c
size 11160
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d790a2fc00ef3550d0611c8d24e5e8b68943a43b74326387969d9a81e0c7698b
size 8235
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:798e46ef25ce18cecbb5525dfed4fbb68a947b2b14016adf073e8523f9bf642f
size 575147
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:76c7bc088d175accc4c125e589b2d6141a1851ea82a94594d2336293aabf51bf
size 332579
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b4bcdf904cc375eaacb3aaffc19fcc12c210bae6a3dd7d289bf6d4468fc8934c
size 5488622

Some files were not shown because too many files have changed in this diff Show More