import json, math, re, sys, heapq
from copy import deepcopy
ROAD_NAME = "Prop00-GR"
ROAD_MAT = 4
WHITE_MAT = 3
YELLOW_NAME = "Props3258"
START_POS = {"x": 9482.810546875, "y": -630.2581787109375, "z": -691.112548828125}
FINISH_POS = {"x": 8385.75, "y": -625.75, "z": 7091.0}
TEMPLATE_T_OFFSETS = [-10.75, 0.5, 11.75, 23.0, 27.75]
TEMPLATE_PERIOD = 50.0
YELLOW_RIGHT_OFFSETS = [0.0, 1.0]
# police sign down (your clean reference)
Q_DOWN = {"x": 0.7071068286895752, "y": 0.0, "z": 0.0, "w": -0.7071068286895752}
# +90 yaw around UP axis (fix yellow long-side direction)
Q_YAW90 = {"x": 0.0, "y": 0.7071068286895752, "z": 0.0, "w": 0.7071068286895752}
WHITE_INSET = 2.0
WHITE_LEN = 1
YELLOW_LIFT = 0.52
WHITE_LIFT = 0.07
HEIGHT_STEP_LIFT = 0.5
K_NEIGHBORS = 4
MAX_EDGE_DIST = 4000.0
def load_loose_json(path):
s = open(path, "r", encoding="utf-8").read()
s = re.sub(r",\s*(\]|\})", r"\1", s)
return json.loads(s)
def save_strict_json(path, data):
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def detect_list_key(data):
if isinstance(data, dict):
for k, v in data.items():
if isinstance(v, list) and any(isinstance(x, dict) and "itemName" in x for x in v):
return k
if isinstance(data, list):
return None
raise RuntimeError("Could not find objects list key.")
def v2_dist(a, b):
dx = a["x"] - b["x"]
dz = a["z"] - b["z"]
return math.sqrt(dx*dx + dz*dz)
def norm2(x, z):
d = math.sqrt(x*x + z*z)
if d < 1e-12:
return (0.0, 0.0)
return (x/d, z/d)
def norm3(x, y, z):
d = math.sqrt(x*x + y*y + z*z)
if d < 1e-12:
return (0.0, 0.0, 0.0)
return (x/d, y/d, z/d)
def dot3(ax, ay, az, bx, by, bz):
return ax*bx + ay*by + az*bz
def cross3(ax, ay, az, bx, by, bz):
return (ay*bz - az*by, az*bx - ax*bz, ax*by - ay*bx)
def quat_norm(q):
x,y,z,w = q["x"],q["y"],q["z"],q["w"]
d = math.sqrt(x*x+y*y+z*z+w*w)
if d < 1e-12:
return {"x":0.0,"y":0.0,"z":0.0,"w":1.0}
return {"x":x/d,"y":y/d,"z":z/d,"w":w/d}
def quat_mul(a, b):
ax,ay,az,aw = a["x"],a["y"],a["z"],a["w"]
bx,by,bz,bw = b["x"],b["y"],b["z"],b["w"]
return {
"w": aw*bw - ax*bx - ay*by - az*bz,
"x": aw*bx + ax*bw + ay*bz - az*by,
"y": aw*by - ax*bz + ay*bw + az*bx,
"z": aw*bz + ax*by - ay*bx + az*bw,
}
def quat_rotate_vec(q, vx, vy, vz):
qn = quat_norm(q)
x, y, z, w = qn["x"], qn["y"], qn["z"], qn["w"]
tx = 2*(y*vz - z*vy)
ty = 2*(z*vx - x*vz)
tz = 2*(x*vy - y*vx)
vpx = vx + w*tx + (y*tz - z*ty)
vpy = vy + w*ty + (z*tx - x*tz)
vpz = vz + w*tz + (x*ty - y*tx)
return (vpx, vpy, vpz)
def quat_from_basis(right, up, fwd):
rx,ry,rz = right
ux,uy,uz = up
fx,fy,fz = fwd
m00, m01, m02 = rx, ux, fx
m10, m11, m12 = ry, uy, fy
m20, m21, m22 = rz, uz, fz
tr = m00 + m11 + m22
if tr > 0.0:
S = math.sqrt(tr + 1.0) * 2.0
w = 0.25 * S
x = (m21 - m12) / S
y = (m02 - m20) / S
z = (m10 - m01) / S
elif (m00 > m11) and (m00 > m22):
S = math.sqrt(1.0 + m00 - m11 - m22) * 2.0
w = (m21 - m12) / S
x = 0.25 * S
y = (m01 + m10) / S
z = (m02 + m20) / S
elif m11 > m22:
S = math.sqrt(1.0 + m11 - m00 - m22) * 2.0
w = (m02 - m20) / S
x = (m01 + m10) / S
y = 0.25 * S
z = (m12 + m21) / S
else:
S = math.sqrt(1.0 + m22 - m00 - m11) * 2.0
w = (m10 - m01) / S
x = (m02 + m20) / S
y = (m12 + m21) / S
z = 0.25 * S
return quat_norm({"x": x, "y": y, "z": z, "w": w})
def is_road(it):
return it.get("itemName") == ROAD_NAME and (it.get("customParameters") or {}).get("cMat") == ROAD_MAT
def is_white(it):
return it.get("itemName") == ROAD_NAME and (it.get("customParameters") or {}).get("cMat") == WHITE_MAT
def is_yellow(it):
return it.get("itemName") == YELLOW_NAME
def closest_road_index(roads, pos):
best_i, best_d = None, 1e30
for i, r in enumerate(roads):
d = v2_dist(r["vectorPosition"], pos)
if d < best_d:
best_d = d
best_i = i
return best_i
def build_knn_neighbors(roads):
n = len(roads)
neigh = [[] for _ in range(n)]
positions = [r["vectorPosition"] for r in roads]
for i in range(n):
pi = positions[i]
dists = []
for j in range(n):
if i == j:
continue
pj = positions[j]
d = v2_dist(pi, pj)
if d <= MAX_EDGE_DIST:
dists.append((d, j))
dists.sort(key=lambda x: x[0])
for d, j in dists[:K_NEIGHBORS]:
neigh[i].append((j, d))
for i in range(n):
for j, d in neigh[i]:
if all(x[0] != i for x in neigh[j]):
neigh[j].append((i, d))
return neigh
def dijkstra_path(neigh, start_i, end_i):
n = len(neigh)
dist = [1e30]*n
prev = [-1]*n
dist[start_i] = 0.0
pq = [(0.0, start_i)]
while pq:
d, v = heapq.heappop(pq)
if d != dist[v]:
continue
if v == end_i:
break
for to, w in neigh[v]:
nd = d + w
if nd < dist[to]:
dist[to] = nd
prev[to] = v
heapq.heappush(pq, (nd, to))
if start_i != end_i and prev[end_i] == -1:
return []
path = []
cur = end_i
while cur != -1:
path.append(cur)
if cur == start_i:
break
cur = prev[cur]
path.reverse()
return path
def global_dash_S_values(S_total):
S_vals = []
k_max = int(math.ceil((S_total - min(TEMPLATE_T_OFFSETS)) / TEMPLATE_PERIOD)) + 3
for k in range(-3, k_max + 1):
base = k * TEMPLATE_PERIOD
for off in TEMPLATE_T_OFFSETS:
s = base + off
if -1e-6 <= s <= S_total + 1e-6:
S_vals.append(s)
S_vals.sort()
return S_vals
def build_stick_basis_quat(forward_xz, roadRot):
fx, fz = forward_xz
ux, uy, uz = quat_rotate_vec(roadRot, 0, 1, 0)
ux, uy, uz = norm3(ux, uy, uz)
f3x, f3y, f3z = fx, 0.0, fz
dot = dot3(f3x, f3y, f3z, ux, uy, uz)
f3x, f3y, f3z = (f3x - dot*ux, f3y - dot*uy, f3z - dot*uz)
f3x, f3y, f3z = norm3(f3x, f3y, f3z)
rx, ry, rz = cross3(ux, uy, uz, f3x, f3y, f3z)
rx, ry, rz = norm3(rx, ry, rz)
f3x, f3y, f3z = cross3(rx, ry, rz, ux, uy, uz)
f3x, f3y, f3z = norm3(f3x, f3y, f3z)
qBasis = quat_from_basis((rx,ry,rz), (ux,uy,uz), (f3x,f3y,f3z))
return qBasis, (ux,uy,uz), (rx,ry,rz)
def main(in_path, out_path):
data = load_loose_json(in_path)
key = detect_list_key(data)
items = data if key is None else data[key]
kept = []
removed = 0
for it in items:
if isinstance(it, dict) and (is_yellow(it) or is_white(it)):
removed += 1
continue
kept.append(it)
roads = [it for it in kept if isinstance(it, dict) and is_road(it)]
if not roads:
raise RuntimeError("No roads found (Prop00-GR cMat=4).")
neigh = build_knn_neighbors(roads)
s_i = closest_road_index(roads, START_POS)
e_i = closest_road_index(roads, FINISH_POS)
path_idx = dijkstra_path(neigh, s_i, e_i)
if not path_idx:
raise RuntimeError("No path found. Increase MAX_EDGE_DIST or K_NEIGHBORS.")
# Build segments with length = projected center-to-center distance (fix gaps)
seg_info = []
S = 0.0
for k_i, idx in enumerate(path_idx):
road = roads[idx]
rpos = road["vectorPosition"]
# forward from next/prev road (XZ)
if k_i < len(path_idx) - 1:
nxt = roads[path_idx[k_i+1]]["vectorPosition"]
fx, fz = norm2(nxt["x"] - rpos["x"], nxt["z"] - rpos["z"])
# seg length = projection of delta onto forward
dx = nxt["x"] - rpos["x"]
dz = nxt["z"] - rpos["z"]
seg_len = max(0.0, dx*fx + dz*fz)
else:
# last segment: use road cWid as fallback
cp = road.get("customParameters") or {}
fx, fz = (1.0, 0.0) if k_i == 0 else (seg_info[-1]["fx"], seg_info[-1]["fz"])
seg_len = float(cp.get("cWid", 0) or 0)
if seg_len < 0:
seg_len = 0.0
S0 = S
S1 = S + seg_len
seg_info.append({"idx": idx, "fx": fx, "fz": fz, "S0": S0, "S1": S1, "seg_len": seg_len})
S = S1
dash_S = global_dash_S_values(S)
generated = []
for k_i, info in enumerate(seg_info):
road = roads[info["idx"]]
rpos = road["vectorPosition"]
rrot = road["vectorRotation"]
cp = road.get("customParameters") or {}
clen = float(cp.get("cLen", 27) or 27)
cheight = float(cp.get("cHeight", 1) or 1)
fx, fz = info["fx"], info["fz"]
S0, S1 = info["S0"], info["S1"]
seg_len = info["seg_len"]
S_mid = (S0 + S1) / 2.0
qBasis, up, right3 = build_stick_basis_quat((fx, fz), rrot)
ux, uy, uz = up
# right projected to XZ for offsets
rx3, ry3, rz3 = right3
rlen = math.sqrt(rx3*rx3 + rz3*rz3)
if rlen < 1e-6:
rx, rz = (fz, -fx)
rlen2 = math.sqrt(rx*rx + rz*rz)
rx, rz = (rx/rlen2, rz/rlen2) if rlen2 > 1e-6 else (1.0, 0.0)
else:
rx, rz = (rx3/rlen, rz3/rlen)
extra_h = HEIGHT_STEP_LIFT * (cheight - 1.0)
# whites
edge = max(0.0, (clen/2.0) - WHITE_INSET)
for sign in (-1.0, +1.0):
off = sign * edge
wx = rpos["x"] + rx*off + ux*(WHITE_LIFT + extra_h)
wy = rpos["y"] + uy*(WHITE_LIFT + extra_h)
wz = rpos["z"] + rz*off + uz*(WHITE_LIFT + extra_h)
generated.append({
"itemName": ROAD_NAME,
"vectorPosition": {"x": wx, "y": wy, "z": wz},
"vectorRotation": deepcopy(rrot),
"isKinematics": True,
"mass": 0.0,
"maxFloor": -1,
"customParameters": {
"cMat": WHITE_MAT,
"cLen": WHITE_LEN,
"cWid": cp.get("cWid", 50),
"cHeight": 1
}
})
# yellow rotation = basis * (+90 yaw) * down
qYellowRot = quat_norm(quat_mul(quat_mul(qBasis, Q_YAW90), Q_DOWN))
# yellows
if seg_len > 0:
for s in dash_S:
if s < S0 - 1e-6 or s > S1 + 1e-6:
continue
local_t = s - S_mid
base_x = rpos["x"] + fx * local_t
base_z = rpos["z"] + fz * local_t
for offR in YELLOW_RIGHT_OFFSETS:
yx = base_x + rx*offR + ux*(YELLOW_LIFT + extra_h)
yy = rpos["y"] + uy*(YELLOW_LIFT + extra_h)
yz = base_z + rz*offR + uz*(YELLOW_LIFT + extra_h)
generated.append({
"itemName": YELLOW_NAME,
"vectorPosition": {"x": yx, "y": yy, "z": yz},
"vectorRotation": deepcopy(qYellowRot),
"isKinematics": True,
"mass": 0.0,
"maxFloor": -1,
"customParameters": {}
})
out_items = kept + generated
if key is None:
out_data = out_items
else:
out_data = data
out_data[key] = out_items
save_strict_json(out_path, out_data)
print("Removed old whites/yellows:", removed)
print("Roads total:", len(roads))
print("Roads in path:", len(path_idx))
print("Path length S:", S)
print("Generated props:", len(generated))
print("Output:", out_path)
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: py gen_lines_stick_yellow_fix90_nogaps.py <input.json> <output.json>")
sys.exit(1)
main(sys.argv[1], sys.argv[2])