fix(renderer): keep transparent decoration layers out of the way of clicks

Two carousel templates stack a full-bleed transparent shell (washi tape,
punch holes, film sprockets) in front of their content. Every click over
the board landed on that shell instead of the text behind it.

The engine half is the jian bump: a container that paints nothing no
longer claims its own body in the hit-test. The template half marks
those shells `locked`, so the shell is unselectable as a layer while the
marks inside it stay individually selectable — dragging a piece of tape
no longer drags the whole layer, and a marquee no longer nets the board.

Both templates are regenerated output, so they also carry the row wrap
their generators gained in the same pass.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
This commit is contained in:
Fini 2026-08-09 19:53:09 +08:00
parent e6013d71da
commit 3a8c154739
5 changed files with 78 additions and 24 deletions

View file

@ -60,6 +60,7 @@
"height": 1440,
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -1844,6 +1845,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -1929,6 +1931,7 @@
"height": 1440,
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -3726,6 +3729,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -3811,6 +3815,7 @@
"height": 1440,
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -5935,6 +5940,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -6020,6 +6026,7 @@
"height": 1440,
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -7817,6 +7824,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -7875,8 +7883,8 @@
]
}
],
"x": 3600,
"y": 0
"x": 0,
"y": 1800
},
{
"type": "frame",
@ -7902,6 +7910,7 @@
"height": 1440,
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -9881,6 +9890,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -9939,8 +9949,8 @@
]
}
],
"x": 4800,
"y": 0
"x": 1200,
"y": 1800
},
{
"type": "frame",
@ -9966,6 +9976,7 @@
"height": 1440,
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -11626,6 +11637,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -11684,8 +11696,8 @@
]
}
],
"x": 6000,
"y": 0
"x": 2400,
"y": 1800
},
{
"type": "frame",
@ -11711,6 +11723,7 @@
"height": 1440,
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -13612,6 +13625,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "rectangle",
@ -13670,8 +13684,8 @@
]
}
],
"x": 7200,
"y": 0
"x": 0,
"y": 3600
}
]
}

View file

@ -59,6 +59,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "path",
@ -480,6 +481,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "ellipse",
@ -654,6 +656,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "path",
@ -1310,6 +1313,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "ellipse",
@ -1484,6 +1488,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "path",
@ -2019,6 +2024,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "ellipse",
@ -2193,6 +2199,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "path",
@ -2874,6 +2881,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "ellipse",
@ -3022,8 +3030,8 @@
]
}
],
"x": 3600,
"y": 0
"x": 0,
"y": 1800
},
{
"type": "frame",
@ -3048,6 +3056,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "path",
@ -3474,6 +3483,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "ellipse",
@ -3622,8 +3632,8 @@
]
}
],
"x": 4800,
"y": 0
"x": 1200,
"y": 1800
},
{
"type": "frame",
@ -3648,6 +3658,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "path",
@ -4258,6 +4269,7 @@
"height": "fill_container",
"layout": "none",
"fill": [],
"locked": true,
"children": [
{
"type": "ellipse",
@ -4406,8 +4418,8 @@
]
}
],
"x": 6000,
"y": 0
"x": 2400,
"y": 1800
}
]
}

View file

