@innovastudio/contentbuilder 1.5.217 → 1.5.219
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.
|
@@ -17428,8 +17428,16 @@ class CodeElement {
|
|
|
17428
17428
|
if (doc.getElementById('css-cb-code-element')) return;
|
|
17429
17429
|
const style = doc.createElement('style');
|
|
17430
17430
|
style.id = 'css-cb-code-element';
|
|
17431
|
+
/*
|
|
17432
|
+
No 'position' here. The editor must lay a block out exactly as the
|
|
17433
|
+
published page does, and a block whose content is absolutely
|
|
17434
|
+
positioned (a full-bleed scene in an overlay, say) resolves against
|
|
17435
|
+
its nearest positioned ancestor: making the block itself one put
|
|
17436
|
+
that content inside a 30px-tall box in the editor while the same
|
|
17437
|
+
markup filled the section on the page.
|
|
17438
|
+
*/
|
|
17431
17439
|
style.textContent = `
|
|
17432
|
-
[${ATTR}] { min-height: 30px;
|
|
17440
|
+
[${ATTR}] { min-height: 30px; -webkit-user-select: none; user-select: none; }
|
|
17433
17441
|
`;
|
|
17434
17442
|
doc.head.appendChild(style);
|
|
17435
17443
|
}
|
|
@@ -20970,7 +20978,7 @@ function getOffset(element) {
|
|
|
20970
20978
|
top: box.top + (window.pageYOffset - document.documentElement.clientTop)
|
|
20971
20979
|
};
|
|
20972
20980
|
}
|
|
20973
|
-
var location = WINDOW.location;
|
|
20981
|
+
var location$1 = WINDOW.location;
|
|
20974
20982
|
var REGEXP_ORIGINS = /^(\w+:)\/\/([^:/?#]*):?(\d*)/i;
|
|
20975
20983
|
/**
|
|
20976
20984
|
* Check if the given URL is a cross origin URL.
|
|
@@ -20980,7 +20988,7 @@ var REGEXP_ORIGINS = /^(\w+:)\/\/([^:/?#]*):?(\d*)/i;
|
|
|
20980
20988
|
|
|
20981
20989
|
function isCrossOriginURL(url) {
|
|
20982
20990
|
var parts = url.match(REGEXP_ORIGINS);
|
|
20983
|
-
return parts !== null && (parts[1] !== location.protocol || parts[2] !== location.hostname || parts[3] !== location.port);
|
|
20991
|
+
return parts !== null && (parts[1] !== location$1.protocol || parts[2] !== location$1.hostname || parts[3] !== location$1.port);
|
|
20984
20992
|
}
|
|
20985
20993
|
/**
|
|
20986
20994
|
* Add timestamp to the given URL.
|
|
@@ -29555,7 +29563,7 @@ class Image$1 {
|
|
|
29555
29563
|
<label class="label-checkbox">
|
|
29556
29564
|
<input class="input-newwindow" id="__input_newwindow2" type="checkbox" /> ${util.out('Open new window')}
|
|
29557
29565
|
</label>
|
|
29558
|
-
<label class="label-checkbox" id="lblImageLinkOpenLightbox" style="${this.builder.useLightbox ? '' : 'display:none'}">
|
|
29566
|
+
<label class="label-checkbox mt-1" id="lblImageLinkOpenLightbox" style="${this.builder.useLightbox ? '' : 'display:none'}">
|
|
29559
29567
|
<input class="input-openlightbox" id="__input_openlightbox2" type="checkbox" /> ${util.out('Open in a lightbox (for image, video or Youtube)')}
|
|
29560
29568
|
</label>
|
|
29561
29569
|
</div>
|
|
@@ -46265,7 +46273,16 @@ class ColorPickerKelir {
|
|
|
46265
46273
|
}
|
|
46266
46274
|
|
|
46267
46275
|
// Close the picker on scroll — it's positioned once, so scrolling detaches it.
|
|
46268
|
-
|
|
46276
|
+
//
|
|
46277
|
+
// The page moving under the picker is the only scroll that counts. A text
|
|
46278
|
+
// input scrolls its own content whenever the caret passes the edge — on
|
|
46279
|
+
// paste, on Delete, on Cmd+Left/Right — and that is a scroll event too,
|
|
46280
|
+
// which a capture listener on window receives even though it does not
|
|
46281
|
+
// bubble. Without this guard the picker closed while its own hex field
|
|
46282
|
+
// was being edited.
|
|
46283
|
+
const hideOnScroll = e => {
|
|
46284
|
+
const target = e && e.target;
|
|
46285
|
+
if (target && target.nodeType === 1 && popPicker.contains(target)) return;
|
|
46269
46286
|
this.builder.util.hidePop(popPicker);
|
|
46270
46287
|
window.removeEventListener('scroll', hideOnScroll, true);
|
|
46271
46288
|
if (this.builder.win && this.builder.win !== window) this.builder.win.removeEventListener('scroll', hideOnScroll, true);
|
|
@@ -82978,6 +82995,23 @@ ${this.buildHistoryContext()}`;
|
|
|
82978
82995
|
* IMAGE PREVIEW HELPER
|
|
82979
82996
|
* ============================================================================
|
|
82980
82997
|
*/
|
|
82998
|
+
/* Attachments shown under a user message, matching what the composer
|
|
82999
|
+
* renders when a file is attached for real. Demo playback only. */
|
|
83000
|
+
addUserAttachmentPreview(images) {
|
|
83001
|
+
const previewDiv = document.createElement('div');
|
|
83002
|
+
previewDiv.className = 'message user';
|
|
83003
|
+
let html = '<div class="message-content" style="display:flex;flex-wrap:wrap;gap:8px;padding:10px;">';
|
|
83004
|
+
images.forEach(img => {
|
|
83005
|
+
const url = this.escapeHtml(img.url || '');
|
|
83006
|
+
if (!url) return;
|
|
83007
|
+
const alt = this.escapeHtml(img.context || url.split('/').pop() || 'Attachment');
|
|
83008
|
+
html += `<img src="${url}" alt="${alt}" style="width:64px;height:64px;object-fit:cover;border-radius:6px;" />`;
|
|
83009
|
+
});
|
|
83010
|
+
html += '</div>';
|
|
83011
|
+
previewDiv.innerHTML = html;
|
|
83012
|
+
this.messagesContainer.appendChild(previewDiv);
|
|
83013
|
+
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
|
|
83014
|
+
}
|
|
82981
83015
|
addImagePreview(images) {
|
|
82982
83016
|
const out = s => this.out(s);
|
|
82983
83017
|
const previewDiv = document.createElement('div');
|
|
@@ -84528,9 +84562,12 @@ Return ONLY the edited section as a single <div class="is-section ...>. Do not o
|
|
|
84528
84562
|
announcement.setAttribute('aria-live', 'polite');
|
|
84529
84563
|
announcement.className = 'sr-only';
|
|
84530
84564
|
announcement.textContent = message;
|
|
84531
|
-
|
|
84565
|
+
|
|
84566
|
+
// Must live inside the builder container: the .sr-only rule is scoped
|
|
84567
|
+
// to [data-cb-ui], so elsewhere the text would be visible on screen.
|
|
84568
|
+
this.builderStuff.appendChild(announcement);
|
|
84532
84569
|
setTimeout(() => {
|
|
84533
|
-
|
|
84570
|
+
announcement.remove();
|
|
84534
84571
|
}, 1000);
|
|
84535
84572
|
}
|
|
84536
84573
|
|
|
@@ -84545,6 +84582,11 @@ Return ONLY the edited section as a single <div class="is-section ...>. Do not o
|
|
|
84545
84582
|
this.demoConversations.forEach(msg => {
|
|
84546
84583
|
if (msg.role === 'user') {
|
|
84547
84584
|
this.addMessage('user', msg.content);
|
|
84585
|
+
|
|
84586
|
+
// Attachments the user sent with the message
|
|
84587
|
+
if (msg.imagePreview && msg.imagePreview.length > 0) {
|
|
84588
|
+
this.addUserAttachmentPreview(msg.imagePreview);
|
|
84589
|
+
}
|
|
84548
84590
|
} else if (msg.role === 'assistant') {
|
|
84549
84591
|
this.addMessage('assistant', msg.content, true);
|
|
84550
84592
|
|
|
@@ -84885,6 +84927,45 @@ Return ONLY the edited section as a single <div class="is-section ...>. Do not o
|
|
|
84885
84927
|
}
|
|
84886
84928
|
}
|
|
84887
84929
|
|
|
84930
|
+
/**
|
|
84931
|
+
* repair-markup.js
|
|
84932
|
+
* ----------------------------------------------------------------------------
|
|
84933
|
+
* Fixes applied to AI-written HTML on its way to the page, in the targets'
|
|
84934
|
+
* normalize() step.
|
|
84935
|
+
*
|
|
84936
|
+
* Only for defects that are unambiguous — a rule the framework states, where
|
|
84937
|
+
* the result is visibly broken and there is exactly one correct repair. Taste
|
|
84938
|
+
* belongs in the context documentation, not here: this must never turn one
|
|
84939
|
+
* valid design into another.
|
|
84940
|
+
* ----------------------------------------------------------------------------
|
|
84941
|
+
*/
|
|
84942
|
+
|
|
84943
|
+
/*
|
|
84944
|
+
An outline button needs 'hover:border-transparent'.
|
|
84945
|
+
|
|
84946
|
+
'border-current' makes the 2px border follow the text colour, hover colour
|
|
84947
|
+
included, so on hover the border becomes a visible ring and the hover fill
|
|
84948
|
+
stops inside it — the button appears to shrink. The class lets the border take
|
|
84949
|
+
the hover background instead.
|
|
84950
|
+
|
|
84951
|
+
Left alone: a button that sets its own hover border (any 'hover:border-*'
|
|
84952
|
+
class, or 'border' listed in data-cb-hover), which is a deliberate choice the
|
|
84953
|
+
class would override.
|
|
84954
|
+
*/
|
|
84955
|
+
function repairButtons(root) {
|
|
84956
|
+
if (!root || !root.querySelectorAll) return 0;
|
|
84957
|
+
let fixed = 0;
|
|
84958
|
+
root.querySelectorAll('a[role="button"], button').forEach(el => {
|
|
84959
|
+
if (!el.classList.contains('border-current')) return;
|
|
84960
|
+
if ([...el.classList].some(c => c.startsWith('hover:border-'))) return;
|
|
84961
|
+
const hover = (el.getAttribute('data-cb-hover') || '').split(/\s+/);
|
|
84962
|
+
if (hover.includes('border')) return;
|
|
84963
|
+
el.classList.add('hover:border-transparent');
|
|
84964
|
+
fixed++;
|
|
84965
|
+
});
|
|
84966
|
+
return fixed;
|
|
84967
|
+
}
|
|
84968
|
+
|
|
84888
84969
|
/**
|
|
84889
84970
|
* target-section.js
|
|
84890
84971
|
* ----------------------------------------------------------------------------
|
|
@@ -84899,7 +84980,6 @@ Return ONLY the edited section as a single <div class="is-section ...>. Do not o
|
|
|
84899
84980
|
* products run the same engine instead of a copy of it.
|
|
84900
84981
|
* ----------------------------------------------------------------------------
|
|
84901
84982
|
*/
|
|
84902
|
-
|
|
84903
84983
|
function createSectionTarget(engine) {
|
|
84904
84984
|
const b = () => engine.builder;
|
|
84905
84985
|
const nodes = () => b().getSectionNodes();
|
|
@@ -84930,7 +85010,7 @@ function createSectionTarget(engine) {
|
|
|
84930
85010
|
|
|
84931
85011
|
/* Reference the host actually supplied, in the order it is offered. */
|
|
84932
85012
|
const docTopics = () => {
|
|
84933
|
-
const t = ['box', 'animation'].filter(k => engine.contextParts[k]);
|
|
85013
|
+
const t = ['box', 'animation', '3d', 'games', 'code'].filter(k => engine.contextParts[k]);
|
|
84934
85014
|
if (motionPathMode()) t.push('motionPath');
|
|
84935
85015
|
return t;
|
|
84936
85016
|
};
|
|
@@ -84948,6 +85028,7 @@ function createSectionTarget(engine) {
|
|
|
84948
85028
|
const found = holder.querySelectorAll('.is-section');
|
|
84949
85029
|
if (found.length !== 1) return null;
|
|
84950
85030
|
const section = found[0];
|
|
85031
|
+
repairButtons(section);
|
|
84951
85032
|
section.querySelectorAll('div.is-container').forEach(c => {
|
|
84952
85033
|
if (![...c.classList].some(x => /^size-\d/.test(x))) c.classList.add('size-17');
|
|
84953
85034
|
if (![...c.classList].some(x => /^leading-\d/.test(x))) c.classList.add('leading-17');
|
|
@@ -85074,7 +85155,7 @@ Sections are <div class="is-section ...">. Use ONLY documented classes — this
|
|
|
85074
85155
|
${onDemand.length ? `
|
|
85075
85156
|
REFERENCE YOU MUST LOAD WHEN RELEVANT
|
|
85076
85157
|
The documentation below covers grid, typography, spacing and colour. Further reference is available through get_framework_docs and is NOT included here:
|
|
85077
|
-
${onDemand.includes('box') ? '- "box": section and box structure — load before ADDING, REMOVING or RESTRUCTURING a section, or changing its layout, width or background.\n' : ''}${onDemand.includes('animation') ? '- "animation": the motion system — load before adding, changing or removing any animation.\n' : ''}${onDemand.includes('motionPath') ? `- "motionPath": moving an OBJECT along a route — a plane, rocket, car, dot or arrow flying, following or tracing a path, curve or journey across a scene. ${motionPathMode() === 'preserve' ? 'This effect is DISABLED for this site: load it only to preserve or adjust a path already in the markup, never to add one.' : 'Load it only for that; it is not needed for ordinary motion — entrances, fades, parallax, scroll reveals or hover effects.'} ALSO load it whenever markup you are about to edit contains a "data-fx-path" attribute, whatever the request was — those attributes are undocumented anywhere else and you will corrupt them otherwise.\n` : ''}Load what a task needs BEFORE writing markup for it. Never guess at classes from those areas.` : ''}`;
|
|
85158
|
+
${onDemand.includes('box') ? '- "box": section and box structure — load before ADDING, REMOVING or RESTRUCTURING a section, or changing its layout, width or background.\n' : ''}${onDemand.includes('animation') ? '- "animation": the motion system — load before adding, changing or removing any animation.\n' : ''}${onDemand.includes('3d') ? '- "3d": real-time 3D (three.js) — load before creating or editing a 3D scene, 3D object or WebGL content; for a 3D game load it TOGETHER with "games".\n' : ''}${onDemand.includes('games') ? '- "games": building any game, 2D or 3D — load before game work. A 2D/doodle/casual game uses Canvas 2D from this doc alone (no three.js); a 3D game also loads "3d".\n' : ''}${onDemand.includes('code') ? '- "code": the code block (data-cb-code) for elements that need JavaScript — load before writing or editing one. Not needed for 3D (the "3d" reference covers its own block).\n' : ''}${onDemand.includes('motionPath') ? `- "motionPath": moving an OBJECT along a route — a plane, rocket, car, dot or arrow flying, following or tracing a path, curve or journey across a scene. ${motionPathMode() === 'preserve' ? 'This effect is DISABLED for this site: load it only to preserve or adjust a path already in the markup, never to add one.' : 'Load it only for that; it is not needed for ordinary motion — entrances, fades, parallax, scroll reveals or hover effects.'} ALSO load it whenever markup you are about to edit contains a "data-fx-path" attribute, whatever the request was — those attributes are undocumented anywhere else and you will corrupt them otherwise.\n` : ''}Load what a task needs BEFORE writing markup for it. Never guess at classes from those areas.` : ''}`;
|
|
85078
85159
|
},
|
|
85079
85160
|
/* Structural tools. The engine adds the attachment and image tools. */
|
|
85080
85161
|
tools() {
|
|
@@ -85146,7 +85227,7 @@ ${onDemand.includes('box') ? '- "box": section and box structure — load before
|
|
|
85146
85227
|
}
|
|
85147
85228
|
}, ...(topics.length ? [{
|
|
85148
85229
|
name: 'get_framework_docs',
|
|
85149
|
-
description: `Load reference not included in your instructions. "box" covers section and box structure — layout, width, background. "animation" covers the motion system.${topics.includes('motionPath') ? ' "motionPath" covers moving an object along a route (a plane/rocket/dot tracing a path across a scene) — load it for that, and whenever markup you are editing contains a "data-fx-path" attribute.' : ''} Load what you need before writing the markup that uses it.`,
|
|
85230
|
+
description: `Load reference not included in your instructions. "box" covers section and box structure — layout, width, background. "animation" covers the motion system.${topics.includes('3d') ? ' "3d" covers real-time 3D (three.js).' : ''}${topics.includes('games') ? ' "games" covers building games, 2D and 3D.' : ''}${topics.includes('code') ? ' "code" covers the JavaScript code block (data-cb-code).' : ''}${topics.includes('motionPath') ? ' "motionPath" covers moving an object along a route (a plane/rocket/dot tracing a path across a scene) — load it for that, and whenever markup you are editing contains a "data-fx-path" attribute.' : ''} Load what you need before writing the markup that uses it.`,
|
|
85150
85231
|
parameters: {
|
|
85151
85232
|
type: 'object',
|
|
85152
85233
|
properties: {
|
|
@@ -85346,7 +85427,7 @@ function createRowTarget(engine) {
|
|
|
85346
85427
|
const limit = () => engine.limits.toolResult;
|
|
85347
85428
|
|
|
85348
85429
|
/* Reference the host actually supplied, in the order it is offered. */
|
|
85349
|
-
const docTopics = () => ['buttons', 'animation'].filter(k => engine.contextParts[k]);
|
|
85430
|
+
const docTopics = () => ['buttons', 'animation', '3d', 'games', 'code'].filter(k => engine.contextParts[k]);
|
|
85350
85431
|
|
|
85351
85432
|
/*
|
|
85352
85433
|
Exactly one .row, or null. Markup arrives fenced as often as not, and a
|
|
@@ -85360,6 +85441,7 @@ function createRowTarget(engine) {
|
|
|
85360
85441
|
holder.innerHTML = html.replace(/```html\n?/g, '').replace(/```\n?/g, '').trim();
|
|
85361
85442
|
const found = Array.from(holder.children).filter(el => el.classList.contains('row'));
|
|
85362
85443
|
if (found.length !== 1) return null;
|
|
85444
|
+
repairButtons(found[0]);
|
|
85363
85445
|
return found[0].outerHTML;
|
|
85364
85446
|
}
|
|
85365
85447
|
return {
|
|
@@ -86018,6 +86100,78 @@ ${docs}`;
|
|
|
86018
86100
|
}
|
|
86019
86101
|
});
|
|
86020
86102
|
}
|
|
86103
|
+
|
|
86104
|
+
/*
|
|
86105
|
+
3D asset generation (image-to-3D). Offered only when the host lists
|
|
86106
|
+
models3D — the same opt-in convention as the image tools. One image in,
|
|
86107
|
+
one .glb URL out; the model then loads it with GLTFLoader per the "3d"
|
|
86108
|
+
reference doc.
|
|
86109
|
+
*/
|
|
86110
|
+
if (this.threeDConfig()) {
|
|
86111
|
+
const threeDParams = {
|
|
86112
|
+
image_url: {
|
|
86113
|
+
type: 'string',
|
|
86114
|
+
description: 'URL of the source image: a photo on the page, or a generate_image result.'
|
|
86115
|
+
}
|
|
86116
|
+
};
|
|
86117
|
+
if ((this.currentAttachments || []).some(a => a && a.kind === 'image')) {
|
|
86118
|
+
threeDParams.attachment = {
|
|
86119
|
+
type: 'integer',
|
|
86120
|
+
description: 'Use an attached image as the source instead of image_url, numbered from 1.'
|
|
86121
|
+
};
|
|
86122
|
+
}
|
|
86123
|
+
t.push({
|
|
86124
|
+
name: 'generate_3d_model',
|
|
86125
|
+
description: 'Generate a 3D model (.glb file) from an image and return its URL, for use inside a 3D scene (load the "3d" reference doc first if you have not). The image must show ONE object, clearly and mostly front-facing — a product shot, an illustration, a generated image. For an object with no source image, call generate_image first and feed its result in. Generation is billed and takes 30-90 seconds, so one call per object the user asked for. Then load the returned URL with GLTFLoader and the normalize pattern from the "3d" reference — the URL goes inside the scene code, never in an <img>.',
|
|
86126
|
+
parameters: {
|
|
86127
|
+
type: 'object',
|
|
86128
|
+
properties: threeDParams,
|
|
86129
|
+
required: []
|
|
86130
|
+
}
|
|
86131
|
+
});
|
|
86132
|
+
}
|
|
86133
|
+
|
|
86134
|
+
/*
|
|
86135
|
+
The critic loop: the model can SEE what a code block's canvas actually
|
|
86136
|
+
renders and fix visual problems before answering — the difference
|
|
86137
|
+
between shipping a black scene and noticing it. Offered only while the
|
|
86138
|
+
page holds a canvas inside a code block, so ordinary requests never
|
|
86139
|
+
see it.
|
|
86140
|
+
*/
|
|
86141
|
+
/* Gated on the block, not its canvas — right after an insert the
|
|
86142
|
+
scene is still importing modules and the canvas does not exist yet,
|
|
86143
|
+
which is exactly when the model needs the tool in its list.
|
|
86144
|
+
captureRender does the waiting. */
|
|
86145
|
+
/*
|
|
86146
|
+
Only a model that reads images can act on the screenshot — for a
|
|
86147
|
+
text-only model the injected image is a provider error, so the tool is
|
|
86148
|
+
simply not offered and the docs' "when a view_render tool is offered"
|
|
86149
|
+
phrasing degrades the whole flow gracefully. Same capability registry
|
|
86150
|
+
the attach button uses.
|
|
86151
|
+
*/
|
|
86152
|
+
/*
|
|
86153
|
+
Off unless the host turns it on: the self-check costs an extra
|
|
86154
|
+
vision round-trip per scene, so it is opt-in per site.
|
|
86155
|
+
*/
|
|
86156
|
+
const selfCheckOn = !!(this.builder && this.builder.renderSelfCheck);
|
|
86157
|
+
const seesImages = resolveFileCapability(this.model, this.v1 && this.v1.codeModels || this.builder && this.builder.codeModels || []).accepts.includes('image');
|
|
86158
|
+
if (selfCheckOn && seesImages && this.builder && this.builder.doc && this.builder.doc.querySelector('[data-cb-code]')) {
|
|
86159
|
+
t.push({
|
|
86160
|
+
name: 'view_render',
|
|
86161
|
+
description: 'Screenshot the live render of a code block\'s canvas (3D scene, game, visualization). ALWAYS call this after creating a scene or making a significant visual change, then LOOK at the image and fix what is wrong (all black = missing environment/lights; empty = camera not framing the content; clipped or tiny = wrong scale or camera position) before giving your final answer. The screenshot arrives as an image in the next message. HTML overlays are not included — only the canvas.',
|
|
86162
|
+
parameters: {
|
|
86163
|
+
type: 'object',
|
|
86164
|
+
properties: {
|
|
86165
|
+
block: {
|
|
86166
|
+
type: 'integer',
|
|
86167
|
+
description: '1-based index among the page\'s canvas-bearing code blocks; omit for the first',
|
|
86168
|
+
default: 1
|
|
86169
|
+
}
|
|
86170
|
+
},
|
|
86171
|
+
required: []
|
|
86172
|
+
}
|
|
86173
|
+
});
|
|
86174
|
+
}
|
|
86021
86175
|
return t;
|
|
86022
86176
|
}
|
|
86023
86177
|
|
|
@@ -86068,6 +86222,14 @@ ${docs}`;
|
|
|
86068
86222
|
{
|
|
86069
86223
|
return await this.editImage(args);
|
|
86070
86224
|
}
|
|
86225
|
+
case 'generate_3d_model':
|
|
86226
|
+
{
|
|
86227
|
+
return await this.generate3DModel(args);
|
|
86228
|
+
}
|
|
86229
|
+
case 'view_render':
|
|
86230
|
+
{
|
|
86231
|
+
return await this.captureRender(args);
|
|
86232
|
+
}
|
|
86071
86233
|
default:
|
|
86072
86234
|
return {
|
|
86073
86235
|
error: `unknown tool ${name}`
|
|
@@ -86087,6 +86249,12 @@ ${docs}`;
|
|
|
86087
86249
|
|
|
86088
86250
|
async send(text, attachments = []) {
|
|
86089
86251
|
this.abort = new AbortController();
|
|
86252
|
+
// Per-request ceilings on the expensive/loopable tools. MAX_STEPS
|
|
86253
|
+
// bounds the loop as a whole; these stop a model from burning paid
|
|
86254
|
+
// generations or endless self-checks chasing a critique it cannot
|
|
86255
|
+
// satisfy — past the cap the tool refuses and tells it to finish.
|
|
86256
|
+
this.spent3D = 0;
|
|
86257
|
+
this.spentShots = 0;
|
|
86090
86258
|
this.beginBatch(); // one undo entry for the whole request
|
|
86091
86259
|
this.currentAttachments = attachments || [];
|
|
86092
86260
|
this.attachmentUrls = {};
|
|
@@ -86147,6 +86315,30 @@ ${docs}`;
|
|
|
86147
86315
|
content: JSON.stringify(result)
|
|
86148
86316
|
});
|
|
86149
86317
|
}
|
|
86318
|
+
|
|
86319
|
+
/*
|
|
86320
|
+
view_render screenshots cannot ride in the tool message —
|
|
86321
|
+
tool results are text on every provider — so they follow as a
|
|
86322
|
+
user message. dropAttachmentPayloads compacts them on the next
|
|
86323
|
+
request like any other image, so they never accumulate.
|
|
86324
|
+
*/
|
|
86325
|
+
if (this.pendingShots && this.pendingShots.length) {
|
|
86326
|
+
const parts = [{
|
|
86327
|
+
type: 'text',
|
|
86328
|
+
text: 'Screenshot of the current render:'
|
|
86329
|
+
}];
|
|
86330
|
+
this.pendingShots.forEach(u => parts.push({
|
|
86331
|
+
type: 'image_url',
|
|
86332
|
+
image_url: {
|
|
86333
|
+
url: u
|
|
86334
|
+
}
|
|
86335
|
+
}));
|
|
86336
|
+
messages.push({
|
|
86337
|
+
role: 'user',
|
|
86338
|
+
content: parts
|
|
86339
|
+
});
|
|
86340
|
+
this.pendingShots = [];
|
|
86341
|
+
}
|
|
86150
86342
|
}
|
|
86151
86343
|
|
|
86152
86344
|
// Ran out of steps. Not an error the user should see as a crash.
|
|
@@ -86582,6 +86774,267 @@ ${docs}`;
|
|
|
86582
86774
|
freshOutline() {
|
|
86583
86775
|
return this.target.outline();
|
|
86584
86776
|
}
|
|
86777
|
+
|
|
86778
|
+
/*
|
|
86779
|
+
Screenshot a code block's canvas for the critic loop. Waits for the canvas
|
|
86780
|
+
to exist and paint (module imports take a moment after an insert), then
|
|
86781
|
+
downscales to keep the image cheap. Requires the scene's renderer to have
|
|
86782
|
+
preserveDrawingBuffer: true (the 3d reference's shell sets it) — without
|
|
86783
|
+
it a WebGL canvas reads back blank between frames.
|
|
86784
|
+
*/
|
|
86785
|
+
async captureRender(args = {}) {
|
|
86786
|
+
const doc = this.builder && this.builder.doc;
|
|
86787
|
+
if (!doc) return {
|
|
86788
|
+
error: 'no page'
|
|
86789
|
+
};
|
|
86790
|
+
this.spentShots = (this.spentShots || 0) + 1;
|
|
86791
|
+
if (this.spentShots > 6) {
|
|
86792
|
+
return {
|
|
86793
|
+
error: 'limit reached: no more render checks in this request. Answer with the scene as it is, and be honest about anything still not right.'
|
|
86794
|
+
};
|
|
86795
|
+
}
|
|
86796
|
+
const n = Math.max(1, parseInt(args.block, 10) || 1);
|
|
86797
|
+
let canvas = null;
|
|
86798
|
+
for (let i = 0; i < 24; i++) {
|
|
86799
|
+
const canvases = doc.querySelectorAll('[data-cb-code] canvas');
|
|
86800
|
+
canvas = canvases[n - 1] || null;
|
|
86801
|
+
if (canvas && canvas.width > 2 && canvas.height > 2) break;
|
|
86802
|
+
await new Promise(r => setTimeout(r, 500));
|
|
86803
|
+
if (this.abort && this.abort.signal.aborted) throw Object.assign(new Error('aborted'), {
|
|
86804
|
+
name: 'AbortError'
|
|
86805
|
+
});
|
|
86806
|
+
}
|
|
86807
|
+
if (!canvas) return {
|
|
86808
|
+
error: 'no rendering canvas found in a code block (is the scene failing before it creates one?)'
|
|
86809
|
+
};
|
|
86810
|
+
await new Promise(r => setTimeout(r, 1500)); // let the first real frames draw
|
|
86811
|
+
|
|
86812
|
+
try {
|
|
86813
|
+
const scale = Math.min(1, 768 / canvas.width);
|
|
86814
|
+
const w = Math.max(1, Math.round(canvas.width * scale));
|
|
86815
|
+
const h = Math.max(1, Math.round(canvas.height * scale));
|
|
86816
|
+
const off = document.createElement('canvas');
|
|
86817
|
+
off.width = w;
|
|
86818
|
+
off.height = h;
|
|
86819
|
+
const ctx = off.getContext('2d');
|
|
86820
|
+
ctx.drawImage(canvas, 0, 0, w, h);
|
|
86821
|
+
|
|
86822
|
+
// A uniform image means nothing drew (or the renderer lacks
|
|
86823
|
+
// preserveDrawingBuffer) — say so instead of sending a blank.
|
|
86824
|
+
const d = ctx.getImageData(0, 0, w, h).data;
|
|
86825
|
+
let uniform = true;
|
|
86826
|
+
for (let i = 4; i < d.length; i += 997 * 4) {
|
|
86827
|
+
if (d[i] !== d[0] || d[i + 1] !== d[1] || d[i + 2] !== d[2]) {
|
|
86828
|
+
uniform = false;
|
|
86829
|
+
break;
|
|
86830
|
+
}
|
|
86831
|
+
}
|
|
86832
|
+
const dataUrl = off.toDataURL('image/jpeg', 0.8);
|
|
86833
|
+
this.pendingShots = this.pendingShots || [];
|
|
86834
|
+
this.pendingShots.push(dataUrl);
|
|
86835
|
+
if (uniform) {
|
|
86836
|
+
return {
|
|
86837
|
+
ok: true,
|
|
86838
|
+
warning: 'the canvas is a single flat color — either nothing rendered (check for errors: missing environment, objects outside the camera view) or the renderer lacks preserveDrawingBuffer: true, which this check requires. The (blank) screenshot follows as an image.'
|
|
86839
|
+
};
|
|
86840
|
+
}
|
|
86841
|
+
return {
|
|
86842
|
+
ok: true,
|
|
86843
|
+
note: 'screenshot captured — it follows as an image in the next message. Look at it before answering.'
|
|
86844
|
+
};
|
|
86845
|
+
} catch (e) {
|
|
86846
|
+
return {
|
|
86847
|
+
error: 'could not capture the canvas: ' + String(e && e.message || e)
|
|
86848
|
+
};
|
|
86849
|
+
}
|
|
86850
|
+
}
|
|
86851
|
+
|
|
86852
|
+
/*
|
|
86853
|
+
3D generation configuration, resolved per call so a host can switch
|
|
86854
|
+
provider at runtime the same way image generation can. Mirrors v1's
|
|
86855
|
+
provider resolution: endpoint options are looked up as
|
|
86856
|
+
`<base>_<Provider>` in any casing, falling back to the _Fal names.
|
|
86857
|
+
Today every supported provider is queue-shaped (request/status/result);
|
|
86858
|
+
a future single-call 3D provider gets its own branch in generate3DModel,
|
|
86859
|
+
the way SINGLE_CALL_IMAGE_PROVIDERS did for images.
|
|
86860
|
+
*/
|
|
86861
|
+
threeDConfig() {
|
|
86862
|
+
const b = this.builder;
|
|
86863
|
+
if (!b) return null;
|
|
86864
|
+
const models = Array.isArray(b.models3D) ? b.models3D.filter(m => m && m.id) : [];
|
|
86865
|
+
if (!models.length) return null;
|
|
86866
|
+
const provider = b.default3DGenerationProvider || 'fal';
|
|
86867
|
+
const val = base => {
|
|
86868
|
+
const wanted = `${base}_${provider}`.toLowerCase();
|
|
86869
|
+
const key = Object.keys(b).find(k => k.toLowerCase() === wanted && b[k]);
|
|
86870
|
+
return key ? b[key] : undefined;
|
|
86871
|
+
};
|
|
86872
|
+
const generateUrl = val('generateMediaUrl') || b.generateMediaUrl_Fal;
|
|
86873
|
+
const statusUrl = val('checkRequestStatusUrl') || b.checkRequestStatusUrl_Fal;
|
|
86874
|
+
const resultUrl = val('getResultUrl') || b.getResultUrl_Fal;
|
|
86875
|
+
if (!generateUrl || !statusUrl || !resultUrl) return null;
|
|
86876
|
+
|
|
86877
|
+
// The saved selection, snapping to the first entry when it is not listed
|
|
86878
|
+
const saved = this.v1 && this.v1.settings && this.v1.settings.model3D;
|
|
86879
|
+
const model = (models.find(m => m.id === saved) || models[0]).id;
|
|
86880
|
+
return {
|
|
86881
|
+
provider,
|
|
86882
|
+
models,
|
|
86883
|
+
model,
|
|
86884
|
+
generateUrl,
|
|
86885
|
+
statusUrl,
|
|
86886
|
+
resultUrl
|
|
86887
|
+
};
|
|
86888
|
+
}
|
|
86889
|
+
|
|
86890
|
+
/*
|
|
86891
|
+
Image-to-3D. The image reaches the provider the same way reference images
|
|
86892
|
+
do for image editing: data URLs go through as-is, same-origin files are
|
|
86893
|
+
read into a data URL (the provider cannot fetch a private host), and
|
|
86894
|
+
public URLs are passed for the provider to fetch itself.
|
|
86895
|
+
*/
|
|
86896
|
+
async generate3DModel(args = {}) {
|
|
86897
|
+
const cfg = this.threeDConfig();
|
|
86898
|
+
if (!cfg) return {
|
|
86899
|
+
error: '3D generation is not configured'
|
|
86900
|
+
};
|
|
86901
|
+
this.spent3D = (this.spent3D || 0) + 1;
|
|
86902
|
+
if (this.spent3D > 4) {
|
|
86903
|
+
return {
|
|
86904
|
+
error: 'limit reached: no more 3D model generations in this request. Use the models you already have (or build the object from code), finish the scene, and tell the user a model did not come out right if that is the case.'
|
|
86905
|
+
};
|
|
86906
|
+
}
|
|
86907
|
+
const signal = this.abort ? this.abort.signal : null;
|
|
86908
|
+
|
|
86909
|
+
// Resolve the source image
|
|
86910
|
+
let src = null;
|
|
86911
|
+
if (args.attachment !== undefined && args.attachment !== null) {
|
|
86912
|
+
const list = (this.currentAttachments || []).filter(a => a && a.kind === 'image');
|
|
86913
|
+
const att = list[(parseInt(args.attachment, 10) || 1) - 1];
|
|
86914
|
+
if (!att) return {
|
|
86915
|
+
error: `there ${list.length === 1 ? 'is 1 attached image' : `are ${list.length} attached images`}`
|
|
86916
|
+
};
|
|
86917
|
+
src = att.dataUrl;
|
|
86918
|
+
} else if (args.image_url) {
|
|
86919
|
+
src = this.expandAttachmentUrls(String(args.image_url));
|
|
86920
|
+
if (!/^data:/i.test(src)) {
|
|
86921
|
+
let abs;
|
|
86922
|
+
try {
|
|
86923
|
+
abs = new URL(src, location.href);
|
|
86924
|
+
} catch (e) {
|
|
86925
|
+
return {
|
|
86926
|
+
error: 'image_url is not a valid URL'
|
|
86927
|
+
};
|
|
86928
|
+
}
|
|
86929
|
+
if (abs.origin === location.origin) {
|
|
86930
|
+
try {
|
|
86931
|
+
const blob = await (await fetch(abs.href, {
|
|
86932
|
+
signal
|
|
86933
|
+
})).blob();
|
|
86934
|
+
src = await new Promise((resolve, reject) => {
|
|
86935
|
+
const r = new FileReader();
|
|
86936
|
+
r.onload = () => resolve(r.result);
|
|
86937
|
+
r.onerror = reject;
|
|
86938
|
+
r.readAsDataURL(blob);
|
|
86939
|
+
});
|
|
86940
|
+
} catch (e) {
|
|
86941
|
+
return {
|
|
86942
|
+
error: 'could not read the image at image_url'
|
|
86943
|
+
};
|
|
86944
|
+
}
|
|
86945
|
+
} else {
|
|
86946
|
+
src = abs.href; // public URL — the provider fetches it
|
|
86947
|
+
}
|
|
86948
|
+
}
|
|
86949
|
+
} else {
|
|
86950
|
+
return {
|
|
86951
|
+
error: 'give image_url or attachment'
|
|
86952
|
+
};
|
|
86953
|
+
}
|
|
86954
|
+
const b = this.builder;
|
|
86955
|
+
const headers = {
|
|
86956
|
+
...b.headers,
|
|
86957
|
+
...b.defaultHeaders
|
|
86958
|
+
};
|
|
86959
|
+
const customData = b.editor && b.editor.customData || {};
|
|
86960
|
+
const post = async (url, body) => {
|
|
86961
|
+
const response = await fetch(url, {
|
|
86962
|
+
method: 'POST',
|
|
86963
|
+
headers,
|
|
86964
|
+
signal,
|
|
86965
|
+
body: JSON.stringify(body)
|
|
86966
|
+
});
|
|
86967
|
+
return await response.json();
|
|
86968
|
+
};
|
|
86969
|
+
|
|
86970
|
+
// Each model family names its input field differently
|
|
86971
|
+
const payload = /hunyuan/i.test(cfg.model) ? {
|
|
86972
|
+
input_image_url: src
|
|
86973
|
+
} : {
|
|
86974
|
+
image_urls: [src],
|
|
86975
|
+
geometry_file_format: 'glb'
|
|
86976
|
+
}; // Rodin-shaped default
|
|
86977
|
+
|
|
86978
|
+
const firstTwoParts = cfg.model.split('/').slice(0, 2).join('/');
|
|
86979
|
+
const req = await post(cfg.generateUrl, {
|
|
86980
|
+
model: cfg.model,
|
|
86981
|
+
customData,
|
|
86982
|
+
endpoint: `https://queue.fal.run/${cfg.model}`,
|
|
86983
|
+
payload
|
|
86984
|
+
});
|
|
86985
|
+
if (req.error || !req.request_id) {
|
|
86986
|
+
return {
|
|
86987
|
+
error: typeof req.error === 'string' ? req.error : '3D generation request failed'
|
|
86988
|
+
};
|
|
86989
|
+
}
|
|
86990
|
+
const request_id = req.request_id;
|
|
86991
|
+
const statusBody = {
|
|
86992
|
+
model: cfg.model,
|
|
86993
|
+
endpoint: `https://queue.fal.run/${firstTwoParts}/requests/${request_id}/status`,
|
|
86994
|
+
request_id,
|
|
86995
|
+
customData
|
|
86996
|
+
};
|
|
86997
|
+
let status = '';
|
|
86998
|
+
for (let i = 0; i < 150 && status !== 'COMPLETED'; i++) {
|
|
86999
|
+
const st = await post(cfg.statusUrl, statusBody);
|
|
87000
|
+
if (st.ok === false) return {
|
|
87001
|
+
error: st.error || '3D generation failed'
|
|
87002
|
+
};
|
|
87003
|
+
if (st.ok && st.status) {
|
|
87004
|
+
status = st.status;
|
|
87005
|
+
if (status === 'FAILED') return {
|
|
87006
|
+
error: '3D generation failed'
|
|
87007
|
+
};
|
|
87008
|
+
} else if (st.data) {
|
|
87009
|
+
// API-style status (mirrors the image flow's fallback)
|
|
87010
|
+
if (st.data.model_mesh || st.data.model_glb || st.data.model_urls) status = 'COMPLETED';else if (st.data.status && !st.data.error) status = st.data.status;
|
|
87011
|
+
}
|
|
87012
|
+
if (status !== 'COMPLETED') await new Promise(r => setTimeout(r, 2000));
|
|
87013
|
+
if (signal && signal.aborted) throw Object.assign(new Error('aborted'), {
|
|
87014
|
+
name: 'AbortError'
|
|
87015
|
+
});
|
|
87016
|
+
}
|
|
87017
|
+
if (status !== 'COMPLETED') return {
|
|
87018
|
+
error: '3D generation timed out'
|
|
87019
|
+
};
|
|
87020
|
+
const res = await post(cfg.resultUrl, {
|
|
87021
|
+
model: cfg.model,
|
|
87022
|
+
request_id,
|
|
87023
|
+
customData,
|
|
87024
|
+
endpoint: `https://queue.fal.run/${firstTwoParts}/requests/${request_id}`
|
|
87025
|
+
});
|
|
87026
|
+
if (res.error) return {
|
|
87027
|
+
error: typeof res.error === 'string' ? res.error : '3D result failed'
|
|
87028
|
+
};
|
|
87029
|
+
const entry = res.data && res.data.entries && res.data.entries[0];
|
|
87030
|
+
if (!entry || !entry.url) return {
|
|
87031
|
+
error: 'the generation returned no model file'
|
|
87032
|
+
};
|
|
87033
|
+
return {
|
|
87034
|
+
url: entry.url,
|
|
87035
|
+
format: 'glb'
|
|
87036
|
+
};
|
|
87037
|
+
}
|
|
86585
87038
|
async generateImages(prompt, count = 1, size) {
|
|
86586
87039
|
if (!this.v1) throw new Error('image generation is not configured');
|
|
86587
87040
|
const signal = this.abort ? this.abort.signal : null;
|
|
@@ -86748,6 +87201,10 @@ function describeStep(tool, args, out, target) {
|
|
|
86748
87201
|
return out('Reading the attachment') + '…';
|
|
86749
87202
|
case 'generate_image':
|
|
86750
87203
|
return out('Generating image') + '…';
|
|
87204
|
+
case 'generate_3d_model':
|
|
87205
|
+
return out('Generating 3D model') + '…';
|
|
87206
|
+
case 'view_render':
|
|
87207
|
+
return out('Checking the render') + '…';
|
|
86751
87208
|
case 'edit_image':
|
|
86752
87209
|
return out('Editing image') + '…';
|
|
86753
87210
|
case '__waiting':
|
|
@@ -87009,6 +87466,55 @@ function attachCodeChatV2(codechat, builder, opts = {}) {
|
|
|
87009
87466
|
v2.stop();
|
|
87010
87467
|
if (origAbort) origAbort();
|
|
87011
87468
|
};
|
|
87469
|
+
|
|
87470
|
+
/*
|
|
87471
|
+
Settings dialog: the 3D model picker. Injected here rather than written
|
|
87472
|
+
into the panel because the panel belongs to v1 and 3D generation is a v2
|
|
87473
|
+
feature — the same borrowing pattern as sendMessage above. The group
|
|
87474
|
+
exists only when the host configured models3D; with one entry it simply
|
|
87475
|
+
shows what is active.
|
|
87476
|
+
*/
|
|
87477
|
+
const t3d = v2.threeDConfig && v2.threeDConfig();
|
|
87478
|
+
if (t3d && codechat.builderStuff && !codechat.builderStuff.querySelector('#model3DSelect')) {
|
|
87479
|
+
const anchor = codechat.builderStuff.querySelector('.div-image-generation-options');
|
|
87480
|
+
if (anchor) {
|
|
87481
|
+
const group = document.createElement('div');
|
|
87482
|
+
group.className = 'setting-group';
|
|
87483
|
+
group.innerHTML = `
|
|
87484
|
+
<label for="model3DSelect" class="setting-label">${out('3D Model')}</label>
|
|
87485
|
+
<p class="setting-description">${out('Select the AI model used for 3D model generation')}</p>
|
|
87486
|
+
<div class="cb-sel"><select id="model3DSelect"></select></div>`;
|
|
87487
|
+
const sel = group.querySelector('#model3DSelect');
|
|
87488
|
+
t3d.models.forEach(m => {
|
|
87489
|
+
const opt = document.createElement('option');
|
|
87490
|
+
opt.value = m.id;
|
|
87491
|
+
opt.textContent = m.label || m.id;
|
|
87492
|
+
sel.appendChild(opt);
|
|
87493
|
+
});
|
|
87494
|
+
anchor.parentNode.insertBefore(group, anchor.nextSibling);
|
|
87495
|
+
|
|
87496
|
+
// Current value on every open; snap to the first when stale
|
|
87497
|
+
const origOpen = codechat.openSettings ? codechat.openSettings.bind(codechat) : null;
|
|
87498
|
+
if (origOpen) {
|
|
87499
|
+
codechat.openSettings = function () {
|
|
87500
|
+
origOpen();
|
|
87501
|
+
const cfg = v2.threeDConfig();
|
|
87502
|
+
sel.value = cfg && cfg.model || '';
|
|
87503
|
+
if (sel.selectedIndex < 0 && sel.options.length) sel.selectedIndex = 0;
|
|
87504
|
+
};
|
|
87505
|
+
}
|
|
87506
|
+
|
|
87507
|
+
// Persist with the rest of the settings: set before the panel's
|
|
87508
|
+
// own save stringifies this.settings.
|
|
87509
|
+
const origSave = codechat.saveSettings ? codechat.saveSettings.bind(codechat) : null;
|
|
87510
|
+
if (origSave) {
|
|
87511
|
+
codechat.saveSettings = function () {
|
|
87512
|
+
codechat.settings.model3D = sel.value;
|
|
87513
|
+
origSave();
|
|
87514
|
+
};
|
|
87515
|
+
}
|
|
87516
|
+
}
|
|
87517
|
+
}
|
|
87012
87518
|
return v2;
|
|
87013
87519
|
}
|
|
87014
87520
|
|
|
@@ -87038,7 +87544,7 @@ All content lives in rows and columns — NEVER place content directly under a c
|
|
|
87038
87544
|
|
|
87039
87545
|
- Size: 'size-<n>' (px). Available: 12,13,14,15,16,17,18,19,20,21,24,28,32,35,38,42,46,48,50,54,60,64,68,72,76,80,84,88,92,96,100 … up to 400. Anchors: labels 12-14, body 16-18, subheads 20-32, headings 42-60, hero/display 60-96.
|
|
87040
87546
|
- Weight: font-light font-normal font-medium font-semibold font-bold
|
|
87041
|
-
- Tracking: tracking-tight tracking-normal tracking-wide tracking-wider tracking-
|
|
87547
|
+
- Tracking: named steps tracking-tighter tracking-tight tracking-normal tracking-wide tracking-wider tracking-widest, then a numeric scale 'tracking-<n>' where n is a multiple of 25 from 0 to 500 (n/1000 em, so 'tracking-150' = 0.15em). Uppercase labels read best at 150-250; display headings often want tracking-tight.
|
|
87042
87548
|
- Leading: leading-none leading-12 leading-13 leading-14 leading-15 leading-16 leading-17 leading-18 — body: leading-17/18, headings: leading-12/13
|
|
87043
87549
|
- Text utils: uppercase lowercase capitalize text-center text-left text-right — put alignment classes ON the text elements (h/p), NOT on a wrapper div. Keep column content flat.
|
|
87044
87550
|
|
|
@@ -87176,184 +87682,591 @@ Icons: use Bootstrap Icons (NOT emojis), wrapped in a div: <div class="text-cent
|
|
|
87176
87682
|
- Minimalist: generous whitespace; palette of black/white/grays; light heading weights; hierarchy through size and spacing, not color.
|
|
87177
87683
|
`;
|
|
87178
87684
|
|
|
87179
|
-
|
|
87180
|
-
|
|
87181
|
-
|
|
87182
|
-
|
|
87685
|
+
/*
|
|
87686
|
+
* context-code.js — the code element reference for Code Chat.
|
|
87687
|
+
*
|
|
87688
|
+
* Documents the RELEASED code element ([data-cb-code], see
|
|
87689
|
+
* contentbuilder/docs/code-element-guide.md and codeelement.js). The previous
|
|
87690
|
+
* version of this file described an old experimental contract
|
|
87691
|
+
* (data-cb-type="code" + container.mount) that never shipped.
|
|
87692
|
+
*
|
|
87693
|
+
* Sent on every v1 request and served as the on-demand 'code' topic in v2,
|
|
87694
|
+
* so it is kept short. No backticks or ${ } — template literal.
|
|
87695
|
+
*
|
|
87696
|
+
* Takes the host's options because where a block may sit differs by product:
|
|
87697
|
+
* ContentBox also has overlay layers, ContentBuilder does not. One file, one
|
|
87698
|
+
* wording, and neither product is told about markup it has no use for.
|
|
87699
|
+
*/
|
|
87183
87700
|
|
|
87184
|
-
|
|
87701
|
+
function getContextCodeBlock(opts = {}) {
|
|
87702
|
+
// Where a block may sit. ContentBox also has overlay layers; ContentBuilder
|
|
87703
|
+
// has columns only, and must not be told about markup it does not have.
|
|
87704
|
+
const placement = opts.overlay ? `It sits inside a column,
|
|
87705
|
+
beside other content, or inside '.is-overlay > .is-overlay-content'. To fill
|
|
87706
|
+
that layer, give the block 'position:absolute;inset:0' — the overlay is
|
|
87707
|
+
full-screen, the block is not.` : `It sits
|
|
87708
|
+
inside a column, beside other content.`;
|
|
87709
|
+
return `
|
|
87710
|
+
# Code Block
|
|
87185
87711
|
|
|
87186
|
-
|
|
87187
|
-
|
|
87712
|
+
For elements that need JavaScript, use a code block: <div data-cb-code>
|
|
87713
|
+
(that attribute alone marks it — no class, no other bookkeeping). ${placement} The code runs live while editing and
|
|
87714
|
+
runs again on the saved page.
|
|
87188
87715
|
|
|
87189
|
-
|
|
87716
|
+
A simple block is complete as written — no wrapper, no ceremony:
|
|
87190
87717
|
|
|
87191
|
-
<div
|
|
87192
|
-
<
|
|
87193
|
-
|
|
87194
|
-
|
|
87195
|
-
|
|
87196
|
-
|
|
87718
|
+
<div data-cb-code>
|
|
87719
|
+
<h1 id="greet7x">Lorem ipsum</h1>
|
|
87720
|
+
<p>This is a code block.</p>
|
|
87721
|
+
<script>
|
|
87722
|
+
document.querySelector('#greet7x').innerHTML = '<b>Hello World..!</b>';
|
|
87723
|
+
</script>
|
|
87197
87724
|
</div>
|
|
87198
87725
|
|
|
87199
|
-
|
|
87726
|
+
Ids are safe: when a block is duplicated, the editor gives the copy fresh ids
|
|
87727
|
+
and updates the references inside its script to match.
|
|
87200
87728
|
|
|
87201
|
-
|
|
87202
|
-
<div class="is-overlay">
|
|
87203
|
-
<div class="is-overlay-content">
|
|
87204
|
-
<div data-cb-type="code" style="position: absolute; top:0, left:0; width: 100%; height: 100%">
|
|
87205
|
-
<!-- Your Code -->
|
|
87206
|
-
</div>
|
|
87207
|
-
</div>
|
|
87208
|
-
</div>
|
|
87209
|
-
|
|
87210
|
-
<!-- ... -->
|
|
87211
|
-
</div>
|
|
87729
|
+
For blocks that do more, these rules keep them working through editing:
|
|
87212
87730
|
|
|
87213
|
-
|
|
87731
|
+
- The editor re-renders the block on edit, undo and duplicate, and each
|
|
87732
|
+
render runs the code again on FRESH elements. Anything that keeps running
|
|
87733
|
+
— a timer, an interval, a window/document listener, an animation loop —
|
|
87734
|
+
must stop by itself when its elements leave the page. Check isConnected:
|
|
87214
87735
|
|
|
87215
|
-
|
|
87736
|
+
const display = document.querySelector('#clock9k');
|
|
87737
|
+
const iv = setInterval(() => {
|
|
87738
|
+
if (!display.isConnected) { clearInterval(iv); return; }
|
|
87739
|
+
display.textContent = new Date().toLocaleTimeString();
|
|
87740
|
+
}, 1000);
|
|
87741
|
+
|
|
87742
|
+
- Plain classic <script> tags only — never type="module". To load a library,
|
|
87743
|
+
use dynamic import() inside ONE async IIFE wrapping the whole script,
|
|
87744
|
+
closed with exactly one })(); at the end — never nest a second wrapper:
|
|
87216
87745
|
|
|
87217
|
-
<div data-cb-type="code">
|
|
87218
|
-
<h3 class="code-block__title font-normal size-24">Code Block</h3>
|
|
87219
|
-
<p>This is a custom code block. Open HTML code editor to edit this block.</p>
|
|
87220
87746
|
<script>
|
|
87221
|
-
(
|
|
87222
|
-
|
|
87223
|
-
|
|
87224
|
-
// Navigate to the parent container
|
|
87225
|
-
const container = currentScript.parentElement;
|
|
87226
|
-
|
|
87227
|
-
const initialize = async () => {
|
|
87228
|
-
// Find the title within THIS container only
|
|
87229
|
-
const title = container.querySelector('.code-block__title');
|
|
87230
|
-
if (title) {
|
|
87231
|
-
title.textContent = 'Code Block';
|
|
87232
|
-
}
|
|
87233
|
-
};
|
|
87234
|
-
|
|
87235
|
-
// Create mount function and store it on the container
|
|
87236
|
-
// for the Runtime to render the plugin
|
|
87237
|
-
container.mount = function() {
|
|
87238
|
-
initialize(); // Start initialization
|
|
87239
|
-
};
|
|
87240
|
-
// If no runtime, just start initialization
|
|
87241
|
-
if (!window.builderRuntime) {
|
|
87242
|
-
initialize();
|
|
87243
|
-
}
|
|
87747
|
+
(async () => {
|
|
87748
|
+
const confetti = (await import('https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/+esm')).default;
|
|
87749
|
+
// ... the rest of the block's code ...
|
|
87244
87750
|
})();
|
|
87245
87751
|
</script>
|
|
87246
|
-
</div>
|
|
87247
|
-
|
|
87248
|
-
### 2. Inline '<style>' and Dynamic External Script Loading
|
|
87249
|
-
` + `
|
|
87250
|
-
<div data-cb-type="code">
|
|
87251
|
-
<style>
|
|
87252
87752
|
|
|
87253
|
-
|
|
87753
|
+
A classic library that defines a global loads with a created <script>
|
|
87754
|
+
element (check for an existing one first); CSS files the same way with a
|
|
87755
|
+
<link rel="stylesheet">. If you read document.currentScript, do it on the
|
|
87756
|
+
first line — it is null after the first await.
|
|
87254
87757
|
|
|
87255
|
-
|
|
87256
|
-
|
|
87257
|
-
|
|
87758
|
+
- Self-contained only: styles inline or in a <style> tag inside the block;
|
|
87759
|
+
never touch document-level styles or elements outside the block.
|
|
87760
|
+
`;
|
|
87761
|
+
}
|
|
87258
87762
|
|
|
87259
|
-
|
|
87260
|
-
|
|
87261
|
-
|
|
87262
|
-
|
|
87263
|
-
|
|
87264
|
-
|
|
87265
|
-
|
|
87266
|
-
|
|
87267
|
-
|
|
87268
|
-
|
|
87269
|
-
|
|
87270
|
-
|
|
87271
|
-
|
|
87272
|
-
}
|
|
87763
|
+
/*
|
|
87764
|
+
* context-3d.js — the "3d" on-demand doc topic for Code Chat v2.
|
|
87765
|
+
*
|
|
87766
|
+
* Loaded via get_framework_docs ONLY when a request involves real-time 3D:
|
|
87767
|
+
* a three.js scene, WebGL content, a 3D product view, or a browser game.
|
|
87768
|
+
* It is never part of the resident system prompt — 3D requests are rare and
|
|
87769
|
+
* this reference is long, so it rides the same on-demand rail as "animation".
|
|
87770
|
+
*
|
|
87771
|
+
* The recipes here were verified in the ContentBuilder public/test-3d
|
|
87772
|
+
* experiments (Sept 2026): the quality shell, sky-as-light, instanced grass,
|
|
87773
|
+
* and the glb normalize pattern are lifted from working pages, not invented.
|
|
87774
|
+
* Keep code samples free of backticks and ${ } — this is a template literal.
|
|
87775
|
+
*/
|
|
87273
87776
|
|
|
87274
|
-
|
|
87275
|
-
|
|
87276
|
-
link.href = 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css';
|
|
87277
|
-
link.onload = () => resolve();
|
|
87278
|
-
link.onerror = reject;
|
|
87279
|
-
document.head.appendChild(link);
|
|
87280
|
-
});
|
|
87281
|
-
};
|
|
87777
|
+
const context3D = `
|
|
87778
|
+
# Real-Time 3D & Games (three.js)
|
|
87282
87779
|
|
|
87283
|
-
|
|
87284
|
-
|
|
87285
|
-
|
|
87286
|
-
|
|
87287
|
-
resolve(window.Swiper);
|
|
87288
|
-
return;
|
|
87289
|
-
}
|
|
87290
|
-
const script = document.createElement('script');
|
|
87291
|
-
script.src = 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js';
|
|
87292
|
-
script.onload = () => resolve(window.Swiper);
|
|
87293
|
-
script.onerror = reject;
|
|
87294
|
-
document.head.appendChild(script);
|
|
87295
|
-
});
|
|
87296
|
-
};
|
|
87780
|
+
Use this whenever the user asks for a 3D scene, 3D object, WebGL, a 3D
|
|
87781
|
+
product/hero visual, or a playable browser game. Everything lives in a code
|
|
87782
|
+
element (data-cb-code), which runs live in the editor and as plain HTML on
|
|
87783
|
+
the published page.
|
|
87297
87784
|
|
|
87298
|
-
|
|
87299
|
-
|
|
87300
|
-
|
|
87301
|
-
|
|
87302
|
-
await loadSwiperCSS();
|
|
87303
|
-
const Swiper = await loadSwiper();
|
|
87785
|
+
A finished-looking scene is mostly LIGHTING, not geometry. Never ship the
|
|
87786
|
+
naive setup (ambient + directional light, no environment, no tone mapping) —
|
|
87787
|
+
it makes every material look like flat plastic. Always use the Scene Shell
|
|
87788
|
+
below.
|
|
87304
87789
|
|
|
87305
|
-
|
|
87790
|
+
## 1. The block
|
|
87306
87791
|
|
|
87307
|
-
|
|
87308
|
-
|
|
87309
|
-
|
|
87310
|
-
|
|
87792
|
+
3D lives in a code element: <div data-cb-code> (that attribute alone marks
|
|
87793
|
+
it; no class, no other bookkeeping). It sits inside a normal column, beside
|
|
87794
|
+
other content. The code runs LIVE while editing and runs again on the saved
|
|
87795
|
+
page, so what the user sees in the editor is the real scene.
|
|
87311
87796
|
|
|
87312
|
-
|
|
87313
|
-
|
|
87314
|
-
|
|
87315
|
-
|
|
87316
|
-
|
|
87317
|
-
|
|
87318
|
-
|
|
87319
|
-
initializeSlider();
|
|
87320
|
-
}
|
|
87797
|
+
<div data-cb-code>
|
|
87798
|
+
<div class="scene3d-stage" style="width:100%; aspect-ratio:16/9; position:relative; overflow:hidden; border-radius:8px; background:#101014;"></div>
|
|
87799
|
+
<script>
|
|
87800
|
+
(async () => {
|
|
87801
|
+
const container = document.currentScript.parentElement; // BEFORE any await
|
|
87802
|
+
const stage = container.querySelector('.scene3d-stage');
|
|
87803
|
+
// ... imports, then build the scene (see Scene Shell) ...
|
|
87321
87804
|
})();
|
|
87322
87805
|
</script>
|
|
87323
87806
|
</div>
|
|
87324
87807
|
|
|
87325
|
-
|
|
87326
|
-
|
|
87327
|
-
|
|
87328
|
-
|
|
87329
|
-
|
|
87330
|
-
|
|
87331
|
-
-
|
|
87332
|
-
|
|
87808
|
+
ONE async IIFE wraps the whole script — do not nest a second wrapper
|
|
87809
|
+
function, and close it with exactly one })(); at the end. Read
|
|
87810
|
+
document.currentScript on the first line: it is only set before the first
|
|
87811
|
+
await.
|
|
87812
|
+
|
|
87813
|
+
Rules that keep it healthy in the editor:
|
|
87814
|
+
- Plain classic <script> tags only — never type="module" (dynamic import()
|
|
87815
|
+
inside the classic script is the way to load modules).
|
|
87816
|
+
- Query elements from the container (document.currentScript.parentElement),
|
|
87817
|
+
not by id — the block can be duplicated, and container-scoped queries stay
|
|
87818
|
+
correct in every copy.
|
|
87819
|
+
- The editor re-renders the block on edit, undo and duplicate, and each
|
|
87820
|
+
render runs the code again on FRESH elements. Anything that keeps running
|
|
87821
|
+
— the render loop, timers, window/document listeners — must stop by itself
|
|
87822
|
+
when its elements leave the page. The shell's loop below does this with an
|
|
87823
|
+
isConnected check; apply the same rule to any setInterval or listener you
|
|
87824
|
+
add outside the loop.
|
|
87825
|
+
- The stage div is styled inline (width 100% + aspect-ratio); make it taller
|
|
87826
|
+
or full-bleed by changing that style, not by touching anything outside the
|
|
87827
|
+
block.
|
|
87828
|
+
|
|
87829
|
+
## 2. Loading three.js
|
|
87830
|
+
|
|
87831
|
+
Load as ES modules with dynamic import inside the script. Use jsdelivr's
|
|
87832
|
+
+esm build and PIN the version — addons then resolve their internal 'three'
|
|
87833
|
+
import to the same module instance, so no import map is needed:
|
|
87834
|
+
|
|
87835
|
+
const THREE = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/+esm');
|
|
87836
|
+
const { OrbitControls } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/controls/OrbitControls.js/+esm');
|
|
87837
|
+
const { GLTFLoader } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/loaders/GLTFLoader.js/+esm');
|
|
87838
|
+
const { MeshoptDecoder } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/libs/meshopt_decoder.module.js/+esm');
|
|
87839
|
+
const { RoomEnvironment } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/environments/RoomEnvironment.js/+esm');
|
|
87840
|
+
const { Sky } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/objects/Sky.js/+esm');
|
|
87841
|
+
|
|
87842
|
+
Import only what the scene uses. Every addon lives under examples/jsm/ with
|
|
87843
|
+
the same /+esm suffix.
|
|
87844
|
+
|
|
87845
|
+
## 3. The Scene Shell — always
|
|
87846
|
+
|
|
87847
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); // preserveDrawingBuffer: required by the view_render self-check
|
|
87848
|
+
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
87849
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
87850
|
+
renderer.shadowMap.enabled = true;
|
|
87851
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
87852
|
+
stage.appendChild(renderer.domElement);
|
|
87853
|
+
|
|
87854
|
+
const scene = new THREE.Scene();
|
|
87855
|
+
const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 200);
|
|
87856
|
+
|
|
87857
|
+
// ENVIRONMENT LIGHT — the single biggest quality lever. Pick one:
|
|
87858
|
+
// (a) Indoor / product / abstract: the built-in room, no files needed.
|
|
87859
|
+
const pmrem = new THREE.PMREMGenerator(renderer);
|
|
87860
|
+
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
|
|
87861
|
+
// (b) Outdoor: a physical sky that also LIGHTS the scene — see Environments.
|
|
87862
|
+
// (c) A specific mood: an equirect .hdr URL via RGBELoader, same mapping.
|
|
87863
|
+
|
|
87864
|
+
// KEY LIGHT with soft shadows grounds the objects.
|
|
87865
|
+
const sun = new THREE.DirectionalLight(0xfff2e0, 2.2);
|
|
87866
|
+
sun.position.set(4, 6, 3);
|
|
87867
|
+
sun.castShadow = true;
|
|
87868
|
+
sun.shadow.mapSize.set(2048, 2048);
|
|
87869
|
+
sun.shadow.radius = 5;
|
|
87870
|
+
sun.shadow.bias = -0.0005;
|
|
87871
|
+
scene.add(sun);
|
|
87872
|
+
|
|
87873
|
+
// SIZE to the stage, not the window. Handles editor column resizes too.
|
|
87874
|
+
function resize() {
|
|
87875
|
+
const w = stage.clientWidth, h = stage.clientHeight;
|
|
87876
|
+
if (!w || !h) return;
|
|
87877
|
+
camera.aspect = w / h;
|
|
87878
|
+
camera.updateProjectionMatrix();
|
|
87879
|
+
renderer.setSize(w, h);
|
|
87880
|
+
}
|
|
87881
|
+
new ResizeObserver(resize).observe(stage);
|
|
87882
|
+
resize();
|
|
87883
|
+
|
|
87884
|
+
// LOOP — stops itself when this render is replaced (edit/undo/duplicate),
|
|
87885
|
+
// and pauses while the block is off-screen or the tab is hidden.
|
|
87886
|
+
let visible = true;
|
|
87887
|
+
new IntersectionObserver(function(e) { visible = e[0].isIntersecting; }).observe(stage);
|
|
87888
|
+
const clock = new THREE.Clock();
|
|
87889
|
+
renderer.setAnimationLoop(function() {
|
|
87890
|
+
if (!stage.isConnected) { // this render was discarded
|
|
87891
|
+
renderer.setAnimationLoop(null);
|
|
87892
|
+
renderer.dispose();
|
|
87893
|
+
return;
|
|
87894
|
+
}
|
|
87895
|
+
if (!visible || document.hidden) return;
|
|
87896
|
+
const dt = Math.min(clock.getDelta(), 0.05);
|
|
87897
|
+
// ... update(dt) ...
|
|
87898
|
+
renderer.render(scene, camera);
|
|
87899
|
+
});
|
|
87333
87900
|
|
|
87334
|
-
|
|
87901
|
+
Materials: MeshStandardMaterial (metalness/roughness) or MeshPhysicalMaterial
|
|
87902
|
+
(clearcoat, transmission). With the environment set they pick up real
|
|
87903
|
+
reflections; without it, metals render black — that is the "flat plastic"
|
|
87904
|
+
failure. Shadows: castShadow/receiveShadow on meshes, receiveShadow on the
|
|
87905
|
+
ground. A ShadowMaterial plane (opacity ~0.35) gives a shadow-only floor for
|
|
87906
|
+
product shots.
|
|
87907
|
+
|
|
87908
|
+
## 4. Objects: write code or load a .glb
|
|
87909
|
+
|
|
87910
|
+
- WRITE CODE for man-made, regular shapes: buildings, furniture, product
|
|
87911
|
+
mockups, abstract hero geometry. Compose primitives (Box, Cylinder,
|
|
87912
|
+
Extrude, Lathe, RoundedBoxGeometry addon), and add a subtle CanvasTexture
|
|
87913
|
+
for surface interest (wood planks, panel lines). Free, tiny, editable.
|
|
87914
|
+
- LOAD A .glb for organic or specific things: characters, animals, plants, a
|
|
87915
|
+
real product, anything that must match a reference image. Getting one:
|
|
87916
|
+
- When a generate_3d_model tool is offered, use it: give it ONE clear image
|
|
87917
|
+
of the object (a photo on the page, a generate_image result, or the
|
|
87918
|
+
user's attachment) and load the .glb URL it returns. For an object with
|
|
87919
|
+
no image yet, generate_image first, then feed that in — image quality
|
|
87920
|
+
decides model quality.
|
|
87921
|
+
- When no such tool is offered, build the object from primitives with
|
|
87922
|
+
real material craft instead — never fabricate a model URL.
|
|
87923
|
+
|
|
87924
|
+
Always set the meshopt decoder (generated models are usually
|
|
87925
|
+
meshopt-compressed; it is harmless for plain files), and normalize — glbs
|
|
87926
|
+
come in arbitrary scale:
|
|
87927
|
+
|
|
87928
|
+
const loader = new GLTFLoader();
|
|
87929
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
87930
|
+
const gltf = await loader.loadAsync(url);
|
|
87931
|
+
const obj = gltf.scene;
|
|
87932
|
+
const box = new THREE.Box3().setFromObject(obj);
|
|
87933
|
+
const size = box.getSize(new THREE.Vector3());
|
|
87934
|
+
obj.scale.setScalar(TARGET_HEIGHT / Math.max(size.x, size.y, size.z));
|
|
87935
|
+
box.setFromObject(obj);
|
|
87936
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
87937
|
+
obj.position.set(-center.x, -box.min.y, -center.z); // centered, on the floor
|
|
87938
|
+
obj.traverse(function(n) { if (n.isMesh) { n.castShadow = true; n.receiveShadow = true; } });
|
|
87939
|
+
scene.add(obj);
|
|
87940
|
+
|
|
87941
|
+
## 5. Environments (outdoor scenes)
|
|
87942
|
+
|
|
87943
|
+
SKY THAT LIGHTS THE SCENE — golden hour in a few lines, no texture files:
|
|
87944
|
+
|
|
87945
|
+
const sky = new Sky();
|
|
87946
|
+
sky.scale.setScalar(4000);
|
|
87947
|
+
scene.add(sky);
|
|
87948
|
+
const sunDir = new THREE.Vector3().setFromSphericalCoords(1,
|
|
87949
|
+
THREE.MathUtils.degToRad(90 - 6), // elevation: 2-8 = sunset, 30+ = day
|
|
87950
|
+
THREE.MathUtils.degToRad(245)); // azimuth
|
|
87951
|
+
sky.material.uniforms.sunPosition.value.copy(sunDir);
|
|
87952
|
+
sky.material.uniforms.turbidity.value = 6;
|
|
87953
|
+
sky.material.uniforms.rayleigh.value = 2.2;
|
|
87954
|
+
scene.environment = pmrem.fromScene(sky, 0.02).texture; // the sky IS the light
|
|
87955
|
+
sun.position.copy(sunDir).multiplyScalar(120); // key light matches it
|
|
87956
|
+
renderer.toneMappingExposure = 0.55; // Sky is bright; tune 0.4-0.7
|
|
87957
|
+
scene.fog = new THREE.FogExp2(0xe8c8a0, 0.0035); // depth haze sells distance
|
|
87958
|
+
|
|
87959
|
+
TERRAIN — a displaced plane. Define ONE height function and reuse it for
|
|
87960
|
+
placing everything (grass, props) so nothing floats:
|
|
87961
|
+
|
|
87962
|
+
function ground(x, z) {
|
|
87963
|
+
return 1.1 * Math.sin(x * 0.045 + 1) * Math.cos(z * 0.05)
|
|
87964
|
+
+ 0.5 * Math.sin(x * 0.11 + 2.3) * Math.sin(z * 0.13 + 1.1);
|
|
87965
|
+
}
|
|
87966
|
+
Build a PlaneGeometry(240, 240, 180, 180), rotateX(-PI/2), set each vertex y
|
|
87967
|
+
from ground(x, z), give vertices green colors varied by height, then
|
|
87968
|
+
computeVertexNormals() and a MeshStandardMaterial({ vertexColors: true,
|
|
87969
|
+
roughness: 1 }).
|
|
87970
|
+
|
|
87971
|
+
WATER — a large plane with a dark blue-grey MeshStandardMaterial and
|
|
87972
|
+
roughness around 0.15. With the sky environment set it reflects the sunset
|
|
87973
|
+
by itself. Place it below the terrain edge.
|
|
87974
|
+
|
|
87975
|
+
GRASS / vegetation — NEVER individual meshes, always ONE InstancedBufferGeometry
|
|
87976
|
+
draw call. One tapered blade (a 4-row triangle strip about 0.018 wide, 1 tall),
|
|
87977
|
+
instanced 50k-150k times with per-instance attributes: offset vec3 (position,
|
|
87978
|
+
y from ground()), data vec4 (rotation angle, height 0.35-0.9, wind phase,
|
|
87979
|
+
tint 0.8-1.2). A small ShaderMaterial does the rest:
|
|
87980
|
+
- vertex: scale by height, bend tip (p.z += t*t*0.1), rotate by angle, sway by
|
|
87981
|
+
sin(time * 1.5 + phase + offset.x * 0.15) * 0.14 * t * t, add offset.
|
|
87982
|
+
- fragment: mix a dark base green (0.07, 0.13, 0.03) to a light tip green
|
|
87983
|
+
(0.38, 0.46, 0.13) by the blade's v coordinate, times tint; optionally add a
|
|
87984
|
+
small warm term at the very tip for sunset scenes.
|
|
87985
|
+
Scatter on a disc around the focal point (radius * sqrt(random) keeps it
|
|
87986
|
+
even), skip blades under objects, set instanceCount, frustumCulled = false,
|
|
87987
|
+
side: THREE.DoubleSide. Update the time uniform in the loop. The same
|
|
87988
|
+
technique makes flowers, reeds and starfields — change the geometry and count.
|
|
87989
|
+
|
|
87990
|
+
## 6. Interaction
|
|
87991
|
+
|
|
87992
|
+
HOVER + CLICK on 3D objects — a raycaster against the renderer bounds (never
|
|
87993
|
+
window coordinates; the canvas is inside a column):
|
|
87994
|
+
|
|
87995
|
+
const ray = new THREE.Raycaster();
|
|
87996
|
+
const pointer = new THREE.Vector2();
|
|
87997
|
+
function pick(e) {
|
|
87998
|
+
const r = renderer.domElement.getBoundingClientRect();
|
|
87999
|
+
pointer.set(((e.clientX - r.left) / r.width) * 2 - 1,
|
|
88000
|
+
-((e.clientY - r.top) / r.height) * 2 + 1);
|
|
88001
|
+
ray.setFromCamera(pointer, camera);
|
|
88002
|
+
const hit = ray.intersectObjects(pickables, true)[0];
|
|
88003
|
+
return hit ? hit.object : null;
|
|
88004
|
+
}
|
|
88005
|
+
renderer.domElement.addEventListener('pointermove', function(e) {
|
|
88006
|
+
const o = pick(e);
|
|
88007
|
+
renderer.domElement.style.cursor = o ? 'pointer' : '';
|
|
88008
|
+
// hover response: scale/emissive lerp in the loop, not a snap
|
|
88009
|
+
});
|
|
88010
|
+
renderer.domElement.addEventListener('click', function(e) {
|
|
88011
|
+
const o = pick(e); if (o) { /* select, open, score ... */ }
|
|
88012
|
+
});
|
|
87335
88013
|
|
|
87336
|
-
|
|
87337
|
-
|
|
87338
|
-
|
|
87339
|
-
|
|
87340
|
-
|
|
87341
|
-
|
|
87342
|
-
|
|
88014
|
+
Animate responses smoothly in the loop (lerp toward a target scale/rotation/
|
|
88015
|
+
color) instead of setting values on the event — that is what makes it feel
|
|
88016
|
+
polished. Camera orbit: OrbitControls with enableDamping = true; constrain
|
|
88017
|
+
(maxPolarAngle, min/maxDistance) so users cannot go under the floor.
|
|
88018
|
+
|
|
88019
|
+
REVEAL PATTERNS — a click should MEAN something, not just wobble. Four
|
|
88020
|
+
recurring shapes, all built from the same pieces (pick + lerp + HTML):
|
|
88021
|
+
|
|
88022
|
+
CAMERA FOCUS (product tour): click an object, fly the camera to frame it,
|
|
88023
|
+
and offer the way back.
|
|
88024
|
+
|
|
88025
|
+
let camTo = null, lookTo = null;
|
|
88026
|
+
function focus(obj) {
|
|
88027
|
+
const p = new THREE.Vector3();
|
|
88028
|
+
obj.getWorldPosition(p);
|
|
88029
|
+
camTo = p.clone().add(new THREE.Vector3(1.4, 0.8, 2.0)); // offset that frames it
|
|
88030
|
+
lookTo = p.clone();
|
|
88031
|
+
backBtn.style.display = 'block';
|
|
88032
|
+
}
|
|
88033
|
+
function overview() {
|
|
88034
|
+
camTo = HOME_POS.clone(); lookTo = HOME_TARGET.clone();
|
|
88035
|
+
backBtn.style.display = 'none'; card.style.display = 'none';
|
|
88036
|
+
}
|
|
88037
|
+
// in the loop:
|
|
88038
|
+
if (camTo) {
|
|
88039
|
+
const k = 1 - Math.exp(-dt * 4);
|
|
88040
|
+
camera.position.lerp(camTo, k);
|
|
88041
|
+
controls.target.lerp(lookTo, k);
|
|
88042
|
+
if (camera.position.distanceTo(camTo) < 0.01) { camTo = null; lookTo = null; }
|
|
88043
|
+
}
|
|
88044
|
+
controls.addEventListener('start', function() { camTo = null; lookTo = null; }); // user drag wins
|
|
88045
|
+
|
|
88046
|
+
INFO CARD: the revealed details are real HTML overlaid on the stage — never
|
|
88047
|
+
3D text. A fixed-position card (absolute inside the stage, e.g. right side)
|
|
88048
|
+
reads better than one tracking the 3D point, and never clips. Fill it per
|
|
88049
|
+
object from a plain data object keyed by mesh name; give it a real <button>
|
|
88050
|
+
to close, and close on Escape too. Selectable, accessible, translatable.
|
|
88051
|
+
|
|
88052
|
+
STATE REVEAL: the click changes the scene itself — a lid opens, parts fan
|
|
88053
|
+
out into an exploded view, the chosen item stays lit while the rest dim.
|
|
88054
|
+
Record each part's home transform at build time, keep one 0..1 reveal value
|
|
88055
|
+
per group, and lerp every part between home and revealed each frame from
|
|
88056
|
+
that value. Dimming: lerp material color or opacity, never a CSS filter.
|
|
88057
|
+
|
|
88058
|
+
SCENE-TO-PAGE REVEAL: when the revealed content belongs in the page (specs,
|
|
88059
|
+
paragraphs, a price table), toggle a normal HTML element NEXT TO the stage
|
|
88060
|
+
instead of an overlay. The content stays selectable and readable without
|
|
88061
|
+
WebGL; the scene is the index, the page is the content.
|
|
88062
|
+
|
|
88063
|
+
Shared rules: one selection at a time; clicking empty space = Back; Back and
|
|
88064
|
+
close are real, keyboard-reachable buttons; everything moves through the
|
|
88065
|
+
loop's lerp, nothing snaps.
|
|
88066
|
+
|
|
88067
|
+
Touch: pointer events above already cover it; OrbitControls handles touch
|
|
88068
|
+
orbit. Do not call preventDefault on wheel/touch you do not use — the page
|
|
88069
|
+
must keep scrolling normally past the block.
|
|
88070
|
+
|
|
88071
|
+
SCROLL-DRIVEN scenes: read progress from the block's own position — works
|
|
88072
|
+
everywhere:
|
|
88073
|
+
|
|
88074
|
+
function scrollProgress() { // 0 when entering, 1 when leaving
|
|
88075
|
+
const r = stage.getBoundingClientRect();
|
|
88076
|
+
return THREE.MathUtils.clamp((innerHeight - r.top) / (innerHeight + r.height), 0, 1);
|
|
88077
|
+
}
|
|
88078
|
+
Call it in the loop and drive camera/objects from it (again through lerp for
|
|
88079
|
+
smoothness). On a ContentBox page whose section has data-fx-emit, prefer the
|
|
88080
|
+
host's event: container.closest('.is-section').addEventListener('fx:progress',
|
|
88081
|
+
function(e) { p = e.detail.progress; }).
|
|
88082
|
+
|
|
88083
|
+
## 7. Games (3D)
|
|
88084
|
+
|
|
88085
|
+
Game craft — loop, input, state machine, HUD, sound, recycling, cleanup —
|
|
88086
|
+
lives in the "games" reference: load it for any game. This section is only
|
|
88087
|
+
what makes a THREE.JS game look and feel right:
|
|
88088
|
+
|
|
88089
|
+
- The Scene Shell applies to games in full. Environment lighting, real
|
|
88090
|
+
materials, fog and shadows are what separate a game from a tech demo —
|
|
88091
|
+
never fall back to flat MeshBasicMaterial arcade defaults. Emissive
|
|
88092
|
+
materials + bloom give the cheap "neon glow" so many game styles want.
|
|
88093
|
+
- HERO ASSETS: the 2-3 entities that carry the whole look — the player
|
|
88094
|
+
craft, the collectible, one signature obstacle — deserve generated models
|
|
88095
|
+
(generate_3d_model when offered). Load each .glb ONCE, then obj.clone()
|
|
88096
|
+
per spawn; cloning is as cheap as cloning boxes, so recycling pools work
|
|
88097
|
+
unchanged. This single choice is the difference between "colored cubes"
|
|
88098
|
+
and a game with an identity.
|
|
88099
|
+
- FILLER stays coded, but crafted: RoundedBoxGeometry over BoxGeometry,
|
|
88100
|
+
slight bevels, emissive rims, vertex-colored gradients — never bare
|
|
88101
|
+
saturated boxes.
|
|
88102
|
+
- Movement feel: lerp positions and tilt/bank the player toward its motion;
|
|
88103
|
+
camera follows with damping, never rigidly.
|
|
88104
|
+
- Update per frame from the update(dt) function; collisions at this scale
|
|
88105
|
+
are distance checks (player.position.distanceTo(e.position) < r1 + r2).
|
|
88106
|
+
|
|
88107
|
+
## 8. Self-check — see your work
|
|
88108
|
+
|
|
88109
|
+
When a view_render tool is offered, ALWAYS call it after creating a scene or
|
|
88110
|
+
making a significant visual change, and LOOK at the screenshot before
|
|
88111
|
+
answering:
|
|
88112
|
+
- All black or flat: the environment/lighting is missing, or every object is
|
|
88113
|
+
outside the camera view.
|
|
88114
|
+
- Empty or nearly empty: camera position/target does not frame the content —
|
|
88115
|
+
check object positions and scale.
|
|
88116
|
+
- Clipped, tiny or washed out: adjust framing, scale or exposure.
|
|
88117
|
+
Fix what is wrong, re-check once, and only then answer. Do not describe a
|
|
88118
|
+
scene you have not seen. (The screenshot shows the canvas only — HTML
|
|
88119
|
+
overlays are not in it.)
|
|
88120
|
+
|
|
88121
|
+
Judge the render like an art director: the bar is FINISHED, not merely
|
|
88122
|
+
correct. A right-shaped scene rendered badly — glaring or flat light, dead
|
|
88123
|
+
materials, awkward framing, harsh exposure — is not done. Weigh the impact
|
|
88124
|
+
yourself, and reach for the cheap lever first:
|
|
88125
|
+
- Anything fixable in code — lighting, environment, exposure, camera,
|
|
88126
|
+
materials, scale — is FREE. If a change would noticeably improve the
|
|
88127
|
+
scene, make it.
|
|
88128
|
+
- Regenerating a model costs the user money. Do it only when the asset
|
|
88129
|
+
itself is the problem (mangled geometry, clearly the wrong object) and
|
|
88130
|
+
code cannot compensate — better light, exposure and framing rescue a
|
|
88131
|
+
mediocre asset more often than a re-roll does.
|
|
88132
|
+
When the scene genuinely reads well, it is done — stop; do not iterate on
|
|
88133
|
+
differences a viewer would not notice.
|
|
88134
|
+
|
|
88135
|
+
## 9. Performance & hygiene
|
|
88136
|
+
|
|
88137
|
+
- One scene per block; reuse geometries/materials; instancing for anything
|
|
88138
|
+
repeated more than ~20 times.
|
|
88139
|
+
- Keep pixelRatio capped at 2 and shadow maps at 2048 or below.
|
|
88140
|
+
- The visibility pause in the shell is mandatory — a page can hold several
|
|
88141
|
+
blocks and background tabs must not burn CPU.
|
|
88142
|
+
- Respect prefers-reduced-motion: if it matches, skip autonomous camera
|
|
88143
|
+
motion and wind sway; render still frames on interaction only.
|
|
88144
|
+
- Do not touch document-level styles, and never assume the block is the only
|
|
88145
|
+
3D on the page.
|
|
88146
|
+
`;
|
|
87343
88147
|
|
|
87344
|
-
|
|
88148
|
+
/*
|
|
88149
|
+
* context-games.js — the "games" on-demand doc topic for Code Chat v2.
|
|
88150
|
+
*
|
|
88151
|
+
* Loaded via get_framework_docs for ANY game request, 2D or 3D. It owns the
|
|
88152
|
+
* craft every game shares (loop, input, state, HUD, sound, cleanup) plus the
|
|
88153
|
+
* 2D Canvas branch; a 3D game additionally loads the "3d" topic, whose games
|
|
88154
|
+
* section holds only the 3D-specific rendering rules.
|
|
88155
|
+
*
|
|
88156
|
+
* Asset reality check (user-verified, Sept 2026): most image models FAIL at
|
|
88157
|
+
* transparent PNGs — sprites are requested on solid backgrounds and handled
|
|
88158
|
+
* in code. Never instruct the model to ask for transparency.
|
|
88159
|
+
* Keep code samples free of backticks and ${ } — template literal.
|
|
88160
|
+
*/
|
|
87345
88161
|
|
|
87346
|
-
|
|
87347
|
-
|
|
87348
|
-
|
|
87349
|
-
|
|
87350
|
-
|
|
87351
|
-
|
|
87352
|
-
|
|
88162
|
+
const contextGames = `
|
|
88163
|
+
# Building Games
|
|
88164
|
+
|
|
88165
|
+
Use this for any playable game. First decide the dimension — it changes the
|
|
88166
|
+
whole toolchain:
|
|
88167
|
+
|
|
88168
|
+
- 2D (doodle, flat, sprite, puzzle, casual, "simple game"): the Canvas 2D
|
|
88169
|
+
API in a code block. Do NOT use three.js for these — it is heavier, slower
|
|
88170
|
+
to write, and fights the flat aesthetic.
|
|
88171
|
+
- 3D (depth, camera, "3D"): three.js — ALSO load the "3d" reference and
|
|
88172
|
+
follow its games section for rendering and assets.
|
|
88173
|
+
|
|
88174
|
+
Target scale either way: an arcade mini-game — one scene, one core mechanic,
|
|
88175
|
+
polished. Refine with the user over turns.
|
|
88176
|
+
|
|
88177
|
+
## The skeleton every game shares
|
|
88178
|
+
|
|
88179
|
+
Everything lives in one code block (see the "code" reference for the block
|
|
88180
|
+
contract). Inside it:
|
|
88181
|
+
|
|
88182
|
+
- LOOP: requestAnimationFrame with a clamped delta; stop yourself when the
|
|
88183
|
+
block is re-rendered:
|
|
88184
|
+
|
|
88185
|
+
let last = performance.now();
|
|
88186
|
+
function frame(now) {
|
|
88187
|
+
if (!stage.isConnected) return; // this render was replaced
|
|
88188
|
+
const dt = Math.min((now - last) / 1000, 0.05);
|
|
88189
|
+
last = now;
|
|
88190
|
+
update(dt); draw();
|
|
88191
|
+
requestAnimationFrame(frame);
|
|
88192
|
+
}
|
|
88193
|
+
requestAnimationFrame(frame);
|
|
88194
|
+
|
|
88195
|
+
- INPUT: one flags object; keyboard listeners attach on game start (the
|
|
88196
|
+
start click also gives focus), ignore keys while the user types elsewhere,
|
|
88197
|
+
and detach themselves via the isConnected check. Map pointerdown /
|
|
88198
|
+
pointermove on the stage to the same flags so touch works. Do not
|
|
88199
|
+
preventDefault on events you do not use — the page must keep scrolling.
|
|
88200
|
+
- STATE: a small machine — ready / playing / over — with restart resetting
|
|
88201
|
+
state, never reloading. Show "click or tap to start".
|
|
88202
|
+
- HUD: HTML positioned over the stage (score, lives, messages) — real text,
|
|
88203
|
+
updated via textContent, never drawn glyph-by-glyph into the canvas.
|
|
88204
|
+
- SOUND: only if asked; short WebAudio oscillator beeps, no external files.
|
|
88205
|
+
- ENTITIES: recycle, never allocate per frame — spawn from a pool, move
|
|
88206
|
+
off-screen items back in.
|
|
88207
|
+
- SELF-CHECK: when a view_render tool is offered, screenshot after building
|
|
88208
|
+
and after big visual changes, judge it, fix, re-check once.
|
|
88209
|
+
|
|
88210
|
+
## 2D games (Canvas 2D)
|
|
88211
|
+
|
|
88212
|
+
Setup — crisp on every screen:
|
|
88213
|
+
|
|
88214
|
+
<div data-cb-code>
|
|
88215
|
+
<div class="game-stage" style="position:relative; width:100%; aspect-ratio:16/10; overflow:hidden; border-radius:8px; background:#fdfdf8;">
|
|
88216
|
+
<canvas style="position:absolute; inset:0; width:100%; height:100%;"></canvas>
|
|
88217
|
+
<!-- HUD elements here -->
|
|
87353
88218
|
</div>
|
|
87354
|
-
|
|
87355
|
-
|
|
88219
|
+
<script>
|
|
88220
|
+
(async () => {
|
|
88221
|
+
const container = document.currentScript.parentElement;
|
|
88222
|
+
const stage = container.querySelector('.game-stage');
|
|
88223
|
+
const canvas = stage.querySelector('canvas');
|
|
88224
|
+
const ctx = canvas.getContext('2d');
|
|
88225
|
+
function fit() {
|
|
88226
|
+
const dpr = Math.min(devicePixelRatio, 2);
|
|
88227
|
+
canvas.width = stage.clientWidth * dpr;
|
|
88228
|
+
canvas.height = stage.clientHeight * dpr;
|
|
88229
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // draw in CSS pixels
|
|
88230
|
+
}
|
|
88231
|
+
new ResizeObserver(fit).observe(stage);
|
|
88232
|
+
fit();
|
|
88233
|
+
// ... game ...
|
|
88234
|
+
})();
|
|
88235
|
+
</script>
|
|
87356
88236
|
</div>
|
|
88237
|
+
|
|
88238
|
+
Collision at this scale is rectangles and circle distances — no physics
|
|
88239
|
+
library unless the game truly is physics.
|
|
88240
|
+
|
|
88241
|
+
ART DIRECTION — a 2D game must look intentional, not like debug rectangles:
|
|
88242
|
+
pick a style (hand-drawn doodle, flat pastel, neon minimal) and carry it
|
|
88243
|
+
through background, entities, HUD typography and even the game-over screen.
|
|
88244
|
+
|
|
88245
|
+
- DOODLE STYLE, pure code: draw with strokes, not fills — rounded lineJoin,
|
|
88246
|
+
slightly thick lines, and a small random jitter added to each vertex,
|
|
88247
|
+
re-rolled only a few times per second (a wobble timer, not every frame) so
|
|
88248
|
+
shapes shiver like a flipbook. Paper background (#fdfdf8, faint grid),
|
|
88249
|
+
handwritten-feel HUD (a casual system font, slight rotation).
|
|
88250
|
+
- SPRITES from generate_image, when the style needs real drawings: request
|
|
88251
|
+
each sprite "on a plain solid white background, no shadow" in one
|
|
88252
|
+
consistent style. Do NOT request transparent backgrounds — image models
|
|
88253
|
+
usually fail at true transparency, delivering fake checkerboards or dirty
|
|
88254
|
+
edges. Then either:
|
|
88255
|
+
- design the game on the SAME background color, so the sprite's white
|
|
88256
|
+
simply disappears (the doodle-on-paper trick — zero code); or
|
|
88257
|
+
- key it out at load: draw to an offscreen canvas, getImageData, and set
|
|
88258
|
+
alpha 0 on pixels close to the corner color (tolerance ~28); cache the
|
|
88259
|
+
result and draw that.
|
|
88260
|
+
- Parallax sells motion cheaply: two or three background layers scrolling at
|
|
88261
|
+
different speeds.
|
|
88262
|
+
|
|
88263
|
+
## 3D games
|
|
88264
|
+
|
|
88265
|
+
Load the "3d" reference and follow its games section. The short version:
|
|
88266
|
+
the scene shell (environment light, real materials, fog) applies to games
|
|
88267
|
+
exactly as to scenes — no flat-material arcade fallback — and the 2-3 hero
|
|
88268
|
+
entities that carry the look (player, collectible, signature obstacle)
|
|
88269
|
+
deserve generated models, loaded once and cloned per spawn.
|
|
87357
88270
|
`;
|
|
87358
88271
|
|
|
87359
88272
|
const contextDesignGuide = `
|
|
@@ -88961,7 +89874,7 @@ class ContentBuilder {
|
|
|
88961
89874
|
this.ShortcutInfo = new ShortcutInfo(this);
|
|
88962
89875
|
if (!this.opts.isContentBox) {
|
|
88963
89876
|
this.codechat = new CodeChat({
|
|
88964
|
-
context: contextContentFramework +
|
|
89877
|
+
context: contextContentFramework + getContextCodeBlock() + contextDesignGuide
|
|
88965
89878
|
}, this);
|
|
88966
89879
|
|
|
88967
89880
|
/*
|
|
@@ -88976,10 +89889,16 @@ class ContentBuilder {
|
|
|
88976
89889
|
attachCodeChatV2(this.codechat, this, {
|
|
88977
89890
|
contextParts: {
|
|
88978
89891
|
content: contextContentFramework,
|
|
88979
|
-
designCore: contextDesignGuide
|
|
89892
|
+
designCore: contextDesignGuide,
|
|
89893
|
+
code: getContextCodeBlock(),
|
|
89894
|
+
// on-demand 'code' doc topic
|
|
89895
|
+
'3d': context3D,
|
|
89896
|
+
// on-demand '3d' doc topic
|
|
89897
|
+
games: contextGames // on-demand 'games' doc topic
|
|
88980
89898
|
}
|
|
88981
89899
|
});
|
|
88982
89900
|
}
|
|
89901
|
+
|
|
88983
89902
|
if (this.startAIAssistant) {
|
|
88984
89903
|
this.openAIAssistant();
|
|
88985
89904
|
}
|
|
@@ -91937,6 +92856,90 @@ class ContentBuilder {
|
|
|
91937
92856
|
openFilePicker(type, callback) {
|
|
91938
92857
|
this.openAssetSelect(type, callback);
|
|
91939
92858
|
}
|
|
92859
|
+
|
|
92860
|
+
/**
|
|
92861
|
+
* Is an asset manager reachable for this type of file? Mirrors the branches
|
|
92862
|
+
* openAsset() takes: either a picker page (filePicker / imageSelect / ...)
|
|
92863
|
+
* or the host's own onXSelectClick handler.
|
|
92864
|
+
*/
|
|
92865
|
+
hasFilePicker(type) {
|
|
92866
|
+
if (type === 'media') return !!(this.onMediaSelectClick || this.onImageSelectClick || this.mediaSelect || this.imageSelect);
|
|
92867
|
+
if (type === 'video') return !!(this.onVideoSelectClick || this.videoSelect);
|
|
92868
|
+
if (type === 'audio') return !!(this.onAudioSelectClick || this.audioSelect);
|
|
92869
|
+
if (type === 'all') return !!(this.onFileSelectClick || this.fileSelect);
|
|
92870
|
+
return !!(this.onImageSelectClick || this.imageSelect);
|
|
92871
|
+
}
|
|
92872
|
+
|
|
92873
|
+
/**
|
|
92874
|
+
* Upload a file from the user's computer and hand its URL to `callback`.
|
|
92875
|
+
*
|
|
92876
|
+
* The per-type upload handler always exists — a builder configured without
|
|
92877
|
+
* one gets the data-URL fallback — so this is available whatever the host
|
|
92878
|
+
* set up. The URL comes back through returnUrl(), the same path the image
|
|
92879
|
+
* tool's upload button uses.
|
|
92880
|
+
*/
|
|
92881
|
+
openFileUpload(type, callback) {
|
|
92882
|
+
const accept = type === 'video' ? 'video/*' : type === 'audio' ? 'audio/*' : type === 'media' ? 'image/*,video/*' : type === 'all' ? '*' : 'image/*';
|
|
92883
|
+
const handler = type === 'video' ? this.onVideoUpload : type === 'audio' ? this.onAudioUpload : type === 'media' ? this.onMediaUpload : type === 'all' ? this.onFileUpload : this.onImageUpload;
|
|
92884
|
+
if (!handler) return;
|
|
92885
|
+
const inpFile = document.createElement('input');
|
|
92886
|
+
inpFile.type = 'file';
|
|
92887
|
+
inpFile.accept = accept;
|
|
92888
|
+
inpFile.style.display = 'none';
|
|
92889
|
+
document.body.appendChild(inpFile);
|
|
92890
|
+
inpFile.addEventListener('change', async e => {
|
|
92891
|
+
if (!e.target.files || !e.target.files.length) {
|
|
92892
|
+
inpFile.remove();
|
|
92893
|
+
return;
|
|
92894
|
+
}
|
|
92895
|
+
this.onAssetUpload = url => {
|
|
92896
|
+
if (callback) callback(url);
|
|
92897
|
+
};
|
|
92898
|
+
await handler(e);
|
|
92899
|
+
inpFile.remove();
|
|
92900
|
+
});
|
|
92901
|
+
inpFile.click();
|
|
92902
|
+
}
|
|
92903
|
+
|
|
92904
|
+
/**
|
|
92905
|
+
* The file controls for a URL field in a plugin's settings: select from the
|
|
92906
|
+
* asset manager, and upload from the computer.
|
|
92907
|
+
*
|
|
92908
|
+
* Returns the buttons to append next to the input, in the order and with the
|
|
92909
|
+
* icons the editor's own image dialog uses — select first, upload last, and
|
|
92910
|
+
* select only where an asset manager is configured. A plugin appends what it
|
|
92911
|
+
* gets rather than deciding, so a builder without an asset manager never
|
|
92912
|
+
* shows a button that cannot open one.
|
|
92913
|
+
*
|
|
92914
|
+
* const [ ...buttons ] = builder.createFileButtons('media', (url) => {...});
|
|
92915
|
+
* row.append(input, ...buttons);
|
|
92916
|
+
*
|
|
92917
|
+
* `type` may be a function for a field whose kind follows another control
|
|
92918
|
+
* (a Media Type select, say) — it is then read on each click.
|
|
92919
|
+
*/
|
|
92920
|
+
createFileButtons(type, callback) {
|
|
92921
|
+
const out = s => this.util.out(s);
|
|
92922
|
+
const fileType = () => typeof type === 'function' ? type() : type;
|
|
92923
|
+
const button = (label, icon, onClick) => {
|
|
92924
|
+
const btn = document.createElement('button');
|
|
92925
|
+
btn.type = 'button';
|
|
92926
|
+
btn.className = 'cbx-iconbtn';
|
|
92927
|
+
btn.title = label;
|
|
92928
|
+
btn.setAttribute('aria-label', label);
|
|
92929
|
+
btn.innerHTML = `<svg aria-hidden="true"><use xlink:href="#${icon}"></use></svg>`;
|
|
92930
|
+
btn.addEventListener('click', e => {
|
|
92931
|
+
e.preventDefault();
|
|
92932
|
+
onClick(btn);
|
|
92933
|
+
});
|
|
92934
|
+
return btn;
|
|
92935
|
+
};
|
|
92936
|
+
const buttons = [];
|
|
92937
|
+
if (this.hasFilePicker(fileType())) {
|
|
92938
|
+
buttons.push(button(out('Select'), 'icon-folder', btn => this.openFilePicker(fileType(), callback, btn)));
|
|
92939
|
+
}
|
|
92940
|
+
buttons.push(button(out('Upload'), 'icon-upload', () => this.openFileUpload(fileType(), callback)));
|
|
92941
|
+
return buttons;
|
|
92942
|
+
}
|
|
91940
92943
|
openAssetSelect(targetAssetType, callback, defaultValue) {
|
|
91941
92944
|
const inpUrl = document.createElement('input');
|
|
91942
92945
|
|