@vanduo-oss/vd3-cbun 1.4.0 → 1.4.2
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 +121 -0
- package/README.md +18 -6
- package/SKILL.md +21 -5
- package/dist/charts/core.d.ts +6 -0
- package/dist/charts/index.cjs +235 -14
- package/dist/charts/index.cjs.map +3 -3
- package/dist/charts/index.js +235 -14
- package/dist/charts/index.js.map +3 -3
- package/dist/charts/vd3-charts.css +82 -0
- package/dist/charts/vue.d.ts +6 -0
- package/dist/code-editor/vd3-code-editor.css +5 -0
- package/dist/draw/core.d.ts +7 -1
- package/dist/draw/index.cjs +484 -52
- package/dist/draw/index.cjs.map +2 -2
- package/dist/draw/index.js +484 -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 +311 -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 +311 -13
- package/dist/hex-grid/index.js.map +3 -3
- package/dist/hex-grid/vue.d.ts +4 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/meta.json +33 -33
- package/package.json +10 -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-shapes > * {
|
|
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,198 @@ 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(visible);
|
|
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(visible) {
|
|
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.total = this.hexes.size;
|
|
379
|
+
this._stats.visible = visible ? visible.length : this.hexes.size;
|
|
380
|
+
this._stats.drawn = 0;
|
|
381
|
+
this._stats.mode = "fast";
|
|
382
|
+
this._stats.pixelRatio = ratio;
|
|
383
|
+
this._stats.scale = scale;
|
|
384
|
+
this._stats.lastRenderMs = 0;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Capture the current canvas into the offscreen frame snapshot.
|
|
388
|
+
*/
|
|
389
|
+
_captureFrame() {
|
|
390
|
+
const w = this.canvas.width;
|
|
391
|
+
const h2 = this.canvas.height;
|
|
392
|
+
if (!w || !h2) return;
|
|
393
|
+
if (!this._frameCanvas) {
|
|
394
|
+
this._frameCanvas = typeof document !== "undefined" ? document.createElement("canvas") : null;
|
|
395
|
+
if (!this._frameCanvas) return;
|
|
396
|
+
}
|
|
397
|
+
const frame = this._frameCanvas;
|
|
398
|
+
if (frame.width !== w) frame.width = w;
|
|
399
|
+
if (frame.height !== h2) frame.height = h2;
|
|
400
|
+
const fctx = frame.getContext("2d");
|
|
401
|
+
if (!fctx) return;
|
|
402
|
+
this._frameCtx = fctx;
|
|
403
|
+
fctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
404
|
+
fctx.clearRect(0, 0, w, h2);
|
|
405
|
+
fctx.drawImage(this.canvas, 0, 0);
|
|
406
|
+
this._frameTransform = { ...this.transform };
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Compute the cells intersecting the current viewport (always culled by the
|
|
410
|
+
* viewport, independent of the `cull` render switch). Uses the spatial bucket
|
|
411
|
+
* index when present.
|
|
412
|
+
* @returns {HexCell[]}
|
|
413
|
+
*/
|
|
414
|
+
_computeVisibleHexes() {
|
|
415
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
416
|
+
const displayWidth = rect.width || 800;
|
|
417
|
+
const displayHeight = rect.height || 400;
|
|
418
|
+
const { x, y, scale } = this.transform;
|
|
419
|
+
const minX = -x / scale - this.size;
|
|
420
|
+
const maxX = (displayWidth - x) / scale + this.size;
|
|
421
|
+
const minY = -y / scale - this.size;
|
|
422
|
+
const maxY = (displayHeight - y) / scale + this.size;
|
|
423
|
+
if (!this._index || this._index.size === 0) return this.getAllHexes();
|
|
424
|
+
const bs = this._bucketSize;
|
|
425
|
+
const bx0 = Math.floor(minX / bs);
|
|
426
|
+
const bx1 = Math.floor(maxX / bs);
|
|
427
|
+
const by0 = Math.floor(minY / bs);
|
|
428
|
+
const by1 = Math.floor(maxY / bs);
|
|
429
|
+
const out = [];
|
|
430
|
+
for (let by = by0; by <= by1; by++) {
|
|
431
|
+
for (let bx = bx0; bx <= bx1; bx++) {
|
|
432
|
+
const bucket = this._index.get(`${bx},${by}`);
|
|
433
|
+
if (!bucket) continue;
|
|
434
|
+
for (let i = 0; i < bucket.length; i++) {
|
|
435
|
+
const hex = bucket[i];
|
|
436
|
+
if (hex.x >= minX && hex.x <= maxX && hex.y >= minY && hex.y <= maxY) {
|
|
437
|
+
out.push(hex);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return out;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Get the cells intersecting the current viewport, sorted by row then column.
|
|
446
|
+
* @returns {HexCell[]}
|
|
447
|
+
*/
|
|
448
|
+
getVisibleHexes() {
|
|
449
|
+
return this._computeVisibleHexes().sort((a, b) => a.r - b.r || a.q - b.q);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Last-frame render metrics.
|
|
453
|
+
* @returns {{total: number, visible: number, drawn: number, mode: string, lastRenderMs: number, pixelRatio: number, scale: number}}
|
|
454
|
+
*/
|
|
455
|
+
getRenderStats() {
|
|
456
|
+
return { ...this._stats };
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Set the device-pixel-ratio multiplier and re-render (no grid regeneration).
|
|
460
|
+
* @param {number|'auto'} ratio
|
|
461
|
+
*/
|
|
462
|
+
setPixelRatio(ratio) {
|
|
463
|
+
this.pixelRatio = ratio;
|
|
464
|
+
this._render();
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Toggle viewport culling and re-render (no grid regeneration).
|
|
468
|
+
* @param {boolean} cull
|
|
469
|
+
*/
|
|
470
|
+
setCull(cull) {
|
|
471
|
+
this.cull = !!cull;
|
|
472
|
+
this._render();
|
|
473
|
+
}
|
|
242
474
|
/**
|
|
243
475
|
* Convert screen coordinates to world coordinates
|
|
244
476
|
*/
|
|
@@ -266,6 +498,8 @@ var VdHexGrid = class {
|
|
|
266
498
|
*/
|
|
267
499
|
_generateGrid() {
|
|
268
500
|
this.hexes.clear();
|
|
501
|
+
this._bucketSize = Math.max(this.size * 2, MIN_BUCKET_SIZE);
|
|
502
|
+
this._index = /* @__PURE__ */ new Map();
|
|
269
503
|
for (let r = 0; r < this.height; r++) {
|
|
270
504
|
const qOffset = Math.floor(r / 2);
|
|
271
505
|
for (let q = -qOffset; q < this.width - qOffset; q++) {
|
|
@@ -282,6 +516,15 @@ var VdHexGrid = class {
|
|
|
282
516
|
data: {}
|
|
283
517
|
};
|
|
284
518
|
this.hexes.set(`${q},${r}`, hex);
|
|
519
|
+
const bx = Math.floor(hex.x / this._bucketSize);
|
|
520
|
+
const by = Math.floor(hex.y / this._bucketSize);
|
|
521
|
+
const key = `${bx},${by}`;
|
|
522
|
+
let bucket = this._index.get(key);
|
|
523
|
+
if (!bucket) {
|
|
524
|
+
bucket = [];
|
|
525
|
+
this._index.set(key, bucket);
|
|
526
|
+
}
|
|
527
|
+
bucket.push(hex);
|
|
285
528
|
}
|
|
286
529
|
}
|
|
287
530
|
}
|
|
@@ -293,29 +536,55 @@ var VdHexGrid = class {
|
|
|
293
536
|
this.selectedHex = this.hexes.get(`${this.selectedHex.q},${this.selectedHex.r}`) ?? null;
|
|
294
537
|
}
|
|
295
538
|
/**
|
|
296
|
-
* Render the hex grid on canvas
|
|
539
|
+
* Render the hex grid on canvas (synchronous sharp render).
|
|
540
|
+
*
|
|
541
|
+
* @param {HexCell[]|null} [precomputedVisible] - Visible cells to draw when
|
|
542
|
+
* culling is on; computed from the viewport when omitted.
|
|
297
543
|
*/
|
|
298
|
-
_render() {
|
|
544
|
+
_render(precomputedVisible) {
|
|
545
|
+
const started = now();
|
|
299
546
|
const rect = this.canvas.getBoundingClientRect();
|
|
300
547
|
const displayWidth = rect.width || 800;
|
|
301
548
|
const displayHeight = rect.height || 400;
|
|
302
|
-
|
|
303
|
-
|
|
549
|
+
const ratio = this._resolvePixelRatio();
|
|
550
|
+
const bufferWidth = Math.max(1, Math.round(displayWidth * ratio));
|
|
551
|
+
const bufferHeight = Math.max(1, Math.round(displayHeight * ratio));
|
|
552
|
+
if (this.canvas.width !== bufferWidth) this.canvas.width = bufferWidth;
|
|
553
|
+
if (this.canvas.height !== bufferHeight) this.canvas.height = bufferHeight;
|
|
554
|
+
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
304
555
|
this.ctx.fillStyle = this.themeColors.bgPrimary;
|
|
305
556
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
557
|
+
this.ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
306
558
|
this.ctx.save();
|
|
307
559
|
this.ctx.translate(this.transform.x, this.transform.y);
|
|
308
560
|
this.ctx.scale(this.transform.scale, this.transform.scale);
|
|
309
|
-
this.
|
|
561
|
+
const visible = this.cull && this._index && this._index.size > 0 ? precomputedVisible || this._computeVisibleHexes() : null;
|
|
562
|
+
let drawn = 0;
|
|
563
|
+
const drawHex = (hex) => {
|
|
310
564
|
this._drawHex(hex);
|
|
565
|
+
drawn += 1;
|
|
311
566
|
if (this.customRenderCallback) {
|
|
312
567
|
this.customRenderCallback(this.ctx, hex, this.size);
|
|
313
568
|
}
|
|
314
|
-
}
|
|
569
|
+
};
|
|
570
|
+
if (visible) {
|
|
571
|
+
for (let i = 0; i < visible.length; i++) drawHex(visible[i]);
|
|
572
|
+
} else {
|
|
573
|
+
this.hexes.forEach(drawHex);
|
|
574
|
+
}
|
|
315
575
|
if (this.selectedHex) {
|
|
316
576
|
this._drawHex(this.selectedHex, true);
|
|
317
577
|
}
|
|
318
578
|
this.ctx.restore();
|
|
579
|
+
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
580
|
+
this._stats.total = this.hexes.size;
|
|
581
|
+
this._stats.visible = visible ? visible.length : this.hexes.size;
|
|
582
|
+
this._stats.drawn = drawn;
|
|
583
|
+
this._stats.mode = "sharp";
|
|
584
|
+
this._stats.pixelRatio = ratio;
|
|
585
|
+
this._stats.scale = this.transform.scale;
|
|
586
|
+
this._stats.lastRenderMs = now() - started;
|
|
587
|
+
this._captureFrame();
|
|
319
588
|
}
|
|
320
589
|
/**
|
|
321
590
|
* Draw a single hex
|
|
@@ -381,11 +650,20 @@ var VdHexGrid = class {
|
|
|
381
650
|
this.transform.x += dx;
|
|
382
651
|
this.transform.y += dy;
|
|
383
652
|
this.lastPos = cur;
|
|
384
|
-
this.
|
|
653
|
+
this._scheduleGestureRender();
|
|
385
654
|
},
|
|
386
655
|
// Pan - pointer up / leave (shared stopDrag)
|
|
387
656
|
pointerup: () => {
|
|
657
|
+
const wasDragging = this.dragging;
|
|
388
658
|
this.dragging = false;
|
|
659
|
+
if (wasDragging) {
|
|
660
|
+
this._cancelGestureRender();
|
|
661
|
+
if (this._sharpTimer) {
|
|
662
|
+
clearTimeout(this._sharpTimer);
|
|
663
|
+
this._sharpTimer = null;
|
|
664
|
+
}
|
|
665
|
+
this._render();
|
|
666
|
+
}
|
|
389
667
|
if (!this.hasMoved) {
|
|
390
668
|
this.canvas.style.cursor = "pointer";
|
|
391
669
|
}
|
|
@@ -412,7 +690,7 @@ var VdHexGrid = class {
|
|
|
412
690
|
this.transform.x = mouse.x - (mouse.x - this.transform.x) * scaleDiff;
|
|
413
691
|
this.transform.y = mouse.y - (mouse.y - this.transform.y) * scaleDiff;
|
|
414
692
|
this.transform.scale = newScale;
|
|
415
|
-
this.
|
|
693
|
+
this._scheduleGestureRender();
|
|
416
694
|
this._emit("zoom", { scale: this.transform.scale });
|
|
417
695
|
},
|
|
418
696
|
// Touch events for pinch-to-zoom
|
|
@@ -437,12 +715,18 @@ var VdHexGrid = class {
|
|
|
437
715
|
this.transform.x = center.x - (center.x - this.transform.x) * scaleDiff;
|
|
438
716
|
this.transform.y = center.y - (center.y - this.transform.y) * scaleDiff;
|
|
439
717
|
this.transform.scale = newScale;
|
|
440
|
-
this.
|
|
718
|
+
this._scheduleGestureRender();
|
|
441
719
|
this._emit("zoom", { scale: this.transform.scale });
|
|
442
720
|
}
|
|
443
721
|
},
|
|
444
722
|
touchend: () => {
|
|
445
723
|
this.touchState.touches = [];
|
|
724
|
+
this._cancelGestureRender();
|
|
725
|
+
if (this._sharpTimer) {
|
|
726
|
+
clearTimeout(this._sharpTimer);
|
|
727
|
+
this._sharpTimer = null;
|
|
728
|
+
}
|
|
729
|
+
this._render();
|
|
446
730
|
},
|
|
447
731
|
// Cursor style
|
|
448
732
|
mouseenter: () => {
|
|
@@ -893,7 +1177,11 @@ var VdHexGrid2 = (0, import_vue.defineComponent)({
|
|
|
893
1177
|
/** Grid rows (number of hexes). */
|
|
894
1178
|
height: { type: Number, default: 10 },
|
|
895
1179
|
/** Grid rotation (radians). */
|
|
896
|
-
rotation: { type: Number, default: 0 }
|
|
1180
|
+
rotation: { type: Number, default: 0 },
|
|
1181
|
+
/** Canvas backing-store multiplier: a number or `'auto'` (default). */
|
|
1182
|
+
pixelRatio: { type: [Number, String], default: "auto" },
|
|
1183
|
+
/** Viewport culling (default true). */
|
|
1184
|
+
cull: { type: Boolean, default: true }
|
|
897
1185
|
},
|
|
898
1186
|
emits: ["select", "zoom", "pan", "ready"],
|
|
899
1187
|
setup(props, { emit, expose }) {
|
|
@@ -905,7 +1193,9 @@ var VdHexGrid2 = (0, import_vue.defineComponent)({
|
|
|
905
1193
|
size: props.size,
|
|
906
1194
|
width: props.width,
|
|
907
1195
|
height: props.height,
|
|
908
|
-
rotation: props.rotation
|
|
1196
|
+
rotation: props.rotation,
|
|
1197
|
+
pixelRatio: props.pixelRatio,
|
|
1198
|
+
cull: props.cull
|
|
909
1199
|
});
|
|
910
1200
|
FORWARDED_EVENTS.forEach((name) => {
|
|
911
1201
|
instance.on(name, (data) => emit(name, data));
|
|
@@ -928,6 +1218,14 @@ var VdHexGrid2 = (0, import_vue.defineComponent)({
|
|
|
928
1218
|
() => props.rotation,
|
|
929
1219
|
(v) => instance && instance.setRotation(v)
|
|
930
1220
|
);
|
|
1221
|
+
(0, import_vue.watch)(
|
|
1222
|
+
() => props.pixelRatio,
|
|
1223
|
+
(v) => instance && instance.setPixelRatio(v)
|
|
1224
|
+
);
|
|
1225
|
+
(0, import_vue.watch)(
|
|
1226
|
+
() => props.cull,
|
|
1227
|
+
(v) => instance && instance.setCull(v)
|
|
1228
|
+
);
|
|
931
1229
|
(0, import_vue.onBeforeUnmount)(() => {
|
|
932
1230
|
if (instance) {
|
|
933
1231
|
instance.destroy();
|