bolajiev commited on
Commit
e0e4588
·
1 Parent(s): 82b7ebe

Sprint 2: material presets via MeshPhysicalMaterial

Browse files

Add optional `preset` field (gold/chrome/glass/neon/matte/plastic) to Obj and
ExtrudeNode. Preset routes through _preset_mat_js() in realistic style; wireframe/
toon/flat style overrides still win. gold/chrome use clearcoat for polished metal,
glass uses physical transmission+ior, neon maps node.color → emissiveIntensity:2.
Existing scenes with no preset are unchanged. Add 3 FEWSHOT examples and update
SYSTEM prompt with preset field documentation.

Files changed (3) hide show
  1. compiler.py +49 -0
  2. llm.py +54 -1
  3. scene.py +19 -0
compiler.py CHANGED
@@ -69,6 +69,51 @@ def geometry_js(o: Obj) -> str:
69
  return "new THREE.BoxGeometry(1, 1, 1)"
70
 
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  def material_js(o: Obj, style: str = "realistic") -> str:
73
  col = _col_str(o.color)
74
  emi = _col_str(o.emissive)
@@ -79,6 +124,10 @@ def material_js(o: Obj, style: str = "realistic") -> str:
79
  if style == "flat":
80
  return (f"new THREE.MeshStandardMaterial({{ color: {col}, emissive: {emi}, "
81
  f"metalness: {o.metalness}, roughness: {o.roughness}, flatShading: true }})")
 
 
 
 
82
  # realistic — honour per-object material field
83
  if o.material == "basic":
84
  return f"new THREE.MeshBasicMaterial({{ color: {col} }})"
 
69
  return "new THREE.BoxGeometry(1, 1, 1)"
70
 
71
 
72
+ def _preset_mat_js(node) -> str | None:
73
+ """Return a JS material string for a named preset, or None if no preset is set.
74
+
75
+ color override: if node.color differs from the default (#88ccff), use it;
76
+ otherwise fall back to each preset's canonical color so it looks right out of the box.
77
+ For neon, node.color is the emissive glow color regardless.
78
+ """
79
+ preset = getattr(node, "preset", None)
80
+ if not preset:
81
+ return None
82
+ col = _col_str(node.color)
83
+ has_custom = node.color != "#88ccff"
84
+
85
+ if preset == "gold":
86
+ c = col if has_custom else _col_str("#ffd700")
87
+ return (f"new THREE.MeshPhysicalMaterial({{ color: {c}, "
88
+ f"metalness: 1.0, roughness: 0.25, clearcoat: 1.0, clearcoatRoughness: 0.1 }})")
89
+
90
+ if preset == "chrome":
91
+ c = col if has_custom else _col_str("#ffffff")
92
+ return (f"new THREE.MeshPhysicalMaterial({{ color: {c}, "
93
+ f"metalness: 1.0, roughness: 0.05, clearcoat: 1.0, clearcoatRoughness: 0.05 }})")
94
+
95
+ if preset == "glass":
96
+ return (f"new THREE.MeshPhysicalMaterial({{ color: {col}, "
97
+ f"metalness: 0.0, roughness: 0.05, "
98
+ f"transmission: 1.0, thickness: 0.5, ior: 1.5, "
99
+ f"transparent: true, opacity: 1.0 }})")
100
+
101
+ if preset == "neon":
102
+ return (f"new THREE.MeshStandardMaterial({{ "
103
+ f"color: new THREE.Color('#000000'), "
104
+ f"emissive: {col}, emissiveIntensity: 2.0 }})")
105
+
106
+ if preset == "matte":
107
+ return (f"new THREE.MeshStandardMaterial({{ color: {col}, "
108
+ f"metalness: 0.0, roughness: 0.9 }})")
109
+
110
+ if preset == "plastic":
111
+ return (f"new THREE.MeshPhysicalMaterial({{ color: {col}, "
112
+ f"metalness: 0.0, roughness: 0.4, clearcoat: 0.5, clearcoatRoughness: 0.3 }})")
113
+
114
+ return None
115
+
116
+
117
  def material_js(o: Obj, style: str = "realistic") -> str:
118
  col = _col_str(o.color)
119
  emi = _col_str(o.emissive)
 
124
  if style == "flat":
125
  return (f"new THREE.MeshStandardMaterial({{ color: {col}, emissive: {emi}, "
126
  f"metalness: {o.metalness}, roughness: {o.roughness}, flatShading: true }})")
127
+ # Preset overrides per-object material field in realistic mode
128
+ preset_mat = _preset_mat_js(o)
129
+ if preset_mat:
130
+ return preset_mat
131
  # realistic — honour per-object material field
