Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
·
6737b3a
1
Parent(s):
66f720d
re-added collision. Hardened app.py. Some more small changes.
Browse files- app.py +423 -188
- static/index.html +179 -790
app.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import logging, faulthandler, sys, time,
|
2 |
faulthandler.enable()
|
3 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
4 |
logger = logging.getLogger("jax_ik_server")
|
@@ -38,8 +38,10 @@ def download_and_setup_files():
|
|
38 |
if not os.path.isdir(pepper_dir):
|
39 |
logger.info("Downloading Pepper model...")
|
40 |
try:
|
41 |
-
r = requests.get("https://uni-bielefeld.sciebo.de/s/98Sy5s143XgNntb/download", stream=True)
|
42 |
-
|
|
|
|
|
43 |
except Exception as e:
|
44 |
logger.warning(f"Pepper download failed: {e}")
|
45 |
# SMPLX
|
@@ -47,7 +49,8 @@ def download_and_setup_files():
|
|
47 |
if not os.path.isfile(smplx_file):
|
48 |
logger.info("Downloading SMPLX model...")
|
49 |
try:
|
50 |
-
r = requests.get("https://uni-bielefeld.sciebo.de/s/B5StwQdiR4DW5mc/download")
|
|
|
51 |
open(smplx_file, "wb").write(r.content)
|
52 |
except Exception as e:
|
53 |
logger.warning(f"SMPLX download failed: {e}")
|
@@ -56,51 +59,189 @@ class IKServer:
|
|
56 |
def __init__(self, args):
|
57 |
self.args = args
|
58 |
self.solve_lock = threading.Lock()
|
|
|
59 |
self.process = psutil.Process(os.getpid())
|
60 |
|
61 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
self.solver_cache = {}
|
63 |
self.urdf_solver_cache = {}
|
64 |
-
self.max_cache_size = 5
|
65 |
self.cache_access_order = []
|
66 |
self.urdf_cache_access_order = []
|
|
|
67 |
|
68 |
# Animation buffers
|
69 |
self.animation_frames_agent = []
|
70 |
self.animation_frames_urdf = []
|
71 |
|
72 |
-
# FastAPI
|
73 |
self.app = FastAPI()
|
74 |
self.app.add_middleware(
|
75 |
CORSMiddleware,
|
76 |
allow_origins=["*"], allow_credentials=True,
|
77 |
-
allow_methods=["*"], allow_headers=["*"]
|
78 |
)
|
79 |
os.makedirs("static", exist_ok=True)
|
80 |
os.makedirs("files", exist_ok=True)
|
81 |
self.app.mount("/static", StaticFiles(directory="static"), name="static")
|
82 |
self.app.mount("/files", StaticFiles(directory="files"), name="files")
|
83 |
|
84 |
-
#
|
85 |
self._init_agent()
|
86 |
self._setup_agent_objectives()
|
87 |
self._init_urdf()
|
88 |
self._setup_urdf_objectives()
|
89 |
|
90 |
-
# Cleanup
|
91 |
self.last_cleanup_time = time.time()
|
92 |
self.cleanup_interval = 30
|
93 |
|
94 |
-
self.agent_solve_counter = 0 # added
|
95 |
-
self.urdf_solve_counter = 0 # added
|
96 |
-
|
97 |
self._register_routes()
|
98 |
logger.info("Server ready.")
|
99 |
-
# NEW warmup thread to JIT both solvers early
|
100 |
if getattr(self.args, 'warmup', True):
|
101 |
threading.Thread(target=self._warmup_all, daemon=True).start()
|
102 |
|
103 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
def _warmup_agent(self):
|
105 |
try:
|
106 |
mand = [DistanceObjTraj(target_points=np.array([0.0,0.2,0.35]), bone_name=self.current_end_effector, use_head=True, weight=1.0)]
|
@@ -128,61 +269,9 @@ class IKServer:
|
|
128 |
self._warmup_urdf()
|
129 |
logger.info(f"Warmup finished in {time.time()-t0:.2f}s")
|
130 |
|
131 |
-
# ---------- Cache ----------
|
132 |
-
def _evict_lru(self, is_urdf=False):
|
133 |
-
cache = self.urdf_solver_cache if is_urdf else self.solver_cache
|
134 |
-
order = self.urdf_cache_access_order if is_urdf else self.cache_access_order
|
135 |
-
if not order: return
|
136 |
-
key = order.pop(0)
|
137 |
-
cache.pop(key, None)
|
138 |
-
gc.collect()
|
139 |
-
|
140 |
-
def _cache_key(self, bones, num_steps): return tuple(sorted(bones)) + (int(num_steps),)
|
141 |
-
|
142 |
-
def _get_solver(self, bones, is_urdf=False, num_steps=None):
|
143 |
-
if num_steps is None:
|
144 |
-
num_steps = self.args.num_steps
|
145 |
-
key = self._cache_key(bones, num_steps)
|
146 |
-
cache = self.urdf_solver_cache if is_urdf else self.solver_cache
|
147 |
-
order = self.urdf_cache_access_order if is_urdf else self.cache_access_order
|
148 |
-
if key in cache:
|
149 |
-
if key in order: order.remove(key)
|
150 |
-
order.append(key)
|
151 |
-
return cache[key]
|
152 |
-
if len(cache) >= self.max_cache_size:
|
153 |
-
self._evict_lru(is_urdf)
|
154 |
-
if is_urdf:
|
155 |
-
solver = InverseKinematicsSolver(
|
156 |
-
model_file=self.urdf_file,
|
157 |
-
controlled_bones=bones,
|
158 |
-
bounds=None,
|
159 |
-
threshold=self.args.threshold,
|
160 |
-
num_steps=int(num_steps),
|
161 |
-
compute_sdf=False,
|
162 |
-
)
|
163 |
-
else:
|
164 |
-
bounds = []
|
165 |
-
for b in bones:
|
166 |
-
if b in self.bounds_dict:
|
167 |
-
lower, upper = self.bounds_dict[b]
|
168 |
-
bounds.extend(list(zip(lower, upper)))
|
169 |
-
else:
|
170 |
-
bounds.extend([(-90, 90)] * 3)
|
171 |
-
solver = InverseKinematicsSolver(
|
172 |
-
model_file=self.args.gltf_file,
|
173 |
-
controlled_bones=bones,
|
174 |
-
bounds=bounds,
|
175 |
-
threshold=self.args.threshold,
|
176 |
-
num_steps=int(num_steps),
|
177 |
-
compute_sdf=False,
|
178 |
-
)
|
179 |
-
cache[key] = solver
|
180 |
-
order.append(key)
|
181 |
-
return solver
|
182 |
-
|
183 |
# ---------- Initialization ----------
|
184 |
def _init_agent(self):
|
185 |
-
self.current_num_steps = self.args.num_steps
|
186 |
basic = InverseKinematicsSolver(
|
187 |
model_file=self.args.gltf_file,
|
188 |
controlled_bones=["left_collar"],
|
@@ -209,7 +298,7 @@ class IKServer:
|
|
209 |
self.animation_frames_agent = self._frames_from_angles([self.initial_rotations], False)
|
210 |
|
211 |
def _init_urdf(self):
|
212 |
-
self.urdf_current_num_steps = self.args.num_steps
|
213 |
self.urdf_file = "files/pepper_description-master/urdf/pepper.urdf"
|
214 |
basic = InverseKinematicsSolver(
|
215 |
model_file=self.urdf_file,
|
@@ -244,6 +333,10 @@ class IKServer:
|
|
244 |
min_clearance=0.0,
|
245 |
weight=1.0,
|
246 |
)
|
|
|
|
|
|
|
|
|
247 |
|
248 |
def _setup_urdf_objectives(self):
|
249 |
self.urdf_distance_obj = DistanceObjTraj(
|
@@ -257,58 +350,34 @@ class IKServer:
|
|
257 |
min_clearance=0.0,
|
258 |
weight=1.0,
|
259 |
)
|
|
|
|
|
|
|
260 |
|
261 |
-
# ----------
|
262 |
-
def configure_agent(self, bones, eff, num_steps=None):
|
263 |
-
if num_steps is None:
|
264 |
-
num_steps = self.current_num_steps
|
265 |
-
if not bones: bones = self.default_controlled_bones
|
266 |
-
if eff not in self.available_bones: eff = self.default_end_effector
|
267 |
-
changed = (
|
268 |
-
bones != self.current_controlled_bones or
|
269 |
-
eff != self.current_end_effector or
|
270 |
-
int(num_steps) != int(self.current_num_steps)
|
271 |
-
)
|
272 |
-
if bones != self.current_controlled_bones or int(num_steps) != int(self.current_num_steps):
|
273 |
-
self.current_controlled_bones = bones
|
274 |
-
self.current_num_steps = int(num_steps)
|
275 |
-
self.solver = self._get_solver(bones, is_urdf=False, num_steps=self.current_num_steps)
|
276 |
-
self.initial_rotations = np.zeros(len(self.solver.controlled_bones)*3, dtype=np.float32)
|
277 |
-
self.best_angles = self.initial_rotations.copy()
|
278 |
-
if eff != self.current_end_effector:
|
279 |
-
self.current_end_effector = eff
|
280 |
-
if changed:
|
281 |
-
self._setup_agent_objectives()
|
282 |
-
return {"controlled_bones": self.current_controlled_bones, "end_effector": self.current_end_effector, "num_steps": self.current_num_steps}
|
283 |
-
|
284 |
-
def configure_urdf(self, bones, eff, num_steps=None):
|
285 |
-
if num_steps is None:
|
286 |
-
num_steps = self.urdf_current_num_steps
|
287 |
-
if not bones: bones = self.urdf_default_controlled_bones
|
288 |
-
if eff not in self.urdf_available_bones: eff = self.urdf_default_end_effector
|
289 |
-
changed = (
|
290 |
-
bones != self.urdf_current_controlled_bones or
|
291 |
-
eff != self.urdf_current_end_effector or
|
292 |
-
int(num_steps) != int(self.urdf_current_num_steps)
|
293 |
-
)
|
294 |
-
if bones != self.urdf_current_controlled_bones or int(num_steps) != int(self.urdf_current_num_steps):
|
295 |
-
self.urdf_current_controlled_bones = bones
|
296 |
-
self.urdf_current_num_steps = int(num_steps)
|
297 |
-
self.urdf_solver = self._get_solver(bones, is_urdf=True, num_steps=self.urdf_current_num_steps)
|
298 |
-
self.urdf_initial_rotations = np.zeros(len(self.urdf_solver.controlled_bones)*3, dtype=np.float32)
|
299 |
-
self.urdf_best_angles = self.urdf_initial_rotations.copy()
|
300 |
-
if eff != self.urdf_current_end_effector:
|
301 |
-
self.urdf_current_end_effector = eff
|
302 |
-
if changed:
|
303 |
-
self._setup_urdf_objectives()
|
304 |
-
return {"controlled_bones": self.urdf_current_controlled_bones, "end_effector": self.urdf_current_end_effector, "num_steps": self.urdf_current_num_steps}
|
305 |
-
|
306 |
-
# ---------- Objectives build ----------
|
307 |
def _build_agent_objectives(self, payload):
|
308 |
tgt = np.array(payload.get("target",[0.0,0.2,0.35]))
|
309 |
self.distance_obj.update_params({"bone_name": self.current_end_effector, "target_points": tgt, "weight": float(payload.get("distance_weight",1.0))})
|
310 |
-
|
311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
312 |
mandatory, optional = [], []
|
313 |
if payload.get("distance_enabled", True): mandatory.append(self.distance_obj)
|
314 |
if payload.get("collision_enabled", False): optional.append(self.collision_obj)
|
@@ -318,7 +387,7 @@ class IKServer:
|
|
318 |
optional.append(CombinedDerivativeObj(max_order=3, weights=[float(payload.get("derivative_weight",0.05))]*3))
|
319 |
elif not payload.get("bone_zero_enabled", True) and not payload.get("derivative_enabled", True):
|
320 |
optional.append(BoneZeroRotationObj(weight=0.01))
|
321 |
-
# Hand
|
322 |
hand_shape = payload.get("hand_shape","None")
|
323 |
hand_position = payload.get("hand_position","None")
|
324 |
params = {
|
@@ -352,8 +421,24 @@ class IKServer:
|
|
352 |
def _build_urdf_objectives(self, payload):
|
353 |
tgt = np.array(payload.get("target",[0.3,0.3,0.35]))
|
354 |
self.urdf_distance_obj.update_params({"bone_name": self.urdf_current_end_effector, "target_points": tgt, "weight": float(payload.get("distance_weight",1.0))})
|
355 |
-
|
356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
357 |
mandatory, optional = [], []
|
358 |
if payload.get("distance_enabled", True): mandatory.append(self.urdf_distance_obj)
|
359 |
if payload.get("collision_enabled", False): optional.append(self.urdf_collision_obj)
|
@@ -365,85 +450,212 @@ class IKServer:
|
|
365 |
optional.append(BoneZeroRotationObj(weight=0.01))
|
366 |
return mandatory, optional, subpoints
|
367 |
|
368 |
-
# ----------
|
369 |
def _frames_from_angles(self, angles_seq, is_urdf):
|
370 |
frames = []
|
371 |
for ang in angles_seq:
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
|
|
|
|
|
|
379 |
return frames
|
380 |
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
387 |
mandatory_objective_functions=tuple(mand),
|
388 |
optional_objective_functions=tuple(opt),
|
389 |
-
ik_points=
|
390 |
verbose=False,
|
391 |
)
|
392 |
-
|
393 |
-
|
394 |
-
if
|
395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
396 |
else:
|
397 |
-
self.
|
|
|
|
|
|
|
398 |
self.agent_solve_counter += 1
|
|
|
|
|
|
|
|
|
399 |
return {
|
|
|
400 |
"solve_time": time.time()-start,
|
401 |
"iterations": steps,
|
402 |
"objective": obj_val,
|
403 |
"frames": len(self.animation_frames_agent),
|
404 |
"solve_id": self.agent_solve_counter,
|
|
|
|
|
405 |
}
|
406 |
|
407 |
def solve_urdf(self, payload, last_only=False):
|
|
|
|
|
408 |
mand, opt, sub = self._build_urdf_objectives(payload)
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
422 |
else:
|
423 |
-
self.
|
|
|
|
|
|
|
424 |
self.urdf_solve_counter += 1
|
|
|
|
|
|
|
425 |
return {
|
|
|
426 |
"solve_time": time.time()-start,
|
427 |
"iterations": steps,
|
428 |
"objective": obj_val,
|
429 |
"frames": len(self.animation_frames_urdf),
|
430 |
"solve_id": self.urdf_solve_counter,
|
|
|
|
|
431 |
}
|
432 |
|
433 |
# ---------- Housekeeping ----------
|
434 |
def _cleanup(self):
|
435 |
now = time.time()
|
436 |
-
if now - self.last_cleanup_time < self.cleanup_interval:
|
437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
438 |
self.last_cleanup_time = now
|
439 |
|
440 |
# ---------- API ----------
|
441 |
def _register_routes(self):
|
442 |
@self.app.get("/")
|
443 |
-
def index():
|
|
|
444 |
|
445 |
@self.app.get("/threejs_viewer")
|
446 |
-
def legacy():
|
|
|
447 |
|
448 |
@self.app.get("/animation")
|
449 |
def animation(request: Request):
|
@@ -463,8 +675,11 @@ class IKServer:
|
|
463 |
"end_effector_choices": self.urdf_available_bones,
|
464 |
"hand_shapes": [],
|
465 |
"hand_positions": [],
|
466 |
-
"max_subpoints": 20,
|
467 |
-
"default_num_steps": self.urdf_current_num_steps,
|
|
|
|
|
|
|
468 |
}
|
469 |
return {
|
470 |
"model":"agent",
|
@@ -479,48 +694,50 @@ class IKServer:
|
|
479 |
"Look 45° X Downwards","Look 45° X Upwards","Look X Inward","Look to Body",
|
480 |
"Arm Down","Arm 45° Down","Arm Flat"
|
481 |
],
|
482 |
-
"max_subpoints": 20,
|
483 |
-
"default_num_steps": self.current_num_steps,
|
|
|
|
|
|
|
484 |
}
|
485 |
|
486 |
@self.app.post("/configure")
|
487 |
async def configure(request: Request):
|
488 |
payload = await request.json()
|
489 |
model = payload.get("model","agent").lower()
|
490 |
-
num_steps =
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
|
|
|
|
|
|
496 |
|
497 |
@self.app.post("/solve")
|
498 |
async def solve(request: Request):
|
499 |
payload = await request.json()
|
500 |
model = payload.get("model","agent").lower()
|
501 |
return_mode = payload.get("frames_mode", "auto")
|
502 |
-
num_steps =
|
503 |
-
subpoints =
|
504 |
last_only = (return_mode != 'all' and subpoints == 1)
|
505 |
self._cleanup()
|
506 |
with self.solve_lock:
|
507 |
try:
|
508 |
if model == "pepper":
|
509 |
-
self.configure_urdf(payload.get("controlled_bones", []),
|
510 |
-
payload.get("end_effector", self.urdf_current_end_effector),
|
511 |
-
num_steps=num_steps)
|
512 |
result = self.solve_urdf(payload, last_only=last_only); result["model"]="pepper"; result["num_steps"] = num_steps
|
513 |
if last_only:
|
514 |
-
frames = list(self.animation_frames_urdf)
|
515 |
elif return_mode == "all" or (return_mode == "auto" and subpoints > 1):
|
516 |
frames = list(self.animation_frames_urdf)
|
517 |
else:
|
518 |
frames = [self.animation_frames_urdf[-1]]
|
519 |
result["frames_data"] = frames
|
520 |
else:
|
521 |
-
self.configure_agent(payload.get("controlled_bones", []),
|
522 |
-
payload.get("end_effector", self.current_end_effector),
|
523 |
-
num_steps=num_steps)
|
524 |
result = self.solve_agent(payload, last_only=last_only); result["model"]="agent"; result["num_steps"] = num_steps
|
525 |
if last_only:
|
526 |
frames = list(self.animation_frames_agent)
|
@@ -529,35 +746,53 @@ class IKServer:
|
|
529 |
else:
|
530 |
frames = [self.animation_frames_agent[-1]]
|
531 |
result["frames_data"] = frames
|
532 |
-
|
|
|
|
|
533 |
except Exception as e:
|
534 |
-
logger.exception("Solve failed")
|
535 |
return JSONResponse({"status":"error","message":str(e)}, status_code=500)
|
536 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
537 |
@self.app.get("/health")
|
538 |
def health():
|
|
|
539 |
return {
|
540 |
"status":"ok",
|
541 |
"agent_frames": len(self.animation_frames_agent),
|
542 |
"urdf_frames": len(self.animation_frames_urdf),
|
543 |
"cache_agent": len(self.solver_cache),
|
544 |
"cache_urdf": len(self.urdf_solver_cache),
|
|
|
|
|
|
|
|
|
|
|
|
|
545 |
}
|
546 |
|
547 |
@self.app.get("/favicon.ico")
|
548 |
-
def favicon():
|
|
|
549 |
|
550 |
# ---------- Main ----------
|
551 |
def main():
|
552 |
parser = configargparse.ArgumentParser(description="Inverse Kinematics Solver - Three.js Web UI", default_config_files=["config.ini"])
|
553 |
-
parser.
|
554 |
-
parser.
|
555 |
-
parser.
|
556 |
-
parser.
|
557 |
-
parser.
|
558 |
-
parser.
|
559 |
-
parser.
|
560 |
-
parser.
|
561 |
args = parser.parse_args()
|
562 |
|
563 |
download_and_setup_files()
|
|
|
1 |
+
import logging, faulthandler, sys, time, os, requests, zipfile, io, gc, threading, psutil, json, configargparse
|
2 |
faulthandler.enable()
|
3 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
4 |
logger = logging.getLogger("jax_ik_server")
|
|
|
38 |
if not os.path.isdir(pepper_dir):
|
39 |
logger.info("Downloading Pepper model...")
|
40 |
try:
|
41 |
+
r = requests.get("https://uni-bielefeld.sciebo.de/s/98Sy5s143XgNntb/download", stream=True)
|
42 |
+
r.raise_for_status()
|
43 |
+
with zipfile.ZipFile(io.BytesIO(r.content)) as z:
|
44 |
+
z.extractall("files/")
|
45 |
except Exception as e:
|
46 |
logger.warning(f"Pepper download failed: {e}")
|
47 |
# SMPLX
|
|
|
49 |
if not os.path.isfile(smplx_file):
|
50 |
logger.info("Downloading SMPLX model...")
|
51 |
try:
|
52 |
+
r = requests.get("https://uni-bielefeld.sciebo.de/s/B5StwQdiR4DW5mc/download")
|
53 |
+
r.raise_for_status()
|
54 |
open(smplx_file, "wb").write(r.content)
|
55 |
except Exception as e:
|
56 |
logger.warning(f"SMPLX download failed: {e}")
|
|
|
59 |
def __init__(self, args):
|
60 |
self.args = args
|
61 |
self.solve_lock = threading.Lock()
|
62 |
+
self.config_lock = threading.Lock()
|
63 |
self.process = psutil.Process(os.getpid())
|
64 |
|
65 |
+
# Failure / resilience counters
|
66 |
+
self.agent_fail_count = 0
|
67 |
+
self.urdf_fail_count = 0
|
68 |
+
self.last_agent_error = None
|
69 |
+
self.last_urdf_error = None
|
70 |
+
self.agent_solve_counter = 0
|
71 |
+
self.urdf_solve_counter = 0
|
72 |
+
self.max_fail_retries = 2 # total attempts (initial + retries)
|
73 |
+
self.circuit_breaker_threshold = 5 # open after N consecutive failures
|
74 |
+
self.circuit_open_until = 0 # epoch time until which solves short-circuit
|
75 |
+
self.circuit_backoff_seconds = 5
|
76 |
+
|
77 |
+
# Numeric safety
|
78 |
+
self.max_num_steps_global = 20000
|
79 |
+
self.max_learning_rate = 5.0
|
80 |
+
|
81 |
+
# Caches for solver reuse (LRU)
|
82 |
self.solver_cache = {}
|
83 |
self.urdf_solver_cache = {}
|
|
|
84 |
self.cache_access_order = []
|
85 |
self.urdf_cache_access_order = []
|
86 |
+
self.max_cache_size = 5
|
87 |
|
88 |
# Animation buffers
|
89 |
self.animation_frames_agent = []
|
90 |
self.animation_frames_urdf = []
|
91 |
|
92 |
+
# FastAPI app
|
93 |
self.app = FastAPI()
|
94 |
self.app.add_middleware(
|
95 |
CORSMiddleware,
|
96 |
allow_origins=["*"], allow_credentials=True,
|
97 |
+
allow_methods=["*"], allow_headers=["*"],
|
98 |
)
|
99 |
os.makedirs("static", exist_ok=True)
|
100 |
os.makedirs("files", exist_ok=True)
|
101 |
self.app.mount("/static", StaticFiles(directory="static"), name="static")
|
102 |
self.app.mount("/files", StaticFiles(directory="files"), name="files")
|
103 |
|
104 |
+
# Initialize models
|
105 |
self._init_agent()
|
106 |
self._setup_agent_objectives()
|
107 |
self._init_urdf()
|
108 |
self._setup_urdf_objectives()
|
109 |
|
110 |
+
# Cleanup timing
|
111 |
self.last_cleanup_time = time.time()
|
112 |
self.cleanup_interval = 30
|
113 |
|
|
|
|
|
|
|
114 |
self._register_routes()
|
115 |
logger.info("Server ready.")
|
|
|
116 |
if getattr(self.args, 'warmup', True):
|
117 |
threading.Thread(target=self._warmup_all, daemon=True).start()
|
118 |
|
119 |
+
# ---------- Utility / Safety ----------
|
120 |
+
def _safe_int(self, v, default, lo=None, hi=None):
|
121 |
+
try:
|
122 |
+
v = int(v)
|
123 |
+
except Exception:
|
124 |
+
v = default
|
125 |
+
if lo is not None and v < lo: v = lo
|
126 |
+
if hi is not None and v > hi: v = hi
|
127 |
+
return v
|
128 |
+
|
129 |
+
def _safe_float(self, v, default, lo=None, hi=None):
|
130 |
+
try:
|
131 |
+
v = float(v)
|
132 |
+
if np.isnan(v) or np.isinf(v): raise ValueError
|
133 |
+
except Exception:
|
134 |
+
v = default
|
135 |
+
if lo is not None and v < lo: v = lo
|
136 |
+
if hi is not None and v > hi: v = hi
|
137 |
+
return v
|
138 |
+
|
139 |
+
def _sanitize_num_steps(self, steps):
|
140 |
+
return self._safe_int(steps, self.args.num_steps, 1, self.max_num_steps_global)
|
141 |
+
|
142 |
+
def _sanitize_bones(self, bones, is_urdf=False):
|
143 |
+
if not isinstance(bones, (list, tuple)):
|
144 |
+
return []
|
145 |
+
allowed = self.urdf_available_bones if is_urdf else self.available_bones
|
146 |
+
return [b for b in bones if b in allowed]
|
147 |
+
|
148 |
+
def _cache_key(self, bones, num_steps):
|
149 |
+
return tuple(sorted(bones)) + (int(num_steps),)
|
150 |
+
|
151 |
+
def _evict_lru(self, is_urdf=False):
|
152 |
+
cache = self.urdf_solver_cache if is_urdf else self.solver_cache
|
153 |
+
order = self.urdf_cache_access_order if is_urdf else self.cache_access_order
|
154 |
+
if not order:
|
155 |
+
return
|
156 |
+
k = order.pop(0)
|
157 |
+
cache.pop(k, None)
|
158 |
+
gc.collect()
|
159 |
+
|
160 |
+
def _invalidate_caches(self):
|
161 |
+
self.solver_cache.clear(); self.urdf_solver_cache.clear()
|
162 |
+
self.cache_access_order.clear(); self.urdf_cache_access_order.clear()
|
163 |
+
gc.collect()
|
164 |
+
|
165 |
+
def _create_solver(self, bones, is_urdf, num_steps):
|
166 |
+
if is_urdf:
|
167 |
+
return InverseKinematicsSolver(
|
168 |
+
model_file=self.urdf_file,
|
169 |
+
controlled_bones=bones,
|
170 |
+
bounds=None,
|
171 |
+
threshold=self.args.threshold,
|
172 |
+
num_steps=int(num_steps),
|
173 |
+
compute_sdf=False,
|
174 |
+
)
|
175 |
+
bounds = []
|
176 |
+
for b in bones:
|
177 |
+
if b in self.bounds_dict:
|
178 |
+
lower, upper = self.bounds_dict[b]
|
179 |
+
bounds.extend(list(zip(lower, upper)))
|
180 |
+
else:
|
181 |
+
bounds.extend([(-90, 90)] * 3)
|
182 |
+
return InverseKinematicsSolver(
|
183 |
+
model_file=self.args.gltf_file,
|
184 |
+
controlled_bones=bones,
|
185 |
+
bounds=bounds,
|
186 |
+
threshold=self.args.threshold,
|
187 |
+
num_steps=int(num_steps),
|
188 |
+
compute_sdf=False,
|
189 |
+
)
|
190 |
+
|
191 |
+
def _get_solver(self, bones, is_urdf=False, num_steps=None):
|
192 |
+
if num_steps is None:
|
193 |
+
num_steps = self.args.num_steps
|
194 |
+
num_steps = self._sanitize_num_steps(num_steps)
|
195 |
+
key = self._cache_key(bones, num_steps)
|
196 |
+
cache = self.urdf_solver_cache if is_urdf else self.solver_cache
|
197 |
+
order = self.urdf_cache_access_order if is_urdf else self.cache_access_order
|
198 |
+
if key in cache:
|
199 |
+
if key in order: order.remove(key)
|
200 |
+
order.append(key)
|
201 |
+
return cache[key]
|
202 |
+
if len(cache) >= self.max_cache_size:
|
203 |
+
self._evict_lru(is_urdf)
|
204 |
+
try:
|
205 |
+
solver = self._create_solver(bones, is_urdf, num_steps)
|
206 |
+
except Exception as e:
|
207 |
+
logger.warning(f"Primary solver creation failure (is_urdf={is_urdf}): {e}; using defaults")
|
208 |
+
bones = self.urdf_default_controlled_bones if is_urdf else self.default_controlled_bones
|
209 |
+
solver = self._create_solver(bones, is_urdf, num_steps)
|
210 |
+
cache[key] = solver
|
211 |
+
order.append(key)
|
212 |
+
return solver
|
213 |
+
|
214 |
+
def _rebuild_solver_safe(self, is_urdf=False, force_defaults=False):
|
215 |
+
try:
|
216 |
+
if is_urdf:
|
217 |
+
bones = self.urdf_default_controlled_bones if force_defaults else self.urdf_current_controlled_bones
|
218 |
+
bones = self._sanitize_bones(bones, True) or self.urdf_default_controlled_bones
|
219 |
+
self.urdf_solver = self._create_solver(bones, True, self.urdf_current_num_steps)
|
220 |
+
self.urdf_initial_rotations = np.zeros(len(self.urdf_solver.controlled_bones)*3, dtype=np.float32)
|
221 |
+
self.urdf_best_angles = self.urdf_initial_rotations.copy()
|
222 |
+
else:
|
223 |
+
bones = self.default_controlled_bones if force_defaults else self.current_controlled_bones
|
224 |
+
bones = self._sanitize_bones(bones, False) or self.default_controlled_bones
|
225 |
+
self.solver = self._create_solver(bones, False, self.current_num_steps)
|
226 |
+
self.initial_rotations = np.zeros(len(self.solver.controlled_bones)*3, dtype=np.float32)
|
227 |
+
self.best_angles = self.initial_rotations.copy()
|
228 |
+
return True
|
229 |
+
except Exception as e:
|
230 |
+
(self.last_urdf_error if is_urdf else setattr(self, 'last_agent_error', str(e)))
|
231 |
+
logger.exception("Solver rebuild failed")
|
232 |
+
return False
|
233 |
+
|
234 |
+
def _attempt_solver_recovery(self, is_urdf=False):
|
235 |
+
logger.warning("Attempting full solver recovery (invalidate caches + defaults)")
|
236 |
+
self._invalidate_caches()
|
237 |
+
if is_urdf:
|
238 |
+
self.urdf_current_controlled_bones = self.urdf_default_controlled_bones.copy()
|
239 |
+
return self._rebuild_solver_safe(True, force_defaults=True)
|
240 |
+
else:
|
241 |
+
self.current_controlled_bones = self.default_controlled_bones.copy()
|
242 |
+
return self._rebuild_solver_safe(False, force_defaults=True)
|
243 |
+
|
244 |
+
# ---------- Warmup ----------
|
245 |
def _warmup_agent(self):
|
246 |
try:
|
247 |
mand = [DistanceObjTraj(target_points=np.array([0.0,0.2,0.35]), bone_name=self.current_end_effector, use_head=True, weight=1.0)]
|
|
|
269 |
self._warmup_urdf()
|
270 |
logger.info(f"Warmup finished in {time.time()-t0:.2f}s")
|
271 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
272 |
# ---------- Initialization ----------
|
273 |
def _init_agent(self):
|
274 |
+
self.current_num_steps = self._sanitize_num_steps(self.args.num_steps)
|
275 |
basic = InverseKinematicsSolver(
|
276 |
model_file=self.args.gltf_file,
|
277 |
controlled_bones=["left_collar"],
|
|
|
298 |
self.animation_frames_agent = self._frames_from_angles([self.initial_rotations], False)
|
299 |
|
300 |
def _init_urdf(self):
|
301 |
+
self.urdf_current_num_steps = self._sanitize_num_steps(self.args.num_steps)
|
302 |
self.urdf_file = "files/pepper_description-master/urdf/pepper.urdf"
|
303 |
basic = InverseKinematicsSolver(
|
304 |
model_file=self.urdf_file,
|
|
|
333 |
min_clearance=0.0,
|
334 |
weight=1.0,
|
335 |
)
|
336 |
+
# store defaults for config exposure
|
337 |
+
self.collision_default_center = [0.1,0.0,0.35]
|
338 |
+
self.collision_default_radius = 0.1
|
339 |
+
self.collision_default_min_clearance = 0.0
|
340 |
|
341 |
def _setup_urdf_objectives(self):
|
342 |
self.urdf_distance_obj = DistanceObjTraj(
|
|
|
350 |
min_clearance=0.0,
|
351 |
weight=1.0,
|
352 |
)
|
353 |
+
self.urdf_collision_default_center = [0.2,0.0,0.35]
|
354 |
+
self.urdf_collision_default_radius = 0.1
|
355 |
+
self.urdf_collision_default_min_clearance = 0.0
|
356 |
|
357 |
+
# ---------- Build objectives per request ----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
358 |
def _build_agent_objectives(self, payload):
|
359 |
tgt = np.array(payload.get("target",[0.0,0.2,0.35]))
|
360 |
self.distance_obj.update_params({"bone_name": self.current_end_effector, "target_points": tgt, "weight": float(payload.get("distance_weight",1.0))})
|
361 |
+
# collision updates (weight, center, radius, min_clearance)
|
362 |
+
if payload.get("collision_enabled", False):
|
363 |
+
coll_update = {"weight": float(payload.get("collision_weight",1.0))}
|
364 |
+
center = payload.get("collision_center")
|
365 |
+
if isinstance(center, (list, tuple)) and len(center)==3:
|
366 |
+
coll_update["center"] = center
|
367 |
+
radius = payload.get("collision_radius")
|
368 |
+
if radius is not None:
|
369 |
+
coll_update["radius"] = float(radius)
|
370 |
+
if "collision_min_clearance" in payload:
|
371 |
+
coll_update["min_clearance"] = float(payload.get("collision_min_clearance", 0.0))
|
372 |
+
self.collision_obj.update_params(coll_update)
|
373 |
+
else:
|
374 |
+
# still update weight / min_clearance so they can be changed next enable
|
375 |
+
mc = payload.get("collision_min_clearance")
|
376 |
+
upd = {"weight": float(payload.get("collision_weight",1.0))}
|
377 |
+
if mc is not None:
|
378 |
+
upd["min_clearance"] = float(mc)
|
379 |
+
self.collision_obj.update_params(upd)
|
380 |
+
subpoints = self._safe_int(payload.get("subpoints",1), 1, 1, 100)
|
381 |
mandatory, optional = [], []
|
382 |
if payload.get("distance_enabled", True): mandatory.append(self.distance_obj)
|
383 |
if payload.get("collision_enabled", False): optional.append(self.collision_obj)
|
|
|
387 |
optional.append(CombinedDerivativeObj(max_order=3, weights=[float(payload.get("derivative_weight",0.05))]*3))
|
388 |
elif not payload.get("bone_zero_enabled", True) and not payload.get("derivative_enabled", True):
|
389 |
optional.append(BoneZeroRotationObj(weight=0.01))
|
390 |
+
# Hand specification
|
391 |
hand_shape = payload.get("hand_shape","None")
|
392 |
hand_position = payload.get("hand_position","None")
|
393 |
params = {
|
|
|
421 |
def _build_urdf_objectives(self, payload):
|
422 |
tgt = np.array(payload.get("target",[0.3,0.3,0.35]))
|
423 |
self.urdf_distance_obj.update_params({"bone_name": self.urdf_current_end_effector, "target_points": tgt, "weight": float(payload.get("distance_weight",1.0))})
|
424 |
+
if payload.get("collision_enabled", False):
|
425 |
+
coll_update = {"weight": float(payload.get("collision_weight",1.0))}
|
426 |
+
center = payload.get("collision_center")
|
427 |
+
if isinstance(center, (list, tuple)) and len(center)==3:
|
428 |
+
coll_update["center"] = center
|
429 |
+
radius = payload.get("collision_radius")
|
430 |
+
if radius is not None:
|
431 |
+
coll_update["radius"] = float(radius)
|
432 |
+
if "collision_min_clearance" in payload:
|
433 |
+
coll_update["min_clearance"] = float(payload.get("collision_min_clearance", 0.0))
|
434 |
+
self.urdf_collision_obj.update_params(coll_update)
|
435 |
+
else:
|
436 |
+
mc = payload.get("collision_min_clearance")
|
437 |
+
upd = {"weight": float(payload.get("collision_weight",1.0))}
|
438 |
+
if mc is not None:
|
439 |
+
upd["min_clearance"] = float(mc)
|
440 |
+
self.urdf_collision_obj.update_params(upd)
|
441 |
+
subpoints = self._safe_int(payload.get("subpoints",1),1,1,100)
|
442 |
mandatory, optional = [], []
|
443 |
if payload.get("distance_enabled", True): mandatory.append(self.urdf_distance_obj)
|
444 |
if payload.get("collision_enabled", False): optional.append(self.urdf_collision_obj)
|
|
|
450 |
optional.append(BoneZeroRotationObj(weight=0.01))
|
451 |
return mandatory, optional, subpoints
|
452 |
|
453 |
+
# ---------- Frames ----------
|
454 |
def _frames_from_angles(self, angles_seq, is_urdf):
|
455 |
frames = []
|
456 |
for ang in angles_seq:
|
457 |
+
try:
|
458 |
+
if is_urdf:
|
459 |
+
verts = deform_mesh(ang, self.urdf_solver.fk_solver, self.urdf_mesh_data)
|
460 |
+
faces = self.urdf_mesh_data["faces"]
|
461 |
+
else:
|
462 |
+
verts = deform_mesh(ang, self.solver.fk_solver, self.mesh_data)
|
463 |
+
faces = self.mesh_data["faces"]
|
464 |
+
frames.append({"vertices": verts.tolist(), "faces": faces.tolist()})
|
465 |
+
except Exception as e:
|
466 |
+
logger.warning(f"Mesh deformation failed: {e}")
|
467 |
return frames
|
468 |
|
469 |
+
# ---------- Configuration ----------
|
470 |
+
def configure_agent(self, bones, eff, num_steps=None):
|
471 |
+
with self.config_lock:
|
472 |
+
if num_steps is None:
|
473 |
+
num_steps = self.current_num_steps
|
474 |
+
num_steps = self._sanitize_num_steps(num_steps)
|
475 |
+
bones = self._sanitize_bones(bones, False) or self.default_controlled_bones
|
476 |
+
if eff not in getattr(self, 'available_bones', []):
|
477 |
+
eff = self.default_end_effector
|
478 |
+
changed = (bones != self.current_controlled_bones or eff != self.current_end_effector or int(num_steps) != int(self.current_num_steps))
|
479 |
+
if changed:
|
480 |
+
try:
|
481 |
+
self.current_controlled_bones = bones
|
482 |
+
self.current_end_effector = eff
|
483 |
+
self.current_num_steps = int(num_steps)
|
484 |
+
self.solver = self._get_solver(bones, False, self.current_num_steps)
|
485 |
+
self.initial_rotations = np.zeros(len(self.solver.controlled_bones)*3, dtype=np.float32)
|
486 |
+
self.best_angles = self.initial_rotations.copy()
|
487 |
+
self._setup_agent_objectives()
|
488 |
+
except Exception as e:
|
489 |
+
self.agent_fail_count += 1
|
490 |
+
self.last_agent_error = str(e)
|
491 |
+
logger.exception("configure_agent failed; attempting default recovery")
|
492 |
+
if not self._attempt_solver_recovery(False):
|
493 |
+
raise
|
494 |
+
return {"controlled_bones": self.current_controlled_bones, "end_effector": self.current_end_effector, "num_steps": self.current_num_steps}
|
495 |
+
|
496 |
+
def configure_urdf(self, bones, eff, num_steps=None):
|
497 |
+
with self.config_lock:
|
498 |
+
if num_steps is None:
|
499 |
+
num_steps = self.urdf_current_num_steps
|
500 |
+
num_steps = self._sanitize_num_steps(num_steps)
|
501 |
+
bones = self._sanitize_bones(bones, True) or self.urdf_default_controlled_bones
|
502 |
+
if eff not in getattr(self, 'urdf_available_bones', []):
|
503 |
+
eff = self.urdf_default_end_effector
|
504 |
+
changed = (bones != self.urdf_current_controlled_bones or eff != self.urdf_current_end_effector or int(num_steps) != int(self.urdf_current_num_steps))
|
505 |
+
if changed:
|
506 |
+
try:
|
507 |
+
self.urdf_current_controlled_bones = bones
|
508 |
+
self.urdf_current_end_effector = eff
|
509 |
+
self.urdf_current_num_steps = int(num_steps)
|
510 |
+
self.urdf_solver = self._get_solver(bones, True, self.urdf_current_num_steps)
|
511 |
+
self.urdf_initial_rotations = np.zeros(len(self.urdf_solver.controlled_bones)*3, dtype=np.float32)
|
512 |
+
self.urdf_best_angles = self.urdf_initial_rotations.copy()
|
513 |
+
self._setup_urdf_objectives()
|
514 |
+
except Exception as e:
|
515 |
+
self.urdf_fail_count += 1
|
516 |
+
self.last_urdf_error = str(e)
|
517 |
+
logger.exception("configure_urdf failed; attempting default recovery")
|
518 |
+
if not self._attempt_solver_recovery(True):
|
519 |
+
raise
|
520 |
+
return {"controlled_bones": self.urdf_current_controlled_bones, "end_effector": self.urdf_current_end_effector, "num_steps": self.urdf_current_num_steps}
|
521 |
+
|
522 |
+
# ---------- Solve (hardened) ----------
|
523 |
+
def _solve_core(self, solver, init, mand, opt, subpoints, lr):
|
524 |
+
return solver.solve(
|
525 |
+
initial_rotations=init,
|
526 |
+
learning_rate=lr,
|
527 |
mandatory_objective_functions=tuple(mand),
|
528 |
optional_objective_functions=tuple(opt),
|
529 |
+
ik_points=subpoints,
|
530 |
verbose=False,
|
531 |
)
|
532 |
+
|
533 |
+
def solve_agent(self, payload, last_only=False):
|
534 |
+
if time.time() < self.circuit_open_until:
|
535 |
+
return {"status":"circuit_open","error":"agent circuit open","frames":len(self.animation_frames_agent)}
|
536 |
+
mand, opt, sub = self._build_agent_objectives(payload)
|
537 |
+
lr = self._safe_float(payload.get("learning_rate", self.args.learning_rate), self.args.learning_rate, 1e-5, self.max_learning_rate)
|
538 |
+
attempts = 0
|
539 |
+
best_angles = None; obj_val = float('inf'); steps = 0; error_msg=None; recovered=False
|
540 |
+
start=time.time()
|
541 |
+
while attempts < self.max_fail_retries:
|
542 |
+
attempts += 1
|
543 |
+
try:
|
544 |
+
best_angles, obj_val, steps = self._solve_core(self.solver, self.initial_rotations, mand, opt, sub, lr)
|
545 |
+
break
|
546 |
+
except Exception as e:
|
547 |
+
self.agent_fail_count += 1
|
548 |
+
error_msg = str(e)
|
549 |
+
self.last_agent_error = error_msg
|
550 |
+
logger.warning(f"Agent solve attempt {attempts} failed: {e}")
|
551 |
+
if attempts < self.max_fail_retries:
|
552 |
+
# First retry: rebuild current; second: full recovery
|
553 |
+
if attempts == 1:
|
554 |
+
self._rebuild_solver_safe(False, force_defaults=False)
|
555 |
+
else:
|
556 |
+
recovered = self._attempt_solver_recovery(False)
|
557 |
+
else:
|
558 |
+
break
|
559 |
+
if best_angles is None:
|
560 |
+
# Provide at least one frame (initial)
|
561 |
+
frame_angles = [self.initial_rotations]
|
562 |
+
if recovered:
|
563 |
+
frame_angles = [self.initial_rotations]
|
564 |
else:
|
565 |
+
self.best_angles = best_angles[-1].copy()
|
566 |
+
self.initial_rotations = self.best_angles.copy()
|
567 |
+
frame_angles = [best_angles[-1]] if last_only else best_angles
|
568 |
+
self.animation_frames_agent = self._frames_from_angles(frame_angles, False)
|
569 |
self.agent_solve_counter += 1
|
570 |
+
# Circuit breaker update
|
571 |
+
if best_angles is None and self.agent_fail_count >= self.circuit_breaker_threshold:
|
572 |
+
self.circuit_open_until = time.time() + self.circuit_backoff_seconds
|
573 |
+
status = "ok" if best_angles is not None else ("recovered" if recovered else "failed")
|
574 |
return {
|
575 |
+
"status":status,
|
576 |
"solve_time": time.time()-start,
|
577 |
"iterations": steps,
|
578 |
"objective": obj_val,
|
579 |
"frames": len(self.animation_frames_agent),
|
580 |
"solve_id": self.agent_solve_counter,
|
581 |
+
"attempts": attempts,
|
582 |
+
"error": error_msg,
|
583 |
}
|
584 |
|
585 |
def solve_urdf(self, payload, last_only=False):
|
586 |
+
if time.time() < self.circuit_open_until:
|
587 |
+
return {"status":"circuit_open","error":"urdf circuit open","frames":len(self.animation_frames_urdf)}
|
588 |
mand, opt, sub = self._build_urdf_objectives(payload)
|
589 |
+
lr = self._safe_float(payload.get("learning_rate", self.args.learning_rate), self.args.learning_rate, 1e-5, self.max_learning_rate)
|
590 |
+
attempts=0
|
591 |
+
best_angles=None; obj_val=float('inf'); steps=0; error_msg=None; recovered=False
|
592 |
+
start=time.time()
|
593 |
+
while attempts < self.max_fail_retries:
|
594 |
+
attempts += 1
|
595 |
+
try:
|
596 |
+
best_angles, obj_val, steps = self._solve_core(self.urdf_solver, self.urdf_initial_rotations, mand, opt, sub, lr)
|
597 |
+
break
|
598 |
+
except Exception as e:
|
599 |
+
self.urdf_fail_count += 1
|
600 |
+
error_msg = str(e)
|
601 |
+
self.last_urdf_error = error_msg
|
602 |
+
logger.warning(f"URDF solve attempt {attempts} failed: {e}")
|
603 |
+
if attempts < self.max_fail_retries:
|
604 |
+
if attempts == 1:
|
605 |
+
self._rebuild_solver_safe(True, force_defaults=False)
|
606 |
+
else:
|
607 |
+
recovered = self._attempt_solver_recovery(True)
|
608 |
+
else:
|
609 |
+
break
|
610 |
+
if best_angles is None:
|
611 |
+
frame_angles = [self.urdf_initial_rotations]
|
612 |
+
if recovered:
|
613 |
+
frame_angles = [self.urdf_initial_rotations]
|
614 |
else:
|
615 |
+
self.urdf_best_angles = best_angles[-1].copy()
|
616 |
+
self.urdf_initial_rotations = self.urdf_best_angles.copy()
|
617 |
+
frame_angles = [best_angles[-1]] if last_only else best_angles
|
618 |
+
self.animation_frames_urdf = self._frames_from_angles(frame_angles, True)
|
619 |
self.urdf_solve_counter += 1
|
620 |
+
if best_angles is None and self.urdf_fail_count >= self.circuit_breaker_threshold:
|
621 |
+
self.circuit_open_until = time.time() + self.circuit_backoff_seconds
|
622 |
+
status = "ok" if best_angles is not None else ("recovered" if recovered else "failed")
|
623 |
return {
|
624 |
+
"status":status,
|
625 |
"solve_time": time.time()-start,
|
626 |
"iterations": steps,
|
627 |
"objective": obj_val,
|
628 |
"frames": len(self.animation_frames_urdf),
|
629 |
"solve_id": self.urdf_solve_counter,
|
630 |
+
"attempts": attempts,
|
631 |
+
"error": error_msg,
|
632 |
}
|
633 |
|
634 |
# ---------- Housekeeping ----------
|
635 |
def _cleanup(self):
|
636 |
now = time.time()
|
637 |
+
if now - self.last_cleanup_time < self.cleanup_interval:
|
638 |
+
return
|
639 |
+
try:
|
640 |
+
gc.collect()
|
641 |
+
# Memory guard: if RSS > 2GB, clear caches
|
642 |
+
rss = self.process.memory_info().rss
|
643 |
+
if rss > 2 * 1024**3:
|
644 |
+
logger.warning("High memory usage detected; invalidating caches")
|
645 |
+
self._invalidate_caches()
|
646 |
+
except Exception:
|
647 |
+
pass
|
648 |
self.last_cleanup_time = now
|
649 |
|
650 |
# ---------- API ----------
|
651 |
def _register_routes(self):
|
652 |
@self.app.get("/")
|
653 |
+
def index():
|
654 |
+
return FileResponse("static/index.html")
|
655 |
|
656 |
@self.app.get("/threejs_viewer")
|
657 |
+
def legacy():
|
658 |
+
return FileResponse("static/index.html")
|
659 |
|
660 |
@self.app.get("/animation")
|
661 |
def animation(request: Request):
|
|
|
675 |
"end_effector_choices": self.urdf_available_bones,
|
676 |
"hand_shapes": [],
|
677 |
"hand_positions": [],
|
678 |
+
"max_subpoints": 20,
|
679 |
+
"default_num_steps": self.urdf_current_num_steps,
|
680 |
+
"collision_default_center": getattr(self, 'urdf_collision_default_center', [0.2,0.0,0.35]),
|
681 |
+
"collision_default_radius": getattr(self, 'urdf_collision_default_radius', 0.1),
|
682 |
+
"collision_default_min_clearance": getattr(self, 'urdf_collision_default_min_clearance', 0.0),
|
683 |
}
|
684 |
return {
|
685 |
"model":"agent",
|
|
|
694 |
"Look 45° X Downwards","Look 45° X Upwards","Look X Inward","Look to Body",
|
695 |
"Arm Down","Arm 45° Down","Arm Flat"
|
696 |
],
|
697 |
+
"max_subpoints": 20,
|
698 |
+
"default_num_steps": self.current_num_steps,
|
699 |
+
"collision_default_center": getattr(self, 'collision_default_center', [0.1,0.0,0.35]),
|
700 |
+
"collision_default_radius": getattr(self, 'collision_default_radius', 0.1),
|
701 |
+
"collision_default_min_clearance": getattr(self, 'collision_default_min_clearance', 0.0),
|
702 |
}
|
703 |
|
704 |
@self.app.post("/configure")
|
705 |
async def configure(request: Request):
|
706 |
payload = await request.json()
|
707 |
model = payload.get("model","agent").lower()
|
708 |
+
num_steps = self._sanitize_num_steps(payload.get("num_steps", self.args.num_steps))
|
709 |
+
try:
|
710 |
+
if model == "pepper":
|
711 |
+
cfg = self.configure_urdf(payload.get("controlled_bones", []), payload.get("end_effector"), num_steps=num_steps)
|
712 |
+
else:
|
713 |
+
cfg = self.configure_agent(payload.get("controlled_bones", []), payload.get("end_effector"), num_steps=num_steps)
|
714 |
+
return JSONResponse({"status":"ok","config":cfg})
|
715 |
+
except Exception as e:
|
716 |
+
return JSONResponse({"status":"error","message":str(e)}, status_code=500)
|
717 |
|
718 |
@self.app.post("/solve")
|
719 |
async def solve(request: Request):
|
720 |
payload = await request.json()
|
721 |
model = payload.get("model","agent").lower()
|
722 |
return_mode = payload.get("frames_mode", "auto")
|
723 |
+
num_steps = self._sanitize_num_steps(payload.get("num_steps", self.args.num_steps))
|
724 |
+
subpoints = self._safe_int(payload.get("subpoints",1),1,1,100)
|
725 |
last_only = (return_mode != 'all' and subpoints == 1)
|
726 |
self._cleanup()
|
727 |
with self.solve_lock:
|
728 |
try:
|
729 |
if model == "pepper":
|
730 |
+
self.configure_urdf(payload.get("controlled_bones", []), payload.get("end_effector", self.urdf_current_end_effector), num_steps=num_steps)
|
|
|
|
|
731 |
result = self.solve_urdf(payload, last_only=last_only); result["model"]="pepper"; result["num_steps"] = num_steps
|
732 |
if last_only:
|
733 |
+
frames = list(self.animation_frames_urdf)
|
734 |
elif return_mode == "all" or (return_mode == "auto" and subpoints > 1):
|
735 |
frames = list(self.animation_frames_urdf)
|
736 |
else:
|
737 |
frames = [self.animation_frames_urdf[-1]]
|
738 |
result["frames_data"] = frames
|
739 |
else:
|
740 |
+
self.configure_agent(payload.get("controlled_bones", []), payload.get("end_effector", self.current_end_effector), num_steps=num_steps)
|
|
|
|
|
741 |
result = self.solve_agent(payload, last_only=last_only); result["model"]="agent"; result["num_steps"] = num_steps
|
742 |
if last_only:
|
743 |
frames = list(self.animation_frames_agent)
|
|
|
746 |
else:
|
747 |
frames = [self.animation_frames_agent[-1]]
|
748 |
result["frames_data"] = frames
|
749 |
+
status = result.get("status","ok")
|
750 |
+
top_status = "ok" if status in ("ok","recovered") else status
|
751 |
+
return JSONResponse({"status":top_status,"result":result})
|
752 |
except Exception as e:
|
753 |
+
logger.exception("Solve failed unrecoverably")
|
754 |
return JSONResponse({"status":"error","message":str(e)}, status_code=500)
|
755 |
|
756 |
+
@self.app.post("/reset")
|
757 |
+
def reset():
|
758 |
+
with self.solve_lock:
|
759 |
+
self._invalidate_caches()
|
760 |
+
self._attempt_solver_recovery(False)
|
761 |
+
self._attempt_solver_recovery(True)
|
762 |
+
return {"status":"ok","message":"solvers & caches reset"}
|
763 |
+
|
764 |
@self.app.get("/health")
|
765 |
def health():
|
766 |
+
rss = self.process.memory_info().rss if self.process else 0
|
767 |
return {
|
768 |
"status":"ok",
|
769 |
"agent_frames": len(self.animation_frames_agent),
|
770 |
"urdf_frames": len(self.animation_frames_urdf),
|
771 |
"cache_agent": len(self.solver_cache),
|
772 |
"cache_urdf": len(self.urdf_solver_cache),
|
773 |
+
"agent_fail_count": self.agent_fail_count,
|
774 |
+
"urdf_fail_count": self.urdf_fail_count,
|
775 |
+
"last_agent_error": self.last_agent_error,
|
776 |
+
"last_urdf_error": self.last_urdf_error,
|
777 |
+
"circuit_open_for": max(0, int(self.circuit_open_until - time.time())),
|
778 |
+
"memory_rss_mb": round(rss/1024/1024,1),
|
779 |
}
|
780 |
|
781 |
@self.app.get("/favicon.ico")
|
782 |
+
def favicon():
|
783 |
+
return Response(status_code=204)
|
784 |
|
785 |
# ---------- Main ----------
|
786 |
def main():
|
787 |
parser = configargparse.ArgumentParser(description="Inverse Kinematics Solver - Three.js Web UI", default_config_files=["config.ini"])
|
788 |
+
parser.add_argument("--gltf_file", type=str, default="files/smplx.glb")
|
789 |
+
parser.add_argument("--hand", type=str, choices=["left","right"], default="left")
|
790 |
+
parser.add_argument("--threshold", type=float, default=0.0001)
|
791 |
+
parser.add_argument("--num_steps", type=int, default=100)
|
792 |
+
parser.add_argument("--learning_rate", type=float, default=0.2)
|
793 |
+
parser.add_argument("--subpoints", type=int, default=1)
|
794 |
+
parser.add_argument("--api_port", type=int, default=17861)
|
795 |
+
parser.add_argument("--warmup", action='store_true', default=True, help="Enable background JIT warmup")
|
796 |
args = parser.parse_args()
|
797 |
|
798 |
download_and_setup_files()
|
static/index.html
CHANGED
@@ -37,6 +37,9 @@
|
|
37 |
.toast.error { border-color:#b33939; }
|
38 |
.toast .toast-close { float:right; cursor:pointer; color:#888; margin-left:6px; }
|
39 |
.toast .toast-close:hover { color:#fff; }
|
|
|
|
|
|
|
40 |
</style>
|
41 |
<!-- Added import map to resolve bare specifier 'three' -->
|
42 |
<script type="importmap">
|
@@ -90,8 +93,36 @@
|
|
90 |
<legend>Primary Objectives</legend>
|
91 |
<label><input id="distance_enabled" type="checkbox" checked> Distance Objective</label>
|
92 |
<label>Distance Weight <input id="distance_weight" type="number" value="1.0" step="0.1"></label>
|
93 |
-
|
94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
</fieldset>
|
96 |
<fieldset>
|
97 |
<legend>Regularization</legend>
|
@@ -166,46 +197,28 @@ import { GLTFLoader } from 'https://unpkg.com/[email protected]/examples/jsm/loaders
|
|
166 |
let gltfPrimaryMaterialCaptured = false;
|
167 |
let fallbackMesh = null;
|
168 |
let lastTime = performance.now();
|
169 |
-
// REMOVED: let lastFrameFetch = 0;
|
170 |
-
// REMOVED: const pollInterval = 1000;
|
171 |
-
// (ADDED) Solve ID tracking to avoid "lastSolveIdAgent / lastSolveIdPepper is not defined" errors
|
172 |
let lastSolveIdAgent = 0;
|
173 |
let lastSolveIdPepper = 0;
|
174 |
|
175 |
-
// ----- Auto-solve
|
176 |
let solving = false;
|
177 |
-
let solvePending = false;
|
178 |
let solveDebounceTimer = null;
|
179 |
-
const SOLVE_DEBOUNCE_MS = 25;
|
180 |
-
let currentSolveController = null;
|
181 |
-
|
182 |
function scheduleSolve(immediate=false){
|
183 |
-
if (solving){
|
184 |
-
|
185 |
-
if (currentSolveController){ currentSolveController.abort(); }
|
186 |
-
}
|
187 |
-
if (immediate){
|
188 |
-
// Fire instantly for first / critical interactions
|
189 |
-
triggerSolve(true);
|
190 |
-
return;
|
191 |
-
}
|
192 |
if (solveDebounceTimer) clearTimeout(solveDebounceTimer);
|
193 |
-
solveDebounceTimer = setTimeout(()=>
|
194 |
}
|
195 |
-
|
196 |
-
|
197 |
-
if (solving){
|
198 |
-
// In-flight already (might have just been aborted). Let doSolve handle continuation.
|
199 |
-
}
|
200 |
-
if (currentSolveController){ currentSolveController.abort(); }
|
201 |
currentSolveController = new AbortController();
|
202 |
const controller = currentSolveController;
|
203 |
solving = true;
|
204 |
try { await doSolve(controller.signal); }
|
205 |
-
catch(e){ if (e.name !== 'AbortError') {
|
206 |
-
finally {
|
207 |
-
if (controller === currentSolveController){ solving = false; }
|
208 |
-
}
|
209 |
}
|
210 |
|
211 |
// Playback state
|
@@ -219,19 +232,14 @@ import { GLTFLoader } from 'https://unpkg.com/[email protected]/examples/jsm/loaders
|
|
219 |
let alignAttemptCount = 0;
|
220 |
const maxAlignAttempts = 120;
|
221 |
|
222 |
-
//
|
223 |
let controlFrames = [];
|
224 |
let splineMode = false;
|
225 |
let splineU = 0;
|
226 |
-
let splineDuration = 2.0;
|
227 |
-
// One-time initial camera framing flag (ADDED)
|
228 |
let initialCameraFramed = false;
|
229 |
|
230 |
function cleanupVisuals(){
|
231 |
-
if (ikMesh && usingGLTFGeometry){
|
232 |
-
// keep GLTF mesh for possible reuse after reconfigure? remove anyway for clean slate
|
233 |
-
ikMesh = null;
|
234 |
-
}
|
235 |
if (fallbackMesh){
|
236 |
modelGroup.remove(fallbackMesh);
|
237 |
if (fallbackMesh.geometry) fallbackMesh.geometry.dispose();
|
@@ -239,84 +247,34 @@ import { GLTFLoader } from 'https://unpkg.com/[email protected]/examples/jsm/loaders
|
|
239 |
fallbackMesh = null;
|
240 |
}
|
241 |
if (gltfRoot){
|
242 |
-
gltfRoot.traverse(o=>{
|
243 |
-
|
244 |
-
if (o.geometry) o.geometry.dispose();
|
245 |
-
if (o.material){
|
246 |
-
if (Array.isArray(o.material)) o.material.forEach(m=>m.dispose());
|
247 |
-
else o.material.dispose();
|
248 |
-
}
|
249 |
-
}
|
250 |
-
});
|
251 |
-
modelGroup.remove(gltfRoot);
|
252 |
-
gltfRoot = null;
|
253 |
}
|
254 |
-
// Remove any residual children
|
255 |
while (modelGroup.children.length) modelGroup.remove(modelGroup.children[0]);
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
gltfPrimaryMaterialCaptured = false;
|
260 |
-
ikMesh = null;
|
261 |
-
usingGLTFGeometry = false;
|
262 |
-
animationFrames = [];
|
263 |
-
frameIndex = 0;
|
264 |
-
baseOffsetY = 0;
|
265 |
-
groundAligned = false;
|
266 |
-
pendingAlign = false;
|
267 |
-
alignAttemptCount = 0;
|
268 |
-
log("Visuals cleaned.");
|
269 |
-
}
|
270 |
-
|
271 |
-
function scheduleGroundAlign(force=false){
|
272 |
-
if (force){
|
273 |
-
groundAligned = false;
|
274 |
-
alignAttemptCount = 0;
|
275 |
-
}
|
276 |
-
pendingAlign = true;
|
277 |
}
|
278 |
-
|
279 |
function performGroundAlign(){
|
280 |
if (!pendingAlign) return;
|
281 |
-
if (!modelGroup.children.length){
|
282 |
-
if (++alignAttemptCount > maxAlignAttempts) pendingAlign = false;
|
283 |
-
return;
|
284 |
-
}
|
285 |
const box = new THREE.Box3().setFromObject(modelGroup);
|
286 |
-
if (box.isEmpty() || !isFinite(box.min.y)){
|
287 |
-
|
288 |
-
return;
|
289 |
-
}
|
290 |
-
const currentMin = box.min.y;
|
291 |
-
// Desired world shift so new min ~= 0
|
292 |
-
const delta = -currentMin;
|
293 |
-
// Avoid large downward moves; only allow raising or tiny corrections
|
294 |
if (delta > 0 || Math.abs(delta) < 1e-3){
|
295 |
-
modelGroup.position.y += delta;
|
296 |
-
baseOffsetY = modelGroup.position.y;
|
297 |
-
groundAligned = true;
|
298 |
-
updateTargetSphere(); // (NEW) keep target indicator vertically aligned
|
299 |
}
|
300 |
-
pendingAlign
|
301 |
}
|
302 |
-
|
303 |
-
// (ADD) fitCamera helper (uses modelGroup if populated)
|
304 |
function fitCamera(obj){
|
305 |
-
const targetObj = modelGroup.children.length ? modelGroup : obj;
|
306 |
-
|
307 |
-
const
|
308 |
-
|
309 |
-
const
|
310 |
-
const center = box.getCenter(new THREE.Vector3());
|
311 |
-
controls.target.copy(center);
|
312 |
-
const maxDim = Math.max(size.x,size.y,size.z);
|
313 |
-
const dist = maxDim * 3;
|
314 |
-
const dir = new THREE.Vector3(0.0,0.5,1).normalize();
|
315 |
camera.position.copy(center).addScaledVector(dir, dist);
|
316 |
-
camera.near = maxDim/100;
|
317 |
-
camera.far = maxDim*100;
|
318 |
-
camera.updateProjectionMatrix();
|
319 |
-
controls.update();
|
320 |
}
|
321 |
|
322 |
// ----- DOM refs -----
|
@@ -327,10 +285,21 @@ import { GLTFLoader } from 'https://unpkg.com/[email protected]/examples/jsm/loaders
|
|
327 |
const handPosSel = document.getElementById('hand_position');
|
328 |
const handFieldset = document.getElementById('handFieldset');
|
329 |
const modelBadge = document.getElementById('modelBadge');
|
330 |
-
const subpointsInput = document.getElementById('subpoints');
|
331 |
-
const numStepsInput = document.getElementById('num_steps');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
332 |
|
333 |
-
// Inputs
|
334 |
function val(id){ return document.getElementById(id).value; }
|
335 |
function num(id){ return parseFloat(val(id)); }
|
336 |
function bool(id){ return document.getElementById(id).checked; }
|
@@ -341,743 +310,163 @@ import { GLTFLoader } from 'https://unpkg.com/[email protected]/examples/jsm/loaders
|
|
341 |
renderer.setSize(document.getElementById('viewerPane').clientWidth, document.getElementById('viewerPane').clientHeight);
|
342 |
renderer.domElement.className = 'viewer-canvas';
|
343 |
document.getElementById('viewerPane').appendChild(renderer.domElement);
|
344 |
-
|
345 |
-
const scene = new THREE.Scene();
|
346 |
-
scene.background = new THREE.Color(0x222222);
|
347 |
-
|
348 |
const camera = new THREE.PerspectiveCamera(45, renderer.domElement.clientWidth / renderer.domElement.clientHeight, 0.01, 1000);
|
349 |
camera.position.set(0,1,3);
|
350 |
-
|
351 |
-
const controls = new OrbitControls(camera, renderer.domElement);
|
352 |
-
controls.target.set(0,0.8,0);
|
353 |
-
|
354 |
scene.add(new THREE.HemisphereLight(0xffffff,0x444444,1.0));
|
355 |
-
const d = new THREE.DirectionalLight(0xffffff,0.8);
|
356 |
-
|
357 |
-
|
358 |
-
const
|
359 |
-
new THREE.CircleGeometry(5,48),
|
360 |
-
new THREE.MeshStandardMaterial({color:0x303030, metalness:0.1, roughness:0.9})
|
361 |
-
);
|
362 |
-
ground.rotation.x = -Math.PI/2;
|
363 |
-
ground.receiveShadow = true;
|
364 |
-
scene.add(ground);
|
365 |
-
ground.visible = false; // DISABLED ground plane
|
366 |
|
367 |
-
//
|
368 |
-
const modelGroup = new THREE.Group();
|
369 |
-
scene.add(modelGroup);
|
370 |
-
|
371 |
-
// (ADDED) Target sphere visual
|
372 |
const targetSphereGeom = new THREE.SphereGeometry(0.01, 24, 24);
|
373 |
const targetSphereMat = new THREE.MeshStandardMaterial({color:0x00ff55, emissive:0x008833});
|
374 |
-
const targetSphere = new THREE.Mesh(targetSphereGeom, targetSphereMat);
|
375 |
-
|
|
|
|
|
|
|
376 |
|
377 |
-
// (REPLACED) updateTargetSphere: now accounts for modelGroup translation (baseOffsetY)
|
378 |
-
// and uniform scaling (Pepper) on all axes so sphere aligns with the visual robot pose.
|
379 |
function updateTargetSphere(){
|
380 |
const xRaw = parseFloat(document.getElementById('target_x').value) || 0;
|
381 |
const yRaw = parseFloat(document.getElementById('target_y').value) || 0;
|
382 |
const zRaw = parseFloat(document.getElementById('target_z').value) || 0;
|
383 |
-
|
384 |
-
const
|
385 |
-
|
386 |
-
const sz = modelGroup?.scale?.z ?? 1;
|
387 |
-
const tx = modelGroup?.position?.x ?? 0;
|
388 |
-
const ty = modelGroup?.position?.y ?? 0; // this already equals baseOffsetY after ground align
|
389 |
-
const tz = modelGroup?.position?.z ?? 0;
|
390 |
-
// Apply uniform / per-axis scaling then translation so sphere sits where the solver
|
391 |
-
// target maps in the displayed (scaled & lifted) robot coordinates.
|
392 |
-
targetSphere.position.set(
|
393 |
-
xRaw * sx + tx,
|
394 |
-
yRaw * sy + ty,
|
395 |
-
zRaw * sz + tz
|
396 |
-
);
|
397 |
}
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
updateTargetSphere();
|
407 |
-
|
408 |
-
window.addEventListener('resize', () => {
|
409 |
-
renderer.setSize(document.getElementById('viewerPane').clientWidth, document.getElementById('viewerPane').clientHeight);
|
410 |
-
camera.aspect = renderer.domElement.clientWidth / renderer.domElement.clientHeight;
|
411 |
-
camera.updateProjectionMatrix();
|
412 |
-
});
|
413 |
-
|
414 |
-
function resetCamera(){
|
415 |
-
camera.position.set(0,1,3);
|
416 |
-
controls.target.set(0,0.8,0);
|
417 |
-
controls.update();
|
418 |
}
|
|
|
|
|
|
|
|
|
|
|
419 |
|
420 |
// ----- Config fetch -----
|
421 |
-
async function loadConfig(){
|
422 |
-
const res = await fetch(`/config?model=${currentModel}`);
|
423 |
-
configData = await res.json();
|
424 |
-
populateUIFromConfig();
|
425 |
-
log(`Config loaded for ${currentModel}`);
|
426 |
-
}
|
427 |
|
428 |
function populateUIFromConfig(){
|
429 |
-
|
430 |
-
|
431 |
-
configData.
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
bonesContainer.innerHTML = "";
|
439 |
-
configData.selectable_bones.forEach(b=>{
|
440 |
-
const id = `bone_${b}`;
|
441 |
-
const label = document.createElement('label');
|
442 |
-
label.innerHTML = `<input type="checkbox" id="${id}" ${configData.default_controlled_bones.includes(b)?'checked':''}> ${b}`;
|
443 |
-
bonesContainer.appendChild(label);
|
444 |
-
// Add listener for auto-solve
|
445 |
-
setTimeout(()=>{ // ensure element in DOM
|
446 |
-
const cb = document.getElementById(id);
|
447 |
-
if (cb) cb.addEventListener('change', ()=> scheduleSolve());
|
448 |
-
},0);
|
449 |
-
});
|
450 |
-
// Hand controls (agent only)
|
451 |
-
if (currentModel === 'agent'){
|
452 |
-
handFieldset.style.display = '';
|
453 |
-
handShapeSel.innerHTML = "";
|
454 |
-
configData.hand_shapes.forEach(s=>{
|
455 |
-
const opt = document.createElement('option');
|
456 |
-
opt.value = s; opt.textContent = s;
|
457 |
-
handShapeSel.appendChild(opt);
|
458 |
-
});
|
459 |
-
handPosSel.innerHTML = "";
|
460 |
-
configData.hand_positions.forEach(s=>{
|
461 |
-
const opt = document.createElement('option');
|
462 |
-
opt.value = s; opt.textContent = s;
|
463 |
-
handPosSel.appendChild(opt);
|
464 |
-
});
|
465 |
-
} else {
|
466 |
-
handFieldset.style.display = 'none';
|
467 |
-
}
|
468 |
-
// Set subpoints max if provided
|
469 |
-
if (configData && typeof configData.max_subpoints !== 'undefined'){
|
470 |
-
subpointsInput.max = configData.max_subpoints;
|
471 |
-
}
|
472 |
-
if (configData && typeof configData.default_num_steps !== 'undefined'){
|
473 |
-
numStepsInput.value = configData.default_num_steps;
|
474 |
-
}
|
475 |
-
updateTargetSphere();
|
476 |
}
|
477 |
|
478 |
-
|
479 |
-
subpointsInput.addEventListener('change', ()=>{
|
480 |
-
const v = parseInt(subpointsInput.value,10);
|
481 |
-
if (v <= 1){
|
482 |
-
document.getElementById('derivative_enabled').checked = false;
|
483 |
-
}
|
484 |
-
scheduleSolve();
|
485 |
-
});
|
486 |
|
487 |
-
|
488 |
-
function applyModelSpecificGLTFPolicy(){
|
489 |
-
const urlInput = document.getElementById('gltf_url');
|
490 |
-
const showGltfChk = document.getElementById('show_gltf');
|
491 |
-
if (currentModel === 'pepper'){
|
492 |
-
// Do NOT permanently clear stored URL; just hide GLTF reference
|
493 |
-
// (removed previous urlInput.value = "" to preserve texture reuse when switching back)
|
494 |
-
showGltfChk.disabled = true;
|
495 |
-
if (gltfRoot) { gltfRoot.visible = false; }
|
496 |
-
} else {
|
497 |
-
// Restore default GLTF path if empty so textures load properly after tab switching
|
498 |
-
if (!urlInput.value) urlInput.value = "/files/smplx.glb";
|
499 |
-
showGltfChk.disabled = false;
|
500 |
-
}
|
501 |
-
}
|
502 |
|
503 |
-
// ----- GLTF Loader (reuse its primary mesh for IK if vertex counts match) -----
|
504 |
const gltfLoader = new GLTFLoader();
|
505 |
-
function loadGLTF(url){
|
506 |
-
|
507 |
-
const u = url.startsWith('http') ? url : url;
|
508 |
-
statusBar.textContent = `Loading GLTF: ${u}`;
|
509 |
-
log(`Loading GLTF ${u}`);
|
510 |
-
gltfLoader.load(u, gltf=>{
|
511 |
-
if (gltfRoot) modelGroup.remove(gltfRoot);
|
512 |
-
gltfRoot = gltf.scene;
|
513 |
-
gltfPrimaryMaterial = null;
|
514 |
-
gltfPrimaryMesh = null;
|
515 |
-
gltfPrimaryMaterialCaptured = false;
|
516 |
-
|
517 |
-
gltfRoot.traverse(o=>{
|
518 |
-
if (o.isMesh && o.geometry?.attributes?.position){
|
519 |
-
if (!gltfPrimaryMesh || o.geometry.attributes.position.count > gltfPrimaryMesh.geometry.attributes.position.count){
|
520 |
-
gltfPrimaryMesh = o;
|
521 |
-
}
|
522 |
-
}
|
523 |
-
});
|
524 |
-
|
525 |
-
if (gltfPrimaryMesh){
|
526 |
-
if (Array.isArray(gltfPrimaryMesh.material)){
|
527 |
-
const mm = gltfPrimaryMesh.material.find(m=>m.map) || gltfPrimaryMesh.material[0];
|
528 |
-
gltfPrimaryMaterial = mm.clone();
|
529 |
-
} else {
|
530 |
-
gltfPrimaryMaterial = gltfPrimaryMesh.material.clone();
|
531 |
-
}
|
532 |
-
gltfPrimaryMaterialCaptured = true;
|
533 |
-
log(`Captured GLTF primary mesh (verts=${gltfPrimaryMesh.geometry.attributes.position.count})`);
|
534 |
-
} else {
|
535 |
-
log("No primary mesh found in GLTF.");
|
536 |
-
}
|
537 |
-
|
538 |
-
modelGroup.add(gltfRoot); // (CHANGED) add to modelGroup instead of scene
|
539 |
-
gltfRoot.visible = document.getElementById('show_gltf').checked;
|
540 |
-
|
541 |
-
// Bind deformation if frames already exist
|
542 |
-
if (animationFrames.length){
|
543 |
-
ensureActiveMeshBound(animationFrames[0]);
|
544 |
-
applyFrame(animationFrames[Math.min(frameIndex, animationFrames.length-1)]);
|
545 |
-
}
|
546 |
-
|
547 |
-
fitCamera(gltfRoot);
|
548 |
-
// adjustModelHeight(gltfRoot); // (ADD) ensure height set even without frames yet
|
549 |
-
statusBar.textContent = "GLTF loaded.";
|
550 |
-
}, undefined, err=>{
|
551 |
-
statusBar.textContent = "GLTF error: " + err.message;
|
552 |
-
log("GLTF error: "+err.message);
|
553 |
-
});
|
554 |
-
}
|
555 |
-
|
556 |
-
// ----- Animation Frames Fetch -----
|
557 |
-
async function fetchFrames(force=false){
|
558 |
-
const now = performance.now();
|
559 |
-
if (!force && now - lastFrameFetch < pollInterval) return;
|
560 |
-
lastFrameFetch = now;
|
561 |
-
const url = `/animation?model=${currentModel==='pepper'?'pepper':'agent'}`;
|
562 |
-
try {
|
563 |
-
const res = await fetch(url);
|
564 |
-
if (!res.ok) throw new Error(res.status);
|
565 |
-
const data = await res.json();
|
566 |
-
if (force || data.length !== animationFrames.length){
|
567 |
-
animationFrames = data;
|
568 |
-
if (animationFrames.length){
|
569 |
-
ensureActiveMeshBound(animationFrames[0]);
|
570 |
-
controlFrames = [];
|
571 |
-
splineMode = false;
|
572 |
-
playbackEnabled = false;
|
573 |
-
const requestedSub = payload.subpoints;
|
574 |
-
if (animationFrames.length > 1 && requestedSub > 1){
|
575 |
-
setupSplinePlayback(animationFrames);
|
576 |
-
applyFrame(animationFrames[0]);
|
577 |
-
} else {
|
578 |
-
frameIndex = animationFrames.length - 1;
|
579 |
-
applyFrame(animationFrames[frameIndex]);
|
580 |
-
}
|
581 |
-
scheduleGroundAlign();
|
582 |
-
lastFrameFetch = performance.now();
|
583 |
-
}
|
584 |
-
}
|
585 |
-
} catch(e){
|
586 |
-
log("Animation fetch error: "+e.message);
|
587 |
-
}
|
588 |
-
}
|
589 |
|
590 |
-
|
591 |
-
function
|
592 |
-
if (!animationFrames.length) return;
|
593 |
-
ensureActiveMeshBound(animationFrames[0]);
|
594 |
-
applyFrame(animationFrames[0]);
|
595 |
-
}
|
596 |
-
|
597 |
-
// Deprecate old createOrUpdateFallbackMesh usage by redirect (kept if referenced)
|
598 |
-
function createOrUpdateFallbackMesh(){
|
599 |
-
if (!animationFrames.length) return;
|
600 |
-
ensureActiveMeshBound(animationFrames[0]);
|
601 |
-
}
|
602 |
-
|
603 |
-
// ----- Playback control (NEW) -----
|
604 |
-
function updateFramePlayback(){
|
605 |
-
if (!animationFrames.length) return;
|
606 |
-
const idx = Math.min(Math.floor(frameIndex), animationFrames.length - 1); // clamp
|
607 |
-
applyFrame(animationFrames[idx]);
|
608 |
-
}
|
609 |
-
|
610 |
-
// ----- Solve -----
|
611 |
-
// REMOVE old binding to doSolve directly (replaced below)
|
612 |
-
// document.getElementById('solve_btn').onclick = () => doSolve();
|
613 |
-
document.getElementById('solve_btn').onclick = () => triggerSolve(true);
|
614 |
-
|
615 |
-
function collectBones(){
|
616 |
-
const bones = [];
|
617 |
-
if (!configData) return bones;
|
618 |
-
configData.selectable_bones.forEach(b=>{
|
619 |
-
const cb = document.getElementById(`bone_${b}`);
|
620 |
-
if (cb && cb.checked) bones.push(b);
|
621 |
-
});
|
622 |
-
return bones;
|
623 |
-
}
|
624 |
|
625 |
async function doSolve(abortSignal){
|
626 |
-
statusBar.textContent
|
627 |
-
|
628 |
-
const
|
629 |
-
const payload = {
|
630 |
model: currentModel,
|
631 |
target: [num('target_x'), num('target_y'), num('target_z')],
|
632 |
subpoints: subpointsVal,
|
633 |
num_steps: parseInt(numStepsInput.value,10) || 100,
|
634 |
distance_enabled: bool('distance_enabled'),
|
635 |
distance_weight: num('distance_weight'),
|
636 |
-
collision_enabled:
|
637 |
-
collision_weight:
|
|
|
|
|
638 |
bone_zero_enabled: bool('bone_zero_enabled'),
|
639 |
bone_zero_weight: num('bone_zero_weight'),
|
640 |
derivative_enabled: bool('derivative_enabled'),
|
641 |
derivative_weight: num('derivative_weight'),
|
642 |
controlled_bones: collectBones(),
|
643 |
end_effector: endEffectorSel.value,
|
644 |
-
hand_shape: currentModel==='agent'? handShapeSel.value :
|
645 |
-
hand_position: currentModel==='agent'? handPosSel.value :
|
646 |
-
|
647 |
-
|
648 |
};
|
649 |
-
try {
|
650 |
-
const
|
651 |
-
|
652 |
-
|
653 |
-
method:"POST",
|
654 |
-
headers:{'Content-Type':'application/json'},
|
655 |
-
body: JSON.stringify(payload),
|
656 |
-
signal: abortSignal
|
657 |
-
});
|
658 |
-
if (!res.ok) throw new Error(res.status+" "+res.statusText);
|
659 |
-
const json = await res.json();
|
660 |
-
if (json.status !== 'ok') throw new Error(json.message || 'Solve failed');
|
661 |
-
|
662 |
-
// Stale solve guard
|
663 |
-
const sid = json.result.solve_id || 0;
|
664 |
-
if (payload.model === 'agent'){
|
665 |
-
if (sid <= lastSolveIdAgent){ log(`Stale agent solve ignored (sid=${sid} <= ${lastSolveIdAgent})`); return; }
|
666 |
-
lastSolveIdAgent = sid;
|
667 |
-
} else {
|
668 |
-
if (sid <= lastSolveIdPepper){ log(`Stale pepper solve ignored (sid=${sid} <= ${lastSolveIdPepper})`); return; }
|
669 |
-
lastSolveIdPepper = sid;
|
670 |
-
}
|
671 |
-
|
672 |
-
const server = json.result.solve_time;
|
673 |
-
const total = (performance.now()-t0)/1000;
|
674 |
-
statusBar.textContent =
|
675 |
-
`Solved: server=${server.toFixed(2)}s total=${total.toFixed(2)}s it=${json.result.iterations} obj=${json.result.objective.toFixed(6)} frames=${json.result.frames}`;
|
676 |
-
log(`Solve completed (frames received=${json.result.frames})`);
|
677 |
-
if (json.result.frames_data && json.result.frames_data.length){
|
678 |
-
animationFrames = json.result.frames_data;
|
679 |
-
ensureActiveMeshBound(animationFrames[0]);
|
680 |
-
controlFrames = [];
|
681 |
-
splineMode = false;
|
682 |
-
playbackEnabled = false;
|
683 |
-
if (animationFrames.length > 1 && subpointsVal > 1){
|
684 |
-
setupSplinePlayback(animationFrames);
|
685 |
-
applyFrame(animationFrames[0]);
|
686 |
-
} else {
|
687 |
-
frameIndex = animationFrames.length - 1;
|
688 |
-
applyFrame(animationFrames[frameIndex]);
|
689 |
-
}
|
690 |
-
scheduleGroundAlign();
|
691 |
-
}
|
692 |
-
updateTargetSphere();
|
693 |
-
showToast(
|
694 |
-
`Solve OK • it=${json.result.iterations} • t=${json.result.solve_time.toFixed(2)}s • err=${json.result.objective.toExponential(2)}`,
|
695 |
-
'success',
|
696 |
-
4000
|
697 |
-
);
|
698 |
-
} catch(e){
|
699 |
-
if (e.name === 'AbortError'){ log('Solve aborted (superseded by new request)'); return; }
|
700 |
-
statusBar.textContent = "Solve error: " + e.message;
|
701 |
-
log("Solve error: "+e.message);
|
702 |
-
showToast(`Solve ERROR: ${e.message}`, 'error', 6000);
|
703 |
-
}
|
704 |
-
}
|
705 |
-
|
706 |
-
// ----- Toast System (ADDED) -----
|
707 |
-
const toastContainer = document.getElementById('toastContainer');
|
708 |
-
function showToast(message, type='success', ttl=4000){
|
709 |
-
const el = document.createElement('div');
|
710 |
-
el.className = `toast ${type}`;
|
711 |
-
const close = document.createElement('span');
|
712 |
-
close.className = 'toast-close';
|
713 |
-
close.textContent = '✕';
|
714 |
-
close.onclick = (e)=>{ e.stopPropagation(); remove(); };
|
715 |
-
const content = document.createElement('div');
|
716 |
-
content.textContent = message;
|
717 |
-
el.appendChild(close);
|
718 |
-
el.appendChild(content);
|
719 |
-
toastContainer.appendChild(el);
|
720 |
-
requestAnimationFrame(()=> el.classList.add('show'));
|
721 |
-
function remove(){
|
722 |
-
el.classList.remove('show');
|
723 |
-
setTimeout(()=> el.remove(), 180);
|
724 |
-
}
|
725 |
-
setTimeout(remove, ttl);
|
726 |
-
}
|
727 |
-
|
728 |
-
// ----- Target sliders (NEW) -----
|
729 |
-
const xNum = document.getElementById('target_x');
|
730 |
-
const yNum = document.getElementById('target_y');
|
731 |
-
const zNum = document.getElementById('target_z');
|
732 |
-
const xSlider = document.getElementById('target_x_slider');
|
733 |
-
const ySlider = document.getElementById('target_y_slider');
|
734 |
-
|
735 |
-
function syncSliderToNum(axis){
|
736 |
-
if (axis==='x') xSlider.value = xNum.value;
|
737 |
-
else if (axis==='y') ySlider.value = yNum.value;
|
738 |
-
}
|
739 |
-
function syncNumToSlider(axis){
|
740 |
-
if (axis==='x') xNum.value = xSlider.value;
|
741 |
-
else if (axis==='y') yNum.value = ySlider.value;
|
742 |
}
|
743 |
|
744 |
-
|
745 |
-
|
746 |
-
|
747 |
-
|
748 |
-
|
749 |
-
|
750 |
-
|
751 |
-
|
752 |
-
|
753 |
-
|
754 |
-
|
755 |
-
|
756 |
-
|
757 |
-
|
758 |
-
|
759 |
-
|
760 |
-
|
761 |
-
|
|
|
|
|
|
|
|
|
762 |
'#distance_enabled','#distance_weight',
|
763 |
-
'#collision_enabled','#collision_weight',
|
764 |
'#bone_zero_enabled','#bone_zero_weight',
|
765 |
'#derivative_enabled','#derivative_weight',
|
766 |
'#hand_shape','#hand_position',
|
767 |
'#end_effector','#wireframe','#show_gltf','#show_ikmesh',
|
768 |
-
'#play_fps','#num_steps'
|
769 |
];
|
770 |
-
autoSolveSelectors.forEach(sel=>{
|
771 |
-
const el = document.querySelector(sel);
|
772 |
-
if (el){
|
773 |
-
const evt = (el.type === 'checkbox' || el.tagName === 'SELECT') ? 'change' : 'input';
|
774 |
-
el.addEventListener(evt, ()=> scheduleSolve());
|
775 |
-
}
|
776 |
-
});
|
777 |
-
|
778 |
-
// Wireframe / visibility still immediate visual update (keep old logic if any)
|
779 |
-
document.getElementById('wireframe').addEventListener('change', ()=>{
|
780 |
-
if (ikMesh && ikMesh.material){
|
781 |
-
ikMesh.material.wireframe = document.getElementById('wireframe').checked;
|
782 |
-
ikMesh.material.needsUpdate = true;
|
783 |
-
}
|
784 |
-
if (gltfPrimaryMesh && gltfPrimaryMesh.material){
|
785 |
-
if (Array.isArray(gltfPrimaryMesh.material)) gltfPrimaryMesh.material.forEach(m=>{m.wireframe=document.getElementById('wireframe').checked; m.needsUpdate=true;});
|
786 |
-
else { gltfPrimaryMesh.material.wireframe = document.getElementById('wireframe').checked; gltfPrimaryMesh.material.needsUpdate=true; }
|
787 |
-
}
|
788 |
-
});
|
789 |
|
790 |
-
document.getElementById('
|
791 |
-
|
792 |
-
});
|
793 |
-
document.getElementById('show_ikmesh').addEventListener('change', ()=>{
|
794 |
-
if (ikMesh) ikMesh.visible = document.getElementById('show_ikmesh').checked;
|
795 |
-
});
|
796 |
|
797 |
-
|
798 |
-
|
799 |
-
|
800 |
-
|
801 |
-
|
802 |
-
const opt = document.createElement('option');
|
803 |
-
opt.value = b; opt.textContent = b;
|
804 |
-
if (b === configData.default_end_effector) opt.selected = true;
|
805 |
-
endEffectorSel.appendChild(opt);
|
806 |
-
});
|
807 |
-
// Bones
|
808 |
-
bonesContainer.innerHTML = "";
|
809 |
-
configData.selectable_bones.forEach(b=>{
|
810 |
-
const id = `bone_${b}`;
|
811 |
-
const label = document.createElement('label');
|
812 |
-
label.innerHTML = `<input type="checkbox" id="${id}" ${configData.default_controlled_bones.includes(b)?'checked':''}> ${b}`;
|
813 |
-
bonesContainer.appendChild(label);
|
814 |
-
// Add listener for auto-solve
|
815 |
-
setTimeout(()=>{ // ensure element in DOM
|
816 |
-
const cb = document.getElementById(id);
|
817 |
-
if (cb) cb.addEventListener('change', ()=> scheduleSolve());
|
818 |
-
},0);
|
819 |
-
});
|
820 |
-
// Hand controls (agent only)
|
821 |
-
if (currentModel === 'agent'){
|
822 |
-
handFieldset.style.display = '';
|
823 |
-
handShapeSel.innerHTML = "";
|
824 |
-
configData.hand_shapes.forEach(s=>{
|
825 |
-
const opt = document.createElement('option');
|
826 |
-
opt.value = s; opt.textContent = s;
|
827 |
-
handShapeSel.appendChild(opt);
|
828 |
-
});
|
829 |
-
handPosSel.innerHTML = "";
|
830 |
-
configData.hand_positions.forEach(s=>{
|
831 |
-
const opt = document.createElement('option');
|
832 |
-
opt.value = s;
|
833 |
-
opt.textContent = s;
|
834 |
-
handPosSel.appendChild(opt);
|
835 |
-
});
|
836 |
-
} else {
|
837 |
-
handFieldset.style.display = 'none';
|
838 |
-
}
|
839 |
-
// Set subpoints max if provided
|
840 |
-
if (configData && typeof configData.max_subpoints !== 'undefined'){
|
841 |
-
subpointsInput.max = configData.max_subpoints;
|
842 |
-
}
|
843 |
-
if (configData && typeof configData.default_num_steps !== 'undefined'){
|
844 |
-
numStepsInput.value = configData.default_num_steps;
|
845 |
-
}
|
846 |
-
updateTargetSphere();
|
847 |
-
}
|
848 |
|
849 |
-
|
850 |
-
|
|
|
|
|
|
|
|
|
851 |
|
852 |
-
|
853 |
-
// (MODIFY tab switch handler to schedule solve after frames)
|
854 |
-
document.querySelectorAll('.tab-btn').forEach(btn=>{
|
855 |
-
btn.onclick = () => {
|
856 |
-
if (btn.classList.contains('active')) return;
|
857 |
-
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
858 |
-
btn.classList.add('active');
|
859 |
-
currentModel = btn.getAttribute('data-model');
|
860 |
-
modelBadge.textContent = currentModel === 'pepper' ? "Pepper Robot" : "Agent";
|
861 |
-
cleanupVisuals();
|
862 |
-
// Ensure camera will refit once new geometry (fallback mesh or GLTF) is created
|
863 |
-
initialCameraFramed = false; // ADDED
|
864 |
-
applyModelSpecificGLTFPolicy();
|
865 |
-
loadConfig().then(()=>{
|
866 |
-
if (currentModel === 'agent') {
|
867 |
-
const url = document.getElementById('gltf_url').value;
|
868 |
-
if (url) loadGLTF(url);
|
869 |
-
}
|
870 |
-
scheduleSolve(true); // immediate first solve on tab switch
|
871 |
-
updateTargetSphere();
|
872 |
-
});
|
873 |
-
};
|
874 |
-
});
|
875 |
|
876 |
-
|
877 |
-
function ensureActiveMeshBound(firstFrame){
|
878 |
-
if (!firstFrame) return;
|
879 |
-
const verts = firstFrame.vertices;
|
880 |
-
let bound = false;
|
881 |
-
|
882 |
-
// Try GLTF primary mesh first
|
883 |
-
if (gltfPrimaryMesh &&
|
884 |
-
gltfPrimaryMesh.geometry?.attributes?.position &&
|
885 |
-
gltfPrimaryMesh.geometry.attributes.position.count === verts.length){
|
886 |
-
// Remove fallback if exists
|
887 |
-
if (fallbackMesh){
|
888 |
-
modelGroup.remove(fallbackMesh);
|
889 |
-
if (fallbackMesh.geometry) fallbackMesh.geometry.dispose();
|
890 |
-
if (fallbackMesh.material && !Array.isArray(fallbackMesh.material)) fallbackMesh.material.dispose();
|
891 |
-
fallbackMesh = null;
|
892 |
-
}
|
893 |
-
ikMesh = gltfPrimaryMesh;
|
894 |
-
usingGLTFGeometry = true;
|
895 |
-
if (gltfPrimaryMaterial) ikMesh.material = gltfPrimaryMaterial;
|
896 |
-
bound = true;
|
897 |
-
log("ensureActiveMeshBound: using GLTF primary mesh");
|
898 |
-
}
|
899 |
-
|
900 |
-
// Fallback creation/update
|
901 |
-
if (!bound){
|
902 |
-
if (!fallbackMesh ||
|
903 |
-
!fallbackMesh.geometry?.getAttribute('position') ||
|
904 |
-
fallbackMesh.geometry.getAttribute('position').count !== verts.length){
|
905 |
-
if (fallbackMesh){
|
906 |
-
modelGroup.remove(fallbackMesh);
|
907 |
-
if (fallbackMesh.geometry) fallbackMesh.geometry.dispose();
|
908 |
-
if (fallbackMesh.material && !Array.isArray(fallbackMesh.material)) fallbackMesh.material.dispose();
|
909 |
-
}
|
910 |
-
const geom = new THREE.BufferGeometry();
|
911 |
-
const posArr = new Float32Array(verts.length * 3);
|
912 |
-
for (let i=0;i<verts.length;i++){
|
913 |
-
const v = verts[i];
|
914 |
-
posArr[i*3]=v[0]; posArr[i*3+1]=v[1]; posArr[i*3+2]=v[2];
|
915 |
-
}
|
916 |
-
geom.setAttribute('position', new THREE.BufferAttribute(posArr,3));
|
917 |
-
geom.getAttribute('position').setUsage(THREE.DynamicDrawUsage);
|
918 |
-
const faces = firstFrame.faces;
|
919 |
-
const idx = new Uint32Array(faces.length*3);
|
920 |
-
for (let i=0;i<faces.length;i++){
|
921 |
-
const f = faces[i];
|
922 |
-
idx[i*3]=f[0]; idx[i*3+1]=f[1]; idx[i*3+2]=f[2];
|
923 |
-
}
|
924 |
-
geom.setIndex(new THREE.BufferAttribute(idx,1));
|
925 |
-
geom.computeVertexNormals();
|
926 |
-
const mat = gltfPrimaryMaterial ? gltfPrimaryMaterial.clone()
|
927 |
-
: new THREE.MeshStandardMaterial({color:0x6699ff, metalness:0.2, roughness:0.6});
|
928 |
-
fallbackMesh = new THREE.Mesh(geom, mat);
|
929 |
-
fallbackMesh.visible = document.getElementById('show_ikmesh').checked;
|
930 |
-
modelGroup.add(fallbackMesh);
|
931 |
-
ikMesh = fallbackMesh;
|
932 |
-
usingGLTFGeometry = false;
|
933 |
-
log("ensureActiveMeshBound: created fallback mesh");
|
934 |
-
} else {
|
935 |
-
ikMesh = fallbackMesh;
|
936 |
-
usingGLTFGeometry = false;
|
937 |
-
log("ensureActiveMeshBound: reused fallback mesh");
|
938 |
-
}
|
939 |
-
}
|
940 |
-
|
941 |
-
// Pepper scaling (visual only)
|
942 |
-
if (currentModel === 'pepper' && ikMesh){
|
943 |
-
const box = new THREE.Box3().setFromObject(modelGroup);
|
944 |
-
const h = box.max.y - box.min.y;
|
945 |
-
const targetH = 1.2;
|
946 |
-
if (h > 0 && (h < 0.6 || h > 2.0)){
|
947 |
-
const s = targetH / h;
|
948 |
-
modelGroup.scale.set(s,s,s);
|
949 |
-
log(`Pepper scaled: origH=${h.toFixed(3)} scale=${s.toFixed(3)}`);
|
950 |
-
scheduleGroundAlign(true);
|
951 |
-
}
|
952 |
-
}
|
953 |
-
|
954 |
-
if (gltfRoot) gltfRoot.visible = document.getElementById('show_gltf').checked;
|
955 |
-
if (ikMesh) ikMesh.visible = document.getElementById('show_ikmesh').checked;
|
956 |
-
|
957 |
-
updateTargetSphere();
|
958 |
-
}
|
959 |
-
|
960 |
-
// ===== Frame Application (ADDED) =====
|
961 |
-
function applyFrame(frame){
|
962 |
-
if (!ikMesh || !frame) return;
|
963 |
-
const posAttr = ikMesh.geometry.getAttribute('position');
|
964 |
-
if (!posAttr || posAttr.count !== frame.vertices.length){
|
965 |
-
log("applyFrame: vertex count mismatch");
|
966 |
-
return;
|
967 |
-
}
|
968 |
-
const arr = posAttr.array;
|
969 |
-
const verts = frame.vertices;
|
970 |
-
for (let i=0;i<verts.length;i++){
|
971 |
-
const v = verts[i];
|
972 |
-
arr[i*3]=v[0]; arr[i*3+1]=v[1]; arr[i*3+2]=v[2];
|
973 |
-
}
|
974 |
-
posAttr.needsUpdate = true;
|
975 |
-
if (!groundAligned) scheduleGroundAlign();
|
976 |
-
}
|
977 |
-
|
978 |
-
// ----- B-spline helpers (ADDED) ---
|
979 |
-
function bsplineBasis(t){
|
980 |
-
const t2=t*t, t3=t2*t;
|
981 |
-
return [
|
982 |
-
(1 - 3*t + 3*t2 - t3)/6,
|
983 |
-
(4 - 6*t2 + 3*t3)/6,
|
984 |
-
(1 + 3*t + 3*t2 - 3*t3)/6,
|
985 |
-
t3/6
|
986 |
-
];
|
987 |
-
}
|
988 |
-
function getControlFrame(i){
|
989 |
-
if (i < 0) return controlFrames[0];
|
990 |
-
if (i >= controlFrames.length) return controlFrames[controlFrames.length-1];
|
991 |
-
return controlFrames[i];
|
992 |
-
}
|
993 |
-
function evalSplineVertices(u){
|
994 |
-
if (!ikMesh || controlFrames.length < 2) return;
|
995 |
-
const n = controlFrames.length;
|
996 |
-
const seg = Math.min(Math.floor(u), n-2);
|
997 |
-
const t = u - seg;
|
998 |
-
const [w0,w1,w2,w3] = bsplineBasis(t);
|
999 |
-
const f0 = getControlFrame(seg-1);
|
1000 |
-
const f1 = getControlFrame(seg);
|
1001 |
-
const f2 = getControlFrame(seg+1);
|
1002 |
-
const f3 = getControlFrame(seg+2);
|
1003 |
-
const posAttr = ikMesh.geometry.getAttribute('position');
|
1004 |
-
if (!posAttr) return;
|
1005 |
-
const arr = posAttr.array;
|
1006 |
-
const v0=f0.vertices, v1=f1.vertices, v2=f2.vertices, v3=f3.vertices;
|
1007 |
-
const count = posAttr.count;
|
1008 |
-
for (let i=0;i<count;i++){
|
1009 |
-
const a0=v0[i], a1=v1[i], a2=v2[i], a3=v3[i];
|
1010 |
-
arr[i*3] = w0*a0[0]+w1*a1[0]+w2*a2[0]+w3*a3[0];
|
1011 |
-
arr[i*3+1] = w0*a0[1]+w1*a1[1]+w2*a2[1]+w3*a3[1];
|
1012 |
-
arr[i*3+2] = w0*a0[2]+w1*a1[2]+w2*a2[2]+w3*a3[2];
|
1013 |
-
}
|
1014 |
-
posAttr.needsUpdate = true;
|
1015 |
-
if (!groundAligned) scheduleGroundAlign();
|
1016 |
-
}
|
1017 |
-
function setupSplinePlayback(frames){
|
1018 |
-
controlFrames = frames;
|
1019 |
-
splineMode = true;
|
1020 |
-
splineU = 0;
|
1021 |
-
const segments = frames.length - 1;
|
1022 |
-
splineDuration = Math.min(segments * 0.6, 8.0); // heuristic
|
1023 |
-
playbackEnabled = true;
|
1024 |
-
}
|
1025 |
-
|
1026 |
-
// ----- Render Loop -----
|
1027 |
-
function animate(now){
|
1028 |
-
requestAnimationFrame(animate);
|
1029 |
-
const dt = (now - lastTime)/1000;
|
1030 |
-
lastTime = now;
|
1031 |
-
if (splineMode && playbackEnabled){
|
1032 |
-
const n = controlFrames.length;
|
1033 |
-
if (n > 1){
|
1034 |
-
splineU += (dt / splineDuration) * (n - 1);
|
1035 |
-
if (splineU >= (n - 1)){
|
1036 |
-
splineU = n - 1;
|
1037 |
-
playbackEnabled = false; // stop at end
|
1038 |
-
}
|
1039 |
-
evalSplineVertices(splineU);
|
1040 |
-
}
|
1041 |
-
} else if (animationFrames.length > 1 && playbackEnabled){
|
1042 |
-
const fps = parseInt(document.getElementById('play_fps').value,10) || 24;
|
1043 |
-
frameIndex += dt * fps;
|
1044 |
-
if (frameIndex >= animationFrames.length){
|
1045 |
-
if (allowLoopPlayback){
|
1046 |
-
frameIndex = 0;
|
1047 |
-
} else {
|
1048 |
-
frameIndex = animationFrames.length - 1;
|
1049 |
-
playbackEnabled = false; // stop at final frame
|
1050 |
-
}
|
1051 |
-
}
|
1052 |
-
updateFramePlayback();
|
1053 |
-
}
|
1054 |
-
// If not animating but we just solved / loaded, ensure final frame rendered
|
1055 |
-
if (!playbackEnabled && animationFrames.length && frameIndex === animationFrames.length - 1){
|
1056 |
-
// Single application safeguard (light cost)
|
1057 |
-
// applyFrame(animationFrames[frameIndex]); // already applied; can omit
|
1058 |
-
}
|
1059 |
-
if (pendingAlign) performGroundAlign();
|
1060 |
-
|
1061 |
-
// (ADDED) One-time initial camera framing after first geometry appears
|
1062 |
-
if (!initialCameraFramed && modelGroup.children.length){
|
1063 |
-
fitCamera(modelGroup);
|
1064 |
-
initialCameraFramed = true;
|
1065 |
-
}
|
1066 |
-
|
1067 |
-
renderer.render(scene, camera);
|
1068 |
-
}
|
1069 |
requestAnimationFrame(animate);
|
1070 |
|
1071 |
-
|
1072 |
-
applyModelSpecificGLTFPolicy();
|
1073 |
-
loadConfig().then(()=>{
|
1074 |
-
const url = document.getElementById('gltf_url').value;
|
1075 |
-
if (url) loadGLTF(url);
|
1076 |
-
scheduleGroundAlign(true);
|
1077 |
-
scheduleSolve(true);
|
1078 |
-
updateTargetSphere();
|
1079 |
-
});
|
1080 |
|
|
|
|
|
1081 |
})();
|
1082 |
</script>
|
1083 |
</body>
|
|
|
37 |
.toast.error { border-color:#b33939; }
|
38 |
.toast .toast-close { float:right; cursor:pointer; color:#888; margin-left:6px; }
|
39 |
.toast .toast-close:hover { color:#fff; }
|
40 |
+
.axis-pair { margin:4px 0; }
|
41 |
+
.axis-pair .flex-row { align-items:center; }
|
42 |
+
.axis-pair span { width:14px; display:inline-block; font-size:11px; color:#aaa; }
|
43 |
</style>
|
44 |
<!-- Added import map to resolve bare specifier 'three' -->
|
45 |
<script type="importmap">
|
|
|
93 |
<legend>Primary Objectives</legend>
|
94 |
<label><input id="distance_enabled" type="checkbox" checked> Distance Objective</label>
|
95 |
<label>Distance Weight <input id="distance_weight" type="number" value="1.0" step="0.1"></label>
|
96 |
+
</fieldset>
|
97 |
+
<fieldset id="collisionFieldset">
|
98 |
+
<legend>Collision Sphere</legend>
|
99 |
+
<label><input id="collision_enabled" type="checkbox"> Enable Collision Sphere</label>
|
100 |
+
<div class="axis-pair">X
|
101 |
+
<div class="flex-row">
|
102 |
+
<input id="collision_cx" type="number" value="0.1" step="0.01">
|
103 |
+
<input id="collision_cx_slider" type="range" min="-1" max="1" step="0.01" value="0.1">
|
104 |
+
</div>
|
105 |
+
</div>
|
106 |
+
<div class="axis-pair">Y
|
107 |
+
<div class="flex-row">
|
108 |
+
<input id="collision_cy" type="number" value="0.0" step="0.01">
|
109 |
+
<input id="collision_cy_slider" type="range" min="-1" max="1" step="0.01" value="0.0">
|
110 |
+
</div>
|
111 |
+
</div>
|
112 |
+
<div class="axis-pair">Z
|
113 |
+
<div class="flex-row">
|
114 |
+
<input id="collision_cz" type="number" value="0.35" step="0.01">
|
115 |
+
<input id="collision_cz_slider" type="range" min="-1" max="1" step="0.01" value="0.35">
|
116 |
+
</div>
|
117 |
+
</div>
|
118 |
+
<label>Collision Weight <input id="collision_weight" type="number" value="1.0" step="0.1"></label>
|
119 |
+
<label>Sphere Radius <input id="collision_radius" type="number" value="0.1" step="0.01" min="0.01" max="1.0"></label>
|
120 |
+
<label>Min Clearance
|
121 |
+
<div class="flex-row">
|
122 |
+
<input id="collision_min_clearance" type="number" value="0.0" step="0.005" min="0" max="0.5">
|
123 |
+
<input id="collision_min_clearance_slider" type="range" min="0" max="0.5" step="0.005" value="0.0">
|
124 |
+
</div>
|
125 |
+
</label>
|
126 |
</fieldset>
|
127 |
<fieldset>
|
128 |
<legend>Regularization</legend>
|
|
|
197 |
let gltfPrimaryMaterialCaptured = false;
|
198 |
let fallbackMesh = null;
|
199 |
let lastTime = performance.now();
|
|
|
|
|
|
|
200 |
let lastSolveIdAgent = 0;
|
201 |
let lastSolveIdPepper = 0;
|
202 |
|
203 |
+
// ----- Auto-solve -----
|
204 |
let solving = false;
|
|
|
205 |
let solveDebounceTimer = null;
|
206 |
+
const SOLVE_DEBOUNCE_MS = 25;
|
207 |
+
let currentSolveController = null;
|
|
|
208 |
function scheduleSolve(immediate=false){
|
209 |
+
if (solving){ if (currentSolveController) currentSolveController.abort(); }
|
210 |
+
if (immediate){ triggerSolve(true); return; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
211 |
if (solveDebounceTimer) clearTimeout(solveDebounceTimer);
|
212 |
+
solveDebounceTimer = setTimeout(()=> triggerSolve(false), SOLVE_DEBOUNCE_MS);
|
213 |
}
|
214 |
+
async function triggerSolve(){
|
215 |
+
if (currentSolveController) currentSolveController.abort();
|
|
|
|
|
|
|
|
|
216 |
currentSolveController = new AbortController();
|
217 |
const controller = currentSolveController;
|
218 |
solving = true;
|
219 |
try { await doSolve(controller.signal); }
|
220 |
+
catch(e){ if (e.name !== 'AbortError') {} }
|
221 |
+
finally { if (controller === currentSolveController) solving = false; }
|
|
|
|
|
222 |
}
|
223 |
|
224 |
// Playback state
|
|
|
232 |
let alignAttemptCount = 0;
|
233 |
const maxAlignAttempts = 120;
|
234 |
|
235 |
+
// Trajectory / spline state
|
236 |
let controlFrames = [];
|
237 |
let splineMode = false;
|
238 |
let splineU = 0;
|
239 |
+
let splineDuration = 2.0;
|
|
|
240 |
let initialCameraFramed = false;
|
241 |
|
242 |
function cleanupVisuals(){
|
|
|
|
|
|
|
|
|
243 |
if (fallbackMesh){
|
244 |
modelGroup.remove(fallbackMesh);
|
245 |
if (fallbackMesh.geometry) fallbackMesh.geometry.dispose();
|
|
|
247 |
fallbackMesh = null;
|
248 |
}
|
249 |
if (gltfRoot){
|
250 |
+
gltfRoot.traverse(o=>{ if (o.isMesh){ if (o.geometry) o.geometry.dispose(); if (o.material){ if (Array.isArray(o.material)) o.material.forEach(m=>m.dispose()); else o.material.dispose(); } } });
|
251 |
+
modelGroup.remove(gltfRoot); gltfRoot = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
252 |
}
|
|
|
253 |
while (modelGroup.children.length) modelGroup.remove(modelGroup.children[0]);
|
254 |
+
gltfPrimaryMesh = null; gltfPrimaryMaterial = null; gltfPrimaryMaterialCaptured = false; ikMesh = null; usingGLTFGeometry = false;
|
255 |
+
animationFrames = []; frameIndex = 0; baseOffsetY = 0; groundAligned = false; pendingAlign = false; alignAttemptCount = 0;
|
256 |
+
log('Visuals cleaned.');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
257 |
}
|
258 |
+
function scheduleGroundAlign(force=false){ if (force){ groundAligned=false; alignAttemptCount=0; } pendingAlign=true; }
|
259 |
function performGroundAlign(){
|
260 |
if (!pendingAlign) return;
|
261 |
+
if (!modelGroup.children.length){ if (++alignAttemptCount > maxAlignAttempts) pendingAlign=false; return; }
|
|
|
|
|
|
|
262 |
const box = new THREE.Box3().setFromObject(modelGroup);
|
263 |
+
if (box.isEmpty() || !isFinite(box.min.y)){ if (++alignAttemptCount > maxAlignAttempts) pendingAlign=false; return; }
|
264 |
+
const currentMin = box.min.y; const delta = -currentMin;
|
|
|
|
|
|
|
|
|
|
|
|
|
265 |
if (delta > 0 || Math.abs(delta) < 1e-3){
|
266 |
+
modelGroup.position.y += delta; baseOffsetY = modelGroup.position.y; groundAligned = true; updateTargetSphere(); updateCollisionSphere();
|
|
|
|
|
|
|
267 |
}
|
268 |
+
pendingAlign=false;
|
269 |
}
|
|
|
|
|
270 |
function fitCamera(obj){
|
271 |
+
const targetObj = modelGroup.children.length ? modelGroup : obj; if (!targetObj) return;
|
272 |
+
const box = new THREE.Box3().setFromObject(targetObj); if (box.isEmpty()) return;
|
273 |
+
const size = box.getSize(new THREE.Vector3()); const center = box.getCenter(new THREE.Vector3());
|
274 |
+
controls.target.copy(center); const maxDim = Math.max(size.x,size.y,size.z);
|
275 |
+
const dist = maxDim * 3; const dir = new THREE.Vector3(0.0,0.5,1).normalize();
|
|
|
|
|
|
|
|
|
|
|
276 |
camera.position.copy(center).addScaledVector(dir, dist);
|
277 |
+
camera.near = maxDim/100; camera.far = maxDim*100; camera.updateProjectionMatrix(); controls.update();
|
|
|
|
|
|
|
278 |
}
|
279 |
|
280 |
// ----- DOM refs -----
|
|
|
285 |
const handPosSel = document.getElementById('hand_position');
|
286 |
const handFieldset = document.getElementById('handFieldset');
|
287 |
const modelBadge = document.getElementById('modelBadge');
|
288 |
+
const subpointsInput = document.getElementById('subpoints');
|
289 |
+
const numStepsInput = document.getElementById('num_steps');
|
290 |
+
// Collision inputs
|
291 |
+
const collisionEnabledEl = document.getElementById('collision_enabled');
|
292 |
+
const collisionWeightEl = document.getElementById('collision_weight');
|
293 |
+
const collCx = document.getElementById('collision_cx');
|
294 |
+
const collCy = document.getElementById('collision_cy');
|
295 |
+
const collCz = document.getElementById('collision_cz');
|
296 |
+
const collCxSlider = document.getElementById('collision_cx_slider');
|
297 |
+
const collCySlider = document.getElementById('collision_cy_slider');
|
298 |
+
const collCzSlider = document.getElementById('collision_cz_slider');
|
299 |
+
const collRadiusEl = document.getElementById('collision_radius');
|
300 |
+
const collMinClearEl = document.getElementById('collision_min_clearance');
|
301 |
+
const collMinClearSlider = document.getElementById('collision_min_clearance_slider');
|
302 |
|
|
|
303 |
function val(id){ return document.getElementById(id).value; }
|
304 |
function num(id){ return parseFloat(val(id)); }
|
305 |
function bool(id){ return document.getElementById(id).checked; }
|
|
|
310 |
renderer.setSize(document.getElementById('viewerPane').clientWidth, document.getElementById('viewerPane').clientHeight);
|
311 |
renderer.domElement.className = 'viewer-canvas';
|
312 |
document.getElementById('viewerPane').appendChild(renderer.domElement);
|
313 |
+
const scene = new THREE.Scene(); scene.background = new THREE.Color(0x222222);
|
|
|
|
|
|
|
314 |
const camera = new THREE.PerspectiveCamera(45, renderer.domElement.clientWidth / renderer.domElement.clientHeight, 0.01, 1000);
|
315 |
camera.position.set(0,1,3);
|
316 |
+
const controls = new OrbitControls(camera, renderer.domElement); controls.target.set(0,0.8,0);
|
|
|
|
|
|
|
317 |
scene.add(new THREE.HemisphereLight(0xffffff,0x444444,1.0));
|
318 |
+
const d = new THREE.DirectionalLight(0xffffff,0.8); d.position.set(3,10,10); scene.add(d);
|
319 |
+
const ground = new THREE.Mesh(new THREE.CircleGeometry(5,48), new THREE.MeshStandardMaterial({color:0x303030, metalness:0.1, roughness:0.9}));
|
320 |
+
ground.rotation.x = -Math.PI/2; ground.receiveShadow = true; scene.add(ground); ground.visible = false;
|
321 |
+
const modelGroup = new THREE.Group(); scene.add(modelGroup);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
322 |
|
323 |
+
// Target sphere
|
|
|
|
|
|
|
|
|
324 |
const targetSphereGeom = new THREE.SphereGeometry(0.01, 24, 24);
|
325 |
const targetSphereMat = new THREE.MeshStandardMaterial({color:0x00ff55, emissive:0x008833});
|
326 |
+
const targetSphere = new THREE.Mesh(targetSphereGeom, targetSphereMat); scene.add(targetSphere);
|
327 |
+
// Collision sphere (unit scaled later)
|
328 |
+
const collisionSphereGeom = new THREE.SphereGeometry(1, 32, 32);
|
329 |
+
const collisionSphereMat = new THREE.MeshStandardMaterial({color:0xff4444, emissive:0x660000, transparent:true, opacity:0.18, wireframe:false});
|
330 |
+
const collisionSphere = new THREE.Mesh(collisionSphereGeom, collisionSphereMat); scene.add(collisionSphere); collisionSphere.visible = false;
|
331 |
|
|
|
|
|
332 |
function updateTargetSphere(){
|
333 |
const xRaw = parseFloat(document.getElementById('target_x').value) || 0;
|
334 |
const yRaw = parseFloat(document.getElementById('target_y').value) || 0;
|
335 |
const zRaw = parseFloat(document.getElementById('target_z').value) || 0;
|
336 |
+
const sx = modelGroup?.scale?.x ?? 1; const sy = modelGroup?.scale?.y ?? 1; const sz = modelGroup?.scale?.z ?? 1;
|
337 |
+
const tx = modelGroup?.position?.x ?? 0; const ty = modelGroup?.position?.y ?? 0; const tz = modelGroup?.position?.z ?? 0;
|
338 |
+
targetSphere.position.set(xRaw * sx + tx, yRaw * sy + ty, zRaw * sz + tz);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
339 |
}
|
340 |
+
function updateCollisionSphere(){
|
341 |
+
const cx = parseFloat(collCx.value) || 0; const cy = parseFloat(collCy.value) || 0; const cz = parseFloat(collCz.value) || 0;
|
342 |
+
const r = parseFloat(collRadiusEl.value) || 0.1; const mc = parseFloat(collMinClearEl.value) || 0.0;
|
343 |
+
const sx = modelGroup?.scale?.x ?? 1; const sy = modelGroup?.scale?.y ?? 1; const sz = modelGroup?.scale?.z ?? 1; const tx = modelGroup?.position?.x ?? 0; const ty = modelGroup?.position?.y ?? 0; const tz = modelGroup?.position?.z ?? 0;
|
344 |
+
collisionSphere.position.set(cx * sx + tx, cy * sy + ty, cz * sz + tz);
|
345 |
+
const uniform = (sx+sy+sz)/3; const effective = r + mc; // visualize base radius + clearance
|
346 |
+
collisionSphere.scale.set(effective*uniform, effective*uniform, effective*uniform);
|
347 |
+
collisionSphere.visible = collisionEnabledEl.checked;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
348 |
}
|
349 |
+
['target_x','target_y','target_z'].forEach(id=>{ document.getElementById(id).addEventListener('input', updateTargetSphere); document.getElementById(id).addEventListener('change', updateTargetSphere); });
|
350 |
+
updateTargetSphere(); updateCollisionSphere();
|
351 |
+
|
352 |
+
window.addEventListener('resize', () => { renderer.setSize(document.getElementById('viewerPane').clientWidth, document.getElementById('viewerPane').clientHeight); camera.aspect = renderer.domElement.clientWidth / renderer.domElement.clientHeight; camera.updateProjectionMatrix(); });
|
353 |
+
function resetCamera(){ camera.position.set(0,1,3); controls.target.set(0,0.8,0); controls.update(); }
|
354 |
|
355 |
// ----- Config fetch -----
|
356 |
+
async function loadConfig(){ const res = await fetch(`/config?model=${currentModel}`); configData = await res.json(); populateUIFromConfig(); log(`Config loaded for ${currentModel}`); }
|
|
|
|
|
|
|
|
|
|
|
357 |
|
358 |
function populateUIFromConfig(){
|
359 |
+
endEffectorSel.innerHTML = ""; configData.end_effector_choices.forEach(b=>{ const opt=document.createElement('option'); opt.value=b; opt.textContent=b; if (b===configData.default_end_effector) opt.selected=true; endEffectorSel.appendChild(opt); });
|
360 |
+
bonesContainer.innerHTML = ""; configData.selectable_bones.forEach(b=>{ const id=`bone_${b}`; const label=document.createElement('label'); label.innerHTML = `<input type="checkbox" id="${id}" ${configData.default_controlled_bones.includes(b)?'checked':''}> ${b}`; bonesContainer.appendChild(label); setTimeout(()=>{ const cb=document.getElementById(id); if (cb) cb.addEventListener('change', ()=> scheduleSolve()); },0); });
|
361 |
+
if (currentModel === 'agent'){ handFieldset.style.display=''; handShapeSel.innerHTML=""; configData.hand_shapes.forEach(s=>{ const o=document.createElement('option'); o.value=s; o.textContent=s; handShapeSel.appendChild(o); }); handPosSel.innerHTML=""; configData.hand_positions.forEach(s=>{ const o=document.createElement('option'); o.value=s; o.textContent=s; handPosSel.appendChild(o); }); } else { handFieldset.style.display='none'; }
|
362 |
+
if (configData && typeof configData.max_subpoints !== 'undefined') subpointsInput.max = configData.max_subpoints;
|
363 |
+
if (configData && typeof configData.default_num_steps !== 'undefined') numStepsInput.value = configData.default_num_steps;
|
364 |
+
if (configData && configData.collision_default_center){ const c=configData.collision_default_center; collCx.value=c[0]; collCy.value=c[1]; collCz.value=c[2]; collCxSlider.value=c[0]; collCySlider.value=c[1]; collCzSlider.value=c[2]; }
|
365 |
+
if (configData && typeof configData.collision_default_radius !== 'undefined'){ collRadiusEl.value = configData.collision_default_radius; }
|
366 |
+
if (configData && typeof configData.collision_default_min_clearance !== 'undefined'){ collMinClearEl.value = configData.collision_default_min_clearance; collMinClearSlider.value = configData.collision_default_min_clearance; }
|
367 |
+
updateTargetSphere(); updateCollisionSphere();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
368 |
}
|
369 |
|
370 |
+
subpointsInput.addEventListener('change', ()=>{ const v=parseInt(subpointsInput.value,10); if (v<=1){ document.getElementById('derivative_enabled').checked=false; } scheduleSolve(); });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
371 |
|
372 |
+
function applyModelSpecificGLTFPolicy(){ const urlInput=document.getElementById('gltf_url'); const showGltfChk=document.getElementById('show_gltf'); if (currentModel==='pepper'){ showGltfChk.disabled=true; if (gltfRoot) gltfRoot.visible=false; } else { if (!urlInput.value) urlInput.value="/files/smplx.glb"; showGltfChk.disabled=false; } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
373 |
|
|
|
374 |
const gltfLoader = new GLTFLoader();
|
375 |
+
function loadGLTF(url){ if (!url){ log('No GLTF URL (possibly Pepper) - skipping load.'); return; } const u=url.startsWith('http')?url:url; statusBar.textContent=`Loading GLTF: ${u}`; log(`Loading GLTF ${u}`); gltfLoader.load(u, gltf=>{ if (gltfRoot) modelGroup.remove(gltfRoot); gltfRoot=gltf.scene; gltfPrimaryMaterial=null; gltfPrimaryMesh=null; gltfPrimaryMaterialCaptured=false; gltfRoot.traverse(o=>{ if (o.isMesh && o.geometry?.attributes?.position){ if (!gltfPrimaryMesh || o.geometry.attributes.position.count > gltfPrimaryMesh.geometry.attributes.position.count){ gltfPrimaryMesh=o; } } }); if (gltfPrimaryMesh){ if (Array.isArray(gltfPrimaryMesh.material)){ const mm=gltfPrimaryMesh.material.find(m=>m.map) || gltfPrimaryMesh.material[0]; gltfPrimaryMaterial=mm.clone(); } else { gltfPrimaryMaterial=gltfPrimaryMesh.material.clone(); } gltfPrimaryMaterialCaptured=true; log(`Captured GLTF primary mesh (verts=${gltfPrimaryMesh.geometry.attributes.position.count})`); } else { log('No primary mesh found in GLTF.'); }
|
376 |
+
modelGroup.add(gltfRoot); gltfRoot.visible=document.getElementById('show_gltf').checked; if (animationFrames.length){ ensureActiveMeshBound(animationFrames[0]); applyFrame(animationFrames[Math.min(frameIndex, animationFrames.length-1)]); } fitCamera(gltfRoot); statusBar.textContent='GLTF loaded.'; }, undefined, err=>{ statusBar.textContent='GLTF error: '+err.message; log('GLTF error: '+err.message); }); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
377 |
|
378 |
+
document.getElementById('solve_btn').onclick = ()=> triggerSolve(true);
|
379 |
+
function collectBones(){ const bones=[]; if (!configData) return bones; configData.selectable_bones.forEach(b=>{ const cb=document.getElementById(`bone_${b}`); if (cb && cb.checked) bones.push(b); }); return bones; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
380 |
|
381 |
async function doSolve(abortSignal){
|
382 |
+
statusBar.textContent='Solving...'; log('Solve request started');
|
383 |
+
const subpointsVal=parseInt(val('subpoints'),10);
|
384 |
+
const payload={
|
|
|
385 |
model: currentModel,
|
386 |
target: [num('target_x'), num('target_y'), num('target_z')],
|
387 |
subpoints: subpointsVal,
|
388 |
num_steps: parseInt(numStepsInput.value,10) || 100,
|
389 |
distance_enabled: bool('distance_enabled'),
|
390 |
distance_weight: num('distance_weight'),
|
391 |
+
collision_enabled: collisionEnabledEl.checked,
|
392 |
+
collision_weight: parseFloat(collisionWeightEl.value)||1.0,
|
393 |
+
collision_center: [parseFloat(collCx.value)||0, parseFloat(collCy.value)||0, parseFloat(collCz.value)||0],
|
394 |
+
collision_radius: parseFloat(collRadiusEl.value)||0.1,
|
395 |
bone_zero_enabled: bool('bone_zero_enabled'),
|
396 |
bone_zero_weight: num('bone_zero_weight'),
|
397 |
derivative_enabled: bool('derivative_enabled'),
|
398 |
derivative_weight: num('derivative_weight'),
|
399 |
controlled_bones: collectBones(),
|
400 |
end_effector: endEffectorSel.value,
|
401 |
+
hand_shape: currentModel==='agent'? handShapeSel.value : 'None',
|
402 |
+
hand_position: currentModel==='agent'? handPosSel.value : 'None',
|
403 |
+
frames_mode: subpointsVal > 1 ? 'auto' : 'last',
|
404 |
+
collision_min_clearance: parseFloat(collMinClearEl.value)||0.0,
|
405 |
};
|
406 |
+
try { const t0=performance.now(); const res=await fetch('/solve',{method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload), signal:abortSignal}); if (!res.ok) throw new Error(res.status+' '+res.statusText); const json=await res.json(); if (json.status!=='ok') throw new Error(json.message || 'Solve failed'); const sid=json.result.solve_id||0; if (payload.model==='agent'){ if (sid <= lastSolveIdAgent){ log(`Stale agent solve ignored (sid=${sid} <= ${lastSolveIdAgent})`); return; } lastSolveIdAgent=sid; } else { if (sid <= lastSolveIdPepper){ log(`Stale pepper solve ignored (sid=${sid} <= ${lastSolveIdPepper})`); return; } lastSolveIdPepper=sid; }
|
407 |
+
const server=json.result.solve_time; const total=(performance.now()-t0)/1000; statusBar.textContent=`Solved: server=${server.toFixed(2)}s total=${total.toFixed(2)}s it=${json.result.iterations} obj=${json.result.objective.toFixed(6)} frames=${json.result.frames}`; log(`Solve completed (frames received=${json.result.frames})`); if (json.result.frames_data && json.result.frames_data.length){ animationFrames=json.result.frames_data; ensureActiveMeshBound(animationFrames[0]); controlFrames=[]; splineMode=false; playbackEnabled=false; if (animationFrames.length>1 && subpointsVal>1){ setupSplinePlayback(animationFrames); applyFrame(animationFrames[0]); } else { frameIndex=animationFrames.length-1; applyFrame(animationFrames[frameIndex]); } scheduleGroundAlign(); }
|
408 |
+
updateTargetSphere(); updateCollisionSphere(); showToast(`Solve OK • it=${json.result.iterations} • t=${json.result.solve_time.toFixed(2)}s • err=${json.result.objective.toExponential(2)}`,'success',4000); }
|
409 |
+
catch(e){ if (e.name==='AbortError'){ log('Solve aborted (superseded)'); return; } statusBar.textContent='Solve error: '+e.message; log('Solve error: '+e.message); showToast(`Solve ERROR: ${e.message}`,'error',6000); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
410 |
}
|
411 |
|
412 |
+
const toastContainer=document.getElementById('toastContainer');
|
413 |
+
function showToast(message, type='success', ttl=4000){ const el=document.createElement('div'); el.className=`toast ${type}`; const close=document.createElement('span'); close.className='toast-close'; close.textContent='✕'; close.onclick=e=>{ e.stopPropagation(); remove(); }; const content=document.createElement('div'); content.textContent=message; el.appendChild(close); el.appendChild(content); toastContainer.appendChild(el); requestAnimationFrame(()=> el.classList.add('show')); function remove(){ el.classList.remove('show'); setTimeout(()=> el.remove(), 180); } setTimeout(remove, ttl); }
|
414 |
+
|
415 |
+
const xNum=document.getElementById('target_x'); const yNum=document.getElementById('target_y'); const zNum=document.getElementById('target_z'); const xSlider=document.getElementById('target_x_slider'); const ySlider=document.getElementById('target_y_slider');
|
416 |
+
function syncSliderToNum(axis){ if (axis==='x') xSlider.value=xNum.value; else if (axis==='y') ySlider.value=yNum.value; }
|
417 |
+
function syncNumToSlider(axis){ if (axis==='x') xNum.value=xSlider.value; else if (axis==='y') yNum.value=ySlider.value; }
|
418 |
+
xSlider.addEventListener('input', ()=>{ syncNumToSlider('x'); updateTargetSphere(); }); ySlider.addEventListener('input', ()=>{ syncNumToSlider('y'); updateTargetSphere(); });
|
419 |
+
xSlider.addEventListener('change', ()=>{ syncNumToSlider('x'); updateTargetSphere(); scheduleSolve(); }); ySlider.addEventListener('change', ()=>{ syncNumToSlider('y'); updateTargetSphere(); scheduleSolve(); });
|
420 |
+
xNum.addEventListener('change', ()=>{ syncSliderToNum('x'); updateTargetSphere(); scheduleSolve(); }); yNum.addEventListener('change', ()=>{ syncSliderToNum('y'); updateTargetSphere(); scheduleSolve(); }); zNum.addEventListener('change', ()=>{ updateTargetSphere(); scheduleSolve(); });
|
421 |
+
|
422 |
+
// Collision sliders
|
423 |
+
function syncCollSliderToNum(axis){ if (axis==='x') collCxSlider.value=collCx.value; else if (axis==='y') collCySlider.value=collCy.value; else if (axis==='z') collCzSlider.value=collCz.value; else if (axis==='mc') collMinClearSlider.value=collMinClearEl.value; }
|
424 |
+
function syncCollNumToSlider(axis){ if (axis==='x') collCx.value=collCxSlider.value; else if (axis==='y') collCy.value=collCySlider.value; else if (axis==='z') collCz.value=collCzSlider.value; else if (axis==='mc') collMinClearEl.value=collMinClearSlider.value; }
|
425 |
+
['x','y','z'].forEach(a=>{ const slider = a==='x'?collCxSlider: a==='y'?collCySlider:collCzSlider; slider.addEventListener('input', ()=>{ syncCollNumToSlider(a); updateCollisionSphere(); }); slider.addEventListener('change', ()=>{ syncCollNumToSlider(a); updateCollisionSphere(); scheduleSolve(); }); });
|
426 |
+
[collCx, collCy, collCz].forEach((el,i)=> el.addEventListener('change', ()=>{ syncCollSliderToNum(i===0?'x':i===1?'y':'z'); updateCollisionSphere(); scheduleSolve(); }));
|
427 |
+
collRadiusEl.addEventListener('change', ()=>{ updateCollisionSphere(); scheduleSolve(); });
|
428 |
+
collisionEnabledEl.addEventListener('change', ()=>{ updateCollisionSphere(); scheduleSolve(); });
|
429 |
+
collMinClearSlider.addEventListener('input', ()=>{ syncCollNumToSlider('mc'); updateCollisionSphere(); });
|
430 |
+
collMinClearSlider.addEventListener('change', ()=>{ syncCollNumToSlider('mc'); updateCollisionSphere(); scheduleSolve(); });
|
431 |
+
collMinClearEl.addEventListener('change', ()=>{ syncCollSliderToNum('mc'); updateCollisionSphere(); scheduleSolve(); });
|
432 |
+
|
433 |
+
const autoSolveSelectors=[
|
434 |
'#distance_enabled','#distance_weight',
|
435 |
+
'#collision_enabled','#collision_weight', '#collision_radius', '#collision_cx', '#collision_cy', '#collision_cz', '#collision_min_clearance',
|
436 |
'#bone_zero_enabled','#bone_zero_weight',
|
437 |
'#derivative_enabled','#derivative_weight',
|
438 |
'#hand_shape','#hand_position',
|
439 |
'#end_effector','#wireframe','#show_gltf','#show_ikmesh',
|
440 |
+
'#play_fps','#num_steps'
|
441 |
];
|
442 |
+
autoSolveSelectors.forEach(sel=>{ const el=document.querySelector(sel); if (el){ const evt=(el.type==='checkbox'||el.tagName==='SELECT')?'change':'input'; el.addEventListener(evt, ()=> scheduleSolve()); }});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
443 |
|
444 |
+
document.getElementById('wireframe').addEventListener('change', ()=>{ if (ikMesh && ikMesh.material){ ikMesh.material.wireframe=document.getElementById('wireframe').checked; ikMesh.material.needsUpdate=true; } if (gltfPrimaryMesh && gltfPrimaryMesh.material){ if (Array.isArray(gltfPrimaryMesh.material)) gltfPrimaryMesh.material.forEach(m=>{m.wireframe=document.getElementById('wireframe').checked; m.needsUpdate=true;}); else { gltfPrimaryMesh.material.wireframe=document.getElementById('wireframe').checked; gltfPrimaryMesh.material.needsUpdate=true; } } });
|
445 |
+
document.getElementById('show_gltf').addEventListener('change', ()=>{ if (gltfRoot) gltfRoot.visible=document.getElementById('show_gltf').checked; });
|
446 |
+
document.getElementById('show_ikmesh').addEventListener('change', ()=>{ if (ikMesh) ikMesh.visible=document.getElementById('show_ikmesh').checked; });
|
|
|
|
|
|
|
447 |
|
448 |
+
function ensureActiveMeshBound(firstFrame){ if (!firstFrame) return; const verts=firstFrame.vertices; let bound=false; if (gltfPrimaryMesh && gltfPrimaryMesh.geometry?.attributes?.position && gltfPrimaryMesh.geometry.attributes.position.count === verts.length){ if (fallbackMesh){ modelGroup.remove(fallbackMesh); if (fallbackMesh.geometry) fallbackMesh.geometry.dispose(); if (fallbackMesh.material && !Array.isArray(fallbackMesh.material)) fallbackMesh.material.dispose(); fallbackMesh=null; } ikMesh=gltfPrimaryMesh; usingGLTFGeometry=true; if (gltfPrimaryMaterial) ikMesh.material=gltfPrimaryMaterial; bound=true; log('ensureActiveMeshBound: using GLTF primary mesh'); }
|
449 |
+
if (!bound){ if (!fallbackMesh || !fallbackMesh.geometry?.getAttribute('position') || fallbackMesh.geometry.getAttribute('position').count !== verts.length){ if (fallbackMesh){ modelGroup.remove(fallbackMesh); if (fallbackMesh.geometry) fallbackMesh.geometry.dispose(); if (fallbackMesh.material && !Array.isArray(fallbackMesh.material)) fallbackMesh.material.dispose(); }
|
450 |
+
const geom=new THREE.BufferGeometry(); const posArr=new Float32Array(verts.length*3); for (let i=0;i<verts.length;i++){ const v=verts[i]; posArr[i*3]=v[0]; posArr[i*3+1]=v[1]; posArr[i*3+2]=v[2]; } geom.setAttribute('position', new THREE.BufferAttribute(posArr,3)); geom.getAttribute('position').setUsage(THREE.DynamicDrawUsage); const faces=firstFrame.faces; const idx=new Uint32Array(faces.length*3); for (let i=0;i<faces.length;i++){ const f=faces[i]; idx[i*3]=f[0]; idx[i*3+1]=f[1]; idx[i*3+2]=f[2]; } geom.setIndex(new THREE.BufferAttribute(idx,1)); geom.computeVertexNormals(); const mat=gltfPrimaryMaterial?gltfPrimaryMaterial.clone(): new THREE.MeshStandardMaterial({color:0x6699ff, metalness:0.2, roughness:0.6}); fallbackMesh=new THREE.Mesh(geom, mat); fallbackMesh.visible=document.getElementById('show_ikmesh').checked; modelGroup.add(fallbackMesh); ikMesh=fallbackMesh; usingGLTFGeometry=false; log('ensureActiveMeshBound: created fallback mesh'); } else { ikMesh=fallbackMesh; usingGLTFGeometry=false; log('ensureActiveMeshBound: reused fallback mesh'); } }
|
451 |
+
if (currentModel==='pepper' && ikMesh){ const box=new THREE.Box3().setFromObject(modelGroup); const h=box.max.y - box.min.y; const targetH=1.2; if (h>0 && (h<0.6 || h>2.0)){ const s=targetH / h; modelGroup.scale.set(s,s,s); log(`Pepper scaled: origH=${h.toFixed(3)} scale=${s.toFixed(3)}`); scheduleGroundAlign(true); } }
|
452 |
+
if (gltfRoot) gltfRoot.visible=document.getElementById('show_gltf').checked; if (ikMesh) ikMesh.visible=document.getElementById('show_ikmesh').checked; updateTargetSphere(); updateCollisionSphere(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
453 |
|
454 |
+
function applyFrame(frame){ if (!ikMesh || !frame) return; const posAttr=ikMesh.geometry.getAttribute('position'); if (!posAttr || posAttr.count !== frame.vertices.length){ log('applyFrame: vertex count mismatch'); return; } const arr=posAttr.array; const verts=frame.vertices; for (let i=0;i<verts.length;i++){ const v=verts[i]; arr[i*3]=v[0]; arr[i*3+1]=v[1]; arr[i*3+2]=v[2]; } posAttr.needsUpdate=true; if (!groundAligned) scheduleGroundAlign(); }
|
455 |
+
function bsplineBasis(t){ const t2=t*t, t3=t2*t; return [(1 - 3*t + 3*t2 - t3)/6,(4 - 6*t2 + 3*t3)/6,(1 + 3*t + 3*t2 - 3*t3)/6,t3/6]; }
|
456 |
+
function getControlFrame(i){ if (i<0) return controlFrames[0]; if (i>=controlFrames.length) return controlFrames[controlFrames.length-1]; return controlFrames[i]; }
|
457 |
+
function evalSplineVertices(u){ if (!ikMesh || controlFrames.length<2) return; const n=controlFrames.length; const seg=Math.min(Math.floor(u), n-2); const t=u - seg; const [w0,w1,w2,w3]=bsplineBasis(t); const f0=getControlFrame(seg-1); const f1=getControlFrame(seg); const f2=getControlFrame(seg+1); const f3=getControlFrame(seg+2); const posAttr=ikMesh.geometry.getAttribute('position'); if (!posAttr) return; const arr=posAttr.array; const v0=f0.vertices, v1=f1.vertices, v2=f2.vertices, v3=f3.vertices; const count=posAttr.count; for (let i=0;i<count;i++){ const a0=v0[i], a1=v1[i], a2=v2[i], a3=v3[i]; arr[i*3]=w0*a0[0]+w1*a1[0]+w2*a2[0]+w3*a3[0]; arr[i*3+1]=w0*a0[1]+w1*a1[1]+w2*a2[1]+w3*a3[1]; arr[i*3+2]=w0*a0[2]+w1*a1[2]+w2*a2[2]+w3*a3[2]; } posAttr.needsUpdate=true; if (!groundAligned) scheduleGroundAlign(); }
|
458 |
+
function setupSplinePlayback(frames){ controlFrames=frames; splineMode=true; splineU=0; const segments=frames.length - 1; splineDuration=Math.min(segments * 0.6, 8.0); playbackEnabled=true; }
|
459 |
+
function updateFramePlayback(){ if (!animationFrames.length) return; const idx=Math.min(Math.floor(frameIndex), animationFrames.length - 1); applyFrame(animationFrames[idx]); }
|
460 |
|
461 |
+
document.querySelectorAll('.tab-btn').forEach(btn=>{ btn.onclick=()=>{ if (btn.classList.contains('active')) return; document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); currentModel=btn.getAttribute('data-model'); modelBadge.textContent=currentModel==='pepper'? 'Pepper Robot':'Agent'; cleanupVisuals(); initialCameraFramed=false; applyModelSpecificGLTFPolicy(); loadConfig().then(()=>{ if (currentModel==='agent'){ const url=document.getElementById('gltf_url').value; if (url) loadGLTF(url); } scheduleSolve(true); updateTargetSphere(); updateCollisionSphere(); }); }; });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
462 |
|
463 |
+
function animate(now){ requestAnimationFrame(animate); const dt=(now - lastTime)/1000; lastTime=now; if (splineMode && playbackEnabled){ const n=controlFrames.length; if (n>1){ splineU += (dt / splineDuration) * (n - 1); if (splineU >= (n - 1)){ splineU = n - 1; playbackEnabled=false; } evalSplineVertices(splineU); } } else if (animationFrames.length>1 && playbackEnabled){ const fps=parseInt(document.getElementById('play_fps').value,10) || 24; frameIndex += dt * fps; if (frameIndex >= animationFrames.length){ if (allowLoopPlayback){ frameIndex=0; } else { frameIndex=animationFrames.length - 1; playbackEnabled=false; } } updateFramePlayback(); } if (pendingAlign) performGroundAlign(); if (!initialCameraFramed && modelGroup.children.length){ fitCamera(modelGroup); initialCameraFramed=true; } renderer.render(scene, camera); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
464 |
requestAnimationFrame(animate);
|
465 |
|
466 |
+
document.getElementById('reset_cam').addEventListener('click', ()=>{ fitCamera(modelGroup); });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
467 |
|
468 |
+
applyModelSpecificGLTFPolicy();
|
469 |
+
loadConfig().then(()=>{ const url=document.getElementById('gltf_url').value; if (url) loadGLTF(url); scheduleGroundAlign(true); scheduleSolve(true); updateTargetSphere(); updateCollisionSphere(); });
|
470 |
})();
|
471 |
</script>
|
472 |
</body>
|