@growgroup/visual-editor 0.9.0 → 0.9.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/package.json +1 -1
- package/src/editor/components/comments/CommentBoard.tsx +5 -5
- package/src/editor/components/ppt/PptComments.tsx +27 -6
- package/src/editor/hooks/useContextMenuHandler.ts +3 -1
- package/src/editor/hooks/useElementSelection.ts +5 -1
- package/src/editor/hooks/useIframeSetup.ts +11 -2
- package/src/editor/parts.ts +2 -1
- package/src/editor/utils/svg-hit.ts +141 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -24,7 +24,7 @@ import { useState } from 'react';
|
|
|
24
24
|
import { Check, CornerUpLeft, Crosshair, ExternalLink, MapPin, MessageSquare, RotateCcw, Send, Trash2 } from 'lucide-react';
|
|
25
25
|
import type { Deck, SlideComment } from '../../../lib/deck';
|
|
26
26
|
import { PPT_PALETTES, type PptTheme } from '../ppt/PptChrome';
|
|
27
|
-
import { describeRect, fmtTime, unresolvedCount } from '../ppt/PptComments';
|
|
27
|
+
import { anchorRectOf, describeRect, fmtTime, unresolvedCount } from '../ppt/PptComments';
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* アンカー・返信のリンク色。
|
|
@@ -371,14 +371,14 @@ function BoardCard({
|
|
|
371
371
|
{/* 何に対する指摘か。範囲 > 要素 > ページ全体 */}
|
|
372
372
|
<div
|
|
373
373
|
className="mt-1.5 flex items-center gap-1 text-[10px]"
|
|
374
|
-
style={{ color: comment
|
|
374
|
+
style={{ color: anchorRectOf(comment) || comment.anchorSrc ? accent : pal.sub }}
|
|
375
375
|
title={comment.anchorSrc}
|
|
376
376
|
>
|
|
377
|
-
{comment
|
|
377
|
+
{anchorRectOf(comment) ? <Crosshair className="h-3 w-3 shrink-0" /> : <MapPin className="h-3 w-3 shrink-0" />}
|
|
378
378
|
<span className="truncate">
|
|
379
379
|
{comment.seq != null && <span className="mr-1 tabular-nums">#{comment.seq}</span>}
|
|
380
|
-
{comment
|
|
381
|
-
? describeRect(comment
|
|
380
|
+
{anchorRectOf(comment)
|
|
381
|
+
? describeRect(anchorRectOf(comment)!)
|
|
382
382
|
: comment.anchorSrc ? (comment.anchorLabel ? `「${comment.anchorLabel}」` : '要素に添付') : 'ページ全体'}
|
|
383
383
|
</span>
|
|
384
384
|
</div>
|
|
@@ -223,6 +223,26 @@ export function fitRect(rect: CommentRect, artboard: HTMLElement): CommentRect {
|
|
|
223
223
|
return { x: rect.x * sx, y: rect.y, width: rect.width * sx, height: rect.height };
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* コメントの範囲。anchorRect が無くても、表示用の文言(「範囲 (x, y) w×h」「点 (x, y)」)から戻す。
|
|
228
|
+
*
|
|
229
|
+
* 【なぜ文言から戻すか】
|
|
230
|
+
* 範囲コメント(0.4.0)より前の提案書・構成ラフの殻は、コメントの保存で anchorRect と seq を捨て、
|
|
231
|
+
* anchorLabel だけを残す(殻の vite-plugin の許可リストに無いため)。そのままだと範囲を指定して投稿しても
|
|
232
|
+
* 紙面に枠が出ず、「範囲が表示されない」になる。文言はエディタが describeRect で書いた形なので、そこから戻せる。
|
|
233
|
+
* 要素に付けたコメント(anchorSrc)の anchorLabel は要素の文字なので読まない
|
|
234
|
+
*/
|
|
235
|
+
export function anchorRectOf(c: Pick<SlideComment, 'anchorRect' | 'anchorLabel' | 'anchorSrc'>): CommentRect | undefined {
|
|
236
|
+
if (c.anchorRect) return c.anchorRect;
|
|
237
|
+
if (c.anchorSrc || !c.anchorLabel) return undefined;
|
|
238
|
+
const num = '(-?\\d+(?:\\.\\d+)?)';
|
|
239
|
+
const range = c.anchorLabel.match(new RegExp(`^範囲 \\(${num}, ${num}\\) ${num}×${num}$`));
|
|
240
|
+
if (range) return { x: +range[1], y: +range[2], width: +range[3], height: +range[4] };
|
|
241
|
+
const point = c.anchorLabel.match(new RegExp(`^点 \\(${num}, ${num}\\)$`));
|
|
242
|
+
if (point) return { x: +point[1], y: +point[2], width: 0, height: 0 };
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
226
246
|
/** 矩形を人が読む形に(「範囲 (120, 840) 600×240」「点 (120, 840)」) */
|
|
227
247
|
export function describeRect(r: CommentRect): string {
|
|
228
248
|
const n = (v: number) => String(Math.round(v));
|
|
@@ -282,7 +302,7 @@ export function useCommentMarkers({
|
|
|
282
302
|
const artboard = doc?.getElementById('artboard');
|
|
283
303
|
if (!doc || !artboard) return;
|
|
284
304
|
const layer = ensureCommentLayer(doc, artboard);
|
|
285
|
-
const shown = comments.filter((c) => (c
|
|
305
|
+
const shown = comments.filter((c) => (anchorRectOf(c) || c.anchorSrc) && !c.resolved);
|
|
286
306
|
const wanted = new Set(shown.map((c) => c.id));
|
|
287
307
|
layer.querySelectorAll<HTMLElement>('[data-comment-id]').forEach((el) => {
|
|
288
308
|
if (!wanted.has(el.dataset.commentId!)) el.remove();
|
|
@@ -318,8 +338,9 @@ export function useCommentMarkers({
|
|
|
318
338
|
const isActive = c.id === activeThreadId;
|
|
319
339
|
let x = 0;
|
|
320
340
|
let y = 0;
|
|
321
|
-
|
|
322
|
-
|
|
341
|
+
const anchorRect = anchorRectOf(c);
|
|
342
|
+
if (anchorRect) {
|
|
343
|
+
const r = fitRect(anchorRect, artboard);
|
|
323
344
|
if (isPointRect(r)) {
|
|
324
345
|
layer.querySelector(`[data-comment-id="${CSS.escape(c.id)}"][data-comment-rect]`)?.remove();
|
|
325
346
|
x = r.x - size / 2;
|
|
@@ -404,7 +425,7 @@ export function useCommentMarkers({
|
|
|
404
425
|
}, [
|
|
405
426
|
active, page, getIframeDoc, outerZoom, activeThreadId,
|
|
406
427
|
JSON.stringify(draftRect),
|
|
407
|
-
JSON.stringify(comments.map((c) => [c.id, c.anchorSrc, c.anchorRect, c.seq, c.resolved, c.author, c.text])),
|
|
428
|
+
JSON.stringify(comments.map((c) => [c.id, c.anchorSrc, c.anchorRect, c.anchorLabel, c.seq, c.resolved, c.author, c.text])),
|
|
408
429
|
]);
|
|
409
430
|
}
|
|
410
431
|
|
|
@@ -770,10 +791,10 @@ export function PptCommentsPanel({
|
|
|
770
791
|
</button>
|
|
771
792
|
</div>
|
|
772
793
|
|
|
773
|
-
{c
|
|
794
|
+
{anchorRectOf(c) ? (
|
|
774
795
|
<div className="mt-1.5 flex items-center gap-1 text-[10px]" style={{ color: 'var(--ed-accent)' }} data-comment-rect-label>
|
|
775
796
|
<Crosshair className="h-3 w-3" />
|
|
776
|
-
{describeRect(c
|
|
797
|
+
{describeRect(anchorRectOf(c)!)}
|
|
777
798
|
</div>
|
|
778
799
|
) : c.anchorSrc && (
|
|
779
800
|
<div className="mt-1.5 flex items-center gap-1 text-[10px]" style={{ color: 'var(--ed-accent)' }}>
|
|
@@ -9,6 +9,7 @@ import { useCallback, useRef, useEffect, MutableRefObject } from 'react';
|
|
|
9
9
|
import { useEditorContext } from '../EditorContext';
|
|
10
10
|
import { updateSelectionBox } from '../utils/dom-utils';
|
|
11
11
|
import { extractElementInfo } from '../utils/style-utils';
|
|
12
|
+
import { passThroughSvg } from '../utils/svg-hit';
|
|
12
13
|
|
|
13
14
|
interface UseContextMenuHandlerOptions {
|
|
14
15
|
/** 編集可能要素を取得する関数 */
|
|
@@ -86,7 +87,8 @@ export function useContextMenuHandler(
|
|
|
86
87
|
const y = iframeRect.top + e.clientY * outerScale;
|
|
87
88
|
|
|
88
89
|
// 要素がクリックされた場合は選択
|
|
89
|
-
|
|
90
|
+
// SVG の箱の空きは素通りさせる(クリックの判別と同じ。utils/svg-hit.ts)
|
|
91
|
+
const element = getEditableElement(passThroughSvg(iframeDoc, e.target, e.clientX, e.clientY), iframeDoc);
|
|
90
92
|
if (element) {
|
|
91
93
|
const elementId = element.getAttribute('data-element-id') || '';
|
|
92
94
|
const currentIds = selectedElementIdsRef.current;
|
|
@@ -26,6 +26,7 @@ import { extractElementInfo } from "../utils/style-utils";
|
|
|
26
26
|
import { MARQUEE_DRAG_THRESHOLD } from "../constants";
|
|
27
27
|
import type { DragState, ResizeState, MarqueeState } from "../types";
|
|
28
28
|
import { debugLog } from '../utils/debug';
|
|
29
|
+
import { passThroughSvg } from '../utils/svg-hit';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* mousedown で保留し、mouseup で意味を確定するクリック情報
|
|
@@ -1181,6 +1182,9 @@ export function useElementSelection(
|
|
|
1181
1182
|
),
|
|
1182
1183
|
) ?? null;
|
|
1183
1184
|
}
|
|
1185
|
+
// SVG の箱の中で何も描かれていない所を押したら、SVG を素通りして下の要素を押したことにする。
|
|
1186
|
+
// 装飾の SVG が文字やボタンに重なっていると、素のクリックも Cmd+クリックも SVG しか選べなかった(utils/svg-hit.ts)
|
|
1187
|
+
hitSource = passThroughSvg(iframeDoc, hitSource, e.clientX, e.clientY);
|
|
1184
1188
|
const hit = getEditableElement(hitSource, iframeDoc);
|
|
1185
1189
|
// 空白=押した点の祖先に編集可能要素がまったく無い状態。
|
|
1186
1190
|
// #artboard / 紙面と同じ大きさの器 / #artboard-wrapper / #canvas-container は
|
|
@@ -1432,7 +1436,7 @@ export function useElementSelection(
|
|
|
1432
1436
|
|
|
1433
1437
|
|
|
1434
1438
|
const isCtrlCmd = e.ctrlKey || e.metaKey;
|
|
1435
|
-
const deepElement = getEditableElement(e.target, iframeDoc);
|
|
1439
|
+
const deepElement = getEditableElement(passThroughSvg(iframeDoc, e.target, e.clientX, e.clientY), iframeDoc);
|
|
1436
1440
|
|
|
1437
1441
|
if (!deepElement) return;
|
|
1438
1442
|
|
|
@@ -12,6 +12,7 @@ import { buildDomTree, isInlineElement, isNonEditableTag } from '../utils/dom-ut
|
|
|
12
12
|
import { EDITOR_IFRAME_STYLES } from '../constants';
|
|
13
13
|
import { isLockedInsidePart } from '../parts';
|
|
14
14
|
import { debugLog } from '../utils/debug';
|
|
15
|
+
import { isInsideSvg, passThroughSvg } from '../utils/svg-hit';
|
|
15
16
|
|
|
16
17
|
interface UseIframeSetupReturn {
|
|
17
18
|
/**
|
|
@@ -107,7 +108,14 @@ export function useIframeSetup(): UseIframeSetupReturn {
|
|
|
107
108
|
element.removeAttribute('data-inline');
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
110
|
-
if (element.closest('script, style
|
|
111
|
+
if (element.closest('script, style')) return;
|
|
112
|
+
// SVG の中(path / circle / g など)は SVG ごと 1 つの図として扱う(utils/svg-hit.ts)。
|
|
113
|
+
// 以前は path と g だけを外していたので、circle や rect が単独で選ばれていた
|
|
114
|
+
if (isInsideSvg(element)) {
|
|
115
|
+
element.removeAttribute('data-editable');
|
|
116
|
+
element.removeAttribute('data-element-id');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
111
119
|
if (element.classList.contains('material-icons')) return;
|
|
112
120
|
if (element.classList.contains('material-icons-outlined')) return;
|
|
113
121
|
// selection-box およびその子要素はスキップ
|
|
@@ -274,7 +282,8 @@ export function useIframeSetup(): UseIframeSetupReturn {
|
|
|
274
282
|
clearPreview();
|
|
275
283
|
return;
|
|
276
284
|
}
|
|
277
|
-
|
|
285
|
+
// SVG の箱の空きは素通りさせる(クリックの判別と同じ。utils/svg-hit.ts)
|
|
286
|
+
const next = resolveHoverTarget(passThroughSvg(iframeDoc, e.target, e.clientX, e.clientY), iframeDoc, {
|
|
278
287
|
meta: e.metaKey || e.ctrlKey,
|
|
279
288
|
});
|
|
280
289
|
if (next === currentPreviewElement) return;
|
package/src/editor/parts.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import type { EditorPartDef } from '../io';
|
|
18
18
|
import { findFlowInsertion } from './utils/drop-target';
|
|
19
19
|
import { isNonEditableTag } from './utils/dom-utils';
|
|
20
|
+
import { isInsideSvg } from './utils/svg-hit';
|
|
20
21
|
|
|
21
22
|
export const PART_ATTR = 'data-part';
|
|
22
23
|
export const PART_VERSION_ATTR = 'data-part-v';
|
|
@@ -317,7 +318,7 @@ export function unlockPartDescendants(root: HTMLElement): number {
|
|
|
317
318
|
let i = 0;
|
|
318
319
|
const stamp = Date.now();
|
|
319
320
|
for (const el of Array.from(root.querySelectorAll('*'))) {
|
|
320
|
-
if (el.closest('script, style
|
|
321
|
+
if (el.closest('script, style') || isInsideSvg(el)) continue;
|
|
321
322
|
if (el.tagName === 'SCRIPT' || el.tagName === 'STYLE') continue;
|
|
322
323
|
if (isNonEditableTag(el)) continue;
|
|
323
324
|
if (!el.getAttribute('data-element-id')) {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG をクリックしたときの当たり判定。
|
|
3
|
+
*
|
|
4
|
+
* 【なぜ要るか】
|
|
5
|
+
* HTML の中の `<svg>` は、描いた線や塗りが無い所も含めて**箱全体**でクリックを受ける。
|
|
6
|
+
* 装飾の SVG(斜めの線・背景の図形)が文字やボタンの上に重なっていると、その箱の中を
|
|
7
|
+
* どこを押しても SVG が選ばれ、下の要素を選べなかった。Cmd/Ctrl+クリック(最下層を選ぶ)も
|
|
8
|
+
* 押した点の最上面から探すので同じだった。
|
|
9
|
+
*
|
|
10
|
+
* 【どう直したか】
|
|
11
|
+
* Figma と同じく、見えている形(線・塗り・画像)の上を押したときだけ SVG を選び、
|
|
12
|
+
* 箱の中の何も描かれていない所は**素通り**して下の要素を選ぶ。
|
|
13
|
+
* 形の上かどうかは elementsFromPoint で見る。SVG の子(path / circle など)は
|
|
14
|
+
* 描かれている所でしか当たらない(pointer-events の既定 visiblePainted)ので、
|
|
15
|
+
* その点に子が返らなければ「箱の空き」と分かる。細い線は掴みにくいので、周り 3px も見る。
|
|
16
|
+
*
|
|
17
|
+
* SVG の中の要素(path / circle / g など)は、SVG ごと 1 つの図として扱う。
|
|
18
|
+
* 編集の印(data-editable)は付けない(isInsideSvg)。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** エディタが紙面に重ねる層(選択枠・マーキー・ガイド)。当たり判定から外す */
|
|
22
|
+
const EDITOR_LAYERS =
|
|
23
|
+
'.selection-box,.marquee-selection-box,[data-editor-overlay],#gg-smart-guides,#gg-measure-layer,.gg-collab-layer';
|
|
24
|
+
|
|
25
|
+
/** 細い線を掴めるようにする許容(px) */
|
|
26
|
+
const STROKE_TOLERANCE = 3;
|
|
27
|
+
|
|
28
|
+
/** SVG の中の要素か(`<svg>` 自身は含まない)。中身は SVG ごと 1 つの図として扱う */
|
|
29
|
+
export function isInsideSvg(el: Element): boolean {
|
|
30
|
+
const parent = el.parentElement;
|
|
31
|
+
return !!parent && typeof parent.closest === 'function' && !!parent.closest('svg');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 入れ子の SVG なら一番外の `<svg>` */
|
|
35
|
+
function outermostSvg(el: Element): Element | null {
|
|
36
|
+
let svg = typeof el.closest === 'function' ? el.closest('svg') : null;
|
|
37
|
+
while (svg) {
|
|
38
|
+
const up = svg.parentElement?.closest('svg') ?? null;
|
|
39
|
+
if (!up) return svg;
|
|
40
|
+
svg = up;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isEditorLayer(el: Element): boolean {
|
|
46
|
+
return typeof el.closest === 'function' && !!el.closest(EDITOR_LAYERS);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** SVG 自身に背景(CSS の background)が塗られていれば、箱全体が見えている */
|
|
50
|
+
function hasOwnBackground(svg: Element, doc: Document): boolean {
|
|
51
|
+
const cs = doc.defaultView?.getComputedStyle(svg);
|
|
52
|
+
if (!cs) return false;
|
|
53
|
+
const bg = cs.backgroundColor;
|
|
54
|
+
const transparent = !bg || bg === 'transparent' || /rgba\([^)]*,\s*0\)$/.test(bg);
|
|
55
|
+
return !transparent || (cs.backgroundImage && cs.backgroundImage !== 'none') || false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* SVG を包んでいるだけの透明な器か(図形ツールで描いた線・矢印・ペンは
|
|
60
|
+
* `<div style="position:absolute;background-color:transparent"><svg …></div>` の形)。
|
|
61
|
+
* 子が全部「素通りさせた要素」で、自分の文字も背景・枠線・影も無ければ、器も素通りさせる。
|
|
62
|
+
* これが無いと、SVG を素通りした先で器の箱が当たり、線の周りの空きで結局同じことが起きる
|
|
63
|
+
*/
|
|
64
|
+
function isBareWrapper(el: Element, doc: Document, skipped: Set<Element>): boolean {
|
|
65
|
+
if (el.children.length === 0) return false;
|
|
66
|
+
if (!Array.from(el.children).every((c) => skipped.has(c))) return false;
|
|
67
|
+
if (Array.from(el.childNodes).some((n) => n.nodeType === 3 && (n.textContent ?? '').trim() !== '')) return false;
|
|
68
|
+
const cs = doc.defaultView?.getComputedStyle(el);
|
|
69
|
+
if (!cs) return false;
|
|
70
|
+
const paintsBox =
|
|
71
|
+
hasOwnBackground(el, doc) ||
|
|
72
|
+
['Top', 'Right', 'Bottom', 'Left'].some(
|
|
73
|
+
(s) => parseFloat(cs.getPropertyValue(`border-${s.toLowerCase()}-width`)) > 0 && cs.getPropertyValue(`border-${s.toLowerCase()}-style`) !== 'none',
|
|
74
|
+
) ||
|
|
75
|
+
(!!cs.boxShadow && cs.boxShadow !== 'none');
|
|
76
|
+
// outline は見ない。エディタが編集できる要素すべてに透明な outline(ホバー予告用)を付けているため
|
|
77
|
+
return !paintsBox;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 図形ツールの線・矢印・ペンは、位置と大きさを持つ透明な器の中に `position:absolute` の SVG が 1 つだけ入る。
|
|
82
|
+
* 動かす・大きさを変える対象は器なので、SVG を押したら器を選ぶ(SVG を選ぶと器の中で SVG だけが動く)
|
|
83
|
+
*/
|
|
84
|
+
function drawnShapeOf(svg: Element, doc: Document): Element {
|
|
85
|
+
const parent = svg.parentElement;
|
|
86
|
+
if (!parent || parent.children.length !== 1) return svg;
|
|
87
|
+
if (parent.hasAttribute('data-gg-fx-host')) return svg;
|
|
88
|
+
if (doc.defaultView?.getComputedStyle(svg).position !== 'absolute') return svg;
|
|
89
|
+
return isBareWrapper(parent, doc, new Set([svg])) ? parent : svg;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** (x, y) の近くで、その SVG の形(子要素)が描かれているか */
|
|
93
|
+
function paintedNear(doc: Document, svg: Element, x: number, y: number): boolean {
|
|
94
|
+
const t = STROKE_TOLERANCE;
|
|
95
|
+
const offsets = [[0, 0], [t, 0], [-t, 0], [0, t], [0, -t], [t, t], [t, -t], [-t, t], [-t, -t]];
|
|
96
|
+
for (const [dx, dy] of offsets) {
|
|
97
|
+
for (const el of doc.elementsFromPoint(x + dx, y + dy)) {
|
|
98
|
+
if (el !== svg && svg.contains(el)) return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* クリック(ホバー・右クリック・ダブルクリック)の対象を、SVG の空きを素通りさせて決める。
|
|
106
|
+
* SVG と関係の無い点では target をそのまま返す(elementsFromPoint も呼ばない)。
|
|
107
|
+
*
|
|
108
|
+
* @param target 生のイベント対象(e.target、または選択枠の下から拾った要素)
|
|
109
|
+
* @param x, y 紙面(iframe)の中の座標(e.clientX / e.clientY)
|
|
110
|
+
* @returns 選択の判別に使う要素。空白(body 等)になることもある
|
|
111
|
+
*/
|
|
112
|
+
export function passThroughSvg(
|
|
113
|
+
doc: Document,
|
|
114
|
+
target: EventTarget | null,
|
|
115
|
+
x: number,
|
|
116
|
+
y: number,
|
|
117
|
+
): Element | null {
|
|
118
|
+
const el = target as Element | null;
|
|
119
|
+
if (!el || el.nodeType !== 1 || typeof el.closest !== 'function') return el;
|
|
120
|
+
if (!el.closest('svg')) return el;
|
|
121
|
+
if (typeof doc.elementsFromPoint !== 'function') return el;
|
|
122
|
+
|
|
123
|
+
const skipped = new Set<Element>();
|
|
124
|
+
for (const candidate of doc.elementsFromPoint(x, y)) {
|
|
125
|
+
if (isEditorLayer(candidate)) continue;
|
|
126
|
+
const svg = outermostSvg(candidate);
|
|
127
|
+
if (!svg) {
|
|
128
|
+
// 素通りさせた SVG を包むだけの透明な器なら、それも素通りする
|
|
129
|
+
if (skipped.size > 0 && isBareWrapper(candidate, doc, skipped)) {
|
|
130
|
+
skipped.add(candidate);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
return candidate; // SVG の外の要素 = 下にあるもの
|
|
134
|
+
}
|
|
135
|
+
if (skipped.has(svg)) continue;
|
|
136
|
+
if (candidate !== svg) return drawnShapeOf(svg, doc); // 形の上を押した = その SVG
|
|
137
|
+
if (hasOwnBackground(svg, doc) || paintedNear(doc, svg, x, y)) return drawnShapeOf(svg, doc);
|
|
138
|
+
skipped.add(svg); // 箱の空き = 素通りして下を見る
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
package/src/version.ts
CHANGED