castle-web-cli 0.4.177 → 0.4.179

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 (40) hide show
  1. package/dist/agent-failures.js +4 -4
  2. package/dist/agent-prompts.d.ts +9 -1
  3. package/dist/agent-prompts.js +15 -2
  4. package/dist/agent.js +125 -40
  5. package/dist/deckLocatorShape.d.ts +16 -0
  6. package/dist/deckLocatorShape.js +28 -0
  7. package/dist/editorConfig.d.ts +1 -0
  8. package/dist/shell/assets/index-C5DS_7UM.js +447 -0
  9. package/dist/shell/assets/index-CoU3ETYM.css +1 -0
  10. package/dist/shell/index.html +2 -2
  11. package/kits/base/CLAUDE.md +3 -0
  12. package/kits/base/castle.json +21 -11
  13. package/kits/physics-2d/CLAUDE.md +41 -8
  14. package/kits/physics-2d/behaviors/AnalogStick.jsx +80 -4
  15. package/kits/physics-2d/behaviors/Slingshot.jsx +14 -2
  16. package/kits/physics-2d/behaviors/Sprite.jsx +17 -8
  17. package/kits/physics-2d/behaviors/Style.jsx +270 -0
  18. package/kits/physics-2d/behaviors/Text.jsx +213 -0
  19. package/kits/physics-2d/behaviors/Video.jsx +8 -4
  20. package/kits/physics-2d/blueprints/text.scene +12 -0
  21. package/kits/physics-2d/castle.json +10 -6
  22. package/kits/physics-2d/editors/SceneEditor.jsx +41 -0
  23. package/kits/physics-2d/editors/deckFont.js +65 -55
  24. package/kits/physics-2d/editors/fontPreview.js +6 -38
  25. package/kits/physics-2d/editors/pixelInspector.jsx +4 -154
  26. package/kits/physics-2d/engine/blueprint.js +28 -0
  27. package/kits/physics-2d/engine/fonts.js +125 -23
  28. package/kits/physics-2d/engine/paletteField.jsx +235 -0
  29. package/kits/physics-2d/engine/physics/controls.js +5 -81
  30. package/kits/physics-2d/engine/popoverDismiss.js +17 -0
  31. package/kits/physics-2d/engine/scene.js +12 -3
  32. package/kits/physics-2d/engine/spriteField.jsx +2 -15
  33. package/kits/physics-2d/engine/tap.js +90 -0
  34. package/kits/physics-2d/engine/text.js +94 -0
  35. package/kits/physics-2d/engine/ui.jsx +20 -0
  36. package/kits/physics-2d/engine/ui.module.css +10 -2
  37. package/kits/physics-3d/castle.json +4 -2
  38. package/package.json +1 -1
  39. package/dist/shell/assets/index-BWOEraUy.js +0 -447
  40. package/dist/shell/assets/index-BkVF1OXc.css +0 -1
