@ash-qw/dsh-theme-prts 1.1.1 → 1.2.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 +18 -0
- package/README.en.md +10 -8
- package/README.md +10 -8
- package/lib/client.js +434 -83
- package/package.json +2 -2
- package/src/client/custom-emblem.js +390 -55
- package/src/client/emblem-reorder.js +16 -4
- package/src/client/facility-vector.js +49 -32
- package/src/client/sidebar-control-adapter.js +7 -8
- package/src/client/theme-settings-workbench.js +7 -1
- package/src/index.js +1 -1
- package/src/server/api.js +3 -3
- package/src/server/emblems.js +51 -18
- package/src/server/image-worker.js +30 -13
- package/src/server/tools.js +19 -8
- package/src/styles/cursor.css +7 -1
- package/src/styles/prts.css +1 -3
- package/src/styles/theme-settings-workbench.css +547 -643
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ash-qw/dsh-theme-prts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web",
|
|
6
|
+
"description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web and the official desktop client",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"deepseek-harness",
|
|
9
9
|
"dsh",
|
|
@@ -1,10 +1,101 @@
|
|
|
1
1
|
import { mountEmblemReorder } from './emblem-reorder.js'
|
|
2
2
|
import { PARTICLE_DENSITY_PROFILES } from './particle-field-adapter.js'
|
|
3
3
|
|
|
4
|
+
// Smooth, detect and thin luminance edges before snapping them to each density's grid.
|
|
5
|
+
// Transparent pixels are composited on white so hidden RGB cannot introduce contours.
|
|
6
|
+
export function extractEmblemLines(rgba, size, threshold, source = 'edges') {
|
|
7
|
+
const count = size * size, luminance = new Float32Array(count), smooth = new Float32Array(count)
|
|
8
|
+
for (let i = 0; i < count; i++) {
|
|
9
|
+
const j = i * 4, opacity = rgba[j + 3] / 255
|
|
10
|
+
luminance[i] = (rgba[j] * .2126 + rgba[j + 1] * .7152 + rgba[j + 2] * .0722) * opacity + 255 * (1 - opacity)
|
|
11
|
+
}
|
|
12
|
+
// Existing ink strokes must be sampled through their centers, not both borders.
|
|
13
|
+
if (source === 'ink') return Uint8Array.from(luminance, value => 255 - value >= threshold / 4 ? 1 : 0)
|
|
14
|
+
const sample = (values, x, y) => values[Math.max(0, Math.min(size - 1, y)) * size + Math.max(0, Math.min(size - 1, x))]
|
|
15
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
|
16
|
+
let sum = 0
|
|
17
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
|
18
|
+
sum += sample(luminance, x + dx, y + dy) * (dx === 0 ? 2 : 1) * (dy === 0 ? 2 : 1)
|
|
19
|
+
}
|
|
20
|
+
smooth[y * size + x] = sum / 16
|
|
21
|
+
}
|
|
22
|
+
const magnitude = new Float32Array(count), direction = new Uint8Array(count)
|
|
23
|
+
for (let y = 1; y < size - 1; y++) for (let x = 1; x < size - 1; x++) {
|
|
24
|
+
const i = y * size + x
|
|
25
|
+
const gx = -smooth[i-size-1] + smooth[i-size+1] - 2*smooth[i-1] + 2*smooth[i+1] - smooth[i+size-1] + smooth[i+size+1]
|
|
26
|
+
const gy = -smooth[i-size-1] - 2*smooth[i-size] - smooth[i-size+1] + smooth[i+size-1] + 2*smooth[i+size] + smooth[i+size+1]
|
|
27
|
+
magnitude[i] = Math.hypot(gx, gy) / 4
|
|
28
|
+
const angle = (Math.atan2(gy, gx) * 180 / Math.PI + 180) % 180
|
|
29
|
+
direction[i] = angle < 22.5 || angle >= 157.5 ? 0 : angle < 67.5 ? 1 : angle < 112.5 ? 2 : 3
|
|
30
|
+
}
|
|
31
|
+
const edges = new Uint8Array(count), weak = new Uint8Array(count), pending = []
|
|
32
|
+
const high = threshold / 4, low = high * .4, offsets = [1, size + 1, size, size - 1]
|
|
33
|
+
for (let y = 1; y < size - 1; y++) for (let x = 1; x < size - 1; x++) {
|
|
34
|
+
const i = y * size + x, strength = magnitude[i], offset = offsets[direction[i]]
|
|
35
|
+
if (strength < low || strength < magnitude[i-offset] || strength <= magnitude[i+offset]) continue
|
|
36
|
+
weak[i] = 1
|
|
37
|
+
if (strength >= high) { edges[i] = 1; pending.push(i) }
|
|
38
|
+
}
|
|
39
|
+
// Hysteresis keeps faint continuations of strong lines without retaining isolated noise.
|
|
40
|
+
while (pending.length) {
|
|
41
|
+
const i = pending.pop()
|
|
42
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
|
43
|
+
const next = i + dy * size + dx
|
|
44
|
+
if (weak[next] && !edges[next]) { edges[next] = 1; pending.push(next) }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return edges
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Zhang–Suen thinning preserves connected strokes and loops on the output grid.
|
|
51
|
+
export function simplifyEmblemLines(cells, size, cleanup = 2) {
|
|
52
|
+
const mask = new Uint8Array(size * size)
|
|
53
|
+
for (const i of cells) mask[i] = 1
|
|
54
|
+
const around = [-size, -size+1, 1, size+1, size, size-1, -1, -size-1]
|
|
55
|
+
let changed = true
|
|
56
|
+
while (changed) {
|
|
57
|
+
changed = false
|
|
58
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
59
|
+
const remove = []
|
|
60
|
+
for (let y = 1; y < size-1; y++) for (let x = 1; x < size-1; x++) {
|
|
61
|
+
const i = y*size+x
|
|
62
|
+
if (!mask[i]) continue
|
|
63
|
+
const p = around.map(offset => mask[i+offset]), count = p.reduce((a,b)=>a+b,0)
|
|
64
|
+
if (count < 2 || count > 6) continue
|
|
65
|
+
let transitions = 0
|
|
66
|
+
for (let k = 0; k < 8; k++) if (!p[k] && p[(k+1)%8]) transitions++
|
|
67
|
+
if (transitions !== 1) continue
|
|
68
|
+
if (pass === 0 ? p[0]*p[2]*p[4] || p[2]*p[4]*p[6] : p[0]*p[2]*p[6] || p[0]*p[4]*p[6]) continue
|
|
69
|
+
remove.push(i)
|
|
70
|
+
}
|
|
71
|
+
for (const i of remove) mask[i] = 0
|
|
72
|
+
if (remove.length) changed = true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (cleanup > 0) {
|
|
76
|
+
const seen = new Uint8Array(mask.length)
|
|
77
|
+
for (let i = 0; i < mask.length; i++) {
|
|
78
|
+
if (!mask[i] || seen[i]) continue
|
|
79
|
+
const component = [i]; seen[i] = 1
|
|
80
|
+
for (let j = 0; j < component.length; j++) {
|
|
81
|
+
const n = component[j], x = n % size, y = Math.floor(n / size)
|
|
82
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
|
83
|
+
if (x+dx<0 || x+dx>=size || y+dy<0 || y+dy>=size) continue
|
|
84
|
+
const next = n + dy*size+dx
|
|
85
|
+
if (mask[next] && !seen[next]) { seen[next]=1; component.push(next) }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (component.length <= cleanup) for (const n of component) mask[n] = 0
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return new Set(Array.from(mask, (value,i)=>value?i:-1).filter(i=>i>=0))
|
|
92
|
+
}
|
|
93
|
+
|
|
4
94
|
// Shared with the image worker; inject the same density profiles into its scope.
|
|
5
|
-
export function processCustomEmblemPixels(rgba, size, mode = 'auto', threshold = 128, profiles = PARTICLE_DENSITY_PROFILES) {
|
|
6
|
-
if (size
|
|
95
|
+
export function processCustomEmblemPixels(rgba, size, mode = 'auto', threshold = 128, profiles = PARTICLE_DENSITY_PROFILES, lineOptions = {}) {
|
|
96
|
+
if ((!Number.isInteger(size) || size < 320 || size > 640) || rgba.length !== size * size * 4) throw new Error('图片数据无效')
|
|
7
97
|
const alpha = new Uint8Array(size * size)
|
|
98
|
+
const lines = mode === 'lineart' ? extractEmblemLines(rgba, size, threshold, lineOptions.lineSource) : null
|
|
8
99
|
let transparent = false
|
|
9
100
|
for (let i = 3; i < rgba.length; i += 4) if (rgba[i] < 128) { transparent = true; break }
|
|
10
101
|
const method = mode === 'auto' ? (transparent ? 'alpha' : 'dark') : mode
|
|
@@ -18,10 +109,18 @@ export function processCustomEmblemPixels(rgba, size, mode = 'auto', threshold =
|
|
|
18
109
|
const targets = {}
|
|
19
110
|
for (const [density, { pitch }] of Object.entries(profiles)) {
|
|
20
111
|
const points = []
|
|
112
|
+
// Map every contour pixel to its containing lattice cell. Unlike point sampling,
|
|
113
|
+
// this retains thin strokes between lattice centers at every density and crop.
|
|
114
|
+
const gridSize = Math.ceil(size / pitch) + 1
|
|
115
|
+
let lineCells = new Set()
|
|
116
|
+
if (lines) for (let i = 0; i < lines.length; i++) if (lines[i]) {
|
|
117
|
+
lineCells.add(Math.floor(Math.floor(i / size) / pitch) * gridSize + Math.floor((i % size) / pitch))
|
|
118
|
+
}
|
|
119
|
+
if (lines) lineCells = simplifyEmblemLines(lineCells, gridSize, lineOptions.cleanup ?? 2)
|
|
21
120
|
for (let y = pitch / 2, row = 0; y < size; y += pitch, row++) {
|
|
22
121
|
for (let x = pitch / 2, column = 0; x < size; x += pitch, column++) {
|
|
23
|
-
if (!at(x, y)) continue
|
|
24
|
-
const edge = !at(x - pitch, y) || !at(x + pitch, y) || !at(x, y - pitch) || !at(x, y + pitch)
|
|
122
|
+
if (lines ? !lineCells.has(row * gridSize + column) : !at(x, y)) continue
|
|
123
|
+
const edge = Boolean(lines) || !at(x - pitch, y) || !at(x + pitch, y) || !at(x, y - pitch) || !at(x, y + pitch)
|
|
25
124
|
points.push({ x: (x - size / 2) / size, y: (y - size / 2) / size,
|
|
26
125
|
kind: edge ? 'skeleton' : 'fill', latticeColumn: column, latticeRow: row, component: 0 })
|
|
27
126
|
}
|
|
@@ -67,6 +166,25 @@ export function createCustomEmblemStore({ window, emblems = [], preview = () =>
|
|
|
67
166
|
if (!response.ok) throw new Error(result.error || '无法连接徽记服务')
|
|
68
167
|
return result
|
|
69
168
|
}
|
|
169
|
+
// Draft IDs are immutable. Bound the cache and share in-flight reads.
|
|
170
|
+
const drafts = new Map()
|
|
171
|
+
const remember = result => {
|
|
172
|
+
if (!disposed) {
|
|
173
|
+
drafts.delete(result.draftId); drafts.set(result.draftId,Promise.resolve(result))
|
|
174
|
+
while (drafts.size > 24) drafts.delete(drafts.keys().next().value)
|
|
175
|
+
}
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
const getDraft = id => {
|
|
179
|
+
if (drafts.has(id)) return drafts.get(id)
|
|
180
|
+
const pending = request(`/draft/${encodeURIComponent(id)}`).catch(error => {
|
|
181
|
+
if (drafts.get(id) === pending) drafts.delete(id)
|
|
182
|
+
throw error
|
|
183
|
+
})
|
|
184
|
+
drafts.set(id,pending)
|
|
185
|
+
while (drafts.size > 24) drafts.delete(drafts.keys().next().value)
|
|
186
|
+
return pending
|
|
187
|
+
}
|
|
70
188
|
const accept = state => {
|
|
71
189
|
if (disposed || state.revision <= revision) return
|
|
72
190
|
if (!validEmblemConfiguration(state.configuration,emblems)) throw new Error('徽记配置无效')
|
|
@@ -108,8 +226,8 @@ export function createCustomEmblemStore({ window, emblems = [], preview = () =>
|
|
|
108
226
|
catch(error) { await refresh().catch(()=>{}); throw error }
|
|
109
227
|
},
|
|
110
228
|
exportAsset: id => request(`/draft/${encodeURIComponent(id)}/export`),
|
|
111
|
-
importAsset: (asset,signal) => request('/import',asset,signal),
|
|
112
|
-
getDraft
|
|
229
|
+
importAsset: (asset,signal) => request('/import',asset,signal).then(remember),
|
|
230
|
+
getDraft,
|
|
113
231
|
async process({file,draftId,params},signal) {
|
|
114
232
|
let image
|
|
115
233
|
if (file) image = await new Promise((resolve,reject)=>{
|
|
@@ -117,9 +235,9 @@ export function createCustomEmblemStore({ window, emblems = [], preview = () =>
|
|
|
117
235
|
reader.onload=()=>resolve(String(reader.result).split(',')[1]);reader.onerror=()=>reject(new Error('图片读取失败'))
|
|
118
236
|
reader.readAsDataURL(file)
|
|
119
237
|
})
|
|
120
|
-
return request('/preview',{image,draftId,params,label:file?.name},signal)
|
|
238
|
+
return request('/preview',{image,draftId,params,label:file?.name},signal).then(remember)
|
|
121
239
|
},
|
|
122
|
-
dispose() { disposed=true; controller.abort(); window.clearTimeout(reconnectTimer);events?.close();subscribers.clear();window.removeEventListener('focus',focus) },
|
|
240
|
+
dispose() { disposed=true; drafts.clear(); controller.abort(); window.clearTimeout(reconnectTimer);events?.close();subscribers.clear();window.removeEventListener('focus',focus) },
|
|
123
241
|
}
|
|
124
242
|
}
|
|
125
243
|
|
|
@@ -131,22 +249,49 @@ export function customEmblemMarkup() {
|
|
|
131
249
|
<ol data-prts-emblem-list></ol>
|
|
132
250
|
<div data-prts-emblem-actions><button type="button" data-prts-emblem-add>新增徽记</button><button type="button" data-prts-emblem-reset-list>恢复内置列表</button></div>
|
|
133
251
|
</div>
|
|
134
|
-
<
|
|
252
|
+
<dialog data-prts-emblem-reset-confirm hidden aria-label="确认恢复内置徽记">
|
|
253
|
+
<strong data-prts-emblem-reset-title></strong>
|
|
254
|
+
<p>是否先导出自定义徽记?恢复内置后,以下徽记将从此处移除;未导出的素材将无法从设置中找回。请确认下载文件已保存,再恢复内置。</p>
|
|
255
|
+
<div data-prts-emblem-reset-exports></div>
|
|
256
|
+
<p data-prts-emblem-reset-status role="status"></p>
|
|
257
|
+
<div data-prts-emblem-actions>
|
|
258
|
+
<button type="button" data-prts-emblem-reset-cancel autofocus>取消</button>
|
|
259
|
+
<button type="button" data-prts-emblem-reset-proceed>确认恢复内置</button>
|
|
260
|
+
</div>
|
|
261
|
+
</dialog>
|
|
262
|
+
<dialog data-prts-emblem-discard hidden aria-label="放弃未应用的修改">
|
|
263
|
+
<strong>放弃未应用的修改?</strong><p>当前预览和参数尚未应用,关闭后需要重新编辑。</p>
|
|
264
|
+
<div data-prts-emblem-actions><button type="button" data-prts-emblem-keep autofocus>继续编辑</button><button type="button" data-prts-emblem-discard-apply>放弃修改</button></div>
|
|
265
|
+
</dialog>
|
|
266
|
+
<dialog data-prts-emblem-workspace hidden aria-label="编辑粒子徽记">
|
|
267
|
+
<div data-prts-emblem-edit-header>
|
|
135
268
|
<strong data-prts-emblem-target></strong>
|
|
136
|
-
<
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
269
|
+
<button type="button" data-prts-emblem-cancel aria-label="关闭徽记编辑">×</button></div>
|
|
270
|
+
<div data-prts-emblem-edit-body>
|
|
271
|
+
<aside data-prts-emblem-edit-preview><canvas width="280" height="280" data-prts-emblem-preview aria-label="拖动调整徽记位置,也可使用方向键微调" tabindex="0" hidden></canvas><p data-prts-emblem-empty>选择图片后在此预览粒子效果</p></aside>
|
|
272
|
+
<div data-prts-emblem-edit-controls>
|
|
273
|
+
<label>素材来源<select data-prts-emblem-source><option value="image">上传图片</option><option value="package">导入素材包</option></select></label>
|
|
274
|
+
<label data-prts-emblem-source-kind="image">选择图片<input type="file" accept="image/png,image/jpeg,image/webp" data-prts-emblem-file></label>
|
|
275
|
+
<small data-prts-emblem-source-kind="image">支持 PNG、WebP、JPEG,最大 8 MB / 1600 万像素。透明底标志和黑白剪影效果更好。</small>
|
|
276
|
+
<label data-prts-emblem-source-kind="package" hidden>选择素材包<input type="file" accept=".prts-emblem.json,application/json" data-prts-emblem-import></label>
|
|
277
|
+
<small data-prts-emblem-source-kind="package" hidden>导入 .prts-emblem.json 可恢复原图和调整参数,预览后再应用。</small>
|
|
140
278
|
<div data-prts-emblem-editor hidden>
|
|
141
|
-
<label>提取方式<select data-prts-emblem-mode><option value="auto">自动识别</option><option value="alpha">透明轮廓</option><option value="dark">保留暗部</option><option value="light">保留亮部</option></select></label>
|
|
279
|
+
<label>提取方式<select data-prts-emblem-mode><option value="auto">自动识别</option><option value="alpha">透明轮廓</option><option value="dark">保留暗部</option><option value="light">保留亮部</option><option value="lineart">线稿轮廓</option></select></label>
|
|
280
|
+
<small>线稿模式提取轮廓;阈值越高,保留的细节越少。彩色原图建议先由 AI 简化背景与纹理。</small>
|
|
281
|
+
<div data-prts-emblem-line-options hidden>
|
|
282
|
+
<label>线稿来源<select data-prts-emblem-lineSource><option value="edges">彩色图提轮廓</option><option value="ink">纯线稿提笔画</option></select></label>
|
|
283
|
+
<label>去除碎线<input type="range" min="0" max="12" step="1" value="2" data-prts-emblem-cleanup></label>
|
|
284
|
+
<small>数值越大,去掉的孤立碎线越多;五官小细节丢失时可降到 0。</small>
|
|
285
|
+
</div>
|
|
142
286
|
<label>阈值<input type="range" min="1" max="254" value="128" data-prts-emblem-threshold></label>
|
|
143
287
|
<label>缩放与留白<input type="range" min="0.5" max="2" step="0.05" value="0.85" data-prts-emblem-zoom></label>
|
|
144
|
-
<
|
|
145
|
-
<
|
|
288
|
+
<input type="hidden" value="0" data-prts-emblem-x><input type="hidden" value="0" data-prts-emblem-y>
|
|
289
|
+
<div data-prts-emblem-actions><button type="button" data-prts-emblem-center>居中</button></div>
|
|
290
|
+
<small>拖动取景框内的图案调整位置;方向键微调,Shift 加快。松手后生成准确预览。</small>
|
|
146
291
|
</div>
|
|
147
|
-
|
|
148
|
-
<div data-prts-emblem-actions><button type="button" data-prts-emblem-
|
|
149
|
-
</
|
|
292
|
+
</div></div>
|
|
293
|
+
<footer data-prts-emblem-edit-bar><div data-prts-emblem-edit-status></div><div data-prts-emblem-actions><button type="button" data-prts-emblem-cancel>取消编辑</button><button type="button" data-prts-emblem-apply disabled>应用到此处</button></div></footer>
|
|
294
|
+
</dialog>
|
|
150
295
|
<p role="status" aria-live="polite" data-prts-emblem-status>正在读取徽记设置…</p>
|
|
151
296
|
</section>`
|
|
152
297
|
}
|
|
@@ -157,47 +302,71 @@ export function mountCustomEmblemEditor({ panel, window, store }) {
|
|
|
157
302
|
const get = suffix => root.querySelector(`[data-prts-emblem-${suffix}]`)
|
|
158
303
|
const fileInput = get('file'), canvas = get('preview'), status = get('status')
|
|
159
304
|
let file, draft, processingController, sourceDraftId, timer, generation = 0, disposed = false, saving = false
|
|
160
|
-
let destination = null, previewValue = null, loaded = false, reorder
|
|
161
|
-
|
|
305
|
+
let destination = null, previewValue = null, loaded = false, reorder, pendingReset = null, dirty = false, editTrigger = null
|
|
306
|
+
let drag = null, dragFrame = null, rendered = null, processing = false, pendingPosition = false
|
|
307
|
+
const draw = (value, offset) => {
|
|
308
|
+
if (!offset) rendered = value ? {value,x:Number(get('x').value),y:Number(get('y').value)} : null
|
|
162
309
|
canvas.hidden = !value
|
|
310
|
+
get('empty').hidden = Boolean(value)
|
|
163
311
|
const ctx = canvas.getContext('2d')
|
|
164
312
|
if (!ctx) return
|
|
165
313
|
ctx.clearRect(0, 0, 280, 280)
|
|
166
314
|
ctx.fillStyle = '#0d1114'; ctx.fillRect(0, 0, 280, 280)
|
|
315
|
+
canvas.setAttribute('aria-disabled', String(!file && !sourceDraftId))
|
|
316
|
+
if(!drag)canvas.style.cursor=file || sourceDraftId?'grab':'default'
|
|
317
|
+
ctx.save();ctx.beginPath();ctx.rect(15,15,250,250);ctx.clip()
|
|
167
318
|
ctx.fillStyle = '#edf1f2'
|
|
168
|
-
for (const point of value?.targets.standard ?? []) {
|
|
169
|
-
|
|
170
|
-
if (pointer) {
|
|
171
|
-
const dx = x - pointer.x, dy = y - pointer.y, distance = Math.hypot(dx, dy)
|
|
172
|
-
if (distance > 0 && distance < 48) { const shift = (48 - distance) * .35; x += dx / distance * shift; y += dy / distance * shift }
|
|
173
|
-
}
|
|
319
|
+
for (const point of value?.previewPoints ?? value?.targets.standard ?? []) {
|
|
320
|
+
const x = 140 + (point.x + (offset?.x || 0)) * 250, y = 140 + (point.y + (offset?.y || 0)) * 250
|
|
174
321
|
ctx.fillRect(x, y, point.kind === 'skeleton' ? 2 : 1.5, point.kind === 'skeleton' ? 2 : 1.5)
|
|
175
322
|
}
|
|
323
|
+
ctx.restore();ctx.strokeStyle='#68747a';ctx.strokeRect(15.5,15.5,249,249)
|
|
176
324
|
}
|
|
177
|
-
|
|
325
|
+
|
|
326
|
+
let rerunRequested = false, lastEditParams = ''
|
|
327
|
+
const readParams = () => ({mode:get('mode').value,lineSource:get('lineSource').value,cleanup:Number(get('cleanup').value),threshold:Number(get('threshold').value),zoom:Number(get('zoom').value),x:Number(get('x').value),y:Number(get('y').value)})
|
|
328
|
+
const stop = () => { processingController?.abort(); processingController = undefined; processing = false; rerunRequested = false }
|
|
329
|
+
|
|
178
330
|
const process = async () => {
|
|
331
|
+
window.clearTimeout(timer);pendingPosition=false
|
|
332
|
+
if (processing) { rerunRequested = true; return }
|
|
333
|
+
get('line-options').hidden = get('mode').value !== 'lineart'
|
|
179
334
|
if ((!file && !sourceDraftId) || disposed) return
|
|
180
335
|
const token = ++generation
|
|
181
336
|
stop(); draft = null; get('apply').disabled = true
|
|
337
|
+
processing = true
|
|
182
338
|
status.textContent = '正在处理图片…'
|
|
183
|
-
processingController = new window.AbortController()
|
|
339
|
+
const controller = processingController = new window.AbortController()
|
|
340
|
+
const params = readParams(), sourceFile = file
|
|
341
|
+
lastEditParams = JSON.stringify(params)
|
|
184
342
|
try {
|
|
185
|
-
const result = await store.process({file,draftId:
|
|
343
|
+
const result = await store.process({file:sourceFile,draftId:sourceFile?undefined:sourceDraftId,params},controller.signal)
|
|
344
|
+
if (!disposed && processingController === controller && !controller.signal.aborted) {
|
|
345
|
+
// Reuse the uploaded source even when a newer parameter value is waiting.
|
|
346
|
+
sourceDraftId = result.draftId; file = null
|
|
347
|
+
}
|
|
186
348
|
if (disposed || token !== generation) return
|
|
187
|
-
draft = {...result.value,draftId:result.draftId,label:result.label}
|
|
349
|
+
draft = {...result.value,previewPoints:result.previewPoints,draftId:result.draftId,label:result.label}
|
|
188
350
|
draw(draft); get('apply').disabled = saving
|
|
189
|
-
status.textContent = `标准密度预览:${draft.targets.standard.length} 个粒子 ·
|
|
351
|
+
status.textContent = `标准密度预览:${draft.targets.standard.length} 个粒子 · 尚未应用,可拖动取景框调整位置`
|
|
190
352
|
} catch(error) {
|
|
191
353
|
if (disposed || token !== generation) return
|
|
192
354
|
status.textContent = error.message || '图片处理失败';draw(null)
|
|
355
|
+
} finally {
|
|
356
|
+
if (processingController === controller) {
|
|
357
|
+
processing = false; processingController = undefined
|
|
358
|
+
if (rerunRequested && !disposed) { rerunRequested = false; void process() }
|
|
359
|
+
}
|
|
193
360
|
}
|
|
194
361
|
}
|
|
195
362
|
const importAsset = async input => {
|
|
196
363
|
const selected=input.files?.[0]
|
|
197
364
|
if (!selected) return
|
|
198
365
|
input.value=''
|
|
366
|
+
dirty = true
|
|
199
367
|
const token=++generation
|
|
200
368
|
stop();window.clearTimeout(timer);draft=null;get('apply').disabled=true
|
|
369
|
+
processing=true
|
|
201
370
|
status.textContent='正在导入徽记素材…'
|
|
202
371
|
processingController=new window.AbortController()
|
|
203
372
|
const signal=processingController.signal
|
|
@@ -209,14 +378,21 @@ export function mountCustomEmblemEditor({ panel, window, store }) {
|
|
|
209
378
|
const result=await store.importAsset(asset,signal)
|
|
210
379
|
if(disposed || token!==generation)return
|
|
211
380
|
file=null;fileInput.value='';sourceDraftId=result.draftId
|
|
212
|
-
draft={...result.value,draftId:result.draftId,label:result.label}
|
|
213
|
-
for(const [key,value] of Object.entries(result.params))get(key).value=String(value)
|
|
381
|
+
draft={...result.value,previewPoints:result.previewPoints,draftId:result.draftId,label:result.label}
|
|
382
|
+
for(const [key,value] of Object.entries({lineSource:'edges',cleanup:2,...result.params}))get(key).value=String(value)
|
|
383
|
+
get('line-options').hidden = get('mode').value !== 'lineart'
|
|
214
384
|
get('editor').hidden=false;draw(draft);get('apply').disabled=saving
|
|
215
385
|
status.textContent='素材已导入预览,可继续调整;点击“应用到此处”才会保存'
|
|
216
386
|
}catch(error){if(!disposed && token===generation)status.textContent=error.message}
|
|
387
|
+
finally{if(token===generation)processing=false}
|
|
388
|
+
}
|
|
389
|
+
const selectSource = value => {
|
|
390
|
+
get('source').value = value
|
|
391
|
+
for (const element of root.querySelectorAll('[data-prts-emblem-source-kind]')) element.hidden = element.dataset.prtsEmblemSourceKind !== value
|
|
217
392
|
}
|
|
218
393
|
const change = event => {
|
|
219
394
|
if (saving) return
|
|
395
|
+
if(event.target===get('source')) { selectSource(event.target.value); return }
|
|
220
396
|
if(event.target===get('import')) {
|
|
221
397
|
if(event.type==='change')void importAsset(event.target)
|
|
222
398
|
return
|
|
@@ -230,14 +406,59 @@ export function mountCustomEmblemEditor({ panel, window, store }) {
|
|
|
230
406
|
get('apply').disabled = true; get('editor').hidden = true; draw(previewValue)
|
|
231
407
|
status.textContent = '请选择不超过 8 MB 的 PNG、WebP 或 JPEG 图片'; fileInput.value = ''; return
|
|
232
408
|
}
|
|
233
|
-
file = next; get('editor').hidden = false
|
|
409
|
+
stop(); dirty = true; file = next; get('editor').hidden = false
|
|
234
410
|
process()
|
|
235
411
|
} else if (event.target.closest('[data-prts-emblem-editor]')) {
|
|
236
|
-
|
|
237
|
-
|
|
412
|
+
const signature = JSON.stringify(readParams())
|
|
413
|
+
if (signature === lastEditParams) return
|
|
414
|
+
lastEditParams = signature
|
|
415
|
+
dirty = true
|
|
416
|
+
window.clearTimeout(timer); ++generation; draft = null; get('apply').disabled = true
|
|
417
|
+
pendingPosition=true;timer = window.setTimeout(process, 140)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const closeReset = (focus = false) => {
|
|
421
|
+
const trigger = pendingReset?.trigger
|
|
422
|
+
pendingReset = null; get('reset-confirm').close?.(); get('reset-confirm').hidden = true
|
|
423
|
+
get('reset-exports').replaceChildren()
|
|
424
|
+
if (focus && trigger?.isConnected) trigger.focus({preventScroll:true})
|
|
425
|
+
}
|
|
426
|
+
const requestReset = async (scope, trigger) => {
|
|
427
|
+
const state = store.get()
|
|
428
|
+
const entries = (scope === 'hero' ? [state.hero] : state.conversation).filter(e=>e?.kind==='custom')
|
|
429
|
+
if (!entries.length) {
|
|
430
|
+
await persist(scope==='hero'?{...state,hero:null}:{...state,conversation:store.defaults().conversation})
|
|
431
|
+
return
|
|
238
432
|
}
|
|
433
|
+
closeReset()
|
|
434
|
+
pendingReset = {scope,trigger,entries}
|
|
435
|
+
get('reset-title').textContent = scope==='hero' ? '恢复新会话内置徽记' : '恢复正文内置徽记列表'
|
|
436
|
+
for (const [index,entry] of entries.entries()) {
|
|
437
|
+
const row = window.document.createElement('div')
|
|
438
|
+
row.setAttribute('data-prts-emblem-reset-asset','')
|
|
439
|
+
const label = window.document.createElement('span'); label.textContent = entry.label
|
|
440
|
+
const exportButton = button('导出素材','export-reset',index)
|
|
441
|
+
const icon = window.document.createElement('span'); icon.textContent = '↓'; icon.setAttribute('aria-hidden','true')
|
|
442
|
+
exportButton.prepend(icon)
|
|
443
|
+
row.append(thumbnail(entry),label,exportButton)
|
|
444
|
+
get('reset-exports').append(row)
|
|
445
|
+
}
|
|
446
|
+
get('reset-confirm').hidden = false
|
|
447
|
+
get('reset-status').textContent = ''
|
|
448
|
+
get('reset-confirm').showModal()
|
|
449
|
+
get('reset-cancel').focus({preventScroll:true})
|
|
450
|
+
}
|
|
451
|
+
const closeDiscard = () => { get('discard').close?.(); get('discard').hidden = true }
|
|
452
|
+
const requestClose = () => {
|
|
453
|
+
if (saving) return
|
|
454
|
+
if (!dirty) { cancel(); return }
|
|
455
|
+
get('discard').hidden = false; get('discard').showModal()
|
|
239
456
|
}
|
|
240
457
|
const cancel = () => {
|
|
458
|
+
endDrag(false); processing=false;pendingPosition=false;lastEditParams=''
|
|
459
|
+
closeReset(); closeDiscard(); dirty = false
|
|
460
|
+
get('workspace').close?.(); root.append(status)
|
|
461
|
+
editTrigger?.isConnected && editTrigger.focus({preventScroll:true})
|
|
241
462
|
++generation; stop(); window.clearTimeout(timer)
|
|
242
463
|
file = null; draft = null; sourceDraftId = null; previewValue = null; destination = null; fileInput.value = ''
|
|
243
464
|
get('import').value = ''
|
|
@@ -285,33 +506,65 @@ export function mountCustomEmblemEditor({ panel, window, store }) {
|
|
|
285
506
|
get('add').disabled = saving || !loaded || state.conversation.length >= 12
|
|
286
507
|
get('reset-list').disabled = saving || !loaded
|
|
287
508
|
}
|
|
509
|
+
let prefetchEnabled = false
|
|
510
|
+
const prefetch = async () => {
|
|
511
|
+
prefetchEnabled = true
|
|
512
|
+
await store.ready
|
|
513
|
+
if (disposed) return
|
|
514
|
+
const state = store.get()
|
|
515
|
+
const ids = new Set([state.hero,...state.conversation].map(e=>e?.draftId).filter(Boolean))
|
|
516
|
+
// Keep background reads sequential; opening an entry shares its pending request.
|
|
517
|
+
for (const id of ids) {
|
|
518
|
+
if (disposed) return
|
|
519
|
+
await store.getDraft(id).catch(()=>{})
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const setLoading = loading => {
|
|
523
|
+
get('edit-controls').style.visibility = loading ? 'hidden' : ''
|
|
524
|
+
get('edit-controls').inert = loading
|
|
525
|
+
get('workspace').setAttribute('aria-busy',String(loading))
|
|
526
|
+
}
|
|
288
527
|
const startEdit = async target => {
|
|
289
|
-
|
|
528
|
+
const trigger = window.document.activeElement
|
|
529
|
+
cancel(); editTrigger = trigger; destination = target
|
|
290
530
|
const state = store.get()
|
|
291
531
|
const entry = target === 'hero' ? state.hero : target === 'new' ? null : state.conversation[target]
|
|
292
532
|
previewValue = target === 'new' ? null : entryPreview(entry)
|
|
293
533
|
get('target').textContent = target === 'hero' ? '编辑新会话徽记' : target === 'new' ? '新增正文轮换徽记' : `替换正文第 ${target+1} 张徽记`
|
|
534
|
+
selectSource('image')
|
|
294
535
|
get('workspace').hidden = false
|
|
295
|
-
|
|
536
|
+
get('edit-body').style.height = entry?.draftId ? '60dvh' : ''
|
|
537
|
+
setLoading(Boolean(entry?.draftId))
|
|
538
|
+
for (const [key,value] of Object.entries({mode:'auto',lineSource:'edges',cleanup:'2',threshold:'128',zoom:'0.85',x:'0',y:'0'})) get(key).value = value
|
|
539
|
+
get('line-options').hidden = true
|
|
296
540
|
draw(previewValue)
|
|
297
541
|
status.textContent = '上传图片或导入徽记素材包后可调整并预览,点击“应用到此处”才会保存'
|
|
298
|
-
|
|
542
|
+
if (entry?.draftId) status.textContent = '正在读取徽记设置…'
|
|
543
|
+
get('edit-status').append(status)
|
|
544
|
+
get('workspace').showModal()
|
|
299
545
|
if (entry?.draftId) {
|
|
300
546
|
const token = generation
|
|
301
547
|
try {
|
|
302
548
|
const saved = await store.getDraft(entry.draftId)
|
|
303
549
|
if (disposed || token !== generation) return
|
|
304
550
|
sourceDraftId = saved.draftId
|
|
305
|
-
for (const [key,value] of Object.entries(saved.params)) get(key).value = String(value)
|
|
551
|
+
for (const [key,value] of Object.entries({lineSource:'edges',cleanup:2,...saved.params})) get(key).value = String(value)
|
|
552
|
+
get('line-options').hidden = get('mode').value !== 'lineart'
|
|
306
553
|
get('editor').hidden = false
|
|
554
|
+
draw({...saved.value,previewPoints:saved.previewPoints})
|
|
307
555
|
status.textContent = '可继续调整原图参数,或上传替换图片;应用后才会保存'
|
|
308
|
-
|
|
556
|
+
setLoading(false)
|
|
557
|
+
if (!saved.previewPoints) await process()
|
|
558
|
+
} catch(error) { if (!disposed && token === generation) status.textContent = `${error.message};可关闭后重试,或选择替换素材` }
|
|
559
|
+
finally { if (!disposed && token === generation) setLoading(false) }
|
|
309
560
|
}
|
|
310
561
|
|
|
311
562
|
}
|
|
312
563
|
const unsubscribe = store.subscribe(() => {
|
|
564
|
+
closeReset()
|
|
313
565
|
if (destination !== null && !saving) { cancel();status.textContent='徽记设置已在其他页面或会话更新,请重新选择要编辑的徽记' }
|
|
314
566
|
renderList()
|
|
567
|
+
if (prefetchEnabled) void prefetch()
|
|
315
568
|
})
|
|
316
569
|
store.ready.then(() => { if (!disposed) { loaded = true; renderList(); status.textContent = '新会话与正文徽记独立保存;正文至少保留一张' } })
|
|
317
570
|
const persist = async value => {
|
|
@@ -333,10 +586,23 @@ export function mountCustomEmblemEditor({ panel, window, store }) {
|
|
|
333
586
|
const click = async event => {
|
|
334
587
|
const target = event.target.closest('button')
|
|
335
588
|
if (!target || target.disabled || saving || !loaded || reorder?.active) return
|
|
336
|
-
if (target.hasAttribute('data-prts-emblem-
|
|
589
|
+
if (target.hasAttribute('data-prts-emblem-center')) {
|
|
590
|
+
if ((!file && !sourceDraftId) || processing) return
|
|
591
|
+
get('x').value='0';get('y').value='0';dirty=true;await process();return
|
|
592
|
+
}
|
|
593
|
+
if (target.hasAttribute('data-prts-emblem-keep')) { closeDiscard(); return }
|
|
594
|
+
if (target.hasAttribute('data-prts-emblem-discard-apply')) { cancel(); status.textContent='已放弃未应用的修改'; return }
|
|
595
|
+
if (target.hasAttribute('data-prts-emblem-reset-cancel')) { closeReset(true); return }
|
|
596
|
+
if (target.hasAttribute('data-prts-emblem-reset-proceed')) {
|
|
597
|
+
if (!pendingReset) return
|
|
598
|
+
const {scope} = pendingReset, state = store.get()
|
|
599
|
+
await persist(scope==='hero'?{...state,hero:null}:{...state,conversation:store.defaults().conversation})
|
|
600
|
+
return
|
|
601
|
+
}
|
|
602
|
+
if (target.hasAttribute('data-prts-emblem-cancel')) { requestClose(); return }
|
|
337
603
|
if (target.hasAttribute('data-prts-emblem-add')) { startEdit('new'); return }
|
|
338
604
|
const state = store.get()
|
|
339
|
-
if (target.hasAttribute('data-prts-emblem-reset-list')) { await
|
|
605
|
+
if (target.hasAttribute('data-prts-emblem-reset-list')) { await requestReset('conversation',target); return }
|
|
340
606
|
if (target.hasAttribute('data-prts-emblem-apply')) {
|
|
341
607
|
if (!draft || destination === null) return
|
|
342
608
|
const value = {...draft,kind:'custom',key:window.crypto.randomUUID(),label:(file?.name || draft.label || '自定义徽记').slice(0,100)}
|
|
@@ -350,8 +616,8 @@ export function mountCustomEmblemEditor({ panel, window, store }) {
|
|
|
350
616
|
return
|
|
351
617
|
}
|
|
352
618
|
const action = target.dataset.emblemAction, index = Number(target.dataset.emblemIndex)
|
|
353
|
-
if(action==='export-hero'||action==='export') {
|
|
354
|
-
const entry=action==='export-hero'?state.hero:state.conversation[index]
|
|
619
|
+
if(action==='export-hero'||action==='export'||action==='export-reset') {
|
|
620
|
+
const entry=action==='export-reset'?pendingReset?.entries[index]:action==='export-hero'?state.hero:state.conversation[index]
|
|
355
621
|
if(!entry?.draftId)return
|
|
356
622
|
target.disabled=true
|
|
357
623
|
try {
|
|
@@ -363,12 +629,13 @@ export function mountCustomEmblemEditor({ panel, window, store }) {
|
|
|
363
629
|
link.hidden=true;root.append(link);link.click();link.remove()
|
|
364
630
|
window.setTimeout(()=>window.URL.revokeObjectURL(url),1000)
|
|
365
631
|
status.textContent='已导出徽记素材包,包含原图和调整参数,可在其他 DSH 中导入'
|
|
366
|
-
|
|
632
|
+
if(action==='export-reset' && pendingReset)get('reset-status').textContent='已发起下载,请确认素材包已保存,再恢复内置。'
|
|
633
|
+
}catch(error){if(!disposed){status.textContent=`导出失败:${error.message}`;if(action==='export-reset' && pendingReset)get('reset-status').textContent=status.textContent}}
|
|
367
634
|
finally{if(!disposed)target.disabled=saving}
|
|
368
635
|
return
|
|
369
636
|
}
|
|
370
637
|
if (action === 'hero') { startEdit('hero'); return }
|
|
371
|
-
if (action === 'reset-hero') { await
|
|
638
|
+
if (action === 'reset-hero') { await requestReset('hero',target); return }
|
|
372
639
|
if (!Number.isInteger(index) || index < 0 || index >= state.conversation.length) return
|
|
373
640
|
if (action === 'edit') { startEdit(index); return }
|
|
374
641
|
const conversation = [...state.conversation]
|
|
@@ -386,19 +653,87 @@ export function mountCustomEmblemEditor({ panel, window, store }) {
|
|
|
386
653
|
moved?.querySelector('[data-prts-emblem-reorder]')?.focus({preventScroll:true})
|
|
387
654
|
},
|
|
388
655
|
})
|
|
656
|
+
const resetKey = event => {
|
|
657
|
+
if (event.key==='Escape' && destination !== null) {
|
|
658
|
+
event.preventDefault(); event.stopPropagation()
|
|
659
|
+
if (get('discard').open) closeDiscard(); else requestClose()
|
|
660
|
+
return
|
|
661
|
+
}
|
|
662
|
+
if (event.key==='Escape' && pendingReset) { event.preventDefault(); event.stopPropagation(); if(!saving)closeReset(true) }
|
|
663
|
+
}
|
|
664
|
+
const resetCancel = event => { event.preventDefault(); event.stopPropagation(); if(!saving)closeReset(true) }
|
|
665
|
+
const editorCancel = event => {
|
|
666
|
+
if (event.target !== get('workspace') && event.target !== get('discard')) return
|
|
667
|
+
event.preventDefault(); event.stopPropagation()
|
|
668
|
+
if (event.target===get('discard')) closeDiscard(); else requestClose()
|
|
669
|
+
}
|
|
670
|
+
get('workspace').addEventListener('cancel',editorCancel)
|
|
671
|
+
get('discard').addEventListener('cancel',editorCancel)
|
|
672
|
+
get('reset-confirm').addEventListener('cancel',resetCancel)
|
|
673
|
+
root.addEventListener('keydown',resetKey)
|
|
674
|
+
const clampPosition = value => Math.max(-.5,Math.min(.5,value))
|
|
675
|
+
const renderPosition = () => {
|
|
676
|
+
if (rendered) draw(rendered.value,{x:Number(get('x').value)-rendered.x,y:Number(get('y').value)-rendered.y})
|
|
677
|
+
}
|
|
678
|
+
const beginDrag = event => {
|
|
679
|
+
if (event.button!==0 || !event.isPrimary || saving || processing || pendingPosition || (!file && !sourceDraftId) || !rendered || drag) return
|
|
680
|
+
event.preventDefault();canvas.focus({preventScroll:true})
|
|
681
|
+
window.clearTimeout(timer)
|
|
682
|
+
drag={id:event.pointerId,startX:event.clientX,startY:event.clientY,x:Number(get('x').value),y:Number(get('y').value),draft,dirty}
|
|
683
|
+
canvas.setPointerCapture(event.pointerId);canvas.style.cursor='grabbing'
|
|
684
|
+
}
|
|
389
685
|
const move = event => {
|
|
390
|
-
|
|
391
|
-
|
|
686
|
+
if (!drag || event.pointerId!==drag.id) return
|
|
687
|
+
const box=canvas.getBoundingClientRect(), scale=box.width*250/280
|
|
688
|
+
get('x').value=String(clampPosition(drag.x+(event.clientX-drag.startX)/scale))
|
|
689
|
+
get('y').value=String(clampPosition(drag.y+(event.clientY-drag.startY)/scale))
|
|
690
|
+
get('apply').disabled=true
|
|
691
|
+
if(dragFrame===null)dragFrame=window.requestAnimationFrame(()=>{dragFrame=null;renderPosition()})
|
|
692
|
+
}
|
|
693
|
+
function endDrag(commit) {
|
|
694
|
+
if(!drag)return
|
|
695
|
+
const previous=drag;drag=null
|
|
696
|
+
if(dragFrame!==null){window.cancelAnimationFrame(dragFrame);dragFrame=null}
|
|
697
|
+
canvas.style.removeProperty('cursor')
|
|
698
|
+
if(canvas.hasPointerCapture?.(previous.id))canvas.releasePointerCapture(previous.id)
|
|
699
|
+
if(!commit) {
|
|
700
|
+
get('x').value=String(previous.x);get('y').value=String(previous.y)
|
|
701
|
+
draft=previous.draft;dirty=previous.dirty;renderPosition();get('apply').disabled=saving || !draft
|
|
702
|
+
return
|
|
703
|
+
}
|
|
704
|
+
if(Number(get('x').value)===previous.x && Number(get('y').value)===previous.y){get('apply').disabled=saving || !draft;return}
|
|
705
|
+
dirty=true;renderPosition();void process()
|
|
706
|
+
}
|
|
707
|
+
const release = event => {if(drag?.id===event.pointerId){move(event);endDrag(true)}}
|
|
708
|
+
const abortDrag = event => {if(drag?.id===event.pointerId)endDrag(false)}
|
|
709
|
+
const positionKey = event => {
|
|
710
|
+
if(event.key==='Escape' && drag){event.preventDefault();event.stopPropagation();endDrag(false);return}
|
|
711
|
+
if(!['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(event.key) || saving || processing || (!file && !sourceDraftId) || !rendered || drag)return
|
|
712
|
+
event.preventDefault();event.stopPropagation()
|
|
713
|
+
const axis=['ArrowLeft','ArrowRight'].includes(event.key)?'x':'y',sign=['ArrowLeft','ArrowUp'].includes(event.key)?-1:1
|
|
714
|
+
get(axis).value=String(clampPosition(Number(get(axis).value)+sign*(event.shiftKey ? .025 : .005)))
|
|
715
|
+
dirty=true;get('apply').disabled=true;renderPosition()
|
|
716
|
+
window.clearTimeout(timer);pendingPosition=true;timer=window.setTimeout(process,140)
|
|
392
717
|
}
|
|
393
|
-
const leave = () => draw(draft || previewValue)
|
|
394
718
|
root.addEventListener('change', change)
|
|
395
719
|
root.addEventListener('input', change)
|
|
396
720
|
root.addEventListener('click', click)
|
|
397
|
-
canvas.addEventListener('
|
|
398
|
-
canvas.addEventListener('
|
|
399
|
-
|
|
721
|
+
canvas.addEventListener('pointerdown',beginDrag)
|
|
722
|
+
canvas.addEventListener('pointermove',move)
|
|
723
|
+
canvas.addEventListener('pointerup',release)
|
|
724
|
+
canvas.addEventListener('pointercancel',abortDrag)
|
|
725
|
+
canvas.addEventListener('lostpointercapture',abortDrag)
|
|
726
|
+
canvas.addEventListener('keydown',positionKey)
|
|
727
|
+
return { prefetch, close:cancel, cancelReorder: () => reorder.cancel(), dispose() {
|
|
728
|
+
cancel()
|
|
729
|
+
get('workspace').removeEventListener('cancel',editorCancel)
|
|
730
|
+
get('discard').removeEventListener('cancel',editorCancel)
|
|
731
|
+
get('reset-confirm').removeEventListener('cancel',resetCancel)
|
|
732
|
+
root.removeEventListener('keydown',resetKey); closeReset()
|
|
400
733
|
disposed = true; reorder.dispose(); ++generation; stop(); window.clearTimeout(timer); unsubscribe()
|
|
401
734
|
root.removeEventListener('change', change); root.removeEventListener('input', change); root.removeEventListener('click', click)
|
|
402
|
-
canvas.removeEventListener('
|
|
735
|
+
canvas.removeEventListener('pointerdown',beginDrag);canvas.removeEventListener('pointermove',move)
|
|
736
|
+
canvas.removeEventListener('pointerup',release);canvas.removeEventListener('pointercancel',abortDrag)
|
|
737
|
+
canvas.removeEventListener('lostpointercapture',abortDrag);canvas.removeEventListener('keydown',positionKey)
|
|
403
738
|
} }
|
|
404
739
|
}
|