112- "cover_mesh_loading": real_mesh_candidate, 113- "flat_heightmap_processing": real_mesh_candidate, 114- "adaptive_surface_wrap": False, 115- "protected_zone_mesh_intersection": False, 116- "wall_thickness_analysis": False, 117- "mesh_repair": False, 118: "stl_export": False, 119: "three_mf_export": False, 120: "step_export": False, 121: "manufacturing_export": False 122- }, 123: "manufacturing_export_blocked": True, 124- "safety": CONFIG["safety"] 125- } 126- 127- 128-def sha256_json(value: Any) -> str: 129- encoded = json.dumps( 130- value, 131- sort_keys=True, 132- separators=(",", ":"), 133- ensure_ascii=False, 134- ).encode("utf-8") 135- return hashlib.sha256(encoded).hexdigest() 136- 137- 138:def validate_number( 139- errors: list[str], 140- value: Any, 141- name: str, 142- minimum: float | None = None, 143- maximum: float | None = None, 144-) -> None: 145- if value is None: 146- return 147- if isinstance(value, bool) or not isinstance(value, (int, float)): 148- errors.append(f"{name} must be numeric") 149- return 150- if minimum is not None and value < minimum: 151- errors.append(f"{name} must be at least {minimum}") 152- if maximum is not None and value > maximum: 153- errors.append(f"{name} must be no more than {maximum}") 154- 155- 156:def validate_geometry_request(payload: Any) -> list[str]: 157- errors: list[str] = [] 158- 159- if not isinstance(payload, dict): 160- return ["Request payload must be a JSON object"] 161- 162- cover = payload.get("cover_model") 163- if not isinstance(cover, dict): 164- errors.append("cover_model is required") 165- cover = {} 166- 167: if CONFIG["safety"]["require_validated_cover"]: 168- if not cover.get("id"): 169- errors.append("cover_model.id is required") 170- if not cover.get("reference"): 171- errors.append("cover_model.reference is required") 172- 173- layers = payload.get("layers") 174- if not isinstance(layers, list) or not layers: 175- errors.append("At least one visible artwork layer is required") 176- layers = [] 177- 178- for index, layer in enumerate(layers): 179- prefix = f"layers[{index}]" -- 186- 187- transform = layer.get("transform") 188- if not isinstance(transform, dict): 189- errors.append(f"{prefix}.transform is required") 190- continue 191- 192: validate_number(errors, transform.get("position_x"), f"{prefix}.transform.position_x", -2, 2) 193: validate_number(errors, transform.get("position_y"), f"{prefix}.transform.position_y", -2, 2) 194: validate_number(errors, transform.get("scale_x"), f"{prefix}.transform.scale_x", 0.01, 10) 195: validate_number(errors, transform.get("scale_y"), f"{prefix}.transform.scale_y", 0.01, 10) 196: validate_number(errors, transform.get("rotation"), f"{prefix}.transform.rotation", -360, 360) 197: validate_number(errors, transform.get("opacity"), f"{prefix}.transform.opacity", 0, 1) 198- 199: if layer.get("projection_mode") not in ("planar", "cylindrical", "surface"): 200- errors.append( 201: f"{prefix}.projection_mode must be planar, cylindrical or surface" 202- ) 203- 204- mode = payload.get("application_mode") 205- valid_modes = { 206- "colour_texture", 207- "raised_emboss", 208- "engraving", 209: "sculpted_relief", 210- "stencil", 211- } 212- if mode not in valid_modes: 213- errors.append(f"application_mode must be one of {sorted(valid_modes)}") 214- 215- limits = CONFIG["safety"] 216- 217- emboss = payload.get("emboss") 218- if emboss is not None: 219- if not isinstance(emboss, dict): 220- errors.append("emboss must be an object or null") 221- else: 222: validate_number( 223- errors, 224- emboss.get("height"), 225- "emboss.height", 226- 0, 227: limits["maximum_relief_height_mm"], 228- ) 229: validate_number( 230- errors, 231- emboss.get("minimum_feature"), 232- "emboss.minimum_feature", 233- limits["minimum_raised_feature_mm"], 234- ) 235- 236- engraving = payload.get("engraving") 237- if engraving is not None: 238- if not isinstance(engraving, dict): 239- errors.append("engraving must be an object or null") 240- else: 241: validate_number( 242- errors, 243- engraving.get("depth"), 244- "engraving.depth", 245- 0, 246- limits["maximum_engraving_depth_mm"], 247- ) 248: validate_number( 249- errors, 250- engraving.get("minimum_feature"), 251- "engraving.minimum_feature", 252- limits["minimum_recessed_feature_mm"], 253- ) 254- 255: relief = payload.get("relief") 256: if relief is not None: 257: if not isinstance(relief, dict): 258: errors.append("relief must be an object or null") 259- else: 260: validate_number(errors, relief.get("strength"), "relief.strength", 0, 1) 261- 262- protected = payload.get("protected_zones") 263- if CONFIG["safety"]["require_protected_zone_check"]: 264- if protected is None: 265- errors.append("protected_zones must be supplied, even when empty") 266- elif not isinstance(protected, list): 267- errors.append("protected_zones must be an array") 268- 269- decoration = payload.get("decoration_zones") 270- if decoration is not None and not isinstance(decoration, list): 271- errors.append("decoration_zones must be an array") 272- -- 313- status="processing", 314- progress=10, 315- stage="validating_request", 316- ) 317- payload = job["request"] 318- 319: errors = validate_geometry_request(payload) 320- if errors: 321- update_job( 322- job_id, 323- status="blocked", 324- progress=100, 325- stage="request_rejected", 326- errors=errors, 327- manufacturing_ready=False, 328- simulation=False, 329- ) 330- return 331- -- 352- status="blocked", 353- progress=100, 354- stage="mesh_engine_not_installed", 355- result={ 356- "real_service": True, 357- "simulation": False, 358: "request_validated": True, 359: "geometry_generated": False, 360- "manufacturing_ready": False, 361: "export_allowed": False, 362- "next_required_stage": "adaptive_surface_wrap_v0.2", 363- "message": ( 364: "The native engine accepted and validated this job. " 365: "Adaptive mesh wrapping is not enabled in v0.1, so export remains blocked." 366- ), 367- }, 368- manufacturing_ready=False, 369- simulation=False, 370- ) 371- except Exception as exc: 372- update_job( 373- job_id, 374- status="failed", 375- progress=100, 376- stage="engine_error", 377- error=str(exc), -- 422- self.send_response(status) 423- self.send_header("Content-Type", "application/json; charset=utf-8") 424- self.send_header("Content-Length", str(len(encoded))) 425- self.send_header("Cache-Control", "no-store") 426- self.send_header("Access-Control-Allow-Origin", "http://127.0.0.1:4174") 427- self.send_header("Access-Control-Allow-Headers", "Content-Type") 428: self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") 429- self.end_headers() 430- self.wfile.write(encoded) 431- 432- def read_body(self) -> dict[str, Any]: 433- content_length = int(self.headers.get("Content-Length", "0")) 434- maximum = int(CONFIG["max_request_bytes"]) 435- if content_length <= 0: 436- raise ValueError("Request body is required") 437- if content_length > maximum: 438- raise ValueError(f"Request exceeds maximum size of {maximum} bytes") 439- 440- raw = self.rfile.read(content_length) -- 443- raise ValueError("Request body must be a JSON object") 444- return value 445- 446- def do_OPTIONS(self) -> None: 447- self.send_json(200, {"ok": True}) 448- 449: def do_GET(self) -> None: 450- path = urlparse(self.path).path 451- 452: if path == "/health": 453- self.send_json( 454- 200, 455- { 456- "ok": True, 457- "status": "ready", 458- "service": CONFIG["service_name"], 459- "service_version": CONFIG["service_version"], 460- "real_service": True, 461- "simulation": False, 462: "manufacturing_export_blocked": True, 463- "time": utc_now(), 464- }, 465- ) 466- return 467- 468- if path == "/capabilities": 469- self.send_json(200, {"ok": True, **capabilities()}) 470- return 471- 472- if path.startswith("/jobs/"): 473- job_id = path.split("/", 2)[2] 474- try: -- 478- except ValueError as exc: 479- self.send_json(400, {"ok": False, "error": str(exc)}) 480- return 481- 482- self.send_json(404, {"ok": False, "error": "Not found"}) 483- 484: def do_POST(self) -> None: 485- path = urlparse(self.path).path 486- 487- try: 488- payload = self.read_body() 489- except (ValueError, json.JSONDecodeError) as exc: 490- self.send_json(400, {"ok": False, "error": str(exc)}) 491- return 492- 493: if path == "/validate": 494: errors = validate_geometry_request(payload) 495- self.send_json( 496- 200, 497- { 498- "ok": not errors, 499- "valid": not errors, 500- "errors": errors, 501- "real_service": True, 502- "simulation": False, 503- "manufacturing_ready": False, 504- }, 505- ) 506- return