132
  if o.material == "basic":
133
  return f"new THREE.MeshBasicMaterial({{ color: {col} }})"
llm.py CHANGED
@@ -35,6 +35,7 @@ Schema — primitive object:
35
  "scale": [x, y, z],
36
  "color": "#RRGGBB",
37
  "material": "standard|basic|phong|wireframe",
 
38
  "metalness": 0.0-1.0,
39
  "roughness": 0.0-1.0,
40
  "emissive": "#RRGGBB",
@@ -54,13 +55,21 @@ For stars, badges, shields, hearts, hexagons, coins, logos, or emblems use an EX
54
  "depth": 0.2,
55
  "bevel": true,
56
  "color": "#RRGGBB",
57
- "material": "standard",
58
  "metalness": 0.0-1.0,
59
  "roughness": 0.0-1.0,
60
  "emissive": "#RRGGBB",
61
  "position": [x, y, z]
62
  }
63
 
 
 
 
 
 
 
 
 
64
  Keep scenes small (1-4 objects). Center the composition near the origin."""
65
 
66
  FEWSHOT = [
@@ -211,6 +220,50 @@ FEWSHOT = [
211
  ],
212
  "animation": {"type": "rotate", "speed": 0.5, "axis": "y"},
213
  })},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  ]
215
 
216
  _tok = None
 
35
  "scale": [x, y, z],
36
  "color": "#RRGGBB",
37
  "material": "standard|basic|phong|wireframe",
38
+ "preset": "gold|chrome|glass|neon|matte|plastic",
39
  "metalness": 0.0-1.0,
40
  "roughness": 0.0-1.0,
41
  "emissive": "#RRGGBB",
 
55
  "depth": 0.2,
56
  "bevel": true,
57
  "color": "#RRGGBB",
58
+ "preset": "gold|chrome|glass|neon|matte|plastic",
59
  "metalness": 0.0-1.0,
60
  "roughness": 0.0-1.0,
61
  "emissive": "#RRGGBB",
62
  "position": [x, y, z]
63
  }
64
 
65
+ Material preset notes:
66
+ - gold/chrome: use for shiny metallic looks; color overrides the base hue
67
+ - glass: transparent refractive; color adds a tint
68
+ - neon: glowing edge; set color to the desired glow color
69
+ - matte: no shine, soft diffuse
70
+ - plastic: slight sheen, clearcoat
71
+ Omit preset to use metalness/roughness directly.
72
+
73
  Keep scenes small (1-4 objects). Center the composition near the origin."""
74
 
75
  FEWSHOT = [
 
220
  ],
221
  "animation": {"type": "rotate", "speed": 0.5, "axis": "y"},
222
  })},
223
+ {"role": "user", "content": "a glass star badge"},
224
+ {"role": "assistant", "content": json.dumps({
225
+ "background": "#0a0a18",
226
+ "objects": [{
227
+ "type": "extrude", "shape": "star", "depth": 0.25, "bevel": True,
228
+ "color": "#aaddff", "preset": "glass",
229
+ "position": [0, 0, 0],
230
+ }],
231
+ "lights": [
232
+ {"type": "ambient", "intensity": 0.6},
233
+ {"type": "directional", "color": "#ffffff", "intensity": 2.0, "position": [5, 8, 4]},
234
+ {"type": "point", "color": "#88ccff", "intensity": 1.5, "position": [-3, 2, 3]},
235
+ ],
236
+ "animation": {"type": "rotate", "speed": 0.5, "axis": "y"},
237
+ })},
238
+ {"role": "user", "content": "a neon glowing shield"},
239
+ {"role": "assistant", "content": json.dumps({
240
+ "background": "#050508",
241
+ "objects": [{
242
+ "type": "extrude", "shape": "shield", "depth": 0.2, "bevel": True,
243
+ "color": "#ff2266", "preset": "neon",
244
+ "position": [0, 0, 0],
245
+ }],
246
+ "lights": [
247
+ {"type": "ambient", "intensity": 0.1},
248
+ {"type": "point", "color": "#ff2266", "intensity": 3.0, "position": [0, 1, 2]},
249
+ ],
250
+ "animation": {"type": "float", "speed": 1.0, "axis": "y"},
251
+ })},
252
+ {"role": "user", "content": "a chrome hexagon"},
253
+ {"role": "assistant", "content": json.dumps({
254
+ "background": "#1a1a2e",
255
+ "objects": [{
256
+ "type": "extrude", "shape": "hexagon", "depth": 0.3, "bevel": True,
257
+ "color": "#ffffff", "preset": "chrome",
258
+ "position": [0, 0, 0],
259
+ }],
260
+ "lights": [
261
+ {"type": "ambient", "intensity": 0.4},
262
+ {"type": "directional", "color": "#ffffff", "intensity": 2.5, "position": [4, 6, 3]},
263
+ {"type": "point", "color": "#88aaff", "intensity": 1.0, "position": [-3, 2, 2]},
264
+ ],
265
+ "animation": {"type": "rotate", "speed": 0.6, "axis": "y"},
266
+ })},
267
  ]
