@vanduo-oss/vd3-cbun 1.3.2 → 1.4.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.
- package/CHANGELOG.md +83 -0
- package/README.md +24 -9
- package/SKILL.md +42 -15
- package/dist/charts/core.d.ts +4 -0
- package/dist/charts/index.cjs +197 -10
- package/dist/charts/index.cjs.map +3 -3
- package/dist/charts/index.js +197 -10
- package/dist/charts/index.js.map +3 -3
- package/dist/charts/vd3-charts.css +82 -0
- package/dist/charts/vue.d.ts +4 -0
- package/dist/code-editor/core.d.ts +19 -2
- package/dist/code-editor/highlight.cjs +1242 -0
- package/dist/code-editor/highlight.cjs.map +7 -0
- package/dist/code-editor/highlight.d.ts +6 -0
- package/dist/code-editor/highlight.js +1219 -0
- package/dist/code-editor/highlight.js.map +7 -0
- package/dist/code-editor/index.cjs +694 -70
- package/dist/code-editor/index.cjs.map +4 -4
- package/dist/code-editor/index.d.ts +2 -0
- package/dist/code-editor/index.js +694 -70
- package/dist/code-editor/index.js.map +4 -4
- package/dist/code-editor/vd3-code-editor.css +5 -0
- package/dist/draw/core.d.ts +7 -1
- package/dist/draw/index.cjs +470 -52
- package/dist/draw/index.cjs.map +2 -2
- package/dist/draw/index.js +470 -52
- package/dist/draw/index.js.map +2 -2
- package/dist/draw/vd3-draw.css +45 -0
- package/dist/draw/vue.d.ts +4 -0
- package/dist/hex-grid/core.d.ts +41 -0
- package/dist/hex-grid/index.cjs +308 -13
- package/dist/hex-grid/index.cjs.map +3 -3
- package/dist/hex-grid/index.d.ts +1 -0
- package/dist/hex-grid/index.js +308 -13
- package/dist/hex-grid/index.js.map +3 -3
- package/dist/hex-grid/vue.d.ts +4 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +2 -2
- package/dist/meta.json +177 -66
- package/package.json +15 -10
package/dist/draw/vd3-draw.css
CHANGED
|
@@ -211,6 +211,31 @@
|
|
|
211
211
|
cursor: crosshair;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
.vd-draw-canvas[data-tool="select"] {
|
|
215
|
+
cursor: default;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.vd-draw-canvas[data-tool="select"] .vd-draw-shape-layer > * {
|
|
219
|
+
cursor: move;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.vd-draw-canvas[data-tool="hand"] {
|
|
223
|
+
cursor: grab;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.vd-draw-canvas[data-tool="hand"].vd-draw-panning,
|
|
227
|
+
.vd-draw-canvas.vd-draw-panning {
|
|
228
|
+
cursor: grabbing;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.vd-draw-canvas[data-tool="text"] {
|
|
232
|
+
cursor: text;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.vd-draw-canvas[data-tool="eraser"] {
|
|
236
|
+
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='10' fill='rgba(230,74,25,0.08)' stroke='%23e64a19' stroke-width='1.5' stroke-dasharray='2 2'/%3E%3Ccircle cx='12' cy='12' r='1' fill='%23e64a19'/%3E%3C/svg%3E") 12 12, crosshair;
|
|
237
|
+
}
|
|
238
|
+
|
|
214
239
|
.vd-draw-canvas:focus {
|
|
215
240
|
outline: none;
|
|
216
241
|
}
|
|
@@ -264,6 +289,26 @@
|
|
|
264
289
|
cursor: nwse-resize;
|
|
265
290
|
}
|
|
266
291
|
|
|
292
|
+
.vd-draw-handle[data-handle="n"],
|
|
293
|
+
.vd-draw-handle[data-handle="s"] {
|
|
294
|
+
cursor: ns-resize;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.vd-draw-handle[data-handle="e"],
|
|
298
|
+
.vd-draw-handle[data-handle="w"] {
|
|
299
|
+
cursor: ew-resize;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.vd-draw-handle[data-handle="nw"],
|
|
303
|
+
.vd-draw-handle[data-handle="se"] {
|
|
304
|
+
cursor: nwse-resize;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.vd-draw-handle[data-handle="ne"],
|
|
308
|
+
.vd-draw-handle[data-handle="sw"] {
|
|
309
|
+
cursor: nesw-resize;
|
|
310
|
+
}
|
|
311
|
+
|
|
267
312
|
.vd-draw-marquee-rect {
|
|
268
313
|
fill: color-mix(in srgb, var(--vd-draw-selection) 12%, transparent);
|
|
269
314
|
stroke: var(--vd-draw-selection);
|
package/dist/draw/vue.d.ts
CHANGED
|
@@ -46,6 +46,10 @@ export interface VdDrawEmits {
|
|
|
46
46
|
export interface VdDrawExposed {
|
|
47
47
|
getInstance(): VdDrawCore | null;
|
|
48
48
|
setTool(tool: DrawTool): void;
|
|
49
|
+
setReadonly(readonly: boolean): void;
|
|
50
|
+
setSnap(snap: boolean): void;
|
|
51
|
+
setHistoryEnabled(enabled: boolean): void;
|
|
52
|
+
setHistoryLimit(limit: number): void;
|
|
49
53
|
undo(): void;
|
|
50
54
|
redo(): void;
|
|
51
55
|
canUndo(): boolean;
|
package/dist/hex-grid/core.d.ts
CHANGED
|
@@ -20,6 +20,13 @@ export interface VdHexGridOptions {
|
|
|
20
20
|
height?: number;
|
|
21
21
|
/** Grid rotation in radians. Default 0. */
|
|
22
22
|
rotation?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Canvas backing-store multiplier. `'auto'` (default) resolves to
|
|
25
|
+
* `min(devicePixelRatio || 1, 2)`. Pass `1` for a 1:1 CSS-pixel buffer.
|
|
26
|
+
*/
|
|
27
|
+
pixelRatio?: number | 'auto';
|
|
28
|
+
/** Viewport culling. Default true. */
|
|
29
|
+
cull?: boolean;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/** A single hex cell stored in the grid. */
|
|
@@ -47,6 +54,24 @@ export interface HexGridTransform {
|
|
|
47
54
|
scale: number;
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
/** Last-frame render metrics from {@link VdHexGrid.getRenderStats}. */
|
|
58
|
+
export interface HexRenderStats {
|
|
59
|
+
/** Total hexes in the grid. */
|
|
60
|
+
total: number;
|
|
61
|
+
/** Hexes intersecting the current viewport. */
|
|
62
|
+
visible: number;
|
|
63
|
+
/** Hexes actually drawn in the last frame. */
|
|
64
|
+
drawn: number;
|
|
65
|
+
/** Whether the last frame was a sharp culled render or a fast blit. */
|
|
66
|
+
mode: 'sharp' | 'fast';
|
|
67
|
+
/** Duration of the last sharp render in milliseconds. */
|
|
68
|
+
lastRenderMs: number;
|
|
69
|
+
/** Effective device-pixel-ratio multiplier. */
|
|
70
|
+
pixelRatio: number;
|
|
71
|
+
/** Current zoom scale. */
|
|
72
|
+
scale: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
50
75
|
/** Event payloads for {@link VdHexGrid.on}. */
|
|
51
76
|
export interface VdHexGridEventMap {
|
|
52
77
|
/** A hex was selected by click/tap. */
|
|
@@ -83,6 +108,10 @@ export declare class VdHexGrid {
|
|
|
83
108
|
height: number;
|
|
84
109
|
/** Grid rotation in radians. */
|
|
85
110
|
rotation: number;
|
|
111
|
+
/** Canvas backing-store multiplier (`'auto'` or a number). */
|
|
112
|
+
pixelRatio: number | 'auto';
|
|
113
|
+
/** Viewport culling flag. */
|
|
114
|
+
cull: boolean;
|
|
86
115
|
/** All hexes, keyed by `"q,r"`. */
|
|
87
116
|
hexes: Map<string, HexCell>;
|
|
88
117
|
/** Currently selected hex, or null. */
|
|
@@ -117,6 +146,18 @@ export declare class VdHexGrid {
|
|
|
117
146
|
/** Get all hexes. */
|
|
118
147
|
getAllHexes(): HexCell[];
|
|
119
148
|
|
|
149
|
+
/** Get the hexes intersecting the current viewport, sorted by row then column. */
|
|
150
|
+
getVisibleHexes(): HexCell[];
|
|
151
|
+
|
|
152
|
+
/** Get last-frame render metrics. */
|
|
153
|
+
getRenderStats(): HexRenderStats;
|
|
154
|
+
|
|
155
|
+
/** Set the device-pixel-ratio multiplier and re-render (no grid regeneration). */
|
|
156
|
+
setPixelRatio(ratio: number | 'auto'): void;
|
|
157
|
+
|
|
158
|
+
/** Toggle viewport culling and re-render (no grid regeneration). */
|
|
159
|
+
setCull(cull: boolean): void;
|
|
160
|
+
|
|
120
161
|
/** Set hex fill color. */
|
|
121
162
|
setHexFill(q: number, r: number, color: string): void;
|
|
122
163
|
|
package/dist/hex-grid/index.cjs
CHANGED
|
@@ -154,22 +154,54 @@ function getTerrainColor(terrainType) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// src/hex-grid/core.js
|
|
157
|
-
var VD_HEX_VERSION = "1.0
|
|
157
|
+
var VD_HEX_VERSION = "1.1.0";
|
|
158
158
|
var ZOOM_MIN = 0.3;
|
|
159
159
|
var ZOOM_MAX = 3;
|
|
160
160
|
var ZOOM_FACTOR = 0.1;
|
|
161
161
|
var DRAG_THRESHOLD = 2;
|
|
162
|
+
var FAST_RENDER_LIMIT = 8e3;
|
|
163
|
+
var SHARP_IDLE_MS = 120;
|
|
164
|
+
var MAX_PIXEL_RATIO = 2;
|
|
165
|
+
var MIN_BUCKET_SIZE = 64;
|
|
166
|
+
var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
162
167
|
var VdHexGrid = class {
|
|
163
|
-
constructor({
|
|
168
|
+
constructor({
|
|
169
|
+
element,
|
|
170
|
+
canvas,
|
|
171
|
+
size = 30,
|
|
172
|
+
width = 10,
|
|
173
|
+
height = 10,
|
|
174
|
+
rotation = 0,
|
|
175
|
+
pixelRatio = "auto",
|
|
176
|
+
cull = true
|
|
177
|
+
}) {
|
|
164
178
|
this.element = element;
|
|
165
179
|
this.canvas = canvas;
|
|
166
180
|
this.size = size;
|
|
167
181
|
this.width = width;
|
|
168
182
|
this.height = height;
|
|
169
183
|
this.rotation = rotation;
|
|
184
|
+
this.pixelRatio = pixelRatio;
|
|
185
|
+
this.cull = cull;
|
|
170
186
|
this.hexes = /* @__PURE__ */ new Map();
|
|
171
187
|
this.selectedHex = null;
|
|
172
188
|
this.listeners = {};
|
|
189
|
+
this._bucketSize = Math.max(size * 2, MIN_BUCKET_SIZE);
|
|
190
|
+
this._index = /* @__PURE__ */ new Map();
|
|
191
|
+
this._gestureCancel = null;
|
|
192
|
+
this._sharpTimer = null;
|
|
193
|
+
this._frameCanvas = null;
|
|
194
|
+
this._frameCtx = null;
|
|
195
|
+
this._frameTransform = null;
|
|
196
|
+
this._stats = {
|
|
197
|
+
total: 0,
|
|
198
|
+
visible: 0,
|
|
199
|
+
drawn: 0,
|
|
200
|
+
mode: "sharp",
|
|
201
|
+
lastRenderMs: 0,
|
|
202
|
+
pixelRatio: 1,
|
|
203
|
+
scale: 1
|
|
204
|
+
};
|
|
173
205
|
this.transform = { x: 0, y: 0, scale: 1 };
|
|
174
206
|
this.dragging = false;
|
|
175
207
|
this.lastPos = null;
|
|
@@ -229,6 +261,14 @@ var VdHexGrid = class {
|
|
|
229
261
|
*/
|
|
230
262
|
destroy() {
|
|
231
263
|
this._teardownEvents();
|
|
264
|
+
this._cancelGestureRender();
|
|
265
|
+
if (this._sharpTimer) {
|
|
266
|
+
clearTimeout(this._sharpTimer);
|
|
267
|
+
this._sharpTimer = null;
|
|
268
|
+
}
|
|
269
|
+
this._frameCanvas = null;
|
|
270
|
+
this._frameCtx = null;
|
|
271
|
+
this._frameTransform = null;
|
|
232
272
|
if (this._themeObserver) {
|
|
233
273
|
this._themeObserver.disconnect();
|
|
234
274
|
this._themeObserver = null;
|
|
@@ -239,6 +279,195 @@ var VdHexGrid = class {
|
|
|
239
279
|
this._themeMediaHandler = null;
|
|
240
280
|
}
|
|
241
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Resolve the effective device-pixel-ratio multiplier.
|
|
284
|
+
* @returns {number}
|
|
285
|
+
*/
|
|
286
|
+
_resolvePixelRatio() {
|
|
287
|
+
const requested = this.pixelRatio;
|
|
288
|
+
if (requested === "auto" || requested == null) {
|
|
289
|
+
const dpr = typeof window !== "undefined" && typeof window.devicePixelRatio === "number" ? window.devicePixelRatio : 1;
|
|
290
|
+
return Math.max(1, Math.min(dpr || 1, MAX_PIXEL_RATIO));
|
|
291
|
+
}
|
|
292
|
+
const n = Number(requested);
|
|
293
|
+
return Number.isFinite(n) && n > 0 ? n : 1;
|
|
294
|
+
}
|
|
295
|
+
/** Request an animation frame, returning a cancel function. */
|
|
296
|
+
_requestFrame(callback) {
|
|
297
|
+
if (typeof requestAnimationFrame === "function") {
|
|
298
|
+
const id2 = requestAnimationFrame(callback);
|
|
299
|
+
return () => cancelAnimationFrame(id2);
|
|
300
|
+
}
|
|
301
|
+
const id = setTimeout(callback, 16);
|
|
302
|
+
return () => clearTimeout(id);
|
|
303
|
+
}
|
|
304
|
+
/** Cancel a pending gesture frame, if any. */
|
|
305
|
+
_cancelGestureRender() {
|
|
306
|
+
if (this._gestureCancel) {
|
|
307
|
+
this._gestureCancel();
|
|
308
|
+
this._gestureCancel = null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Coalesce gesture-driven transform changes into at most one render/frame.
|
|
313
|
+
*/
|
|
314
|
+
_scheduleGestureRender() {
|
|
315
|
+
if (this._gestureCancel) return;
|
|
316
|
+
this._gestureCancel = this._requestFrame(() => {
|
|
317
|
+
this._gestureCancel = null;
|
|
318
|
+
this._renderGesture();
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Schedule a sharp culled re-render after the gesture goes idle.
|
|
323
|
+
*/
|
|
324
|
+
_scheduleSharpRender() {
|
|
325
|
+
if (this._sharpTimer) clearTimeout(this._sharpTimer);
|
|
326
|
+
this._sharpTimer = setTimeout(() => {
|
|
327
|
+
this._sharpTimer = null;
|
|
328
|
+
this._render();
|
|
329
|
+
}, SHARP_IDLE_MS);
|
|
330
|
+
}
|
|
331
|
+
/** Render the current gesture frame (fast blit or sharp, adaptively). */
|
|
332
|
+
_renderGesture() {
|
|
333
|
+
const visible = this.cull ? this._computeVisibleHexes() : null;
|
|
334
|
+
const count = visible ? visible.length : this.hexes.size;
|
|
335
|
+
if (count > FAST_RENDER_LIMIT && this._frameCanvas) {
|
|
336
|
+
this._blitFrame();
|
|
337
|
+
} else {
|
|
338
|
+
this._render(visible);
|
|
339
|
+
}
|
|
340
|
+
this._scheduleSharpRender();
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Blit the last sharp frame snapshot offset/scaled for the current transform.
|
|
344
|
+
* The snapshot is in screen (CSS) space at `_frameTransform`; this maps the
|
|
345
|
+
* pixel under the old transform to its position under the current transform.
|
|
346
|
+
*/
|
|
347
|
+
_blitFrame() {
|
|
348
|
+
const frame = this._frameCanvas;
|
|
349
|
+
const from = this._frameTransform;
|
|
350
|
+
if (!frame || !from || !frame.width || !frame.height) {
|
|
351
|
+
this._render();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
355
|
+
const displayWidth = rect.width || 800;
|
|
356
|
+
const displayHeight = rect.height || 400;
|
|
357
|
+
const ratio = this._resolvePixelRatio();
|
|
358
|
+
const { x, y, scale } = this.transform;
|
|
359
|
+
const k = scale / (from.scale || 1);
|
|
360
|
+
const bx = x - k * from.x;
|
|
361
|
+
const by = y - k * from.y;
|
|
362
|
+
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
363
|
+
this.ctx.fillStyle = this.themeColors.bgPrimary;
|
|
364
|
+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
365
|
+
this.ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
366
|
+
this.ctx.drawImage(
|
|
367
|
+
frame,
|
|
368
|
+
0,
|
|
369
|
+
0,
|
|
370
|
+
frame.width,
|
|
371
|
+
frame.height,
|
|
372
|
+
bx,
|
|
373
|
+
by,
|
|
374
|
+
displayWidth * k,
|
|
375
|
+
displayHeight * k
|
|
376
|
+
);
|
|
377
|
+
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
378
|
+
this._stats.mode = "fast";
|
|
379
|
+
this._stats.pixelRatio = ratio;
|
|
380
|
+
this._stats.scale = scale;
|
|
381
|
+
this._stats.lastRenderMs = 0;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Capture the current canvas into the offscreen frame snapshot.
|
|
385
|
+
*/
|
|
386
|
+
_captureFrame() {
|
|
387
|
+
const w = this.canvas.width;
|
|
388
|
+
const h2 = this.canvas.height;
|
|
389
|
+
if (!w || !h2) return;
|
|
390
|
+
if (!this._frameCanvas) {
|
|
391
|
+
this._frameCanvas = typeof document !== "undefined" ? document.createElement("canvas") : null;
|
|
392
|
+
if (!this._frameCanvas) return;
|
|
393
|
+
}
|
|
394
|
+
const frame = this._frameCanvas;
|
|
395
|
+
if (frame.width !== w) frame.width = w;
|
|
396
|
+
if (frame.height !== h2) frame.height = h2;
|
|
397
|
+
const fctx = frame.getContext("2d");
|
|
398
|
+
if (!fctx) return;
|
|
399
|
+
this._frameCtx = fctx;
|
|
400
|
+
fctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
401
|
+
fctx.clearRect(0, 0, w, h2);
|
|
402
|
+
fctx.drawImage(this.canvas, 0, 0);
|
|
403
|
+
this._frameTransform = { ...this.transform };
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Compute the cells intersecting the current viewport (always culled by the
|
|
407
|
+
* viewport, independent of the `cull` render switch). Uses the spatial bucket
|
|
408
|
+
* index when present.
|
|
409
|
+
* @returns {HexCell[]}
|
|
410
|
+
*/
|
|
411
|
+
_computeVisibleHexes() {
|
|
412
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
413
|
+
const displayWidth = rect.width || 800;
|
|
414
|
+
const displayHeight = rect.height || 400;
|
|
415
|
+
const { x, y, scale } = this.transform;
|
|
416
|
+
const minX = -x / scale - this.size;
|
|
417
|
+
const maxX = (displayWidth - x) / scale + this.size;
|
|
418
|
+
const minY = -y / scale - this.size;
|
|
419
|
+
const maxY = (displayHeight - y) / scale + this.size;
|
|
420
|
+
if (!this._index || this._index.size === 0) return this.getAllHexes();
|
|
421
|
+
const bs = this._bucketSize;
|
|
422
|
+
const bx0 = Math.floor(minX / bs);
|
|
423
|
+
const bx1 = Math.floor(maxX / bs);
|
|
424
|
+
const by0 = Math.floor(minY / bs);
|
|
425
|
+
const by1 = Math.floor(maxY / bs);
|
|
426
|
+
const out = [];
|
|
427
|
+
for (let by = by0; by <= by1; by++) {
|
|
428
|
+
for (let bx = bx0; bx <= bx1; bx++) {
|
|
429
|
+
const bucket = this._index.get(`${bx},${by}`);
|
|
430
|
+
if (!bucket) continue;
|
|
431
|
+
for (let i = 0; i < bucket.length; i++) {
|
|
432
|
+
const hex = bucket[i];
|
|
433
|
+
if (hex.x >= minX && hex.x <= maxX && hex.y >= minY && hex.y <= maxY) {
|
|
434
|
+
out.push(hex);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Get the cells intersecting the current viewport, sorted by row then column.
|
|
443
|
+
* @returns {HexCell[]}
|
|
444
|
+
*/
|
|
445
|
+
getVisibleHexes() {
|
|
446
|
+
return this._computeVisibleHexes().sort((a, b) => a.r - b.r || a.q - b.q);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Last-frame render metrics.
|
|
450
|
+
* @returns {{total: number, visible: number, drawn: number, mode: string, lastRenderMs: number, pixelRatio: number, scale: number}}
|
|
451
|
+
*/
|
|
452
|
+
getRenderStats() {
|
|
453
|
+
return { ...this._stats };
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Set the device-pixel-ratio multiplier and re-render (no grid regeneration).
|
|
457
|
+
* @param {number|'auto'} ratio
|
|
458
|
+
*/
|
|
459
|
+
setPixelRatio(ratio) {
|
|
460
|
+
this.pixelRatio = ratio;
|
|
461
|
+
this._render();
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Toggle viewport culling and re-render (no grid regeneration).
|
|
465
|
+
* @param {boolean} cull
|
|
466
|
+
*/
|
|
467
|
+
setCull(cull) {
|
|
468
|
+
this.cull = !!cull;
|
|
469
|
+
this._render();
|
|
470
|
+
}
|
|
242
471
|
/**
|
|
243
472
|
* Convert screen coordinates to world coordinates
|
|
244
473
|
*/
|
|
@@ -266,6 +495,8 @@ var VdHexGrid = class {
|
|
|
266
495
|
*/
|
|
267
496
|
_generateGrid() {
|
|
268
497
|
this.hexes.clear();
|
|
498
|
+
this._bucketSize = Math.max(this.size * 2, MIN_BUCKET_SIZE);
|
|
499
|
+
this._index = /* @__PURE__ */ new Map();
|
|
269
500
|
for (let r = 0; r < this.height; r++) {
|
|
270
501
|
const qOffset = Math.floor(r / 2);
|
|
271
502
|
for (let q = -qOffset; q < this.width - qOffset; q++) {
|
|
@@ -282,6 +513,15 @@ var VdHexGrid = class {
|
|
|
282
513
|
data: {}
|
|
283
514
|
};
|
|
284
515
|
this.hexes.set(`${q},${r}`, hex);
|
|
516
|
+
const bx = Math.floor(hex.x / this._bucketSize);
|
|
517
|
+
const by = Math.floor(hex.y / this._bucketSize);
|
|
518
|
+
const key = `${bx},${by}`;
|
|
519
|
+
let bucket = this._index.get(key);
|
|
520
|
+
if (!bucket) {
|
|
521
|
+
bucket = [];
|
|
522
|
+
this._index.set(key, bucket);
|
|
523
|
+
}
|
|
524
|
+
bucket.push(hex);
|
|
285
525
|
}
|
|
286
526
|
}
|
|
287
527
|
}
|
|
@@ -293,29 +533,55 @@ var VdHexGrid = class {
|
|
|
293
533
|
this.selectedHex = this.hexes.get(`${this.selectedHex.q},${this.selectedHex.r}`) ?? null;
|
|
294
534
|
}
|
|
295
535
|
/**
|
|
296
|
-
* Render the hex grid on canvas
|
|
536
|
+
* Render the hex grid on canvas (synchronous sharp render).
|
|
537
|
+
*
|
|
538
|
+
* @param {HexCell[]|null} [precomputedVisible] - Visible cells to draw when
|
|
539
|
+
* culling is on; computed from the viewport when omitted.
|
|
297
540
|
*/
|
|
298
|
-
_render() {
|
|
541
|
+
_render(precomputedVisible) {
|
|
542
|
+
const started = now();
|
|
299
543
|
const rect = this.canvas.getBoundingClientRect();
|
|
300
544
|
const displayWidth = rect.width || 800;
|
|
301
545
|
const displayHeight = rect.height || 400;
|
|
302
|
-
|
|
303
|
-
|
|
546
|
+
const ratio = this._resolvePixelRatio();
|
|
547
|
+
const bufferWidth = Math.max(1, Math.round(displayWidth * ratio));
|
|
548
|
+
const bufferHeight = Math.max(1, Math.round(displayHeight * ratio));
|
|
549
|
+
if (this.canvas.width !== bufferWidth) this.canvas.width = bufferWidth;
|
|
550
|
+
if (this.canvas.height !== bufferHeight) this.canvas.height = bufferHeight;
|
|
551
|
+
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
304
552
|
this.ctx.fillStyle = this.themeColors.bgPrimary;
|
|
305
553
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
554
|
+
this.ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
306
555
|
this.ctx.save();
|
|
307
556
|
this.ctx.translate(this.transform.x, this.transform.y);
|
|
308
557
|
this.ctx.scale(this.transform.scale, this.transform.scale);
|
|
309
|
-
this.
|
|
558
|
+
const visible = this.cull && this._index && this._index.size > 0 ? precomputedVisible || this._computeVisibleHexes() : null;
|
|
559
|
+
let drawn = 0;
|
|
560
|
+
const drawHex = (hex) => {
|
|
310
561
|
this._drawHex(hex);
|
|
562
|
+
drawn += 1;
|
|
311
563
|
if (this.customRenderCallback) {
|
|
312
564
|
this.customRenderCallback(this.ctx, hex, this.size);
|
|
313
565
|
}
|
|
314
|
-
}
|
|
566
|
+
};
|
|
567
|
+
if (visible) {
|
|
568
|
+
for (let i = 0; i < visible.length; i++) drawHex(visible[i]);
|
|
569
|
+
} else {
|
|
570
|
+
this.hexes.forEach(drawHex);
|
|
571
|
+
}
|
|
315
572
|
if (this.selectedHex) {
|
|
316
573
|
this._drawHex(this.selectedHex, true);
|
|
317
574
|
}
|
|
318
575
|
this.ctx.restore();
|
|
576
|
+
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
577
|
+
this._stats.total = this.hexes.size;
|
|
578
|
+
this._stats.visible = visible ? visible.length : this.hexes.size;
|
|
579
|
+
this._stats.drawn = drawn;
|
|
580
|
+
this._stats.mode = "sharp";
|
|
581
|
+
this._stats.pixelRatio = ratio;
|
|
582
|
+
this._stats.scale = this.transform.scale;
|
|
583
|
+
this._stats.lastRenderMs = now() - started;
|
|
584
|
+
this._captureFrame();
|
|
319
585
|
}
|
|
320
586
|
/**
|
|
321
587
|
* Draw a single hex
|
|
@@ -381,11 +647,20 @@ var VdHexGrid = class {
|
|
|
381
647
|
this.transform.x += dx;
|
|
382
648
|
this.transform.y += dy;
|
|
383
649
|
this.lastPos = cur;
|
|
384
|
-
this.
|
|
650
|
+
this._scheduleGestureRender();
|
|
385
651
|
},
|
|
386
652
|
// Pan - pointer up / leave (shared stopDrag)
|
|
387
653
|
pointerup: () => {
|
|
654
|
+
const wasDragging = this.dragging;
|
|
388
655
|
this.dragging = false;
|
|
656
|
+
if (wasDragging) {
|
|
657
|
+
this._cancelGestureRender();
|
|
658
|
+
if (this._sharpTimer) {
|
|
659
|
+
clearTimeout(this._sharpTimer);
|
|
660
|
+
this._sharpTimer = null;
|
|
661
|
+
}
|
|
662
|
+
this._render();
|
|
663
|
+
}
|
|
389
664
|
if (!this.hasMoved) {
|
|
390
665
|
this.canvas.style.cursor = "pointer";
|
|
391
666
|
}
|
|
@@ -412,7 +687,7 @@ var VdHexGrid = class {
|
|
|
412
687
|
this.transform.x = mouse.x - (mouse.x - this.transform.x) * scaleDiff;
|
|
413
688
|
this.transform.y = mouse.y - (mouse.y - this.transform.y) * scaleDiff;
|
|
414
689
|
this.transform.scale = newScale;
|
|
415
|
-
this.
|
|
690
|
+
this._scheduleGestureRender();
|
|
416
691
|
this._emit("zoom", { scale: this.transform.scale });
|
|
417
692
|
},
|
|
418
693
|
// Touch events for pinch-to-zoom
|
|
@@ -437,12 +712,18 @@ var VdHexGrid = class {
|
|
|
437
712
|
this.transform.x = center.x - (center.x - this.transform.x) * scaleDiff;
|
|
438
713
|
this.transform.y = center.y - (center.y - this.transform.y) * scaleDiff;
|
|
439
714
|
this.transform.scale = newScale;
|
|
440
|
-
this.
|
|
715
|
+
this._scheduleGestureRender();
|
|
441
716
|
this._emit("zoom", { scale: this.transform.scale });
|
|
442
717
|
}
|
|
443
718
|
},
|
|
444
719
|
touchend: () => {
|
|
445
720
|
this.touchState.touches = [];
|
|
721
|
+
this._cancelGestureRender();
|
|
722
|
+
if (this._sharpTimer) {
|
|
723
|
+
clearTimeout(this._sharpTimer);
|
|
724
|
+
this._sharpTimer = null;
|
|
725
|
+
}
|
|
726
|
+
this._render();
|
|
446
727
|
},
|
|
447
728
|
// Cursor style
|
|
448
729
|
mouseenter: () => {
|
|
@@ -893,7 +1174,11 @@ var VdHexGrid2 = (0, import_vue.defineComponent)({
|
|
|
893
1174
|
/** Grid rows (number of hexes). */
|
|
894
1175
|
height: { type: Number, default: 10 },
|
|
895
1176
|
/** Grid rotation (radians). */
|
|
896
|
-
rotation: { type: Number, default: 0 }
|
|
1177
|
+
rotation: { type: Number, default: 0 },
|
|
1178
|
+
/** Canvas backing-store multiplier: a number or `'auto'` (default). */
|
|
1179
|
+
pixelRatio: { type: [Number, String], default: "auto" },
|
|
1180
|
+
/** Viewport culling (default true). */
|
|
1181
|
+
cull: { type: Boolean, default: true }
|
|
897
1182
|
},
|
|
898
1183
|
emits: ["select", "zoom", "pan", "ready"],
|
|
899
1184
|
setup(props, { emit, expose }) {
|
|
@@ -905,7 +1190,9 @@ var VdHexGrid2 = (0, import_vue.defineComponent)({
|
|
|
905
1190
|
size: props.size,
|
|
906
1191
|
width: props.width,
|
|
907
1192
|
height: props.height,
|
|
908
|
-
rotation: props.rotation
|
|
1193
|
+
rotation: props.rotation,
|
|
1194
|
+
pixelRatio: props.pixelRatio,
|
|
1195
|
+
cull: props.cull
|
|
909
1196
|
});
|
|
910
1197
|
FORWARDED_EVENTS.forEach((name) => {
|
|
911
1198
|
instance.on(name, (data) => emit(name, data));
|
|
@@ -928,6 +1215,14 @@ var VdHexGrid2 = (0, import_vue.defineComponent)({
|
|
|
928
1215
|
() => props.rotation,
|
|
929
1216
|
(v) => instance && instance.setRotation(v)
|
|
930
1217
|
);
|
|
1218
|
+
(0, import_vue.watch)(
|
|
1219
|
+
() => props.pixelRatio,
|
|
1220
|
+
(v) => instance && instance.setPixelRatio(v)
|
|
1221
|
+
);
|
|
1222
|
+
(0, import_vue.watch)(
|
|
1223
|
+
() => props.cull,
|
|
1224
|
+
(v) => instance && instance.setCull(v)
|
|
1225
|
+
);
|
|
931
1226
|
(0, import_vue.onBeforeUnmount)(() => {
|
|
932
1227
|
if (instance) {
|
|
933
1228
|
instance.destroy();
|