@ -111,6 +111,13 @@ CJK = "Noto Sans SC"
NUM = "DM Mono"
W, H, GAP = 1080, 1440, 120
# 3 板一行 —— 与 deck 体系(deckkit.BOARDS_PER_ROW)同一约定:多板模板在画布上
# 分行铺开,而不是拖成一长排。行间距比列间距多 240 不是手滑:画布在帧上方以
# **屏幕空间**固定偏移画帧名,缩到能整屏看时 120 文档像素只剩十几个屏幕像素,
# 第二行的帧名会压到上一行的板上。
BOARDS_PER_ROW = 3
ROW_GAP = GAP + 240
BORDER = 24 # 相纸白边
EDGE = 80 - BORDER # 片窗内的左右内边距,合计仍是 80
TOP, BOT = 96 - BORDER, 128 - BORDER
@ -241,7 +248,13 @@ def footer(edge_code):
def board(page, name, main, edge_code):
"""一板。外壳是相纸白,白边之内才是片基 —— 白边是本套的第一识别信号。"""
"""一板。外壳是相纸白,白边之内才是片基 —— 白边是本套的第一识别信号。
齿孔层与色偏层都上 `locked`:两者都是铺满整板的壳子,自己一寸不画(墨
在里面那 24 个孔 / 两条色带上)。齿孔层还压在正文之上,不锁的话它自身
就是一块盖住全板的透明板。locked 让壳子不可选而壳内的孔照常可选
(hit_test_walk 的 locked 判定在子节点遍历之后)。
"""
content = frame(ids, f"{name} · 内容", width="fill_container",
height="fill_container", layout="vertical",
padding=[TOP, EDGE, BOT, EDGE], gap=0,
@ -253,19 +266,20 @@ def board(page, name, main, edge_code):
fill=solid("$c-bg"), clipContent=True)
# 装饰(色偏)最后 —— jian 里 index 0 最上,写反了会盖住正文。
cast = frame(ids, f"{name} · 色偏", width="fill_container",
height="fill_container", layout="none", fill=[])
height="fill_container", layout="none", fill=[],
locked=True)
cast["children"] = colour_cast()
gate["children"] = [content, cast]
shell = frame(ids, f"{page:02d} {name}", width=W, height=H, layout="none",
fill=solid("$c-print"), padding=BORDER, clipContent=True)
holes = frame(ids, f"{name} · 齿孔", width=W, height=H, layout="none",
fill=[])
fill=[], locked=True)
holes["children"] = sprockets()
holes["x"], holes["y"] = -BORDER, -BORDER
shell["children"] = [holes, gate]
shell["x"] = (page - 1) * (W + GAP)
shell["y"] = 0
shell["x"] = ((page - 1) % BOARDS_PER_ROW) * (W + GAP)
shell["y"] = ((page - 1) // BOARDS_PER_ROW) * (H + ROW_GAP)
return shell

View file

@ -102,6 +102,13 @@ CJK = "Noto Sans SC"
NUM = "Inter"
W, H, GAP = 1080, 1440, 120
# 3 板一行 —— 与 deck 体系(deckkit.BOARDS_PER_ROW)同一约定:多板模板在画布上
# 分行铺开,而不是拖成一长排。行间距比列间距多 240 不是手滑:画布在帧上方以
# **屏幕空间**固定偏移画帧名,缩到能整屏看时 120 文档像素只剩十几个屏幕像素,
# 第二行的帧名会压到上一行的板上。
BOARDS_PER_ROW = 3
ROW_GAP = GAP + 240
EDGE = 80
RING_GUTTER = 56 # 活页孔占掉的左侧宽度
TEXT_EDGE = EDGE + RING_GUTTER
@ -265,6 +272,11 @@ def board(page, name, main, tapes):
jian 的兄弟顺序是 index 0 最上(canvas_viewport_paint_mask 反着遍历)。
胶带层用 group:group 不是图片投放目标(image_drop.rs 不匹配 Group),
所以它盖在整板上也不会截走用户拖进来的图。
两个装饰层都上 `locked`:它们是 fill_container 的满幅壳子,压在正文之
上,但自己一寸也不画(真正的墨在里面那几条胶带 / 六个孔上)。locked 让
壳子自身不可选,壳内的胶带和孔照常可选(hit_test_walk 的 locked 判定在
子节点遍历之后),于是拖动装饰不会拽走整层,框选也不会一网兜住整板。
"""
content = frame(ids, f"{name} · 内容", width="fill_container",
height="fill_container", layout="vertical",
@ -273,18 +285,20 @@ def board(page, name, main, tapes):
content["children"] = [header(page), main, footer()]
tape_layer = group(ids, f"{name} · 胶带", width="fill_container",
height="fill_container", layout="none", fill=[])
height="fill_container", layout="none", fill=[],
locked=True)
tape_layer["children"] = tapes
holes = frame(ids, f"{name} · 活页孔", width="fill_container",
height="fill_container", layout="none", fill=[])
height="fill_container", layout="none", fill=[],
locked=True)
holes["children"] = ring_holes()
shell = frame(ids, f"{page:02d} {name}", width=W, height=H, layout="none",
fill=solid("$c-bg"), clipContent=True)
shell["children"] = [tape_layer, content, holes]
shell["x"] = (page - 1) * (W + GAP)
shell["y"] = 0
shell["x"] = ((page - 1) % BOARDS_PER_ROW) * (W + GAP)
shell["y"] = ((page - 1) // BOARDS_PER_ROW) * (H + ROW_GAP)
return shell

2
vendor/jian vendored

@ -1 +1 @@
Subproject commit 453bd70b646417e58f0d5c9a51594e27d399d849
Subproject commit 18691c42169f59374d58d13065fc606fa6c39c24