@@ -0,0 +1,90 @@
1
+ import { TAP_CLAIM } from './physics/controls';
2
+
3
+ // Built-in tap. Any enabled behavior may implement `onTap(actor, scene)`; the
4
+ // actor is then a tap target with no extra component. Fires on release inside
5
+ // the hit zone (Collider when present, else the axis-aligned Layout box).
6
+ // Screen-space Text uses screen coords so a HUD label hits where it draws.
7
+ //
8
+ // Claims at TAP_CLAIM: beats a greedy control (stick, grab-anywhere sling) so
9
+ // the finger isn't also those, and yields to a targeted control (Draggable, a
10
+ // sling pressed on its own body) that claims later the same frame.
11
+
12
+ export function updateTaps(scene) {
13
+ for (const actor of scene.actors.values()) {
14
+ const id = actor.runtime?._tapPointerId;
15
+ if (id == null) continue;
16
+ const held = scene.pointers.get(id);
17
+ if (held?.down) {
18
+ if (!scene.ownsPointer(id, actor.id)) clearTap(actor);
19
+ else actor.runtime._tapInside = hits(actor, scene, held);
20
+ continue;
21
+ }
22
+ const stillOwned = held ? scene.ownsPointer(id, actor.id) : true;
23
+ const inside = held ? hits(actor, scene, held) : Boolean(actor.runtime._tapInside);
24
+ if (stillOwned && inside && hasOnTap(scene, actor)) fireTap(scene, actor);
25
+ clearTap(actor);
26
+ }
27
+
28
+ for (const pointer of scene.pointers.values()) {
29
+ if (!pointer.justPressed) continue;
30
+ const actor = topTapActor(scene, pointer);
31
+ if (!actor) continue;
32
+ if (!scene.claimPointer(pointer.id, actor.id, TAP_CLAIM)) continue;
33
+ actor.runtime._tapPointerId = pointer.id;
34
+ actor.runtime._tapInside = true;
35
+ // A click that ends before the next frame is justPressed with down:false.
36
+ if (!pointer.down) {
37
+ if (scene.ownsPointer(pointer.id, actor.id)) fireTap(scene, actor);
38
+ clearTap(actor);
39
+ }
40
+ }
41
+ }
42
+
43
+ function fireTap(scene, actor) {
44
+ scene.forEachBehavior(actor, (inst) => inst.onTap?.(actor, scene));
45
+ }
46
+
47
+ function clearTap(actor) {
48
+ actor.runtime._tapPointerId = null;
49
+ actor.runtime._tapInside = false;
50
+ }
51
+
52
+ function hasOnTap(scene, actor) {
53
+ for (const [name, props] of Object.entries(actor.components ?? {})) {
54
+ if (!props || props.$enabled === false) continue;
55
+ const Behavior = scene.behaviors.get(name);
56
+ if (typeof Behavior?.prototype?.onTap === 'function') return true;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ function topTapActor(scene, pointer) {
62
+ let best = null;
63
+ let bestZ = -Infinity;
64
+ for (const actor of scene.actors.values()) {
65
+ if (!hasOnTap(scene, actor)) continue;
66
+ const z = actor.components?.Layout?.z ?? 0;
67
+ if (z < bestZ) continue;
68
+ if (!hits(actor, scene, pointer)) continue;
69
+ best = actor;
70
+ bestZ = z;
71
+ }
72
+ return best;
73
+ }
74
+
75
+ function pointerCoords(actor, pointer) {
76
+ if (actor.components?.Text?.space === 'screen') {
77
+ return { x: pointer.screenX, y: pointer.screenY };
78
+ }
79
+ return { x: pointer.x, y: pointer.y };
80
+ }
81
+
82
+ function hits(actor, scene, pointer) {
83
+ const { x, y } = pointerCoords(actor, pointer);
84
+ if (actor.components?.Collider && scene.physics?.containsPoint) {
85
+ return scene.physics.containsPoint(actor, x, y);
86
+ }
87
+ const layout = actor.components?.Layout;
88
+ if (!layout) return false;
89
+ return x >= layout.x && x <= layout.x + layout.width && y >= layout.y && y <= layout.y + layout.height;
90
+ }
@@ -0,0 +1,94 @@
1
+ // Canvas text layout helpers shared by the Text behavior and editor chrome.
2
+ // Pure: no imports. Callers set `ctx.font` before wrapText / pass size+font into
3
+ // drawTextBlock.
4
+
5
+ // Greedy word-wrap against the ctx's current font. Paragraphs split on `\n`
6
+ // (after normalizing `\r\n`); an empty paragraph yields an empty line. A single
7
+ // word wider than maxWidth stays on its own line and overflows.
8
+ export function wrapText(ctx, text, maxWidth) {
9
+ const source = String(text ?? '').replace(/\r\n/g, '\n');
10
+ const paragraphs = source.split('\n');
11
+ const lines = [];
12
+ for (const paragraph of paragraphs) {
13
+ if (!paragraph) {
14
+ lines.push('');
15
+ continue;
16
+ }
17
+ const words = paragraph.split(/\s+/);
18
+ let line = '';
19
+ for (const word of words) {
20
+ if (!word) continue;
21
+ const next = line ? `${line} ${word}` : word;
22
+ if (line && ctx.measureText(next).width > maxWidth) {
23
+ lines.push(line);
24
+ line = word;
25
+ } else {
26
+ line = next;
27
+ }
28
+ }
29
+ lines.push(line);
30
+ }
31
+ return lines;
32
+ }
33
+
34
+ const ALIGN_X = { left: 0, center: 0.5, right: 1 };
35
+ const ALIGN_Y = { top: 0, middle: 0.5, bottom: 1 };
36
+
37
+ // Draw wrapped, aligned text into a box. No vertical clipping — lines that
38
+ // overrun the box still paint (OG Text parity). Returns the laid-out lines
39
+ // and the resolved line height in card units.
40
+ export function drawTextBlock(
41
+ ctx,
42
+ {
43
+ x,
44
+ y,
45
+ width,
46
+ height,
47
+ text,
48
+ size = 24,
49
+ font = 'sans-serif',
50
+ color = '#131313',
51
+ align = 'left',
52
+ verticalAlign = 'top',
53
+ padding = 0,
54
+ lineHeight = 1.25,
55
+ outlineWidth = 0,
56
+ outlineColor = '#ffffff',
57
+ }
58
+ ) {
59
+ const pad = Math.max(0, Number(padding) || 0);
60
+ const innerW = Math.max(0, width - pad * 2);
61
+ const innerH = Math.max(0, height - pad * 2);
62
+ const fontSize = Number.isFinite(size) ? size : 24;
63
+ const lineH = fontSize * (Number.isFinite(lineHeight) ? lineHeight : 1.25);
64
+
65
+ ctx.save();
66
+ ctx.font = `${fontSize}px ${font}`;
67
+ ctx.textAlign = align === 'center' || align === 'right' ? align : 'left';
68
+ ctx.textBaseline = 'middle';
69
+
70
+ const lines = wrapText(ctx, text, innerW);
71
+ const blockH = lines.length * lineH;
72
+ const yFactor = ALIGN_Y[verticalAlign] ?? 0;
73
+ const blockTop = y + pad + (innerH - blockH) * yFactor;
74
+ const xFactor = ALIGN_X[align] ?? 0;
75
+ const anchorX = x + pad + innerW * xFactor;
76
+
77
+ const stroke = Number(outlineWidth) > 0 && Boolean(outlineColor);
78
+ const fill = Boolean(color);
79
+ if (stroke) {
80
+ ctx.lineWidth = outlineWidth;
81
+ ctx.strokeStyle = outlineColor;
82
+ ctx.lineJoin = 'round';
83
+ ctx.miterLimit = 2;
84
+ }
85
+ if (fill) ctx.fillStyle = color;
86
+
87
+ for (let i = 0; i < lines.length; i++) {
88
+ const ly = blockTop + lineH * (i + 0.5);
89
+ if (stroke) ctx.strokeText(lines[i], anchorX, ly);
90
+ if (fill) ctx.fillText(lines[i], anchorX, ly);
91
+ }
92
+ ctx.restore();
93
+ return { lines, lineHeight: lineH };
94
+ }
@@ -419,6 +419,26 @@ export function TextField({ label, value, onChange, overridden, defaultValue, on
419
419
  </FieldRow>
420
420
  );
421
421
  }
422
+ export function TextAreaField({
423
+ label,
424
+ value,
425
+ onChange,
426
+ rows = 3,
427
+ overridden,
428
+ defaultValue,
429
+ onReset,
430
+ }) {
431
+ return (
432
+ <FieldRow label={label} overridden={overridden} defaultValue={defaultValue} onReset={onReset}>
433
+ <textarea
434
+ className={styles.textarea}
435
+ rows={rows}
436
+ value={value ?? ''}
437
+ onChange={(event) => onChange(event.target.value)}
438
+ />
439
+ </FieldRow>
440
+ );
441
+ }
422
442
  function numberText(v) {
423
443
  return Number.isFinite(v) ? String(v) : '0';
424
444
  }
@@ -1773,6 +1773,7 @@
1773
1773
 
1774
1774
  .fieldRowOverridden .input,
1775
1775
  .fieldRowOverridden .select,
1776
+ .fieldRowOverridden .textarea,
1776
1777
  .fieldRowOverridden .colorInput,
1777
1778
  .fieldRowOverridden .checkbox {
1778
1779
  border-color: var(--castle-override-border);
@@ -1883,6 +1884,13 @@
1883
1884
  color: var(--castle-inspector-muted);
1884
1885
  }
1885
1886
 
1887
+ /* PaletteColorField: swatch + hex in the closed chrome. */
1888
+ .paletteColorField {
1889
+ gap: 8px;
1890
+ padding-left: 6px;
1891
+ min-height: 32px;
1892
+ }
1893
+
1886
1894
  /* SpriteField: closed chrome carries a tiny thumb beside the basename so you
1887
1895
  can see which art is selected without opening the picker. */
1888
1896
  .spriteField {
@@ -2326,8 +2334,8 @@
2326
2334
 
2327
2335
  .textarea {
2328
2336
  min-height: 0;
2329
- resize: none;
2330
- font-family: var(--castle-font-mono);
2337
+ resize: vertical;
2338
+ font-family: inherit;
2331
2339
  font-size: 14px;
2332
2340
  line-height: 1.45;
2333
2341
  }
@@ -36,7 +36,8 @@
36
36
  "new": "New scene",
37
37
  "icon": "globe",
38
38
  "editor": "kit",
39
- "data": true
39
+ "data": true,
40
+ "width": 650
40
41
  },
41
42
  {
42
43
  "ext": ".pxmodel",
@@ -44,7 +45,8 @@
44
45
  "new": "New model",
45
46
  "icon": "cube-transparent",
46
47
  "editor": "kit",
47
- "data": true
48
+ "data": true,
49
+ "width": 480
48
50
  },
49
51
  {
50
52
  "ext": ".jsx",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.177",
3
+ "version": "0.4.179",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/castle-xyz/castle-experimental-web.git"