castle-web-cli 0.4.166 → 0.4.167
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.d.ts +1 -2
- package/dist/agent.js +22 -13
- package/dist/api.js +16 -1
- package/dist/imports.js +19 -4
- package/dist/localPaths.d.ts +2 -0
- package/dist/localPaths.js +6 -0
- package/dist/serve.js +5 -1
- package/dist/serveSecurity.d.ts +12 -0
- package/dist/serveSecurity.js +42 -0
- package/dist/shell/assets/{index-CG9TrvCm.js → index-C8M-p00y.js} +36 -36
- package/dist/shell/index.html +1 -1
- package/kits/base/castle.json +64 -2
- package/kits/base/editors/ImageViewer.js +259 -0
- package/kits/base/editors/MediaPlayer.js +90 -0
- package/kits/base/editors/imageViewer.css +95 -0
- package/kits/base/editors/mediaPlayer.css +58 -0
- package/kits/multiplayer-2d/castle.json +1 -1
- package/kits/multiplayer-2d/package-lock.json +4 -1
- package/kits/multiplayer-3d/castle.json +1 -1
- package/kits/multiplayer-3d/package-lock.json +1 -1
- package/kits/physics-2d/behaviors/Joints.jsx +11 -5
- package/kits/physics-2d/behaviors/Sprite.jsx +6 -21
- package/kits/physics-2d/castle.json +1 -61
- package/kits/physics-2d/editors/BlueprintLibrary.jsx +8 -68
- package/kits/physics-2d/editors/SingleEditor.jsx +3 -10
- package/kits/physics-2d/editors/editorRegistry.jsx +36 -2
- package/kits/physics-2d/engine/art.js +25 -1
- package/kits/physics-2d/engine/artThumbnail.jsx +92 -0
- package/kits/physics-2d/engine/files.js +4 -15
- package/kits/physics-2d/engine/spriteField.jsx +176 -0
- package/kits/physics-2d/engine/ui.module.css +136 -0
- package/kits/physics-2d/package-lock.json +5 -0
- package/kits/physics-2d/pnpm-lock.yaml +96 -3
- package/kits/physics-3d/castle.json +1 -1
- package/kits/physics-3d/package-lock.json +1 -1
- package/kits/physics-3d/pnpm-lock.yaml +88 -3
- package/kits/real-time/castle.json +1 -1
- package/kits/real-time/package.json +3 -0
- package/kits/turn-based/castle.json +1 -1
- package/kits/turn-based/package-lock.json +1 -1
- package/package.json +1 -1
- package/kits/base/node_modules/.package-lock.json +0 -28
- package/kits/base/node_modules/castle-web-fonts/README.md +0 -114
- package/kits/base/node_modules/castle-web-fonts/dist/catalog.d.ts +0 -18
- package/kits/base/node_modules/castle-web-fonts/dist/catalog.js +0 -142
- package/kits/base/node_modules/castle-web-fonts/dist/face.d.ts +0 -1
- package/kits/base/node_modules/castle-web-fonts/dist/face.js +0 -52
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Bore.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Bore.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/CourierPrime.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/CourierPrime.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/DMSans.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/DMSans.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Glacier.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Glacier.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/HelicoCentrica.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/HelicoCentrica.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Piazzolla.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Piazzolla.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Synco.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Synco.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Tektur.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Tektur.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/YatraOne.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/YatraOne.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/index.d.ts +0 -16
- package/kits/base/node_modules/castle-web-fonts/dist/index.js +0 -15
- package/kits/base/node_modules/castle-web-fonts/licenses/CC-BY-ND-4.0.txt +0 -393
- package/kits/base/node_modules/castle-web-fonts/licenses/FONTS.md +0 -64
- package/kits/base/node_modules/castle-web-fonts/licenses/OFL-1.1.txt +0 -95
- package/kits/base/node_modules/castle-web-fonts/package.json +0 -47
- package/kits/base/node_modules/castle-web-fonts/woff2/Bore.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/CourierPrime.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/DMSans.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Glacier.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/HelicoCentrica.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Piazzolla.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/SOURCE.txt +0 -2
- package/kits/base/node_modules/castle-web-fonts/woff2/Synco.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Tektur.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/YatraOne.woff2 +0 -0
- package/kits/physics-2d/editors/ImageViewer.jsx +0 -206
- package/kits/physics-2d/editors/MediaPlayer.jsx +0 -57
- package/kits/physics-2d/editors/mediaFile.js +0 -31
- package/kits/physics-2d/editors/mediaViewer.module.css +0 -128
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import { Panel, SelectField, NumberField, CheckboxField, Button
|
|
2
|
+
import { Panel, SelectField, NumberField, CheckboxField, Button } from '../engine/ui';
|
|
3
|
+
import { SpriteField } from '../engine/spriteField';
|
|
3
4
|
import { centerOf, inActorWorldSpace } from '../engine/physics/controls';
|
|
4
5
|
import { JOINT_TYPES } from '../engine/physics/joints';
|
|
5
6
|
import { drawJointArt } from '../engine/physics/jointArt';
|
|
@@ -75,9 +76,12 @@ export class Joints {
|
|
|
75
76
|
});
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
static Inspector({ component, setComponent, override, beginPick, pickActive,
|
|
79
|
+
static Inspector({ component, setComponent, override, beginPick, pickActive, sprites }) {
|
|
79
80
|
const list = Array.isArray(component.list) ? component.list : [];
|
|
80
|
-
|
|
81
|
+
// Joint art is drawings-only (tile/stretch along a link); images stay out.
|
|
82
|
+
const spriteFiles = Object.keys(sprites ?? {})
|
|
83
|
+
.filter((f) => f.endsWith('.sprite') || f.endsWith('.pxart'))
|
|
84
|
+
.sort();
|
|
81
85
|
// Which entry's target-pick is live. Combined with the editor's global
|
|
82
86
|
// `pickActive` so a cancel (Esc/empty click) clears every entry's state.
|
|
83
87
|
const [pickingIndex, setPickingIndex] = useState(null);
|
|
@@ -95,6 +99,7 @@ export class Joints {
|
|
|
95
99
|
key={i}
|
|
96
100
|
index={i}
|
|
97
101
|
joint={joint}
|
|
102
|
+
sprites={sprites}
|
|
98
103
|
spriteFiles={spriteFiles}
|
|
99
104
|
active={pickActive && pickingIndex === i}
|
|
100
105
|
onPickToggle={() => onPickToggle(i)}
|
|
@@ -114,7 +119,7 @@ function defaultJoint() {
|
|
|
114
119
|
|
|
115
120
|
// One editable link in the list: type, target picker, type-specific fields, and
|
|
116
121
|
// how it renders in play (line / hidden / sprite art along the joint).
|
|
117
|
-
function JointEntry({ index, joint, spriteFiles, active, onPickToggle, onPatch, onRemove }) {
|
|
122
|
+
function JointEntry({ index, joint, sprites, spriteFiles, active, onPickToggle, onPatch, onRemove }) {
|
|
118
123
|
const [showAnchors, setShowAnchors] = useState(false);
|
|
119
124
|
const type = joint.type ?? 'spring';
|
|
120
125
|
const isPivot = type === 'weld';
|
|
@@ -180,10 +185,11 @@ function JointEntry({ index, joint, spriteFiles, active, onPickToggle, onPatch,
|
|
|
180
185
|
<SelectField label="Show in play" value={render} onChange={(v) => onPatch({ render: v })} options={RENDER_MODES} />
|
|
181
186
|
{render === 'sprite' ? (
|
|
182
187
|
<>
|
|
183
|
-
<
|
|
188
|
+
<SpriteField
|
|
184
189
|
label="Art"
|
|
185
190
|
value={joint.sprite || ''}
|
|
186
191
|
onChange={(v) => onPatch({ sprite: v })}
|
|
192
|
+
sprites={sprites}
|
|
187
193
|
files={spriteFiles}
|
|
188
194
|
allowEmpty
|
|
189
195
|
/>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { resolveDeckFile } from 'castle-web-sdk';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { artFrameCount, isImageArt,
|
|
3
|
+
import { artFrameCount, isImageArt, renderDisplayFrame } from '../engine/art';
|
|
4
4
|
import { isVectorRenderer } from '../engine/pxart';
|
|
5
|
-
import { renderSmoothCompositeFrame, renderSmoothSpriteFrame } from '../engine/pxartSmooth';
|
|
6
5
|
import { spriteDestRect } from '../engine/spriteGeometry';
|
|
7
|
-
import {
|
|
6
|
+
import { Panel, SelectField } from '../engine/ui';
|
|
7
|
+
import { SpriteField } from '../engine/spriteField';
|
|
8
8
|
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
9
9
|
import { parseTint, tintCanvas } from './tint';
|
|
10
10
|
|
|
@@ -166,14 +166,13 @@ export class Sprite {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
static Inspector({ component, setComponent, sprites, override }) {
|
|
169
|
-
const spriteFiles = getSpriteFiles(sprites);
|
|
170
169
|
return (
|
|
171
170
|
<Panel title="Sprite" overridden={override?.anyOverridden()}>
|
|
172
|
-
<
|
|
171
|
+
<SpriteField
|
|
173
172
|
label="File"
|
|
174
173
|
value={component.file}
|
|
175
174
|
onChange={(file) => setComponent({ file })}
|
|
176
|
-
|
|
175
|
+
sprites={sprites}
|
|
177
176
|
{...overrideProps(override, 'file')}
|
|
178
177
|
/>
|
|
179
178
|
<SelectField
|
|
@@ -270,15 +269,7 @@ const MAX_CACHE_LONG_SIDE = 2048;
|
|
|
270
269
|
function frameSource(sprite, frameIndex) {
|
|
271
270
|
if (isImageArt(sprite)) return sprite.ready ? sprite.image : null;
|
|
272
271
|
const canvas = document.createElement('canvas');
|
|
273
|
-
|
|
274
|
-
renderSmoothCompositeFrame(sprite, frameIndex, canvas);
|
|
275
|
-
return canvas;
|
|
276
|
-
}
|
|
277
|
-
if (sprite.cornerRadius > 0) {
|
|
278
|
-
renderSmoothSpriteFrame(sprite, frameIndex, canvas, { cornerRadius: sprite.cornerRadius });
|
|
279
|
-
return canvas;
|
|
280
|
-
}
|
|
281
|
-
return renderArtFrame(sprite, frameIndex, canvas) ? canvas : null;
|
|
272
|
+
return renderDisplayFrame(sprite, frameIndex, canvas) ? canvas : null;
|
|
282
273
|
}
|
|
283
274
|
|
|
284
275
|
function sourceSize(source) {
|
|
@@ -431,9 +422,3 @@ function advanceAnim(state, sprite, sequence, tag, dt) {
|
|
|
431
422
|
}
|
|
432
423
|
state.frameIndex = sequence[state.pos];
|
|
433
424
|
}
|
|
434
|
-
|
|
435
|
-
// Everything the deck can point a Sprite at: `.pxart` drawings and image files
|
|
436
|
-
// alike, which is exactly what the resolved art map holds.
|
|
437
|
-
function getSpriteFiles(sprites) {
|
|
438
|
-
return Object.keys(sprites ?? {}).sort();
|
|
439
|
-
}
|
|
@@ -63,66 +63,6 @@
|
|
|
63
63
|
"icon": "file",
|
|
64
64
|
"editor": "kit",
|
|
65
65
|
"data": true
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
"ext": ".png",
|
|
69
|
-
"label": "Image",
|
|
70
|
-
"icon": "image",
|
|
71
|
-
"editor": "kit"
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
"ext": ".jpg",
|
|
75
|
-
"label": "Image",
|
|
76
|
-
"icon": "image",
|
|
77
|
-
"editor": "kit"
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
"ext": ".jpeg",
|
|
81
|
-
"label": "Image",
|
|
82
|
-
"icon": "image",
|
|
83
|
-
"editor": "kit"
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
"ext": ".gif",
|
|
87
|
-
"label": "Image",
|
|
88
|
-
"icon": "image",
|
|
89
|
-
"editor": "kit"
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
"ext": ".webp",
|
|
93
|
-
"label": "Image",
|
|
94
|
-
"icon": "image",
|
|
95
|
-
"editor": "kit"
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
"ext": ".svg",
|
|
99
|
-
"label": "Image",
|
|
100
|
-
"icon": "image",
|
|
101
|
-
"editor": "kit"
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
"ext": ".mp3",
|
|
105
|
-
"label": "Audio",
|
|
106
|
-
"icon": "music",
|
|
107
|
-
"editor": "kit"
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
"ext": ".wav",
|
|
111
|
-
"label": "Audio",
|
|
112
|
-
"icon": "music",
|
|
113
|
-
"editor": "kit"
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
"ext": ".m4a",
|
|
117
|
-
"label": "Audio",
|
|
118
|
-
"icon": "music",
|
|
119
|
-
"editor": "kit"
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
"ext": ".mp4",
|
|
123
|
-
"label": "Video",
|
|
124
|
-
"icon": "film",
|
|
125
|
-
"editor": "kit"
|
|
126
66
|
}
|
|
127
67
|
],
|
|
128
68
|
"defaultPlayFile": "scenes/main.scene"
|
|
@@ -132,7 +72,7 @@
|
|
|
132
72
|
"main": "main.jsx",
|
|
133
73
|
"autoUpdateWhenImported": true,
|
|
134
74
|
"title": "physics-2d",
|
|
135
|
-
"publishedVersion": "2026-09-
|
|
75
|
+
"publishedVersion": "2026-09-05T00:07:35.112Z",
|
|
136
76
|
"imports": {
|
|
137
77
|
"castle.base": {
|
|
138
78
|
"deckId": "yRcmH4_aYllE",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
|
+
import { ArtThumbnail } from '../engine/artThumbnail';
|
|
3
3
|
import { screenToCard } from '../engine/scene';
|
|
4
4
|
import { countBlueprintInstances, listBlueprints } from '../engine/blueprint';
|
|
5
5
|
import { ConfirmDialog, ContextMenu, cx, Icon, styles, useWheelToHorizontal } from '../engine/ui';
|
|
@@ -235,77 +235,17 @@ function BlueprintDragPreview({ preview, sprites }) {
|
|
|
235
235
|
);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
245
|
-
const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
246
|
-
for (let i = 3; i < data.length; i += 4) if (data[i] !== 0) return false;
|
|
247
|
-
return true;
|
|
248
|
-
} catch {
|
|
249
|
-
// Tainted or zero-sized canvas: assume it draws something rather than
|
|
250
|
-
// mislabeling real art as blank.
|
|
251
|
-
return false;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// A blueprint's slot art, falling back to its NAME when there is nothing to
|
|
256
|
-
// show -- no Sprite, a file that doesn't resolve, or art that is entirely
|
|
257
|
-
// transparent (a "Draw new" sprite nobody has drawn in yet). Without it a belt
|
|
258
|
-
// of not-yet-drawn actors is a row of identical placeholders.
|
|
259
|
-
// `showName={false}` for callers that already print the name right beside the
|
|
260
|
-
// thumbnail (the creation modal's tiles) -- otherwise a sprite-less blueprint
|
|
261
|
-
// renders its name inside the tile AND under it, reading as "GoalGoal".
|
|
238
|
+
// A blueprint's slot art — delegates to ArtThumbnail so the belt and the
|
|
239
|
+
// sprite picker share blank-name fallback. `showName={false}` for callers that
|
|
240
|
+
// already print the name right beside the thumbnail (the creation modal's
|
|
241
|
+
// tiles) -- otherwise a sprite-less blueprint renders its name inside the tile
|
|
242
|
+
// AND under it, reading as "GoalGoal".
|
|
262
243
|
export function BlueprintThumbnail({ blueprint, sprites, preview = false, showName = true }) {
|
|
263
|
-
const canvasRef = useRef(null);
|
|
264
244
|
const spritePath = blueprint.components?.Sprite?.file;
|
|
265
245
|
const sprite = spritePath ? sprites?.[spritePath] : null;
|
|
266
|
-
// Thumbnails draw on change, not on a frame loop, so an image that finishes
|
|
267
|
-
// decoding after this rendered has to say so or the slot stays blank.
|
|
268
|
-
const [loadTick, setLoadTick] = useState(0);
|
|
269
|
-
const [blank, setBlank] = useState(false);
|
|
270
|
-
useEffect(() => onArtLoaded(() => setLoadTick((tick) => tick + 1)), []);
|
|
271
|
-
useEffect(() => {
|
|
272
|
-
const canvas = canvasRef.current;
|
|
273
|
-
if (!canvas || !sprite) return;
|
|
274
|
-
const drew = renderArtFrame(sprite, 0, canvas);
|
|
275
|
-
// `false` means "not here yet" (an image still loading), not blank.
|
|
276
|
-
if (drew !== false) setBlank(rendersNothing(sprite, canvas));
|
|
277
|
-
}, [sprite, loadTick]);
|
|
278
246
|
const name = showName && typeof blueprint.name === 'string' ? blueprint.name.trim() : '';
|
|
279
|
-
// The canvas stays mounted underneath, so art arriving later clears this.
|
|
280
|
-
// The inner span is load-bearing, not decoration: bare text inside a flex
|
|
281
|
-
// container becomes an anonymous item sized to max-content, so it never
|
|
282
|
-
// "overflows a line" and `overflow-wrap` never fires -- a long single-word
|
|
283
|
-
// name silently clipped at both edges instead of wrapping. A real child can
|
|
284
|
-
// be held to the box's width, which is what gives the wrap something to
|
|
285
|
-
// break against.
|
|
286
|
-
const label = name ? (
|
|
287
|
-
<span className={cx(styles.bpSlotName, preview && styles.bpSlotNamePreview)} title={name}>
|
|
288
|
-
<span className={styles.bpSlotNameText}>{name}</span>
|
|
289
|
-
</span>
|
|
290
|
-
) : (
|
|
291
|
-
<span className={styles.bpSlotFallback}>
|
|
292
|
-
<Icon name="clone" />
|
|
293
|
-
</span>
|
|
294
|
-
);
|
|
295
|
-
if (!sprite) return label;
|
|
296
247
|
return (
|
|
297
|
-
<
|
|
298
|
-
<canvas
|
|
299
|
-
ref={canvasRef}
|
|
300
|
-
className={cx(
|
|
301
|
-
styles.bpSlotThumb,
|
|
302
|
-
preview && styles.bpPreviewThumb,
|
|
303
|
-
blank && styles.bpSlotThumbHidden
|
|
304
|
-
)}
|
|
305
|
-
aria-hidden="true"
|
|
306
|
-
/>
|
|
307
|
-
{blank ? label : null}
|
|
308
|
-
</span>
|
|
248
|
+
<ArtThumbnail art={sprite} name={name} preview={preview} fallbackIcon="clone" />
|
|
309
249
|
);
|
|
310
250
|
}
|
|
311
251
|
|
|
@@ -27,8 +27,6 @@ import { hasEditorModule, loadEditorModule } from './editorRegistry';
|
|
|
27
27
|
import { useLiveDeckFiles } from '../engine/liveReload';
|
|
28
28
|
import { collectAssets } from '../engine/assets';
|
|
29
29
|
import { MainEditor, styles } from '../engine/ui';
|
|
30
|
-
import { ImageViewer } from './ImageViewer';
|
|
31
|
-
import { MediaPlayer } from './MediaPlayer';
|
|
32
30
|
import { PxArtEditor } from './PxArtEditor';
|
|
33
31
|
import { SceneEditor } from './SceneEditor';
|
|
34
32
|
import { StyleEditor } from './StyleEditor';
|
|
@@ -321,15 +319,10 @@ export function SingleEditor({ path, editor, editorModule }) {
|
|
|
321
319
|
readOnly={readOnly}
|
|
322
320
|
/>
|
|
323
321
|
);
|
|
324
|
-
} else if (kind === 'image') {
|
|
325
|
-
// Viewers read their file's BYTES over the serve, not the text file map --
|
|
326
|
-
// `files` holds source, and an image decoded as utf-8 is nothing.
|
|
327
|
-
body = <ImageViewer path={path} />;
|
|
328
|
-
} else if (kind === 'audio' || kind === 'video') {
|
|
329
|
-
body = <MediaPlayer path={path} kind={kind} />;
|
|
330
322
|
}
|
|
331
|
-
//
|
|
332
|
-
//
|
|
323
|
+
// Image / audio / video are declared by castle.base and land in the
|
|
324
|
+
// DeclaredEditor branch above via editorModule. No `else`: code/text files
|
|
325
|
+
// are handled by the builtin shell code editor.
|
|
333
326
|
return (
|
|
334
327
|
<div className={styles.panelBare} style={{ position: 'relative' }}>
|
|
335
328
|
<MainEditor>{body}</MainEditor>
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Editors live under `editors/` by convention; that is what makes a third
|
|
11
11
|
// party's editor reachable from here without this kit knowing it exists.
|
|
12
|
+
import { createElement, useEffect, useRef } from 'react';
|
|
13
|
+
|
|
12
14
|
const modules = {
|
|
13
15
|
...import.meta.glob('/imports/*/editors/**/*.{jsx,js}'),
|
|
14
16
|
...import.meta.glob('/imports/*/code/editors/**/*.{jsx,js}'),
|
|
@@ -20,7 +22,35 @@ export function hasEditorModule(path) {
|
|
|
20
22
|
return Boolean(path && modules[`/${path}`]);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
//
|
|
25
|
+
// Base-kit (and bare-deck) editors default-export `mount(el, props)`. This kit's
|
|
26
|
+
// DeclaredEditor expects a React component. Arity distinguishes them: a mount
|
|
27
|
+
// takes the host element first; a React editor takes props only.
|
|
28
|
+
function wrapMountEditor(mount) {
|
|
29
|
+
return function MountEditorBridge(props) {
|
|
30
|
+
const ref = useRef(null);
|
|
31
|
+
const propsRef = useRef(props);
|
|
32
|
+
propsRef.current = props;
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const node = ref.current;
|
|
35
|
+
if (!node) return undefined;
|
|
36
|
+
node.textContent = '';
|
|
37
|
+
// Mount once per open file. Callers that need live prop updates should
|
|
38
|
+
// read them themselves; remounting on every onChange identity would wipe
|
|
39
|
+
// viewers that hold zoom/pan state.
|
|
40
|
+
const cleanup = mount(node, propsRef.current);
|
|
41
|
+
return () => {
|
|
42
|
+
if (typeof cleanup === 'function') cleanup();
|
|
43
|
+
else node.textContent = '';
|
|
44
|
+
};
|
|
45
|
+
}, [props.path, mount]);
|
|
46
|
+
return createElement('div', {
|
|
47
|
+
ref,
|
|
48
|
+
style: { display: 'flex', flex: 1, width: '100%', height: '100%', minHeight: 0 },
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Load one, returning a React component. Throws a legible error rather than
|
|
24
54
|
// letting the caller see `undefined is not a function` three frames later.
|
|
25
55
|
export async function loadEditorModule(path) {
|
|
26
56
|
const load = modules[`/${path}`];
|
|
@@ -30,7 +60,11 @@ export async function loadEditorModule(path) {
|
|
|
30
60
|
);
|
|
31
61
|
}
|
|
32
62
|
const mod = await load();
|
|
33
|
-
const Editor = mod.default ?? mod.Editor;
|
|
63
|
+
const Editor = mod.default ?? mod.Editor ?? mod.mount;
|
|
34
64
|
if (!Editor) throw new Error(`"${path}" has no default export to render as an editor.`);
|
|
65
|
+
// Mount editors (base kit): (el, props) => void|cleanup. React editors: (props) => node.
|
|
66
|
+
if (typeof Editor === 'function' && Editor.length >= 2) {
|
|
67
|
+
return wrapMountEditor(Editor);
|
|
68
|
+
}
|
|
35
69
|
return Editor;
|
|
36
70
|
}
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
// really do differ -- rendering a frame, and how many frames there are -- are
|
|
12
12
|
// `renderArtFrame` / `artFrameCount` here.
|
|
13
13
|
|
|
14
|
-
import { frameCount, renderSpriteFrame } from './pxart';
|
|
14
|
+
import { displayFinish, frameCount, renderSpriteFrame } from './pxart';
|
|
15
|
+
import { renderSmoothCompositeFrame, renderSmoothSpriteFrame } from './pxartSmooth';
|
|
15
16
|
|
|
16
17
|
// An image is one frame, held for as long as anyone asks (a still).
|
|
17
18
|
const STILL_FRAME = Object.freeze({ durationMs: 0 });
|
|
@@ -89,6 +90,10 @@ export function artFrameCount(art) {
|
|
|
89
90
|
// Draw one frame into `canvas` at the art's native resolution, resizing it to
|
|
90
91
|
// match. Returns false when there was nothing to draw yet (an image still
|
|
91
92
|
// loading), so callers don't cache the blank result.
|
|
93
|
+
//
|
|
94
|
+
// Pixel finish only — 1px/cell. Prefer `renderDisplayFrame` anywhere the art
|
|
95
|
+
// is shown to a person (belt, picker, scene), so Smooth / vector sprites keep
|
|
96
|
+
// their finish instead of looking blocky.
|
|
92
97
|
export function renderArtFrame(art, frameIndex, canvas) {
|
|
93
98
|
if (!isImageArt(art)) {
|
|
94
99
|
renderSpriteFrame(art, frameIndex, canvas);
|
|
@@ -103,3 +108,22 @@ export function renderArtFrame(art, frameIndex, canvas) {
|
|
|
103
108
|
ctx.drawImage(art.image, 0, 0);
|
|
104
109
|
return true;
|
|
105
110
|
}
|
|
111
|
+
|
|
112
|
+
// Same contract as `renderArtFrame`, but honours the sprite's display finish
|
|
113
|
+
// (vector / rounded corners → supersampled smooth render). Use this for any
|
|
114
|
+
// on-screen thumbnail or blit that should match what Sprite draws in the scene.
|
|
115
|
+
export function renderDisplayFrame(art, frameIndex, canvas) {
|
|
116
|
+
if (isImageArt(art)) return renderArtFrame(art, frameIndex, canvas);
|
|
117
|
+
if (!art) return false;
|
|
118
|
+
const finish = displayFinish(art);
|
|
119
|
+
if (finish.vector) {
|
|
120
|
+
renderSmoothCompositeFrame(art, frameIndex, canvas);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (finish.cornerRadius > 0) {
|
|
124
|
+
renderSmoothSpriteFrame(art, frameIndex, canvas, { cornerRadius: finish.cornerRadius });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
renderSpriteFrame(art, frameIndex, canvas);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { isImageArt, onArtLoaded, renderDisplayFrame } from './art';
|
|
3
|
+
import { displayFinish } from './pxart';
|
|
4
|
+
import { cx, Icon, styles } from './ui';
|
|
5
|
+
|
|
6
|
+
// Asked of the CANVAS, not the source art: counting non-transparent grid chars
|
|
7
|
+
// would call a path-drawn sprite blank. Skipped for image art, whose native
|
|
8
|
+
// resolution is whatever was uploaded.
|
|
9
|
+
export function artRendersNothing(art, canvas) {
|
|
10
|
+
if (isImageArt(art)) return false;
|
|
11
|
+
try {
|
|
12
|
+
// Must match the options the render path already used to create the
|
|
13
|
+
// context. Asking for `{ willReadFrequently: true }` after a plain
|
|
14
|
+
// `getContext('2d')` returns null in browsers, and the catch below would
|
|
15
|
+
// then treat every transparent sprite as "has art" — blank names never
|
|
16
|
+
// appear in the belt or the picker.
|
|
17
|
+
const ctx = canvas.getContext('2d');
|
|
18
|
+
if (!ctx || canvas.width < 1 || canvas.height < 1) return true;
|
|
19
|
+
const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
20
|
+
for (let i = 3; i < data.length; i += 4) if (data[i] !== 0) return false;
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
// Tainted canvas: assume it draws something rather than mislabeling real
|
|
24
|
+
// art as blank.
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Shared art thumb used by the blueprint belt and the sprite picker. Falls
|
|
30
|
+
// back to `name` when there is no art, or the art is entirely transparent
|
|
31
|
+
// (a "Draw new" sprite nobody has drawn in yet) — same treatment the belt
|
|
32
|
+
// has always given undrawn blueprints.
|
|
33
|
+
//
|
|
34
|
+
// `className` sizes the outer slot (belt defaults to 36px via CSS; the picker
|
|
35
|
+
// stretches it to fill its tile). `fallbackIcon` is what shows when there is
|
|
36
|
+
// neither art nor a name (`clone` on the belt, `image` in the picker).
|
|
37
|
+
export function ArtThumbnail({
|
|
38
|
+
art,
|
|
39
|
+
name = '',
|
|
40
|
+
className,
|
|
41
|
+
preview = false,
|
|
42
|
+
fallbackIcon = 'image',
|
|
43
|
+
}) {
|
|
44
|
+
const canvasRef = useRef(null);
|
|
45
|
+
const [loadTick, setLoadTick] = useState(0);
|
|
46
|
+
const [blank, setBlank] = useState(false);
|
|
47
|
+
useEffect(() => onArtLoaded(() => setLoadTick((tick) => tick + 1)), []);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const canvas = canvasRef.current;
|
|
50
|
+
if (!canvas || !art) return;
|
|
51
|
+
const drew = renderDisplayFrame(art, 0, canvas);
|
|
52
|
+
// `false` means "not here yet" (an image still loading), not blank.
|
|
53
|
+
if (drew !== false) setBlank(artRendersNothing(art, canvas));
|
|
54
|
+
}, [art, loadTick]);
|
|
55
|
+
|
|
56
|
+
// The inner span is load-bearing, not decoration: bare text inside a flex
|
|
57
|
+
// container becomes an anonymous item sized to max-content, so it never
|
|
58
|
+
// "overflows a line" and `overflow-wrap` never fires -- a long single-word
|
|
59
|
+
// name silently clipped at both edges instead of wrapping. A real child can
|
|
60
|
+
// be held to the box's width, which is what gives the wrap something to
|
|
61
|
+
// break against.
|
|
62
|
+
const label = name ? (
|
|
63
|
+
<span className={cx(styles.bpSlotName, preview && styles.bpSlotNamePreview)} title={name}>
|
|
64
|
+
<span className={styles.bpSlotNameText}>{name}</span>
|
|
65
|
+
</span>
|
|
66
|
+
) : (
|
|
67
|
+
<span className={styles.bpSlotFallback}>
|
|
68
|
+
<Icon name={fallbackIcon} />
|
|
69
|
+
</span>
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (!art) {
|
|
73
|
+
return className ? <span className={className}>{label}</span> : label;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const smooth = displayFinish(art).smooth;
|
|
77
|
+
return (
|
|
78
|
+
<span className={cx(styles.bpSlotArt, className)}>
|
|
79
|
+
<canvas
|
|
80
|
+
ref={canvasRef}
|
|
81
|
+
className={cx(
|
|
82
|
+
styles.bpSlotThumb,
|
|
83
|
+
smooth && styles.bpSlotThumbSmooth,
|
|
84
|
+
preview && styles.bpPreviewThumb,
|
|
85
|
+
blank && styles.bpSlotThumbHidden
|
|
86
|
+
)}
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
/>
|
|
89
|
+
{blank ? label : null}
|
|
90
|
+
</span>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -117,26 +117,15 @@ export function mediaFilesOfKind(kind) {
|
|
|
117
117
|
const matches = { image: isImageFile, audio: isAudioFile, video: isVideoFile }[kind];
|
|
118
118
|
return Object.keys(initialMedia).filter((path) => matches(path));
|
|
119
119
|
}
|
|
120
|
-
// Which editor a file opens in
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
// `endsWith` per `return`.
|
|
120
|
+
// Which editor a file opens in when no `editorModule` was resolved. Media
|
|
121
|
+
// viewing (image / audio / video) lives on castle.base now; this only covers
|
|
122
|
+
// types this kit still routes itself (scene / sprite / style). Each extension
|
|
123
|
+
// gets its own line: legacy tooling matched one `endsWith` per `return`.
|
|
125
124
|
export function getFileKind(path) {
|
|
126
125
|
if (path.endsWith('.scene')) return 'scene';
|
|
127
126
|
if (path.endsWith('.sprite')) return 'pxart';
|
|
128
127
|
if (path.endsWith('.pxart')) return 'pxart';
|
|
129
128
|
if (path.endsWith('.style')) return 'style';
|
|
130
|
-
if (path.endsWith('.png')) return 'image';
|
|
131
|
-
if (path.endsWith('.jpg')) return 'image';
|
|
132
|
-
if (path.endsWith('.jpeg')) return 'image';
|
|
133
|
-
if (path.endsWith('.gif')) return 'image';
|
|
134
|
-
if (path.endsWith('.webp')) return 'image';
|
|
135
|
-
if (path.endsWith('.svg')) return 'image';
|
|
136
|
-
if (path.endsWith('.mp3')) return 'audio';
|
|
137
|
-
if (path.endsWith('.wav')) return 'audio';
|
|
138
|
-
if (path.endsWith('.m4a')) return 'audio';
|
|
139
|
-
if (path.endsWith('.mp4')) return 'video';
|
|
140
129
|
if (path.endsWith('.js') || path.endsWith('.jsx')) return 'code';
|
|
141
130
|
return 'text';
|
|
142
131
|
}
|