hyperbook 0.94.0 → 0.95.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/dist/assets/codemirror/codemirror.bundle.js +26 -0
- package/dist/assets/directive-abc-music/client.js +42 -42
- package/dist/assets/directive-abc-music/style.css +6 -0
- package/dist/assets/directive-openscad/client.js +350 -219
- package/dist/assets/directive-openscad/style.css +59 -4
- package/dist/assets/directive-openscad/worker.js +310 -0
- package/dist/assets/directive-p5/client.js +27 -26
- package/dist/assets/directive-p5/style.css +12 -6
- package/dist/assets/directive-pyide/client.js +37 -40
- package/dist/assets/directive-pyide/style.css +12 -6
- package/dist/assets/directive-typst/client.js +20 -55
- package/dist/assets/directive-typst/style.css +12 -6
- package/dist/assets/directive-webide/client.js +45 -79
- package/dist/assets/directive-webide/style.css +12 -6
- package/dist/index.js +37 -71
- package/dist/locales/de.json +1 -0
- package/dist/locales/en.json +1 -0
- package/package.json +4 -4
- package/dist/assets/code-input/auto-close-brackets.min.js +0 -1
- package/dist/assets/code-input/code-input.min.css +0 -1
- package/dist/assets/code-input/code-input.min.js +0 -12
- package/dist/assets/code-input/indent.min.js +0 -1
|
@@ -179,11 +179,14 @@
|
|
|
179
179
|
|
|
180
180
|
.directive-openscad .parameters-body {
|
|
181
181
|
flex: 1;
|
|
182
|
+
min-height: 0;
|
|
182
183
|
overflow-y: auto;
|
|
183
184
|
padding: 12px;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
box-sizing: border-box;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.directive-openscad .parameters-body > * + * {
|
|
189
|
+
margin-top: 10px;
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
.directive-openscad .editor-container {
|
|
@@ -286,7 +289,13 @@
|
|
|
286
289
|
width: 100%;
|
|
287
290
|
border: 1px solid var(--color-spacer);
|
|
288
291
|
flex: 1;
|
|
289
|
-
margin: 0;
|
|
292
|
+
margin: 0;
|
|
293
|
+
min-height: 0;
|
|
294
|
+
overflow: hidden;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.directive-openscad .editor .cm-editor {
|
|
298
|
+
height: 100%;
|
|
290
299
|
}
|
|
291
300
|
|
|
292
301
|
/* The parameters textarea is always hidden — the form replaces it visually */
|
|
@@ -343,6 +352,52 @@
|
|
|
343
352
|
text-align: center;
|
|
344
353
|
}
|
|
345
354
|
|
|
355
|
+
.directive-openscad .param-group {
|
|
356
|
+
border: 1px solid var(--color-spacer);
|
|
357
|
+
border-radius: 6px;
|
|
358
|
+
overflow: hidden;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.directive-openscad .param-group-summary {
|
|
362
|
+
padding: 6px 12px;
|
|
363
|
+
font-weight: 600;
|
|
364
|
+
cursor: pointer;
|
|
365
|
+
background: var(--color-background, var(--color--background, #fff));
|
|
366
|
+
user-select: none;
|
|
367
|
+
list-style: none;
|
|
368
|
+
display: flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
gap: 6px;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.directive-openscad .param-group-summary::-webkit-details-marker {
|
|
374
|
+
display: none;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.directive-openscad .param-group-summary::before {
|
|
378
|
+
content: "▶";
|
|
379
|
+
font-size: 0.65em;
|
|
380
|
+
transition: transform 0.15s ease;
|
|
381
|
+
flex-shrink: 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.directive-openscad .param-group[open] .param-group-summary::before {
|
|
385
|
+
transform: rotate(90deg);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.directive-openscad .param-group-summary:hover {
|
|
389
|
+
background: var(--color-spacer);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.directive-openscad .param-group-body {
|
|
393
|
+
padding: 8px 12px;
|
|
394
|
+
border-top: 1px solid var(--color-spacer);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.directive-openscad .param-group-body > * + * {
|
|
398
|
+
margin-top: 10px;
|
|
399
|
+
}
|
|
400
|
+
|
|
346
401
|
.directive-openscad:fullscreen {
|
|
347
402
|
width: 100vw;
|
|
348
403
|
height: 100dvh !important;
|
|
@@ -204,6 +204,261 @@ const serializeInvocationResults = (result) => {
|
|
|
204
204
|
};
|
|
205
205
|
};
|
|
206
206
|
|
|
207
|
+
const getExtractedParameters = (result) => {
|
|
208
|
+
if (result?.error || result?.exitCode !== 0) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
const output = result?.outputs?.[0]?.[1];
|
|
212
|
+
if (!output) return [];
|
|
213
|
+
try {
|
|
214
|
+
const bytes = output instanceof Uint8Array ? output : new Uint8Array(output);
|
|
215
|
+
const json = new TextDecoder().decode(bytes);
|
|
216
|
+
const paramSet = JSON.parse(json);
|
|
217
|
+
if (!Array.isArray(paramSet?.parameters)) return [];
|
|
218
|
+
return paramSet.parameters.filter((p) => !p.name?.startsWith("$"));
|
|
219
|
+
} catch (_) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const escapeHtml = (value) =>
|
|
225
|
+
String(value)
|
|
226
|
+
.replace(/&/g, "&")
|
|
227
|
+
.replace(/</g, "<")
|
|
228
|
+
.replace(/>/g, ">")
|
|
229
|
+
.replace(/"/g, """)
|
|
230
|
+
.replace(/'/g, "'");
|
|
231
|
+
|
|
232
|
+
const toFiniteNumber = (value, fallback = 0) => {
|
|
233
|
+
const num = Number(value);
|
|
234
|
+
return Number.isFinite(num) ? num : fallback;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const buildParamFormUi = (codeParams, currentOverrides = {}, id = "model") => {
|
|
238
|
+
if (!Array.isArray(codeParams) || codeParams.length === 0) {
|
|
239
|
+
return { hasParams: false, html: "", values: {} };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const values = {};
|
|
243
|
+
|
|
244
|
+
const appendNumberAttrs = (min, max, step) => {
|
|
245
|
+
let attrs = "";
|
|
246
|
+
if (step != null) attrs += ` step="${escapeHtml(String(step))}"`;
|
|
247
|
+
else attrs += ' step="any"';
|
|
248
|
+
if (min != null) attrs += ` min="${escapeHtml(String(min))}"`;
|
|
249
|
+
if (max != null) attrs += ` max="${escapeHtml(String(max))}"`;
|
|
250
|
+
return attrs;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const buildParamRow = (param, index) => {
|
|
254
|
+
const name = param?.name;
|
|
255
|
+
if (!name) return null;
|
|
256
|
+
const caption = param?.caption || name;
|
|
257
|
+
const type = param?.type;
|
|
258
|
+
const initial = param?.initial;
|
|
259
|
+
const min = param?.min;
|
|
260
|
+
const max = param?.max;
|
|
261
|
+
const step = param?.step;
|
|
262
|
+
const options = Array.isArray(param?.options) ? param.options : [];
|
|
263
|
+
const current = Object.prototype.hasOwnProperty.call(currentOverrides, name)
|
|
264
|
+
? currentOverrides[name]
|
|
265
|
+
: initial;
|
|
266
|
+
|
|
267
|
+
const inputId = `openscad-param-${id}-${index}`;
|
|
268
|
+
let controlHtml = "";
|
|
269
|
+
|
|
270
|
+
if (type === "boolean") {
|
|
271
|
+
const value = Boolean(current);
|
|
272
|
+
values[name] = value;
|
|
273
|
+
controlHtml = `<input id="${escapeHtml(inputId)}" type="checkbox" data-param-name="${escapeHtml(name)}" data-param-kind="boolean"${value ? " checked" : ""}>`;
|
|
274
|
+
} else if (options.length > 0) {
|
|
275
|
+
let selectedIndex = options.findIndex((opt) => String(opt?.value) === String(current));
|
|
276
|
+
if (selectedIndex < 0) selectedIndex = 0;
|
|
277
|
+
const selectedValue = options[selectedIndex]?.value ?? null;
|
|
278
|
+
values[name] = selectedValue;
|
|
279
|
+
const optionHtml = options.map((opt, optIndex) => {
|
|
280
|
+
const optLabel = opt?.name || String(opt?.value);
|
|
281
|
+
const optValueJson = JSON.stringify(opt?.value ?? null);
|
|
282
|
+
const selected = optIndex === selectedIndex ? " selected" : "";
|
|
283
|
+
return `<option value="${escapeHtml(String(optIndex))}" data-param-option-value="${escapeHtml(optValueJson)}"${selected}>${escapeHtml(optLabel)}</option>`;
|
|
284
|
+
}).join("");
|
|
285
|
+
controlHtml = `<select id="${escapeHtml(inputId)}" data-param-name="${escapeHtml(name)}" data-param-kind="option">${optionHtml}</select>`;
|
|
286
|
+
} else if (type === "number" && Array.isArray(initial)) {
|
|
287
|
+
const source = Array.isArray(current) ? current : initial;
|
|
288
|
+
const vector = source.map((entry) => toFiniteNumber(entry));
|
|
289
|
+
values[name] = vector;
|
|
290
|
+
const vectorInputs = vector.map((entry, vectorIndex) =>
|
|
291
|
+
`<input id="${escapeHtml(`${inputId}-${vectorIndex}`)}" type="number" value="${escapeHtml(String(entry))}" data-param-name="${escapeHtml(name)}" data-param-kind="vector" data-vector-index="${vectorIndex}"${appendNumberAttrs(min, max, step)}>`
|
|
292
|
+
).join("");
|
|
293
|
+
controlHtml = `<span class="param-vector">${vectorInputs}</span>`;
|
|
294
|
+
} else if (type === "number") {
|
|
295
|
+
const value = toFiniteNumber(current, toFiniteNumber(initial));
|
|
296
|
+
values[name] = value;
|
|
297
|
+
controlHtml = `<input id="${escapeHtml(inputId)}" type="number" value="${escapeHtml(String(value))}" data-param-name="${escapeHtml(name)}" data-param-kind="number"${appendNumberAttrs(min, max, step)}>`;
|
|
298
|
+
} else {
|
|
299
|
+
const value = current == null ? "" : String(current);
|
|
300
|
+
values[name] = value;
|
|
301
|
+
controlHtml = `<input id="${escapeHtml(inputId)}" type="text" value="${escapeHtml(value)}" data-param-name="${escapeHtml(name)}" data-param-kind="string">`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return `<div class="param-row"><label for="${escapeHtml(inputId)}">${escapeHtml(caption)}</label>${controlHtml}</div>`;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Separate global/ungrouped params from named groups.
|
|
308
|
+
// Parameters with group=null/undefined/"Global" are always shown outside any accordion.
|
|
309
|
+
const globalRows = [];
|
|
310
|
+
const groupMap = new Map(); // preserves insertion order
|
|
311
|
+
|
|
312
|
+
codeParams.forEach((param, index) => {
|
|
313
|
+
const row = buildParamRow(param, index);
|
|
314
|
+
if (!row) return;
|
|
315
|
+
const group = param?.group;
|
|
316
|
+
if (!group || group === "Global") {
|
|
317
|
+
globalRows.push(row);
|
|
318
|
+
} else {
|
|
319
|
+
if (!groupMap.has(group)) groupMap.set(group, []);
|
|
320
|
+
groupMap.get(group).push(row);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const parts = [...globalRows];
|
|
325
|
+
|
|
326
|
+
for (const [groupName, groupRows] of groupMap) {
|
|
327
|
+
parts.push(
|
|
328
|
+
`<details class="param-group" open><summary class="param-group-summary">${escapeHtml(groupName)}</summary><div class="param-group-body">${groupRows.join("")}</div></details>`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const totalRows = globalRows.length + [...groupMap.values()].reduce((sum, rows) => sum + rows.length, 0);
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
hasParams: totalRows > 0,
|
|
336
|
+
html: parts.join(""),
|
|
337
|
+
values,
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const DEFAULT_FACE_COLOR_WORKER = [0xf9 / 255, 0xd7 / 255, 0x2c / 255, 1];
|
|
342
|
+
|
|
343
|
+
// Compute flat (per-triangle) normals for a non-indexed positions array.
|
|
344
|
+
// Every 9 consecutive floats represent one triangle (3 vertices × xyz).
|
|
345
|
+
const computeFlatNormals = (positions) => {
|
|
346
|
+
const normals = new Float32Array(positions.length);
|
|
347
|
+
for (let i = 0; i < positions.length; i += 9) {
|
|
348
|
+
const ax = positions[i], ay = positions[i + 1], az = positions[i + 2];
|
|
349
|
+
const bx = positions[i + 3], by = positions[i + 4], bz = positions[i + 5];
|
|
350
|
+
const cx = positions[i + 6], cy = positions[i + 7], cz = positions[i + 8];
|
|
351
|
+
const ex = bx - ax, ey = by - ay, ez = bz - az;
|
|
352
|
+
const fx = cx - ax, fy = cy - ay, fz = cz - az;
|
|
353
|
+
const nx = ey * fz - ez * fy;
|
|
354
|
+
const ny = ez * fx - ex * fz;
|
|
355
|
+
const nz = ex * fy - ey * fx;
|
|
356
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
|
|
357
|
+
const nnx = nx / len, nny = ny / len, nnz = nz / len;
|
|
358
|
+
normals[i] = nnx; normals[i + 1] = nny; normals[i + 2] = nnz;
|
|
359
|
+
normals[i + 3] = nnx; normals[i + 4] = nny; normals[i + 5] = nnz;
|
|
360
|
+
normals[i + 6] = nnx; normals[i + 7] = nny; normals[i + 8] = nnz;
|
|
361
|
+
}
|
|
362
|
+
return normals;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// Parse an OFF file into flat Float32Array colour buckets so the main thread can
|
|
366
|
+
// construct Three.js geometries without any text-parsing work.
|
|
367
|
+
// Returns { colorBuckets: [{color, positions, normals}], transfer } or null on failure.
|
|
368
|
+
const parseOffToColorBuckets = (offData) => {
|
|
369
|
+
try {
|
|
370
|
+
const text = new TextDecoder().decode(
|
|
371
|
+
offData instanceof Uint8Array ? offData : new Uint8Array(offData),
|
|
372
|
+
);
|
|
373
|
+
const lines = text
|
|
374
|
+
.split("\n")
|
|
375
|
+
.map((l) => l.trim())
|
|
376
|
+
.filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
377
|
+
if (lines.length === 0) return null;
|
|
378
|
+
|
|
379
|
+
let countsLine = "";
|
|
380
|
+
let currentLine = 0;
|
|
381
|
+
if (/^OFF(\s|$)/.test(lines[0])) {
|
|
382
|
+
countsLine = lines[0].substring(3).trim();
|
|
383
|
+
currentLine = 1;
|
|
384
|
+
}
|
|
385
|
+
// Handle standard two-line OFF header: "OFF\nV F E"
|
|
386
|
+
if (!countsLine && currentLine < lines.length) {
|
|
387
|
+
countsLine = lines[currentLine];
|
|
388
|
+
currentLine++;
|
|
389
|
+
}
|
|
390
|
+
if (!countsLine) return null;
|
|
391
|
+
|
|
392
|
+
const countParts = countsLine.split(/\s+/).map(Number);
|
|
393
|
+
const vertexCount = Number.isFinite(countParts[0]) ? Math.floor(countParts[0]) : NaN;
|
|
394
|
+
const faceCount = Number.isFinite(countParts[1]) ? Math.floor(countParts[1]) : NaN;
|
|
395
|
+
if (!Number.isFinite(vertexCount) || !Number.isFinite(faceCount) || vertexCount <= 0 || faceCount <= 0) return null;
|
|
396
|
+
if (currentLine + vertexCount + faceCount > lines.length) return null;
|
|
397
|
+
|
|
398
|
+
const vertices = new Float32Array(vertexCount * 3);
|
|
399
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
400
|
+
const parts = lines[currentLine + i].split(/\s+/).map(Number);
|
|
401
|
+
vertices[i * 3] = parts[0] || 0;
|
|
402
|
+
vertices[i * 3 + 1] = parts[1] || 0;
|
|
403
|
+
vertices[i * 3 + 2] = parts[2] || 0;
|
|
404
|
+
}
|
|
405
|
+
currentLine += vertexCount;
|
|
406
|
+
|
|
407
|
+
const colorMap = new Map();
|
|
408
|
+
const buckets = [];
|
|
409
|
+
|
|
410
|
+
for (let i = 0; i < faceCount; i++) {
|
|
411
|
+
const parts = lines[currentLine + i].split(/\s+/).map(Number);
|
|
412
|
+
const numVerts = Number.isFinite(parts[0]) ? Math.floor(parts[0]) : 0;
|
|
413
|
+
const faceVertices = parts.slice(1, numVerts + 1).map(Math.floor);
|
|
414
|
+
if (faceVertices.length < 3) continue;
|
|
415
|
+
|
|
416
|
+
let color = DEFAULT_FACE_COLOR_WORKER;
|
|
417
|
+
if (parts.length >= numVerts + 4) {
|
|
418
|
+
const raw = parts.slice(numVerts + 1, numVerts + 5).filter(Number.isFinite);
|
|
419
|
+
if (raw.length >= 3) {
|
|
420
|
+
const r = raw[0], g = raw[1], b = raw[2];
|
|
421
|
+
const a = raw.length >= 4 ? raw[3] : (Math.max(r, g, b) > 1 ? 255 : 1);
|
|
422
|
+
const div = Math.max(r, g, b, a) > 1 ? 255 : 1;
|
|
423
|
+
color = [r / div, g / div, b / div, a / div];
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const colorKey = color.join(",");
|
|
428
|
+
let bucket = colorMap.get(colorKey);
|
|
429
|
+
if (!bucket) {
|
|
430
|
+
bucket = { color, positionsList: [] };
|
|
431
|
+
colorMap.set(colorKey, bucket);
|
|
432
|
+
buckets.push(bucket);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Fan triangulation of potentially non-triangular faces
|
|
436
|
+
for (let j = 1; j < faceVertices.length - 1; j++) {
|
|
437
|
+
const i1 = faceVertices[0], i2 = faceVertices[j], i3 = faceVertices[j + 1];
|
|
438
|
+
bucket.positionsList.push(
|
|
439
|
+
vertices[i1 * 3], vertices[i1 * 3 + 1], vertices[i1 * 3 + 2],
|
|
440
|
+
vertices[i2 * 3], vertices[i2 * 3 + 1], vertices[i2 * 3 + 2],
|
|
441
|
+
vertices[i3 * 3], vertices[i3 * 3 + 1], vertices[i3 * 3 + 2],
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (buckets.length === 0) return null;
|
|
447
|
+
|
|
448
|
+
const transfer = [];
|
|
449
|
+
const colorBuckets = buckets.map((bucket) => {
|
|
450
|
+
const positions = new Float32Array(bucket.positionsList);
|
|
451
|
+
const normals = computeFlatNormals(positions);
|
|
452
|
+
transfer.push(positions.buffer, normals.buffer);
|
|
453
|
+
return { color: bucket.color, positions, normals };
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
return { colorBuckets, transfer };
|
|
457
|
+
} catch (_) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
207
462
|
const runOpenScadInvocation = async ({
|
|
208
463
|
code,
|
|
209
464
|
sourcePath,
|
|
@@ -284,6 +539,40 @@ self.addEventListener("message", async (event) => {
|
|
|
284
539
|
self.postMessage({ requestId, ok: true, result: serialized.result }, serialized.transfer);
|
|
285
540
|
return;
|
|
286
541
|
}
|
|
542
|
+
if (type === "buildParamForm") {
|
|
543
|
+
const sourcePath = "/tmp/params_model.scad";
|
|
544
|
+
const outPath = "/tmp/params_out.json";
|
|
545
|
+
result = await runOpenScadInvocation({
|
|
546
|
+
code: `$preview=true;\n${payload?.code || ""}`,
|
|
547
|
+
sourcePath,
|
|
548
|
+
outputPaths: [outPath],
|
|
549
|
+
libraryNames: payload?.libraryNames || [],
|
|
550
|
+
args: [
|
|
551
|
+
sourcePath,
|
|
552
|
+
"-o",
|
|
553
|
+
outPath,
|
|
554
|
+
"--export-format=param",
|
|
555
|
+
],
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const codeParams = getExtractedParameters(result);
|
|
559
|
+
const ui = buildParamFormUi(
|
|
560
|
+
codeParams,
|
|
561
|
+
payload?.currentOverrides || {},
|
|
562
|
+
payload?.id || "model",
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
self.postMessage({
|
|
566
|
+
requestId,
|
|
567
|
+
ok: true,
|
|
568
|
+
result: {
|
|
569
|
+
hasParams: ui.hasParams,
|
|
570
|
+
html: ui.html,
|
|
571
|
+
values: ui.values,
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
287
576
|
if (type === "render") {
|
|
288
577
|
const format = payload?.format || "stl";
|
|
289
578
|
const sourcePath = "/tmp/model.scad";
|
|
@@ -304,6 +593,27 @@ self.addEventListener("message", async (event) => {
|
|
|
304
593
|
...OPENSCAD_FEATURE_ARGS,
|
|
305
594
|
],
|
|
306
595
|
});
|
|
596
|
+
// For preview OFF renders, parse geometry here in the worker so the main thread
|
|
597
|
+
// never has to do heavy text parsing or Float32Array building.
|
|
598
|
+
if (format === "off" && payload?.isPreview && result.exitCode === 0 && !result.error) {
|
|
599
|
+
const offOutput = result.outputs?.[0]?.[1];
|
|
600
|
+
if (offOutput) {
|
|
601
|
+
const parsed = parseOffToColorBuckets(offOutput);
|
|
602
|
+
if (parsed) {
|
|
603
|
+
self.postMessage({
|
|
604
|
+
requestId,
|
|
605
|
+
ok: true,
|
|
606
|
+
result: {
|
|
607
|
+
exitCode: result.exitCode,
|
|
608
|
+
mergedOutputs: result.mergedOutputs,
|
|
609
|
+
elapsedMillis: result.elapsedMillis,
|
|
610
|
+
parsedGeometry: parsed.colorBuckets,
|
|
611
|
+
},
|
|
612
|
+
}, parsed.transfer);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
307
617
|
const serialized = serializeInvocationResults(result);
|
|
308
618
|
self.postMessage({ requestId, ok: true, result: serialized.result }, serialized.transfer);
|
|
309
619
|
return;
|
|
@@ -7,14 +7,6 @@
|
|
|
7
7
|
* @see hyperbook.store
|
|
8
8
|
*/
|
|
9
9
|
hyperbook.p5 = (function () {
|
|
10
|
-
window.codeInput?.registerTemplate(
|
|
11
|
-
"p5-highlighted",
|
|
12
|
-
codeInput.templates.prism(window.Prism, [
|
|
13
|
-
new codeInput.plugins.AutoCloseBrackets(),
|
|
14
|
-
new codeInput.plugins.Indent(true, 2),
|
|
15
|
-
]),
|
|
16
|
-
);
|
|
17
|
-
|
|
18
10
|
const wrapSketch = (sketchCode) => {
|
|
19
11
|
if (sketchCode !== "" && !sketchCode?.includes("setup")) {
|
|
20
12
|
return `
|
|
@@ -128,7 +120,7 @@ hyperbook.p5 = (function () {
|
|
|
128
120
|
const container = elem.querySelector(".container");
|
|
129
121
|
const editorContainer = elem.querySelector(".editor-container");
|
|
130
122
|
const splitter = elem.querySelector(".splitter");
|
|
131
|
-
const
|
|
123
|
+
const editorDiv = elem.getElementsByClassName("editor")[0];
|
|
132
124
|
/** @type {HTMLButtonElement} */
|
|
133
125
|
const update = elem.getElementsByClassName("update")[0];
|
|
134
126
|
const frame = elem.getElementsByTagName("iframe")[0];
|
|
@@ -139,6 +131,20 @@ hyperbook.p5 = (function () {
|
|
|
139
131
|
const downloadEl = elem.getElementsByClassName("download")[0];
|
|
140
132
|
const fullscreenEl = elem.getElementsByClassName("fullscreen")[0];
|
|
141
133
|
|
|
134
|
+
// Initialize CodeMirror editor (only in editor mode)
|
|
135
|
+
let cm = null;
|
|
136
|
+
if (editorDiv) {
|
|
137
|
+
const initialSource = editorDiv.textContent;
|
|
138
|
+
editorDiv.textContent = "";
|
|
139
|
+
cm = HyperbookCM.create(editorDiv, {
|
|
140
|
+
lang: editorDiv.dataset.lang,
|
|
141
|
+
value: initialSource,
|
|
142
|
+
onChange: (code) => {
|
|
143
|
+
if (id) hyperbook.store.db.p5.put({ id, sketch: code });
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
142
148
|
setupSplitter(elem, container, editorContainer, splitter);
|
|
143
149
|
|
|
144
150
|
fullscreenEl?.addEventListener("click", async () => {
|
|
@@ -159,7 +165,7 @@ hyperbook.p5 = (function () {
|
|
|
159
165
|
|
|
160
166
|
copyEl?.addEventListener("click", async () => {
|
|
161
167
|
try {
|
|
162
|
-
await navigator.clipboard.writeText(
|
|
168
|
+
await navigator.clipboard.writeText(cm?.getValue() ?? "");
|
|
163
169
|
} catch (error) {
|
|
164
170
|
console.error(error.message);
|
|
165
171
|
}
|
|
@@ -172,35 +178,30 @@ hyperbook.p5 = (function () {
|
|
|
172
178
|
|
|
173
179
|
downloadEl?.addEventListener("click", () => {
|
|
174
180
|
const a = document.createElement("a");
|
|
175
|
-
const blob = new Blob([
|
|
181
|
+
const blob = new Blob([cm?.getValue() ?? ""], { type: "text/plain" });
|
|
176
182
|
a.href = URL.createObjectURL(blob);
|
|
177
183
|
a.download = `sketch-${id}.js`;
|
|
178
184
|
a.click();
|
|
179
185
|
});
|
|
180
186
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const result = await hyperbook.store.db.p5.get(id);
|
|
187
|
+
if (id && cm) {
|
|
188
|
+
hyperbook.store.db.p5.get(id).then((result) => {
|
|
184
189
|
if (result) {
|
|
185
|
-
|
|
190
|
+
cm.setValue(result.sketch);
|
|
186
191
|
const code = result.sketch;
|
|
187
192
|
frame.srcdoc = template
|
|
188
193
|
.replace("###SLOT###", wrapSketch(code))
|
|
189
194
|
.replaceAll("###ORIGIN###", window.location.origin)
|
|
190
195
|
.replace(/\u00A0/g, " ");
|
|
191
196
|
}
|
|
192
|
-
|
|
193
|
-
editor.addEventListener("input", () => {
|
|
194
|
-
hyperbook.store.db.p5.put({ id, sketch: editor.value });
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
update?.addEventListener("click", () => {
|
|
199
|
-
const code = editor.value;
|
|
200
|
-
frame.srcdoc = template
|
|
201
|
-
.replace("###SLOT###", wrapSketch(code))
|
|
202
|
-
.replaceAll("###ORIGIN###", window.location.origin);
|
|
203
197
|
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
update?.addEventListener("click", () => {
|
|
201
|
+
const code = cm?.getValue() ?? "";
|
|
202
|
+
frame.srcdoc = template
|
|
203
|
+
.replace("###SLOT###", wrapSketch(code))
|
|
204
|
+
.replaceAll("###ORIGIN###", window.location.origin);
|
|
204
205
|
});
|
|
205
206
|
}
|
|
206
207
|
|
|
@@ -11,6 +11,18 @@ code-input {
|
|
|
11
11
|
margin: 0;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
.directive-p5 .editor {
|
|
15
|
+
width: 100%;
|
|
16
|
+
border: 1px solid var(--color-spacer);
|
|
17
|
+
flex: 1;
|
|
18
|
+
min-height: 0;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.directive-p5 .editor .cm-editor {
|
|
23
|
+
height: 100%;
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
.directive-p5 .container {
|
|
15
27
|
width: 100%;
|
|
16
28
|
min-height: 120px;
|
|
@@ -70,12 +82,6 @@ code-input {
|
|
|
70
82
|
opacity: 0.75;
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
.directive-p5 .editor {
|
|
74
|
-
width: 100%;
|
|
75
|
-
border: 1px solid var(--color-spacer);
|
|
76
|
-
flex: 1;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
85
|
.directive-p5 .buttons {
|
|
80
86
|
display: flex;
|
|
81
87
|
border: 1px solid var(--color-spacer);
|