refactor: rebrand project to HartOMat
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 0–4 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,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,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
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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])
|
||||
"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
"
|
||||
```
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
|
||||
+8
-8
@@ -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")
|
||||
@@ -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)
|
||||
""")
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'"))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"},
|
||||
]
|
||||
@@ -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",
|
||||
|
||||
@@ -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"},
|
||||
]
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
{
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"].
|
||||
|
||||
@@ -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=[
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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" />
|
||||
|
||||
Generated
+2
-2
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"name": "schaefflerautomat-frontend",
|
||||
"name": "hartomat-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -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 |
@@ -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,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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
Applied via data-accent="<key>" on <html>
|
||||
============================================================ */
|
||||
|
||||
/* Default / Schaeffler Green */
|
||||
/* Default / HartOMat Green */
|
||||
:root,
|
||||
[data-accent="green"] {
|
||||
--color-accent: #00893d;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "Starting Schaeffler Automat..."
|
||||
echo "Starting HartOMat..."
|
||||
docker compose up -d
|
||||
|
||||
echo ""
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "Stopping Schaeffler Automat..."
|
||||
echo "Stopping HartOMat..."
|
||||
docker compose down
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user