LOT Q0 : socle du démonstrateur Quelque part en 2035

Scaffold Vite + TypeScript + Three.js (scène vide : sol + boîte, marqueur de build),
store et encodage de l'état dans l'URL (5 tests), lint de frontières entre modules,
pipeline d'extraction Rhino (graine du 26/09) et première géométrie v0,
deploy.sh statique (releases + lien symbolique + smoke), AGENTS.md, RUNBOOK, licences MIT + CC BY-SA 4.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jules Nény
2026-09-27 15:21:40 +02:00
co-authored by Claude Fable 5.1
commit 2e249f1027
33 changed files with 2026 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
# quartier-seed — graine du repo `quartier` (Quartier 3D TMIP)
Produit le 2026-09-26 par la session de cadrage (Fable 5.1), hors Dropbox, pas encore un repo git.
LOT-Q0 crée le repo Gitea `jules/quartier` et adopte ce dossier (comme `reinnover` a adopté son scaffold).
- `pipeline/extract_quartier.py <plan.3dm> <sortie.json>` : lit le plan Rhino 2D du concours 2020 avec `rhino3dm`
(calques PARCELLES, B_EAU noues, C_SENTIER, 7_TOPO, planté ; maisons = quadrilatères 40-400 m² compacts
sur les calques 3_ TRAITS VUS / 4_ TRAITS VUS FIINS). Unités cm → m.
- `pipeline/plot_quartier.py` : contrôle visuel → `extraction-quartier.png`.
- `data/quartier.geo.v0.json` : première extraction (79 parcelles, 11 noues, 9 sentiers, 39 courbes topo,
5 zones plantées, 27 maisons candidates sur ~40 visibles). La voirie n'est pas sur un calque : à dériver ou tracer (LOT-Q1).
- Source : `2 CASQUETTES/Architecte/TMIP/Archives TMIP/04_ArchiJeunes/02_rhino,autocad,illus/PL-quartier transformé/plan de quartier-concours VP TRANSFO.3dm`
(le vault n'est pas lu par le build : copier le `.3dm` dans `sources/` du repo, hors Dropbox, au LOT-Q1).
Venv : `python3 -m venv v && ./v/bin/pip install -r pipeline/requirements.txt`.
Pilote : vault `1 PROJETS/TMIP/REinNOVER/Quartier 3D/PILOTE - Quartier 3D TMIP.md`.
+34
View File
@@ -0,0 +1,34 @@
import sys, json, math, rhino3dm
from shapely.geometry import Polygon
p = sys.argv[1]; m = rhino3dm.File3dm.Read(p)
L = {i: l.Name for i, l in enumerate(m.Layers)}
def pts(c):
n = 64 if not isinstance(c,(rhino3dm.PolylineCurve,)) else c.PointCount
if isinstance(c, rhino3dm.PolylineCurve):
return [(c.Point(i).X/100, c.Point(i).Y/100) for i in range(c.PointCount)]
pl = c.ToPolyline(0.0,0.0,0.1,5.0,0.0,0.0,0.0,0.0,True) if hasattr(c,'ToPolyline') else None
if pl is None:
d=c.Domain; return [(c.PointAt(d.T0+(d.T1-d.T0)*i/n).X/100, c.PointAt(d.T0+(d.T1-d.T0)*i/n).Y/100) for i in range(n+1)]
return [(pl[i].X/100, pl[i].Y/100) for i in range(pl.Count)]
out = {"parcelles":[], "noues":[], "sentiers":[], "topo":[], "plante":[], "maisons_candidates":[], "autres_fermes":[]}
for o in m.Objects:
g=o.Geometry; ln=L.get(o.Attributes.LayerIndex,"")
if not isinstance(g, rhino3dm.Curve): continue
try: P=pts(g)
except Exception: continue
if len(P)<2: continue
if ln=="7_TOPO": out["topo"].append(P); continue
if not g.IsClosed: continue
try: poly=Polygon(P); a=poly.area
except Exception: continue
if ln=="PARCELLES": out["parcelles"].append(P)
elif ln=="B_EAU noues": out["noues"].append(P)
elif ln=="C_SENTIER": out["sentiers"].append(P)
elif ln=="planté": out["plante"].append(P)
elif ln in ("0","caché","3_ TRAITS VUS","4_ TRAITS VUS FIINS","traits cachés","Défaut","ombres portées") and 40<=a<=400:
# maison candidate : quadrilatère-ish compact
mrr=poly.minimum_rotated_rectangle;
if mrr.area>0 and a/mrr.area>0.7: out["maisons_candidates"].append({"layer":ln,"area":round(a,1),"pts":P})
json.dump(out, open(sys.argv[2],"w"))
print({k:len(v) for k,v in out.items()})
import collections; print(collections.Counter(c["layer"] for c in out["maisons_candidates"]))
+19
View File
@@ -0,0 +1,19 @@
import sys, rhino3dm, collections
for p in sys.argv[1:]:
m = rhino3dm.File3dm.Read(p)
if m is None: print("FAIL", p); continue
types = collections.Counter(); dims = collections.Counter()
bb=None
for o in m.Objects:
g=o.Geometry; t=type(g).__name__; types[t]+=1
try:
b=g.GetBoundingBox()
if b.IsValid:
dims['3D' if (b.Max.Z-b.Min.Z)>0.01 else 'flat']+=1
bb = b if bb is None else rhino3dm.BoundingBox(min(bb.Min.X,b.Min.X),min(bb.Min.Y,b.Min.Y),min(bb.Min.Z,b.Min.Z),max(bb.Max.X,b.Max.X),max(bb.Max.Y,b.Max.Y),max(bb.Max.Z,b.Max.Z))
except Exception: pass
print("==", p.split('/')[-1], "| unités:", m.Settings.ModelUnitSystem, "| objets:", len(m.Objects), "| calques:", len(m.Layers))
print(" types:", dict(types.most_common(8)))
print(" 3D vs plat:", dict(dims))
if bb: print(" emprise: %.0f x %.0f x %.0f" % (bb.Max.X-bb.Min.X, bb.Max.Y-bb.Min.Y, bb.Max.Z-bb.Min.Z))
print(" calques:", [l.Name for l in m.Layers][:25])
+12
View File
@@ -0,0 +1,12 @@
import sys, rhino3dm, collections
m = rhino3dm.File3dm.Read(sys.argv[1])
layers = {i: l.Name for i, l in enumerate(m.Layers)}
cnt = collections.Counter(); closed = collections.Counter(); blocks=collections.Counter()
for o in m.Objects:
li = o.Attributes.LayerIndex; g = o.Geometry
cnt[li]+=1
if isinstance(g, rhino3dm.Curve) and g.IsClosed: closed[li]+=1
if isinstance(g, rhino3dm.InstanceReference): blocks[li]+=1
for li,n in cnt.most_common():
print(f"{n:6d} obj | {closed[li]:5d} fermés | {blocks[li]:5d} blocs | {layers.get(li)}")
print("blocs définis:", [d.Name for d in m.InstanceDefinitions][:30])
+11
View File
@@ -0,0 +1,11 @@
import json, matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt
d=json.load(open("quartier.json"))
fig,ax=plt.subplots(figsize=(14,12)); ax.set_aspect('equal'); ax.set_facecolor("#fbfaf5")
for P in d["topo"]: xs,ys=zip(*P); ax.plot(xs,ys,color="#d8d2c0",lw=0.5)
for P in d["parcelles"]: xs,ys=zip(*P); ax.plot(xs,ys,color="#8fbf6a",lw=0.8)
for P in d["sentiers"]: xs,ys=zip(*P); ax.fill(xs,ys,color="#e6c77a",alpha=0.6)
for P in d["noues"]: xs,ys=zip(*P); ax.fill(xs,ys,color="#6fb3d9",alpha=0.7)
for P in d["plante"]: xs,ys=zip(*P); ax.fill(xs,ys,color="#b7d9a0",alpha=0.5)
for c in d["maisons_candidates"]: xs,ys=zip(*c["pts"]); ax.fill(xs,ys,color="#444",alpha=0.85)
ax.set_title("Extraction rhino3dm — plan quartier TRANSFO 2020 (m) : %d parcelles, %d noues, %d sentiers, %d maisons candidates"%(len(d["parcelles"]),len(d["noues"]),len(d["sentiers"]),len(d["maisons_candidates"])),fontsize=10)
plt.savefig("extraction-quartier.png",dpi=110,bbox_inches="tight"); print("ok")
+3
View File
@@ -0,0 +1,3 @@
rhino3dm
shapely
matplotlib