@things-factory/figure-ui 10.1.270 → 10.1.272
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/client/graphql/index.ts +2 -0
- package/client/modeller/canvas.ts +4 -2
- package/client/modeller/clips.ts +74 -4
- package/client/modeller/data-interface.ts +14 -0
- package/client/modeller/editor-styles.ts +11 -0
- package/client/modeller/editor.ts +39 -11
- package/client/modeller/fasten-form.ts +73 -3
- package/client/modeller/fasten-geometry.ts +11 -1
- package/client/modeller/figure-cavity-drawing.ts +1 -3
- package/client/modeller/figure-clips.ts +101 -8
- package/client/modeller/figure-fasten-dialog.ts +41 -30
- package/client/modeller/figure-fasten-drawing.ts +5 -4
- package/client/modeller/figure-first-save.ts +4 -3
- package/client/modeller/figure-links.ts +85 -28
- package/client/modeller/figure-preview.ts +50 -4
- package/client/modeller/figure-size-editor.ts +7 -2
- package/client/modeller/first-save.ts +8 -0
- package/client/modeller/focus-on-render.ts +4 -0
- package/client/modeller/palette.ts +27 -0
- package/client/modeller/pivot.ts +76 -0
- package/client/modeller/product-session.ts +5 -1
- package/client/modeller/size-response.ts +14 -0
- package/client/modeller/stage-model.ts +50 -6
- package/client/types.ts +2 -0
- package/dist-client/graphql/index.js +2 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +1 -0
- package/dist-client/modeller/canvas.js +3 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +23 -6
- package/dist-client/modeller/clips.js +69 -4
- package/dist-client/modeller/clips.js.map +1 -1
- package/dist-client/modeller/data-interface.d.ts +5 -0
- package/dist-client/modeller/data-interface.js +15 -0
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +11 -0
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +6 -0
- package/dist-client/modeller/editor.js +35 -11
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +8 -0
- package/dist-client/modeller/fasten-form.js +68 -5
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +6 -0
- package/dist-client/modeller/fasten-geometry.js +10 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-cavity-drawing.js +1 -3
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-clips.d.ts +9 -0
- package/dist-client/modeller/figure-clips.js +104 -8
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +39 -24
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.js +5 -4
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-first-save.js +4 -3
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +9 -0
- package/dist-client/modeller/figure-links.js +89 -35
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +48 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.js +7 -3
- package/dist-client/modeller/figure-size-editor.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +5 -0
- package/dist-client/modeller/first-save.js +7 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/focus-on-render.d.ts +2 -0
- package/dist-client/modeller/focus-on-render.js +4 -0
- package/dist-client/modeller/focus-on-render.js.map +1 -0
- package/dist-client/modeller/links.d.ts +1 -1
- package/dist-client/modeller/palette.d.ts +14 -0
- package/dist-client/modeller/palette.js +23 -0
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/pivot.d.ts +12 -0
- package/dist-client/modeller/pivot.js +59 -0
- package/dist-client/modeller/pivot.js.map +1 -0
- package/dist-client/modeller/product-session.js +6 -2
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/size-response.d.ts +6 -0
- package/dist-client/modeller/size-response.js +14 -0
- package/dist-client/modeller/size-response.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +17 -3
- package/dist-client/modeller/stage-model.js +44 -6
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +2 -0
- package/dist-client/types.js.map +1 -1
- package/package.json +5 -5
- package/test/browser/editing.check.mjs +6 -5
- package/test/browser/section.check.mjs +21 -0
- package/test/clips.test.ts +49 -4
- package/test/data-motion.test.ts +8 -0
- package/test/fasten-form.test.ts +57 -0
- package/test/fasten-geometry.test.ts +25 -1
- package/test/first-part-on-plane.test.ts +52 -0
- package/test/first-save.test.ts +11 -1
- package/test/pivot.test.ts +57 -0
- package/test/save-landed.test.ts +20 -1
- package/test/size-response.test.ts +10 -2
- package/test/stage-model.test.ts +23 -1
- package/translations/en.json +9 -0
- package/translations/es.json +9 -0
- package/translations/ja.json +9 -0
- package/translations/ko.json +9 -0
- package/translations/ms.json +9 -0
- package/translations/zh.json +9 -0
- package/tutorial/humanoid/README.md +22 -0
- package/tutorial/humanoid/chapters.mjs +247 -0
- package/tutorial/humanoid/encode.mjs +26 -0
- package/tutorial/humanoid/lib.mjs +197 -0
- package/tutorial/humanoid/record.mjs +65 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The tutorial recorder's hands: it finds what a person would look for on the screen -- a button by its words or its
|
|
3
|
+
* title, a field by the words beside it -- and moves a drawn pointer there before it clicks or types, so a recording
|
|
4
|
+
* shows where the hand went. Nothing here reaches into the editor's state: every step is a click, a key or a drag.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Everything on the page, shadow roots included, as the page's own function (run in the page). */
|
|
8
|
+
export const PAGE_HELPERS = `
|
|
9
|
+
window.__tut = {
|
|
10
|
+
all() {
|
|
11
|
+
const out = []
|
|
12
|
+
const walk = root => root.querySelectorAll('*').forEach(e => { out.push(e); if (e.shadowRoot) walk(e.shadowRoot) })
|
|
13
|
+
walk(document)
|
|
14
|
+
return out
|
|
15
|
+
},
|
|
16
|
+
visible(e) {
|
|
17
|
+
const b = e.getBoundingClientRect()
|
|
18
|
+
if (b.width <= 0 || b.height <= 0) return false
|
|
19
|
+
let n = e
|
|
20
|
+
while (n) {
|
|
21
|
+
if (n instanceof Element && getComputedStyle(n).visibility === 'hidden') return false
|
|
22
|
+
n = n.parentElement || (n.getRootNode && n.getRootNode().host)
|
|
23
|
+
}
|
|
24
|
+
return b.bottom > 0 && b.right > 0 && b.top < innerHeight && b.left < innerWidth
|
|
25
|
+
},
|
|
26
|
+
words(e) { return (e.textContent || '').replace(/\\s+/g, ' ').trim() },
|
|
27
|
+
/* Elements matching a CSS selector, visible, in page order. */
|
|
28
|
+
query(selector) { return this.all().filter(e => e.matches && e.matches(selector) && this.visible(e)) },
|
|
29
|
+
/* The visible element of the given tags whose words are exactly, or end with, the given words (icons come first). */
|
|
30
|
+
byWords(tags, words, { within } = {}) {
|
|
31
|
+
const box = within ? within.getBoundingClientRect() : null
|
|
32
|
+
return this.all().filter(e => tags.includes(e.tagName.toLowerCase()) && this.visible(e)).filter(e => {
|
|
33
|
+
const w = this.words(e)
|
|
34
|
+
if (!(w === words || w.endsWith(words))) return false
|
|
35
|
+
if (!box) return true
|
|
36
|
+
const b = e.getBoundingClientRect()
|
|
37
|
+
return b.top >= box.top - 1 && b.bottom <= box.bottom + 1
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
/* The field that stands after the given words: the first input or select after a label-like element with them. */
|
|
41
|
+
fieldAfter(words, nth = 0, { after } = {}) {
|
|
42
|
+
const all = this.all()
|
|
43
|
+
const labels = all.filter(e => ['span', 'label', 'strong', 'div'].includes(e.tagName.toLowerCase()) && e.children.length <= 1 && this.words(e) === words && this.visible(e))
|
|
44
|
+
.filter(e => !after || e.getBoundingClientRect().top > after)
|
|
45
|
+
for (const label of labels) {
|
|
46
|
+
const i = all.indexOf(label)
|
|
47
|
+
const fields = all.slice(i + 1).filter(e => ['input', 'select'].includes(e.tagName.toLowerCase()) && this.visible(e))
|
|
48
|
+
if (fields[nth]) return fields[nth]
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
},
|
|
52
|
+
/* The first element matching a selector that has a size, scrolled to the middle of its pane when it stands out of view. */
|
|
53
|
+
bring(selector) {
|
|
54
|
+
const e = this.all().find(e => e.matches && e.matches(selector) && e.getBoundingClientRect().width > 0)
|
|
55
|
+
if (e && !this.visible(e)) e.scrollIntoView({ block: 'center' })
|
|
56
|
+
return e || null
|
|
57
|
+
},
|
|
58
|
+
/* A clip's section, brought into view by its name: its time and 「현재 자세 기록」 stand under it. */
|
|
59
|
+
clipTop(clip) {
|
|
60
|
+
const name = this.all().find(e => e.tagName === 'INPUT' && e.type !== 'number' && e.value === clip)
|
|
61
|
+
if (!name) return -1
|
|
62
|
+
name.scrollIntoView({ block: 'center' })
|
|
63
|
+
return name.getBoundingClientRect().top
|
|
64
|
+
},
|
|
65
|
+
/* Whether an element stands in the fastening dialog (its drawing, its chips, its buttons). */
|
|
66
|
+
inDialog(e) {
|
|
67
|
+
for (let n = e; n; n = n.parentElement || (n.getRootNode && n.getRootNode().host))
|
|
68
|
+
if (n.tagName === 'FIGURE-FASTEN-DIALOG') return true
|
|
69
|
+
return false
|
|
70
|
+
},
|
|
71
|
+
centre(e) {
|
|
72
|
+
if (!e) return null
|
|
73
|
+
e.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
74
|
+
const b = e.getBoundingClientRect()
|
|
75
|
+
return { x: b.left + b.width / 2, y: b.top + b.height / 2, w: b.width, h: b.height }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
`
|
|
79
|
+
|
|
80
|
+
/** The drawn pointer, the ring of a click and the caption under the screen: what a recording needs to be followed. */
|
|
81
|
+
export const OVERLAY = `
|
|
82
|
+
(() => {
|
|
83
|
+
if (document.getElementById('tut-overlay')) return
|
|
84
|
+
const style = document.createElement('style')
|
|
85
|
+
style.textContent = \`
|
|
86
|
+
#tut-pointer { position: fixed; z-index: 2147483647; width: 22px; height: 22px; margin: -2px 0 0 -2px; pointer-events: none;
|
|
87
|
+
background: no-repeat center/contain url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 2l7 19 2.6-7.4L20 11z' fill='%23111' stroke='%23fff' stroke-width='1.6' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
88
|
+
transition: left .35s ease, top .35s ease; left: 50%; top: 50% }
|
|
89
|
+
.tut-ring { position: fixed; z-index: 2147483646; width: 34px; height: 34px; margin: -17px 0 0 -17px; border-radius: 50%;
|
|
90
|
+
border: 3px solid #0d8f86; pointer-events: none; animation: tut-ring .6s ease-out forwards }
|
|
91
|
+
@keyframes tut-ring { from { transform: scale(.4); opacity: 1 } to { transform: scale(1.4); opacity: 0 } }
|
|
92
|
+
#tut-caption { position: fixed; z-index: 2147483645; left: 50%; bottom: 18px; transform: translateX(-50%); max-width: 70%;
|
|
93
|
+
padding: 10px 18px; border-radius: 10px; background: rgba(16, 32, 43, .88); color: #fff; font: 600 17px/1.45 -apple-system, 'Apple SD Gothic Neo', sans-serif;
|
|
94
|
+
text-align: center; pointer-events: none; box-shadow: 0 6px 20px rgba(0,0,0,.25) }
|
|
95
|
+
#tut-caption:empty { display: none }
|
|
96
|
+
#tut-caption small { display: block; font-weight: 500; font-size: 13px; opacity: .8 }
|
|
97
|
+
#tut-chapter { position: fixed; z-index: 2147483645; left: 50%; top: 40%; transform: translate(-50%, -50%); padding: 22px 34px;
|
|
98
|
+
border-radius: 14px; background: rgba(16, 32, 43, .92); color: #fff; font: 700 28px/1.4 -apple-system, 'Apple SD Gothic Neo', sans-serif;
|
|
99
|
+
text-align: center; pointer-events: none }
|
|
100
|
+
#tut-chapter small { display: block; font-size: 16px; font-weight: 500; opacity: .85; margin-top: 6px }
|
|
101
|
+
#tut-chapter:empty { display: none }
|
|
102
|
+
\`
|
|
103
|
+
document.head.append(style)
|
|
104
|
+
const pointer = Object.assign(document.createElement('div'), { id: 'tut-pointer' })
|
|
105
|
+
const caption = Object.assign(document.createElement('div'), { id: 'tut-caption' })
|
|
106
|
+
const chapter = Object.assign(document.createElement('div'), { id: 'tut-chapter' })
|
|
107
|
+
const overlay = Object.assign(document.createElement('div'), { id: 'tut-overlay' })
|
|
108
|
+
document.body.append(overlay, pointer, caption, chapter)
|
|
109
|
+
})()
|
|
110
|
+
`
|
|
111
|
+
|
|
112
|
+
const sleep = ms => new Promise(done => setTimeout(done, ms))
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The hands, bound to a page: each step finds its target the way a person would, moves the drawn pointer to it and acts
|
|
116
|
+
* with the real mouse and keyboard. `pace` stretches every pause, so the same steps record slower for a lesson.
|
|
117
|
+
*/
|
|
118
|
+
export function handsFor(page, { pace = 1 } = {}) {
|
|
119
|
+
const wait = ms => sleep(ms * pace)
|
|
120
|
+
const say = async (text, note = '') => {
|
|
121
|
+
await page.evaluate((t, n) => {
|
|
122
|
+
const c = document.getElementById('tut-caption')
|
|
123
|
+
c.textContent = t
|
|
124
|
+
if (n) c.append(Object.assign(document.createElement('small'), { textContent: n }))
|
|
125
|
+
}, text, note)
|
|
126
|
+
}
|
|
127
|
+
const title = async (text, note = '') => {
|
|
128
|
+
await page.evaluate((t, n) => {
|
|
129
|
+
const c = document.getElementById('tut-chapter')
|
|
130
|
+
c.textContent = t
|
|
131
|
+
if (n) c.append(Object.assign(document.createElement('small'), { textContent: n }))
|
|
132
|
+
}, text, note)
|
|
133
|
+
await wait(1800)
|
|
134
|
+
await page.evaluate(() => (document.getElementById('tut-chapter').textContent = ''))
|
|
135
|
+
}
|
|
136
|
+
const point = async (x, y) => {
|
|
137
|
+
await page.evaluate((px, py) => Object.assign(document.getElementById('tut-pointer').style, { left: px + 'px', top: py + 'px' }), x, y)
|
|
138
|
+
await page.mouse.move(x, y, { steps: 8 })
|
|
139
|
+
await wait(380)
|
|
140
|
+
}
|
|
141
|
+
const ring = (x, y) => page.evaluate((px, py) => {
|
|
142
|
+
const r = Object.assign(document.createElement('div'), { className: 'tut-ring' })
|
|
143
|
+
Object.assign(r.style, { left: px + 'px', top: py + 'px' })
|
|
144
|
+
document.body.append(r)
|
|
145
|
+
setTimeout(() => r.remove(), 700)
|
|
146
|
+
}, x, y)
|
|
147
|
+
/** The centre of what `find` (run in the page, given __tut) returns, or an error naming what was looked for. */
|
|
148
|
+
/* Found and standing still: a dialog that grows as it is filled moves its buttons, and a click aimed before it settled missed. */
|
|
149
|
+
const locate = async (what, find, ...args) => {
|
|
150
|
+
let last = null
|
|
151
|
+
for (let tries = 0; tries < 30; tries++) {
|
|
152
|
+
const at = await page.evaluate(new Function('args', `const t = window.__tut; return t.centre((${find})(t, ...args))`), args)
|
|
153
|
+
if (at && last && Math.abs(at.x - last.x) < 1 && Math.abs(at.y - last.y) < 1) return at
|
|
154
|
+
last = at
|
|
155
|
+
await sleep(120)
|
|
156
|
+
}
|
|
157
|
+
if (last) return last
|
|
158
|
+
throw Error(`not found on the screen: ${what}`)
|
|
159
|
+
}
|
|
160
|
+
const clickAt = async ({ x, y }, { clicks = 1 } = {}) => {
|
|
161
|
+
await point(x, y)
|
|
162
|
+
await ring(x, y)
|
|
163
|
+
await page.mouse.click(x, y, { clickCount: clicks })
|
|
164
|
+
await wait(450)
|
|
165
|
+
}
|
|
166
|
+
const click = async (what, find, ...args) => clickAt(await locate(what, find, ...args))
|
|
167
|
+
const typeInto = async (what, find, text, ...args) => typeKeyed(what, find, text, 'Enter', ...args)
|
|
168
|
+
/* In a dialog Enter applies the dialog: a field there is left by Tab. */
|
|
169
|
+
const typeInDialog = async (what, find, text, ...args) => typeKeyed(what, find, text, 'Tab', ...args)
|
|
170
|
+
const typeKeyed = async (what, find, text, key, ...args) => {
|
|
171
|
+
const at = await locate(what, find, ...args)
|
|
172
|
+
await clickAt(at, { clicks: 3 })
|
|
173
|
+
await page.keyboard.down('Meta')
|
|
174
|
+
await page.keyboard.press('KeyA')
|
|
175
|
+
await page.keyboard.up('Meta')
|
|
176
|
+
await page.keyboard.type(String(text), { delay: 60 * pace })
|
|
177
|
+
await page.keyboard.press(key)
|
|
178
|
+
await wait(350)
|
|
179
|
+
}
|
|
180
|
+
const drag = async (from, to) => {
|
|
181
|
+
await point(from.x, from.y)
|
|
182
|
+
await page.mouse.down()
|
|
183
|
+
await page.mouse.move(to.x, to.y, { steps: 18 })
|
|
184
|
+
await page.evaluate((px, py) => Object.assign(document.getElementById('tut-pointer').style, { left: px + 'px', top: py + 'px' }), to.x, to.y)
|
|
185
|
+
await page.mouse.up()
|
|
186
|
+
await wait(500)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* What a person names on the screen. */
|
|
190
|
+
const button = words => click(`button 「${words}」`, (t, w) => t.byWords(['button'], w)[0], words)
|
|
191
|
+
const buttonTitled = title => click(`button titled 「${title}」`, (t, w) => t.query(`button[title="${w}"]`)[0], title)
|
|
192
|
+
const tab = name => click(`tab 「${name}」`, (t, w) => t.query(`button[tab][aria-label="${w}"]`)[0], name)
|
|
193
|
+
const field = (label, text, nth = 0) => typeInto(`field after 「${label}」`, (t, l, n) => t.fieldAfter(l, n), text, label, nth)
|
|
194
|
+
const chip = name => click(`chip 「${name}」`, (t, w) => t.byWords(['button', 'span', 'label'], w).find(e => e.closest('[open], dialog, ox-popup, figure-fasten-dialog') || true), name)
|
|
195
|
+
|
|
196
|
+
return { page, wait, say, title, point, click, clickAt, typeInto, typeInDialog, locate, drag, button, buttonTitled, tab, field, chip }
|
|
197
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 휴머노이드 튜토리얼 녹화기: 모델러를 열고 장(章)마다 사람이 하는 그대로 따라 하며, 장마다 한 편의 영상(WebM)을 남긴다.
|
|
3
|
+
*
|
|
4
|
+
* node tutorial/humanoid/record.mjs --url http://localhost:5179/?fresh --out ./tutorial-videos [--only 01-pelvis] [--no-video] [--pace 1.2]
|
|
5
|
+
*
|
|
6
|
+
* --url 모델러 페이지. 빈 피규어로 열리는 곳이어야 한다(로컬 개발 페이지는 ?fresh).
|
|
7
|
+
* --out 영상과 장마다 마지막 화면(PNG)을 둘 폴더.
|
|
8
|
+
* --only 한 장만(앞 장들은 영상 없이 먼저 따라 한다).
|
|
9
|
+
* --pace 쉬는 시간 배율 -- 강의용은 1.2~1.5.
|
|
10
|
+
*
|
|
11
|
+
* ffmpeg 이 PATH 에 있어야 영상이 남는다(puppeteer screencast).
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync } from 'node:fs'
|
|
14
|
+
import { join, resolve } from 'node:path'
|
|
15
|
+
import puppeteer from 'puppeteer'
|
|
16
|
+
import { handsFor, OVERLAY, PAGE_HELPERS } from './lib.mjs'
|
|
17
|
+
import { chapters } from './chapters.mjs'
|
|
18
|
+
|
|
19
|
+
const arg = (name, fallback) => {
|
|
20
|
+
const i = process.argv.indexOf(`--${name}`)
|
|
21
|
+
return i < 0 ? fallback : (process.argv[i + 1] ?? true)
|
|
22
|
+
}
|
|
23
|
+
const url = arg('url', 'http://localhost:5179/?fresh')
|
|
24
|
+
const out = resolve(arg('out', './tutorial-videos'))
|
|
25
|
+
const only = arg('only', '')
|
|
26
|
+
const video = !process.argv.includes('--no-video')
|
|
27
|
+
const pace = Number(arg('pace', 1))
|
|
28
|
+
const headed = process.argv.includes('--headed')
|
|
29
|
+
mkdirSync(out, { recursive: true })
|
|
30
|
+
|
|
31
|
+
const browser = await puppeteer.launch({ headless: !headed, args: ['--no-sandbox', '--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--window-size=1600,900'] })
|
|
32
|
+
const page = await browser.newPage()
|
|
33
|
+
await page.setViewport({ width: 1600, height: 900 })
|
|
34
|
+
const errors = []
|
|
35
|
+
page.on('pageerror', e => errors.push(String(e)))
|
|
36
|
+
await page.goto(url, { waitUntil: 'networkidle0' })
|
|
37
|
+
await new Promise(done => setTimeout(done, 2500))
|
|
38
|
+
await page.evaluate(PAGE_HELPERS)
|
|
39
|
+
await page.evaluate(OVERLAY)
|
|
40
|
+
const h = handsFor(page, { pace })
|
|
41
|
+
|
|
42
|
+
const index = only ? chapters.findIndex(c => c.id === only) : -1
|
|
43
|
+
for (const [i, chapter] of chapters.entries()) {
|
|
44
|
+
const recording = video && (!only || i === index)
|
|
45
|
+
if (only && i > index) break
|
|
46
|
+
const started = Date.now()
|
|
47
|
+
const recorder = recording ? await page.screencast({ path: join(out, `${chapter.id}.webm`) }) : null
|
|
48
|
+
if (recording) await h.title(chapter.title, chapter.note)
|
|
49
|
+
try {
|
|
50
|
+
await chapter.run(recording ? h : handsFor(page, { pace: 0.2 }))
|
|
51
|
+
} catch (e) {
|
|
52
|
+
await page.screenshot({ path: join(out, `${chapter.id}-failed.png`) })
|
|
53
|
+
await recorder?.stop()
|
|
54
|
+
console.error(`✗ ${chapter.id}: ${e.message}`)
|
|
55
|
+
await browser.close()
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
await page.evaluate(() => document.querySelector('figure-editor')?.renderRoot?.querySelector('button[data-action="fit"]'))
|
|
59
|
+
await new Promise(done => setTimeout(done, 600))
|
|
60
|
+
await page.screenshot({ path: join(out, `${chapter.id}.png`) })
|
|
61
|
+
await recorder?.stop()
|
|
62
|
+
console.log(`✓ ${chapter.id} ${((Date.now() - started) / 1000).toFixed(1)}s${recording ? ' (영상)' : ''}`)
|
|
63
|
+
}
|
|
64
|
+
if (errors.length) console.log('page errors:', errors.slice(0, 5).join('\n'))
|
|
65
|
+
await browser.close()
|