feat(facades): 0.1.5 — hero detail, scene audit, material orphaning fixed (#32)
Merged PR #32: 0.1.5 — hero detail, scene audit, material orphaning fixed Co-authored-by: Zwitschi <zwitschi82@gmail.com>
This commit is contained in:
1 parent
743a290ac9
commit
1b8d9b851e
50 files changed
+1326
-88
No files matched your search
Binary file not shown.
@@ -37,6 +37,15 @@
|
||||
],
|
||||
"evidence": "6 columns at 4.96 m centres (sibling notes x measured frontage)"
|
||||
},
|
||||
{
|
||||
"object": "Hero_Flute_x120",
|
||||
"dims_m": [
|
||||
0.064,
|
||||
0.064,
|
||||
6.53
|
||||
],
|
||||
"evidence": "photographs: fluted shafts; 20 grooves per column modelled as shallow cylinders in the shadow tone rather than boolean cut grooves"
|
||||
},
|
||||
{
|
||||
"object": "Hero_ColumnBase_x6",
|
||||
"dims_m": [
|
||||
@@ -104,10 +113,10 @@
|
||||
"object": "Hero_MottoBand",
|
||||
"dims_m": [
|
||||
22.75,
|
||||
0.08,
|
||||
0.1,
|
||||
0.85
|
||||
],
|
||||
"evidence": "photographs: motto panel above the colonnade"
|
||||
"evidence": "photographs: motto panel above the colonnade, now an inset panel with a moulded surround rather than a flat stripe"
|
||||
},
|
||||
{
|
||||
"object": "Hero_Medallion_x2",
|
||||
|
||||
+38
-1
@@ -8,6 +8,42 @@ Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [
|
||||
|
||||
Nothing yet.
|
||||
|
||||
## [0.1.5] — 2026-11-09
|
||||
|
||||
Façade finishing. The kit gets the hero detail it was missing, and a class of silent defect is found and gated: the map had been rendering with **604 of 620 mesh actors unmaterialed** without a single error.
|
||||
|
||||
### Added
|
||||
|
||||
- **Scene audit** (`Tools/ue_audit_scene.py`): fails when any staged mesh has no material, when a frontage building is more than ±1 m off its verified footprint, or when two actors share a label. Wired into `Tools/render_screenshots.sh`, which now runs the whole staging chain in the correct order and stops on an audit failure.
|
||||
- **`Tools/ue_placement.py`**: one source of truth for staged positions and the verified frontage spans, with `span_error` / `describe_span_failure` validators.
|
||||
- **Hero fluting**: 20 grooves per column as shallow shadow-tone cylinders (120 pieces) rather than boolean cuts.
|
||||
- **Presence of references for the retail run**: `Art/reference/retail-neighbours/` (6800 Lee Drug and its neighbour). Local only, not committed.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Motto band is a real recess** now — moulded surround 6 cm proud, panel 12 cm behind — instead of an 8 cm dark stripe. Hero: 148 parts / 3 044 tris → 271 / 5 492, still 14.50 m.
|
||||
- **Kit layout for wide and tall buildings**: bays are capped at 9 with the module widened (6922 was 13 bays across 65.8 m — a window every 4.96 m on a 37 m block), and the storey spread is derived from the available height so the top band always lands inside the volume.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **604 of 620 mesh actors rendered untextured.** `ensure_material` deleted and recreated each site material on every run, orphaning every reference held by the kit pieces and the theatre trim; the map looked only slightly flat. Materials are now updated in place.
|
||||
- **The hero could be teleported 13 m east onto the wrong side of the boulevard.** `ue_hero_facade_import.py` carried its own copy of the pre-`0.1.0` hero position; running the importer alone moved the building, invisible in a render and visible only as a reference-score drop. Placement now has one definition and the import asserts the staged span (`span ok: X -14644..-12028`).
|
||||
- **53 duplicated kit actor labels** (both shop piers of a bay shared one name).
|
||||
|
||||
### Verified
|
||||
|
||||
- Scene audit: **0 failures** — every mesh materialed, all 7 verified spans within tolerance, no duplicate labels.
|
||||
- Packaged `-qaselftest`: walk, reset, pause/unpause, seam transect 1456 samples / 0 holes, wall probe 0/35, façade probe 0/9 — all PASS.
|
||||
- **1440p re-measured with the kit**: 2.76 ms average (**363 fps**), p99 3.64 ms, against `0.1.0`'s 2.42 ms (414 fps) — the 625 kit actors and 8 textures cost about 12 % and leave roughly six times the 60 fps floor. Series at `Docs/perf/0.1.5-kit-walk-frametimes.csv`.
|
||||
- 78 tool tests pass.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- **Retail neighbours 6822 and 6806–6820 are not modelled**: Commons holds no usable reference for either, and substituting the El Capitan Theatre's photographs would misattribute their façades. Tracked in `TODO.md`.
|
||||
- Cornice dentil spacing, abacus and plinth dimensions remain recorded assumptions.
|
||||
- Textures are generated, not authored — no decals, signage text or weathering maps.
|
||||
- The vision pre-check stays advisory; it has rejected shots the owner approved.
|
||||
|
||||
## [0.1.4] — 2026-11-09
|
||||
|
||||
Building façades. The block goes from flat extruded boxes with flat colours to coursed, textured surfaces assembled from a reusable module kit, and the El Capitan Theatre gets the marquee and blade sign that make it read as itself.
|
||||
@@ -172,7 +208,8 @@ The first playable release: a walkable exterior demo of the **6840 Hollywood Blv
|
||||
- Kerb height (0.13 m as built vs 0.15 m assumed), kerb radii, driveway aprons and graded crossing ramps are deferred to `0.1.1`; crossing ramps are kerb flushes.
|
||||
- One block only; no vehicles, NPCs, audio, or settings UI yet.
|
||||
|
||||
[Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.4...main
|
||||
[Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.5...main
|
||||
[0.1.5]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.5
|
||||
[0.1.4]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.4
|
||||
[0.1.3]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.3
|
||||
[0.1.2]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.2
|
||||
|
||||
@@ -86,8 +86,13 @@ The hero block and the demo level are built by a fixed sequence. Each step write
|
||||
| North import | `Tools/ue_import_north_buildings.py` | `/Game/Hollywood/North/*` |
|
||||
| Hero import | `Tools/ue_hero_facade_import.py` | `/Game/Hollywood/ElCapitan/SM_HeroFacade` |
|
||||
| Demo map | `Tools/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` |
|
||||
| Façade kit assembly | `Tools/ue_apply_facade_kit.py` | kit actors in `DemoMap` |
|
||||
| Theatre trim | `Tools/ue_stage_theatre_trim.py` | marquee + blade sign actors |
|
||||
| Scene audit | `Tools/ue_audit_scene.py` | materials, placement spans, labels (gate) |
|
||||
| Review shots | `Tools/ue_build_review_map.py` + `Tools/render_screenshots.sh` | `Screenshots/` |
|
||||
|
||||
**Order matters.** `ue_build_demo_map.py` rebuilds the site materials, so any actor staged by another tool must be (re)staged _after_ it, or it keeps a reference to a material that no longer exists — that is how 604 actors once ended up untethered from their materials while the map only looked slightly flat. `Tools/render_screenshots.sh` runs the full chain in the right order and gates on `ue_audit_scene.py`.
|
||||
|
||||
Every Blender exporter calls [`Tools/fbx_export_preset.py`](../Tools/fbx_export_preset.py) (`ue_metric_v1` / `ue_character_v1`) instead of repeating the FBX argument block — one place for the scale conversion, the axis mapping and `mesh_smooth_type="FACE"` (without which every Unreal import warns about missing smoothing groups and round geometry arrives faceted).
|
||||
|
||||
Two rules that the pipeline enforces rather than documents:
|
||||
|
||||
@@ -0,0 +1,728 @@
|
||||
frame,ms
|
||||
0,2.599
|
||||
1,2.877
|
||||
2,2.709
|
||||
3,2.646
|
||||
4,3.168
|
||||
5,2.729
|
||||
6,2.524
|
||||
7,2.726
|
||||
8,2.639
|
||||
9,2.814
|
||||
10,2.819
|
||||
11,2.708
|
||||
12,2.822
|
||||
13,2.703
|
||||
14,2.365
|
||||
15,2.796
|
||||
16,2.899
|
||||
17,2.972
|
||||
18,2.908
|
||||
19,2.356
|
||||
20,2.781
|
||||
21,2.574
|
||||
22,3.148
|
||||
23,2.306
|
||||
24,2.668
|
||||
25,2.855
|
||||
26,2.588
|
||||
27,3.321
|
||||
28,3.419
|
||||
29,3.121
|
||||
30,3.391
|
||||
31,2.355
|
||||
32,2.679
|
||||
33,3.463
|
||||
34,2.614
|
||||
35,2.922
|
||||
36,2.429
|
||||
37,2.830
|
||||
38,2.842
|
||||
39,2.782
|
||||
40,3.252
|
||||
41,2.731
|
||||
42,2.577
|
||||
43,2.363
|
||||
44,2.786
|
||||
45,2.547
|
||||
46,2.594
|
||||
47,2.652
|
||||
48,2.836
|
||||
49,2.792
|
||||
50,2.552
|
||||
51,2.792
|
||||
52,2.718
|
||||
53,3.333
|
||||
54,2.165
|
||||
55,2.880
|
||||
56,2.613
|
||||
57,2.460
|
||||
58,2.919
|
||||
59,2.770
|
||||
60,2.551
|
||||
61,2.622
|
||||
62,2.712
|
||||
63,2.818
|
||||
64,3.155
|
||||
65,2.472
|
||||
66,2.861
|
||||
67,2.651
|
||||
68,2.573
|
||||
69,2.892
|
||||
70,2.689
|
||||
71,2.729
|
||||
72,2.668
|
||||
73,2.753
|
||||
74,2.812
|
||||
75,2.606
|
||||
76,2.686
|
||||
77,2.842
|
||||
78,2.660
|
||||
79,2.903
|
||||
80,2.486
|
||||
81,2.591
|
||||
82,3.626
|
||||
83,2.455
|
||||
84,2.475
|
||||
85,2.922
|
||||
86,2.603
|
||||
87,2.728
|
||||
88,3.057
|
||||
89,2.731
|
||||
90,3.632
|
||||
91,1.950
|
||||
92,2.694
|
||||
93,3.020
|
||||
94,2.230
|
||||
95,2.796
|
||||
96,2.707
|
||||
97,2.807
|
||||
98,2.658
|
||||
99,2.697
|
||||
100,2.453
|
||||
101,3.051
|
||||
102,2.758
|
||||
103,2.662
|
||||
104,2.599
|
||||
105,2.733
|
||||
106,2.751
|
||||
107,2.894
|
||||
108,2.501
|
||||
109,2.753
|
||||
110,2.873
|
||||
111,2.439
|
||||
112,2.634
|
||||
113,2.682
|
||||
114,2.913
|
||||
115,2.692
|
||||
116,2.735
|
||||
117,2.758
|
||||
118,2.726
|
||||
119,2.907
|
||||
120,2.568
|
||||
121,2.764
|
||||
122,2.815
|
||||
123,2.530
|
||||
124,2.684
|
||||
125,2.719
|
||||
126,2.846
|
||||
127,2.879
|
||||
128,2.647
|
||||
129,2.770
|
||||
130,2.477
|
||||
131,2.657
|
||||
132,2.675
|
||||
133,2.917
|
||||
134,2.700
|
||||
135,2.493
|
||||
136,2.678
|
||||
137,2.786
|
||||
138,3.004
|
||||
139,2.697
|
||||
140,2.642
|
||||
141,2.624
|
||||
142,2.684
|
||||
143,2.717
|
||||
144,2.743
|
||||
145,2.740
|
||||
146,2.825
|
||||
147,2.849
|
||||
148,2.991
|
||||
149,2.297
|
||||
150,3.084
|
||||
151,2.459
|
||||
152,3.214
|
||||
153,2.260
|
||||
154,2.550
|
||||
155,2.621
|
||||
156,2.660
|
||||
157,2.739
|
||||
158,2.574
|
||||
159,3.078
|
||||
160,2.735
|
||||
161,2.607
|
||||
162,2.793
|
||||
163,2.798
|
||||
164,2.584
|
||||
165,2.637
|
||||
166,2.627
|
||||
167,2.726
|
||||
168,2.656
|
||||
169,2.788
|
||||
170,2.674
|
||||
171,2.782
|
||||
172,2.766
|
||||
173,2.613
|
||||
174,2.724
|
||||
175,2.770
|
||||
176,2.709
|
||||
177,2.700
|
||||
178,2.600
|
||||
179,2.686
|
||||
180,2.747
|
||||
181,2.841
|
||||
182,2.601
|
||||
183,2.666
|
||||
184,2.846
|
||||
185,2.599
|
||||
186,2.806
|
||||
187,3.126
|
||||
188,2.521
|
||||
189,2.634
|
||||
190,2.716
|
||||
191,2.584
|
||||
192,2.714
|
||||
193,2.923
|
||||
194,2.277
|
||||
195,2.706
|
||||
196,2.606
|
||||
197,2.758
|
||||
198,2.747
|
||||
199,2.958
|
||||
200,2.798
|
||||
201,2.387
|
||||
202,2.596
|
||||
203,2.861
|
||||
204,2.801
|
||||
205,2.521
|
||||
206,2.869
|
||||
207,2.884
|
||||
208,2.515
|
||||
209,2.592
|
||||
210,3.723
|
||||
211,2.474
|
||||
212,2.736
|
||||
213,2.774
|
||||
214,2.769
|
||||
215,3.007
|
||||
216,2.768
|
||||
217,2.716
|
||||
218,2.694
|
||||
219,2.722
|
||||
220,2.862
|
||||
221,2.587
|
||||
222,2.833
|
||||
223,2.792
|
||||
224,2.746
|
||||
225,2.585
|
||||
226,2.741
|
||||
227,2.585
|
||||
228,2.802
|
||||
229,2.674
|
||||
230,2.742
|
||||
231,2.718
|
||||
232,2.881
|
||||
233,2.707
|
||||
234,2.585
|
||||
235,2.810
|
||||
236,2.948
|
||||
237,2.718
|
||||
238,2.773
|
||||
239,2.571
|
||||
240,2.788
|
||||
241,2.702
|
||||
242,2.634
|
||||
243,2.788
|
||||
244,2.678
|
||||
245,2.914
|
||||
246,2.527
|
||||
247,2.637
|
||||
248,2.877
|
||||
249,2.693
|
||||
250,2.644
|
||||
251,2.670
|
||||
252,2.700
|
||||
253,2.877
|
||||
254,2.638
|
||||
255,2.617
|
||||
256,2.774
|
||||
257,2.660
|
||||
258,2.731
|
||||
259,2.671
|
||||
260,2.838
|
||||
261,2.726
|
||||
262,2.671
|
||||
263,2.744
|
||||
264,2.735
|
||||
265,2.684
|
||||
266,3.850
|
||||
267,2.120
|
||||
268,2.669
|
||||
269,2.729
|
||||
270,2.712
|
||||
271,2.817
|
||||
272,2.926
|
||||
273,2.678
|
||||
274,2.786
|
||||
275,2.927
|
||||
276,2.493
|
||||
277,2.846
|
||||
278,2.610
|
||||
279,2.644
|
||||
280,2.577
|
||||
281,2.939
|
||||
282,2.727
|
||||
283,2.595
|
||||
284,2.819
|
||||
285,2.778
|
||||
286,2.774
|
||||
287,2.628
|
||||
288,2.711
|
||||
289,2.699
|
||||
290,2.715
|
||||
291,2.763
|
||||
292,2.710
|
||||
293,2.892
|
||||
294,2.721
|
||||
295,2.575
|
||||
296,3.608
|
||||
297,2.811
|
||||
298,2.739
|
||||
299,2.751
|
||||
300,2.608
|
||||
301,2.913
|
||||
302,3.052
|
||||
303,2.862
|
||||
304,2.653
|
||||
305,2.727
|
||||
306,2.646
|
||||
307,2.888
|
||||
308,3.338
|
||||
309,2.743
|
||||
310,2.639
|
||||
311,2.681
|
||||
312,2.846
|
||||
313,2.766
|
||||
314,3.184
|
||||
315,2.834
|
||||
316,2.728
|
||||
317,2.581
|
||||
318,2.770
|
||||
319,3.351
|
||||
320,3.157
|
||||
321,2.508
|
||||
322,2.835
|
||||
323,2.754
|
||||
324,2.411
|
||||
325,3.398
|
||||
326,2.802
|
||||
327,2.611
|
||||
328,3.058
|
||||
329,2.407
|
||||
330,2.640
|
||||
331,3.645
|
||||
332,2.729
|
||||
333,2.580
|
||||
334,2.902
|
||||
335,2.523
|
||||
336,3.005
|
||||
337,3.173
|
||||
338,2.840
|
||||
339,2.610
|
||||
340,2.576
|
||||
341,2.706
|
||||
342,3.405
|
||||
343,2.849
|
||||
344,2.678
|
||||
345,2.796
|
||||
346,2.782
|
||||
347,2.636
|
||||
348,3.475
|
||||
349,2.798
|
||||
350,2.745
|
||||
351,2.564
|
||||
352,2.792
|
||||
353,2.714
|
||||
354,3.496
|
||||
355,2.841
|
||||
356,2.465
|
||||
357,2.708
|
||||
358,2.692
|
||||
359,2.934
|
||||
360,3.304
|
||||
361,2.561
|
||||
362,2.689
|
||||
363,2.749
|
||||
364,2.780
|
||||
365,2.940
|
||||
366,3.025
|
||||
367,2.706
|
||||
368,2.881
|
||||
369,2.619
|
||||
370,2.716
|
||||
371,3.133
|
||||
372,2.874
|
||||
373,2.816
|
||||
374,2.414
|
||||
375,2.737
|
||||
376,2.797
|
||||
377,2.651
|
||||
378,2.920
|
||||
379,3.032
|
||||
380,2.606
|
||||
381,2.710
|
||||
382,2.649
|
||||
383,2.700
|
||||
384,3.469
|
||||
385,2.224
|
||||
386,2.567
|
||||
387,2.733
|
||||
388,2.511
|
||||
389,2.950
|
||||
390,3.979
|
||||
391,2.798
|
||||
392,2.487
|
||||
393,2.740
|
||||
394,2.741
|
||||
395,2.718
|
||||
396,3.291
|
||||
397,2.769
|
||||
398,2.697
|
||||
399,2.796
|
||||
400,2.641
|
||||
401,2.652
|
||||
402,3.151
|
||||
403,2.673
|
||||
404,2.741
|
||||
405,2.606
|
||||
406,2.665
|
||||
407,2.859
|
||||
408,2.739
|
||||
409,2.586
|
||||
410,2.910
|
||||
411,2.745
|
||||
412,2.698
|
||||
413,2.670
|
||||
414,2.797
|
||||
415,2.829
|
||||
416,2.643
|
||||
417,2.776
|
||||
418,2.772
|
||||
419,2.530
|
||||
420,2.567
|
||||
421,2.826
|
||||
422,2.782
|
||||
423,2.738
|
||||
424,2.640
|
||||
425,2.967
|
||||
426,2.600
|
||||
427,3.020
|
||||
428,2.502
|
||||
429,2.642
|
||||
430,2.625
|
||||
431,2.770
|
||||
432,2.659
|
||||
433,2.717
|
||||
434,2.698
|
||||
435,2.780
|
||||
436,2.992
|
||||
437,2.686
|
||||
438,2.560
|
||||
439,3.048
|
||||
440,2.721
|
||||
441,2.507
|
||||
442,2.694
|
||||
443,2.695
|
||||
444,2.892
|
||||
445,3.443
|
||||
446,2.633
|
||||
447,2.908
|
||||
448,2.823
|
||||
449,2.346
|
||||
450,2.599
|
||||
451,2.914
|
||||
452,2.774
|
||||
453,3.391
|
||||
454,2.384
|
||||
455,2.585
|
||||
456,2.667
|
||||
457,2.704
|
||||
458,2.674
|
||||
459,2.755
|
||||
460,2.718
|
||||
461,2.804
|
||||
462,2.583
|
||||
463,2.694
|
||||
464,2.797
|
||||
465,3.079
|
||||
466,2.434
|
||||
467,2.723
|
||||
468,3.671
|
||||
469,2.211
|
||||
470,2.567
|
||||
471,2.701
|
||||
472,2.717
|
||||
473,2.657
|
||||
474,2.581
|
||||
475,2.889
|
||||
476,3.006
|
||||
477,2.595
|
||||
478,2.703
|
||||
479,2.498
|
||||
480,2.694
|
||||
481,2.915
|
||||
482,2.655
|
||||
483,2.686
|
||||
484,2.833
|
||||
485,2.717
|
||||
486,2.723
|
||||
487,2.451
|
||||
488,2.913
|
||||
489,2.784
|
||||
490,2.684
|
||||
491,2.651
|
||||
492,2.809
|
||||
493,2.819
|
||||
494,2.676
|
||||
495,2.851
|
||||
496,2.775
|
||||
497,2.638
|
||||
498,2.520
|
||||
499,2.824
|
||||
500,2.930
|
||||
501,2.757
|
||||
502,2.608
|
||||
503,2.808
|
||||
504,2.508
|
||||
505,2.819
|
||||
506,2.654
|
||||
507,2.748
|
||||
508,2.705
|
||||
509,2.798
|
||||
510,2.559
|
||||
511,2.694
|
||||
512,2.821
|
||||
513,2.706
|
||||
514,2.612
|
||||
515,2.928
|
||||
516,2.475
|
||||
517,2.830
|
||||
518,2.658
|
||||
519,2.784
|
||||
520,2.572
|
||||
521,2.935
|
||||
522,2.688
|
||||
523,2.600
|
||||
524,2.675
|
||||
525,2.828
|
||||
526,2.774
|
||||
527,2.848
|
||||
528,2.764
|
||||
529,2.379
|
||||
530,2.962
|
||||
531,2.989
|
||||
532,2.528
|
||||
533,2.639
|
||||
534,2.906
|
||||
535,2.543
|
||||
536,2.582
|
||||
537,2.949
|
||||
538,2.813
|
||||
539,2.504
|
||||
540,2.754
|
||||
541,2.638
|
||||
542,2.695
|
||||
543,2.662
|
||||
544,2.922
|
||||
545,2.616
|
||||
546,2.810
|
||||
547,2.675
|
||||
548,2.551
|
||||
549,2.894
|
||||
550,2.754
|
||||
551,2.632
|
||||
552,2.748
|
||||
553,2.827
|
||||
554,2.751
|
||||
555,2.759
|
||||
556,2.684
|
||||
557,2.766
|
||||
558,2.948
|
||||
559,2.461
|
||||
560,2.621
|
||||
561,2.772
|
||||
562,2.985
|
||||
563,2.582
|
||||
564,2.897
|
||||
565,2.574
|
||||
566,2.661
|
||||
567,2.742
|
||||
568,2.797
|
||||
569,2.632
|
||||
570,2.755
|
||||
571,2.878
|
||||
572,2.759
|
||||
573,3.812
|
||||
574,2.651
|
||||
575,2.862
|
||||
576,2.577
|
||||
577,2.634
|
||||
578,3.173
|
||||
579,2.418
|
||||
580,2.714
|
||||
581,2.746
|
||||
582,2.794
|
||||
583,2.774
|
||||
584,2.583
|
||||
585,3.659
|
||||
586,3.222
|
||||
587,2.489
|
||||
588,2.868
|
||||
589,2.558
|
||||
590,2.586
|
||||
591,2.599
|
||||
592,2.930
|
||||
593,2.401
|
||||
594,2.540
|
||||
595,2.889
|
||||
596,2.733
|
||||
597,2.664
|
||||
598,2.877
|
||||
599,2.588
|
||||
600,2.677
|
||||
601,2.897
|
||||
602,2.672
|
||||
603,2.780
|
||||
604,2.740
|
||||
605,2.780
|
||||
606,2.493
|
||||
607,2.794
|
||||
608,2.631
|
||||
609,2.768
|
||||
610,2.870
|
||||
611,2.626
|
||||
612,2.862
|
||||
613,2.602
|
||||
614,2.868
|
||||
615,2.627
|
||||
616,2.638
|
||||
617,2.702
|
||||
618,2.904
|
||||
619,2.918
|
||||
620,2.487
|
||||
621,3.225
|
||||
622,2.350
|
||||
623,2.820
|
||||
624,2.837
|
||||
625,2.408
|
||||
626,2.593
|
||||
627,3.526
|
||||
628,2.554
|
||||
629,2.746
|
||||
630,2.684
|
||||
631,2.729
|
||||
632,2.762
|
||||
633,2.768
|
||||
634,3.034
|
||||
635,2.514
|
||||
636,2.732
|
||||
637,2.739
|
||||
638,2.692
|
||||
639,2.832
|
||||
640,2.810
|
||||
641,2.484
|
||||
642,2.632
|
||||
643,3.284
|
||||
644,2.311
|
||||
645,3.079
|
||||
646,2.656
|
||||
647,2.683
|
||||
648,2.622
|
||||
649,2.668
|
||||
650,2.779
|
||||
651,2.796
|
||||
652,2.874
|
||||
653,2.427
|
||||
654,2.644
|
||||
655,2.872
|
||||
656,2.867
|
||||
657,2.404
|
||||
658,2.917
|
||||
659,2.773
|
||||
660,2.857
|
||||
661,2.789
|
||||
662,2.443
|
||||
663,2.651
|
||||
664,2.839
|
||||
665,2.814
|
||||
666,2.575
|
||||
667,2.796
|
||||
668,2.709
|
||||
669,2.873
|
||||
670,2.670
|
||||
671,2.936
|
||||
672,2.496
|
||||
673,2.608
|
||||
674,2.641
|
||||
675,2.793
|
||||
676,2.765
|
||||
677,2.736
|
||||
678,2.679
|
||||
679,2.987
|
||||
680,2.884
|
||||
681,2.469
|
||||
682,2.774
|
||||
683,2.948
|
||||
684,2.598
|
||||
685,3.478
|
||||
686,2.856
|
||||
687,4.623
|
||||
688,3.038
|
||||
689,2.507
|
||||
690,2.717
|
||||
691,2.682
|
||||
692,2.807
|
||||
693,2.778
|
||||
694,2.699
|
||||
695,2.802
|
||||
696,2.605
|
||||
697,2.650
|
||||
698,2.789
|
||||
699,2.791
|
||||
700,3.015
|
||||
701,2.539
|
||||
702,2.790
|
||||
703,2.698
|
||||
704,2.637
|
||||
705,2.657
|
||||
706,2.800
|
||||
707,2.720
|
||||
708,2.702
|
||||
709,2.845
|
||||
710,2.583
|
||||
711,2.780
|
||||
712,2.782
|
||||
713,2.593
|
||||
714,2.695
|
||||
715,2.747
|
||||
716,2.871
|
||||
717,2.671
|
||||
718,2.676
|
||||
719,2.991
|
||||
720,2.727
|
||||
721,2.576
|
||||
722,2.539
|
||||
723,2.770
|
||||
724,2.712
|
||||
725,2.818
|
||||
726,2.615
|
||||
|
@@ -127,6 +127,22 @@ Run on the packaged build (`GigLineQA`, Development, Vulkan SM5) with `-qaselfte
|
||||
|
||||
Committed frame-time series: `Docs/perf/0.1.0-walk-frametimes.csv`.
|
||||
|
||||
### `0.1.5` re-measure with the façade kit in the map
|
||||
|
||||
Same route, same preset, packaged Development build, **2560×1440**:
|
||||
|
||||
| Metric | `0.1.0` | `0.1.5` (kit + textures) |
|
||||
| ------- | ----------------: | -----------------------: |
|
||||
| Average | 2.42 ms (414 fps) | **2.76 ms (363 fps)** |
|
||||
| p50 | 2.38 ms | 2.73 ms |
|
||||
| p95 | 2.71 ms | 3.17 ms |
|
||||
| p99 | 3.01 ms | 3.64 ms |
|
||||
| Max | 4.00 ms | 4.62 ms |
|
||||
|
||||
The **625 kit actors and 8 generated textures cost about 12 %** of frame time and still leave **363 fps average, p99 3.64 ms** — roughly six times the 60 fps floor. Frame-time series: `Docs/perf/0.1.5-kit-walk-frametimes.csv`.
|
||||
|
||||
**Measurement trap, again:** runs that reported ~105 fps were two game instances overlapping (a previous launch had not exited). Confirm a single instance (`pgrep -af Binaries/Linux/GigLine`) before trusting a figure — 363 fps reproduced twice on a clean run.
|
||||
|
||||
## `0.1.0` — Fresh checkout build — record (2026-11-08)
|
||||
|
||||
Followed the documented steps in [`Docs/build.md`](build.md) exactly, into a clean directory with no prior state:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -436,3 +436,90 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
|
||||
scope is cut deliberately and recorded in `CHANGELOG.md`, never the frame rate.
|
||||
Walkability outranks realism in one direction: a block the player falls through is
|
||||
not a demo.
|
||||
|
||||
## 2026-11-09 — 0.1.5 start: retail reference + kit layout maths
|
||||
|
||||
- Commons holds exactly **one** usable reference for the retail run:
|
||||
`Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg` (Gb321, CC BY-SA
|
||||
4.0, 1920×953) — 6800 Lee Drug **and** its neighbour at street level, shopfronts
|
||||
and parapets visible (vision pre-check: yes). Nothing for 6822 or 6806–6820, so
|
||||
those two façades still have no reference and stay blocked.
|
||||
- **Kit layout was wrong for tall/wide buildings**, not just ugly: bays were
|
||||
`round(width / 496)`, so 6922 (65.8 m wide, 37.1 m tall) got **13 bays** — a
|
||||
window every 4.96 m across the whole frontage — and the storey count came out at
|
||||
11 × 310 cm = 34.1 m against a 35.7 m window zone, leaving bare wall under the
|
||||
cornice. Fixed: `MAX_BAYS = 9` (module widens instead of subdividing), and the
|
||||
storey spread is derived from the available height (`usable / storeys`) so the top
|
||||
band always lands inside the building. Result: 6922 9 × 11, theatre 8 × 6,
|
||||
6806 7 × 3, 6800 4 × 3, TCL 9 × 5.
|
||||
|
||||
## 2026-11-09 — hero fluting + motto band; and a silent placement divergence
|
||||
|
||||
- Fluting: 20 grooves per column as thin shadow-tone cylinders set into the shaft
|
||||
(120 pieces) rather than boolean cuts — the eye reads them in silhouette and
|
||||
grazing light, booleans would cost far more for that. Motto band is now a real
|
||||
recess: surround 6 cm proud, panel 12 cm behind (was an 8 cm dark stripe).
|
||||
Hero 148 parts/3 044 tris -> 271/5 492, still 14.50 m.
|
||||
- **The real find:** `ue_hero_facade_import.py` had its own hero position
|
||||
`(-12024.5, -1450, 0)` — the pre-`0.1.0` value, before the frontage placement fix
|
||||
moved the hero to `(-13336, +1307)`. Running the importer on its own therefore
|
||||
teleported the hero **13 m east and onto the north side of the boulevard**, and
|
||||
nothing complained: a displaced building still renders. The only symptom was the
|
||||
reference score dropping 0.69 -> 0.56 with the 3x3 composition correlation
|
||||
flipping +0.54 -> -0.08.
|
||||
- Fix: `Tools/ue_placement.py` is now the single source of truth (hero position,
|
||||
verified frontage spans, `span_error`/`describe_span_failure`). Both
|
||||
`ue_hero_facade_import.py` and `ue_build_demo_map.py` import it, the import asserts
|
||||
the staged span and logs `PLACEMENT ERROR` when it does not match, and tests assert
|
||||
the stale literal cannot come back. Spawn-view score back to **0.72** (best yet).
|
||||
- Lesson to keep: **duplicated placement constants drift silently.** The metric that
|
||||
caught it was the composition correlation, not a visual review.
|
||||
|
||||
## 2026-11-09 — 1440p re-measured with the kit: 363 fps, ~12% cost
|
||||
|
||||
- 2560×1440, Epic preset, TSR, VSync off, no frame generation: **2.76 ms average
|
||||
(363 fps)**, p50 2.73, p95 3.17, p99 3.64, max 4.62 — against `0.1.0`'s 2.42 ms
|
||||
(414 fps). The 625 kit actors + 8 generated textures cost about **12 %** and leave
|
||||
~6× the 60 fps floor.
|
||||
- **Variance trap confirmed a third time:** runs reporting ~105 fps (9.5 ms) were
|
||||
two overlapping game instances — an earlier launch had not exited. `pgrep -af
|
||||
Binaries/Linux/GigLine` should show one wrapper + one binary; 363 fps reproduced
|
||||
twice on a clean run. Always count instances before believing a perf number.
|
||||
- Series committed at `Docs/perf/0.1.5-kit-walk-frametimes.csv`; comparison table in
|
||||
`Docs/release-checks.md`.
|
||||
|
||||
## 2026-11-09 — 604 unmaterialed actors: material rebuild was delete+recreate
|
||||
|
||||
- The "blockout cleaning pass" turned out to need **material and placement hygiene,
|
||||
not more geometry**. `Tools/ue_audit_scene.py` (new) found **604 of 620 mesh actors
|
||||
with no material**: kit pieces and theatre trim all untextured.
|
||||
- **Cause:** `ensure_material` in `ue_build_demo_map.py` deleted and recreated each
|
||||
`/Game/Hollywood/Site/M_*` asset every run ("so a look change cannot be masked by a
|
||||
stale asset"). That orphane every reference: actors staged by other tools pointed at
|
||||
deleted assets and fell back to the default material. The map looked only *slightly*
|
||||
flat — no error, no warning.
|
||||
- **Fix:** update the material **in place** — load it, `MaterialEditingLibrary.
|
||||
delete_all_material_expressions(mat)`, rebuild the graph, recompile, save. Note
|
||||
`Material.expressions` is protected from Python; use the library's deleter.
|
||||
- **Ordering rule now documented and enforced:** the demo builder rebuilds materials,
|
||||
so kit and theatre staging must run *after* it. `render_screenshots.sh` now runs
|
||||
demo map → kit assembly → theatre trim → scene audit → shot cameras → capture, and
|
||||
**fails on a non-zero audit count**.
|
||||
- Second find: 53 duplicated kit labels (both shop piers of a bay shared one name).
|
||||
- Scene audit after the fixes: **0 failures** (all materialed, all spans verified, no
|
||||
duplicate labels). Packaged QA still PASS; spawn-view reference score 0.72.
|
||||
|
||||
## 2026-11-09 — v0.1.5 released
|
||||
|
||||
- All actionable `0.1.5` work is done: hero fluting + recessed motto band, kit layout
|
||||
for wide/tall buildings, placement single-source-of-truth, 1440p re-measure
|
||||
(363 fps), and the material/placement hygiene that the "blockout cleaning" task
|
||||
actually needed (604 unmaterialed actors, 53 duplicate labels) with a new scene
|
||||
audit gate.
|
||||
- Only item carried forward is the **retail frontage (6822 / 6806–6820 / 6800)**,
|
||||
blocked because Commons holds no usable reference for two of the three buildings
|
||||
and substituting the theatre's photographs would misattribute the façades. It now
|
||||
lives in a `0.1.6` Retail frontage patch.
|
||||
- Precedent applied again: tag when the stated scope is done, carry blocked scope
|
||||
into the next patch rather than letting a release wait indefinitely on missing
|
||||
reference photography.
|
||||
+1
-1
@@ -8,7 +8,7 @@ Development roadmap for this project: milestone goals, scope, decisions, and rel
|
||||
|
||||
## Status at a glance — 2026-11-08 15:30
|
||||
|
||||
Workstation, geo pipeline, and engine validation are done. The art pipeline has a first blockout, terrain, and street in metres, in one shared coordinate frame. **Project bootstrap is complete**: `Game/GigLine` builds, packages, and both content-acceptance tests (static-mesh scale, MPFB skeletal character) pass. **`v0.1.0` released 2026-11-08**: the walkable demo, hero façade, verified block frontage, first-person controls, ODbL attribution, and every release check pass (traversal, placement, collision, 60 fps at 1440p, fresh-checkout build, packaged build with controls). Next: the `0.1.1`–`0.1.4` patch series (collision detail, scale/façade corrections, packaging fixes, real building façades), then `0.2.0` (player body, third-person camera, animation).
|
||||
Workstation, geo pipeline, and engine validation are done. The art pipeline has a first blockout, terrain, and street in metres, in one shared coordinate frame. **Project bootstrap is complete**: `Game/GigLine` builds, packages, and both content-acceptance tests (static-mesh scale, MPFB skeletal character) pass. **`v0.1.0` released 2026-11-08**: the walkable demo, hero façade, verified block frontage, first-person controls, ODbL attribution, and every release check pass (traversal, placement, collision, 60 fps at 1440p, fresh-checkout build, packaged build with controls). Next: the `0.1.1`–`0.1.4` patch series (collision detail, scale/façade corrections, packaging fixes, real building façades), **Next:** `0.1.6` retail frontage (6822, 6806–6820, 6800 — blocked on reference photography for two of the three), then `0.2.0` (player body, third-person camera, animation).
|
||||
|
||||
| Area | State |
|
||||
| --- | --- |
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,19 +9,15 @@ Actionable task list for this project. This file is the single, central place fo
|
||||
- When all steps in a version section are done: write a `CHANGELOG.md` entry, then remove the version section from this file.
|
||||
- Release gate: a version is tagged only on the owner's explicit go-ahead once its release checks pass, not automatically when the checkboxes close.
|
||||
|
||||
## 0.1.5 — Façade finishing (patch)
|
||||
## 0.1.6 — Retail frontage (patch)
|
||||
|
||||
Suggested blender skills: **references** (reference-image-match against the photographs), **historical-worlds** / **archviz** (1920s Hollywood Blvd neoclassical detail), **materials** and **lookdev** for the remaining surface work.
|
||||
|
||||
The `0.1.4` kit, materials and theatre trim are signed off; these are the geometry items that were carried forward rather than grown inside that patch.
|
||||
Suggested blender skills: **references** (reference-image-match), **archviz**, **historical-worlds** for the 1920s–1990s retail mix on this run.
|
||||
|
||||
### Patch tasks
|
||||
|
||||
- [ ] **Retail neighbours (6822 Budget, 6806–6820, 6800 Lee Drug) from their own references.** The 18-frame set is the El Capitan Theatre, so it cannot stand in for these façades. Fetch or shoot per-building references first, then model the shopfront line and upper storeys rather than relying on the kit alone.
|
||||
- [ ] **Hero façade: fluting and the motto band as geometry.** The colonnade is plain cylinders and the motto/medallions are flat panels; both are legible in the reference photography.
|
||||
- [ ] **Blosm blockout cleaning pass.** Roofs and facades remain untextured volumes and the `setupScript` styles stay unused; with the kit carrying the elevations, this is now cleanup rather than a second façade system.
|
||||
- [ ] **Re-measure 1440p performance** with 625 kit actors and 8 generated textures in the map. `0.1.0` measured 414 fps average, so there is headroom, but it has not been re-run since.
|
||||
- [ ] Tag `v0.1.5` and write `CHANGELOG.md` entry; then remove this section.
|
||||
- [ ] **Model 6822 Budget, 6806–6820 and 6800 Lee Drug from their own references.** `Art/reference/retail-neighbours/` holds 6800 and its neighbour (Gb321, CC BY-SA 4.0); Commons has nothing usable for 6822 or 6806–6820, so those two need photography before they can be modelled honestly. Do not substitute the El Capitan Theatre's 18-frame set — it would misattribute the façades.
|
||||
- [ ] Review every façade change with `Tools/render_screenshots.sh --verify` before tagging.
|
||||
- [ ] Tag `v0.1.6` and write `CHANGELOG.md` entry; then remove this section.
|
||||
|
||||
## 0.2.0 — MPFB player body, third-person camera, basic animation (weeks 7–9)
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
||||
| `ue_stage_theatre_trim.py` | Imports the trim to `/Game/Hollywood/Theatre` and stages one actor per piece in the DemoMap with site materials; clears a previous run first. |
|
||||
| `ue_apply_facade_kit.py` | Lays the kit over a building's street face from its own bounding box: bay count = round(width / 4.96), storeys from the height, shopfronts on the ground floor, windows above, pilasters on bay boundaries, cornice and parapet at the top. Clears a previous application first; unique labels per building. |
|
||||
| `generate_facade_textures.py` | Deterministic stdlib PNG generator for the facade maps (`T_StoneAshlar`, `T_Stucco`, `T_Asphalt`, `T_Concrete`, each `_BC` base colour + `_R` roughness). Pure-zlib writer, hash-based value noise, identical bytes on every run; `--size` and `--out` (`/tmp/gigline/textures` by default). `ue_build_demo_map.py` imports them into `/Game/Hollywood/Textures` and samples them world-aligned. |
|
||||
| `ue_audit_scene.py` | Gate on the DemoMap: no unmaterialed actor, every verified frontage span within tolerance (`ue_placement`), no duplicate actor labels. Writes `/tmp/gigline/scene-audit.txt`; `render_screenshots.sh --verify` fails on a non-zero failure count. Catches the class of defect that still renders plausibly. |
|
||||
| `ue_placement.py` | Single source of truth for where staged actors belong in the DemoMap (hero position, verified frontage spans) plus `span_error`/`describe_span_failure` validators. Exists because the hero position was written twice and drifted — the importer kept the pre-`0.1.0` value and moved the hero 13 m east onto the wrong side of the boulevard. No `unreal` import, so it is unit-tested. |
|
||||
| `ue_common.py` | Shared helpers for the Unreal commandlet scripts: `make_writable()` clears the read-only bit an LFS `.umap` checks out with (otherwise `save_map()` returns False silently), and `require_saved()` makes that failure explicit. |
|
||||
| `fbx_export_preset.py` | The **single** FBX export preset (`ue_metric_v1` / `ue_character_v1`): metres→centimetres via `FBX_SCALE_NONE`, axis mapping east +X / north −Y / up +Z, `mesh_smooth_type="FACE"` (smoothing groups survive the import). Every Blender exporter imports it instead of copying the argument block; `preset_kwargs()` is bpy-free and unit-tested. |
|
||||
| `sibling_asset_audit.py` | Reads the sibling project's prop library from GLB headers into `Art/props/reuse-manifest.json`; copies nothing |
|
||||
|
||||
@@ -174,6 +174,20 @@ def build():
|
||||
cap_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H - CAPITAL_H
|
||||
cylinder(f"Hero_Column_{i + 1}", x, -0.55, shaft_z0, cap_z0,
|
||||
COLUMN_D / 2.0, stone)
|
||||
|
||||
# Fluting: 20 shallow grooves around the shaft, as the photographs show on
|
||||
# the colonnade. Each flute is a thin cylinder set into the shaft surface;
|
||||
# modelling them as grooves would need booleans on 6 columns for detail the
|
||||
# eye reads only in silhouette and grazing light, so they are added mass in
|
||||
# the *shadow* direction instead — cheap, and it breaks the flat highlight.
|
||||
flutes = 20
|
||||
flute_r = 0.032
|
||||
for f in range(flutes):
|
||||
ang = (2.0 * math.pi * f) / flutes
|
||||
fx = x + (COLUMN_D / 2.0 - flute_r * 0.4) * math.cos(ang)
|
||||
fy = -0.55 + (COLUMN_D / 2.0 - flute_r * 0.4) * math.sin(ang)
|
||||
cylinder(f"Hero_Flute_{i + 1}_{f + 1}", fx, fy, shaft_z0 + 0.05,
|
||||
cap_z0 - 0.05, flute_r, stone_dark, vertices=6)
|
||||
# Capital block, then the square abacus that carries the architrave.
|
||||
# The abacus takes the top 0.14 m of the capital zone so the colonnade
|
||||
# still ends at HEIGHT - ENTABLATURE_H - PARAPET_H (proportion unchanged).
|
||||
@@ -185,6 +199,9 @@ def build():
|
||||
1.30, 1.30, abacus_h, stone)
|
||||
note("Hero_Column_x6", (COLUMN_D, COLUMN_D, HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H),
|
||||
f"6 columns at {COLUMN_BAY} m centres (sibling notes x measured frontage)")
|
||||
note("Hero_Flute_x120", (0.064, 0.064, cap_z0 - shaft_z0),
|
||||
"photographs: fluted shafts; 20 grooves per column modelled as shallow "
|
||||
"cylinders in the shadow tone rather than boolean cut grooves")
|
||||
note("Hero_ColumnBase_x6", (1.36, 1.36, 0.22),
|
||||
"assumption: Ionic base moulding; photographs show a wider base at the shaft foot")
|
||||
note("Hero_Abacus_x6", (1.30, 1.30, 0.14),
|
||||
@@ -223,10 +240,22 @@ def build():
|
||||
FRONTAGE_W, 1.2, PARAPET_H, stone)
|
||||
note("Hero_Parapet", (FRONTAGE_W, 1.2, PARAPET_H), "assumption: parapet carries the motto band")
|
||||
|
||||
# Motto band: a recessed panel across the parapet, as the photographs show.
|
||||
cube("Hero_MottoBand", 0.0, -1.32, HEIGHT - PARAPET_H / 2.0,
|
||||
FRONTAGE_W - 3.0, 0.08, 0.85, stone_dark)
|
||||
note("Hero_MottoBand", (FRONTAGE_W - 3.0, 0.08, 0.85), "photographs: motto panel above the colonnade")
|
||||
# Motto band: a recessed panel across the parapet with a moulded surround, as
|
||||
# the photographs show. The panel was a 8 cm stripe of dark stone; it is now a
|
||||
# real recess — surround frame standing 6 cm proud, panel set 12 cm behind it —
|
||||
# so it reads as an inscription panel from the street instead of a paint line.
|
||||
motto_z = HEIGHT - PARAPET_H / 2.0
|
||||
motto_w = FRONTAGE_W - 3.0
|
||||
for x_off in (-(motto_w / 2.0 + 0.16), (motto_w / 2.0 + 0.16)):
|
||||
cube(f"Hero_MottoSurround_v{int(x_off > 0)}", x_off, -1.13, motto_z,
|
||||
0.32, 0.26, 1.15, stone)
|
||||
for z_off in (-0.575, 0.575):
|
||||
cube(f"Hero_MottoSurround_h{int(z_off > 0)}", 0.0, -1.13, motto_z + z_off,
|
||||
motto_w + 0.64, 0.26, 0.32, stone)
|
||||
cube("Hero_MottoBand", 0.0, -1.06, motto_z, motto_w, 0.10, 0.85, stone_dark)
|
||||
note("Hero_MottoBand", (motto_w, 0.10, 0.85),
|
||||
"photographs: motto panel above the colonnade, now an inset panel with a "
|
||||
"moulded surround rather than a flat stripe")
|
||||
|
||||
# Medallions either side of the inscription: discs on the parapet face, not
|
||||
# pancakes on the coping (see disc_on_wall).
|
||||
|
||||
@@ -20,18 +20,29 @@ PROJ="$REPO/Game/GigLine/GigLine.uproject"
|
||||
VERIFY="no"
|
||||
[ "${1:-}" = "--verify" ] && VERIFY="yes"
|
||||
|
||||
echo "== 1/3 stage the demo shot cameras =="
|
||||
cd "$ENGINE"
|
||||
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \
|
||||
-run=pythonscript -script="$REPO/Tools/ue_build_review_map.py" \
|
||||
-nullrhi -unattended -nopause -stdout
|
||||
run_cmdlet() {
|
||||
local script="$1" label="$2"
|
||||
echo "== $label =="
|
||||
( cd "$ENGINE" && ./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \
|
||||
-run=pythonscript -script="$REPO/Tools/$script" \
|
||||
-nullrhi -unattended -nopause -stdout )
|
||||
}
|
||||
|
||||
# Order matters. The demo builder rebuilds the site materials, so any actor staged
|
||||
# by another tool must be (re)staged after it or it keeps a reference to a material
|
||||
# that no longer exists - that is how 604 actors ended up untextured once.
|
||||
run_cmdlet ue_build_demo_map.py "1/5 rebuild the demo map (materials, site, frontage)"
|
||||
run_cmdlet ue_apply_facade_kit.py "2/5 assemble the facade kit"
|
||||
run_cmdlet ue_stage_theatre_trim.py "3/5 stage the theatre trim"
|
||||
run_cmdlet ue_audit_scene.py "4/6 audit the scene (materials, placement, labels)"
|
||||
run_cmdlet ue_build_review_map.py "5/6 stage the demo shot cameras"
|
||||
tail -3 /tmp/ue-review-map-result.txt 2>/dev/null || true
|
||||
|
||||
echo "== 2/3 capture the demo map =="
|
||||
echo "== 6/6 capture the demo map =="
|
||||
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" /Game/Hollywood/DemoMap \
|
||||
-game -windowed -ResX=1920 -ResY=1080 -nopause -nosplash -stdout
|
||||
|
||||
echo "== 3/3 collect the demo screenshots =="
|
||||
echo "== collect the demo screenshots =="
|
||||
mkdir -p "$REPO/Screenshots/demo"
|
||||
rm -f "$REPO"/Screenshots/demo/review-*.png
|
||||
found=0
|
||||
@@ -47,6 +58,14 @@ done
|
||||
[ "$found" = "1" ] || { echo "ERROR: no demo screenshots found"; exit 1; }
|
||||
ls -1 "$REPO/Screenshots/demo"
|
||||
|
||||
# The scene audit is a gate: a wrong-material or misplaced actor is exactly the
|
||||
# kind of failure that still renders plausibly.
|
||||
if grep -q "failure(s) ==" /tmp/gigline/scene-audit.txt 2>/dev/null; then
|
||||
grep -E "FAIL|failure\(s\)" /tmp/gigline/scene-audit.txt | tail -5
|
||||
grep -q "== 0 failure(s) ==" /tmp/gigline/scene-audit.txt || {
|
||||
echo "ERROR: scene audit failed — see /tmp/gigline/scene-audit.txt"; exit 1; }
|
||||
fi
|
||||
|
||||
if [ "$VERIFY" = "yes" ]; then
|
||||
echo "== verify: does each shot show what it should? =="
|
||||
cd "$REPO"
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Tests for Tools/ue_placement.py — the single source of truth for placement.
|
||||
|
||||
A regression test for a real bug: `ue_hero_facade_import.py` carried its own hero
|
||||
position (the pre-0.1.0 one) while `ue_build_demo_map.py` staged the verified one.
|
||||
Running the importer moved the hero 13 m east and onto the north side of the
|
||||
boulevard, and nothing complained because a displaced building still renders.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
sys.path.insert(0, os.path.join(ROOT, "Tools"))
|
||||
|
||||
import ue_placement # noqa: E402
|
||||
|
||||
|
||||
class HeroPlacementTest(unittest.TestCase):
|
||||
def test_hero_actor_name(self):
|
||||
self.assertEqual(ue_placement.HERO_ACTOR, "Hero_Facade")
|
||||
|
||||
def test_hero_location_is_the_verified_building_line(self):
|
||||
x, y, z = ue_placement.HERO_LOCATION_CM
|
||||
self.assertEqual((x, y, z), (-13336.0, 1307.0, 0.0))
|
||||
# North is -Y in Unreal, so the hero on the south side must have Y > 0.
|
||||
self.assertGreater(y, 0.0, "hero must be on the south side (Y > 0)")
|
||||
|
||||
def test_hero_span_is_verified(self):
|
||||
# The mesh spans -1308..+1308 cm in X about its origin.
|
||||
lo = ue_placement.HERO_LOCATION_CM[0] - 1308.0
|
||||
hi = ue_placement.HERO_LOCATION_CM[0] + 1308.0
|
||||
exp_min, exp_max, ok = ue_placement.span_error("hero", lo, hi)
|
||||
self.assertTrue(ok)
|
||||
# The mesh spans exactly the verified footprint, so the two agree.
|
||||
self.assertAlmostEqual(exp_min, lo, delta=ue_placement.SPAN_TOLERANCE_CM)
|
||||
self.assertAlmostEqual(exp_max, hi, delta=ue_placement.SPAN_TOLERANCE_CM)
|
||||
|
||||
|
||||
class SpanValidatorTest(unittest.TestCase):
|
||||
def test_misplaced_hero_is_reported(self):
|
||||
# The exact failure observed: the stale import position put the hero here.
|
||||
lo, hi = -13332.0, -10716.0
|
||||
exp_min, exp_max, ok = ue_placement.span_error("hero", lo, hi)
|
||||
self.assertFalse(ok)
|
||||
message = ue_placement.describe_span_failure("hero", lo, hi)
|
||||
self.assertIn("does not match the verified", message)
|
||||
self.assertIn(f"{exp_min:.0f}", message)
|
||||
|
||||
def test_correct_span_reports_nothing(self):
|
||||
self.assertEqual(ue_placement.describe_span_failure("hero", -14644, -12028), "")
|
||||
|
||||
def test_within_tolerance_passes(self):
|
||||
# 99 cm off is inside the ±100 cm tolerance.
|
||||
self.assertTrue(ue_placement.span_error("6922", -24276 + 99, -17700 - 99)[2])
|
||||
|
||||
def test_outside_tolerance_fails(self):
|
||||
self.assertFalse(ue_placement.span_error("6922", -24276 + 101, -17700)[2])
|
||||
|
||||
def test_unknown_building_raises(self):
|
||||
with self.assertRaises(KeyError):
|
||||
ue_placement.span_error("no-such-building", 0, 0)
|
||||
|
||||
def test_every_frontage_span_is_ordered(self):
|
||||
for name, (lo, hi) in ue_placement.FRONTAGE_SPANS_CM.items():
|
||||
self.assertLess(lo, hi, f"{name} span is inverted")
|
||||
|
||||
|
||||
class SingleSourceOfTruthTest(unittest.TestCase):
|
||||
"""The import script and the demo builder must not carry their own literals."""
|
||||
|
||||
def test_import_script_uses_the_shared_constant(self):
|
||||
src = open(os.path.join(ROOT, "Tools", "ue_hero_facade_import.py"),
|
||||
encoding="utf-8").read()
|
||||
self.assertIn("ue_placement.HERO_LOCATION_CM", src)
|
||||
self.assertNotIn("(-12024.5, -1450.0, 0.0)", src,
|
||||
"the stale pre-0.1.0 hero position must not come back")
|
||||
|
||||
def test_demo_builder_uses_the_shared_constant(self):
|
||||
src = open(os.path.join(ROOT, "Tools", "ue_build_demo_map.py"),
|
||||
encoding="utf-8").read()
|
||||
self.assertIn("ue_placement.HERO_LOCATION_CM", src)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -123,12 +123,28 @@ def apply_to_actor(actor, kit, materials=None, prefix=None):
|
||||
f"({width / 100:.1f} x {height / 100:.1f} m)")
|
||||
return 0
|
||||
|
||||
# Bays: the kit module is one window bay (4.96 m). A very wide building gets a
|
||||
# bay count that keeps the rhythm readable rather than one window per 4.96 m of
|
||||
# frontage — 6922 is 65.8 m wide, which is 13 bays, giving a window every 4.96 m
|
||||
# across a 37 m tall block: the rhythm reads as a wall of glass, not a building.
|
||||
# Above MAX_BAYS the module is widened instead, so the bay count stays legible.
|
||||
MAX_BAYS = 9
|
||||
bays = max(1, int(round(width / BAY_CM)))
|
||||
if bays > MAX_BAYS:
|
||||
bays = MAX_BAYS
|
||||
bay_w = width / bays # actual bay width for this building
|
||||
# Street face: the frontage runs along X, and the street is at smaller Y.
|
||||
face_y = mn.y
|
||||
base_z = mn.z
|
||||
storeys = max(1, int((height - SHOP_FLOOR_CM) // STOREY_CM))
|
||||
# Storeys never overrun the volume: cap the count so the top band (cornice +
|
||||
# parapet, 145 cm) always lands inside the building.
|
||||
storeys = max(1, int((height - SHOP_FLOOR_CM - 145.0) // STOREY_CM) + 1)
|
||||
top_z = base_z + height
|
||||
# Windows above the shopfront, spread over the available height rather than
|
||||
# assumed to be exactly STOREY_CM apart, so a 37 m block does not leave a gap
|
||||
# of bare wall under the cornice.
|
||||
usable = max(0.0, (top_z - 145.0) - (base_z + SHOP_FLOOR_CM))
|
||||
storey_step = (usable / storeys) if storeys else STOREY_CM
|
||||
|
||||
def kit_material(name):
|
||||
if materials is None:
|
||||
@@ -140,11 +156,13 @@ def apply_to_actor(actor, kit, materials=None, prefix=None):
|
||||
for bay in range(bays):
|
||||
cx = mn.x + bay_w * (bay + 0.5)
|
||||
# ground floor: shopfront
|
||||
for piece, dx in (("Kit_ShopPier_L", -bay_w / 2 + 30.0),
|
||||
("Kit_ShopPier_R", bay_w / 2 - 30.0)):
|
||||
for piece, dx, side in (("Kit_ShopPier_L", -bay_w / 2 + 30.0, "L"),
|
||||
("Kit_ShopPier_R", bay_w / 2 - 30.0, "R")):
|
||||
if piece in kit:
|
||||
# The side belongs in the label: both piers of a bay shared one
|
||||
# label, which left 53 ambiguous pairs in the outliner.
|
||||
if _spawn(kit[piece], unreal.Vector(cx + dx, face_y, base_z),
|
||||
f"{prefix}shop_pier_{bay}", kit_material(piece)):
|
||||
f"{prefix}shop_pier_{side}_{bay}", kit_material(piece)):
|
||||
placed += 1
|
||||
if "Kit_ShopGlass" in kit:
|
||||
if _spawn(kit["Kit_ShopGlass"], unreal.Vector(cx, face_y, base_z),
|
||||
@@ -153,7 +171,7 @@ def apply_to_actor(actor, kit, materials=None, prefix=None):
|
||||
# upper storeys: window bays
|
||||
for storey in range(storeys):
|
||||
if "Kit_WindowBay" in kit:
|
||||
z = base_z + SHOP_FLOOR_CM + STOREY_CM * storey + 65.0
|
||||
z = base_z + SHOP_FLOOR_CM + storey_step * storey + 65.0
|
||||
if _spawn(kit["Kit_WindowBay"], unreal.Vector(cx, face_y, z),
|
||||
f"{prefix}window_{bay}_{storey}", kit_material("Kit_WindowBay")):
|
||||
placed += 1
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit the DemoMap for staged actors that would render wrong.
|
||||
|
||||
Checks that are cheap and catch the failures this project actually had:
|
||||
|
||||
1. **No unmaterialed actors.** 604 actors once rendered with the default material
|
||||
because the demo builder deleted and recreated the site materials, orphaning
|
||||
every reference; the map looked merely "a bit flat".
|
||||
2. **Placement spans.** Each verified frontage building is probed against
|
||||
`Tools/ue_placement.py`; a building 13 m off its footprint still renders fine,
|
||||
which is exactly why it needs a number, not an eye.
|
||||
3. **No duplicate actor labels.** Two buildings' kit pieces sharing a label made
|
||||
the outliner ambiguous and hid one of them.
|
||||
|
||||
UnrealEditor-Cmd <uproject> -run=pythonscript \
|
||||
-script=Tools/ue_audit_scene.py -nullrhi -unattended -nopause -stdout
|
||||
|
||||
Result file: `/tmp/gigline/scene-audit.txt`. Exit code from the commandlet is
|
||||
always 0, so **read the file** — `visual_loop.py --verify` does not depend on it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import os
|
||||
import sys
|
||||
|
||||
import unreal
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ue_placement # noqa: E402
|
||||
|
||||
MAP = "/Game/Hollywood/DemoMap"
|
||||
RESULT = "/tmp/gigline/scene-audit.txt"
|
||||
|
||||
lines: list[str] = []
|
||||
failures = 0
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
lines.append(message)
|
||||
print(message)
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
global failures
|
||||
failures += 1
|
||||
log("FAIL " + message)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not unreal.EditorLevelLibrary.load_level(MAP):
|
||||
log(f"could not load {MAP}")
|
||||
return
|
||||
|
||||
actors = unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
|
||||
# 1. materials
|
||||
unmaterialed = []
|
||||
for actor in actors:
|
||||
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
|
||||
if not comp or not comp.static_mesh:
|
||||
continue
|
||||
if comp.get_material(0) is None:
|
||||
unmaterialed.append(actor.get_actor_label())
|
||||
log(f"mesh actors: {sum(1 for a in actors if a.get_component_by_class(unreal.StaticMeshComponent))}")
|
||||
if unmaterialed:
|
||||
fail(f"{len(unmaterialed)} actor(s) with no material, e.g. {unmaterialed[:6]}")
|
||||
else:
|
||||
log("OK every staged mesh has a material")
|
||||
|
||||
# 2. placement spans
|
||||
for actor in actors:
|
||||
label = actor.get_actor_label()
|
||||
key = None
|
||||
if label.startswith("Frontage_SM_"):
|
||||
name = label[len("Frontage_SM_"):]
|
||||
key = {"Hollywood_6922": "6922", "Hollywood_6902": "6902",
|
||||
"ElCapitanTheatre": "6838", "Budget_6822": "6822",
|
||||
"Hollywood_6806": "6806", "LeeDrug_6800": "6800"}.get(name)
|
||||
elif label == ue_placement.HERO_ACTOR:
|
||||
key = "hero"
|
||||
if not key:
|
||||
continue
|
||||
origin, extent = actor.get_actor_bounds(False)
|
||||
lo, hi = origin.x - extent.x, origin.x + extent.x
|
||||
problem = ue_placement.describe_span_failure(key, lo, hi)
|
||||
if problem:
|
||||
fail(problem)
|
||||
else:
|
||||
log(f"OK {key}: X {lo:.0f}..{hi:.0f}")
|
||||
|
||||
# 3. duplicate labels
|
||||
counts = collections.Counter(a.get_actor_label() for a in actors)
|
||||
duplicates = {k: v for k, v in counts.items() if v > 1}
|
||||
if duplicates:
|
||||
sample = list(duplicates.items())[:6]
|
||||
fail(f"{len(duplicates)} duplicated actor label(s), e.g. {sample}")
|
||||
else:
|
||||
log("OK no duplicate actor labels")
|
||||
|
||||
log(f"== {failures} failure(s) ==")
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as exc: # noqa: BLE001 - surface in the result file
|
||||
import traceback
|
||||
log(f"FAILED: {type(exc).__name__}: {exc}")
|
||||
log(traceback.format_exc())
|
||||
|
||||
os.makedirs(os.path.dirname(RESULT), exist_ok=True)
|
||||
with open(RESULT, "w", encoding="utf-8") as handle:
|
||||
handle.write("\n".join(lines) + "\n")
|
||||
@@ -23,6 +23,8 @@ import unreal
|
||||
MAP = "/Game/Hollywood/DemoMap"
|
||||
# Repository root, so the texture generator can be invoked by path.
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ue_placement # noqa: E402
|
||||
RESULT = "/tmp/gigline/demo-map.txt"
|
||||
lines = []
|
||||
|
||||
@@ -116,14 +118,23 @@ def ensure_material(mat_name, spec, textures=None):
|
||||
does not read as one flat specular value.
|
||||
"""
|
||||
path = f"/Game/Hollywood/Site/{mat_name}"
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(path):
|
||||
# Rebuilt every run so a look change cannot be masked by a stale asset.
|
||||
unreal.EditorAssetLibrary.delete_asset(path)
|
||||
|
||||
mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
||||
mat_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew())
|
||||
lib = unreal.MaterialEditingLibrary
|
||||
|
||||
# Update the existing material IN PLACE rather than deleting and recreating it.
|
||||
# Deleting the asset to force a look rebuild silently orphaned every reference:
|
||||
# after one demo-builder run, all 598 kit actors and the theatre trim pointed at
|
||||
# deleted materials and rendered with the default one (604 actors with no
|
||||
# material), while the map itself looked merely "a bit flat".
|
||||
mat = unreal.EditorAssetLibrary.load_asset(path)
|
||||
if mat is None:
|
||||
mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
||||
mat_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew())
|
||||
else:
|
||||
# Clear the old graph so a removed node cannot survive a look change.
|
||||
# `Material.expressions` is protected from Python, so use the editing
|
||||
# library's own deleter rather than reading the array.
|
||||
lib.delete_all_material_expressions(mat)
|
||||
|
||||
def node(cls):
|
||||
return lib.create_material_expression(mat, cls)
|
||||
|
||||
@@ -401,7 +412,7 @@ def build():
|
||||
# the NORTH side of the street, which only looked right while it stood alone.
|
||||
hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade")
|
||||
if hero:
|
||||
actor = spawn(unreal.StaticMeshActor, unreal.Vector(-13336.0, 1307.0, 0.0), "Hero_Facade")
|
||||
actor = spawn(unreal.StaticMeshActor, unreal.Vector(*ue_placement.HERO_LOCATION_CM), ue_placement.HERO_ACTOR)
|
||||
if actor:
|
||||
actor.static_mesh_component.set_static_mesh(hero)
|
||||
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC)
|
||||
|
||||
@@ -19,14 +19,19 @@ import unreal
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ue_common # noqa: E402
|
||||
import ue_placement # noqa: E402
|
||||
|
||||
FBX = "/tmp/gigline/hero-facade.fbx"
|
||||
DEST = "/Game/Hollywood/ElCapitan"
|
||||
ACTOR_LABEL = "Hero_Facade"
|
||||
ACTOR_LABEL = ue_placement.HERO_ACTOR
|
||||
LEVEL = "/Game/Hollywood/DemoMap"
|
||||
|
||||
# Unreal centimetres: south-east corner of the façade, on the building line.
|
||||
PLACEMENT_CM = (-12024.5, -1450.0, 0.0)
|
||||
# Placement lives in Tools/ue_placement.py. It used to be a literal here, and it
|
||||
# drifted from ue_build_demo_map.py: this file kept the pre-0.1.0 position
|
||||
# (-12024.5, -1450) while the builder staged (-13336, 1307), so running the importer
|
||||
# on its own moved the hero 13 m east and onto the north side of the boulevard.
|
||||
PLACEMENT_CM = ue_placement.HERO_LOCATION_CM
|
||||
|
||||
RESULT = "/tmp/gigline/hero-facade-import.txt"
|
||||
lines = []
|
||||
@@ -118,6 +123,17 @@ def measure_and_place():
|
||||
comp.set_static_mesh(asset)
|
||||
comp.set_mobility(unreal.ComponentMobility.STATIC)
|
||||
log(f"placed {ACTOR_LABEL} at {PLACEMENT_CM} (cm) on {LEVEL}")
|
||||
|
||||
# Prove it landed on its verified footprint, not merely that it spawned.
|
||||
box = asset.get_bounding_box() if asset else None
|
||||
if box:
|
||||
lo = box.min.x + PLACEMENT_CM[0]
|
||||
hi = box.max.x + PLACEMENT_CM[0]
|
||||
problem = ue_placement.describe_span_failure("hero", lo, hi)
|
||||
if problem:
|
||||
log(f"PLACEMENT ERROR: {problem}")
|
||||
else:
|
||||
log(f"span ok: X {lo:.0f}..{hi:.0f}")
|
||||
_make_map_writable(LEVEL)
|
||||
saved = unreal.EditorLoadingAndSavingUtils.save_map(
|
||||
unreal.EditorLevelLibrary.get_editor_world(), LEVEL)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Single source of truth for where staged actors belong in the DemoMap.
|
||||
|
||||
This exists because the same value was written in two places and they drifted:
|
||||
`ue_hero_facade_import.py` kept the pre-`0.1.0` position `(-12024.5, -1450)` while
|
||||
`ue_build_demo_map.py` staged the hero at `(-13336, 1307)`. Running the importer on
|
||||
its own therefore moved the hero **13 m east and onto the north side of the
|
||||
boulevard** — and nothing complained, because a displaced building still renders.
|
||||
|
||||
The values are Unreal centimetres, absolute world space. Every one of them is
|
||||
verified against `Geo/verification/2026-11-06-el-capitan-block.md` and probed by
|
||||
`Tools/visual_loop.py --verify` (via the demo screenshot expectations) and by the
|
||||
world-span probes recorded in `MEMORY.md`.
|
||||
|
||||
Keep this file boring: constants and one validator, no `unreal` import, so it can
|
||||
be unit-tested on the host.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# The hero façade sits INSIDE the frontage gap between 6902 (ends at -17749) and
|
||||
# the El Capitan Theatre (starts at -12082), so its actor origin is the measured
|
||||
# building line. The mesh is authored with its front plane 7 cm ahead of the actor
|
||||
# origin, which is why the X here is the frontage centre and Y is the building line.
|
||||
HERO_ACTOR = "Hero_Facade"
|
||||
HERO_LOCATION_CM = (-13336.0, 1307.0, 0.0)
|
||||
|
||||
# Tolerance for the span probe: the verified footprints are within ~1 m.
|
||||
SPAN_TOLERANCE_CM = 100.0
|
||||
|
||||
# Expected world span per frontage actor (min X, max X), from the block
|
||||
# verification notes. Used to prove a staged building is on its own footprint
|
||||
# rather than merely present.
|
||||
FRONTAGE_SPANS_CM = {
|
||||
"6922": (-24276.0, -17700.0),
|
||||
"6902": (-17749.0, -14591.0),
|
||||
"hero": (-14644.0, -12028.0),
|
||||
"6838": (-12082.0, -8210.0),
|
||||
"6822": (-7854.0, -7252.0),
|
||||
"6806": (-6608.0, -3307.0),
|
||||
"6800": (-3320.0, -1437.0),
|
||||
}
|
||||
|
||||
|
||||
def span_error(name: str, observed_min: float, observed_max: float):
|
||||
"""Return (expected_min, expected_max, ok) for an observed building span.
|
||||
|
||||
`name` is a key of `FRONTAGE_SPANS_CM`; `ok` is False when either edge is more
|
||||
than `SPAN_TOLERANCE_CM` from the verified value.
|
||||
"""
|
||||
if name not in FRONTAGE_SPANS_CM:
|
||||
raise KeyError(f"unknown building {name!r}; known: {sorted(FRONTAGE_SPANS_CM)}")
|
||||
exp_min, exp_max = FRONTAGE_SPANS_CM[name]
|
||||
ok = (abs(observed_min - exp_min) <= SPAN_TOLERANCE_CM
|
||||
and abs(observed_max - exp_max) <= SPAN_TOLERANCE_CM)
|
||||
return exp_min, exp_max, ok
|
||||
|
||||
|
||||
def describe_span_failure(name: str, observed_min: float, observed_max: float) -> str:
|
||||
"""A one-line message for a misplaced building, or "" when it is correct."""
|
||||
exp_min, exp_max, ok = span_error(name, observed_min, observed_max)
|
||||
if ok:
|
||||
return ""
|
||||
return (f"{name}: span X {observed_min:.0f}..{observed_max:.0f} does not match the "
|
||||
f"verified {exp_min:.0f}..{exp_max:.0f} (±{SPAN_TOLERANCE_CM:.0f} cm)")
|
||||
Reference in new issue
Block a user