@aiquants/spreadsheet 0.8.0 → 0.9.0
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 +50 -0
- package/README.md +86 -8
- package/dist/index.cjs +14 -14
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +4159 -3886
- package/dist/styles/spreadsheet.css +1 -1
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/package.json +3 -3
- package/src/Spreadsheet.tsx +198 -22
- package/src/grid/GridBody.tsx +16 -0
- package/src/header/ColumnHeader.tsx +84 -12
- package/src/header/RowHeader.tsx +79 -11
- package/src/header/resizeStrips.ts +86 -1
- package/src/header/useHeaderResize.ts +88 -20
- package/src/header/useHeaderSelection.ts +28 -9
- package/src/keyboard/ensureCellVisible.ts +40 -17
- package/src/keyboard/useGridKeyboard.ts +10 -4
- package/src/model/frozenPartition.ts +183 -59
- package/src/overlay/SelectionOverlay.tsx +599 -116
- package/src/overlay/useFillHandle.ts +65 -11
- package/src/popover/usePopovers.ts +39 -7
- package/src/selection/useAutoScroller.ts +58 -15
- package/src/selection/useSelectionEngine.ts +27 -10
- package/src/styles/spreadsheet.css +32 -0
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
* plus every declared aspect (format / validation / hyperlink / note), comments never; a v0.7
|
|
16
16
|
* adapter yields exactly `{ row, col, text }`.
|
|
17
17
|
*
|
|
18
|
+
* v0.9 trailing freeze (plan §2.1 / §4.5 — census #5 / #6): the hover hit test builds BOTH
|
|
19
|
+
* inverse ctx literals inline from one handle snapshot (a NEW judgment-time `getViewportSize()`
|
|
20
|
+
* read supplies `extent`; `trailingSize` cites the TREE fields); the press-time grab arithmetic
|
|
21
|
+
* builds the forward ctx pair, classifies the corner strictly (`endCol >= count − T` is
|
|
22
|
+
* trailing — the boundary track is SCROLL) and gains the ceiling clamp twins
|
|
23
|
+
* `min(inside, extent − vis − 1)` so a non-trailing corner's inside pixel never re-anchors into
|
|
24
|
+
* the trailing band (the FZ19 floor's far mirror). T = 0 keeps every path byte-identical.
|
|
25
|
+
*
|
|
18
26
|
* fill ハンドル機械 (§8 PT-2 — copy fill のみ)。常設ハンドルノードへ wire-once の pointerdown
|
|
19
27
|
* を 1 本。press は義務 repaint より先に自機フラグをセット (§8.1 の可視凍結が評価前に成立して
|
|
20
28
|
* いないと display:none が capture を殺す)。ドラッグはボックス方式 (v0.3 の斜め
|
|
@@ -29,6 +37,13 @@
|
|
|
29
37
|
* `readFillSource` / `fillSourceEdit`): `getCellFormula` 宣言時は対象毎に相対シフトした数式ソース、
|
|
30
38
|
* 宣言済み側面 (format / validation / hyperlink / note) の全部、comments は決して運ばず、v0.7
|
|
31
39
|
* アダプタは `{ row, col, text }` ちょうど。
|
|
40
|
+
*
|
|
41
|
+
* v0.9 末尾凍結 (計画 §2.1 / §4.5 — census #5 / #6): hover ヒットテストは 1 ハンドル快照から
|
|
42
|
+
* 両軸の逆写像 ctx をインライン構築 (新規の判断時 `getViewportSize()` 読みが `extent` を供給、
|
|
43
|
+
* `trailingSize` は TREE フィールドを引用)。押下時の grab 算術は順写像 ctx 対を構築し、隅を
|
|
44
|
+
* 厳密分類 (`endCol >= count − T` が末尾 — 境界トラックは SCROLL) して天井クランプ双子
|
|
45
|
+
* `min(inside, extent − vis − 1)` を得る (非末尾隅の内側 px が末尾帯へ再アンカーされない —
|
|
46
|
+
* FZ19 床の遠ミラー)。T = 0 は全経路 byte 恒等。
|
|
32
47
|
*/
|
|
33
48
|
|
|
34
49
|
import type { VirtualGridHandle } from "@aiquants/virtualscroll"
|
|
@@ -37,7 +52,7 @@ import { estimateFillCellCount, expandFill, fillSourceEdit, readFillSource } fro
|
|
|
37
52
|
import type { AxisModel } from "../model/axisModel.ts"
|
|
38
53
|
import { type CellCoord, type CellRange, clampCoord, makeRange, type SpreadsheetOperation } from "../model/cellCoord.ts"
|
|
39
54
|
import type { CellEdit, EditRejection, SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
40
|
-
import { colToViewportX, rowToViewportY, viewportXToCol, viewportYToRow } from "../model/frozenPartition.ts"
|
|
55
|
+
import { type AxisForwardCtx, colToViewportX, rowToViewportY, viewportXToCol, viewportYToRow } from "../model/frozenPartition.ts"
|
|
41
56
|
import type { SelectionState } from "../selection/selectionState.ts"
|
|
42
57
|
import type { AutoScroller } from "../selection/useAutoScroller.ts"
|
|
43
58
|
import type { SelectionEngine } from "../selection/useSelectionEngine.ts"
|
|
@@ -56,6 +71,17 @@ import type { SelectionEngine } from "../selection/useSelectionEngine.ts"
|
|
|
56
71
|
*/
|
|
57
72
|
const FILL_ARM_THRESHOLD = 4
|
|
58
73
|
|
|
74
|
+
/** `getFrozenSize()` snapshot shape (engine 3.5.0 — 10 fields incl. the trailing family). / `getFrozenSize()` 快照の形 (エンジン 3.5.0 — 末尾族込みの 10 フィールド)。 */
|
|
75
|
+
type FrozenSnapshot = ReturnType<VirtualGridHandle["getFrozenSize"]>
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Pre-attach frozen-size snapshot (all zeros — the capability branch for a not-yet-attached
|
|
79
|
+
* handle, byte-matching the engine's pre-measure state, not a value fallback).
|
|
80
|
+
* 装着前の凍結寸法快照 (全 0 — 未装着ハンドルの能力分岐。エンジンの未測定状態と byte 一致で、
|
|
81
|
+
* 値フォールバックではない)。
|
|
82
|
+
*/
|
|
83
|
+
const EMPTY_FROZEN_SIZE: FrozenSnapshot = Object.freeze({ cols: 0, width: 0, rows: 0, height: 0, trailingCols: 0, trailingRows: 0, trailingWidth: 0, trailingHeight: 0, trailingVisibleWidth: 0, trailingVisibleHeight: 0 })
|
|
84
|
+
|
|
59
85
|
type UseFillHandleParams = {
|
|
60
86
|
/** Zoom scale reader (v0.3): client px = logical px × zoom — divide client deltas once at this boundary. / ズーム倍率 reader (v0.3)。client px = 論理 px × zoom — client 差分はこの境界で 1 回だけ除算する。 */
|
|
61
87
|
zoomRef: RefObject<number>
|
|
@@ -150,10 +176,21 @@ export const useFillHandle = (params: UseFillHandleParams): SelectionEngine => {
|
|
|
150
176
|
return null
|
|
151
177
|
}
|
|
152
178
|
// ズーム境界 (v0.3): client 差分を z で 1 回だけ除算して論理 px へ。
|
|
153
|
-
//
|
|
179
|
+
// 両軸とも凍結パーティション 3 分岐逆写像 (v0.5 / v0.6 計画 §2、v0.9 §2.1)
|
|
154
180
|
const z = paramsRef.current.zoomRef.current
|
|
155
181
|
const frozen = handle.getFrozenSize()
|
|
156
|
-
|
|
182
|
+
// 判断時 extent (v0.9 §2.1 — census #5 の新規 getViewportSize() 読み)。逆写像 ctx は
|
|
183
|
+
// この場でインライン構築し (跨サイト共有ビルダ禁止 — §5.3)、trailingSize は TREE
|
|
184
|
+
// フィールド trailingWidth / trailingHeight を正準に引く (vis 代替は分類等価ゆえ命名ピン)
|
|
185
|
+
const viewport = handle.getViewportSize()
|
|
186
|
+
return clampCoord(
|
|
187
|
+
{
|
|
188
|
+
row: viewportYToRow(rowAxis, (clientY - rect.top) / z, { scroll: vy, frozenSize: frozen.height, trailingSize: frozen.trailingHeight, extent: viewport.height }),
|
|
189
|
+
col: viewportXToCol(colAxis, (clientX - rect.left) / z, { scroll: hx, frozenSize: frozen.width, trailingSize: frozen.trailingWidth, extent: viewport.width }),
|
|
190
|
+
},
|
|
191
|
+
rowAxis.count,
|
|
192
|
+
colAxis.count,
|
|
193
|
+
)
|
|
157
194
|
}, [])
|
|
158
195
|
|
|
159
196
|
/**
|
|
@@ -357,13 +394,19 @@ export const useFillHandle = (params: UseFillHandleParams): SelectionEngine => {
|
|
|
357
394
|
const hx = pressPosition.x
|
|
358
395
|
// ズーム境界 (v0.3): 論理オフセットは ×z で視覚 px へ写してから client と比較する
|
|
359
396
|
const zPress = p.zoomRef.current
|
|
360
|
-
// 隅の x は凍結パーティション順写像 (v0.5 計画 §2
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
const
|
|
397
|
+
// 隅の x は凍結パーティション順写像 (v0.5 計画 §2、v0.9 §2.1 で 3 分岐 —
|
|
398
|
+
// repaintOverlay のハンドル描画と同式)。無条件 −hx だと凍結 endCol の押下で dx が
|
|
399
|
+
// hx·z ぶん膨らみ、以後の全ヒットテストが左へ hx ずれる (誤列への fill 確定 +
|
|
400
|
+
// 暴走左オートスクロール)
|
|
401
|
+
const pressFrozen = p.handleRef.current?.getFrozenSize() ?? EMPTY_FROZEN_SIZE
|
|
402
|
+
// 判断時 forward ctx (v0.9 §2.1 — census #6): 押下 1 スナップショットの
|
|
403
|
+
// getViewportSize() を extent に両軸をその場インライン構築 (§5.3)
|
|
404
|
+
const pressViewport = p.handleRef.current?.getViewportSize() ?? { width: 0, height: 0 }
|
|
405
|
+
const pressCtxX: AxisForwardCtx = { scroll: hx, frozenCount: pressFrozen.cols, trailingCount: pressFrozen.trailingCols, extent: pressViewport.width }
|
|
406
|
+
const pressCtxY: AxisForwardCtx = { scroll: pressVy, frozenCount: pressFrozen.rows, trailingCount: pressFrozen.trailingRows, extent: pressViewport.height }
|
|
407
|
+
const cornerViewportX = colToViewportX(p.colAxis, source.endCol, pressCtxX) + p.colAxis.sizeOf(source.endCol)
|
|
365
408
|
// 隅 y も凍結パーティション順写像 (v0.6 — repaintOverlay のハンドル描画と同式)
|
|
366
|
-
const cornerViewportY = rowToViewportY(p.rowAxis, source.endRow,
|
|
409
|
+
const cornerViewportY = rowToViewportY(p.rowAxis, source.endRow, pressCtxY) + p.rowAxis.sizeOf(source.endRow)
|
|
367
410
|
// 内側 1px は論理 px の概念 (「隅の内側最終 px」) — 視覚 1px のままだと z < 1 で
|
|
368
411
|
// 1/z 論理 px に膨らみ、極小トラック (< 1/z px) では隣トラックへ再アンカーされて
|
|
369
412
|
// 押下点回帰キャンセルが 1 トラック fill に化ける。×zPress で論理 1px に固定。
|
|
@@ -371,11 +414,22 @@ export const useFillHandle = (params: UseFillHandleParams): SelectionEngine => {
|
|
|
371
414
|
// 内側 px が凍結帯へ潜って全ヒットテストが F−1 列へ解決する — 帯下限 W_F で
|
|
372
415
|
// クランプ (R2 是正。凍結 endCol と W_F = 0 はクランプ不要 — 後者は v0.4 恒等)
|
|
373
416
|
const insideRawX = cornerViewportX - 1
|
|
374
|
-
const
|
|
417
|
+
const flooredInsideX = source.endCol >= pressFrozen.cols && pressFrozen.width > 0 ? Math.max(insideRawX, pressFrozen.width) : insideRawX
|
|
418
|
+
// 厳密分類 (v0.9 §4.5 — FZ21 ミラー): endCol >= count − T が末尾 (境界列
|
|
419
|
+
// count − T − 1 は SCROLL)。T = 0 は count − 0 = count で恒偽
|
|
420
|
+
const endColIsTrailing = source.endCol >= p.colAxis.count - pressFrozen.trailingCols
|
|
421
|
+
// 天井双子 (v0.9 §4.5 — FZ19 の帯床クランプのミラー): 非末尾 endCol の内側 px が
|
|
422
|
+
// 末尾帯 [extent − vis, extent) へ潜ると全ヒットテストが末尾列へ解決し、押下点回帰
|
|
423
|
+
// キャンセルが末尾への fill 書込に化ける — 帯直前の最終 px extent − vis − 1 で
|
|
424
|
+
// クランプ (末尾 endCol 自身は帯内が正位置なのでクランプしない。T_px = 0 は恒等)
|
|
425
|
+
const insideViewportX = !endColIsTrailing && pressFrozen.trailingWidth > 0 ? Math.min(flooredInsideX, pressViewport.width - pressFrozen.trailingVisibleWidth - 1) : flooredInsideX
|
|
375
426
|
// 内側 py も帯下限 H_F でクランプ (v0.6 — 横の insideViewportX と対称。小数 vy の隅
|
|
376
427
|
// すきまでスクロール帯 endRow が凍結帯へ潜る dy ギャップを閉塞)
|
|
377
428
|
const insideRawY = cornerViewportY - 1
|
|
378
|
-
const
|
|
429
|
+
const flooredInsideY = source.endRow >= pressFrozen.rows && pressFrozen.height > 0 ? Math.max(insideRawY, pressFrozen.height) : insideRawY
|
|
430
|
+
// 縦の天井双子 (v0.9 §4.5 — 横と対称の末尾帯クランプ)
|
|
431
|
+
const endRowIsTrailing = source.endRow >= p.rowAxis.count - pressFrozen.trailingRows
|
|
432
|
+
const insideViewportY = !endRowIsTrailing && pressFrozen.trailingHeight > 0 ? Math.min(flooredInsideY, pressViewport.height - pressFrozen.trailingVisibleHeight - 1) : flooredInsideY
|
|
379
433
|
grabOffsetRef.current = { dx: event.clientX - (rect.left + insideViewportX * zPress), dy: event.clientY - (rect.top + insideViewportY * zPress) }
|
|
380
434
|
} else {
|
|
381
435
|
grabOffsetRef.current = { dx: 0, dy: 0 }
|
|
@@ -207,16 +207,28 @@ export const usePopovers = (params: UsePopoversParams): PopoverController => {
|
|
|
207
207
|
const compositionEndedAtRef = useRef<number | null>(null)
|
|
208
208
|
const tipId = useId()
|
|
209
209
|
|
|
210
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* The active cell's rect in layer px (the overlay's three-way forward map — the same recipe
|
|
212
|
+
* as the keyboard cellRect; v0.9 plan §4.6, census #7): the AxisForwardCtx pair is built
|
|
213
|
+
* INLINE from one handle snapshot per judgment, `extent` from a judgment-time
|
|
214
|
+
* `getViewportSize()` read (plan §2.1 rule 1) — a trailing cell's anchor is extent-anchored
|
|
215
|
+
* and scroll-static, exactly like the engine's trailing band.
|
|
216
|
+
* active セルの層 px 矩形 (overlay の 3 分岐順写像 — keyboard cellRect と同じ式。v0.9 計画
|
|
217
|
+
* §4.6、census #7)。AxisForwardCtx の対は判断毎に 1 ハンドルスナップショットからインライン
|
|
218
|
+
* 構築し、`extent` は判断時 `getViewportSize()` 読み (計画 §2.1 規則 1) — 末尾セルのアンカーは
|
|
219
|
+
* extent アンカーでスクロール静的 (エンジンの末尾帯と同じ)。
|
|
220
|
+
*/
|
|
211
221
|
const anchorRectOf = useCallback((cell: CellCoord): PopoverRect => {
|
|
212
222
|
const p = paramsRef.current
|
|
213
223
|
const handle = p.handleRef.current
|
|
214
|
-
// detached ハンドルは原点扱い (番兵クランプは keyboard と同じ)
|
|
224
|
+
// detached ハンドルは原点扱い (番兵クランプは keyboard と同じ)。件数 0 / extent 0 の縮退で
|
|
225
|
+
// 末尾分岐は不活性 — 未装着ホストの矩形は v0.8 と byte 一致
|
|
215
226
|
const position = handle === null ? { x: 0, y: 0 } : handle.getScrollPosition()
|
|
216
|
-
const frozen = handle === null ? { rows: 0, cols: 0,
|
|
227
|
+
const frozen = handle === null ? { rows: 0, cols: 0, trailingRows: 0, trailingCols: 0 } : handle.getFrozenSize()
|
|
228
|
+
const viewport = handle === null ? { width: 0, height: 0 } : handle.getViewportSize()
|
|
217
229
|
return {
|
|
218
|
-
top: rowToViewportY(p.rowAxis, cell.row, Math.max(0, position.y), frozen.rows),
|
|
219
|
-
left: colToViewportX(p.colAxis, cell.col, Math.max(0, position.x), frozen.cols),
|
|
230
|
+
top: rowToViewportY(p.rowAxis, cell.row, { scroll: Math.max(0, position.y), frozenCount: frozen.rows, trailingCount: frozen.trailingRows, extent: viewport.height }),
|
|
231
|
+
left: colToViewportX(p.colAxis, cell.col, { scroll: Math.max(0, position.x), frozenCount: frozen.cols, trailingCount: frozen.trailingCols, extent: viewport.width }),
|
|
220
232
|
width: p.colAxis.sizeOf(cell.col),
|
|
221
233
|
height: p.rowAxis.sizeOf(cell.row),
|
|
222
234
|
}
|
|
@@ -348,7 +360,18 @@ export const usePopovers = (params: UsePopoversParams): PopoverController => {
|
|
|
348
360
|
[showTip, cancelGrace, startGrace],
|
|
349
361
|
)
|
|
350
362
|
|
|
351
|
-
/**
|
|
363
|
+
/**
|
|
364
|
+
* Hit-tests a client position to a cell — the fill machine's recipe, three-way since v0.9
|
|
365
|
+
* (plan §4.6, census #8): the AxisInverseCtx pair is built INLINE per judgment,
|
|
366
|
+
* `trailingSize` from the TREE fields and `extent` from a NEW judgment-time
|
|
367
|
+
* `getViewportSize()` read (plan §2.1 rules 1/2 — the pre-existing dimension-probe read in
|
|
368
|
+
* `getLayerSize` is unrelated; extent 0 keeps the trailing branch inert, byte-matching v0.8).
|
|
369
|
+
* client 座標をセルへヒットテスト — fill 機械と同じ式、v0.9 から 3 分岐 (計画 §4.6、census
|
|
370
|
+
* #8)。AxisInverseCtx の対は判断毎にインライン構築し、`trailingSize` は TREE フィールド、
|
|
371
|
+
* `extent` は**新規**の判断時 `getViewportSize()` 読み (計画 §2.1 規則 1/2 — 既存の
|
|
372
|
+
* `getLayerSize` の寸法プローブ読みは無関係。extent 0 は末尾分岐を不活性に保ち v0.8 と byte
|
|
373
|
+
* 一致)。
|
|
374
|
+
*/
|
|
352
375
|
const hitTest = useCallback((clientX: number, clientY: number): CellCoord | null => {
|
|
353
376
|
const p = paramsRef.current
|
|
354
377
|
const body = p.bodyRef.current
|
|
@@ -360,7 +383,16 @@ export const usePopovers = (params: UsePopoversParams): PopoverController => {
|
|
|
360
383
|
const z = p.zoomRef.current
|
|
361
384
|
const position = handle.getScrollPosition()
|
|
362
385
|
const frozen = handle.getFrozenSize()
|
|
363
|
-
|
|
386
|
+
// 判断時の viewport 読み (§2.1 規則 1 — census #8 の新規読み。キャッシュ / ストア由来の extent は不可)
|
|
387
|
+
const viewport = handle.getViewportSize()
|
|
388
|
+
return clampCoord(
|
|
389
|
+
{
|
|
390
|
+
row: viewportYToRow(p.rowAxis, (clientY - rect.top) / z, { scroll: Math.max(0, position.y), frozenSize: frozen.height, trailingSize: frozen.trailingHeight, extent: viewport.height }),
|
|
391
|
+
col: viewportXToCol(p.colAxis, (clientX - rect.left) / z, { scroll: Math.max(0, position.x), frozenSize: frozen.width, trailingSize: frozen.trailingWidth, extent: viewport.width }),
|
|
392
|
+
},
|
|
393
|
+
p.rowAxis.count,
|
|
394
|
+
p.colAxis.count,
|
|
395
|
+
)
|
|
364
396
|
}, [])
|
|
365
397
|
|
|
366
398
|
/**
|
|
@@ -2,11 +2,22 @@
|
|
|
2
2
|
* @fileoverview Edge-zone rAF auto-scroller for drag selection (§6.4.4). Driven by the
|
|
3
3
|
* selection engine via start/stop; hit tests use the 2-axis scrollBy synchronous return
|
|
4
4
|
* (freshness hierarchy §7-4: scrollBy return > getScrollPosition() — the store carries no
|
|
5
|
-
* positions since v0.4).
|
|
5
|
+
* positions since v0.4). v0.9 (plan §4.4, census #3/#4): the far zones (right / bottom) are
|
|
6
|
+
* band-based — `p > pane − trailingVisible* − EDGE_ZONE` — with the trailing home-territory
|
|
7
|
+
* suppressor (anchor inside the trailing band AND T_px > 0 AND pointer inside the band ⇒ no
|
|
8
|
+
* drive, the exact far twin of the v0.5 R5 leading rule), and the hover resolution builds
|
|
9
|
+
* its AxisInverseCtx inline per tick, reusing the tick-top `getViewportSize()` snapshot as
|
|
10
|
+
* `extent` (same-tick freshness — plan §0.4 R1). T = 0 leaves every zone and ctx expression
|
|
11
|
+
* byte-identical to the v0.8 arithmetic.
|
|
6
12
|
*
|
|
7
13
|
* ドラッグ選択の端ゾーン rAF オートスクロール (§6.4.4)。エンジンが start/stop で駆動し、
|
|
8
14
|
* ヒットテストは 2 軸 scrollBy の同期返り値を使う (鮮度序列 §7-4: scrollBy 返り値 >
|
|
9
|
-
* getScrollPosition — v0.4 からストアは位置を運ばない)。
|
|
15
|
+
* getScrollPosition — v0.4 からストアは位置を運ばない)。v0.9 (計画 §4.4、census #3/#4): 遠
|
|
16
|
+
* ゾーン (右 / 下) は帯基準 `p > pane − trailingVisible* − EDGE_ZONE`、末尾ホームテリトリー
|
|
17
|
+
* (アンカー末尾帯 ∧ T_px > 0 ∧ ポインタ帯内 ⇒ 無駆動 — v0.5 R5 の leading 規則の遠端双子) を
|
|
18
|
+
* 併設。hover 解決は AxisInverseCtx を tick 内その場構築し、extent は tick 冒頭の
|
|
19
|
+
* `getViewportSize()` スナップショットを再利用 (同 tick 鮮度 — 計画 §0.4 R1)。T = 0 は全
|
|
20
|
+
* ゾーン / ctx 式が v0.8 算術と byte 一致。
|
|
10
21
|
*/
|
|
11
22
|
|
|
12
23
|
import type { VirtualGridHandle } from "@aiquants/virtualscroll"
|
|
@@ -49,17 +60,22 @@ export type AutoScrollContext = {
|
|
|
49
60
|
* Drag-anchor column for the frozen home-territory rule (v0.5 R5 fix): when the anchor is
|
|
50
61
|
* a frozen column AND the pointer sits inside the frozen band, the band-based left zone
|
|
51
62
|
* does NOT drive — operating within the frozen pane must not scroll the band (Excel-like).
|
|
63
|
+
* v0.9 (plan §4.4): the same anchor also feeds the TRAILING home territory — an anchor in
|
|
64
|
+
* the trailing band with the pointer inside that band suppresses the band-based right zone.
|
|
52
65
|
* Omit for vertical-only drags.
|
|
53
66
|
* 凍結ホームテリトリー規則 (v0.5 R5 是正) 用のドラッグアンカー列。アンカーが凍結列で
|
|
54
67
|
* ポインタも凍結帯内なら帯基準左ゾーンは駆動しない — 凍結ペイン内の操作は可動帯を
|
|
55
|
-
* スクロールさせない (Excel 同型)
|
|
68
|
+
* スクロールさせない (Excel 同型)。v0.9 (計画 §4.4): 同じアンカーが末尾ホームテリトリーにも
|
|
69
|
+
* 供給され、末尾帯アンカー × ポインタ帯内は帯基準右ゾーンを抑止。縦専用ドラッグでは省略。
|
|
56
70
|
*/
|
|
57
71
|
anchorCol?: number
|
|
58
72
|
/**
|
|
59
73
|
* Drag-anchor row for the vertical frozen home-territory rule (row twin of anchorCol — v0.6):
|
|
60
|
-
* anchor frozen AND pointer inside the frozen band suppresses the band-based top zone
|
|
74
|
+
* anchor frozen AND pointer inside the frozen band suppresses the band-based top zone; the
|
|
75
|
+
* trailing twin (v0.9 plan §4.4) suppresses the band-based bottom zone.
|
|
61
76
|
* 縦の凍結ホームテリトリー規則用のドラッグアンカー行 (anchorCol の行双子 — v0.6)。アンカーが
|
|
62
|
-
*
|
|
77
|
+
* 凍結行でポインタも凍結帯内なら帯基準上ゾーンは駆動しない。末尾双子 (v0.9 計画 §4.4) は
|
|
78
|
+
* 帯基準下ゾーンを抑止。
|
|
63
79
|
*/
|
|
64
80
|
anchorRow?: number
|
|
65
81
|
}
|
|
@@ -143,10 +159,20 @@ export const useAutoScroller = ({ handleRef, rowAxis, colAxis, bodyRef, onDragFr
|
|
|
143
159
|
let dx = 0
|
|
144
160
|
if (axis !== "x") {
|
|
145
161
|
// 上端ゾーンは帯基準 (v0.6 計画 §3 — 横の左端ゾーンと対称): スクロール帯の上端 = H_F。
|
|
146
|
-
// 下端優先 + 凍結ホームテリトリー (アンカー凍結行 × ポインタ帯内は駆動しない、H_F = 0 除外)
|
|
162
|
+
// 下端優先 + 凍結ホームテリトリー (アンカー凍結行 × ポインタ帯内は駆動しない、H_F = 0 除外)。
|
|
163
|
+
// v0.9 (計画 §4.4): 下端ゾーンも帯基準 — スクロール帯の下端 = paneH − H_T_vis (vis =
|
|
164
|
+
// getFrozenSize().trailingVisibleHeight、tick スナップショット)。末尾ホームテリトリーは
|
|
165
|
+
// leading 双子と同形: アンカー末尾帯 (row >= count − trailingRows) ∧ T_px > 0 ∧ ポインタ
|
|
166
|
+
// 帯内 (py >= paneH − vis — 境界画素の >= は逆写像の末尾分類とミラー) ⇒ 駆動しない。
|
|
167
|
+
// T_px = 0 (末尾行が全て非表示) は可視帯が存在しない — ペイン外の深い py を帯内と誤認
|
|
168
|
+
// して抑止しない (leading の H_F = 0 除外の双子)
|
|
147
169
|
const frozenV = handle.getFrozenSize()
|
|
148
|
-
|
|
149
|
-
|
|
170
|
+
const visH = frozenV.trailingVisibleHeight
|
|
171
|
+
if (py > paneH - visH - EDGE_ZONE) {
|
|
172
|
+
const anchorTrailingRow = context.anchorRow !== undefined && frozenV.trailingRows > 0 && context.anchorRow >= rowAxis.count - frozenV.trailingRows
|
|
173
|
+
if (!(anchorTrailingRow && visH > 0 && py >= paneH - visH)) {
|
|
174
|
+
dy = speedFor(py - (paneH - visH - EDGE_ZONE))
|
|
175
|
+
}
|
|
150
176
|
} else if (py < frozenV.height + EDGE_ZONE) {
|
|
151
177
|
const anchorFrozenRow = context.anchorRow !== undefined && context.anchorRow < frozenV.rows
|
|
152
178
|
if (!(anchorFrozenRow && frozenV.height > 0 && py < frozenV.height)) {
|
|
@@ -158,12 +184,19 @@ export const useAutoScroller = ({ handleRef, rowAxis, colAxis, bodyRef, onDragFr
|
|
|
158
184
|
// 左端ゾーンは帯基準 (v0.5 計画 §3-6): スクロール帯の左端 = W_F。スクロール帯アンカーの
|
|
159
185
|
// ドラッグは凍結帯上の保持でも左スクロールが継続する (Excel 同型 — 帯を跨いだ引き寄せ)。
|
|
160
186
|
// 判定は右端優先 (R2 是正): 帯幅 < 2×EDGE_ZONE で両ゾーンが重なると左優先は右端保持の
|
|
161
|
-
// ドラッグを逆走させる —
|
|
162
|
-
//
|
|
163
|
-
//
|
|
187
|
+
// ドラッグを逆走させる — 重なりは右端が勝つ (v0.9 でも far-edge-wins 形は不変)。凍結
|
|
188
|
+
// ホームテリトリー (R5 是正): アンカーが凍結列でポインタも凍結帯内なら駆動しない —
|
|
189
|
+
// 凍結ペイン内の選択操作が可動帯を勝手に左スクロールさせない (帯外 [W_F, W_F+EDGE_ZONE)
|
|
190
|
+
// の戻りドラッグは従来どおり駆動)。v0.9 (計画 §4.4): 右端ゾーンも帯基準 — スクロール帯の
|
|
191
|
+
// 右端 = paneW − W_T_vis。末尾ホームテリトリーは縦と同形 (アンカー末尾列 ∧ T_px > 0 ∧
|
|
192
|
+
// ポインタ帯内 ⇒ 無駆動、T_px = 0 除外)
|
|
164
193
|
const frozen = handle.getFrozenSize()
|
|
165
|
-
|
|
166
|
-
|
|
194
|
+
const visW = frozen.trailingVisibleWidth
|
|
195
|
+
if (px > paneW - visW - EDGE_ZONE) {
|
|
196
|
+
const anchorTrailingCol = context.anchorCol !== undefined && frozen.trailingCols > 0 && context.anchorCol >= colAxis.count - frozen.trailingCols
|
|
197
|
+
if (!(anchorTrailingCol && visW > 0 && px >= paneW - visW)) {
|
|
198
|
+
dx = speedFor(px - (paneW - visW - EDGE_ZONE))
|
|
199
|
+
}
|
|
167
200
|
} else if (px < frozen.width + EDGE_ZONE) {
|
|
168
201
|
// W_F = 0 (凍結列が全て非表示) は可視の凍結帯が存在しない — 負 px を帯内と
|
|
169
202
|
// 誤認して抑止しない (R5 是正)
|
|
@@ -183,9 +216,19 @@ export const useAutoScroller = ({ handleRef, rowAxis, colAxis, bodyRef, onDragFr
|
|
|
183
216
|
// 静止 + 無スクロール + hover 同値の tick は書き込みをスキップ (同値スタイルの毎フレーム
|
|
184
217
|
// 再書き込みを抑止 — スクロール追随の再配置は repaintOverlay 側が担う)
|
|
185
218
|
if (vy >= 0 && hx >= 0 && rowAxis.totalSize() > 0 && colAxis.totalSize() > 0) {
|
|
186
|
-
// 行 /
|
|
219
|
+
// 行 / 列とも凍結パーティション 3 分岐逆写像 (v0.6 / v0.5 計画 §2、末尾は v0.9 §2.1)。
|
|
220
|
+
// ctx は判断のこの場でインライン構築 (§2.1 規則 1 — census #4)。trailingSize は TREE
|
|
221
|
+
// フィールド (規則 2)、extent は tick 冒頭の viewport スナップショットを再利用 (同 tick
|
|
222
|
+
// 鮮度 — 計画 §0.4 R1。paneW/H <= 0 は上で次フレームへ抜けるため extent > 0 が保たれる)
|
|
187
223
|
const frozenHover = handle.getFrozenSize()
|
|
188
|
-
const hover = clampCoord(
|
|
224
|
+
const hover = clampCoord(
|
|
225
|
+
{
|
|
226
|
+
row: viewportYToRow(rowAxis, py, { scroll: vy, frozenSize: frozenHover.height, trailingSize: frozenHover.trailingHeight, extent: viewport.height }),
|
|
227
|
+
col: viewportXToCol(colAxis, px, { scroll: hx, frozenSize: frozenHover.width, trailingSize: frozenHover.trailingWidth, extent: viewport.width }),
|
|
228
|
+
},
|
|
229
|
+
rowAxis.count,
|
|
230
|
+
colAxis.count,
|
|
231
|
+
)
|
|
189
232
|
const last = lastHoverRef.current
|
|
190
233
|
if (dy !== 0 || dx !== 0 || last === null || last.row !== hover.row || last.col !== hover.col) {
|
|
191
234
|
lastHoverRef.current = hover
|
|
@@ -85,15 +85,22 @@ const caretIndexFromPoint = (clientX: number, clientY: number, cell: CellCoord):
|
|
|
85
85
|
/**
|
|
86
86
|
* Body-relative pointer → cell hit test with the §7-4 freshness hierarchy: the handle's
|
|
87
87
|
* synchronous scroll position, the press-cached body rect, one zoom division at the boundary
|
|
88
|
-
* and the frozen-partition inverse mapping on both axes
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
88
|
+
* and the three-way frozen-partition inverse mapping on both axes (leading > trailing >
|
|
89
|
+
* middle — v0.9 plan §4.2, census #1), clamped into the grid. The AxisInverseCtx pair is
|
|
90
|
+
* built INLINE from one handle snapshot per judgment (`trailingSize` = the TREE fields,
|
|
91
|
+
* `extent` = a judgment-time `getViewportSize()` read — plan §2.1 rules 1/2; extent 0 keeps
|
|
92
|
+
* the trailing branch inert, byte-matching v0.8). ONE mapping shared by the cell engine and
|
|
93
|
+
* the reference-drag owner (usePointDrag — plan §4.1.18) so an identical pointer resolves to
|
|
94
|
+
* an identical cell in both. null = unmeasurable (no handle / rect, detached scroll
|
|
95
|
+
* sentinel, empty axis).
|
|
92
96
|
* body 基準のポインタ → セルのヒットテスト (§7-4 の鮮度序列): ハンドルの同期スクロール位置・
|
|
93
|
-
* 押下時計測の body rect・境界で 1
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
+
* 押下時計測の body rect・境界で 1 回だけのズーム除算・両軸の凍結パーティション 3 分岐逆写像
|
|
98
|
+
* (先頭 > 末尾 > 中央 — v0.9 計画 §4.2、census #1)、グリッド内へクランプ。AxisInverseCtx の対は
|
|
99
|
+
* 判断毎に 1 ハンドルスナップショットからインライン構築 (`trailingSize` は TREE フィールド、
|
|
100
|
+
* `extent` は判断時 `getViewportSize()` 読み — 計画 §2.1 規則 1/2。extent 0 は末尾分岐を不活性に
|
|
101
|
+
* 保ち v0.8 と byte 一致)。セルエンジンと参照ドラッグ所有者 (usePointDrag — 計画 §4.1.18) が
|
|
102
|
+
* 共有する単一写像 (同じポインタは両者で同じセルに解決する)。null = 計測不能 (ハンドル / rect
|
|
103
|
+
* 不在、detached 番兵、空軸)。
|
|
97
104
|
*/
|
|
98
105
|
export const hitTestCell = (handle: VirtualGridHandle | null, rect: DOMRect | null, rowAxis: AxisModel, colAxis: AxisModel, zoom: number, clientX: number, clientY: number): CellCoord | null => {
|
|
99
106
|
if (handle === null || rect === null) {
|
|
@@ -109,9 +116,19 @@ export const hitTestCell = (handle: VirtualGridHandle | null, rect: DOMRect | nu
|
|
|
109
116
|
return null
|
|
110
117
|
}
|
|
111
118
|
// ズーム境界 (v0.3): rect / client は視覚 px、軸は論理 px — 差分を zoom で 1 回だけ除算。
|
|
112
|
-
// 行 /
|
|
119
|
+
// 行 / 列とも凍結パーティション 3 分岐逆写像 (v0.6 / v0.5 計画 §2、末尾は v0.9 §2.1)。ctx は
|
|
120
|
+
// この判断のその場構築 (規則 1): trailingSize は TREE フィールド (規則 2)、extent は判断時の
|
|
121
|
+
// getViewportSize() 読み (未測定 {0,0} は末尾分岐が不活性のまま = v0.8 恒等の縮退)
|
|
113
122
|
const frozen = handle.getFrozenSize()
|
|
114
|
-
|
|
123
|
+
const viewport = handle.getViewportSize()
|
|
124
|
+
return clampCoord(
|
|
125
|
+
{
|
|
126
|
+
row: viewportYToRow(rowAxis, (clientY - rect.top) / zoom, { scroll: vy, frozenSize: frozen.height, trailingSize: frozen.trailingHeight, extent: viewport.height }),
|
|
127
|
+
col: viewportXToCol(colAxis, (clientX - rect.left) / zoom, { scroll: hx, frozenSize: frozen.width, trailingSize: frozen.trailingWidth, extent: viewport.width }),
|
|
128
|
+
},
|
|
129
|
+
rowAxis.count,
|
|
130
|
+
colAxis.count,
|
|
131
|
+
)
|
|
115
132
|
}
|
|
116
133
|
|
|
117
134
|
type UseSelectionEngineParams = {
|
|
@@ -1027,3 +1027,35 @@
|
|
|
1027
1027
|
width: 100%;
|
|
1028
1028
|
height: 100%;
|
|
1029
1029
|
}
|
|
1030
|
+
|
|
1031
|
+
/*
|
|
1032
|
+
* v0.9 凍結分割線 (計画 §2.4 — ADR-20 / ADR-24): 全凍結境界の強い Excel / Sheets 風の線。
|
|
1033
|
+
* consumer 面は CSS 変数 2 つだけ (--aqsp-divider-color / --aqsp-divider-width — パッケージは
|
|
1034
|
+
* 読むだけで書き手は consumer スタイルシート単独)。既定は 2px の #9ca3af (gray-400 — 既存全
|
|
1035
|
+
* 罫線より 1 段濃い)。width 0px は逃げ道 — 象限別 fill 断片の継ぎ目が再露出し forced-colors も
|
|
1036
|
+
* 無描画 (意図的、README / 分冊 05 §8 登記)。位置 (leading 縁 0 / trailing 縁 0) と長さは
|
|
1037
|
+
* 各セグメントの静的インライン幾何 + ホストクリップが供給し、このブロックは太さと色だけを持つ。
|
|
1038
|
+
* pointer-events: none — バーは不活性で、リサイズの operand は帯側隣接ストリップ (ADR-20)。
|
|
1039
|
+
*/
|
|
1040
|
+
.aqsp-freeze-divider {
|
|
1041
|
+
position: absolute;
|
|
1042
|
+
background: var(--aqsp-divider-color, #9ca3af);
|
|
1043
|
+
pointer-events: none;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/* 縦線 (x 境界) は太さ = width、横線 (y 境界) は太さ = height (共有変数 1 本) */
|
|
1047
|
+
.aqsp-freeze-divider-v {
|
|
1048
|
+
width: var(--aqsp-divider-width, 2px);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
.aqsp-freeze-divider-h {
|
|
1052
|
+
height: var(--aqsp-divider-width, 2px);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/* forced-colors: 不透明な実線は再着色で生存する — CanvasText へ寄せるだけ (§2.4。凍結クリップの
|
|
1056
|
+
無装飾免除とは別物で、分割線は自前の規則を持つ) */
|
|
1057
|
+
@media (forced-colors: active) {
|
|
1058
|
+
.aqsp-freeze-divider {
|
|
1059
|
+
background: CanvasText;
|
|
1060
|
+
}
|
|
1061
|
+
}
|