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.
- package/dist/agent-failures.js +4 -4
- package/dist/agent-prompts.d.ts +9 -1
- package/dist/agent-prompts.js +15 -2
- package/dist/agent.js +125 -40
- package/dist/deckLocatorShape.d.ts +16 -0
- package/dist/deckLocatorShape.js +28 -0
- package/dist/editorConfig.d.ts +1 -0
- package/dist/shell/assets/index-C5DS_7UM.js +447 -0
- package/dist/shell/assets/index-CoU3ETYM.css +1 -0
- package/dist/shell/index.html +2 -2
- package/kits/base/CLAUDE.md +3 -0
- package/kits/base/castle.json +21 -11
- package/kits/physics-2d/CLAUDE.md +41 -8
- package/kits/physics-2d/behaviors/AnalogStick.jsx +80 -4
- package/kits/physics-2d/behaviors/Slingshot.jsx +14 -2
- package/kits/physics-2d/behaviors/Sprite.jsx +17 -8
- package/kits/physics-2d/behaviors/Style.jsx +270 -0
- package/kits/physics-2d/behaviors/Text.jsx +213 -0
- package/kits/physics-2d/behaviors/Video.jsx +8 -4
- package/kits/physics-2d/blueprints/text.scene +12 -0
- package/kits/physics-2d/castle.json +10 -6
- package/kits/physics-2d/editors/SceneEditor.jsx +41 -0
- package/kits/physics-2d/editors/deckFont.js +65 -55
- package/kits/physics-2d/editors/fontPreview.js +6 -38
- package/kits/physics-2d/editors/pixelInspector.jsx +4 -154
- package/kits/physics-2d/engine/blueprint.js +28 -0
- package/kits/physics-2d/engine/fonts.js +125 -23
- package/kits/physics-2d/engine/paletteField.jsx +235 -0
- package/kits/physics-2d/engine/physics/controls.js +5 -81
- package/kits/physics-2d/engine/popoverDismiss.js +17 -0
- package/kits/physics-2d/engine/scene.js +12 -3
- package/kits/physics-2d/engine/spriteField.jsx +2 -15
- package/kits/physics-2d/engine/tap.js +90 -0
- package/kits/physics-2d/engine/text.js +94 -0
- package/kits/physics-2d/engine/ui.jsx +20 -0
- package/kits/physics-2d/engine/ui.module.css +10 -2
- package/kits/physics-3d/castle.json +4 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-BWOEraUy.js +0 -447
- 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:
|
|
2330
|
-
font-family:
|
|
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",
|