Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/skills/beat-model/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Every `StoryBeat` carries these base fields, plus the story's own entity arrays
| `focus` | keys to spotlight: entity ids and `owner.slot` keys |
| `phase?` | optional story-specific stage (e.g. GC `mark` / `sweep`) |

Stack-and-heap stories take `Slot`, `StackFrame`, `HeapObject`, `Snapshot` and the reference helpers (`slotKey`, `getReferences`, `getReachable`) from `src/shared/snapshot`, because a Playground trace step holds the same snapshot ([ADR 0004](../../../docs/adr/0004-playground-traces-share-the-snapshot.md)). The story narrows them: `Placed<HeapObject<…>>` makes the authored `cell` required. Other domains (e.g. Git commits and refs) keep their own typed snapshots.

Rules that make the model render cleanly:

- **Full snapshot per beat.** Every beat lists every entity present at that moment; stepping backwards is just rendering an earlier beat.
Expand Down
2 changes: 2 additions & 0 deletions .claude/skills/beat-model/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Every `StoryBeat` carries these base fields, plus the story's own entity arrays
| `focus` | keys to spotlight: entity ids and `owner.slot` keys |
| `phase?` | optional story-specific stage (e.g. GC `mark` / `sweep`) |

Stack-and-heap stories take `Slot`, `StackFrame`, `HeapObject`, `Snapshot` and the reference helpers (`slotKey`, `getReferences`, `getReachable`) from `src/shared/snapshot`, because a Playground trace step holds the same snapshot ([ADR 0004](../../../docs/adr/0004-playground-traces-share-the-snapshot.md)). The story narrows them: `Placed<HeapObject<…>>` makes the authored `cell` required. Other domains (e.g. Git commits and refs) keep their own typed snapshots.

Rules that make the model render cleanly:

- **Full snapshot per beat.** Every beat lists every entity present at that moment; stepping backwards is just rendering an earlier beat.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type StackFrame,
type StoryBeat,
} from '../../../utils/stackHeapStory';
import { vizPalette } from '../../../../../shared/viz/palette';

interface StackHeap2DProps {
beat: StoryBeat;
Expand All @@ -30,19 +31,8 @@ const CELL_Y0 = 78;
const CELL_DY = 190;
const OBJ_W = 180;

const COLORS = {
frame: '#e0e7ff',
frameStroke: '#6366f1',
object: '#d1fae5',
objectStroke: '#10b981',
array: '#e0f2fe',
arrayStroke: '#0ea5e9',
ref: '#64748b',
focus: '#f59e0b',
root: '#f59e0b',
unmarked: '#ffe4e6',
unmarkedStroke: '#f43f5e',
};
/** Light only for now; the theme becomes a prop when the view is generalised. */
const P = vizPalette.light;

interface Box {
x: number;
Expand Down Expand Up @@ -149,7 +139,7 @@ const StackHeap2D: React.FC<StackHeap2DProps> = ({ beat }) => {
markerHeight="7"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill={COLORS.ref} />
<path d="M 0 0 L 10 5 L 0 10 z" fill={P.reference.stroke} />
</marker>
<marker
id="sh-arrow-focus"
Expand All @@ -160,7 +150,7 @@ const StackHeap2D: React.FC<StackHeap2DProps> = ({ beat }) => {
markerHeight="7"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill={COLORS.focus} />
<path d="M 0 0 L 10 5 L 0 10 z" fill={P.focus} />
</marker>
</defs>

Expand All @@ -171,8 +161,8 @@ const StackHeap2D: React.FC<StackHeap2DProps> = ({ beat }) => {
width={STACK_W}
height={H - 36}
rx={14}
fill="#f8fafc"
stroke="#cbd5e1"
fill={P.region.fill}
stroke={P.region.stroke}
/>
<text
x={STACK_X + 14}
Expand All @@ -190,8 +180,8 @@ const StackHeap2D: React.FC<StackHeap2DProps> = ({ beat }) => {
width={W - HEAP_X - 16}
height={H - 36}
rx={14}
fill="#f8fafc"
stroke="#cbd5e1"
fill={P.region.fill}
stroke={P.region.stroke}
/>
<text
x={HEAP_X + 14}
Expand All @@ -218,8 +208,8 @@ const StackHeap2D: React.FC<StackHeap2DProps> = ({ beat }) => {
width={b.w}
height={b.h}
rx={8}
fill={COLORS.frame}
stroke={marking || focused ? COLORS.root : COLORS.frameStroke}
fill={P.frame.fill}
stroke={marking || focused ? P.root : P.frame.stroke}
strokeWidth={marking || focused ? 3 : top ? 2 : 1}
strokeDasharray={marking ? '6 4' : undefined}
/>
Expand All @@ -240,7 +230,7 @@ const StackHeap2D: React.FC<StackHeap2DProps> = ({ beat }) => {
fontSize={10}
fontWeight={700}
textAnchor="end"
fill={COLORS.root}
fill={P.root}
>
ROOT
</text>
Expand Down Expand Up @@ -275,7 +265,7 @@ const StackHeap2D: React.FC<StackHeap2DProps> = ({ beat }) => {
className="sh-ref sh-enter"
d={d}
fill="none"
stroke={focused ? COLORS.focus : COLORS.ref}
stroke={focused ? P.focus : P.reference.stroke}
strokeWidth={focused ? 3 : 1.6}
markerEnd={`url(#${focused ? 'sh-arrow-focus' : 'sh-arrow'})`}
/>
Expand Down Expand Up @@ -304,15 +294,15 @@ const StackHeap2D: React.FC<StackHeap2DProps> = ({ beat }) => {
width={b.w}
height={b.h}
rx={isArray ? 4 : 10}
fill={unmarked ? COLORS.unmarked : isArray ? COLORS.array : COLORS.object}
fill={unmarked ? P.unreachable.fill : isArray ? P.array.fill : P.object.fill}
stroke={
focused
? COLORS.focus
? P.focus
: unmarked
? COLORS.unmarkedStroke
? P.unreachable.stroke
: isArray
? COLORS.arrayStroke
: COLORS.objectStroke
? P.array.stroke
: P.object.stroke
}
strokeWidth={focused ? 3 : 1.5}
strokeDasharray={collected ? '5 4' : undefined}
Expand All @@ -337,7 +327,7 @@ const StackHeap2D: React.FC<StackHeap2DProps> = ({ beat }) => {
fontSize={10}
fontWeight={700}
textAnchor="end"
fill={COLORS.unmarkedStroke}
fill={P.unreachable.stroke}
>
{collected ? 'RECLAIMED' : 'UNMARKED'}
</text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '../../../utils/stackHeapStory';
import type { CameraState } from '../../../../../shared/components/viz/viewerCamera';
import { LabelFramer, StoryCamera } from '../../../../../shared/viz3d/camera';
import { vizPalette } from '../../../../../shared/viz/palette';
import { boundsOf, type Box3, type Shot, type V3 } from '../../../../../shared/viz3d/frameShot';

/**
Expand All @@ -37,20 +38,8 @@ export interface StackHeap3DProps {
instant?: boolean;
}

const PALETTE = {
ground: '#e2e8f0',
stackBase: '#cbd5e1',
heapFloor: '#f1f5f9',
heapEdge: '#cbd5e1',
frame: '#818cf8',
frameTop: '#6366f1',
object: '#34d399',
array: '#38bdf8',
unmarked: '#fb7185',
ref: '#475569',
focus: '#f59e0b',
root: '#f59e0b',
};
/** Light only for now; the theme becomes a prop when the view is generalised. */
const P = vizPalette.light;

// ---- Layout (world units) ----------------------------------------------------------

Expand Down Expand Up @@ -156,7 +145,7 @@ const FrameSlab: React.FC<{
<RoundedBox args={FRAME_SIZE} radius={0.08} smoothness={2}>
<meshLambertMaterial
ref={mat}
color={isTop ? PALETTE.frameTop : PALETTE.frame}
color={isTop ? P.frameActive.solid : P.frame.solid}
transparent
opacity={instant ? 1 : 0}
flatShading
Expand All @@ -165,7 +154,7 @@ const FrameSlab: React.FC<{
{highlight && !exiting && (
<mesh position={[0, -FRAME_SIZE[1] / 2 + 0.01, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[FRAME_SIZE[0] + 0.35, FRAME_SIZE[2] + 0.35]} />
<meshBasicMaterial color={PALETTE.root} transparent opacity={0.55} />
<meshBasicMaterial color={P.root} transparent opacity={0.55} />
</mesh>
)}
<Html position={[0, 0, FRAME_SIZE[2] / 2 + 0.02]} zIndexRange={[10, 0]}>
Expand Down Expand Up @@ -230,10 +219,10 @@ const HeapBlock: React.FC<{
() =>
new THREE.Color(
object.state === 'unmarked' || object.state === 'collected'
? PALETTE.unmarked
? P.unreachable.solid
: object.kind === 'array'
? PALETTE.array
: PALETTE.object
? P.array.solid
: P.object.solid
),
[object.state, object.kind]
);
Expand All @@ -253,7 +242,7 @@ const HeapBlock: React.FC<{
{focused && !collected && (
<mesh position={[0, 0.012, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[size[0] + 0.4, size[2] + 0.4]} />
<meshBasicMaterial color={PALETTE.focus} transparent opacity={0.6} />
<meshBasicMaterial color={P.focus} transparent opacity={0.6} />
</mesh>
)}
<group ref={group} scale={instant ? [1, 1, 1] : [0.2, 0.01, 0.2]}>
Expand All @@ -269,7 +258,7 @@ const HeapBlock: React.FC<{
[1, 2, 3].map((i) => (
<mesh key={i} position={[-size[0] / 2 + (i * size[0]) / 4, size[1] + 0.005, 0]}>
<boxGeometry args={[0.03, 0.01, size[2] * 0.8]} />
<meshBasicMaterial color="#0369a1" transparent opacity={0.35} />
<meshBasicMaterial color={P.array.divider} transparent opacity={0.35} />
</mesh>
))}
</group>
Expand Down Expand Up @@ -384,7 +373,7 @@ const RefArc: React.FC<{ start: V3; end: V3; mid: V3; focused: boolean; instant:
instant,
}) => {
const line = useRef<QuadraticBezierLineRef>(null);
const color = focused ? PALETTE.focus : PALETTE.ref;
const color = focused ? P.focus : P.reference.solid;

const cone = useMemo(() => {
const e = new THREE.Vector3(...end);
Expand Down Expand Up @@ -441,7 +430,7 @@ const RootPulse: React.FC = () => {
{[0, 1, 2].map((i) => (
<mesh key={i} ref={(m) => void (rings.current[i] = m)}>
<ringGeometry args={[1, 1.08, 64]} />
<meshBasicMaterial color={PALETTE.root} transparent opacity={0} depthWrite={false} />
<meshBasicMaterial color={P.root} transparent opacity={0} depthWrite={false} />
</mesh>
))}
</group>
Expand All @@ -465,21 +454,21 @@ const Scene: React.FC<StackHeap3DProps> = ({ beat, camera, instant = false }) =>

return (
<>
<color attach="background" args={['#f8fafc']} />
<hemisphereLight args={['#ffffff', '#cbd5e1', 1.4]} />
<color attach="background" args={[P.surface]} />
<hemisphereLight args={[P.lighting.sky, P.lighting.ground, 1.4]} />
<directionalLight position={[6, 12, 8]} intensity={1.6} />

{/* Stack plinth + heap floor */}
<mesh position={[STACK_X, -0.06, 0]}>
<boxGeometry args={[FRAME_SIZE[0] + 0.8, 0.12, FRAME_SIZE[2] + 0.8]} />
<meshLambertMaterial color={PALETTE.stackBase} flatShading />
<meshLambertMaterial color={P.region.plinth} flatShading />
</mesh>
<mesh position={[HEAP_CENTER[0], -0.06, HEAP_CENTER[2]]}>
<boxGeometry args={HEAP_SIZE} />
<meshLambertMaterial color={PALETTE.heapFloor} flatShading />
<meshLambertMaterial color={P.region.floor} flatShading />
</mesh>
<gridHelper
args={[HEAP_SIZE[0], 12, PALETTE.heapEdge, PALETTE.heapEdge]}
args={[HEAP_SIZE[0], 12, P.region.stroke, P.region.stroke]}
position={[HEAP_CENTER[0], 0.001, HEAP_CENTER[2]]}
scale={[1, 1, HEAP_SIZE[2] / HEAP_SIZE[0]]}
/>
Expand Down
Loading
Loading