castle-web-cli 0.4.115 → 0.4.116

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 (55) hide show
  1. package/dist/agent-prompts.js +2 -2
  2. package/dist/agent.d.ts +9 -9
  3. package/dist/agent.js +590 -589
  4. package/dist/castleJson.d.ts +6 -0
  5. package/dist/castleJson.js +10 -0
  6. package/dist/editorConfig.js +27 -3
  7. package/dist/imports.d.ts +4 -0
  8. package/dist/imports.js +59 -10
  9. package/dist/init.d.ts +3 -0
  10. package/dist/init.js +99 -87
  11. package/dist/install.d.ts +1 -1
  12. package/dist/install.js +26 -24
  13. package/dist/shell/assets/{index-CUamb8rK.js → index-DiPlPGyg.js} +2 -2
  14. package/dist/shell/index.html +1 -1
  15. package/dist/vitePlugins.js +1 -1
  16. package/kits/physics-2d/CLAUDE.md +28 -16
  17. package/kits/physics-2d/behaviors/Joints.jsx +1 -1
  18. package/kits/physics-2d/behaviors/Sprite.jsx +17 -12
  19. package/kits/physics-2d/blueprints/ball.scene +1 -1
  20. package/kits/physics-2d/blueprints/block.scene +1 -1
  21. package/kits/physics-2d/blueprints/cauldron.scene +1 -1
  22. package/kits/physics-2d/blueprints/crate.scene +1 -1
  23. package/kits/physics-2d/castle.json +11 -3
  24. package/kits/physics-2d/docs/pxart-format.md +537 -51
  25. package/kits/physics-2d/editors/PxArtEditor.jsx +1794 -65
  26. package/kits/physics-2d/editors/brushFit.js +535 -0
  27. package/kits/physics-2d/editors/brushShapes.js +140 -0
  28. package/kits/physics-2d/editors/mediaFile.js +12 -1
  29. package/kits/physics-2d/editors/pathOverlay.js +340 -0
  30. package/kits/physics-2d/editors/pathTools.js +1906 -0
  31. package/kits/physics-2d/editors/pixelCanvas.js +13 -0
  32. package/kits/physics-2d/editors/pixelEditorChrome.jsx +2 -2
  33. package/kits/physics-2d/editors/pixelGeometry.js +4 -2
  34. package/kits/physics-2d/editors/pixelInspector.jsx +410 -37
  35. package/kits/physics-2d/editors/pxArtEditorModel.js +172 -16
  36. package/kits/physics-2d/editors/pxArtTimeline.jsx +163 -43
  37. package/kits/physics-2d/editors/pxArtTimeline.module.css +31 -5
  38. package/kits/physics-2d/engine/assets.js +1 -1
  39. package/kits/physics-2d/engine/blueprint.js +3 -3
  40. package/kits/physics-2d/engine/files.js +3 -1
  41. package/kits/physics-2d/engine/liveReload.js +1 -1
  42. package/kits/physics-2d/engine/physics/jointArt.js +3 -3
  43. package/kits/physics-2d/engine/pxart.js +153 -35
  44. package/kits/physics-2d/engine/pxartPath.js +1356 -0
  45. package/kits/physics-2d/engine/pxartSmooth.js +276 -125
  46. package/kits/physics-2d/engine/ui.jsx +22 -1
  47. package/kits/physics-2d/engine/ui.module.css +36 -12
  48. package/kits/physics-2d/package-lock.json +1 -1
  49. package/kits/physics-2d/scripts/draw.mjs +7 -7
  50. package/kits/physics-2d/scripts/import-svg.mjs +1231 -0
  51. package/kits/physics-2d/scripts/svg-emission-guide.md +92 -0
  52. package/package.json +1 -1
  53. /package/kits/physics-2d/drawings/{block.pxart → block.sprite} +0 -0
  54. /package/kits/physics-2d/drawings/{cauldron.pxart → cauldron.sprite} +0 -0
  55. /package/kits/physics-2d/drawings/{joint-rope.pxart → joint-rope.sprite} +0 -0
