Skip to content

Commit a7638ae

Browse files
lxb12123claude
andcommitted
feat(02-autograd): flat rounded chip model + dedicated light palette
Addresses the "chips still look like boxes" + "light mode colours need their own design" feedback. Chip model (blender/scripts/autograd_chip.py → public/models/autograd/chip.glb): - Reshaped from a chunky 1×1×0.45 slab into a low, flat, rounded CARD that faces the camera: 1.35 wide × 0.85 tall × 0.28 thin, pronounced-but-restrained rounded corners, a recessed front panel, a thin glowing rim frame, and a faint under-glow (not a hard shadow). Reads as a luminous computation chip at 3/4, not a cube. Material names (ChipBodyMat/ChipRimMat/ChipGlowMat) unchanged. Palette (theme.ts) — dark and light now designed separately, per-kind: - Per-kind body + idle rim; the output chip keeps an amber identity rim (#f59e0b light / #fbbf24 dark) so "the answer" stands out, while variable/op rims flare to the flow colour (forward cyan / backward violet). - Light: bg #f6f8fc, grid #dbe5f2, variable #ffffff, op #eef3f9, output #fff7ed, inactive edge #c8d3e2, card rgba(255,255,255,0.94)/#0f172a/#64748b — white chips stay legible via rim + shading, not lost in the background. AutogradNode: per-kind idle rim, amber output flare, halo follows the active rim and is widened/flattened to the card. Smaller per-kind scale for the larger card; node label cards lifted further off the chip. Preview PNG regenerated. 108 unit tests + 7 e2e pass; check-assets passes (chip 31 KB). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f5bad1d commit a7638ae

7 files changed

Lines changed: 87 additions & 71 deletions

File tree

blender/scripts/autograd_chip.py

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
"""
2-
Autograd lab: chip.glb — a low, rounded "computation chip" node.
2+
Autograd lab: chip.glb — a low, flat, rounded "computation chip" CARD.
3+
4+
Designed to face the camera in the vertical-board DAG: a wide rounded rectangle
5+
(x=1.35, y=0.85) that is THIN toward the viewer (z=0.28), so at a 3/4 angle it
6+
reads as a slim chip module — not a chunky box. Rounded corners, a recessed front
7+
panel, a thin glowing rim frame around the face, and a faint under-glow behind.
38
49
THREE retintable material regions (the React <AutogradNode> clones the glb per
510
instance and recolours these by theme / node type / activation — detection is by
611
material NAME, so DO NOT rename):
712
813
- ChipBodyMat body (ceramic/satin, base-colour retinted per node)
9-
- ChipRimMat glowing top rim frame (emissive, brightens on activation)
10-
- ChipGlowMat soft under-plate halo (low emissive, theme tint)
14+
- ChipRimMat glowing face rim frame (emissive)
15+
- ChipGlowMat recessed front panel + soft under-glow (low emissive)
1116
12-
Footprint ~1×1, height ~0.45 so it reads as a module. LabelTop/LabelBottom empty
13-
anchors are kept for the React label layer. PBR nodes only, no textures.
17+
LabelTop / LabelBottom empties are kept for the React label layer. PBR only.
1418
"""
1519
import os
1620
import bpy
1721

1822
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
1923
OUTPUT_PATH = os.path.join(REPO_ROOT, 'public', 'models', 'autograd', 'chip.glb')
2024

21-
BODY_H = 0.45
25+
W, H, D = 1.35, 0.85, 0.28 # width(x), face-height(y), thickness toward camera(z)
2226

2327

2428
def _mat(name, base, *, rough=0.5, metal=0.1, emit=None, emit_strength=0.0):
@@ -49,46 +53,52 @@ def _box(name, location, scale, material, parent=None):
4953
def main() -> None:
5054
bpy.ops.wm.read_factory_settings(use_empty=True)
5155

52-
body_mat = _mat('ChipBodyMat', (0.82, 0.84, 0.88), rough=0.42, metal=0.12)
53-
rim_mat = _mat('ChipRimMat', (0.0, 0.9, 1.0), rough=0.35, metal=0.0,
56+
body_mat = _mat('ChipBodyMat', (0.86, 0.88, 0.92), rough=0.4, metal=0.1)
57+
rim_mat = _mat('ChipRimMat', (0.0, 0.9, 1.0), rough=0.3, metal=0.0,
5458
emit=(0.0, 0.9, 1.0), emit_strength=3.0)
55-
glow_mat = _mat('ChipGlowMat', (0.0, 0.9, 1.0), rough=0.6, metal=0.0,
56-
emit=(0.0, 0.9, 1.0), emit_strength=0.6)
59+
glow_mat = _mat('ChipGlowMat', (0.0, 0.9, 1.0), rough=0.55, metal=0.0,
60+
emit=(0.0, 0.9, 1.0), emit_strength=0.5)
5761

58-
# Body: low rounded chip.
62+
# --- Body: flat rounded card (thin in z, facing the camera) ---
5963
bpy.ops.mesh.primitive_cube_add(size=1.0, location=(0.0, 0.0, 0.0))
60-
cube = bpy.context.active_object
61-
cube.name = 'Chip'
62-
cube.scale = (1.0, 1.0, BODY_H)
64+
body = bpy.context.active_object
65+
body.name = 'Chip'
66+
body.scale = (W, H, D)
6367
bpy.ops.object.transform_apply(scale=True)
64-
bevel = cube.modifiers.new(name='Bevel', type='BEVEL')
65-
bevel.width = 0.06
66-
bevel.segments = 3
68+
bevel = body.modifiers.new(name='Bevel', type='BEVEL')
69+
bevel.width = 0.11 # pronounced-but-restrained rounded corners
70+
bevel.segments = 4
6771
bevel.limit_method = 'ANGLE'
6872
bpy.ops.object.modifier_apply(modifier='Bevel')
69-
cube.data.materials.append(body_mat)
73+
body.data.materials.append(body_mat)
74+
75+
face_z = D / 2.0 # the +z face plane
76+
77+
# --- Recessed front panel (the "screen") just inside the rim ---
78+
_box('ChipPanel', (0.0, -0.02, face_z - 0.02), (W - 0.34, H - 0.30, 0.03), glow_mat, parent=body)
7079

71-
# Glowing top rim frame (4 thin emissive bars inset from the edges).
72-
top_z = BODY_H / 2.0 + 0.012
73-
inset, bar_t, bar_h = 0.36, 0.045, 0.03
74-
span = inset * 2.0 + bar_t
80+
# --- Thin glowing rim frame around the face perimeter ---
81+
inset_x = W / 2.0 - 0.12
82+
inset_y = H / 2.0 - 0.12
83+
bar = 0.04
84+
rim_z = face_z + 0.012
7585
for name, loc, scale in (
76-
('Rim_PX', (inset, 0.0, top_z), (bar_t, span, bar_h)),
77-
('Rim_NX', (-inset, 0.0, top_z), (bar_t, span, bar_h)),
78-
('Rim_PY', (0.0, inset, top_z), (span, bar_t, bar_h)),
79-
('Rim_NY', (0.0, -inset, top_z), (span, bar_t, bar_h)),
86+
('Rim_T', (0.0, inset_y, rim_z), (inset_x * 2.0 + bar, bar, 0.022)),
87+
('Rim_B', (0.0, -inset_y, rim_z), (inset_x * 2.0 + bar, bar, 0.022)),
88+
('Rim_L', (-inset_x, 0.0, rim_z), (bar, inset_y * 2.0 + bar, 0.022)),
89+
('Rim_R', (inset_x, 0.0, rim_z), (bar, inset_y * 2.0 + bar, 0.022)),
8090
):
81-
_box(name, loc, scale, rim_mat, parent=cube)
91+
_box(name, loc, scale, rim_mat, parent=body)
8292

83-
# Soft under-plate halo.
84-
_box('ChipUnderGlow', (0.0, 0.0, -BODY_H / 2.0 - 0.02), (1.08, 1.08, 0.02), glow_mat, parent=cube)
93+
# --- Soft under-glow plate behind the card (faint, not a hard shadow) ---
94+
_box('ChipUnderGlow', (0.0, 0.0, -D / 2.0 - 0.04), (W + 0.18, H + 0.18, 0.02), glow_mat, parent=body)
8595

86-
# Label anchors for the React label layer.
87-
for anchor_name, z in (('LabelTop', BODY_H / 2.0 + 0.18), ('LabelBottom', -BODY_H / 2.0 - 0.18)):
88-
bpy.ops.object.empty_add(type='PLAIN_AXES', location=(0.0, 0.0, z))
96+
# --- Label anchors ---
97+
for anchor_name, y in (('LabelTop', H / 2.0 + 0.4), ('LabelBottom', -H / 2.0 - 0.3)):
98+
bpy.ops.object.empty_add(type='PLAIN_AXES', location=(0.0, y, 0.0))
8999
anchor = bpy.context.active_object
90100
anchor.name = anchor_name
91-
anchor.parent = cube
101+
anchor.parent = body
92102

93103
bpy.ops.object.select_all(action='SELECT')
94104
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)

components/3d/autograd/AutogradLabels.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function NodeCard({
4141
position, kind, name, value, valueRevealed, detail, grad, gradRevealed, derived, constant, expansion, theme,
4242
}: NodeCardProps) {
4343
const title = kind === 'output' ? 'output' : name || KIND_LABEL[kind];
44-
const accent = kind === 'output' ? theme.gradPos : theme.forward;
44+
const accent = kind === 'output' ? theme.outputRim : theme.forward;
4545
return (
4646
<Html position={position} center distanceFactor={8} style={cardStyle(theme)} zIndexRange={[20, 0]}>
4747
<div style={{ fontWeight: 700, fontSize: 12.5, color: accent, textAlign: 'center', marginBottom: 1 }}>{title}</div>

components/3d/autograd/AutogradNode.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import { useFrame } from '@react-three/fiber';
66
import {
77
AdditiveBlending, CanvasTexture, Color, type Group, type Mesh, type Object3D,
88
} from 'three';
9-
import type { AutogradTheme, NodeKind } from './theme';
9+
import { activeRimColor, type AutogradTheme, type NodeKind } from './theme';
1010

1111
const CHIP_URL = '/microgpt-3d-tutorial/models/autograd/chip.glb';
1212

13-
// Per-kind base scale — the output chip is the biggest (it's "the answer"),
14-
// variables the smallest. Applied on top of the layout group scale.
15-
const KIND_SCALE: Record<NodeKind, number> = { variable: 0.82, op: 0.92, output: 1.12 };
13+
// Per-kind base scale — output slightly bigger ("the answer"), variables
14+
// smallest. The chip card is wide (1.35×0.85), so these are smaller than for a
15+
// unit cube. Applied on top of the layout group scale.
16+
const KIND_SCALE: Record<NodeKind, number> = { variable: 0.7, op: 0.76, output: 0.9 };
1617

1718
// A soft radial sprite for the additive halo behind a glowing chip — generated
1819
// once (client-side) and tinted per node via the material colour.
@@ -73,11 +74,14 @@ export function AutogradNode({ position, kind, theme, flowColor, activation, lit
7374
}, [gltf.scene]);
7475

7576
const bodyColor = theme.body[kind];
76-
// Rim: established nodes keep a soft flow-coloured glow; the active node flares.
77+
// Rim: each kind has an idle identity colour; established/active nodes flare to
78+
// the flow colour (forward/backward) — except output, which keeps amber.
79+
const idleRim = theme.rimIdle[kind];
80+
const activeRim = activeRimColor(theme, kind, flowColor);
7781
const flowLit = lit || activation > 0.03;
78-
const rimColor = flowLit ? flowColor : theme.rimIdle;
79-
const rimIntensity = (lit ? 1.0 : 0.4) + 2.2 * activation;
80-
const glowColor = flowLit ? flowColor : theme.rimIdle;
82+
const rimColor = flowLit ? activeRim : idleRim;
83+
const rimIntensity = (lit ? 1.0 : 0.45) + 2.2 * activation;
84+
const glowColor = flowLit ? activeRim : idleRim;
8185

8286
useLayoutEffect(() => {
8387
scene.traverse((obj: Object3D) => {
@@ -117,14 +121,14 @@ export function AutogradNode({ position, kind, theme, flowColor, activation, lit
117121

118122
const s = KIND_SCALE[kind];
119123
const tex = haloTexture();
120-
const haloColor = useMemo(() => new Color(flowColor), [flowColor]);
124+
const haloColor = useMemo(() => new Color(activeRim), [activeRim]);
121125

122126
return (
123127
<group ref={groupRef} position={position}>
124128
{/* Additive halo behind the chip — only visible as it activates. */}
125129
{tex && activation > 0.04 && (
126130
<Billboard position={[0, 0, -0.25]}>
127-
<mesh scale={[2.2 * s, 2.2 * s, 1]}>
131+
<mesh scale={[2.9 * s, 2.0 * s, 1]}>
128132
<planeGeometry args={[1, 1]} />
129133
<meshBasicMaterial
130134
map={tex}

components/3d/autograd/AutogradSandbox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export function AutogradSandbox({ defaultExpression, defaultVariables }: Autogra
233233
return (
234234
<NodeCard
235235
key={`card-${n.id}`}
236-
position={[pos[n.id][0], pos[n.id][1] + 0.95, pos[n.id][2]]}
236+
position={[pos[n.id][0], pos[n.id][1] + 1.05, pos[n.id][2]]}
237237
kind={kindOf(n.id)}
238238
name={n.label}
239239
value={n.value.data}

components/3d/autograd/theme.ts

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/**
2-
* Semantic palette for the 02-autograd "luminous computation lab". One place for
3-
* every colour so forward/backward flow, gradient sign, node hierarchy, and edge
4-
* state read consistently on both themes.
2+
* Semantic palette for the 02-autograd "luminous computation lab". Dark and
3+
* light are designed SEPARATELY (not one inverted set): dark is the deep-lab
4+
* look; light is a cool, airy lab bench where white chips stay legible against a
5+
* soft blue-grey background via their rims and shading.
56
*/
67
export type Scheme = 'light' | 'dark';
78
export type NodeKind = 'variable' | 'op' | 'output';
@@ -11,34 +12,31 @@ export interface AutogradTheme {
1112
grid: string;
1213
/** Chip body base colour by node hierarchy (retinted onto ChipBodyMat). */
1314
body: Record<NodeKind, string>;
14-
/** Dim rim colour when a node is idle. */
15-
rimIdle: string;
16-
/** Forward data flow (value pulses + active forward edges). */
15+
/** Dim rim colour when a node is idle, by hierarchy (its identity outline). */
16+
rimIdle: Record<NodeKind, string>;
17+
/** The output chip keeps an amber identity rim even when "active". */
18+
outputRim: string;
19+
/** Forward data flow (value pulses + active forward edges + var/op active rim). */
1720
forward: string;
18-
/** Backward gradient flow (grad pulses + active backward edges). */
21+
/** Backward gradient flow. */
1922
backward: string;
20-
/** Gradient value colouring by sign. */
2123
gradPos: string;
2224
gradNeg: string;
23-
/** Inactive edge tint. */
2425
edgeInactive: string;
25-
/** Soft afterglow on a propagated edge. */
2626
edgePropagated: string;
27-
/** Html card. */
2827
cardBg: string;
2928
cardBorder: string;
3029
cardText: string;
3130
cardMuted: string;
32-
/** Title / subtitle ink. */
33-
title: string;
3431
subtitle: string;
3532
}
3633

3734
const DARK: AutogradTheme = {
3835
bg: '#070a12',
3936
grid: '#161d2c',
40-
body: { variable: '#3a465c', op: '#313c50', output: '#46577a' },
41-
rimIdle: '#4a566b',
37+
body: { variable: '#3a465c', op: '#313c50', output: '#473f53' },
38+
rimIdle: { variable: '#5a6680', op: '#4a566b', output: '#7a6336' },
39+
outputRim: '#fbbf24',
4240
forward: '#22d3ee',
4341
backward: '#a78bfa',
4442
gradPos: '#34d399',
@@ -49,27 +47,26 @@ const DARK: AutogradTheme = {
4947
cardBorder: 'rgba(120,140,180,0.30)',
5048
cardText: '#e8eefc',
5149
cardMuted: '#94a3b8',
52-
title: '#e8eefc',
5350
subtitle: '#aab6cc',
5451
};
5552

5653
const LIGHT: AutogradTheme = {
57-
bg: '#eef3fa',
58-
grid: '#d4deeb',
59-
body: { variable: '#ffffff', op: '#e8eef6', output: '#ffffff' },
60-
rimIdle: '#9fb0c8',
54+
bg: '#f6f8fc',
55+
grid: '#dbe5f2',
56+
body: { variable: '#ffffff', op: '#eef3f9', output: '#fff7ed' },
57+
rimIdle: { variable: '#9fb0c8', op: '#94a3b8', output: '#f0b25e' },
58+
outputRim: '#f59e0b',
6159
forward: '#0891b2',
6260
backward: '#7c3aed',
6361
gradPos: '#059669',
6462
gradNeg: '#e11d48',
65-
edgeInactive: '#b8c5d6',
66-
edgePropagated: '#8aa0bd',
67-
cardBg: 'rgba(255,255,255,0.92)',
63+
edgeInactive: '#c8d3e2',
64+
edgePropagated: '#9fb0c8',
65+
cardBg: 'rgba(255,255,255,0.94)',
6866
cardBorder: 'rgba(80,110,150,0.28)',
6967
cardText: '#0f172a',
70-
cardMuted: '#5d6b80',
71-
title: '#0f172a',
72-
subtitle: '#4a5a72',
68+
cardMuted: '#64748b',
69+
subtitle: '#475569',
7370
};
7471

7572
export function getAutogradTheme(scheme: Scheme): AutogradTheme {
@@ -80,3 +77,8 @@ export function getAutogradTheme(scheme: Scheme): AutogradTheme {
8077
export function gradColor(theme: AutogradTheme, g: number): string {
8178
return g < 0 ? theme.gradNeg : theme.gradPos;
8279
}
80+
81+
/** The rim colour a node flares to when active: amber for output, else flow. */
82+
export function activeRimColor(theme: AutogradTheme, kind: NodeKind, flowColor: string): string {
83+
return kind === 'output' ? theme.outputRim : flowColor;
84+
}

public/models/autograd/chip.glb

8.85 KB
Binary file not shown.
-2.4 KB
Loading

0 commit comments

Comments
 (0)