268
 
269
  _tok = None
scene.py CHANGED
@@ -23,6 +23,7 @@ SHAPES = {
23
  }
24
  EXTRUDE_SHAPES = {"star", "heart", "hexagon", "badge", "shield"}
25
  MATERIALS = {"standard", "basic", "phong", "wireframe"}
 
26
  LIGHT_TYPES = {"ambient", "directional", "point"}
27
  ANIM_TYPES = {"none", "rotate", "float", "orbit"}
28
  HEX = re.compile(r"^#[0-9a-fA-F]{6}$")
@@ -141,6 +142,7 @@ class Obj(BaseModel):
141
  scale: List[float] = Field(default_factory=lambda: [1.0, 1.0, 1.0])
142
  color: str = "#88ccff"
143
  material: str = "standard"
 
144
  metalness: float = 0.3
145
  roughness: float = 0.4
146
  emissive: str = "#000000"
@@ -158,6 +160,14 @@ class Obj(BaseModel):
158
  v = str(v)
159
  return v if v in MATERIALS else "standard"
160
 
 
 
 
 
 
 
 
 
161
  @field_validator("position", "rotation", "scale")
162
  @classmethod
163
  def _vec3(cls, v: Any, info) -> List[float]:
@@ -364,6 +374,7 @@ class ExtrudeNode(BaseModel):
364
  bevel: bool = True
365
  color: str = "#88ccff"
366
  material: str = "standard"
 
367
  metalness: float = 0.3
368
  roughness: float = 0.4
369
  emissive: str = "#000000"
@@ -397,6 +408,14 @@ class ExtrudeNode(BaseModel):
397
  v = str(v)
398
  return v if v in MATERIALS else "standard"
399
 
 
 
 
 
 
 
 
 
400
  @field_validator("metalness", "roughness")
401
  @classmethod
402
  def _unit(cls, v: Any) -> float:
 
23
  }
24
  EXTRUDE_SHAPES = {"star", "heart", "hexagon", "badge", "shield"}
25
  MATERIALS = {"standard", "basic", "phong", "wireframe"}
26
+ PRESET_NAMES = {"gold", "chrome", "glass", "neon", "matte", "plastic"}
27
  LIGHT_TYPES = {"ambient", "directional", "point"}
28
  ANIM_TYPES = {"none", "rotate", "float", "orbit"}
29
  HEX = re.compile(r"^#[0-9a-fA-F]{6}$")
 
142
  scale: List[float] = Field(default_factory=lambda: [1.0, 1.0, 1.0])
143
  color: str = "#88ccff"
144
  material: str = "standard"
145
+ preset: Optional[str] = None
146
  metalness: float = 0.3
147
  roughness: float = 0.4
148
  emissive: str = "#000000"
 
160
  v = str(v)
161
  return v if v in MATERIALS else "standard"
162
 
163
+ @field_validator("preset")
164
+ @classmethod
165
+ def _preset_field(cls, v: Any) -> Optional[str]:
166
+ if v is None:
167
+ return None
168
+ v = str(v).lower().strip()
169
+ return v if v in PRESET_NAMES else None
170
+
171
  @field_validator("position", "rotation", "scale")
172
  @classmethod
173
  def _vec3(cls, v: Any, info) -> List[float]:
 
374
  bevel: bool = True
375
  color: str = "#88ccff"
376
  material: str = "standard"
377
+ preset: Optional[str] = None
378
  metalness: float = 0.3
379
  roughness: float = 0.4
380
  emissive: str = "#000000"
 
408
  v = str(v)
409
  return v if v in MATERIALS else "standard"
410
 
411
+ @field_validator("preset")
412
+ @classmethod
413
+ def _preset_field(cls, v: Any) -> Optional[str]:
414
+ if v is None:
415
+ return None
416
+ v = str(v).lower().strip()
417
+ return v if v in PRESET_NAMES else None
418
+
419
  @field_validator("metalness", "roughness")
420
  @classmethod
421
  def _unit(cls, v: Any) -> float: