jspsych-tangram 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +25 -0
  2. package/dist/construct/index.browser.js +20431 -0
  3. package/dist/construct/index.browser.js.map +1 -0
  4. package/dist/construct/index.browser.min.js +42 -0
  5. package/dist/construct/index.browser.min.js.map +1 -0
  6. package/dist/construct/index.cjs +3720 -0
  7. package/dist/construct/index.cjs.map +1 -0
  8. package/dist/construct/index.d.ts +204 -0
  9. package/dist/construct/index.js +3718 -0
  10. package/dist/construct/index.js.map +1 -0
  11. package/dist/index.cjs +3920 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.ts +340 -0
  14. package/dist/index.js +3917 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/prep/index.browser.js +20455 -0
  17. package/dist/prep/index.browser.js.map +1 -0
  18. package/dist/prep/index.browser.min.js +42 -0
  19. package/dist/prep/index.browser.min.js.map +1 -0
  20. package/dist/prep/index.cjs +3744 -0
  21. package/dist/prep/index.cjs.map +1 -0
  22. package/dist/prep/index.d.ts +139 -0
  23. package/dist/prep/index.js +3742 -0
  24. package/dist/prep/index.js.map +1 -0
  25. package/package.json +77 -0
  26. package/src/core/components/README.md +249 -0
  27. package/src/core/components/board/BoardView.tsx +352 -0
  28. package/src/core/components/board/GameBoard.tsx +682 -0
  29. package/src/core/components/board/index.ts +70 -0
  30. package/src/core/components/board/useAnchorGrid.ts +110 -0
  31. package/src/core/components/board/useClickController.ts +436 -0
  32. package/src/core/components/board/useDragController.ts +1051 -0
  33. package/src/core/components/board/usePieceState.ts +178 -0
  34. package/src/core/components/board/utils.ts +76 -0
  35. package/src/core/components/index.ts +33 -0
  36. package/src/core/components/pieces/BlueprintRing.tsx +238 -0
  37. package/src/core/config/config.ts +85 -0
  38. package/src/core/domain/blueprints.ts +25 -0
  39. package/src/core/domain/layout.ts +159 -0
  40. package/src/core/domain/primitives.ts +159 -0
  41. package/src/core/domain/solve.ts +184 -0
  42. package/src/core/domain/types.ts +111 -0
  43. package/src/core/engine/collision/grid-snapping.ts +283 -0
  44. package/src/core/engine/collision/index.ts +4 -0
  45. package/src/core/engine/collision/sat-collision.ts +46 -0
  46. package/src/core/engine/collision/validation.ts +166 -0
  47. package/src/core/engine/geometry/bounds.ts +91 -0
  48. package/src/core/engine/geometry/collision.ts +64 -0
  49. package/src/core/engine/geometry/index.ts +19 -0
  50. package/src/core/engine/geometry/math.ts +101 -0
  51. package/src/core/engine/geometry/pieces.ts +290 -0
  52. package/src/core/engine/geometry/polygons.ts +43 -0
  53. package/src/core/engine/state/BaseGameController.ts +368 -0
  54. package/src/core/engine/validation/border-rendering.ts +318 -0
  55. package/src/core/engine/validation/complete.ts +102 -0
  56. package/src/core/engine/validation/face-to-face.ts +217 -0
  57. package/src/core/index.ts +3 -0
  58. package/src/core/io/InteractionTracker.ts +742 -0
  59. package/src/core/io/data-tracking.ts +271 -0
  60. package/src/core/io/json-to-tangram-spec.ts +110 -0
  61. package/src/core/io/quickstash.ts +141 -0
  62. package/src/core/io/stims.ts +110 -0
  63. package/src/core/types/index.ts +5 -0
  64. package/src/core/types/plugin-interfaces.ts +101 -0
  65. package/src/index.spec.ts +19 -0
  66. package/src/index.ts +2 -0
  67. package/src/plugins/tangram-construct/ConstructionApp.tsx +105 -0
  68. package/src/plugins/tangram-construct/index.ts +156 -0
  69. package/src/plugins/tangram-prep/PrepApp.tsx +182 -0
  70. package/src/plugins/tangram-prep/index.ts +122 -0
  71. package/tangram-construct.min.js +42 -0
  72. package/tangram-prep.min.js +42 -0
