refactor: rebrand project to HartOMat

This commit is contained in:
2026-04-06 12:45:47 +02:00
parent fa7093307a
commit b795f0e6d6
95 changed files with 608 additions and 497 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# Database Migration Agent
You are a specialist for Alembic migrations in the Schaeffler Automat project. You create, verify, and apply database migrations safely.
You are a specialist for Alembic migrations in the HartOMat project. You create, verify, and apply database migrations safely.
## Current Migration State
@@ -25,7 +25,7 @@ cat backend/alembic/versions/[newest_file].py
docker compose exec backend alembic upgrade head
# 5. Verify schema
docker compose exec postgres psql -U schaeffler -d schaeffler -c "\d tablename"
docker compose exec postgres psql -U hartomat -d hartomat -c "\d tablename"
```
## Pre-Apply Checklist
+6 -6
View File
@@ -1,6 +1,6 @@
# Debug Render Agent
You are a specialist for render pipeline problems in the Schaeffler Automat project. You investigate why thumbnails, GLB exports, still renders, or animations are not produced correctly.
You are a specialist for render pipeline problems in the HartOMat project. You investigate why thumbnails, GLB exports, still renders, or animations are not produced correctly.
## Architecture Overview (current)
@@ -27,19 +27,19 @@ render_step_thumbnail [queue: asset_pipeline, render-worker container]
```bash
# CadFile status
docker compose exec postgres psql -U schaeffler -d schaeffler -c "
docker compose exec postgres psql -U hartomat -d hartomat -c "
SELECT id, original_name, processing_status, step_file_hash,
render_job_doc->>'state' AS job_state
FROM cad_files WHERE id = '[cad_file_id]';"
# MediaAssets for a CadFile
docker compose exec postgres psql -U schaeffler -d schaeffler -c "
docker compose exec postgres psql -U hartomat -d hartomat -c "
SELECT asset_type, storage_key, file_size_bytes, is_archived, created_at
FROM media_assets WHERE cad_file_id = '[cad_file_id]'
ORDER BY created_at DESC;"
# OrderLine render status and job document
docker compose exec postgres psql -U schaeffler -d schaeffler -c "
docker compose exec postgres psql -U hartomat -d hartomat -c "
SELECT id, render_status, render_backend_used,
render_job_doc->>'celery_task_id' AS celery_id,
render_job_doc->>'state' AS job_state,
@@ -47,7 +47,7 @@ SELECT id, render_status, render_backend_used,
FROM order_lines WHERE id = '[order_line_id]';"
# Material alias lookup
docker compose exec postgres psql -U schaeffler -d schaeffler -c "
docker compose exec postgres psql -U hartomat -d hartomat -c "
SELECT m.name AS canonical, ma.alias FROM materials m
JOIN material_aliases ma ON ma.material_id = m.id
WHERE lower(ma.alias) = lower('[material_name]');"
@@ -85,7 +85,7 @@ docker compose exec render-worker find /app/uploads/[cad_file_id]/ -name "*.stp"
docker compose exec render-worker find /app/uploads/[cad_file_id]/ -name "*.glb"
# MinIO contents (via mc alias)
docker compose exec minio mc ls local/schaeffler/[cad_file_id]/
docker compose exec minio mc ls local/hartomat/[cad_file_id]/
```
## Step 4: Test Export Scripts Directly
+3 -3
View File
@@ -1,12 +1,12 @@
# Excel Import Agent
You are a specialist for the Excel import parser in the Schaeffler Automat project. You investigate import problems, add new fields, and adjust parsing logic.
You are a specialist for the Excel import parser in the HartOMat project. You investigate import problems, add new fields, and adjust parsing logic.
## Parser Overview
**File**: `backend/app/services/excel_parser.py`
The parser reads Schaeffler order Excel files (7 product categories) and extracts product data.
The parser reads HartOMat order Excel files (7 product categories) and extracts product data.
### Header Detection
- Searches rows 04 for `"Ebene1"` in any column
@@ -46,7 +46,7 @@ for r in rows[:3]:
"
# Check material aliases seeded
docker compose exec postgres psql -U schaeffler -d schaeffler -c "
docker compose exec postgres psql -U hartomat -d hartomat -c "
SELECT m.name, ma.alias FROM materials m
JOIN material_aliases ma ON ma.material_id = m.id
LIMIT 20;"
+1 -1
View File
@@ -1,6 +1,6 @@
# Frontend Agent
You are a specialist for the React/TypeScript frontend of the Schaeffler Automat project. You implement new UI pages, components, and API bindings.
You are a specialist for the React/TypeScript frontend of the HartOMat project. You implement new UI pages, components, and API bindings.
## Tech Stack
+1 -1
View File
@@ -1,6 +1,6 @@
# Implementer Agent
You are the implementer for the Schaeffler Automat project. You read `plan.md` and execute tasks one at a time.
You are the implementer for the HartOMat project. You read `plan.md` and execute tasks one at a time.
## Your Workflow
+2 -2
View File
@@ -1,6 +1,6 @@
# Planner Agent
You are the planner for the Schaeffler Automat project. Your only job is analysis and planning — you implement **nothing**.
You are the planner for the HartOMat project. Your only job is analysis and planning — you implement **nothing**.
## Your Workflow
@@ -106,7 +106,7 @@ Key-value store in `system_settings` table. Updated via direct SQL UPDATE (SQLAl
### USD Work
- Library: `usd-core` pip → `pxr` module
- Seam/sharp: index-space primvars (`primvars:schaeffler:seamEdgeVertexPairs`)
- Seam/sharp: index-space primvars (`primvars:hartomat:seamEdgeVertexPairs`)
- Layer strategy: canonical geometry layer + override layer, flatten via `UsdUtils.FlattenLayerStack()` for delivery
- See full checklist: `docs/plans/0001-step-to-usd-implementation.md`
+3 -3
View File
@@ -1,6 +1,6 @@
# Render Pipeline Agent
You are a specialist for the render script chain in the Schaeffler Automat project. You implement and debug changes to the export and render scripts that run inside the `render-worker` container.
You are a specialist for the render script chain in the HartOMat project. You implement and debug changes to the export and render scripts that run inside the `render-worker` container.
## Pipeline Overview
@@ -93,7 +93,7 @@ material_name = mat_map_lower.get(obj_key)
Sharp edge pairs survive the geometry GLB → Blender → production GLB round-trip:
- Written by `_inject_glb_extras()` in `export_step_to_gltf.py` into `scenes[0].extras`
- Read by Blender's glTF importer as `bpy.context.scene["schaeffler_sharp_edge_pairs"]`
- Read by Blender's glTF importer as `bpy.context.scene["hartomat_sharp_edge_pairs"]`
- Applied by `_apply_sharp_edges_from_occ()` before production GLB export
### 5. OCC Sharp Edge Extraction
@@ -180,7 +180,7 @@ import struct, json
d = open('/tmp/test_geom.glb', 'rb').read()
jl = struct.unpack_from('<I', d, 12)[0]
j = json.loads(d[20:20+jl])
pairs = j.get('scenes', [{}])[0].get('extras', {}).get('schaeffler_sharp_edge_pairs', [])
pairs = j.get('scenes', [{}])[0].get('extras', {}).get('hartomat_sharp_edge_pairs', [])
print(f'{len(pairs)} sharp edge pairs in GLB extras')
if pairs: print('First pair:', pairs[0])
"
+2 -2
View File
@@ -1,6 +1,6 @@
# Review Agent
You are the reviewer for the Schaeffler Automat project. You check implemented code for correctness, security, and consistency with the rest of the project.
You are the reviewer for the HartOMat project. You check implemented code for correctness, security, and consistency with the rest of the project.
## Your Workflow
@@ -62,7 +62,7 @@ You are the reviewer for the Schaeffler Automat project. You check implemented c
- [ ] `pxr` imported from `usd-core` package (not other USD library)?
- [ ] Delivery flatten uses `UsdUtils.FlattenLayerStack()`, not `stage.Flatten()`?
- [ ] Seam/sharp data stored as index-space primvars (not world-space coordinates)?
- [ ] `schaeffler:partKey` attribute authored on all part prims?
- [ ] `hartomat:partKey` attribute authored on all part prims?
### Security
- [ ] No credentials in code
+6 -6
View File
@@ -1,6 +1,6 @@
# Tenant Audit Agent
You are a specialist for tenant isolation correctness in the Schaeffler Automat project. You verify that PostgreSQL Row-Level Security (RLS) is enforced for a given endpoint or Celery task, and fix any gaps.
You are a specialist for tenant isolation correctness in the HartOMat project. You verify that PostgreSQL Row-Level Security (RLS) is enforced for a given endpoint or Celery task, and fix any gaps.
## Current Isolation State (ROADMAP Priority 8)
@@ -51,7 +51,7 @@ Expected: `tenant_id` in `create_access_token()` payload.
### Step 3: Verify RLS policy exists for the table
```bash
docker compose exec postgres psql -U schaeffler -d schaeffler -c "
docker compose exec postgres psql -U hartomat -d hartomat -c "
SELECT schemaname, tablename, policyname, cmd, qual
FROM pg_policies
WHERE tablename = '[tablename]';"
@@ -61,16 +61,16 @@ WHERE tablename = '[tablename]';"
```bash
# Get tenant A and tenant B IDs
docker compose exec postgres psql -U schaeffler -d schaeffler -c "
docker compose exec postgres psql -U hartomat -d hartomat -c "
SELECT id, name FROM tenants LIMIT 5;"
# Count rows visible to tenant A
docker compose exec postgres psql -U schaeffler -d schaeffler -c "
docker compose exec postgres psql -U hartomat -d hartomat -c "
SET LOCAL app.current_tenant_id = '[tenant_a_id]';
SELECT COUNT(*) FROM [tablename];"
# Count total rows (bypass RLS)
docker compose exec postgres psql -U schaeffler -d schaeffler -c "
docker compose exec postgres psql -U hartomat -d hartomat -c "
SELECT COUNT(*) FROM [tablename];"
# If visible count == total count when tenant B has data → RLS not enforced
@@ -165,7 +165,7 @@ if hasattr(request.state, 'tenant_id') and request.state.tenant_id:
Verify these tables have policies:
```bash
docker compose exec postgres psql -U schaeffler -d schaeffler -c "
docker compose exec postgres psql -U hartomat -d hartomat -c "
SELECT tablename, COUNT(*) as policies
FROM pg_policies
GROUP BY tablename
+17 -17
View File
@@ -1,6 +1,6 @@
# USD Export Agent
You are a specialist for the USD pipeline in the Schaeffler Automat project. You implement and debug everything related to `export_step_to_usd.py`, `import_usd.py`, and the `pxr` authoring API.
You are a specialist for the USD pipeline in the HartOMat project. You implement and debug everything related to `export_step_to_usd.py`, `import_usd.py`, and the `pxr` authoring API.
## Architecture Decisions (all locked)
@@ -41,16 +41,16 @@ stage.Save()
# Define an Xform for a part
part_xform = UsdGeom.Xform.Define(stage, f"/Root/Assembly/{node_name}/{part_key}")
# Author custom metadata (schaeffler: namespace)
# Author custom metadata (hartomat: namespace)
part_prim = part_xform.GetPrim()
part_prim.SetCustomDataByKey("schaeffler:partKey", part_key)
part_prim.SetCustomDataByKey("schaeffler:sourceName", source_name)
part_prim.SetCustomDataByKey("schaeffler:sourceColor", hex_color)
part_prim.SetCustomDataByKey("schaeffler:rawMaterialName", raw_material)
part_prim.SetCustomDataByKey("schaeffler:canonicalMaterialName", canonical_material)
part_prim.SetCustomDataByKey("schaeffler:cadFileId", str(cad_file_id))
part_prim.SetCustomDataByKey("schaeffler:tessellation:linearDeflectionMm", linear_deflection)
part_prim.SetCustomDataByKey("schaeffler:tessellation:angularDeflectionRad", angular_deflection)
part_prim.SetCustomDataByKey("hartomat:partKey", part_key)
part_prim.SetCustomDataByKey("hartomat:sourceName", source_name)
part_prim.SetCustomDataByKey("hartomat:sourceColor", hex_color)
part_prim.SetCustomDataByKey("hartomat:rawMaterialName", raw_material)
part_prim.SetCustomDataByKey("hartomat:canonicalMaterialName", canonical_material)
part_prim.SetCustomDataByKey("hartomat:cadFileId", str(cad_file_id))
part_prim.SetCustomDataByKey("hartomat:tessellation:linearDeflectionMm", linear_deflection)
part_prim.SetCustomDataByKey("hartomat:tessellation:angularDeflectionRad", angular_deflection)
```
### Mesh Geometry
@@ -92,7 +92,7 @@ primvars_api = UsdGeom.PrimvarsAPI(mesh)
sharp_pairs = [(vi0, vi1), (vi2, vi3), ...] # local mesh vertex indices
sharp_array = Vt.Vec2iArray([Gf.Vec2i(a, b) for a, b in sharp_pairs])
pv_sharp = primvars_api.CreatePrimvar(
"schaeffler:sharpEdgeVertexPairs",
"hartomat:sharpEdgeVertexPairs",
Sdf.ValueTypeNames.Int2Array,
UsdGeom.Tokens.constant,
)
@@ -102,7 +102,7 @@ pv_sharp.Set(sharp_array)
seam_pairs = [(vi0, vi1), ...]
seam_array = Vt.Vec2iArray([Gf.Vec2i(a, b) for a, b in seam_pairs])
pv_seam = primvars_api.CreatePrimvar(
"schaeffler:seamEdgeVertexPairs",
"hartomat:seamEdgeVertexPairs",
Sdf.ValueTypeNames.Int2Array,
UsdGeom.Tokens.constant,
)
@@ -139,7 +139,7 @@ override_stage.GetRootLayer().subLayerPaths.append("/path/to/overrides.usd")
# Author override opinions
with Usd.EditContext(override_stage, override_stage.GetRootLayer()):
prim = override_stage.GetPrimAtPath("/Root/Assembly/Node/ring_outer")
prim.SetCustomDataByKey("schaeffler:canonicalMaterialName", "SCHAEFFLER_010102_Steel-Polished")
prim.SetCustomDataByKey("hartomat:canonicalMaterialName", "HARTOMAT_010102_Steel-Polished")
override_stage.Save()
@@ -159,8 +159,8 @@ for obj in bpy.context.scene.objects:
if obj.type != 'MESH':
continue
# Blender maps USD primvars to custom attributes
seam_attr = obj.data.attributes.get("schaeffler:seamEdgeVertexPairs")
sharp_attr = obj.data.attributes.get("schaeffler:sharpEdgeVertexPairs")
seam_attr = obj.data.attributes.get("hartomat:seamEdgeVertexPairs")
sharp_attr = obj.data.attributes.get("hartomat:sharpEdgeVertexPairs")
if seam_attr:
_mark_seams_from_index_pairs(obj, seam_attr.data)
if sharp_attr:
@@ -260,14 +260,14 @@ from pxr import Usd
stage = Usd.Stage.Open('/tmp/test.usd')
for prim in stage.Traverse():
if prim.GetTypeName() == 'Mesh':
print(prim.GetPath(), '| partKey:', prim.GetCustomDataByKey('schaeffler:partKey'))
print(prim.GetPath(), '| partKey:', prim.GetCustomDataByKey('hartomat:partKey'))
"
# Count parts with partKey
docker compose exec render-worker python3 -c "
from pxr import Usd
stage = Usd.Stage.Open('/tmp/test.usd')
parts = [p for p in stage.Traverse() if p.GetCustomDataByKey('schaeffler:partKey')]
parts = [p for p in stage.Traverse() if p.GetCustomDataByKey('hartomat:partKey')]
print(f'{len(parts)} parts with partKey')
"
```
+1 -1
View File
@@ -88,7 +88,7 @@ For each suggestion include:
Structure your final report as follows:
# Schaeffler Automat — UX & Quality Audit Report
# HartOMat — UX & Quality Audit Report
**Date**: [date]
**Overall Score**: [X/10]
+1 -1
View File
@@ -10,7 +10,7 @@
"hooks": [
{
"type": "command",
"command": "python3 /home/hartmut/Documents/Copilot/schaefflerautomat/.claude/hooks/pre_tool_use.py"
"command": "python3 /home/hartmut/Documents/Copilot/hartomat/.claude/hooks/pre_tool_use.py"
}
]
}
+3 -3
View File
@@ -1,7 +1,7 @@
# Database
POSTGRES_DB=schaeffler
POSTGRES_USER=schaeffler
POSTGRES_PASSWORD=schaeffler
POSTGRES_DB=hartomat
POSTGRES_USER=hartomat
POSTGRES_PASSWORD=hartomat
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
+4 -4
View File
@@ -1,12 +1,12 @@
{
"mcpServers": {
"schaeffler": {
"hartomat": {
"command": "uv",
"args": ["run", "--with", "mcp[cli]", "--with", "psycopg2-binary", "--with", "httpx", "python", "schaeffler_mcp_server.py"],
"args": ["run", "--with", "mcp[cli]", "--with", "psycopg2-binary", "--with", "httpx", "python", "hartomat_mcp_server.py"],
"env": {
"DATABASE_URL": "postgresql://schaeffler:schaeffler@localhost:5432/schaeffler",
"DATABASE_URL": "postgresql://hartomat:hartomat@localhost:5432/hartomat",
"API_URL": "http://localhost:8888",
"API_EMAIL": "admin@schaeffler.com",
"API_EMAIL": "admin@hartomat.com",
"API_PASSWORD": "Admin1234!"
}
}
+5 -5
View File
@@ -1,8 +1,8 @@
# Schaeffler Automat
# HartOMat
## Ziel
Automatisiertes Render-System für Schaeffler-Produktbilder. Kunden (intern) laden Excel-Auftragslisten hoch, das System extrahiert Produktdaten, verknüpft STEP-CAD-Dateien, rendert Thumbnails und Animationen über Blender (Cycles/EEVEE), und liefert fertige PNG/MP4-Ausgaben.
Automatisiertes Render-System für HartOMat-Produktbilder. Kunden (intern) laden Excel-Auftragslisten hoch, das System extrahiert Produktdaten, verknüpft STEP-CAD-Dateien, rendert Thumbnails und Animationen über Blender (Cycles/EEVEE), und liefert fertige PNG/MP4-Ausgaben.
## Tech Stack
@@ -48,14 +48,14 @@ docker compose up -d --build backend worker render-worker beat
## Standard-Zugangsdaten (Entwicklung)
- **Admin**: admin@schaeffler.com / Admin1234!
- **Admin**: admin@hartomat.com / Admin1234!
- **Backend API**: http://localhost:8888/docs
- **Frontend**: http://localhost:5173
## Projektstruktur
```
schaefflerautomat/
hartomat/
├── backend/
│ ├── app/
│ │ ├── api/routers/ # FastAPI Router (admin, cad, orders, products, ...)
@@ -138,7 +138,7 @@ docker compose exec backend alembic current
## Material-Alias-System
- Materialien werden per STEP-Part-Name auf Schaeffler-Bibliotheksmaterialien (`SCHAEFFLER_...`) gemappt
- Materialien werden per STEP-Part-Name auf HartOMat-Bibliotheksmaterialien (`HARTOMAT_...`) gemappt
- Lookup-Reihenfolge: **Alias-Tabelle zuerst**, dann exakter `Material.name`-Match, dann Pass-through
- Alias-Seeding: Admin → "Seed Aliases" oder via `POST /api/materials/seed-aliases`
+4 -4
View File
@@ -1,4 +1,4 @@
# Projekt-Learnings — Schaeffler Automat
# Projekt-Learnings — HartOMat
## Format
**Datum | Kategorie | Problem → Lösung**
@@ -52,7 +52,7 @@ STEP in mm, Blender in m → 50mm-Lager erscheint 50m breit. `_scale_mm_to_m(par
**Lösung:** Compositing entfernt; bg_color via FFmpeg (`-f lavfi -i color=...` + overlay). Pflicht: `try: main() except SystemExit: raise except Exception: traceback; sys.exit(1)` in allen Blender-Scripts.
### 2026-02-05 | Material-System | Material-Alias-Lookup-Reihenfolge falsch
`Steel--Stahl` war sowohl `Material.name` als auch Alias für `SCHAEFFLER_010101_Steel-Bare`. Lookup fand zuerst den Namen → Blender konnte ihn nicht in der Library finden.
`Steel--Stahl` war sowohl `Material.name` als auch Alias für `HARTOMAT_010101_Steel-Bare`. Lookup fand zuerst den Namen → Blender konnte ihn nicht in der Library finden.
**Lösung:** `material_service.py`: **Aliases zuerst**, dann exakter Name, dann Pass-through.
### 2026-02-10 | Render-Pipeline | Blender-Template überschreibt HDRI/World
@@ -282,7 +282,7 @@ Absolute Pfade in `storage_key` → nach Volume-Umzug 398 Assets nicht erreichba
**Lösung:** `api.get(..., { responseType: 'blob' })``URL.createObjectURL()` + programmatischer `<a>.click()`. Gilt für alle geschützten Download-Endpoints.
### 2026-03-07 | PostgreSQL RLS | SET LOCAL muss in jeder Transaktion gesetzt werden
`GRANT BYPASSRLS TO schaeffler` schlug still fehl → Admin-Endpoints bekamen 0 Zeilen.
`GRANT BYPASSRLS TO hartomat` schlug still fehl → Admin-Endpoints bekamen 0 Zeilen.
**Lösung:** `await db.execute(text("SET LOCAL app.current_tenant_id = 'bypass'"))` direkt vor jede RLS-geschützte Query in internen/Admin-Endpoints.
### 2026-03-07 | trimesh | GLB-Export-Scale: STL in mm → Three.js in Metern
@@ -378,7 +378,7 @@ Bei Mesh-Name-Mismatch-Bugs: GLB-Datei direkt parsen statt im Browser debuggen.
```python
import urllib.request, json, struct
# Login
data = json.dumps({'email':'admin@schaeffler.com','password':'Admin1234!'}).encode()
data = json.dumps({'email':'admin@hartomat.com','password':'Admin1234!'}).encode()
req = urllib.request.Request('http://localhost:8888/api/auth/login', data=data, headers={'Content-Type':'application/json'})
token = json.load(urllib.request.urlopen(req))['access_token']
# Media-Assets für CAD-File
+38 -38
View File
@@ -1,4 +1,4 @@
"""Generate material_library.blend with all 35 Schaeffler standard materials.
"""Generate material_library.blend with all 35 HartOMat standard materials.
Run with: blender --background --python generate_blend.py
"""
@@ -9,51 +9,51 @@ import os
# Format: (R, G, B, A) linear color, metallic, roughness
MATERIALS = [
# --- 01 Metals ---
("SCHAEFFLER_010101_Steel-Bare", (0.55, 0.56, 0.58, 1.0), 1.0, 0.35),
("SCHAEFFLER_010102_Steel-Burnished", (0.15, 0.12, 0.10, 1.0), 1.0, 0.25),
("SCHAEFFLER_010103_Steel-Galvanized", (0.65, 0.67, 0.70, 1.0), 1.0, 0.40),
("SCHAEFFLER_010104_Steel-Casted", (0.35, 0.33, 0.31, 1.0), 1.0, 0.60),
("SCHAEFFLER_010105_Steel-Plate", (0.50, 0.51, 0.53, 1.0), 1.0, 0.30),
("SCHAEFFLER_010201_Niro", (0.70, 0.72, 0.74, 1.0), 1.0, 0.20),
("SCHAEFFLER_010301_Tin", (0.75, 0.75, 0.73, 1.0), 1.0, 0.30),
("SCHAEFFLER_010401_Aluminium", (0.80, 0.80, 0.82, 1.0), 1.0, 0.25),
("SCHAEFFLER_010501_Brass", (0.70, 0.55, 0.20, 1.0), 1.0, 0.25),
("SCHAEFFLER_010601_Bronze", (0.55, 0.35, 0.15, 1.0), 1.0, 0.30),
("HARTOMAT_010101_Steel-Bare", (0.55, 0.56, 0.58, 1.0), 1.0, 0.35),
("HARTOMAT_010102_Steel-Burnished", (0.15, 0.12, 0.10, 1.0), 1.0, 0.25),
("HARTOMAT_010103_Steel-Galvanized", (0.65, 0.67, 0.70, 1.0), 1.0, 0.40),
("HARTOMAT_010104_Steel-Casted", (0.35, 0.33, 0.31, 1.0), 1.0, 0.60),
("HARTOMAT_010105_Steel-Plate", (0.50, 0.51, 0.53, 1.0), 1.0, 0.30),
("HARTOMAT_010201_Niro", (0.70, 0.72, 0.74, 1.0), 1.0, 0.20),
("HARTOMAT_010301_Tin", (0.75, 0.75, 0.73, 1.0), 1.0, 0.30),
("HARTOMAT_010401_Aluminium", (0.80, 0.80, 0.82, 1.0), 1.0, 0.25),
("HARTOMAT_010501_Brass", (0.70, 0.55, 0.20, 1.0), 1.0, 0.25),
("HARTOMAT_010601_Bronze", (0.55, 0.35, 0.15, 1.0), 1.0, 0.30),
# --- 02 Coatings ---
("SCHAEFFLER_020101_Durotect-Blue", (0.15, 0.25, 0.50, 1.0), 0.8, 0.20),
("SCHAEFFLER_020102_Durotect-Black", (0.05, 0.05, 0.06, 1.0), 0.8, 0.15),
("SCHAEFFLER_020201_Coat-Black", (0.03, 0.03, 0.03, 1.0), 0.6, 0.10),
("HARTOMAT_020101_Durotect-Blue", (0.15, 0.25, 0.50, 1.0), 0.8, 0.20),
("HARTOMAT_020102_Durotect-Black", (0.05, 0.05, 0.06, 1.0), 0.8, 0.15),
("HARTOMAT_020201_Coat-Black", (0.03, 0.03, 0.03, 1.0), 0.6, 0.10),
# --- 03 Non-metals ---
("SCHAEFFLER_030101_Elastomer-Brown", (0.30, 0.18, 0.08, 1.0), 0.0, 0.55),
("SCHAEFFLER_030102_Elastomer-Green", (0.10, 0.30, 0.10, 1.0), 0.0, 0.55),
("SCHAEFFLER_030103_Elastomer-Black", (0.04, 0.04, 0.04, 1.0), 0.0, 0.55),
("SCHAEFFLER_030201_Plastic-Brown", (0.35, 0.22, 0.10, 1.0), 0.0, 0.40),
("SCHAEFFLER_030202_Plastic-Green", (0.08, 0.35, 0.12, 1.0), 0.0, 0.40),
("SCHAEFFLER_030203_Plastic-Black", (0.02, 0.02, 0.02, 1.0), 0.0, 0.40),
("SCHAEFFLER_030204_Plastic-Blue", (0.10, 0.20, 0.50, 1.0), 0.0, 0.40),
("SCHAEFFLER_030205_Plastic-White", (0.85, 0.85, 0.85, 1.0), 0.0, 0.40),
("SCHAEFFLER_030301_Plastic-Clear", (0.90, 0.90, 0.92, 1.0), 0.0, 0.10), # + transmission
("SCHAEFFLER_030302_Plastic-Translucent-White", (0.80, 0.80, 0.82, 1.0), 0.0, 0.20), # + transmission
("SCHAEFFLER_030401_TPU-Blue", (0.12, 0.25, 0.55, 1.0), 0.0, 0.45),
("SCHAEFFLER_030501_Ceramic-Black", (0.03, 0.03, 0.04, 1.0), 0.0, 0.15),
("HARTOMAT_030101_Elastomer-Brown", (0.30, 0.18, 0.08, 1.0), 0.0, 0.55),
("HARTOMAT_030102_Elastomer-Green", (0.10, 0.30, 0.10, 1.0), 0.0, 0.55),
("HARTOMAT_030103_Elastomer-Black", (0.04, 0.04, 0.04, 1.0), 0.0, 0.55),
("HARTOMAT_030201_Plastic-Brown", (0.35, 0.22, 0.10, 1.0), 0.0, 0.40),
("HARTOMAT_030202_Plastic-Green", (0.08, 0.35, 0.12, 1.0), 0.0, 0.40),
("HARTOMAT_030203_Plastic-Black", (0.02, 0.02, 0.02, 1.0), 0.0, 0.40),
("HARTOMAT_030204_Plastic-Blue", (0.10, 0.20, 0.50, 1.0), 0.0, 0.40),
("HARTOMAT_030205_Plastic-White", (0.85, 0.85, 0.85, 1.0), 0.0, 0.40),
("HARTOMAT_030301_Plastic-Clear", (0.90, 0.90, 0.92, 1.0), 0.0, 0.10), # + transmission
("HARTOMAT_030302_Plastic-Translucent-White", (0.80, 0.80, 0.82, 1.0), 0.0, 0.20), # + transmission
("HARTOMAT_030401_TPU-Blue", (0.12, 0.25, 0.55, 1.0), 0.0, 0.45),
("HARTOMAT_030501_Ceramic-Black", (0.03, 0.03, 0.04, 1.0), 0.0, 0.15),
# --- 04 Compounds ---
("SCHAEFFLER_040101_E40", (0.25, 0.22, 0.18, 1.0), 0.0, 0.50),
("SCHAEFFLER_040102_E50", (0.28, 0.25, 0.20, 1.0), 0.0, 0.50),
("SCHAEFFLER_040201_Elgoglide", (0.20, 0.22, 0.25, 1.0), 0.0, 0.35),
("SCHAEFFLER_040202_Elgotex", (0.05, 0.05, 0.06, 1.0), 0.0, 0.35),
("SCHAEFFLER_040301_PTFE-Niro-Compound", (0.60, 0.62, 0.65, 1.0), 0.3, 0.25),
("SCHAEFFLER_040302_PTFE-Foil", (0.85, 0.85, 0.82, 1.0), 0.0, 0.15),
("SCHAEFFLER_040303_PTFE-Compound-Black", (0.04, 0.04, 0.05, 1.0), 0.0, 0.30),
("SCHAEFFLER_040304_PTFE-Compound-Orange", (0.70, 0.35, 0.08, 1.0), 0.0, 0.30),
("SCHAEFFLER_040305_GFK-PTFE-Compound", (0.08, 0.10, 0.08, 1.0), 0.0, 0.45),
("HARTOMAT_040101_E40", (0.25, 0.22, 0.18, 1.0), 0.0, 0.50),
("HARTOMAT_040102_E50", (0.28, 0.25, 0.20, 1.0), 0.0, 0.50),
("HARTOMAT_040201_Elgoglide", (0.20, 0.22, 0.25, 1.0), 0.0, 0.35),
("HARTOMAT_040202_Elgotex", (0.05, 0.05, 0.06, 1.0), 0.0, 0.35),
("HARTOMAT_040301_PTFE-Niro-Compound", (0.60, 0.62, 0.65, 1.0), 0.3, 0.25),
("HARTOMAT_040302_PTFE-Foil", (0.85, 0.85, 0.82, 1.0), 0.0, 0.15),
("HARTOMAT_040303_PTFE-Compound-Black", (0.04, 0.04, 0.05, 1.0), 0.0, 0.30),
("HARTOMAT_040304_PTFE-Compound-Orange", (0.70, 0.35, 0.08, 1.0), 0.0, 0.30),
("HARTOMAT_040305_GFK-PTFE-Compound", (0.08, 0.10, 0.08, 1.0), 0.0, 0.45),
# --- 05 Misc ---
("SCHAEFFLER_059999_FailedMaterial", (1.00, 0.00, 0.50, 1.0), 0.0, 0.50),
("HARTOMAT_059999_FailedMaterial", (1.00, 0.00, 0.50, 1.0), 0.0, 0.50),
]
# Translucent materials that need transmission
TRANSLUCENT = {
"SCHAEFFLER_030301_Plastic-Clear": 0.9,
"SCHAEFFLER_030302_Plastic-Translucent-White": 0.5,
"HARTOMAT_030301_Plastic-Clear": 0.9,
"HARTOMAT_030302_Plastic-Translucent-White": 0.5,
}
+10 -10
View File
@@ -1,4 +1,4 @@
# Schaeffler Automat — Master Roadmap
# HartOMat — Master Roadmap
> **Consolidated:** 2026-03-11
> **Branch:** `refactor/v2`
@@ -53,7 +53,7 @@ Verified against the repository on `2026-03-13`.
This roadmap now treats the USD refactor as an implementation workstream, not as a blocked strategic idea.
The key architectural clarification from [docs/rfcs/0001-step-to-usd-workflow.md](/home/hartmut/Documents/Copilot/schaefflerautomat/docs/rfcs/0001-step-to-usd-workflow.md#L139) is:
The key architectural clarification from [docs/rfcs/0001-step-to-usd-workflow.md](/home/hartmut/Documents/Copilot/hartomat/docs/rfcs/0001-step-to-usd-workflow.md#L139) is:
- USD becomes the canonical persisted scene asset
- the browser does not need to render USD directly
@@ -112,7 +112,7 @@ This priority combines dead-code deletion and task decomposition because both ar
**Status:** Not started in code. Architecture decisions are documented, but repo work has not begun.
**Milestones:**
- M1: `export_step_to_usd.py` produces valid USD with part hierarchy and `schaeffler:partKey` on every prim
- M1: `export_step_to_usd.py` produces valid USD with part hierarchy and `hartomat:partKey` on every prim
- M2: `usd_master` MediaAsset type exists in DB and is stored after each export
- M3: `GET /api/cad/{id}/scene-manifest` returns partKey list with effective assignments
- M4: `PUT /api/cad/{id}/part-materials` accepts `{partKey → materialName}` map and persists it
@@ -139,7 +139,7 @@ This priority combines dead-code deletion and task decomposition because both ar
- None blocking at the architecture level. The roadmap decisions for `usd-core` and index-space seam/sharp primvars are already captured in `docs/plans/0001-step-to-usd-implementation.md`.
**Acceptance gates:**
- `python3 export_step_to_usd.py --step_path 81113-l_cut.stp` → valid `.usd` file, 25 part prims, each has `schaeffler:partKey` attribute
- `python3 export_step_to_usd.py --step_path 81113-l_cut.stp` → valid `.usd` file, 25 part prims, each has `hartomat:partKey` attribute
- `GET /api/cad/{id}/scene-manifest` returns `parts[]` array with `part_key`, `source_name`, `effective_material`, `is_unassigned`
- Click part in ThreeDViewer → assign material → reload page → material still assigned (persisted via `partKey`, not mesh name)
- CAD file with mismatched Excel names: UI shows `unmatched_source_rows` count > 0 and unassigned parts highlighted
@@ -226,7 +226,7 @@ This priority combines dead-code deletion and task decomposition because both ar
The current USD render path matches 0/25 parts for material assignment because Blender has no way to resolve canonical material names from the imported USD prims. This milestone embeds that metadata directly into the USD file.
- Pass resolved `material_map` to `export_step_to_usd.py` via `--material_map` CLI arg (JSON string)
- Write `schaeffler:canonicalMaterialName` as a STRING primvar on each Mesh prim during USD export
- Write `hartomat:canonicalMaterialName` as a STRING primvar on each Mesh prim during USD export
- `import_usd.py` reads the primvar after import and performs direct material lookup (no name-matching heuristics)
- Acceptance: Blender log shows `25/25 parts matched` for material assignment from USD
@@ -244,20 +244,20 @@ Currently `generate_usd_master_task` does not resolve the material map before pa
- `generate_usd_master_task` must resolve `material_map` via `material_service.resolve_material_map()` and pass to subprocess
- This makes the USD file self-contained: canonical material names baked into the asset
- Acceptance: USD file inspected via `pxr` shows `schaeffler:canonicalMaterialName` on every mesh prim
- Acceptance: USD file inspected via `pxr` shows `hartomat:canonicalMaterialName` on every mesh prim
**File targets:**
| Action | Path |
|---|---|
| CREATE | `render-worker/scripts/export_step_to_usd.py` — STEP→USD exporter (seam/sharp payload on mesh prims) |
| CREATE | `render-worker/scripts/import_usd.py` — Blender USD import helper: reads `primvars:schaeffler:seamEdgeVertexPairs`, marks seam+sharp |
| CREATE | `render-worker/scripts/import_usd.py` — Blender USD import helper: reads `primvars:hartomat:seamEdgeVertexPairs`, marks seam+sharp |
| MODIFY | `render-worker/scripts/blender_render.py` — accept `--usd_path` flag alongside `--glb_path` |
| MODIFY | `backend/app/services/render_blender.py` — pass `usd_master` asset path when available |
| MODIFY | `backend/app/domains/pipeline/tasks/export_glb.py` — retire `generate_gltf_production_task` once USD path validated |
| KEEP (compat) | `render-worker/scripts/export_gltf.py` — retained as fallback until USD path confirmed stable |
| MODIFY (M5) | `render-worker/scripts/export_step_to_usd.py` — accept `--material_map` CLI arg, write `schaeffler:canonicalMaterialName` primvar on each Mesh prim |
| MODIFY (M5) | `render-worker/scripts/import_usd.py` — read `schaeffler:canonicalMaterialName` primvar after import, use for direct material lookup |
| MODIFY (M5) | `render-worker/scripts/export_step_to_usd.py` — accept `--material_map` CLI arg, write `hartomat:canonicalMaterialName` primvar on each Mesh prim |
| MODIFY (M5) | `render-worker/scripts/import_usd.py` — read `hartomat:canonicalMaterialName` primvar after import, use for direct material lookup |
| MODIFY (M6) | `render-worker/scripts/export_step_to_usd.py` — fix `_extract_mesh()` to strip shape Location; accumulate parent transforms in `_traverse_xcaf` |
| MODIFY (M7) | `backend/app/domains/pipeline/tasks/export_glb.py` (or USD task) — call `resolve_material_map()` and pass result to export subprocess |
@@ -268,7 +268,7 @@ Currently `generate_usd_master_task` does not resolve the material map before pa
- Turntable MP4 plays without texture or material pop artifacts
- (M5) Blender log shows `[USD_IMPORT] 25/25 parts matched` for material assignment (not 0/25)
- (M6) USD render geometry matches STEP import geometry side-by-side for multi-level assemblies (no displaced/rotated parts)
- (M7) `python3 -c "from pxr import Usd; stage=Usd.Stage.Open('usd_master.usd'); [print(p.GetAttribute('primvars:schaeffler:canonicalMaterialName').Get()) for p in stage.Traverse()]"` → prints canonical material name for every mesh prim
- (M7) `python3 -c "from pxr import Usd; stage=Usd.Stage.Open('usd_master.usd'); [print(p.GetAttribute('primvars:hartomat:canonicalMaterialName').Get()) for p in stage.Traverse()]"` → prints canonical material name for every mesh prim
### Priority 6 — Admin and Product Surface Simplification
+1 -1
View File
@@ -2,7 +2,7 @@
script_location = alembic
prepend_sys_path = .
version_path_separator = os
sqlalchemy.url = postgresql://schaeffler:schaeffler@localhost:5432/schaeffler
sqlalchemy.url = postgresql://hartomat:hartomat@localhost:5432/hartomat
[post_write_hooks]
@@ -1,4 +1,4 @@
"""Schaeffler standard materials — add schaeffler_code column and seed 35 materials
"""HartOMat standard materials — add hartomat_code column and seed 35 materials
Revision ID: 019
Revises: 018
@@ -16,23 +16,23 @@ depends_on = None
def upgrade() -> None:
op.add_column("materials", sa.Column("schaeffler_code", sa.Integer(), nullable=True))
op.add_column("materials", sa.Column("hartomat_code", sa.Integer(), nullable=True))
from app.data.schaeffler_materials import SCHAEFFLER_MATERIALS
from app.data.hartomat_materials import HARTOMAT_MATERIALS
conn = op.get_bind()
now = datetime.utcnow().isoformat()
for mat in SCHAEFFLER_MATERIALS:
for mat in HARTOMAT_MATERIALS:
desc = mat["description"].replace("'", "''")
name = mat["name"].replace("'", "''")
conn.execute(sa.text(
f"INSERT INTO materials (id, name, description, source, schaeffler_code, created_at, updated_at) "
f"INSERT INTO materials (id, name, description, source, hartomat_code, created_at, updated_at) "
f"VALUES ('{uuid.uuid4()}', '{name}', '{desc}', '{mat['source']}', "
f"{mat['schaeffler_code']}, '{now}', '{now}') "
f"{mat['hartomat_code']}, '{now}', '{now}') "
f"ON CONFLICT (name) DO NOTHING"
))
def downgrade() -> None:
op.execute("DELETE FROM materials WHERE source = 'schaeffler_standard'")
op.drop_column("materials", "schaeffler_code")
op.execute("DELETE FROM materials WHERE source = 'hartomat_standard'")
op.drop_column("materials", "hartomat_code")
+1 -1
View File
@@ -28,7 +28,7 @@ def upgrade():
# Seed default tenant — all existing data will be assigned to this tenant
op.execute("""
INSERT INTO tenants (name, slug, is_active)
VALUES ('Schaeffler', 'schaeffler', true)
VALUES ('HartOMat', 'hartomat', true)
""")
+2 -2
View File
@@ -74,10 +74,10 @@ def upgrade():
),
)
# 2. Backfill with the default 'schaeffler' tenant
# 2. Backfill with the default 'hartomat' tenant
op.execute(
f"UPDATE {table} "
"SET tenant_id = (SELECT id FROM tenants WHERE slug = 'schaeffler')"
"SET tenant_id = (SELECT id FROM tenants WHERE slug = 'hartomat')"
)
# 3. Make NOT NULL now that every row has a value
@@ -20,7 +20,7 @@ _DEFAULT_CONFIG = """{
"max_concurrent_renders": 3,
"render_engines_allowed": ["cycles", "eevee"],
"max_order_size": 500,
"fallback_material": "SCHAEFFLER_059999_FailedMaterial",
"fallback_material": "HARTOMAT_059999_FailedMaterial",
"notifications_enabled": true,
"invoice_prefix": "INV"
}"""
@@ -0,0 +1,106 @@
"""Backfill persisted Schaeffler branding to HartOMat.
Revision ID: 063
Revises: 062
"""
from alembic import op
import sqlalchemy as sa
revision = "063"
down_revision = "062"
branch_labels = None
depends_on = None
def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
material_columns = {column["name"] for column in inspector.get_columns("materials")}
if "schaeffler_code" in material_columns and "hartomat_code" not in material_columns:
op.alter_column("materials", "schaeffler_code", new_column_name="hartomat_code")
op.execute(
"""
UPDATE materials
SET name = REPLACE(name, 'SCHAEFFLER_', 'HARTOMAT_')
WHERE name LIKE 'SCHAEFFLER_%'
"""
)
op.execute(
"""
UPDATE materials
SET source = 'hartomat_standard'
WHERE source = 'schaeffler_standard'
"""
)
op.execute(
"""
UPDATE tenants
SET name = 'HartOMat',
slug = 'hartomat'
WHERE lower(name) = 'schaeffler'
OR lower(slug) = 'schaeffler'
"""
)
op.execute(
"""
UPDATE tenants
SET tenant_config = jsonb_set(
tenant_config,
'{fallback_material}',
'\"HARTOMAT_059999_FailedMaterial\"'::jsonb,
true
)
WHERE tenant_config ? 'fallback_material'
"""
)
def downgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
material_columns = {column["name"] for column in inspector.get_columns("materials")}
op.execute(
"""
UPDATE materials
SET name = REPLACE(name, 'HARTOMAT_', 'SCHAEFFLER_')
WHERE name LIKE 'HARTOMAT_%'
"""
)
op.execute(
"""
UPDATE materials
SET source = 'schaeffler_standard'
WHERE source = 'hartomat_standard'
"""
)
op.execute(
"""
UPDATE tenants
SET name = 'Schaeffler',
slug = 'schaeffler'
WHERE lower(name) = 'hartomat'
OR lower(slug) = 'hartomat'
"""
)
op.execute(
"""
UPDATE tenants
SET tenant_config = jsonb_set(
tenant_config,
'{fallback_material}',
'\"SCHAEFFLER_059999_FailedMaterial\"'::jsonb,
true
)
WHERE tenant_config ? 'fallback_material'
"""
)
if "hartomat_code" in material_columns and "schaeffler_code" not in material_columns:
op.alter_column("materials", "hartomat_code", new_column_name="schaeffler_code")
+2 -2
View File
@@ -1014,9 +1014,9 @@ async def get_dashboard_stats(
if isinstance(entry, dict) and entry.get("material"):
all_mat_names.add(entry["material"])
# Library materials (name starts with SCHAEFFLER_)
# Library materials (name starts with HARTOMAT_)
lib_count_result = await db.execute(
select(func.count(Material.id)).where(Material.name.like("SCHAEFFLER_%"))
select(func.count(Material.id)).where(Material.name.like("HARTOMAT_%"))
)
library_material_count = lib_count_result.scalar() or 0
+1 -1
View File
@@ -89,7 +89,7 @@ async def get_material_pbr_map(db: AsyncSession = Depends(get_db)):
}
# Also index by aliases so frontend can look up by raw Excel names
# (e.g. "Steel--Stahl" → same PBR as "SCHAEFFLER_010101_Steel-Bare")
# (e.g. "Steel--Stahl" → same PBR as "HARTOMAT_010101_Steel-Bare")
# Bypass RLS — this is public data and aliases may have NULL tenant_id
if pbr_map:
await db.execute(text("SET LOCAL app.current_tenant_id = 'bypass'"))
+14 -14
View File
@@ -23,7 +23,7 @@ class MaterialOut(BaseModel):
name: str
description: str | None
source: str
schaeffler_code: int | None = None
hartomat_code: int | None = None
created_by_name: str | None = None
aliases: list[str] = []
created_at: datetime
@@ -42,7 +42,7 @@ class MaterialCreate(BaseModel):
name: str
description: str | None = None
source: str = "manual"
schaeffler_code: int | None = None
hartomat_code: int | None = None
class MaterialUpdate(BaseModel):
@@ -64,7 +64,7 @@ def _to_out(mat: Material) -> MaterialOut:
name=mat.name,
description=mat.description,
source=mat.source,
schaeffler_code=mat.schaeffler_code,
hartomat_code=mat.hartomat_code,
created_by_name=creator_name,
aliases=alias_names,
created_at=mat.created_at,
@@ -94,9 +94,9 @@ async def get_next_code(
range_end = prefix_int + 99
result = await db.execute(
select(func.max(Material.schaeffler_code)).where(
Material.schaeffler_code >= range_start,
Material.schaeffler_code <= range_end,
select(func.max(Material.hartomat_code)).where(
Material.hartomat_code >= range_start,
Material.hartomat_code <= range_end,
)
)
max_code = result.scalar_one_or_none()
@@ -113,16 +113,16 @@ async def get_next_code(
}
@router.post("/seed-schaeffler")
async def seed_schaeffler_materials(
@router.post("/seed-hartomat")
async def seed_hartomat_materials(
user: User = Depends(require_admin_or_pm),
db: AsyncSession = Depends(get_db),
):
"""Bulk-create the 35 standard Schaeffler materials. Skips existing by name."""
from app.data.schaeffler_materials import SCHAEFFLER_MATERIALS
"""Bulk-create the 35 standard HartOMat materials. Skips existing by name."""
from app.data.hartomat_materials import HARTOMAT_MATERIALS
inserted = 0
for mat_data in SCHAEFFLER_MATERIALS:
for mat_data in HARTOMAT_MATERIALS:
existing = await db.execute(
select(Material).where(Material.name == mat_data["name"])
)
@@ -132,14 +132,14 @@ async def seed_schaeffler_materials(
name=mat_data["name"],
description=mat_data["description"],
source=mat_data["source"],
schaeffler_code=mat_data["schaeffler_code"],
hartomat_code=mat_data["hartomat_code"],
created_by=user.id,
)
db.add(mat)
inserted += 1
await db.commit()
return {"inserted": inserted, "total": len(SCHAEFFLER_MATERIALS)}
return {"inserted": inserted, "total": len(HARTOMAT_MATERIALS)}
@router.post("/seed-aliases")
@@ -273,7 +273,7 @@ async def create_material(
name=body.name,
description=body.description,
source=body.source,
schaeffler_code=body.schaeffler_code,
hartomat_code=body.hartomat_code,
created_by=user.id,
)
db.add(mat)
+2 -2
View File
@@ -475,8 +475,8 @@ async def scale_workers(
compose_file = os.path.join(compose_dir, "docker-compose.yml")
# Derive project name from compose dir on host (directory name = project name).
# Inside the container the compose file is at /compose, but the host project
# dir name determines the container naming prefix (e.g. "schaefflerautomat").
compose_project = os.environ.get("COMPOSE_PROJECT_NAME", "schaefflerautomat")
# dir name determines the container naming prefix (e.g. "hartomat").
compose_project = os.environ.get("COMPOSE_PROJECT_NAME", "hartomat")
def _scale() -> subprocess.CompletedProcess:
return subprocess.run(
+3 -3
View File
@@ -4,9 +4,9 @@ from typing import Optional
class Settings(BaseSettings):
# Database
postgres_db: str = "schaeffler"
postgres_user: str = "schaeffler"
postgres_password: str = "schaeffler"
postgres_db: str = "hartomat"
postgres_user: str = "hartomat"
postgres_password: str = "hartomat"
postgres_host: str = "localhost"
postgres_port: int = 5432
+48
View File
@@ -0,0 +1,48 @@
"""HartOMat standard materials — single source of truth.
Naming convention: HARTOMAT_[TypeCode(2)][SubType(2)][Consecutive(2)]_[Name-Parts-Dashed]
Type codes: 01=Metals, 02=Coatings, 03=Non-metals, 04=Compounds, 05=Misc
"""
HARTOMAT_MATERIALS: list[dict] = [
# --- 01 Metals ---
{"name": "HARTOMAT_010101_Steel-Bare", "description": "Stahl / Stahl, glänzend / Stahl, konserviert", "hartomat_code": 10101, "source": "hartomat_standard"},
{"name": "HARTOMAT_010102_Steel-Burnished", "description": "Stahl, brüniert", "hartomat_code": 10102, "source": "hartomat_standard"},
{"name": "HARTOMAT_010103_Steel-Galvanized", "description": "Stahl, verzinkt", "hartomat_code": 10103, "source": "hartomat_standard"},
{"name": "HARTOMAT_010104_Steel-Casted", "description": "Stahl Körnung", "hartomat_code": 10104, "source": "hartomat_standard"},
{"name": "HARTOMAT_010105_Steel-Plate", "description": "Stahlblech", "hartomat_code": 10105, "source": "hartomat_standard"},
{"name": "HARTOMAT_010201_Niro", "description": "Niro", "hartomat_code": 10201, "source": "hartomat_standard"},
{"name": "HARTOMAT_010301_Tin", "description": "MU-Stahl, Zinnüberzug / MX-Stahl, Zinnüberzug", "hartomat_code": 10301, "source": "hartomat_standard"},
{"name": "HARTOMAT_010401_Aluminium", "description": "Aluminium", "hartomat_code": 10401, "source": "hartomat_standard"},
{"name": "HARTOMAT_010501_Brass", "description": "Messing", "hartomat_code": 10501, "source": "hartomat_standard"},
{"name": "HARTOMAT_010601_Bronze", "description": "MU-B, Bronze", "hartomat_code": 10601, "source": "hartomat_standard"},
# --- 02 Coatings ---
{"name": "HARTOMAT_020101_Durotect-Blue", "description": "Stahl, Durotect CMT", "hartomat_code": 20101, "source": "hartomat_standard"},
{"name": "HARTOMAT_020102_Durotect-Black", "description": "Stahl, Durotect M", "hartomat_code": 20102, "source": "hartomat_standard"},
{"name": "HARTOMAT_020201_Coat-Black", "description": "", "hartomat_code": 20201, "source": "hartomat_standard"},
# --- 03 Non-metals ---
{"name": "HARTOMAT_030101_Elastomer-Brown", "description": "Elastomer, braun", "hartomat_code": 30101, "source": "hartomat_standard"},
{"name": "HARTOMAT_030102_Elastomer-Green", "description": "Elastomer, grün", "hartomat_code": 30102, "source": "hartomat_standard"},
{"name": "HARTOMAT_030103_Elastomer-Black", "description": "Elastomer, schwarz", "hartomat_code": 30103, "source": "hartomat_standard"},
{"name": "HARTOMAT_030201_Plastic-Brown", "description": "Kunststoff, braun", "hartomat_code": 30201, "source": "hartomat_standard"},
{"name": "HARTOMAT_030202_Plastic-Green", "description": "Kunststoff, grün", "hartomat_code": 30202, "source": "hartomat_standard"},
{"name": "HARTOMAT_030203_Plastic-Black", "description": "Kunststoff, schwarz", "hartomat_code": 30203, "source": "hartomat_standard"},
{"name": "HARTOMAT_030204_Plastic-Blue", "description": "Kunststoff, blau", "hartomat_code": 30204, "source": "hartomat_standard"},
{"name": "HARTOMAT_030205_Plastic-White", "description": "Kunststoff, weiß", "hartomat_code": 30205, "source": "hartomat_standard"},
{"name": "HARTOMAT_030301_Plastic-Clear", "description": "Kunststoff, durchsichtig", "hartomat_code": 30301, "source": "hartomat_standard"},
{"name": "HARTOMAT_030302_Plastic-Translucent-White", "description": "", "hartomat_code": 30302, "source": "hartomat_standard"},
{"name": "HARTOMAT_030401_TPU-Blue", "description": "TPU, blau", "hartomat_code": 30401, "source": "hartomat_standard"},
{"name": "HARTOMAT_030501_Ceramic-Black", "description": "Keramik, schwarz", "hartomat_code": 30501, "source": "hartomat_standard"},
# --- 04 Compounds ---
{"name": "HARTOMAT_040101_E40", "description": "E40", "hartomat_code": 40101, "source": "hartomat_standard"},
{"name": "HARTOMAT_040102_E50", "description": "E50", "hartomat_code": 40102, "source": "hartomat_standard"},
{"name": "HARTOMAT_040201_Elgoglide", "description": "Elgoglide", "hartomat_code": 40201, "source": "hartomat_standard"},
{"name": "HARTOMAT_040202_Elgotex", "description": "Elgotex, schwarz", "hartomat_code": 40202, "source": "hartomat_standard"},
{"name": "HARTOMAT_040301_PTFE-Niro-Compound", "description": "PTFE-Compound, Niro-Verbund", "hartomat_code": 40301, "source": "hartomat_standard"},
{"name": "HARTOMAT_040302_PTFE-Foil", "description": "PTFE-Folie", "hartomat_code": 40302, "source": "hartomat_standard"},
{"name": "HARTOMAT_040303_PTFE-Compound-Black", "description": "PTFE-Verbund, schwarz", "hartomat_code": 40303, "source": "hartomat_standard"},
{"name": "HARTOMAT_040304_PTFE-Compound-Orange", "description": "PTFE-Verbundwerkstoff", "hartomat_code": 40304, "source": "hartomat_standard"},
{"name": "HARTOMAT_040305_GFK-PTFE-Compound", "description": "GFK+PTFE Verbundwerkstoff, schwarz / TPU, schwarz", "hartomat_code": 40305, "source": "hartomat_standard"},
# --- 05 Misc ---
{"name": "HARTOMAT_059999_FailedMaterial", "description": "", "hartomat_code": 59999, "source": "hartomat_standard"},
]
+36 -36
View File
@@ -1,9 +1,9 @@
"""Material alias seed data — derived from naming_scheme.xlsx Materialmapping sheet.
Each entry maps a SCHAEFFLER library material name to its known aliases:
Each entry maps a HARTOMAT library material name to its known aliases:
- German description (Col A from Materialmapping)
- Intermediate identifier (Col B, e.g. "Steel_black_oxided--Stahl_brueniert")
- Schaeffler code as string (e.g. "10102")
- HartOMat code as string (e.g. "10102")
- German variants (singular/plural, abbreviations, industry terms, DIN/EN standards)
- English equivalents commonly used in German engineering contexts
"""
@@ -13,7 +13,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
# --- 01 Metals ---
# =====================================================================
{
"material_name": "SCHAEFFLER_010101_Steel-Bare",
"material_name": "HARTOMAT_010101_Steel-Bare",
"aliases": [
"Stahl",
"Stahl, glänzend",
@@ -66,7 +66,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_010102_Steel-Burnished",
"material_name": "HARTOMAT_010102_Steel-Burnished",
"aliases": [
"Stahl, brüniert",
"Steel_black_oxided--Stahl_brueniert",
@@ -94,7 +94,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_010103_Steel-Galvanized",
"material_name": "HARTOMAT_010103_Steel-Galvanized",
"aliases": [
"Stahl, verzinkt",
"Steel_galvanized--Stahl_verzinkt",
@@ -130,7 +130,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_010104_Steel-Casted",
"material_name": "HARTOMAT_010104_Steel-Casted",
"aliases": [
"Stahl Körnung",
"Guss",
@@ -169,7 +169,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_010105_Steel-Plate",
"material_name": "HARTOMAT_010105_Steel-Plate",
"aliases": [
"Stahlblech",
"Steel_sheet--Stahlblech",
@@ -204,7 +204,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_010201_Niro",
"material_name": "HARTOMAT_010201_Niro",
"aliases": [
"Niro",
"Steel_stainless--Niro",
@@ -248,7 +248,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_010301_Tin",
"material_name": "HARTOMAT_010301_Tin",
"aliases": [
"Zinnüberzug",
"Tin--Zinn",
@@ -278,7 +278,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_010401_Aluminium",
"material_name": "HARTOMAT_010401_Aluminium",
"aliases": [
"Aluminium",
"Aluminium--Aluminium",
@@ -319,7 +319,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_010501_Brass",
"material_name": "HARTOMAT_010501_Brass",
"aliases": [
"Messing",
"Brass--Messing",
@@ -351,7 +351,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_010601_Bronze",
"material_name": "HARTOMAT_010601_Bronze",
"aliases": [
"MU-B; Bronze",
"Bronze",
@@ -393,7 +393,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
# --- 02 Coatings ---
# =====================================================================
{
"material_name": "SCHAEFFLER_020101_Durotect-Blue",
"material_name": "HARTOMAT_020101_Durotect-Blue",
"aliases": [
"Stahl, Durotect CMT",
"Durotect_CMT--Durotect_CMT",
@@ -414,7 +414,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_020102_Durotect-Black",
"material_name": "HARTOMAT_020102_Durotect-Black",
"aliases": [
"Stahl, Durotect M",
"Stahl; Durotect M",
@@ -435,7 +435,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_020201_Coat-Black",
"material_name": "HARTOMAT_020201_Coat-Black",
"aliases": [
"Stahl, schwarz",
"Steel_coated_black--Stahl_beschichtet_schwarz",
@@ -468,7 +468,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
# --- 03 Non-metals ---
# =====================================================================
{
"material_name": "SCHAEFFLER_030101_Elastomer-Brown",
"material_name": "HARTOMAT_030101_Elastomer-Brown",
"aliases": [
"Elastomer, braun",
"Elastomer_brown--Elastomer_braun",
@@ -493,7 +493,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_030102_Elastomer-Green",
"material_name": "HARTOMAT_030102_Elastomer-Green",
"aliases": [
"Elastomer, grün",
"Elastomer_green--Elastomer_gruen",
@@ -518,7 +518,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_030103_Elastomer-Black",
"material_name": "HARTOMAT_030103_Elastomer-Black",
"aliases": [
"Elastomer, schwarz",
"Eslastomer_black--Elastomer_schwarz",
@@ -557,7 +557,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_030201_Plastic-Brown",
"material_name": "HARTOMAT_030201_Plastic-Brown",
"aliases": [
"Kunststoff, braun",
"Plastic_brown--Kunststoff_braun",
@@ -585,7 +585,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_030202_Plastic-Green",
"material_name": "HARTOMAT_030202_Plastic-Green",
"aliases": [
"Kunststoff, grün",
"Plastic_green--Kunststoff_gruen",
@@ -612,7 +612,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_030203_Plastic-Black",
"material_name": "HARTOMAT_030203_Plastic-Black",
"aliases": [
"Kunststoff, schwarz",
"Plastic_black--Kunststoff_schwarz",
@@ -642,7 +642,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_030204_Plastic-Blue",
"material_name": "HARTOMAT_030204_Plastic-Blue",
"aliases": [
"Kunststoff, blau",
"Plastic_blue--Kunststoff_blau",
@@ -668,7 +668,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_030205_Plastic-White",
"material_name": "HARTOMAT_030205_Plastic-White",
"aliases": [
"Kunststoff, weiß",
"Plastic_white--Kunststoff_weiss",
@@ -702,7 +702,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_030301_Plastic-Clear",
"material_name": "HARTOMAT_030301_Plastic-Clear",
"aliases": [
"Kunststoff, durchsichtig",
"Plastic_clear--Kunststoff_durchsichtig",
@@ -738,7 +738,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_030302_Plastic-Translucent-White",
"material_name": "HARTOMAT_030302_Plastic-Translucent-White",
"aliases": [
"Plastic_translucent_white--Kunststoff_transluzent_weiss",
"30302",
@@ -769,7 +769,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_030401_TPU-Blue",
"material_name": "HARTOMAT_030401_TPU-Blue",
"aliases": [
"TPU, blau",
"Elastomer_blue--Elastomer_blau",
@@ -798,7 +798,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_030501_Ceramic-Black",
"material_name": "HARTOMAT_030501_Ceramic-Black",
"aliases": [
"Keramik, schwarz",
"Ceramics_black--Keramik_schwarz",
@@ -834,7 +834,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
# --- 04 Compounds ---
# =====================================================================
{
"material_name": "SCHAEFFLER_040101_E40",
"material_name": "HARTOMAT_040101_E40",
"aliases": [
"E40",
"E40--E40",
@@ -851,7 +851,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_040102_E50",
"material_name": "HARTOMAT_040102_E50",
"aliases": [
"E50",
"E50--E50",
@@ -868,7 +868,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_040201_Elgoglide",
"material_name": "HARTOMAT_040201_Elgoglide",
"aliases": [
"Elgoglide",
"Elgoglide--Elgoglide",
@@ -886,7 +886,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_040202_Elgotex",
"material_name": "HARTOMAT_040202_Elgotex",
"aliases": [
"Elgotex, schwarz",
"Elgotex--Elgotex",
@@ -907,7 +907,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_040301_PTFE-Niro-Compound",
"material_name": "HARTOMAT_040301_PTFE-Niro-Compound",
"aliases": [
"PTFE-Compound, Niro-Verbund",
"PTFE_compound_stainless_steel_composite--PTFE_Compound_Niro_Verbund",
@@ -933,7 +933,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_040302_PTFE-Foil",
"material_name": "HARTOMAT_040302_PTFE-Foil",
"aliases": [
"PTFE-Folie",
"PTFE_film--PTFE_Folie",
@@ -962,7 +962,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_040303_PTFE-Compound-Black",
"material_name": "HARTOMAT_040303_PTFE-Compound-Black",
"aliases": [
"PTFE-Verbund, schwarz",
"PTFE_compound_black--PTFE_Verbund_schwarz",
@@ -987,7 +987,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_040304_PTFE-Compound-Orange",
"material_name": "HARTOMAT_040304_PTFE-Compound-Orange",
"aliases": [
"PTFE-Verbundwerkstoff",
"PTFE_composite_material_orange--PTFE_Verbundwerkstoff_orange",
@@ -1014,7 +1014,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [
],
},
{
"material_name": "SCHAEFFLER_040305_GFK-PTFE-Compound",
"material_name": "HARTOMAT_040305_GFK-PTFE-Compound",
"aliases": [
"GFK+PTFE Verbundwerkstoff, schwarz",
"GFK_PTFE_compound--GFK_PTFE_Verbundwerkstoff",
-48
View File
@@ -1,48 +0,0 @@
"""Schaeffler standard materials — single source of truth.
Naming convention: SCHAEFFLER_[TypeCode(2)][SubType(2)][Consecutive(2)]_[Name-Parts-Dashed]
Type codes: 01=Metals, 02=Coatings, 03=Non-metals, 04=Compounds, 05=Misc
"""
SCHAEFFLER_MATERIALS: list[dict] = [
# --- 01 Metals ---
{"name": "SCHAEFFLER_010101_Steel-Bare", "description": "Stahl / Stahl, glänzend / Stahl, konserviert", "schaeffler_code": 10101, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_010102_Steel-Burnished", "description": "Stahl, brüniert", "schaeffler_code": 10102, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_010103_Steel-Galvanized", "description": "Stahl, verzinkt", "schaeffler_code": 10103, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_010104_Steel-Casted", "description": "Stahl Körnung", "schaeffler_code": 10104, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_010105_Steel-Plate", "description": "Stahlblech", "schaeffler_code": 10105, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_010201_Niro", "description": "Niro", "schaeffler_code": 10201, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_010301_Tin", "description": "MU-Stahl, Zinnüberzug / MX-Stahl, Zinnüberzug", "schaeffler_code": 10301, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_010401_Aluminium", "description": "Aluminium", "schaeffler_code": 10401, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_010501_Brass", "description": "Messing", "schaeffler_code": 10501, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_010601_Bronze", "description": "MU-B, Bronze", "schaeffler_code": 10601, "source": "schaeffler_standard"},
# --- 02 Coatings ---
{"name": "SCHAEFFLER_020101_Durotect-Blue", "description": "Stahl, Durotect CMT", "schaeffler_code": 20101, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_020102_Durotect-Black", "description": "Stahl, Durotect M", "schaeffler_code": 20102, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_020201_Coat-Black", "description": "", "schaeffler_code": 20201, "source": "schaeffler_standard"},
# --- 03 Non-metals ---
{"name": "SCHAEFFLER_030101_Elastomer-Brown", "description": "Elastomer, braun", "schaeffler_code": 30101, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_030102_Elastomer-Green", "description": "Elastomer, grün", "schaeffler_code": 30102, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_030103_Elastomer-Black", "description": "Elastomer, schwarz", "schaeffler_code": 30103, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_030201_Plastic-Brown", "description": "Kunststoff, braun", "schaeffler_code": 30201, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_030202_Plastic-Green", "description": "Kunststoff, grün", "schaeffler_code": 30202, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_030203_Plastic-Black", "description": "Kunststoff, schwarz", "schaeffler_code": 30203, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_030204_Plastic-Blue", "description": "Kunststoff, blau", "schaeffler_code": 30204, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_030205_Plastic-White", "description": "Kunststoff, weiß", "schaeffler_code": 30205, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_030301_Plastic-Clear", "description": "Kunststoff, durchsichtig", "schaeffler_code": 30301, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_030302_Plastic-Translucent-White", "description": "", "schaeffler_code": 30302, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_030401_TPU-Blue", "description": "TPU, blau", "schaeffler_code": 30401, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_030501_Ceramic-Black", "description": "Keramik, schwarz", "schaeffler_code": 30501, "source": "schaeffler_standard"},
# --- 04 Compounds ---
{"name": "SCHAEFFLER_040101_E40", "description": "E40", "schaeffler_code": 40101, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_040102_E50", "description": "E50", "schaeffler_code": 40102, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_040201_Elgoglide", "description": "Elgoglide", "schaeffler_code": 40201, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_040202_Elgotex", "description": "Elgotex, schwarz", "schaeffler_code": 40202, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_040301_PTFE-Niro-Compound", "description": "PTFE-Compound, Niro-Verbund", "schaeffler_code": 40301, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_040302_PTFE-Foil", "description": "PTFE-Folie", "schaeffler_code": 40302, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_040303_PTFE-Compound-Black", "description": "PTFE-Verbund, schwarz", "schaeffler_code": 40303, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_040304_PTFE-Compound-Orange", "description": "PTFE-Verbundwerkstoff", "schaeffler_code": 40304, "source": "schaeffler_standard"},
{"name": "SCHAEFFLER_040305_GFK-PTFE-Compound", "description": "GFK+PTFE Verbundwerkstoff, schwarz / TPU, schwarz", "schaeffler_code": 40305, "source": "schaeffler_standard"},
# --- 05 Misc ---
{"name": "SCHAEFFLER_059999_FailedMaterial", "description": "", "schaeffler_code": 59999, "source": "schaeffler_standard"},
]
+1 -1
View File
@@ -17,7 +17,7 @@ class Material(Base):
name: Mapped[str] = mapped_column(String(200), nullable=False, unique=True)
description: Mapped[str] = mapped_column(Text, nullable=True)
source: Mapped[str] = mapped_column(String(20), nullable=False, default="manual")
schaeffler_code: Mapped[int | None] = mapped_column(Integer, nullable=True)
hartomat_code: Mapped[int | None] = mapped_column(Integer, nullable=True)
created_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
+8 -8
View File
@@ -1,7 +1,7 @@
"""Material alias resolution service.
Used from Celery tasks (sync context) to resolve raw material names
(from Excel / user input) to SCHAEFFLER library material names via aliases.
(from Excel / user input) to HARTOMAT library material names via aliases.
Resolution chain:
1. Alias lookup (case-insensitive) → use alias.material.name
@@ -31,7 +31,7 @@ def _get_engine():
def resolve_material_map(raw_map: dict[str, str]) -> dict[str, str]:
"""Resolve raw material names to SCHAEFFLER library names via aliases.
"""Resolve raw material names to HARTOMAT library names via aliases.
For each value in raw_map:
1. Alias lookup (case-insensitive) → return alias.material.name
@@ -66,7 +66,7 @@ def resolve_material_map(raw_map: dict[str, str]) -> dict[str, str]:
raw_lower = raw_material.lower()
# 1. Alias lookup first — aliases explicitly map intermediate/display names
# to the canonical SCHAEFFLER library names
# to the canonical HARTOMAT library names
if raw_lower in alias_lookup:
target = alias_lookup[raw_lower]
logger.info("resolved '%s''%s' (alias match)", raw_material, target)
@@ -147,7 +147,7 @@ async def find_unmapped_materials(
"""Find material names that have no alias or library match.
Returns a list of {"raw_name": str, "suggestions": [...]} for each
unmapped name. Suggestions are the top 5 SCHAEFFLER library materials
unmapped name. Suggestions are the top 5 HARTOMAT library materials
by string similarity.
"""
if not material_names:
@@ -159,8 +159,8 @@ async def find_unmapped_materials(
# Load all materials
mat_rows = (await db.execute(select(Material))).scalars().all()
# Library materials have a schaeffler_code
library_mats = [m for m in mat_rows if m.schaeffler_code is not None]
# Library materials have a hartomat_code
library_mats = [m for m in mat_rows if m.hartomat_code is not None]
# All material names (case-insensitive) for exact-match check
name_lookup: dict[str, Material] = {m.name.lower(): m for m in mat_rows}
@@ -179,7 +179,7 @@ async def find_unmapped_materials(
# 2. Exact name match with a library material → mapped
matched_mat = name_lookup.get(raw_lower)
if matched_mat and matched_mat.schaeffler_code is not None:
if matched_mat and matched_mat.hartomat_code is not None:
continue
# Unmapped — compute suggestions from library materials
@@ -194,7 +194,7 @@ async def find_unmapped_materials(
{
"id": str(m.id),
"name": m.name,
"schaeffler_code": str(m.schaeffler_code),
"hartomat_code": str(m.hartomat_code),
}
for _, m in scored[:5]
]
+1 -1
View File
@@ -235,7 +235,7 @@ def send_email_notification_stub(
from email.mime.text import MIMEText
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = subject
msg["From"] = cfg.get("smtp_from_address") or cfg.get("smtp_user", "noreply@schaeffler.com")
msg["From"] = cfg.get("smtp_from_address") or cfg.get("smtp_user", "noreply@hartomat.com")
msg["To"] = to_address
port = int(cfg.get("smtp_port", "587"))
with smtplib.SMTP(smtp_host, port) as smtp:
@@ -330,7 +330,7 @@ def generate_usd_master_task(self, cad_file_id: str) -> dict:
if part_name and raw_material:
raw_mat_map[part_name] = raw_material
# Resolve raw material names to SCHAEFFLER library names via aliases
# Resolve raw material names to HARTOMAT library names via aliases
material_map: dict[str, str] = {}
if raw_mat_map:
material_map = resolve_material_map(raw_mat_map)
@@ -244,7 +244,7 @@ def render_order_line_task(self, order_line_id: str):
if m.get("part_name") and m.get("material")
}
# Resolve raw material names to SCHAEFFLER library names via aliases
# Resolve raw material names to HARTOMAT library names via aliases
from app.services.material_service import resolve_material_map
material_map = resolve_material_map(material_map)
@@ -61,7 +61,7 @@ _STEP_DESCRIPTIONS: dict[StepName, str] = {
StepName.OCC_OBJECT_EXTRACT: "Extract part objects and metadata from the STEP file using cadquery/OCC",
StepName.OCC_GLB_EXPORT: "Convert STEP geometry to glTF/GLB via cadquery",
StepName.GLB_BBOX: "Compute bounding-box from the exported GLB for camera framing",
StepName.MATERIAL_MAP_RESOLVE: "Resolve raw part-material names to SCHAEFFLER library materials via alias table",
StepName.MATERIAL_MAP_RESOLVE: "Resolve raw part-material names to HARTOMAT library materials via alias table",
StepName.AUTO_POPULATE_MATERIALS: "Auto-create Material records for any newly discovered part names",
StepName.BLENDER_RENDER: "Render a thumbnail PNG using Blender (Cycles or EEVEE)",
StepName.THREEJS_RENDER: "Render a thumbnail PNG using Three.js / Playwright headless browser",
+1 -1
View File
@@ -9,7 +9,7 @@ _DEFAULT_TENANT_CONFIG = {
"max_concurrent_renders": 3,
"render_engines_allowed": ["cycles", "eevee"],
"max_order_size": 500,
"fallback_material": "SCHAEFFLER_059999_FailedMaterial",
"fallback_material": "HARTOMAT_059999_FailedMaterial",
"notifications_enabled": True,
"invoice_prefix": "INV",
# Azure AI validation (per-tenant)
+3 -3
View File
@@ -41,9 +41,9 @@ async def lifespan(app: FastAPI):
app = FastAPI(
title="Schaeffler Automat API",
title="HartOMat API",
version="0.1.0",
description="Media-creation pipeline for Schaeffler CAD/bearing product orders",
description="Media-creation pipeline for HartOMat CAD/bearing product orders",
lifespan=lifespan,
)
@@ -101,7 +101,7 @@ app.include_router(chat_router, prefix="/api")
@app.get("/health")
async def health():
return {"status": "ok", "service": "schaefflerautomat-backend"}
return {"status": "ok", "service": "hartomat-backend"}
@app.websocket("/api/ws")
+2 -2
View File
@@ -8,12 +8,12 @@ from pathlib import Path
logger = logging.getLogger(__name__)
VALIDATION_PROMPT = """You are a quality control expert for Schaeffler bearing product catalog images.
VALIDATION_PROMPT = """You are a quality control expert for HartOMat bearing product catalog images.
Analyze this thumbnail of a bearing/mechanical component and evaluate:
1. Is the component orientation correct for a standard product catalog? (typically isometric view, 30° elevation, 45° rotation)
2. Are the key features visible? (rolling elements, rings, cage if present)
3. Does it match standard Schaeffler catalog angle conventions?
3. Does it match standard HartOMat catalog angle conventions?
Respond in JSON with exactly these fields:
{
+9 -9
View File
@@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
# ── System prompt ────────────────────────────────────────────────────────────
SYSTEM_PROMPT = """You are the Schaeffler Automat AI assistant. You help users manage their automated render pipeline for Schaeffler product images.
SYSTEM_PROMPT = """You are the HartOMat AI assistant. You help users manage their automated render pipeline for HartOMat product images.
You can:
- List and search orders and products
@@ -39,8 +39,8 @@ RULES:
7. Respond in the same language the user writes in.
8. Be concise — short answers are better than long ones.
9. When the user says "beliebig", "any", "random", "irgendein" — just pick one yourself, don't ask back.
10. Material system: Materials have SCHAEFFLER library names (e.g. SCHAEFFLER_020101_Durotect-Blue). Common names like "Durotect", "Stahl", "Bronze" are aliases that map to these library names. When the user asks for a material by a common name, use list_materials to find the correct SCHAEFFLER name, then use that for material_override.
11. When setting material_override, always use the full SCHAEFFLER library name (e.g. SCHAEFFLER_020101_Durotect-Blue), never the alias.
10. Material system: Materials have HARTOMAT library names (e.g. HARTOMAT_020101_Durotect-Blue). Common names like "Durotect", "Stahl", "Bronze" are aliases that map to these library names. When the user asks for a material by a common name, use list_materials to find the correct HARTOMAT name, then use that for material_override.
11. When setting material_override, always use the full HARTOMAT library name (e.g. HARTOMAT_020101_Durotect-Blue), never the alias.
12. When mentioning a product, ALWAYS link to it: [ProductName](/products/UUID). When mentioning an order, link to it: [OrderNumber](/orders/UUID). This makes the response navigable.
13. Materials exist at TWO levels: (a) product_material — materials assigned to the product's CAD parts (from STEP/Excel import, e.g. "Durotect_M", "Stahl"), and (b) material_override — a single material applied to ALL parts at render time. When user asks for a product "with Durotect material", search product_material FIRST (products that naturally have Durotect parts). Only use material_override filter if they specifically say "override" or "alle Teile in".
14. NEVER say "no renders found" when renders DO exist. If no exact match, show the closest match and explain what's different."""
@@ -120,7 +120,7 @@ TOOLS = [
},
"material_override": {
"type": "string",
"description": "Optional SCHAEFFLER library material name to apply to all lines.",
"description": "Optional HARTOMAT library material name to apply to all lines.",
"default": "",
},
},
@@ -176,7 +176,7 @@ TOOLS = [
},
"material_name": {
"type": "string",
"description": "SCHAEFFLER library material name, or empty string to clear.",
"description": "HARTOMAT library material name, or empty string to clear.",
},
},
"required": ["order_id", "material_name"],
@@ -250,7 +250,7 @@ TOOLS = [
"type": "function",
"function": {
"name": "list_materials",
"description": "List available SCHAEFFLER library materials with their aliases. Use this to find the correct material name for material_override. Materials have names like SCHAEFFLER_010101_Steel-Bare. Aliases map common names (Stahl, Bronze, Durotect, etc.) to these library materials. When user asks for a material by a common name, search aliases to find the correct SCHAEFFLER library material name.",
"description": "List available HARTOMAT library materials with their aliases. Use this to find the correct material name for material_override. Materials have names like HARTOMAT_010101_Steel-Bare. Aliases map common names (Stahl, Bronze, Durotect, etc.) to these library materials. When user asks for a material by a common name, search aliases to find the correct HARTOMAT library material name.",
"parameters": {
"type": "object",
"properties": {
@@ -671,13 +671,13 @@ async def _tool_check_materials(db: AsyncSession, tenant_id: str, user_id: str =
async def _tool_list_materials(db: AsyncSession, tenant_id: str, query: str = "") -> str:
"""List library materials with their aliases."""
sql = """
SELECT m.id, m.name, m.schaeffler_code, m.description,
SELECT m.id, m.name, m.hartomat_code, m.description,
COALESCE(
(SELECT json_agg(ma.alias) FROM material_aliases ma WHERE ma.material_id = m.id),
'[]'::json
) AS aliases
FROM materials m
WHERE m.schaeffler_code IS NOT NULL
WHERE m.hartomat_code IS NOT NULL
"""
params: dict = {}
if query:
@@ -698,7 +698,7 @@ async def _tool_list_materials(db: AsyncSession, tenant_id: str, query: str = ""
aliases = r["aliases"] if isinstance(r["aliases"], list) else []
materials.append({
"name": r["name"],
"schaeffler_code": r["schaeffler_code"],
"hartomat_code": r["hartomat_code"],
"description": r["description"],
"aliases": aliases[:10], # cap to avoid token bloat
})
+2 -2
View File
@@ -1,5 +1,5 @@
"""
Excel parser for Schaeffler CAD order lists.
Excel parser for HartOMat CAD order lists.
Supports two formats:
@@ -294,7 +294,7 @@ def _parse_material_mapping(wb) -> list[dict]:
def parse_excel(file_path: str | Path) -> ParsedExcel:
"""
Parse a Schaeffler order list Excel file.
Parse a HartOMat order list Excel file.
Returns a ParsedExcel with all data rows extracted.
Header-driven: finds "Ebene1" in any column within first 5 rows,
+2 -2
View File
@@ -25,9 +25,9 @@ def build_part_colors(
"""
Build {part_name: material_name} for Blender rendering.
Returns a mapping of part name Schaeffler material name (e.g. SCHAEFFLER_010101_Steel-Bare).
Returns a mapping of part name HartOMat material name (e.g. HARTOMAT_010101_Steel-Bare).
Parts with no material assignment are omitted; Blender will use the fallback material
(SCHAEFFLER_059999_FailedMaterial) for unrecognised parts.
(HARTOMAT_059999_FailedMaterial) for unrecognised parts.
Args:
cad_parsed_objects: List of part names from cad_file.parsed_objects["objects"].
+1 -1
View File
@@ -3,7 +3,7 @@ from celery.schedules import crontab
from app.config import settings
celery_app = Celery(
"schaefflerautomat",
"hartomat",
broker=settings.redis_url,
backend=settings.redis_url,
include=[
+3 -3
View File
@@ -1,4 +1,4 @@
"""Seed database with 7 Schaeffler product category templates."""
"""Seed database with 7 HartOMat product category templates."""
import asyncio
import uuid
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
@@ -133,7 +133,7 @@ TEMPLATES = [
]
async def seed(db_url: str, admin_email: str = "admin@schaeffler.com", admin_password: str = "Admin1234!"):
async def seed(db_url: str, admin_email: str = "admin@hartomat.com", admin_password: str = "Admin1234!"):
from app.models.template import Template
from app.models.user import User, UserRole
from app.utils.auth import hash_password
@@ -162,7 +162,7 @@ async def seed(db_url: str, admin_email: str = "admin@schaeffler.com", admin_pas
email=admin_email,
password_hash=hash_password(admin_password),
role=UserRole.global_admin,
full_name="Schaeffler Admin",
full_name="HartOMat Admin",
)
session.add(admin)
print(f" + Admin user: {admin_email}")
+1 -1
View File
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
packages = ["app"]
[project]
name = "schaefflerautomat-backend"
name = "hartomat-backend"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
+2 -2
View File
@@ -1,5 +1,5 @@
"""
Pytest fixtures for the Schaeffler Automat backend test suite.
Pytest fixtures for the HartOMat backend test suite.
The tests in this suite are divided into:
- Unit tests (no DB / network required): excel_parser, models, schemas
@@ -122,7 +122,7 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sess
TEST_DB_URL = os.environ.get(
"TEST_DATABASE_URL",
"postgresql+asyncpg://schaeffler:schaeffler@localhost:5432/schaeffler_test"
"postgresql+asyncpg://hartomat:hartomat@localhost:5432/hartomat_test"
)
+3 -3
View File
@@ -16,13 +16,13 @@
services:
render-worker:
image: schaefflerautomat-render-worker:latest
image: hartomat-render-worker:latest
# Or build locally: build: { context: ./render-worker, dockerfile: Dockerfile }
environment:
- REDIS_URL=${REDIS_URL:?Set REDIS_URL to the main server Redis URL}
- POSTGRES_HOST=${POSTGRES_HOST:?Set POSTGRES_HOST to the main server DB host}
- POSTGRES_DB=${POSTGRES_DB:-schaeffler}
- POSTGRES_USER=${POSTGRES_USER:-schaeffler}
- POSTGRES_DB=${POSTGRES_DB:-hartomat}
- POSTGRES_USER=${POSTGRES_USER:-hartomat}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
- MINIO_URL=${MINIO_URL:?Set MINIO_URL to the main server MinIO URL}
+19 -19
View File
@@ -2,15 +2,15 @@ services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-schaeffler}
POSTGRES_USER: ${POSTGRES_USER:-schaeffler}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-schaeffler}
POSTGRES_DB: ${POSTGRES_DB:-hartomat}
POSTGRES_USER: ${POSTGRES_USER:-hartomat}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-hartomat}
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-schaeffler}"]
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-hartomat}"]
interval: 5s
timeout: 5s
retries: 5
@@ -48,9 +48,9 @@ services:
dockerfile: Dockerfile
command: /start.sh
environment:
- POSTGRES_DB=${POSTGRES_DB:-schaeffler}
- POSTGRES_USER=${POSTGRES_USER:-schaeffler}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-schaeffler}
- POSTGRES_DB=${POSTGRES_DB:-hartomat}
- POSTGRES_USER=${POSTGRES_USER:-hartomat}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-hartomat}
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- REDIS_URL=${REDIS_URL:-redis://redis:6379/0}
@@ -89,9 +89,9 @@ services:
dockerfile: Dockerfile
command: celery -A app.tasks.celery_app worker --loglevel=info -Q step_processing,ai_validation --autoscale=${MAX_CONCURRENCY:-8},${MIN_CONCURRENCY:-2} --concurrency=${MIN_CONCURRENCY:-2}
environment:
- POSTGRES_DB=${POSTGRES_DB:-schaeffler}
- POSTGRES_USER=${POSTGRES_USER:-schaeffler}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-schaeffler}
- POSTGRES_DB=${POSTGRES_DB:-hartomat}
- POSTGRES_USER=${POSTGRES_USER:-hartomat}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-hartomat}
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- REDIS_URL=${REDIS_URL:-redis://redis:6379/0}
@@ -123,9 +123,9 @@ services:
- BLENDER_VERSION=${BLENDER_VERSION:-5.0.1}
command: bash -c "python3 /check_version.py && celery -A app.tasks.celery_app worker --loglevel=info -Q asset_pipeline --autoscale=1,1 --concurrency=1"
environment:
- POSTGRES_DB=${POSTGRES_DB:-schaeffler}
- POSTGRES_USER=${POSTGRES_USER:-schaeffler}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-schaeffler}
- POSTGRES_DB=${POSTGRES_DB:-hartomat}
- POSTGRES_USER=${POSTGRES_USER:-hartomat}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-hartomat}
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- REDIS_URL=${REDIS_URL:-redis://redis:6379/0}
@@ -165,9 +165,9 @@ services:
- BLENDER_VERSION=${BLENDER_VERSION:-5.0.1}
command: bash -c "python3 /check_version.py && celery -A app.tasks.celery_app worker --loglevel=info -Q asset_pipeline_light --autoscale=2,2 --concurrency=2"
environment:
- POSTGRES_DB=${POSTGRES_DB:-schaeffler}
- POSTGRES_USER=${POSTGRES_USER:-schaeffler}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-schaeffler}
- POSTGRES_DB=${POSTGRES_DB:-hartomat}
- POSTGRES_USER=${POSTGRES_USER:-hartomat}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-hartomat}
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- REDIS_URL=${REDIS_URL:-redis://redis:6379/0}
@@ -204,9 +204,9 @@ services:
dockerfile: Dockerfile
command: celery -A app.tasks.celery_app beat --loglevel=info
environment:
- POSTGRES_DB=${POSTGRES_DB:-schaeffler}
- POSTGRES_USER=${POSTGRES_USER:-schaeffler}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-schaeffler}
- POSTGRES_DB=${POSTGRES_DB:-hartomat}
- POSTGRES_USER=${POSTGRES_USER:-hartomat}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-hartomat}
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- REDIS_URL=${REDIS_URL:-redis://redis:6379/0}
+16 -16
View File
@@ -1,6 +1,6 @@
# Schaeffler Automat MCP Server
# HartOMat MCP Server
An MCP (Model Context Protocol) server that gives Claude Code direct access to the Schaeffler Automat render pipeline, product library, and database.
An MCP (Model Context Protocol) server that gives Claude Code direct access to the HartOMat render pipeline, product library, and database.
## Quick Start
@@ -17,9 +17,9 @@ The project includes `.mcp.json` which automatically registers the MCP server wh
### Setup (manual)
```bash
claude mcp add schaeffler -- uv run \
claude mcp add hartomat -- uv run \
--with "mcp[cli]" --with psycopg2-binary --with httpx \
python schaeffler_mcp_server.py
python hartomat_mcp_server.py
```
### Verify
@@ -29,7 +29,7 @@ Inside Claude Code, run:
/mcp
```
You should see `schaeffler` listed with status "connected".
You should see `hartomat` listed with status "connected".
## Available Tools
@@ -61,8 +61,8 @@ You should see `schaeffler` listed with status "connected".
| Resource URI | Description |
|---|---|
| `schaeffler://schema` | Full database schema (tables + columns) |
| `schaeffler://output-types` | All configured output types |
| `hartomat://schema` | Full database schema (tables + columns) |
| `hartomat://output-types` | All configured output types |
## Usage Examples
@@ -98,9 +98,9 @@ The server connects to your local Docker services by default. Override via envir
| Variable | Default | Description |
|---|---|---|
| `DATABASE_URL` | `postgresql://schaeffler:schaeffler@localhost:5432/schaeffler` | PostgreSQL connection string |
| `DATABASE_URL` | `postgresql://hartomat:hartomat@localhost:5432/hartomat` | PostgreSQL connection string |
| `API_URL` | `http://localhost:8888` | Backend API base URL |
| `API_EMAIL` | `admin@schaeffler.com` | API login email |
| `API_EMAIL` | `admin@hartomat.com` | API login email |
| `API_PASSWORD` | `Admin1234!` | API login password |
### Custom configuration
@@ -108,11 +108,11 @@ The server connects to your local Docker services by default. Override via envir
Edit `.mcp.json` in the project root to change defaults, or use `claude mcp add` with `--env` flags:
```bash
claude mcp add schaeffler \
claude mcp add hartomat \
--env DATABASE_URL=postgresql://user:pass@host/db \
--env API_URL=https://staging.example.com \
-- uv run --with "mcp[cli]" --with psycopg2-binary --with httpx \
python schaeffler_mcp_server.py
python hartomat_mcp_server.py
```
## Security Notes
@@ -127,7 +127,7 @@ claude mcp add schaeffler \
### Server not connecting
1. Check Docker services are running: `docker compose ps`
2. Check PostgreSQL is accessible: `psql postgresql://schaeffler:schaeffler@localhost:5432/schaeffler -c "SELECT 1"`
2. Check PostgreSQL is accessible: `psql postgresql://hartomat:hartomat@localhost:5432/hartomat -c "SELECT 1"`
3. Check backend API is up: `curl http://localhost:8888/api/auth/login`
### Dependencies missing
@@ -141,14 +141,14 @@ uv pip install "mcp[cli]" psycopg2-binary httpx
```bash
# Run manually to see errors
uv run --with "mcp[cli]" --with psycopg2-binary --with httpx \
python schaeffler_mcp_server.py
python hartomat_mcp_server.py
```
### Reset MCP connection
```bash
claude mcp remove schaeffler
claude mcp add schaeffler -- uv run \
claude mcp remove hartomat
claude mcp add hartomat -- uv run \
--with "mcp[cli]" --with psycopg2-binary --with httpx \
python schaeffler_mcp_server.py
python hartomat_mcp_server.py
```
+17 -17
View File
@@ -12,7 +12,7 @@
- [ ] `blender_render.py` decomposition is still pending; current file remains monolithic
- [ ] Legacy STL-era cleanup is still pending (`stl_quality`, STL endpoints, orphaned directories)
- [x] Decision: USD authoring library → **`usd-core` (pip)** — provides `pxr` module, no GPU tools needed, pip-installable in render-worker
- [x] Decision: seam/sharp payload encoding → **index-space primvars** (`primvars:schaeffler:seamEdgeVertexPairs`, `primvars:schaeffler:sharpEdgeVertexPairs`) — survives transforms, no KD-tree needed
- [x] Decision: seam/sharp payload encoding → **index-space primvars** (`primvars:hartomat:seamEdgeVertexPairs`, `primvars:hartomat:sharpEdgeVertexPairs`) — survives transforms, no KD-tree needed
- [x] Decision: preview GLB derivation → **co-author from same tessellation pass** during migration (avoid round-trip loss from USD→GLB export)
- [x] Decision: single-file vs override layers → **Option B: canonical geometry layer + material override layer, flattened via `UsdUtils.FlattenLayerStack()` for delivery** — preserves hierarchy AND allows instancing later (`FlattenLayerStack` keeps `instanceable` prims; `UsdStage.Flatten` would expand them). Note: Phase 1 uses no instancing (matching current GLB pipeline), but the delivery path is already instancing-safe.
@@ -63,14 +63,14 @@ Add after the `gmsh` line. `usd-core` is the Pixar-maintained pip distribution o
| Attribute | Value source |
|---|---|
| `schaeffler:partKey` | `generate_part_key(xcaf_label_path)` |
| `schaeffler:sourceName` | XCAF `TDataStd_Name` attribute |
| `schaeffler:sourceColor` | XCAF embedded color (hex string) |
| `schaeffler:rawMaterialName` | from `CadFile.part_materials` if available |
| `schaeffler:tessellation:linearDeflectionMm` | CLI arg value |
| `schaeffler:tessellation:angularDeflectionRad` | CLI arg value |
| `primvars:schaeffler:seamEdgeVertexPairs` | OCC B-rep seam edges (index pairs in mesh-local space) |
| `primvars:schaeffler:sharpEdgeVertexPairs` | sharp edges from `_extract_sharp_edge_pairs()` |
| `hartomat:partKey` | `generate_part_key(xcaf_label_path)` |
| `hartomat:sourceName` | XCAF `TDataStd_Name` attribute |
| `hartomat:sourceColor` | XCAF embedded color (hex string) |
| `hartomat:rawMaterialName` | from `CadFile.part_materials` if available |
| `hartomat:tessellation:linearDeflectionMm` | CLI arg value |
| `hartomat:tessellation:angularDeflectionRad` | CLI arg value |
| `primvars:hartomat:seamEdgeVertexPairs` | OCC B-rep seam edges (index pairs in mesh-local space) |
| `primvars:hartomat:sharpEdgeVertexPairs` | sharp edges from `_extract_sharp_edge_pairs()` |
**CLI interface:**
@@ -86,7 +86,7 @@ python3 export_step_to_usd.py \
**Acceptance gate:** `python3 export_step_to_usd.py --step_path 81113-l_cut.stp --output_path /tmp/test.usd`
- File exists, parseable
- 25 part prims with `schaeffler:partKey` attribute
- 25 part prims with `hartomat:partKey` attribute
- Part count matches `export_step_to_gltf.py` output for same file
### Task 1.2 — `usd_master` MediaAsset type
@@ -150,7 +150,7 @@ def build_scene_manifest(cad_file: CadFile, usd_asset: MediaAsset) -> dict:
"part_key": "ring_outer",
"source_name": "RingOuter_AF0",
"prim_path": "/Root/Assembly/Bearing/RingOuter",
"effective_material": "SCHAEFFLER_010102_...",
"effective_material": "HARTOMAT_010102_...",
"assignment_provenance": "manual|auto|default",
"is_unassigned": false
}
@@ -198,7 +198,7 @@ New endpoint returning `SceneManifest`. Calls `build_scene_manifest()` — reads
**File:** `backend/app/api/routers/cad.py`
Accept `{ "part_key": "ring_outer", "material": "SCHAEFFLER_010102_..." }` body (or bulk map). Write to `manual_material_overrides` column (not the old `part_materials` column).
Accept `{ "part_key": "ring_outer", "material": "HARTOMAT_010102_..." }` body (or bulk map). Write to `manual_material_overrides` column (not the old `part_materials` column).
**Acceptance gate:** PUT with `partKey` → subsequent GET `/scene-manifest` shows that part's `assignment_provenance: "manual"`.
@@ -218,10 +218,10 @@ After tessellation (OCC or GMSH), for each mesh face:
Write to USD mesh prim:
```python
mesh_prim.GetPrimvar("schaeffler:seamEdgeVertexPairs").Set(
mesh_prim.GetPrimvar("hartomat:seamEdgeVertexPairs").Set(
Vt.Vec2iArray(seam_pairs), # [(vi0, vi1), ...]
)
mesh_prim.GetPrimvar("schaeffler:sharpEdgeVertexPairs").Set(
mesh_prim.GetPrimvar("hartomat:sharpEdgeVertexPairs").Set(
Vt.Vec2iArray(sharp_pairs),
)
```
@@ -243,8 +243,8 @@ def import_usd_and_restore_topology(usd_path: str) -> list:
if obj.type != 'MESH':
continue
# Read custom attributes set by USD importer
seam_pairs = obj.get("schaeffler_seamEdgeVertexPairs") or []
sharp_pairs = obj.get("schaeffler_sharpEdgeVertexPairs") or []
seam_pairs = obj.get("hartomat_seamEdgeVertexPairs") or []
sharp_pairs = obj.get("hartomat_sharpEdgeVertexPairs") or []
_mark_seams_from_index_pairs(obj, seam_pairs)
_mark_sharp_from_index_pairs(obj, sharp_pairs)
...
@@ -262,7 +262,7 @@ def import_usd_and_restore_topology(usd_path: str) -> list:
Add `--usd_path` argument. When provided:
1. Call `import_usd.py` instead of `export_gltf.py` GLB import
2. Read `schaeffler:partKey` and `schaeffler:canonicalMaterialName` per mesh object after import
2. Read `hartomat:partKey` and `hartomat:canonicalMaterialName` per mesh object after import
3. Apply materials by `partKey → material library name` lookup instead of object-name heuristics
**Migration:** Keep `--glb_path` working in parallel; switch production task to prefer `--usd_path` when `usd_master` asset exists.
+41 -41
View File
@@ -35,12 +35,12 @@ That split introduces avoidable duplication, fragility, and impedance mismatches
The code confirms this architecture:
- Geometry export task: [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/pipeline/tasks/export_glb.py#L16)
- Production export task: [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/pipeline/tasks/export_glb.py#L176)
- Blender production export script: [render-worker/scripts/export_gltf.py](/home/hartmut/Documents/Copilot/schaefflerautomat/render-worker/scripts/export_gltf.py#L106)
- OCC GLB exporter with XCAF name/color preservation and sharp-edge extras: [render-worker/scripts/export_step_to_gltf.py](/home/hartmut/Documents/Copilot/schaefflerautomat/render-worker/scripts/export_step_to_gltf.py#L301)
- Media asset model: [backend/app/domains/media/models.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/media/models.py#L11)
- Frontend viewer contract: [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L40)
- Geometry export task: [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/pipeline/tasks/export_glb.py#L16)
- Production export task: [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/pipeline/tasks/export_glb.py#L176)
- Blender production export script: [render-worker/scripts/export_gltf.py](/home/hartmut/Documents/Copilot/hartomat/render-worker/scripts/export_gltf.py#L106)
- OCC GLB exporter with XCAF name/color preservation and sharp-edge extras: [render-worker/scripts/export_step_to_gltf.py](/home/hartmut/Documents/Copilot/hartomat/render-worker/scripts/export_step_to_gltf.py#L301)
- Media asset model: [backend/app/domains/media/models.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/media/models.py#L11)
- Frontend viewer contract: [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/components/cad/ThreeDViewer.tsx#L40)
## Goals
@@ -95,18 +95,18 @@ However, this data is not represented in a durable scene data model. Some of it
Material mapping already exists conceptually in the domain model:
- product-level `cad_part_materials`
- canonical material/alias resolution in [backend/app/domains/materials/service.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/materials/service.py#L32)
- canonical material/alias resolution in [backend/app/domains/materials/service.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/materials/service.py#L32)
The weak point is the last mile: materials are currently assigned in Blender by matching imported object names from a GLB round-trip in [render-worker/scripts/export_gltf.py](/home/hartmut/Documents/Copilot/schaefflerautomat/render-worker/scripts/export_gltf.py#L192).
The weak point is the last mile: materials are currently assigned in Blender by matching imported object names from a GLB round-trip in [render-worker/scripts/export_gltf.py](/home/hartmut/Documents/Copilot/hartomat/render-worker/scripts/export_gltf.py#L192).
### Admin and Settings Surface
The admin/backend model still mirrors the dual-GLB architecture:
- separate preview and production tessellation settings in [backend/app/api/routers/admin.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/admin.py#L24)
- a bulk action specifically for missing geometry GLBs in [backend/app/api/routers/admin.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/admin.py#L536)
- an admin UI that exposes preview-vs-production GLB tessellation controls in [frontend/src/pages/Admin.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/pages/Admin.tsx#L1400)
- product detail logic that queries both `gltf_geometry` and `gltf_production` assets in [frontend/src/pages/ProductDetail.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/pages/ProductDetail.tsx#L182)
- separate preview and production tessellation settings in [backend/app/api/routers/admin.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/api/routers/admin.py#L24)
- a bulk action specifically for missing geometry GLBs in [backend/app/api/routers/admin.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/api/routers/admin.py#L536)
- an admin UI that exposes preview-vs-production GLB tessellation controls in [frontend/src/pages/Admin.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/pages/Admin.tsx#L1400)
- product detail logic that queries both `gltf_geometry` and `gltf_production` assets in [frontend/src/pages/ProductDetail.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/pages/ProductDetail.tsx#L182)
That duplication is operationally expensive and should be reduced as part of the refactor, not carried forward under new names.
@@ -216,17 +216,17 @@ Object names imported into Blender must no longer be the primary identity mechan
Each part prim should carry:
- `schaeffler:partKey`
- `schaeffler:sourceName`
- `schaeffler:sourceAssemblyPath`
- `schaeffler:sourceColor`
- `schaeffler:rawMaterialName`
- `schaeffler:canonicalMaterialName`
- `schaeffler:tessellation:linearDeflectionMm`
- `schaeffler:tessellation:angularDeflectionRad`
- `schaeffler:cadFileId`
- `schaeffler:productId` when available
- `schaeffler:mesh:topologyHash`
- `hartomat:partKey`
- `hartomat:sourceName`
- `hartomat:sourceAssemblyPath`
- `hartomat:sourceColor`
- `hartomat:rawMaterialName`
- `hartomat:canonicalMaterialName`
- `hartomat:tessellation:linearDeflectionMm`
- `hartomat:tessellation:angularDeflectionRad`
- `hartomat:cadFileId`
- `hartomat:productId` when available
- `hartomat:mesh:topologyHash`
Each mesh prim should carry:
@@ -241,10 +241,10 @@ Each mesh prim should carry:
USD does not have a first-class standard seam concept for Blender UV editing, so this RFC proposes storing authored topology support data as custom primvars or custom attributes on the mesh prim:
- `primvars:schaeffler:seamEdgeVertexPairs`
- `primvars:schaeffler:sharpEdgeVertexPairs`
- `primvars:schaeffler:faceBatchIds`
- `primvars:schaeffler:sourceUv`
- `primvars:hartomat:seamEdgeVertexPairs`
- `primvars:hartomat:sharpEdgeVertexPairs`
- `primvars:hartomat:faceBatchIds`
- `primvars:hartomat:sourceUv`
The exact encoding can evolve, but the initial implementation should optimize for deterministic Blender reconstruction rather than elegance.
@@ -321,10 +321,10 @@ The current viewer supports several behaviors that must not regress:
Those behaviors currently operate on GLB mesh objects in:
- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L488)
- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L553)
- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L675)
- [frontend/src/components/cad/MaterialPanel.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/MaterialPanel.tsx#L123)
- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/components/cad/ThreeDViewer.tsx#L488)
- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/components/cad/ThreeDViewer.tsx#L553)
- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/components/cad/ThreeDViewer.tsx#L675)
- [frontend/src/components/cad/MaterialPanel.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/components/cad/MaterialPanel.tsx#L123)
The USD refactor must preserve these capabilities. The replacement is not "browser renders USD directly". The replacement is:
@@ -393,10 +393,10 @@ That makes missing assignments visible instead of silently failing.
The current backend already has the right conceptual split, but the naming is misleading:
- [backend/app/domains/products/models.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/products/models.py#L62) `Product.cad_part_materials` behaves like imported or product-authored source material rows
- [backend/app/domains/products/models.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/products/models.py#L34) `CadFile.part_materials` behaves like viewer-side manual assignments or overrides
- [backend/app/api/routers/cad.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/cad.py#L395) still presents those overrides as a part-name keyed map
- [backend/app/api/routers/products.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/products.py#L433) performs Excel reconciliation directly into the product-side list
- [backend/app/domains/products/models.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/products/models.py#L62) `Product.cad_part_materials` behaves like imported or product-authored source material rows
- [backend/app/domains/products/models.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/products/models.py#L34) `CadFile.part_materials` behaves like viewer-side manual assignments or overrides
- [backend/app/api/routers/cad.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/api/routers/cad.py#L395) still presents those overrides as a part-name keyed map
- [backend/app/api/routers/products.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/api/routers/products.py#L433) performs Excel reconciliation directly into the product-side list
The USD refactor should formalize this into three explicit layers:
@@ -567,7 +567,7 @@ The existing `export_step_to_gltf.py` can remain temporarily for migration and f
## 2. Media Asset Model
Extend [backend/app/domains/media/models.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/media/models.py#L11) with at least:
Extend [backend/app/domains/media/models.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/media/models.py#L11) with at least:
- `usd_master`
@@ -580,7 +580,7 @@ The important change is that `usd_master` becomes the canonical CAD scene artifa
## 3. Pipeline Tasks
Replace the current dual-export mental model in [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/pipeline/tasks/export_glb.py#L16) with:
Replace the current dual-export mental model in [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/pipeline/tasks/export_glb.py#L16) with:
- `generate_usd_master_task`
- optional `generate_preview_glb_task`
@@ -590,7 +590,7 @@ The production GLB task should be retired once Blender can render from USD and b
## 4. Render Service
The render service in [backend/app/services/render_blender.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/services/render_blender.py#L18) currently converts `STEP -> GLB -> Blender render`.
The render service in [backend/app/services/render_blender.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/services/render_blender.py#L18) currently converts `STEP -> GLB -> Blender render`.
Target flow:
@@ -608,7 +608,7 @@ This removes the need for a production GLB as an intermediate render artifact.
## 5. Frontend
The current viewer API in [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L40) assumes:
The current viewer API in [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/components/cad/ThreeDViewer.tsx#L40) assumes:
- one geometry GLB
- one production GLB
@@ -633,7 +633,7 @@ The frontend should stop encoding the architectural distinction between geometry
## 6. Viewer Assignment API
The current viewer override endpoint in [backend/app/api/routers/cad.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/cad.py#L395) should evolve from a raw part-name keyed map into a canonical scene-assignment endpoint.
The current viewer override endpoint in [backend/app/api/routers/cad.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/api/routers/cad.py#L395) should evolve from a raw part-name keyed map into a canonical scene-assignment endpoint.
Target behavior:
@@ -720,7 +720,7 @@ Example shape:
"part_key": "ring_outer",
"source_name": "RingOuter_AF0",
"prim_path": "/Root/Assembly/Bearing/RingOuter",
"effective_material": "SCHAEFFLER_010102_Steel-Polished",
"effective_material": "HARTOMAT_010102_Steel-Polished",
"assignment_provenance": "manual",
"is_unassigned": false
}
+2 -2
View File
@@ -2,9 +2,9 @@
<html lang="en" class="" data-accent="green">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/schaeffler.svg" />
<link rel="icon" type="image/svg+xml" href="/hartomat.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hart.O.Mat — Hartomatisierung</title>
<title>HartOMat</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "schaefflerautomat-frontend",
"name": "hartomat-frontend",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "schaefflerautomat-frontend",
"name": "hartomat-frontend",
"version": "0.1.0",
"dependencies": {
"@react-three/drei": "^9.102.3",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "schaefflerautomat-frontend",
"name": "hartomat-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-labelledby="title">
<title>HartOMat</title>
<rect width="128" height="128" rx="28" fill="#0b3d2e"/>
<path d="M28 32h18v24h36V32h18v64H82V72H46v24H28z" fill="#f3efe2"/>
</svg>

After

Width:  |  Height:  |  Size: 259 B

+1 -1
View File
@@ -9,7 +9,7 @@ const api = axios.create({
api.interceptors.request.use((config) => {
const token = useAuthStore.getState().token
if (token) config.headers.Authorization = `Bearer ${token}`
const tenantId = localStorage.getItem('schaeffler_tenant_id')
const tenantId = localStorage.getItem('hartomat_tenant_id')
if (tenantId) config.headers['X-Tenant-ID'] = tenantId
return config
})
+5 -5
View File
@@ -5,7 +5,7 @@ export interface Material {
name: string
description: string | null
source: string
schaeffler_code: number | null
hartomat_code: number | null
created_by_name: string | null
aliases: string[]
created_at: string
@@ -27,7 +27,7 @@ export async function createMaterial(data: {
name: string
description?: string
source?: string
schaeffler_code?: number | null
hartomat_code?: number | null
}) {
const res = await api.post<Material>('/materials', data)
return res.data
@@ -54,8 +54,8 @@ export async function saveCadPartMaterials(
return res.data
}
export async function seedSchaefflerMaterials() {
const res = await api.post<{ inserted: number; total: number }>('/materials/seed-schaeffler')
export async function seedHartOMatMaterials() {
const res = await api.post<{ inserted: number; total: number }>('/materials/seed-hartomat')
return res.data
}
@@ -93,7 +93,7 @@ export async function seedAliases(): Promise<{ inserted: number; total: number }
export interface MaterialSuggestion {
id: string
name: string
schaeffler_code: string
hartomat_code: string
}
export interface UnmappedMaterial {
+5 -5
View File
@@ -98,10 +98,10 @@ export default function MaterialWizard({ open, onClose, onCreated }: Props) {
.replace(/^-|-$/g, '')
const fullMaterialName = fullCode && sanitizedName
? `SCHAEFFLER_${fullCode}_${sanitizedName}`
? `HARTOMAT_${fullCode}_${sanitizedName}`
: null
const schaefflerCodeInt = fullCode ? parseInt(fullCode, 10) : null
const hartomatCodeInt = fullCode ? parseInt(fullCode, 10) : null
const createMut = useMutation({
mutationFn: () =>
@@ -109,7 +109,7 @@ export default function MaterialWizard({ open, onClose, onCreated }: Props) {
name: fullMaterialName!,
description: description.trim() || undefined,
source: 'manual',
schaeffler_code: schaefflerCodeInt,
hartomat_code: hartomatCodeInt,
}),
onSuccess: () => {
toast.success('Material created')
@@ -133,7 +133,7 @@ export default function MaterialWizard({ open, onClose, onCreated }: Props) {
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-border-default">
<div>
<h2 className="text-lg font-semibold text-content">Schaeffler Material Wizard</h2>
<h2 className="text-lg font-semibold text-content">HartOMat Material Wizard</h2>
<p className="text-xs text-content-muted mt-0.5">Step {step} of 3</p>
</div>
<button onClick={onClose} className="text-content-muted hover:text-content-secondary">
@@ -267,7 +267,7 @@ export default function MaterialWizard({ open, onClose, onCreated }: Props) {
<p className="font-mono text-sm font-semibold text-content truncate">
{fullMaterialName || (
<span className="text-content-muted">
SCHAEFFLER_{typeCode || 'XX'}{effectiveSubType || 'YY'}{consecutive !== null ? String(consecutive).padStart(2, '0') : 'ZZ'}_{sanitizedName || 'Name'}
HARTOMAT_{typeCode || 'XX'}{effectiveSubType || 'YY'}{consecutive !== null ? String(consecutive).padStart(2, '0') : 'ZZ'}_{sanitizedName || 'Name'}
</span>
)}
</p>
@@ -47,7 +47,7 @@ export default function OutputTypeTable() {
queryKey: ['materials'],
queryFn: listMaterials,
})
const libraryMaterials = (allMaterials ?? []).filter((m: Material) => m.schaeffler_code !== null).sort((a: Material, b: Material) => a.name.localeCompare(b.name))
const libraryMaterials = (allMaterials ?? []).filter((m: Material) => m.hartomat_code !== null).sort((a: Material, b: Material) => a.name.localeCompare(b.name))
const { data: workflows } = useQuery({
queryKey: ['workflows'],
@@ -856,7 +856,7 @@ export default function OutputTypeTable() {
<td className="px-4 py-2">
{ot.material_override ? (
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-50 text-amber-700 font-mono truncate block max-w-[140px]" title={ot.material_override}>
{ot.material_override.replace('SCHAEFFLER_', '').replace(/_/g, ' ')}
{ot.material_override.replace('HARTOMAT_', '').replace(/_/g, ' ')}
</span>
) : (
<span className="text-xs text-content-muted"></span>
@@ -15,7 +15,7 @@ export interface MaterialOut {
id: string
name: string
description: string | null
schaeffler_code: number | null
hartomat_code: number | null
source: string
}
+1 -1
View File
@@ -159,7 +159,7 @@ export function pbrColorHex(pbr: MaterialPBR): string {
/**
* Get a preview hex color for a material entry, using PBR data when available.
* Replaces the old hardcoded SCHAEFFLER_COLORS lookup.
* Replaces the old hardcoded HARTOMAT_COLORS lookup.
*/
export function previewColorForEntry(
entry: PartMaterialEntry,
+3 -3
View File
@@ -23,7 +23,7 @@ export default function ChatPanel({ open, onClose, contextType, contextId }: Cha
const [messages, setMessages] = useState<ChatMessage[]>([])
const [sessionId, setSessionId] = useState<string | undefined>(() => {
// Restore last session from localStorage
try { return localStorage.getItem('schaeffler-chat-session') || undefined } catch { return undefined }
try { return localStorage.getItem('hartomat-chat-session') || undefined } catch { return undefined }
})
const [input, setInput] = useState('')
const [showSessions, setShowSessions] = useState(false)
@@ -31,8 +31,8 @@ export default function ChatPanel({ open, onClose, contextType, contextId }: Cha
// Persist sessionId to localStorage
useEffect(() => {
try {
if (sessionId) localStorage.setItem('schaeffler-chat-session', sessionId)
else localStorage.removeItem('schaeffler-chat-session')
if (sessionId) localStorage.setItem('hartomat-chat-session', sessionId)
else localStorage.removeItem('hartomat-chat-session')
} catch { /* ignore */ }
}, [sessionId])
const messagesEndRef = useRef<HTMLDivElement>(null)
@@ -27,7 +27,7 @@ export default function UnmappedMaterialsDialog({ unmapped, onResolved, onCancel
})
const libraryMaterials = (allMaterials ?? []).filter(
(m: Material) => m.schaeffler_code !== null
(m: Material) => m.hartomat_code !== null
)
const allMapped = unmapped.every((u) => mappings[u.raw_name])
@@ -11,14 +11,14 @@ const TYPE_GROUPS: Record<string, { label: string; color: string }> = {
}
function getTypeCode(mat: Material): string | null {
if (mat.schaeffler_code == null) return null
const s = String(mat.schaeffler_code).padStart(6, '0')
if (mat.hartomat_code == null) return null
const s = String(mat.hartomat_code).padStart(6, '0')
return s.slice(0, 2)
}
/** Extract the human-readable short name after the last underscore: SCHAEFFLER_010101_Steel-Bare -> Steel-Bare */
/** Extract the human-readable short name after the last underscore: HARTOMAT_010101_Steel-Bare -> Steel-Bare */
function shortName(name: string): string {
const match = name.match(/^SCHAEFFLER_\d{6}_(.+)$/)
const match = name.match(/^HARTOMAT_\d{6}_(.+)$/)
return match ? match[1].replace(/-/g, ' ') : name
}
@@ -52,7 +52,7 @@ export default function MaterialInput({ value, onChange, library, missing, onOpe
buckets.get(tc)!.push(m)
}
// Sorted type codes first, then non-schaeffler
// Sorted type codes first, then non-hartomat
const sortedKeys = [...buckets.keys()].sort((a, b) => {
if (a === null) return 1
if (b === null) return -1
+2 -2
View File
@@ -50,7 +50,7 @@ export const HELP_TEXTS: Record<string, HelpText> = {
},
'action.seed_aliases': {
title: 'Seed Material Aliases',
body: 'Loads the default Schaeffler material alias mappings (Steel→SCHAEFFLER_010101_Steel-Bare, etc). Safe to run multiple times — existing aliases are not overwritten.',
body: 'Loads the default HartOMat material alias mappings (Steel→HARTOMAT_010101_Steel-Bare, etc). Safe to run multiple times — existing aliases are not overwritten.',
},
// Template fields
'template.lighting_only': {
@@ -64,7 +64,7 @@ export const HELP_TEXTS: Record<string, HelpText> = {
},
'template.material_replace_enabled': {
title: 'Material Replacement',
body: 'When enabled, Blender will replace part materials with the mapped Schaeffler library materials. When disabled, the original .blend materials are used.',
body: 'When enabled, Blender will replace part materials with the mapped HartOMat library materials. When disabled, the original .blend materials are used.',
},
// Wizard fields
'wizard.output_type': {
+1 -1
View File
@@ -7,7 +7,7 @@
Applied via data-accent="<key>" on <html>
============================================================ */
/* Default / Schaeffler Green */
/* Default / HartOMat Green */
:root,
[data-accent="green"] {
--color-accent: #00893d;
+1 -1
View File
@@ -12,7 +12,7 @@ import { useThemeStore, applyTheme, resolveTheme, type ThemeMode, type AccentKey
--------------------------------------------------------------- */
;(function () {
try {
const raw = localStorage.getItem('schaeffler-theme')
const raw = localStorage.getItem('hartomat-theme')
if (raw) {
const { state } = JSON.parse(raw) as { state: { mode: ThemeMode; accent: AccentKey; customHex?: string } }
applyTheme(state.mode ?? 'light', state.accent ?? 'green', state.customHex)
+1 -1
View File
@@ -1549,7 +1549,7 @@ export default function AdminPage() {
type="email"
value={smtp.smtp_from_address ?? ''}
onChange={(e) => setSmtpDraft(d => ({ ...d, smtp_from_address: e.target.value }))}
placeholder="noreply@schaeffler.com"
placeholder="noreply@hartomat.com"
className="w-full px-3 py-1.5 rounded-md border border-border-default text-sm bg-surface text-content focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
+1 -1
View File
@@ -55,7 +55,7 @@ function UploadModal({ onClose }: { onClose: () => void }) {
</label>
<input
className="input-base"
placeholder="e.g. Schaeffler Materials v2"
placeholder="e.g. HartOMat Materials v2"
value={name}
onChange={(e) => setName(e.target.value)}
/>
+2 -2
View File
@@ -36,7 +36,7 @@ export default function LoginPage() {
<div className="w-16 h-16 bg-accent rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-white text-2xl font-bold">S</span>
</div>
<h1 className="text-2xl font-bold text-content">Schaeffler Automat</h1>
<h1 className="text-2xl font-bold text-content">HartOMat</h1>
<p className="text-content-muted text-sm mt-1">Media Creation Pipeline</p>
</div>
@@ -49,7 +49,7 @@ export default function LoginPage() {
onChange={(e) => setEmail(e.target.value)}
required
className="input-base w-full"
placeholder="admin@schaeffler.com"
placeholder="admin@hartomat.com"
/>
</div>
<div>
+18 -18
View File
@@ -8,7 +8,7 @@ import {
} from 'lucide-react'
import {
listMaterials, createMaterial, updateMaterial, deleteMaterial,
seedSchaefflerMaterials, addAlias, deleteAlias, seedAliases,
seedHartOMatMaterials, addAlias, deleteAlias, seedAliases,
batchCreateAliases,
} from '../api/materials'
import type { Material } from '../api/materials'
@@ -24,8 +24,8 @@ const TYPE_GROUPS = [
] as const
function getTypeCode(mat: Material): string | null {
if (mat.schaeffler_code == null) return null
return String(mat.schaeffler_code).padStart(6, '0').slice(0, 2)
if (mat.hartomat_code == null) return null
return String(mat.hartomat_code).padStart(6, '0').slice(0, 2)
}
interface MaterialGroup {
@@ -90,12 +90,12 @@ export default function MaterialsPage() {
})
const seedMut = useMutation({
mutationFn: seedSchaefflerMaterials,
mutationFn: seedHartOMatMaterials,
onSuccess: (data) => {
if (data.inserted > 0) {
toast.success(`Imported ${data.inserted} of ${data.total} Schaeffler standard materials`)
toast.success(`Imported ${data.inserted} of ${data.total} HartOMat standard materials`)
} else {
toast.info('All Schaeffler standard materials already exist')
toast.info('All HartOMat standard materials already exist')
}
qc.invalidateQueries({ queryKey: ['materials'] })
},
@@ -147,9 +147,9 @@ export default function MaterialsPage() {
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to create alias'),
})
// Library materials (have schaeffler_code) for quick-map dropdown
// Library materials (have hartomat_code) for quick-map dropdown
const libraryMaterials = useMemo(
() => materials.filter((m) => m.schaeffler_code !== null).sort((a, b) => a.name.localeCompare(b.name)),
() => materials.filter((m) => m.hartomat_code !== null).sort((a, b) => a.name.localeCompare(b.name)),
[materials]
)
@@ -203,7 +203,7 @@ export default function MaterialsPage() {
buckets.delete(tg.code)
}
}
// Custom / non-schaeffler materials
// Custom / non-hartomat materials
const custom = buckets.get(null)
if (custom && custom.length > 0) {
result.push({ code: null, label: 'Custom', icon: Plus, bg: 'bg-surface-alt', border: 'border-border-default', text: 'text-content-secondary', items: custom })
@@ -239,7 +239,7 @@ export default function MaterialsPage() {
setConfirmState({
open: true,
title: 'Import Standard Materials',
message: 'Import 35 Schaeffler standard materials? Existing entries will be skipped.',
message: 'Import 35 HartOMat standard materials? Existing entries will be skipped.',
onConfirm: () => {
seedMut.mutate()
setConfirmState((s) => ({ ...s, open: false }))
@@ -248,7 +248,7 @@ export default function MaterialsPage() {
}}
disabled={seedMut.isPending}
className="btn-secondary text-sm flex items-center gap-1.5"
title="Import the 35 standard Schaeffler SCHAEFFLER_... materials used in Blender material libraries. Existing entries are skipped."
title="Import the 35 standard HartOMat HARTOMAT_... materials used in Blender material libraries. Existing entries are skipped."
>
<Download size={14} /> {seedMut.isPending ? 'Importing...' : 'Import Standards'}
</button>
@@ -266,16 +266,16 @@ export default function MaterialsPage() {
}}
disabled={seedAliasMut.isPending}
className="btn-secondary text-sm flex items-center gap-1.5"
title="Seed ~100 material aliases from the Schaeffler naming scheme (German descriptions, intermediate codes → SCHAEFFLER_... library names). Existing aliases are skipped."
title="Seed ~100 material aliases from the HartOMat naming scheme (German descriptions, intermediate codes → HARTOMAT_... library names). Existing aliases are skipped."
>
<Tag size={14} /> {seedAliasMut.isPending ? 'Seeding...' : 'Seed Aliases'}
</button>
<button
onClick={() => setShowWizard(true)}
className="btn-secondary text-sm flex items-center gap-1.5"
title="Open the Schaeffler Wizard — guided tool to set up SCHAEFFLER_... materials and aliases from the standard naming scheme"
title="Open the HartOMat Wizard — guided tool to set up HARTOMAT_... materials and aliases from the standard naming scheme"
>
<Wand2 size={14} /> Schaeffler Wizard
<Wand2 size={14} /> HartOMat Wizard
</button>
<button onClick={() => setShowAdd(!showAdd)} className="btn-primary">
<Plus size={16} /> Add Material
@@ -407,10 +407,10 @@ export default function MaterialsPage() {
<>
<div className="min-w-0">
<p className="text-sm font-medium text-content truncate">{mat.name}</p>
{mat.schaeffler_code != null && (
<p className="text-xs text-content-muted font-mono">Nr: {mat.schaeffler_code}</p>
{mat.hartomat_code != null && (
<p className="text-xs text-content-muted font-mono">Nr: {mat.hartomat_code}</p>
)}
{mat.schaeffler_code == null && mat.aliases.length === 0 && (
{mat.hartomat_code == null && mat.aliases.length === 0 && (
<div className="flex items-center gap-1.5 mt-1">
<span className="inline-flex items-center gap-1 text-[10px] font-medium text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded">
<AlertTriangle size={10} /> No alias
@@ -614,7 +614,7 @@ function AliasPill({
}
function SourceBadge({ source }: { source: string }) {
if (source === 'schaeffler_standard') {
if (source === 'hartomat_standard') {
return (
<span className="inline-flex items-center gap-1 text-xs font-medium bg-status-success-bg text-status-success-text px-2 py-0.5 rounded-full">
Standard
+3 -3
View File
@@ -81,7 +81,7 @@ export default function NewProductOrderPage() {
queryFn: listMaterials,
enabled: step >= 3,
})
const libMaterials = (allMaterials ?? []).filter((m: Material) => m.schaeffler_code !== null).sort((a: Material, b: Material) => a.name.localeCompare(b.name))
const libMaterials = (allMaterials ?? []).filter((m: Material) => m.hartomat_code !== null).sort((a: Material, b: Material) => a.name.localeCompare(b.name))
function initPositionsForProduct(product: Product, globals: GlobalRenderPosition[] = []) {
// Pre-select all per-product positions (if any)
@@ -822,10 +822,10 @@ export default function NewProductOrderPage() {
value={lineOverrides[line.key] ?? ''}
onChange={(e) => setLineOverrides((prev) => ({ ...prev, [line.key]: e.target.value }))}
>
<option value="">{materialOverride ? `Global: ${materialOverride.replace('SCHAEFFLER_', '').replace(/_/g, ' ')}` : 'No override'}</option>
<option value="">{materialOverride ? `Global: ${materialOverride.replace('HARTOMAT_', '').replace(/_/g, ' ')}` : 'No override'}</option>
{materialOverride && <option value="__none__"> No override (clear) </option>}
{libMaterials.map((m: Material) => (
<option key={m.id} value={m.name}>{m.name.replace('SCHAEFFLER_', '').replace(/_/g, ' ')}</option>
<option key={m.id} value={m.name}>{m.name.replace('HARTOMAT_', '').replace(/_/g, ' ')}</option>
))}
</select>
</td>
+6 -6
View File
@@ -140,7 +140,7 @@ export default function OrderDetailPage() {
}
const { data: matList } = useQuery({ queryKey: ['materials'], queryFn: listMaterials })
const orderLibMats = (matList ?? []).filter((m: Material) => m.schaeffler_code !== null).sort((a: Material, b: Material) => a.name.localeCompare(b.name))
const orderLibMats = (matList ?? []).filter((m: Material) => m.hartomat_code !== null).sort((a: Material, b: Material) => a.name.localeCompare(b.name))
const batchOverrideMut = useMutation({
mutationFn: (val: string | null) => batchMaterialOverride(id!, val),
@@ -660,7 +660,7 @@ export default function OrderDetailPage() {
<option value="">Apply to all lines</option>
<option value="__clear__"> Clear all overrides </option>
{orderLibMats.map((m: Material) => (
<option key={m.id} value={m.name}>{m.name.replace('SCHAEFFLER_', '').replace(/_/g, ' ')}</option>
<option key={m.id} value={m.name}>{m.name.replace('HARTOMAT_', '').replace(/_/g, ' ')}</option>
))}
</select>
{batchOverrideMut.isPending && <Loader2 size={14} className="animate-spin text-accent" />}
@@ -1017,7 +1017,7 @@ function OrderLineRow({
})
const { data: allMats } = useQuery({ queryKey: ['materials'], queryFn: listMaterials })
const libMats = (allMats ?? []).filter((m: Material) => m.schaeffler_code !== null).sort((a: Material, b: Material) => a.name.localeCompare(b.name))
const libMats = (allMats ?? []).filter((m: Material) => m.hartomat_code !== null).sort((a: Material, b: Material) => a.name.localeCompare(b.name))
const overrideMut = useMutation({
mutationFn: (val: string | null) => patchOrderLine(orderId, line.id, { material_override: val }),
@@ -1112,7 +1112,7 @@ function OrderLineRow({
onClick={() => setShowOverride(!showOverride)}
title="Click to change material override"
>
{line.material_override.replace('SCHAEFFLER_', '').replace(/_/g, ' ')}
{line.material_override.replace('HARTOMAT_', '').replace(/_/g, ' ')}
</span>
<button
onClick={() => overrideMut.mutate(null)}
@@ -1131,7 +1131,7 @@ function OrderLineRow({
>
<option value="">No material override</option>
{libMats.map((m: Material) => (
<option key={m.id} value={m.name}>{m.name.replace('SCHAEFFLER_', '').replace(/_/g, ' ')}</option>
<option key={m.id} value={m.name}>{m.name.replace('HARTOMAT_', '').replace(/_/g, ' ')}</option>
))}
</select>
)}
@@ -1147,7 +1147,7 @@ function OrderLineRow({
>
<option value="">No material override</option>
{libMats.map((m: Material) => (
<option key={m.id} value={m.name}>{m.name.replace('SCHAEFFLER_', '').replace(/_/g, ' ')}</option>
<option key={m.id} value={m.name}>{m.name.replace('HARTOMAT_', '').replace(/_/g, ' ')}</option>
))}
</select>
) : (
+1 -1
View File
@@ -14,7 +14,7 @@ export default function PreferencesPage() {
return (
<div className="p-8 max-w-2xl">
<h1 className="text-2xl font-bold text-content mb-1">Preferences</h1>
<p className="text-sm text-content-muted mb-8">Customize your Schaeffler Automat experience.</p>
<p className="text-sm text-content-muted mb-8">Customize your HartOMat experience.</p>
{/* Appearance */}
<section className="card p-6 space-y-6">
+4 -4
View File
@@ -11,7 +11,7 @@ import {
} from '../api/tenants'
import type { Tenant, TenantCreate, TenantUpdate, TenantAIConfig, TenantAIConfigUpdate } from '../api/tenants'
const TENANT_CONTEXT_KEY = 'schaeffler_tenant_id'
const TENANT_CONTEXT_KEY = 'hartomat_tenant_id'
function slugify(name: string): string {
return name
@@ -392,7 +392,7 @@ export default function TenantsPage() {
type="text"
value={createForm.name}
onChange={(e) => handleCreateNameChange(e.target.value)}
placeholder="e.g. Schaeffler GmbH"
placeholder="e.g. HartOMat GmbH"
className="w-full px-3 py-2 rounded-md border border-border-default bg-surface text-content text-sm focus:outline-none focus:ring-2 focus:ring-accent/50"
/>
</div>
@@ -406,7 +406,7 @@ export default function TenantsPage() {
type="text"
value={createForm.slug}
onChange={(e) => handleCreateSlugChange(e.target.value)}
placeholder="e.g. schaeffler-gmbh"
placeholder="e.g. hartomat-gmbh"
className="w-full px-3 py-2 rounded-md border border-border-default bg-surface text-content text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
/>
</div>
@@ -677,7 +677,7 @@ export default function TenantsPage() {
rows={4}
value={aiForm.ai_validation_prompt ?? ''}
onChange={(e) => setAIForm((prev) => ({ ...prev, ai_validation_prompt: e.target.value || null }))}
placeholder="Optional: override the default Schaeffler bearing analysis prompt"
placeholder="Optional: override the default HartOMat bearing analysis prompt"
className="w-full px-3 py-2 rounded-md border border-border-default bg-surface text-content text-sm resize-y focus:outline-none focus:ring-2 focus:ring-accent/50"
/>
</div>
+1 -1
View File
@@ -32,6 +32,6 @@ export const useAuthStore = create<AuthState>()(
setAuth: (token, user) => set({ token, user }),
logout: () => set({ token: null, user: null }),
}),
{ name: 'schaeffler-auth' },
{ name: 'hartomat-auth' },
),
)
+2 -2
View File
@@ -5,7 +5,7 @@ export type ThemeMode = 'light' | 'dark' | 'system'
export type AccentKey = 'green' | 'blue' | 'purple' | 'amber' | 'teal' | 'custom'
export const ACCENT_PRESETS: { key: AccentKey; label: string; hex: string }[] = [
{ key: 'green', label: 'Schaeffler Green', hex: '#00893d' },
{ key: 'green', label: 'HartOMat Green', hex: '#00893d' },
{ key: 'blue', label: 'Blue', hex: '#2563eb' },
{ key: 'purple', label: 'Purple', hex: '#7c3aed' },
{ key: 'amber', label: 'Amber', hex: '#d97706' },
@@ -115,6 +115,6 @@ export const useThemeStore = create<ThemeState>()(
applyTheme(get().mode, 'custom', hex)
},
}),
{ name: 'schaeffler-theme' },
{ name: 'hartomat-theme' },
),
)
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Schaeffler Automat MCP Server.
"""HartOMat MCP Server.
Exposes the render pipeline, product library, material system, and order
management as MCP tools for Claude Code.
@@ -8,7 +8,7 @@ Requirements (install once):
uv pip install "mcp[cli]" psycopg2-binary httpx
Register in Claude Code:
claude mcp add schaeffler -- python schaeffler_mcp_server.py
claude mcp add hartomat -- python hartomat_mcp_server.py
"""
import json
import os
@@ -23,18 +23,18 @@ from mcp.server.fastmcp import FastMCP
DB_URL = os.environ.get(
"DATABASE_URL",
"postgresql://schaeffler:schaeffler@localhost:5432/schaeffler",
"postgresql://hartomat:hartomat@localhost:5432/hartomat",
)
API_URL = os.environ.get("API_URL", "http://localhost:8888")
API_EMAIL = os.environ.get("API_EMAIL", "admin@schaeffler.com")
API_EMAIL = os.environ.get("API_EMAIL", "admin@hartomat.com")
API_PASSWORD = os.environ.get("API_PASSWORD", "Admin1234!")
# ── Server setup ─────────────────────────────────────────────────────────────
mcp = FastMCP(
"Schaeffler Automat",
"HartOMat",
instructions=(
"MCP server for the Schaeffler Automat render pipeline. "
"MCP server for the HartOMat render pipeline. "
"Provides tools to query orders, products, materials, render status, "
"worker health, and run read-only SQL against the PostgreSQL database."
),
@@ -112,7 +112,7 @@ def _api_post(path: str, body: dict | None = None) -> dict | list:
@mcp.tool()
def query_database(sql: str) -> str:
"""Execute a read-only SQL query against the Schaeffler PostgreSQL database.
"""Execute a read-only SQL query against the HartOMat PostgreSQL database.
Only SELECT queries are allowed. The database contains tables for orders,
order_lines, products, cad_files, materials, material_aliases,
@@ -285,7 +285,7 @@ def set_material_override(order_id: str, material_name: str = "") -> str:
Args:
order_id: UUID of the order.
material_name: SCHAEFFLER library material name, or empty to clear.
material_name: HARTOMAT library material name, or empty to clear.
"""
data = _api_post(
f"/api/orders/{order_id}/batch-material-override",
@@ -314,7 +314,7 @@ def create_order(
output_type_id: UUID of the output type (takes priority over name).
output_type_name: Name of the output type (used if output_type_id is empty).
render_overrides: Optional dict of render setting overrides (e.g. {"output_format": "webp", "width": 2048, "height": 2048}).
material_override: Optional SCHAEFFLER library material name to apply to all lines.
material_override: Optional HARTOMAT library material name to apply to all lines.
notes: Optional notes for the order.
"""
# Resolve output_type_id from name if needed
@@ -609,7 +609,7 @@ def get_failed_renders(limit: int = 20) -> str:
# ── Resources ────────────────────────────────────────────────────────────────
@mcp.resource("schaeffler://schema")
@mcp.resource("hartomat://schema")
def get_database_schema() -> str:
"""Database schema overview — table names and column types."""
rows = _db_query("""
@@ -634,7 +634,7 @@ def get_database_schema() -> str:
return "\n".join(lines)
@mcp.resource("schaeffler://output-types")
@mcp.resource("hartomat://output-types")
def get_output_types_resource() -> str:
"""All configured output types with settings."""
data = _api_get("/api/output-types?include_inactive=true")
+1 -1
View File
@@ -89,7 +89,7 @@ def import_usd_file(usd_path: str) -> tuple[list, dict]:
"""Import USD stage into current Blender scene — delegates to import_usd module.
Returns (parts, material_lookup) where material_lookup maps
blender_object_name canonical SCHAEFFLER material name (from USD primvars).
blender_object_name canonical HARTOMAT material name (from USD primvars).
"""
from import_usd import import_usd_file as _impl
result = _impl(usd_path)
+3 -3
View File
@@ -5,7 +5,7 @@ import os
import re as _re
import time as _time
FAILED_MATERIAL_NAME = "SCHAEFFLER_059999_FailedMaterial"
FAILED_MATERIAL_NAME = "HARTOMAT_059999_FailedMaterial"
def _find_material_with_nodes(base_name: str):
@@ -100,7 +100,7 @@ def _batch_append_materials(mat_lib_path: str, names: set[str]) -> dict:
def assign_failed_material(part_obj) -> None:
"""Assign the standard fallback material (magenta) when no library material matches.
Reuses SCHAEFFLER_059999_FailedMaterial if already loaded; otherwise
Reuses HARTOMAT_059999_FailedMaterial if already loaded; otherwise
creates a simple magenta Principled BSDF node tree.
"""
import bpy # type: ignore[import]
@@ -157,7 +157,7 @@ def apply_material_library_direct(
"""Assign materials from library using a direct object_name → material_name mapping.
This bypasses all name-matching heuristics the mapping comes from USD
customData (schaeffler:canonicalMaterialName) read via pxr after Blender import.
customData (hartomat:canonicalMaterialName) read via pxr after Blender import.
Parts not present in material_lookup receive FAILED_MATERIAL_NAME.
material_lookup: {blender_object_name: canonical_material_name}
@@ -83,7 +83,7 @@ def _setup_mode_b(args, lap_fn: Callable[[str], None]) -> None:
_unassigned = [p for p in parts if not p.data.materials or
(len(p.data.materials) == 1 and
p.data.materials[0] and
p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")]
p.data.materials[0].name == "HARTOMAT_059999_FailedMaterial")]
if _unassigned:
print(f"[blender_render] {len(_unassigned)} parts without USD primvar — "
f"falling back to name-matching", flush=True)
@@ -162,7 +162,7 @@ def _setup_mode_a(args) -> None:
_unassigned = [p for p in parts if not p.data.materials or
(len(p.data.materials) == 1 and
p.data.materials[0] and
p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")]
p.data.materials[0].name == "HARTOMAT_059999_FailedMaterial")]
if _unassigned:
apply_material_library(
_unassigned, args.material_library_path,
+1 -1
View File
@@ -19,7 +19,7 @@ def apply_asset_library_materials(blend_path: str, material_map: dict, link: boo
Args:
blend_path: Absolute path to the .blend library file.
material_map: Mapping of current slot material name -> library material name.
E.g. {"Steel--Stahl": "SCHAEFFLER_010101_Steel-Bare"}
E.g. {"Steel--Stahl": "HARTOMAT_010101_Steel-Bare"}
link: If True (default), link materials (external reference, good for rendering).
If False, append materials (local copy required for GLB/GLTF export so
that the exporter can traverse Principled BSDF node trees for PBR values).
+2 -2
View File
@@ -577,7 +577,7 @@ def main():
if material_map:
_unassigned = [p for p in parts if not p.data.materials or
(len(p.data.materials) == 1 and p.data.materials[0] and
p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")]
p.data.materials[0].name == "HARTOMAT_059999_FailedMaterial")]
if _unassigned:
print(f"[cinematic_render] {len(_unassigned)} parts without USD primvar -- "
f"falling back to name-matching", flush=True)
@@ -654,7 +654,7 @@ def main():
if material_map:
_unassigned = [p for p in parts if not p.data.materials or
(len(p.data.materials) == 1 and p.data.materials[0] and
p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")]
p.data.materials[0].name == "HARTOMAT_059999_FailedMaterial")]
if _unassigned:
_apply_material_library_shared(
_unassigned, material_library_path,
+2 -2
View File
@@ -754,8 +754,8 @@ def main() -> None:
try:
extras_payload: dict = {}
if sharp_pairs:
extras_payload["schaeffler_sharp_edge_pairs"] = sharp_pairs
extras_payload["schaeffler_sharp_threshold_deg"] = args.sharp_threshold
extras_payload["hartomat_sharp_edge_pairs"] = sharp_pairs
extras_payload["hartomat_sharp_threshold_deg"] = args.sharp_threshold
if part_key_map:
extras_payload["partKeyMap"] = part_key_map
if extras_payload:
+16 -16
View File
@@ -1,4 +1,4 @@
"""STEP → USD exporter for Schaeffler Automat.
"""STEP → USD exporter for HartOMat.
Reads a STEP file via OCP/XCAF (preserving part names + embedded colors),
tessellates with BRepMesh, builds a USD stage mirroring the full XCAF
@@ -18,7 +18,7 @@ Usage:
[--color_map '{"Ring": "#4C9BE8"}'] \\
[--sharp_threshold 20.0] \\
[--cad_file_id uuid] \\
[--material_map '{"part_name": "SCHAEFFLER_010101_Steel-Bare", ...}']
[--material_map '{"part_name": "HARTOMAT_010101_Steel-Bare", ...}']
Exit 0 on success, exit 1 on failure.
Prints MANIFEST_JSON: {...} to stdout before exit.
@@ -418,8 +418,8 @@ def _author_xcaf_to_usd(
_occ_trsf_to_usd_matrix(local_loc.Transformation()))
prim = xform.GetPrim()
prim.SetCustomDataByKey("schaeffler:sourceName", source_name)
prim.SetCustomDataByKey("schaeffler:sourceAssemblyPath", xcaf_path)
prim.SetCustomDataByKey("hartomat:sourceName", source_name)
prim.SetCustomDataByKey("hartomat:sourceAssemblyPath", xcaf_path)
print(f" {' ' * depth}[asm] {source_name}{xform_path}"
f"{' (transform)' if has_local_trsf else ''}")
@@ -484,16 +484,16 @@ def _author_xcaf_to_usd(
_occ_trsf_to_usd_matrix(local_loc.Transformation()))
prim = xform.GetPrim()
prim.SetCustomDataByKey("schaeffler:partKey", part_key)
prim.SetCustomDataByKey("schaeffler:sourceName", source_name)
prim.SetCustomDataByKey("schaeffler:sourceAssemblyPath", xcaf_path)
prim.SetCustomDataByKey("schaeffler:sourceColor", hex_color)
prim.SetCustomDataByKey("schaeffler:tessellation:linearDeflectionMm",
prim.SetCustomDataByKey("hartomat:partKey", part_key)
prim.SetCustomDataByKey("hartomat:sourceName", source_name)
prim.SetCustomDataByKey("hartomat:sourceAssemblyPath", xcaf_path)
prim.SetCustomDataByKey("hartomat:sourceColor", hex_color)
prim.SetCustomDataByKey("hartomat:tessellation:linearDeflectionMm",
args.linear_deflection)
prim.SetCustomDataByKey("schaeffler:tessellation:angularDeflectionRad",
prim.SetCustomDataByKey("hartomat:tessellation:angularDeflectionRad",
args.angular_deflection)
if args.cad_file_id:
prim.SetCustomDataByKey("schaeffler:cadFileId", args.cad_file_id)
prim.SetCustomDataByKey("hartomat:cadFileId", args.cad_file_id)
# ── UsdGeomMesh ────────────────────────────────────────────
mesh = UsdGeom.Mesh.Define(stage, mesh_path)
@@ -525,13 +525,13 @@ def _author_xcaf_to_usd(
# ── Material metadata on mesh prim (customData) ───────────
mesh_prim = mesh.GetPrim()
mesh_prim.SetCustomDataByKey("schaeffler:partKey", part_key)
mesh_prim.SetCustomDataByKey("schaeffler:sourceName", source_name)
mesh_prim.SetCustomDataByKey("hartomat:partKey", part_key)
mesh_prim.SetCustomDataByKey("hartomat:sourceName", source_name)
canonical_mat = _lookup_material(source_name, part_key, mat_map_lower)
if canonical_mat:
mesh_prim.SetCustomDataByKey(
"schaeffler:canonicalMaterialName", canonical_mat)
"hartomat:canonicalMaterialName", canonical_mat)
primvars_api = UsdGeom.PrimvarsAPI(mesh)
@@ -542,7 +542,7 @@ def _author_xcaf_to_usd(
idx_pairs = _world_to_index_pairs(vertices, sharp_pairs)
if idx_pairs:
pv = primvars_api.CreatePrimvar(
"schaeffler:sharpEdgeVertexPairs",
"hartomat:sharpEdgeVertexPairs",
Sdf.ValueTypeNames.Int2Array,
UsdGeom.Tokens.constant,
)
@@ -556,7 +556,7 @@ def _author_xcaf_to_usd(
seam_idx_pairs = _world_to_index_pairs(vertices, seam_pairs)
if seam_idx_pairs:
pv_seam = primvars_api.CreatePrimvar(
"schaeffler:seamEdgeVertexPairs",
"hartomat:seamEdgeVertexPairs",
Sdf.ValueTypeNames.Int2Array,
UsdGeom.Tokens.constant,
)
+9 -9
View File
@@ -2,7 +2,7 @@
Runs inside Blender's Python environment (bpy available).
Imports a USD stage and restores seam + sharp edges from
schaeffler:*EdgeVertexPairs primvars. Blender's built-in USD importer does
hartomat:*EdgeVertexPairs primvars. Blender's built-in USD importer does
NOT map arbitrary custom primvars (constant Int2Array) to mesh attributes,
so we read them directly via the pxr module and apply via bmesh.
@@ -22,7 +22,7 @@ def import_usd_file(usd_path: str) -> list | tuple:
Returns a tuple of (parts, material_lookup) where:
- parts: list of imported mesh objects, centred at world origin
- material_lookup: dict mapping blender_object_name canonical_material_name
(populated from schaeffler:canonicalMaterialName customData, empty dict if absent)
(populated from hartomat:canonicalMaterialName customData, empty dict if absent)
USD stage is mm Y-up with metersPerUnit=0.001 Blender scales to metres.
"""
@@ -49,20 +49,20 @@ def import_usd_file(usd_path: str) -> list | tuple:
for prim in stage.Traverse():
if prim.GetTypeName() != "Mesh":
continue
part_key = prim.GetCustomDataByKey("schaeffler:partKey") or ""
mat_name = prim.GetCustomDataByKey("schaeffler:canonicalMaterialName") or ""
part_key = prim.GetCustomDataByKey("hartomat:partKey") or ""
mat_name = prim.GetCustomDataByKey("hartomat:canonicalMaterialName") or ""
if not part_key or not mat_name:
parent = prim.GetParent()
if parent:
part_key = part_key or (parent.GetCustomDataByKey("schaeffler:partKey") or "")
mat_name = mat_name or (parent.GetCustomDataByKey("schaeffler:canonicalMaterialName") or "")
part_key = part_key or (parent.GetCustomDataByKey("hartomat:partKey") or "")
mat_name = mat_name or (parent.GetCustomDataByKey("hartomat:canonicalMaterialName") or "")
if part_key and mat_name:
material_lookup[part_key] = mat_name
# Read seam/sharp primvars from USD mesh prim
pvs_api = UsdGeom.PrimvarsAPI(prim)
sharp_pv = pvs_api.GetPrimvar("schaeffler:sharpEdgeVertexPairs")
seam_pv = pvs_api.GetPrimvar("schaeffler:seamEdgeVertexPairs")
sharp_pv = pvs_api.GetPrimvar("hartomat:sharpEdgeVertexPairs")
seam_pv = pvs_api.GetPrimvar("hartomat:seamEdgeVertexPairs")
sharp_list = []
seam_list = []
if sharp_pv and sharp_pv.HasValue():
@@ -83,7 +83,7 @@ def import_usd_file(usd_path: str) -> list | tuple:
print(f"[import_usd] pxr material lookup: {len(material_lookup)}/{len(parts)} parts",
flush=True)
else:
print("[import_usd] no schaeffler:canonicalMaterialName metadata found (legacy USD)",
print("[import_usd] no hartomat:canonicalMaterialName metadata found (legacy USD)",
flush=True)
if edge_data:
+1 -1
View File
@@ -345,7 +345,7 @@ def main():
part_colors_json = args[6] if len(args) > 6 else "{}"
transparent_bg = args[7] == "1" if len(args) > 7 else False
# Template + material library args (passed by schaeffler-still.js)
# Template + material library args (passed by hartomat-still.js)
template_path = args[8] if len(args) > 8 and args[8] else ""
target_collection = args[9] if len(args) > 9 else "Product"
material_library_path = args[10] if len(args) > 10 and args[10] else ""
+3 -3
View File
@@ -317,7 +317,7 @@ def main():
samples = int(args[7])
part_colors_json = args[8] if len(args) > 8 else "{}"
# Template + material library args (passed by schaeffler-turntable.js)
# Template + material library args (passed by hartomat-turntable.js)
template_path = args[9] if len(args) > 9 and args[9] else ""
target_collection = args[10] if len(args) > 10 else "Product"
material_library_path = args[11] if len(args) > 11 and args[11] else ""
@@ -468,7 +468,7 @@ def main():
if material_map:
_unassigned = [p for p in parts if not p.data.materials or
(len(p.data.materials) == 1 and p.data.materials[0] and
p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")]
p.data.materials[0].name == "HARTOMAT_059999_FailedMaterial")]
if _unassigned:
print(f"[turntable_render] {len(_unassigned)} parts without USD primvar — "
f"falling back to name-matching", flush=True)
@@ -559,7 +559,7 @@ def main():
if material_map:
_unassigned = [p for p in parts if not p.data.materials or
(len(p.data.materials) == 1 and p.data.materials[0] and
p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")]
p.data.materials[0].name == "HARTOMAT_059999_FailedMaterial")]
if _unassigned:
_apply_material_library_shared(
_unassigned, material_library_path,
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
cd "$(dirname "$0")/.."
echo "Starting Schaeffler Automat..."
echo "Starting HartOMat..."
docker compose up -d
echo ""
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
cd "$(dirname "$0")/.."
echo "Stopping Schaeffler Automat..."
echo "Stopping HartOMat..."
docker compose down
echo ""
+2 -2
View File
@@ -16,7 +16,7 @@ Usage:
# Custom credentials / host
python scripts/test_render_pipeline.py --sample --host http://localhost:8888 \
--email admin@schaeffler.com --password Admin1234!
--email admin@hartomat.com --password Admin1234!
Environment variables (alternative to flags):
TEST_HOST, TEST_EMAIL, TEST_PASSWORD
@@ -34,7 +34,7 @@ from pathlib import Path
# ---------------------------------------------------------------------------
DEFAULT_HOST = os.environ.get("TEST_HOST", "http://localhost:8888")
DEFAULT_EMAIL = os.environ.get("TEST_EMAIL", "admin@schaeffler.com")
DEFAULT_EMAIL = os.environ.get("TEST_EMAIL", "admin@hartomat.com")
DEFAULT_PASSWORD = os.environ.get("TEST_PASSWORD", "Admin1234!")
SAMPLE_STEP = Path(__file__).parent.parent / "step-sample-file" / "81113-l_cut.stp"
+5 -5
View File
@@ -1,6 +1,6 @@
"""Blender companion script: restore sharp + seam edge marks after importing a production GLB.
After importing a Schaeffler production GLB in Blender, run this script once via
After importing a HartOMat production GLB in Blender, run this script once via
the Scripting workspace (Text Editor Run Script). It reads the sharp angle AND the
OCC B-rep sharp edge pairs baked into the GLB at export time, and re-applies
mark_sharp() + mark_seam() on every mesh object.
@@ -26,7 +26,7 @@ if not mesh_objects:
print("No mesh objects found in scene.")
else:
# --- Pass 1: dihedral-angle-based sharp/seam marks ---
angle_deg = bpy.context.scene.get("schaeffler_sharp_angle_deg", 30.0)
angle_deg = bpy.context.scene.get("hartomat_sharp_angle_deg", 30.0)
smooth_rad = math.radians(float(angle_deg))
total_sharp = 0
@@ -48,7 +48,7 @@ else:
print(f"Pass 1 (dihedral {angle_deg}°): {total_sharp} sharp/seam edges across {len(mesh_objects)} objects.")
# --- Pass 2: OCC B-rep sharp edges from GLB extras ---
# The production GLB embeds schaeffler_sharp_edge_pairs (OCC B-rep topology,
# The production GLB embeds hartomat_sharp_edge_pairs (OCC B-rep topology,
# dense curve samples at 0.3mm) in scenes[0].extras, which Blender maps to
# scene custom properties on import. These cover geometrically sharp edges
# that the dihedral-angle pass misses due to tessellation noise.
@@ -56,7 +56,7 @@ else:
# Coordinate convention (mirrors export_gltf.py _apply_sharp_edges_from_occ):
# OCC STEP space (Z-up, mm) → Blender (Z-up, m):
# Blender(X, Y, Z) = OCC(X*0.001, -Z*0.001, Y*0.001)
occ_pairs = bpy.context.scene.get("schaeffler_sharp_edge_pairs") or []
occ_pairs = bpy.context.scene.get("hartomat_sharp_edge_pairs") or []
if occ_pairs:
print(f"Pass 2 (OCC B-rep): applying {len(occ_pairs)} sharp edge segment pairs...")
@@ -99,4 +99,4 @@ else:
print(f"Pass 2 (OCC B-rep): {marked_total} additional edges marked across {len(mesh_objects)} objects.")
else:
print("Pass 2 (OCC B-rep): no schaeffler_sharp_edge_pairs in GLB extras — skipped.")
print("Pass 2 (OCC B-rep): no hartomat_sharp_edge_pairs in GLB extras — skipped.")
+2 -2
View File
@@ -1,4 +1,4 @@
# Schaeffler Automat — UX & Quality Audit Report
# HartOMat — UX & Quality Audit Report
**Date**: 2026-03-08
**Overall Score**: 6.5/10
@@ -6,7 +6,7 @@
## Executive Summary
Schaeffler Automat is a functionally complete internal tool with a solid design system foundation (semantic CSS variables, Tailwind tokens, dark/light theming, role-aware navigation). The core workflows — Excel import → STEP upload → render dispatch — are implemented end-to-end. However, the UI suffers from **information density without hierarchy**: the Admin page is an undifferentiated ~3000px scroll, the Upload wizard has no progress indicator, and the Activity page mixes live queue data with historical records without clear separation. The most impactful improvements are a redesigned Admin hub with tab navigation, an Upload wizard progress bar, and standardized interaction patterns (replacing `window.confirm()`, adding debounce to search, and skeleton loading states).
HartOMat is a functionally complete internal tool with a solid design system foundation (semantic CSS variables, Tailwind tokens, dark/light theming, role-aware navigation). The core workflows — Excel import → STEP upload → render dispatch — are implemented end-to-end. However, the UI suffers from **information density without hierarchy**: the Admin page is an undifferentiated ~3000px scroll, the Upload wizard has no progress indicator, and the Activity page mixes live queue data with historical records without clear separation. The most impactful improvements are a redesigned Admin hub with tab navigation, an Upload wizard progress bar, and standardized interaction patterns (replacing `window.confirm()`, adding debounce to search, and skeleton loading states).
---