@@ -0,0 +1,340 @@
1
+ import { absoluteSegment, subpathEdgeFrom, subpathPoints } from '../engine/pxartPath.js';
2
+ import { primitiveHandleIndices } from '../engine/pxartPath.js';
3
+ import { anchorKey, coincidentSiteKeys, isOpenPenShape } from './pathTools';
4
+
5
+ // Render-only overlay for path-layer tools: selection chrome (outline + anchor
6
+ // dots), pen rubber-band to hover, fill-tool hover preview, and shape marquee.
7
+
8
+ // Every size below is CSS pixels — drawPathOverlay scales the context by the
9
+ // device pixel ratio, so chrome covers the same amount of screen whatever the
10
+ // display. Sizing in backing-store pixels instead, as this file used to, halves
11
+ // the whole overlay on a 2x screen: the same trap renderPixelOverlay avoids by
12
+ // multiplying its widths by dpr.
13
+ const ANCHOR_R = 6;
14
+ const ANCHOR_HOVER_R = 7;
15
+ const JUNCTION_R = 11;
16
+ const HAIRLINE = 1.25;
17
+ const SKELETON_W = 2.5;
18
+ const JUNCTION_INNER_W = 1.5;
19
+ const EDGE_W = 3.25;
20
+ const RUBBER_W = 1.75;
21
+ const MARQUEE_W = 1;
22
+ const DASH = [4, 4];
23
+
24
+ const FILL_TOOL_WASH_ALPHA = 0.45;
25
+
26
+ // Blue means picked. A selected shape's skeleton is neutral so that the blue is
27
+ // free to say which part of it the next edit will act on — without that, the
28
+ // whole outline shouting the selection colour left edge picking no colour of
29
+ // its own to use.
30
+ const SKELETON = '#9aa0a6';
31
+ const PICKED = '#00d4ff';
32
+ const HOVER_YELLOW = '#ffd23f';
33
+ const INK = '#1c1c1c';
34
+ // Hovering an edge previews picking it, so it takes the pick colour held back
35
+ // to a wash rather than a second hue.
36
+ const HOVER_ALPHA = 0.45;
37
+
38
+ function toCanvas(sx, sy, x, y) {
39
+ return [x * sx, y * sy];
40
+ }
41
+
42
+ export function drawPathOverlay(canvas, resolution, path, ui) {
43
+ if (!canvas || !resolution) return;
44
+ const ctx = canvas.getContext('2d');
45
+ if (!ctx) return;
46
+ const cssW = canvas.clientWidth;
47
+ const cssH = canvas.clientHeight;
48
+ if (!cssW || !cssH) return;
49
+ const dpr = window.devicePixelRatio || 1;
50
+ const bw = Math.round(cssW * dpr);
51
+ const bh = Math.round(cssH * dpr);
52
+ if (canvas.width !== bw) canvas.width = bw;
53
+ if (canvas.height !== bh) canvas.height = bh;
54
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
55
+ ctx.clearRect(0, 0, bw, bh);
56
+ // From here on the drawing units are CSS pixels while the backing store stays
57
+ // at device resolution, so the chrome is both crisp and a fixed screen size.
58
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
59
+
60
+ const sx = cssW / resolution.width;
61
+ const sy = cssH / resolution.height;
62
+ const at = (x, y) => toCanvas(sx, sy, x, y);
63
+
64
+ const selected = new Set(ui?.selectedShapeIndices ?? []);
65
+ if (!selected.size && ui?.selectedShapeIndex != null) selected.add(ui.selectedShapeIndex);
66
+ const soleSelected = selected.size === 1 ? [...selected][0] : null;
67
+
68
+ // Three passes per shape, in the order they have to stack: the neutral
69
+ // skeleton, then the picked edges recolouring the runs they cover, then the
70
+ // anchors on top of both.
71
+ for (const shapeIndex of selected) {
72
+ const shape = path?.shapes?.[shapeIndex];
73
+ if (!shape) continue;
74
+ const subs = shape.subpaths ?? [];
75
+ for (const sub of subs) strokeSubpath(ctx, sub, at, SKELETON, SKELETON_W, 1);
76
+ if (ui?.tool === 'path-select') paintPickedEdges(ctx, subs, shapeIndex, at, ui);
77
+ if (soleSelected === shapeIndex) {
78
+ const junctions = coincidentSiteKeys(shape);
79
+ subs.forEach((sub, subIndex) => {
80
+ drawAnchors(ctx, sub, at, ui, subIndex, primitiveHandleIndices(shape), junctions);
81
+ });
82
+ }
83
+ }
84
+
85
+ const penMode = ui?.tool === 'brush' && ui?.brushMode === 'pen';
86
+
87
+ if ((penMode || ui?.tool === 'bend') && ui.hoverInsertEdge) {
88
+ const { shapeIndex, subIndex, edgeIndex } = ui.hoverInsertEdge;
89
+ const sub = path?.shapes?.[shapeIndex]?.subpaths?.[subIndex];
90
+ if (sub) strokeEdge(ctx, sub, edgeIndex, at, HOVER_YELLOW, EDGE_W, 1);
91
+ }
92
+
93
+ if (penMode && ui.hoverPoint && !ui.hoverInsertEdge && soleSelected != null) {
94
+ const live = path?.shapes?.[soleSelected];
95
+ const limbSub = ui.penLimb?.shapeIndex === soleSelected ? ui.penLimb.subIndex : -1;
96
+ const sub =
97
+ limbSub >= 0 ? live?.subpaths?.[limbSub] : isOpenPenShape(live) ? live.subpaths[0] : null;
98
+ const pts = sub && !sub.closed ? subpathPoints(sub) : [];
99
+ if (pts.length) {
100
+ const last = pts[pts.length - 1];
101
+ const from = at(last[0], last[1]);
102
+ const to = at(ui.hoverPoint[0], ui.hoverPoint[1]);
103
+ ctx.save();
104
+ ctx.globalAlpha = 0.9;
105
+ ctx.strokeStyle = HOVER_YELLOW;
106
+ ctx.lineWidth = RUBBER_W;
107
+ ctx.setLineDash(DASH);
108
+ ctx.beginPath();
109
+ ctx.moveTo(from[0], from[1]);
110
+ ctx.lineTo(to[0], to[1]);
111
+ ctx.stroke();
112
+ ctx.restore();
113
+ }
114
+ }
115
+ if (ui?.tool === 'brush' && ui.hoverPoint) {
116
+ drawGhostAnchor(ctx, at, ui.hoverPoint);
117
+ }
118
+ drawBrushGesture(ctx, at, ui);
119
+ drawFillToolPreview(ctx, path, ui, at, sx, sy);
120
+ drawShapeMarquee(ctx, ui?.shapeMarquee, sx, sy);
121
+ }
122
+
123
+ /**
124
+ * Repaint hovered and picked edges over the skeleton. Slightly wider than the
125
+ * skeleton so the blue covers the grey's antialiased fringe instead of leaving
126
+ * it showing along both sides of a picked run.
127
+ */
128
+ function paintPickedEdges(ctx, subs, shapeIndex, at, ui) {
129
+ const hover = ui.hoverEdge;
130
+ if (hover?.shapeIndex === shapeIndex) {
131
+ const sub = subs[hover.subIndex];
132
+ if (sub) strokeEdge(ctx, sub, hover.edgeIndex, at, PICKED, EDGE_W, HOVER_ALPHA);
133
+ }
134
+ const picked = ui.selectedEdges;
135
+ if (picked?.shapeIndex !== shapeIndex) return;
136
+ for (const { subIndex, edgeIndex } of picked.edges ?? []) {
137
+ const sub = subs[subIndex];
138
+ if (sub) strokeEdge(ctx, sub, edgeIndex, at, PICKED, EDGE_W, 1);
139
+ }
140
+ }
141
+
142
+ function appendSubpathToPath2D(geometry, sub, at) {
143
+ if (!sub?.start) return false;
144
+ const first = at(sub.start[0], sub.start[1]);
145
+ geometry.moveTo(first[0], first[1]);
146
+ let from = sub.start;
147
+ for (const seg of sub.segs ?? []) {
148
+ appendSegment(geometry, from, seg, at);
149
+ from = seg.to;
150
+ }
151
+ if (sub.closed) geometry.closePath();
152
+ return true;
153
+ }
154
+
155
+ function buildShapeFillPath2D(shape, at) {
156
+ const geometry = new Path2D();
157
+ let any = false;
158
+ for (const sub of shape.subpaths ?? []) {
159
+ if (!sub.closed) continue;
160
+ if (appendSubpathToPath2D(geometry, sub, at)) any = true;
161
+ }
162
+ return any ? geometry : null;
163
+ }
164
+
165
+ function drawFillToolPreview(ctx, path, ui, at, sx, sy) {
166
+ if (ui?.tool !== 'fill') return;
167
+ const target = ui.fillHoverTarget;
168
+ const color = ui.activeColor;
169
+ if (!target || target.shapeIndex < 0 || !color) return;
170
+ const shape = path?.shapes?.[target.shapeIndex];
171
+ if (!shape) return;
172
+
173
+ ctx.save();
174
+ if (target.slot === 'fill' && shape.fill) {
175
+ const geometry = buildShapeFillPath2D(shape, at);
176
+ if (geometry) {
177
+ ctx.fillStyle = color;
178
+ ctx.globalAlpha = FILL_TOOL_WASH_ALPHA;
179
+ ctx.fill(geometry, 'evenodd');
180
+ }
181
+ } else if (target.slot === 'stroke') {
182
+ const lineWidth = ui.smoothFinish ? (sx + sy) / 2 : SKELETON_W;
183
+ ctx.globalAlpha = 0.95;
184
+ if (ui.smoothFinish) {
185
+ ctx.lineCap = 'round';
186
+ ctx.lineJoin = 'round';
187
+ }
188
+ for (const sub of shape.subpaths ?? []) {
189
+ strokeSubpath(ctx, sub, at, color, lineWidth, 1);
190
+ }
191
+ }
192
+ ctx.restore();
193
+ }
194
+
195
+ function drawGhostAnchor(ctx, at, point) {
196
+ const [x, y] = at(point[0], point[1]);
197
+ ctx.save();
198
+ ctx.globalAlpha = 0.9;
199
+ ctx.lineWidth = HAIRLINE;
200
+ ctx.setLineDash([]);
201
+ ctx.beginPath();
202
+ ctx.arc(x, y, ANCHOR_R, 0, Math.PI * 2);
203
+ ctx.fillStyle = 'rgba(255, 210, 63, 0.35)';
204
+ ctx.strokeStyle = HOVER_YELLOW;
205
+ ctx.fill();
206
+ ctx.stroke();
207
+ ctx.restore();
208
+ }
209
+
210
+ function appendSegment(ctx, from, seg, at) {
211
+ if (!seg.c1 && !seg.c2) {
212
+ const end = at(seg.to[0], seg.to[1]);
213
+ ctx.lineTo(end[0], end[1]);
214
+ return;
215
+ }
216
+ const { c1, c2, to } = absoluteSegment(from, seg);
217
+ const ac1 = at(c1[0], c1[1]);
218
+ const ac2 = at(c2[0], c2[1]);
219
+ const end = at(to[0], to[1]);
220
+ ctx.bezierCurveTo(ac1[0], ac1[1], ac2[0], ac2[1], end[0], end[1]);
221
+ }
222
+
223
+ function strokeEdgeSegment(ctx, sub, edgeIndex, at) {
224
+ const edge = subpathEdgeFrom(sub, edgeIndex);
225
+ if (!edge) return false;
226
+ const start = at(edge.from[0], edge.from[1]);
227
+ ctx.moveTo(start[0], start[1]);
228
+ appendSegment(ctx, edge.from, edge.seg, at);
229
+ return true;
230
+ }
231
+
232
+ function strokeEdge(ctx, sub, edgeIndex, at, color, lineWidth, alpha) {
233
+ ctx.beginPath();
234
+ if (!strokeEdgeSegment(ctx, sub, edgeIndex, at)) return;
235
+ ctx.setLineDash([]);
236
+ ctx.lineWidth = lineWidth;
237
+ ctx.strokeStyle = color;
238
+ ctx.globalAlpha = alpha;
239
+ ctx.stroke();
240
+ ctx.globalAlpha = 1;
241
+ }
242
+
243
+ function strokeSubpath(ctx, sub, at, color, lineWidth, alpha) {
244
+ if (!sub?.start) return;
245
+ ctx.beginPath();
246
+ const first = at(sub.start[0], sub.start[1]);
247
+ ctx.moveTo(first[0], first[1]);
248
+ let from = sub.start;
249
+ for (const seg of sub.segs ?? []) {
250
+ appendSegment(ctx, from, seg, at);
251
+ from = seg.to;
252
+ }
253
+ if (sub.closed) ctx.closePath();
254
+ ctx.setLineDash([]);
255
+ ctx.lineWidth = lineWidth;
256
+ ctx.strokeStyle = color;
257
+ ctx.globalAlpha = alpha;
258
+ ctx.stroke();
259
+ ctx.globalAlpha = 1;
260
+ }
261
+
262
+ function drawAnchors(ctx, sub, at, ui, subIndex, onlyIndices, junctions) {
263
+ const pts = subpathPoints(sub);
264
+ const shown = onlyIndices?.length ? new Set(onlyIndices) : null;
265
+ const selectedAnchors = new Set((ui?.selectedAnchors ?? []).map(anchorKey));
266
+ pts.forEach((p, i) => {
267
+ if (shown && !shown.has(i)) return;
268
+ const [x, y] = at(p[0], p[1]);
269
+ // A junction ring reports structure, not selection, so it takes the
270
+ // skeleton's neutral rather than the pick colour.
271
+ if (junctions?.has(`${p[0]},${p[1]}`)) {
272
+ ctx.beginPath();
273
+ ctx.arc(x, y, JUNCTION_R, 0, Math.PI * 2);
274
+ ctx.lineWidth = SKELETON_W;
275
+ ctx.strokeStyle = INK;
276
+ ctx.stroke();
277
+ ctx.lineWidth = JUNCTION_INNER_W;
278
+ ctx.strokeStyle = SKELETON;
279
+ ctx.stroke();
280
+ }
281
+ const hover =
282
+ ui?.hoverAnchor &&
283
+ ui.hoverAnchor.subIndex === subIndex &&
284
+ ui.hoverAnchor.pointIndex === i;
285
+ const selected = selectedAnchors.has(anchorKey({ subIndex, pointIndex: i }));
286
+ const r = hover ? ANCHOR_HOVER_R : ANCHOR_R;
287
+ ctx.lineWidth = HAIRLINE;
288
+ ctx.beginPath();
289
+ ctx.arc(x, y, r, 0, Math.PI * 2);
290
+ ctx.fillStyle = selected ? PICKED : '#ffffff';
291
+ ctx.strokeStyle = INK;
292
+ ctx.fill();
293
+ ctx.stroke();
294
+ });
295
+ }
296
+
297
+ function drawShapeMarquee(ctx, rect, sx, sy) {
298
+ if (!rect || rect.w <= 0 || rect.h <= 0) return;
299
+ const x = rect.x * sx;
300
+ const y = rect.y * sy;
301
+ const w = rect.w * sx;
302
+ const h = rect.h * sy;
303
+ ctx.save();
304
+ ctx.lineWidth = MARQUEE_W;
305
+ ctx.setLineDash(DASH);
306
+ ctx.strokeStyle = PICKED;
307
+ ctx.globalAlpha = 0.95;
308
+ // Inset by half the stroke so the border lands inside the rect it reports.
309
+ const inset = MARQUEE_W / 2;
310
+ ctx.strokeRect(x + inset, y + inset, Math.max(0, w - MARQUEE_W), Math.max(0, h - MARQUEE_W));
311
+ ctx.fillStyle = 'rgba(0, 212, 255, 0.12)';
312
+ ctx.setLineDash([]);
313
+ ctx.fillRect(x, y, w, h);
314
+ ctx.restore();
315
+ }
316
+
317
+ function drawBrushGesture(ctx, at, ui) {
318
+ const ink = ui?.brushInk;
319
+ if (ink?.length) {
320
+ ctx.beginPath();
321
+ const first = at(ink[0][0], ink[0][1]);
322
+ ctx.moveTo(first[0], first[1]);
323
+ for (let i = 1; i < ink.length; i++) {
324
+ const p = at(ink[i][0], ink[i][1]);
325
+ ctx.lineTo(p[0], p[1]);
326
+ }
327
+ ctx.setLineDash([]);
328
+ ctx.lineWidth = HAIRLINE;
329
+ ctx.strokeStyle = SKELETON;
330
+ ctx.globalAlpha = 0.45;
331
+ ctx.stroke();
332
+ ctx.globalAlpha = 1;
333
+ }
334
+ const ghost = ui?.brushGhost;
335
+ if (ghost?.subpaths?.length) {
336
+ for (const sub of ghost.subpaths) {
337
+ strokeSubpath(ctx, sub, at, PICKED, SKELETON_W, 1);
338
+ }
339
+ }
340
+ }