@@ -0,0 +1,178 @@
1
+ import React from "react";
2
+ import { BaseGameController } from "@/core/engine/state/BaseGameController";
3
+ import type { Poly } from "@/core/domain/types";
4
+ import { boundsOfBlueprint, piecePolysAt } from "@/core/engine/geometry";
5
+
6
+ /**
7
+ * Piece data interface used throughout the hook system
8
+ *
9
+ * Represents a tangram piece instance with position and blueprint information.
10
+ * This is the normalized interface used across all board components.
11
+ */
12
+ export interface PieceData {
13
+ /** Unique identifier for this piece instance */
14
+ id: string;
15
+
16
+ /** ID of the blueprint that defines this piece's shape */
17
+ blueprintId: string;
18
+
19
+ /** X coordinate of the piece's top-left position in SVG space */
20
+ x: number;
21
+
22
+ /** Y coordinate of the piece's top-left position in SVG space */
23
+ y: number;
24
+
25
+ /** ID of the sector containing this piece (undefined if floating) */
26
+ sectorId?: string;
27
+ }
28
+
29
+ /**
30
+ * Return interface for usePieceState hook
31
+ *
32
+ * Provides normalized piece data and computed polygon functions for collision
33
+ * detection and rendering. All functions are memoized for optimal performance.
34
+ */
35
+ export interface PieceStateHook {
36
+ /** Array of all pieces from all sectors plus floating pieces */
37
+ pieces: PieceData[];
38
+
39
+ /**
40
+ * Lookup function to find a piece by its ID
41
+ * @param id - Piece ID to lookup (null-safe)
42
+ * @returns PieceData or null if not found
43
+ */
44
+ pieceById: (id: string | null) => PieceData | null;
45
+
46
+ /**
47
+ * Compute world polygons for all pieces in a sector (live/no cache)
48
+ * @param sectorId - Target sector ID
49
+ * @returns Array of world-space polygons
50
+ */
51
+ sectorPiecePolysLive: (sectorId: string) => Poly[];
52
+
53
+ /**
54
+ * Cached version of sectorPiecePolysLive with signature-based invalidation
55
+ * @param sectorId - Target sector ID
56
+ * @returns Cached array of world-space polygons
57
+ */
58
+ getSectorPiecePolysCached: (sectorId: string) => Poly[];
59
+ }
60
+
61
+ /**
62
+ * Hook for managing piece data and computed polygon states
63
+ *
64
+ * This hook provides normalized access to piece data from the BaseGameController,
65
+ * combining pieces from sectors and floating state into a unified interface.
66
+ * It also provides optimized polygon computation functions for collision detection.
67
+ *
68
+ * ## Key Features
69
+ * - **Unified piece access**: Combines sector and floating pieces
70
+ * - **Polygon caching**: Optimized collision detection with signature-based cache
71
+ * - **Live updates**: Reactively updates when controller state changes
72
+ * - **Null-safe lookups**: Safe piece retrieval by ID
73
+ *
74
+ * ## Performance
75
+ * - Pieces array is memoized based on controller.updateCount
76
+ * - Polygon cache uses sector "signature" (piece positions) for invalidation
77
+ * - Live polygon computation bypasses cache for real-time operations
78
+ *
79
+ * @param controller - BaseGameController instance managing game state
80
+ * @returns Hook interface with piece data and polygon functions
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * const pieceState = usePieceState(controller);
85
+ * const allPieces = pieceState.pieces;
86
+ * const piece = pieceState.pieceById("piece_123");
87
+ * const sectorPolygons = pieceState.sectorPiecePolysLive("sector_1");
88
+ * ```
89
+ */
90
+ export function usePieceState(controller: BaseGameController): PieceStateHook {
91
+ // --- pieces ---------------------------------------------------------------
92
+ const pieces = React.useMemo(() => {
93
+ const out: PieceData[] = [];
94
+ for (const s of Object.values(controller.state.sectors)) {
95
+ for (const p of s.pieces) {
96
+ out.push({
97
+ id: p.id,
98
+ blueprintId: p.blueprintId,
99
+ x: p.pos.x,
100
+ y: p.pos.y,
101
+ sectorId: s.sectorId
102
+ });
103
+ }
104
+ }
105
+ const floating: any = (controller as any)._floating;
106
+ if (floating?.pieces) {
107
+ for (const p of floating.pieces) {
108
+ out.push({
109
+ id: p.id,
110
+ blueprintId: p.blueprintId,
111
+ x: p.pos.x,
112
+ y: p.pos.y
113
+ });
114
+ }
115
+ }
116
+ return out;
117
+ }, [controller.state.sectors, (controller as any)._floating, controller.updateCount]);
118
+
119
+ const pieceById = React.useCallback(
120
+ (id: string | null) => (id ? pieces.find(p => p.id === id) ?? null : null),
121
+ [pieces]
122
+ );
123
+
124
+ // --- LIVE sector polys (reads controller.state directly) -----------------
125
+ const sectorPiecePolysLive = React.useCallback(
126
+ (sectorId: string): Poly[] => {
127
+ const out: Poly[] = [];
128
+ const ss = controller.state.sectors[sectorId];
129
+ if (!ss) return out;
130
+ for (const p of ss.pieces) {
131
+ const bp = controller.getBlueprint(p.blueprintId)!;
132
+ const bb = boundsOfBlueprint(bp, controller.getPrimitive);
133
+ out.push(...piecePolysAt(bp, bb, { x: p.pos.x, y: p.pos.y }));
134
+ }
135
+ return out;
136
+ },
137
+ [controller]
138
+ );
139
+
140
+ // --- OPTIONAL: cached variant keyed by a sector "signature" --------------
141
+ // Sig = pieceId@x,y;... (order-insensitive)
142
+ const polyCacheRef = React.useRef(
143
+ new Map<string, { sig: string; polys: Poly[] }>()
144
+ );
145
+
146
+ const sectorSignature = React.useCallback(
147
+ (sectorId: string): string => {
148
+ const ss = controller.state.sectors[sectorId];
149
+ if (!ss) return "";
150
+ // stable order by id
151
+ const parts = ss.pieces
152
+ .slice()
153
+ .sort((a, b) => (a.id < b.id ? -1 : 1))
154
+ .map(p => `${p.id}@${p.pos.x},${p.pos.y}`);
155
+ return parts.join("|");
156
+ },
157
+ [controller.state.sectors]
158
+ );
159
+
160
+ const getSectorPiecePolysCached = React.useCallback(
161
+ (sectorId: string): Poly[] => {
162
+ const sig = sectorSignature(sectorId);
163
+ const hit = polyCacheRef.current.get(sectorId);
164
+ if (hit && hit.sig === sig) return hit.polys;
165
+ const polys = sectorPiecePolysLive(sectorId);
166
+ polyCacheRef.current.set(sectorId, { sig, polys });
167
+ return polys;
168
+ },
169
+ [sectorSignature, sectorPiecePolysLive]
170
+ );
171
+
172
+ return {
173
+ pieces,
174
+ pieceById,
175
+ sectorPiecePolysLive,
176
+ getSectorPiecePolysCached
177
+ };
178
+ }
@@ -0,0 +1,76 @@
1
+ import type { Poly } from "@/core/domain/types";
2
+
3
+ /**
4
+ * Board Component Utilities
5
+ *
6
+ * This module provides utility functions used by board interaction hooks.
7
+ * These functions handle coordinate transformations and geometric operations
8
+ * needed for blueprint interaction and polygon scaling.
9
+ */
10
+
11
+ /**
12
+ * Convert world coordinates to blueprint local coordinates
13
+ *
14
+ * When a user clicks on a blueprint glyph in the blueprint ring, this function
15
+ * converts the world click coordinates to local coordinates within the blueprint's
16
+ * coordinate system. This is needed to maintain the clicked point under the cursor
17
+ * when spawning and dragging new pieces.
18
+ *
19
+ * @param px - World X coordinate (SVG space)
20
+ * @param py - World Y coordinate (SVG space)
21
+ * @param bpGeom - Blueprint geometry information
22
+ * @param bpGeom.bx - Blueprint center X in world space
23
+ * @param bpGeom.by - Blueprint center Y in world space
24
+ * @param bpGeom.cx - Blueprint local center X
25
+ * @param bpGeom.cy - Blueprint local center Y
26
+ * @returns Local coordinates within the blueprint's coordinate system
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const localPoint = blueprintLocalFromWorld(100, 200, {
31
+ * bx: 50, by: 50, // Blueprint is rendered at (50, 50) in world space
32
+ * cx: 25, cy: 25 // Blueprint's local center is at (25, 25)
33
+ * });
34
+ * // Result: { x: 75, y: 175 } - point in blueprint's local coordinate system
35
+ * ```
36
+ */
37
+ export function blueprintLocalFromWorld(
38
+ px: number,
39
+ py: number,
40
+ bpGeom: { bx: number; by: number; cx: number; cy: number }
41
+ ) {
42
+ return { x: px - bpGeom.bx + bpGeom.cx, y: py - bpGeom.by + bpGeom.cy };
43
+ }
44
+
45
+ /**
46
+ * Scale polygons by factor S about the origin (no translation/snapping)
47
+ *
48
+ * This function scales polygon coordinates by a factor S around the origin (0,0).
49
+ * Used for scaling silhouette masks and other geometric shapes when computing
50
+ * layouts with different scaling factors. The scaling is uniform (same factor
51
+ * for both X and Y axes).
52
+ *
53
+ * @param polys - Array of polygons to scale
54
+ * @param S - Scale factor (1.0 = no change, 0.5 = half size, 2.0 = double size)
55
+ * @returns New array of scaled polygons (original polygons are not modified)
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const originalPolys = [[
60
+ * { x: 10, y: 10 },
61
+ * { x: 20, y: 10 },
62
+ * { x: 15, y: 20 }
63
+ * ]];
64
+ *
65
+ * const scaledPolys = scalePolys(originalPolys, 2.0);
66
+ * // Result: [[
67
+ * // { x: 20, y: 20 },
68
+ * // { x: 40, y: 20 },
69
+ * // { x: 30, y: 40 }
70
+ * // ]]
71
+ * ```
72
+ */
73
+ export function scalePolys(polys: Poly[], S: number): Poly[] {
74
+ if (!polys || polys.length === 0) return [];
75
+ return polys.map(poly => poly.map(p => ({ x: S * p.x, y: S * p.y })));
76
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Core Components - Plugin-ready exports
3
+ *
4
+ * This module provides the main reusable components for jsPsych plugins.
5
+ * All components use dependency injection and have no hardcoded defaults.
6
+ */
7
+
8
+ // Core game board component
9
+ export { default as GameBoard, useGameBoard } from './board/GameBoard';
10
+ export type { GameBoardConfig, GameBoardProps } from './board/GameBoard';
11
+
12
+ // Blueprint ring component for plugin UI
13
+ export { default as BlueprintRing, useBlueprintRing } from './pieces/BlueprintRing';
14
+ export type { BlueprintRingProps } from './pieces/BlueprintRing';
15
+
16
+ // Legacy Board components (deprecated - use GameBoard instead)
17
+ // export { default as Board } from '../../react/components/Board';
18
+ // export { default as BoardView } from '../../react/components/BoardView';
19
+
20
+ // Re-export hooks for custom implementations
21
+ export { usePieceState } from './board/usePieceState';
22
+ export { useAnchorGrid } from './board/useAnchorGrid';
23
+ export { useDragController } from './board/useDragController';
24
+ export { useClickController } from './board/useClickController';
25
+
26
+ // Re-export types for plugin development
27
+ export type {
28
+ PieceData,
29
+ PieceStateHook,
30
+ AnchorDots,
31
+ DragControllerHook,
32
+ ClickControllerHook
33
+ } from './board';
@@ -0,0 +1,238 @@
1
+ /**
2
+ * BlueprintRing - Reusable blueprint selection interface component
3
+ *
4
+ * This component provides the central blueprint selection interface that displays
5
+ * available piece blueprints in a circular or semicircular arrangement around
6
+ * a center badge. It handles both primitive pieces and quickstash macros.
7
+ *
8
+ * ## Key Features
9
+ * - **Dual Mode Display**: Switches between primitives and quickstash collections
10
+ * - **Flexible Layout**: Adapts to circle and semicircle layout modes
11
+ * - **Interactive Badge**: Center button to toggle between piece collections
12
+ * - **Responsive Geometry**: Automatically calculates ring radius based on content
13
+ * - **Event Integration**: Provides structured callbacks for piece selection
14
+ *
15
+ * ## Architecture
16
+ * - Pure presentation component (no internal state management)
17
+ * - Receives all data and callbacks via props (dependency injection)
18
+ * - Computes ring geometry based on layout constraints
19
+ * - Renders SVG blueprint glyphs with proper scaling and positioning
20
+ *
21
+ * ## Usage
22
+ * Typically used within GameBoard or plugin components:
23
+ * ```typescript
24
+ * <BlueprintRing
25
+ * primitives={primitiveBlueprints}
26
+ * quickstash={macroBlueprints}
27
+ * currentView="primitives"
28
+ * layout={computedLayout}
29
+ * onBlueprintPointerDown={handleBlueprintSelection}
30
+ * onCenterBadgePointerDown={handleViewToggle}
31
+ * />
32
+ * ```
33
+ *
34
+ * @see {@link GameBoard} Primary container that uses this component
35
+ * @see {@link useBlueprintRing} Hook for managing blueprint ring state
36
+ * @since Phase 3.3 - Extracted from monolithic Board component
37
+ * @author Claude Code Assistant
38
+ */
39
+
40
+ import React from "react";
41
+ import type { Blueprint, TanKind, PrimitiveBlueprint } from "@/core/domain/types";
42
+ import type { CircleLayout } from "@/core/domain/layout";
43
+ import { boundsOfBlueprint } from "@/core/engine/geometry";
44
+ import { CONFIG } from "@/core/config/config";
45
+
46
+ function pathD(poly: any[]) {
47
+ return `M ${poly.map((pt: any) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
48
+ }
49
+
50
+ /**
51
+ * Props interface for BlueprintRing component
52
+ *
53
+ * This interface defines all the data and callbacks needed to render
54
+ * the blueprint selection interface. All props use dependency injection.
55
+ */
56
+ export interface BlueprintRingProps {
57
+ /** Array of primitive tangram piece blueprints */
58
+ primitives: PrimitiveBlueprint[];
59
+
60
+ /** Array of quickstash/macro piece blueprints */
61
+ quickstash: Blueprint[];
62
+
63
+ /**
64
+ * Current view mode for blueprint ring
65
+ * - "primitives": Show primitive tangram pieces
66
+ * - "quickstash": Show macro/composite pieces
67
+ */
68
+ currentView: "primitives" | "quickstash";
69
+
70
+ /** Computed circle layout containing geometry information */
71
+ layout: CircleLayout;
72
+
73
+ /** Radius of the center toggle badge in pixels */
74
+ badgeR: number;
75
+
76
+ /** Center position of the toggle badge */
77
+ badgeCenter: { x: number; y: number };
78
+
79
+ /** Maximum number of quickstash slots to display */
80
+ maxQuickstashSlots: number;
81
+
82
+ /** ID of currently dragging piece (null if none) - used for interaction state */
83
+ draggingId: string | null;
84
+
85
+ /**
86
+ * Callback fired when user starts interacting with a blueprint
87
+ * @param e - Pointer event from the interaction
88
+ * @param bp - Blueprint that was selected
89
+ * @param bpGeom - Geometry information for the selected blueprint
90
+ */
91
+ onBlueprintPointerDown: (
92
+ e: React.PointerEvent,
93
+ bp: Blueprint,
94
+ bpGeom: { bx: number; by: number; cx: number; cy: number }
95
+ ) => void;
96
+
97
+ /**
98
+ * Callback fired when user clicks the center badge to toggle views
99
+ * @param e - Pointer event from the badge click
100
+ */
101
+ onCenterBadgePointerDown: (e: React.PointerEvent) => void;
102
+
103
+ /**
104
+ * Helper function to lookup primitive blueprints by kind
105
+ * Used for rendering composite pieces that reference primitives
106
+ */
107
+ getPrimitive: (kind: TanKind | string) => PrimitiveBlueprint | undefined;
108
+ }
109
+
110
+ export default function BlueprintRing(props: BlueprintRingProps) {
111
+ const {
112
+ primitives,
113
+ quickstash,
114
+ currentView,
115
+ layout,
116
+ badgeR,
117
+ badgeCenter,
118
+ maxQuickstashSlots,
119
+ draggingId,
120
+ onBlueprintPointerDown,
121
+ onCenterBadgePointerDown,
122
+ getPrimitive
123
+ } = props;
124
+
125
+ // Determine which blueprints to show
126
+ const blueprints: Blueprint[] = currentView === "primitives" ? primitives : quickstash;
127
+
128
+ // Calculate ring geometry
129
+ const QS_SLOTS = maxQuickstashSlots;
130
+ const PRIM_SLOTS = primitives.length;
131
+ const slotsForView = currentView === "primitives" ? Math.max(1, PRIM_SLOTS) : Math.max(1, QS_SLOTS);
132
+ const sweep = layout.mode === "circle" ? Math.PI * 2 : Math.PI;
133
+ const delta = sweep / slotsForView;
134
+
135
+ const start = layout.mode === "circle" ? -Math.PI / 2 : Math.PI;
136
+ const blueprintTheta = (i: number) => start + (i + 0.5) * delta;
137
+
138
+ // Chord requirement calculation
139
+ const anchorsDiameterToPx = (anchorsDiag: number, gridPx: number = CONFIG.layout.grid.stepPx) =>
140
+ anchorsDiag * Math.SQRT2 * gridPx;
141
+ const reqAnchors = currentView === "primitives"
142
+ ? CONFIG.layout.constraints.primitiveDiamAnchors
143
+ : CONFIG.layout.constraints.quickstashDiamAnchors;
144
+ const D_slot = anchorsDiameterToPx(reqAnchors);
145
+ const R_needed = D_slot / (2 * Math.max(1e-9, Math.sin(delta / 2)));
146
+
147
+ // Minimum radius to avoid overlapping with badge
148
+ // Badge takes up: badgeR + margin, plus we need half the slot diameter for clearance
149
+ const R_min = badgeR + CONFIG.size.centerBadge.marginPx + D_slot / 2;
150
+ const ringMax = layout.innerR - (badgeR + CONFIG.size.centerBadge.marginPx);
151
+
152
+ // Clamp to [R_min, ringMax]
153
+ const blueprintRingR = Math.min(Math.max(R_needed, R_min), ringMax);
154
+
155
+ // Blueprint glyph renderer
156
+ const renderBlueprintGlyph = (bp: Blueprint, bx: number, by: number) => {
157
+ const bb = boundsOfBlueprint(bp, (k: string) => getPrimitive(k as TanKind)!);
158
+ const cx = bb.min.x + bb.width / 2;
159
+ const cy = bb.min.y + bb.height / 2;
160
+ const selected = false; // Future enhancement: add selection highlighting
161
+
162
+ return (
163
+ <g
164
+ key={bp.id}
165
+ transform={`translate(${bx}, ${by}) scale(1) translate(${-cx}, ${-cy})`}
166
+ >
167
+ {("shape" in bp ? bp.shape : []).map((poly, idx) => (
168
+ <path
169
+ key={idx}
170
+ d={pathD(poly)}
171
+ fill={CONFIG.color.blueprint.fill}
172
+ opacity={CONFIG.opacity.blueprint}
173
+ stroke={selected ? CONFIG.color.blueprint.selectedStroke : "none"}
174
+ strokeWidth={selected ? 2 : 0}
175
+ pointerEvents="visiblePainted"
176
+ style={{ cursor: "pointer" }}
177
+ onPointerDown={(e) => onBlueprintPointerDown(e as any, bp, { bx, by, cx, cy })}
178
+ />
179
+ ))}
180
+ </g>
181
+ );
182
+ };
183
+
184
+ return (
185
+ <g className="blueprint-ring">
186
+ {/* Center badge */}
187
+ <g
188
+ transform={`translate(${badgeCenter.x}, ${badgeCenter.y})`}
189
+ style={{ cursor: draggingId ? "default" : "pointer" }}
190
+ onPointerDown={onCenterBadgePointerDown}
191
+ >
192
+ <circle r={badgeR} fill={CONFIG.color.blueprint.badgeFill} />
193
+ <text
194
+ textAnchor="middle"
195
+ dominantBaseline="middle"
196
+ fontSize={CONFIG.size.badgeFontPx}
197
+ fill={CONFIG.color.blueprint.labelFill}
198
+ pointerEvents="none"
199
+ >
200
+ {currentView}
201
+ </text>
202
+ </g>
203
+
204
+ {/* Blueprint glyphs in ring formation */}
205
+ {blueprints.map((bp: Blueprint, i: number) => {
206
+ const theta = blueprintTheta(i);
207
+ const bx = layout.cx + blueprintRingR * Math.cos(theta);
208
+ const by = layout.cy + blueprintRingR * Math.sin(theta);
209
+ return renderBlueprintGlyph(bp, bx, by);
210
+ })}
211
+ </g>
212
+ );
213
+ }
214
+
215
+ /**
216
+ * Hook for managing blueprint ring state
217
+ * Useful for components that need more control over blueprint ring behavior
218
+ */
219
+ export function useBlueprintRing(
220
+ primitives: PrimitiveBlueprint[],
221
+ quickstash: Blueprint[],
222
+ initialView: "primitives" | "quickstash" = "quickstash"
223
+ ) {
224
+ const [currentView, setCurrentView] = React.useState<"primitives" | "quickstash">(initialView);
225
+
226
+ const switchView = React.useCallback(() => {
227
+ setCurrentView(prev => prev === "primitives" ? "quickstash" : "primitives");
228
+ }, []);
229
+
230
+ const blueprints = currentView === "primitives" ? primitives : quickstash;
231
+
232
+ return {
233
+ currentView,
234
+ blueprints,
235
+ switchView,
236
+ setCurrentView
237
+ };
238
+ }
@@ -0,0 +1,85 @@
1
+ // src/core/config/config.ts
2
+ export type Config = {
3
+ color: {
4
+ bands: {
5
+ silhouette: { fillEven: string; fillOdd: string; stroke: string };
6
+ workspace: { fillEven: string; fillOdd: string; stroke: string };
7
+ };
8
+ completion: { fill: string; stroke: string };
9
+ silhouetteMask: string;
10
+ anchors: { invalid: string; valid: string };
11
+ piece: { draggingFill: string; validFill: string; invalidFill: string; invalidStroke: string; selectedStroke: string; allGreenStroke: string; borderStroke: string };
12
+ ui: { light: string; dark: string };
13
+ blueprint: { fill: string; selectedStroke: string; badgeFill: string; labelFill: string };
14
+ };
15
+ opacity: {
16
+ blueprint: number;
17
+ silhouetteMask: number;
18
+ anchors: { invalid: number; valid: number };
19
+ piece: { invalid: number; dragging: number; locked: number; normal: number };
20
+ };
21
+ size: {
22
+ stroke: { bandPx: number; pieceSelectedPx: number; allGreenStrokePx: number; pieceBorderPx: number };
23
+ anchorRadiusPx: { valid: number; invalid: number };
24
+ badgeFontPx: number;
25
+ centerBadge: { fractionOfOuterR: number; minPx: number; marginPx: number };
26
+ };
27
+ layout: {
28
+ grid: { stepPx: number; unitPx: number };
29
+ paddingPx: number;
30
+ /** renamed from capacity → constraints */
31
+ constraints: {
32
+ workspaceDiamAnchors: number;
33
+ quickstashDiamAnchors: number;
34
+ primitiveDiamAnchors: number;
35
+ };
36
+ defaults: { maxQuickstashSlots: number };
37
+ };
38
+ game: {
39
+ snapRadiusPx: number;
40
+ showBorders: boolean;
41
+ hideTouchingBorders: boolean;
42
+ };
43
+ };
44
+
45
+ export const CONFIG: Config = {
46
+ color: {
47
+ bands: {
48
+ silhouette: { fillEven: "#eef2ff", fillOdd: "#f6f7fb", stroke: "#c7d2fe" },
49
+ workspace: { fillEven: "#f3f4f6", fillOdd: "#f9fafb", stroke: "#e5e7eb" }
50
+ },
51
+ completion: { fill: "#dcfce7", stroke: "#86efac" },
52
+ silhouetteMask: "#94a3b8",
53
+ anchors: { invalid: "#7dd3fc", valid: "#475569" },
54
+ piece: { draggingFill: "#1d4ed8", validFill: "#60a5fa", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#111827", allGreenStroke: "#86efac", borderStroke: "#374151" },
55
+ ui: { light: "#60a5fa", dark: "#1d4ed8" },
56
+ blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#eef2ff", labelFill: "#374151" }
57
+ },
58
+ opacity: {
59
+ blueprint: 0.95,
60
+ silhouetteMask: 0.45,
61
+ anchors: { valid: 0.80, invalid: 0.50 },
62
+ piece: { invalid: 0.35, dragging: 0.60, locked: 0.70, normal: 0.95 },
63
+ },
64
+ size: {
65
+ stroke: { bandPx: 1, pieceSelectedPx: 1.5, allGreenStrokePx: 6, pieceBorderPx: 1 },
66
+ anchorRadiusPx: { valid: 1.0, invalid: 1.0 },
67
+ badgeFontPx: 12,
68
+ centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
69
+ },
70
+ layout: {
71
+ grid: { stepPx: 20, unitPx: 40 },
72
+ paddingPx: 1,
73
+ constraints: {
74
+ workspaceDiamAnchors: 10, // num anchors req'd to be on diagonal
75
+ quickstashDiamAnchors: 7, // num anchors req'd to be in single quickstash slot
76
+ primitiveDiamAnchors: 5,
77
+ },
78
+ defaults: { maxQuickstashSlots: 1 }
79
+ },
80
+ game: {
81
+ snapRadiusPx: 15,
82
+ showBorders: true,
83
+ hideTouchingBorders: true
84
+ }
85
+ };
@@ -0,0 +1,25 @@
1
+ import type {
2
+ Blueprint, PrimitiveBlueprint, CompositeBlueprint, TanKind
3
+ } from "./types";
4
+
5
+ export function isComposite(bp: Blueprint): bp is CompositeBlueprint {
6
+ return (bp as CompositeBlueprint).parts !== undefined;
7
+ }
8
+ export function isPrimitive(bp: Blueprint): bp is PrimitiveBlueprint {
9
+ return !isComposite(bp);
10
+ }
11
+
12
+ export class BlueprintRegistry {
13
+ private byId = new Map<string, Blueprint>();
14
+ private primitivesByKind = new Map<TanKind, PrimitiveBlueprint>();
15
+
16
+ registerAll(bps: Blueprint[]) {
17
+ for (const bp of bps) this.register(bp);
18
+ }
19
+ register(bp: Blueprint) {
20
+ this.byId.set(bp.id, bp);
21
+ if (isPrimitive(bp)) this.primitivesByKind.set(bp.kind, bp);
22
+ }
23
+ get(id: string) { return this.byId.get(id); }
24
+ getPrimitive(kind: TanKind | string) { return this.primitivesByKind.get(kind as TanKind); }
25
+ }