@innovastudio/contentbox 1.6.199 → 1.6.200

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.
@@ -27047,7 +27047,10 @@ class Box {
27047
27047
 
27048
27048
  this.html(element, html);
27049
27049
  }); // Box Setup
27050
+ // Same as Section.sectionDuplicate(): re-render the copy's code
27051
+ // elements so they get fresh ids and run again
27050
27052
 
27053
+ if (this.builder.editor.codeElement) this.builder.editor.codeElement.init(box);
27051
27054
  this.builder.boxSetup(box); // ContentBuilder Handling
27052
27055
 
27053
27056
  let containers = box.querySelectorAll('.is-builder');
@@ -61828,8 +61831,16 @@ class CodeElement {
61828
61831
  if (doc.getElementById('css-cb-code-element')) return;
61829
61832
  const style = doc.createElement('style');
61830
61833
  style.id = 'css-cb-code-element';
61834
+ /*
61835
+ No 'position' here. The editor must lay a block out exactly as the
61836
+ published page does, and a block whose content is absolutely
61837
+ positioned (a full-bleed scene in an overlay, say) resolves against
61838
+ its nearest positioned ancestor: making the block itself one put
61839
+ that content inside a 30px-tall box in the editor while the same
61840
+ markup filled the section on the page.
61841
+ */
61831
61842
  style.textContent = `
61832
- [${ATTR$1}] { min-height: 30px; position: relative; -webkit-user-select: none; user-select: none; }
61843
+ [${ATTR$1}] { min-height: 30px; -webkit-user-select: none; user-select: none; }
61833
61844
  `;
61834
61845
  doc.head.appendChild(style);
61835
61846
  }
@@ -65370,7 +65381,7 @@ function getOffset(element) {
65370
65381
  top: box.top + (window.pageYOffset - document.documentElement.clientTop)
65371
65382
  };
65372
65383
  }
65373
- var location = WINDOW.location;
65384
+ var location$1 = WINDOW.location;
65374
65385
  var REGEXP_ORIGINS = /^(\w+:)\/\/([^:/?#]*):?(\d*)/i;
65375
65386
  /**
65376
65387
  * Check if the given URL is a cross origin URL.
@@ -65380,7 +65391,7 @@ var REGEXP_ORIGINS = /^(\w+:)\/\/([^:/?#]*):?(\d*)/i;
65380
65391
 
65381
65392
  function isCrossOriginURL(url) {
65382
65393
  var parts = url.match(REGEXP_ORIGINS);
65383
- return parts !== null && (parts[1] !== location.protocol || parts[2] !== location.hostname || parts[3] !== location.port);
65394
+ return parts !== null && (parts[1] !== location$1.protocol || parts[2] !== location$1.hostname || parts[3] !== location$1.port);
65384
65395
  }
65385
65396
  /**
65386
65397
  * Add timestamp to the given URL.
@@ -127378,6 +127389,23 @@ ${this.buildHistoryContext()}`;
127378
127389
  * IMAGE PREVIEW HELPER
127379
127390
  * ============================================================================
127380
127391
  */
127392
+ /* Attachments shown under a user message, matching what the composer
127393
+ * renders when a file is attached for real. Demo playback only. */
127394
+ addUserAttachmentPreview(images) {
127395
+ const previewDiv = document.createElement('div');
127396
+ previewDiv.className = 'message user';
127397
+ let html = '<div class="message-content" style="display:flex;flex-wrap:wrap;gap:8px;padding:10px;">';
127398
+ images.forEach(img => {
127399
+ const url = this.escapeHtml(img.url || '');
127400
+ if (!url) return;
127401
+ const alt = this.escapeHtml(img.context || url.split('/').pop() || 'Attachment');
127402
+ html += `<img src="${url}" alt="${alt}" style="width:64px;height:64px;object-fit:cover;border-radius:6px;" />`;
127403
+ });
127404
+ html += '</div>';
127405
+ previewDiv.innerHTML = html;
127406
+ this.messagesContainer.appendChild(previewDiv);
127407
+ this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
127408
+ }
127381
127409
  addImagePreview(images) {
127382
127410
  const out = s => this.out(s);
127383
127411
  const previewDiv = document.createElement('div');
@@ -128928,9 +128956,12 @@ Return ONLY the edited section as a single <div class="is-section ...>. Do not o
128928
128956
  announcement.setAttribute('aria-live', 'polite');
128929
128957
  announcement.className = 'sr-only';
128930
128958
  announcement.textContent = message;
128931
- document.body.appendChild(announcement);
128959
+
128960
+ // Must live inside the builder container: the .sr-only rule is scoped
128961
+ // to [data-cb-ui], so elsewhere the text would be visible on screen.
128962
+ this.builderStuff.appendChild(announcement);
128932
128963
  setTimeout(() => {
128933
- document.body.removeChild(announcement);
128964
+ announcement.remove();
128934
128965
  }, 1000);
128935
128966
  }
128936
128967
 
@@ -128945,6 +128976,11 @@ Return ONLY the edited section as a single <div class="is-section ...>. Do not o
128945
128976
  this.demoConversations.forEach(msg => {
128946
128977
  if (msg.role === 'user') {
128947
128978
  this.addMessage('user', msg.content);
128979
+
128980
+ // Attachments the user sent with the message
128981
+ if (msg.imagePreview && msg.imagePreview.length > 0) {
128982
+ this.addUserAttachmentPreview(msg.imagePreview);
128983
+ }
128948
128984
  } else if (msg.role === 'assistant') {
128949
128985
  this.addMessage('assistant', msg.content, true);
128950
128986
 
@@ -129285,6 +129321,45 @@ Return ONLY the edited section as a single <div class="is-section ...>. Do not o
129285
129321
  }
129286
129322
  }
129287
129323
 
129324
+ /**
129325
+ * repair-markup.js
129326
+ * ----------------------------------------------------------------------------
129327
+ * Fixes applied to AI-written HTML on its way to the page, in the targets'
129328
+ * normalize() step.
129329
+ *
129330
+ * Only for defects that are unambiguous — a rule the framework states, where
129331
+ * the result is visibly broken and there is exactly one correct repair. Taste
129332
+ * belongs in the context documentation, not here: this must never turn one
129333
+ * valid design into another.
129334
+ * ----------------------------------------------------------------------------
129335
+ */
129336
+
129337
+ /*
129338
+ An outline button needs 'hover:border-transparent'.
129339
+
129340
+ 'border-current' makes the 2px border follow the text colour, hover colour
129341
+ included, so on hover the border becomes a visible ring and the hover fill
129342
+ stops inside it — the button appears to shrink. The class lets the border take
129343
+ the hover background instead.
129344
+
129345
+ Left alone: a button that sets its own hover border (any 'hover:border-*'
129346
+ class, or 'border' listed in data-cb-hover), which is a deliberate choice the
129347
+ class would override.
129348
+ */
129349
+ function repairButtons$1(root) {
129350
+ if (!root || !root.querySelectorAll) return 0;
129351
+ let fixed = 0;
129352
+ root.querySelectorAll('a[role="button"], button').forEach(el => {
129353
+ if (!el.classList.contains('border-current')) return;
129354
+ if ([...el.classList].some(c => c.startsWith('hover:border-'))) return;
129355
+ const hover = (el.getAttribute('data-cb-hover') || '').split(/\s+/);
129356
+ if (hover.includes('border')) return;
129357
+ el.classList.add('hover:border-transparent');
129358
+ fixed++;
129359
+ });
129360
+ return fixed;
129361
+ }
129362
+
129288
129363
  /**
129289
129364
  * target-section.js
129290
129365
  * ----------------------------------------------------------------------------
@@ -129299,7 +129374,6 @@ Return ONLY the edited section as a single <div class="is-section ...>. Do not o
129299
129374
  * products run the same engine instead of a copy of it.
129300
129375
  * ----------------------------------------------------------------------------
129301
129376
  */
129302
-
129303
129377
  function createSectionTarget$1(engine) {
129304
129378
  const b = () => engine.builder;
129305
129379
  const nodes = () => b().getSectionNodes();
@@ -129330,7 +129404,7 @@ function createSectionTarget$1(engine) {
129330
129404
 
129331
129405
  /* Reference the host actually supplied, in the order it is offered. */
129332
129406
  const docTopics = () => {
129333
- const t = ['box', 'animation'].filter(k => engine.contextParts[k]);
129407
+ const t = ['box', 'animation', '3d', 'games', 'code'].filter(k => engine.contextParts[k]);
129334
129408
  if (motionPathMode()) t.push('motionPath');
129335
129409
  return t;
129336
129410
  };
@@ -129348,6 +129422,7 @@ function createSectionTarget$1(engine) {
129348
129422
  const found = holder.querySelectorAll('.is-section');
129349
129423
  if (found.length !== 1) return null;
129350
129424
  const section = found[0];
129425
+ repairButtons$1(section);
129351
129426
  section.querySelectorAll('div.is-container').forEach(c => {
129352
129427
  if (![...c.classList].some(x => /^size-\d/.test(x))) c.classList.add('size-17');
129353
129428
  if (![...c.classList].some(x => /^leading-\d/.test(x))) c.classList.add('leading-17');
@@ -129474,7 +129549,7 @@ Sections are <div class="is-section ...">. Use ONLY documented classes — this
129474
129549
  ${onDemand.length ? `
129475
129550
  REFERENCE YOU MUST LOAD WHEN RELEVANT
129476
129551
  The documentation below covers grid, typography, spacing and colour. Further reference is available through get_framework_docs and is NOT included here:
129477
- ${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.` : ''}`;
129552
+ ${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.` : ''}`;
129478
129553
  },
129479
129554
  /* Structural tools. The engine adds the attachment and image tools. */
129480
129555
  tools() {
@@ -129546,7 +129621,7 @@ ${onDemand.includes('box') ? '- "box": section and box structure — load before
129546
129621
  }
129547
129622
  }, ...(topics.length ? [{
129548
129623
  name: 'get_framework_docs',
129549
- 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.`,
129624
+ 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.`,
129550
129625
  parameters: {
129551
129626
  type: 'object',
129552
129627
  properties: {
@@ -129746,7 +129821,7 @@ function createRowTarget$1(engine) {
129746
129821
  const limit = () => engine.limits.toolResult;
129747
129822
 
129748
129823
  /* Reference the host actually supplied, in the order it is offered. */
129749
- const docTopics = () => ['buttons', 'animation'].filter(k => engine.contextParts[k]);
129824
+ const docTopics = () => ['buttons', 'animation', '3d', 'games', 'code'].filter(k => engine.contextParts[k]);
129750
129825
 
129751
129826
  /*
129752
129827
  Exactly one .row, or null. Markup arrives fenced as often as not, and a
@@ -129760,6 +129835,7 @@ function createRowTarget$1(engine) {
129760
129835
  holder.innerHTML = html.replace(/```html\n?/g, '').replace(/```\n?/g, '').trim();
129761
129836
  const found = Array.from(holder.children).filter(el => el.classList.contains('row'));
129762
129837
  if (found.length !== 1) return null;
129838
+ repairButtons$1(found[0]);
129763
129839
  return found[0].outerHTML;
129764
129840
  }
129765
129841
  return {
@@ -130418,6 +130494,78 @@ ${docs}`;
130418
130494
  }
130419
130495
  });
130420
130496
  }
130497
+
130498
+ /*
130499
+ 3D asset generation (image-to-3D). Offered only when the host lists
130500
+ models3D — the same opt-in convention as the image tools. One image in,
130501
+ one .glb URL out; the model then loads it with GLTFLoader per the "3d"
130502
+ reference doc.
130503
+ */
130504
+ if (this.threeDConfig()) {
130505
+ const threeDParams = {
130506
+ image_url: {
130507
+ type: 'string',
130508
+ description: 'URL of the source image: a photo on the page, or a generate_image result.'
130509
+ }
130510
+ };
130511
+ if ((this.currentAttachments || []).some(a => a && a.kind === 'image')) {
130512
+ threeDParams.attachment = {
130513
+ type: 'integer',
130514
+ description: 'Use an attached image as the source instead of image_url, numbered from 1.'
130515
+ };
130516
+ }
130517
+ t.push({
130518
+ name: 'generate_3d_model',
130519
+ 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>.',
130520
+ parameters: {
130521
+ type: 'object',
130522
+ properties: threeDParams,
130523
+ required: []
130524
+ }
130525
+ });
130526
+ }
130527
+
130528
+ /*
130529
+ The critic loop: the model can SEE what a code block's canvas actually
130530
+ renders and fix visual problems before answering — the difference
130531
+ between shipping a black scene and noticing it. Offered only while the
130532
+ page holds a canvas inside a code block, so ordinary requests never
130533
+ see it.
130534
+ */
130535
+ /* Gated on the block, not its canvas — right after an insert the
130536
+ scene is still importing modules and the canvas does not exist yet,
130537
+ which is exactly when the model needs the tool in its list.
130538
+ captureRender does the waiting. */
130539
+ /*
130540
+ Only a model that reads images can act on the screenshot — for a
130541
+ text-only model the injected image is a provider error, so the tool is
130542
+ simply not offered and the docs' "when a view_render tool is offered"
130543
+ phrasing degrades the whole flow gracefully. Same capability registry
130544
+ the attach button uses.
130545
+ */
130546
+ /*
130547
+ Off unless the host turns it on: the self-check costs an extra
130548
+ vision round-trip per scene, so it is opt-in per site.
130549
+ */
130550
+ const selfCheckOn = !!(this.builder && this.builder.renderSelfCheck);
130551
+ const seesImages = resolveFileCapability$1(this.model, this.v1 && this.v1.codeModels || this.builder && this.builder.codeModels || []).accepts.includes('image');
130552
+ if (selfCheckOn && seesImages && this.builder && this.builder.doc && this.builder.doc.querySelector('[data-cb-code]')) {
130553
+ t.push({
130554
+ name: 'view_render',
130555
+ 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.',
130556
+ parameters: {
130557
+ type: 'object',
130558
+ properties: {
130559
+ block: {
130560
+ type: 'integer',
130561
+ description: '1-based index among the page\'s canvas-bearing code blocks; omit for the first',
130562
+ default: 1
130563
+ }
130564
+ },
130565
+ required: []
130566
+ }
130567
+ });
130568
+ }
130421
130569
  return t;
130422
130570
  }
130423
130571
 
@@ -130468,6 +130616,14 @@ ${docs}`;
130468
130616
  {
130469
130617
  return await this.editImage(args);
130470
130618
  }
130619
+ case 'generate_3d_model':
130620
+ {
130621
+ return await this.generate3DModel(args);
130622
+ }
130623
+ case 'view_render':
130624
+ {
130625
+ return await this.captureRender(args);
130626
+ }
130471
130627
  default:
130472
130628
  return {
130473
130629
  error: `unknown tool ${name}`
@@ -130487,6 +130643,12 @@ ${docs}`;
130487
130643
 
130488
130644
  async send(text, attachments = []) {
130489
130645
  this.abort = new AbortController();
130646
+ // Per-request ceilings on the expensive/loopable tools. MAX_STEPS
130647
+ // bounds the loop as a whole; these stop a model from burning paid
130648
+ // generations or endless self-checks chasing a critique it cannot
130649
+ // satisfy — past the cap the tool refuses and tells it to finish.
130650
+ this.spent3D = 0;
130651
+ this.spentShots = 0;
130490
130652
  this.beginBatch(); // one undo entry for the whole request
130491
130653
  this.currentAttachments = attachments || [];
130492
130654
  this.attachmentUrls = {};
@@ -130547,6 +130709,30 @@ ${docs}`;
130547
130709
  content: JSON.stringify(result)
130548
130710
  });
130549
130711
  }
130712
+
130713
+ /*
130714
+ view_render screenshots cannot ride in the tool message —
130715
+ tool results are text on every provider — so they follow as a
130716
+ user message. dropAttachmentPayloads compacts them on the next
130717
+ request like any other image, so they never accumulate.
130718
+ */
130719
+ if (this.pendingShots && this.pendingShots.length) {
130720
+ const parts = [{
130721
+ type: 'text',
130722
+ text: 'Screenshot of the current render:'
130723
+ }];
130724
+ this.pendingShots.forEach(u => parts.push({
130725
+ type: 'image_url',
130726
+ image_url: {
130727
+ url: u
130728
+ }
130729
+ }));
130730
+ messages.push({
130731
+ role: 'user',
130732
+ content: parts
130733
+ });
130734
+ this.pendingShots = [];
130735
+ }
130550
130736
  }
130551
130737
 
130552
130738
  // Ran out of steps. Not an error the user should see as a crash.
@@ -130982,6 +131168,267 @@ ${docs}`;
130982
131168
  freshOutline() {
130983
131169
  return this.target.outline();
130984
131170
  }
131171
+
131172
+ /*
131173
+ Screenshot a code block's canvas for the critic loop. Waits for the canvas
131174
+ to exist and paint (module imports take a moment after an insert), then
131175
+ downscales to keep the image cheap. Requires the scene's renderer to have
131176
+ preserveDrawingBuffer: true (the 3d reference's shell sets it) — without
131177
+ it a WebGL canvas reads back blank between frames.
131178
+ */
131179
+ async captureRender(args = {}) {
131180
+ const doc = this.builder && this.builder.doc;
131181
+ if (!doc) return {
131182
+ error: 'no page'
131183
+ };
131184
+ this.spentShots = (this.spentShots || 0) + 1;
131185
+ if (this.spentShots > 6) {
131186
+ return {
131187
+ 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.'
131188
+ };
131189
+ }
131190
+ const n = Math.max(1, parseInt(args.block, 10) || 1);
131191
+ let canvas = null;
131192
+ for (let i = 0; i < 24; i++) {
131193
+ const canvases = doc.querySelectorAll('[data-cb-code] canvas');
131194
+ canvas = canvases[n - 1] || null;
131195
+ if (canvas && canvas.width > 2 && canvas.height > 2) break;
131196
+ await new Promise(r => setTimeout(r, 500));
131197
+ if (this.abort && this.abort.signal.aborted) throw Object.assign(new Error('aborted'), {
131198
+ name: 'AbortError'
131199
+ });
131200
+ }
131201
+ if (!canvas) return {
131202
+ error: 'no rendering canvas found in a code block (is the scene failing before it creates one?)'
131203
+ };
131204
+ await new Promise(r => setTimeout(r, 1500)); // let the first real frames draw
131205
+
131206
+ try {
131207
+ const scale = Math.min(1, 768 / canvas.width);
131208
+ const w = Math.max(1, Math.round(canvas.width * scale));
131209
+ const h = Math.max(1, Math.round(canvas.height * scale));
131210
+ const off = document.createElement('canvas');
131211
+ off.width = w;
131212
+ off.height = h;
131213
+ const ctx = off.getContext('2d');
131214
+ ctx.drawImage(canvas, 0, 0, w, h);
131215
+
131216
+ // A uniform image means nothing drew (or the renderer lacks
131217
+ // preserveDrawingBuffer) — say so instead of sending a blank.
131218
+ const d = ctx.getImageData(0, 0, w, h).data;
131219
+ let uniform = true;
131220
+ for (let i = 4; i < d.length; i += 997 * 4) {
131221
+ if (d[i] !== d[0] || d[i + 1] !== d[1] || d[i + 2] !== d[2]) {
131222
+ uniform = false;
131223
+ break;
131224
+ }
131225
+ }
131226
+ const dataUrl = off.toDataURL('image/jpeg', 0.8);
131227
+ this.pendingShots = this.pendingShots || [];
131228
+ this.pendingShots.push(dataUrl);
131229
+ if (uniform) {
131230
+ return {
131231
+ ok: true,
131232
+ 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.'
131233
+ };
131234
+ }
131235
+ return {
131236
+ ok: true,
131237
+ note: 'screenshot captured — it follows as an image in the next message. Look at it before answering.'
131238
+ };
131239
+ } catch (e) {
131240
+ return {
131241
+ error: 'could not capture the canvas: ' + String(e && e.message || e)
131242
+ };
131243
+ }
131244
+ }
131245
+
131246
+ /*
131247
+ 3D generation configuration, resolved per call so a host can switch
131248
+ provider at runtime the same way image generation can. Mirrors v1's
131249
+ provider resolution: endpoint options are looked up as
131250
+ `<base>_<Provider>` in any casing, falling back to the _Fal names.
131251
+ Today every supported provider is queue-shaped (request/status/result);
131252
+ a future single-call 3D provider gets its own branch in generate3DModel,
131253
+ the way SINGLE_CALL_IMAGE_PROVIDERS did for images.
131254
+ */
131255
+ threeDConfig() {
131256
+ const b = this.builder;
131257
+ if (!b) return null;
131258
+ const models = Array.isArray(b.models3D) ? b.models3D.filter(m => m && m.id) : [];
131259
+ if (!models.length) return null;
131260
+ const provider = b.default3DGenerationProvider || 'fal';
131261
+ const val = base => {
131262
+ const wanted = `${base}_${provider}`.toLowerCase();
131263
+ const key = Object.keys(b).find(k => k.toLowerCase() === wanted && b[k]);
131264
+ return key ? b[key] : undefined;
131265
+ };
131266
+ const generateUrl = val('generateMediaUrl') || b.generateMediaUrl_Fal;
131267
+ const statusUrl = val('checkRequestStatusUrl') || b.checkRequestStatusUrl_Fal;
131268
+ const resultUrl = val('getResultUrl') || b.getResultUrl_Fal;
131269
+ if (!generateUrl || !statusUrl || !resultUrl) return null;
131270
+
131271
+ // The saved selection, snapping to the first entry when it is not listed
131272
+ const saved = this.v1 && this.v1.settings && this.v1.settings.model3D;
131273
+ const model = (models.find(m => m.id === saved) || models[0]).id;
131274
+ return {
131275
+ provider,
131276
+ models,
131277
+ model,
131278
+ generateUrl,
131279
+ statusUrl,
131280
+ resultUrl
131281
+ };
131282
+ }
131283
+
131284
+ /*
131285
+ Image-to-3D. The image reaches the provider the same way reference images
131286
+ do for image editing: data URLs go through as-is, same-origin files are
131287
+ read into a data URL (the provider cannot fetch a private host), and
131288
+ public URLs are passed for the provider to fetch itself.
131289
+ */
131290
+ async generate3DModel(args = {}) {
131291
+ const cfg = this.threeDConfig();
131292
+ if (!cfg) return {
131293
+ error: '3D generation is not configured'
131294
+ };
131295
+ this.spent3D = (this.spent3D || 0) + 1;
131296
+ if (this.spent3D > 4) {
131297
+ return {
131298
+ 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.'
131299
+ };
131300
+ }
131301
+ const signal = this.abort ? this.abort.signal : null;
131302
+
131303
+ // Resolve the source image
131304
+ let src = null;
131305
+ if (args.attachment !== undefined && args.attachment !== null) {
131306
+ const list = (this.currentAttachments || []).filter(a => a && a.kind === 'image');
131307
+ const att = list[(parseInt(args.attachment, 10) || 1) - 1];
131308
+ if (!att) return {
131309
+ error: `there ${list.length === 1 ? 'is 1 attached image' : `are ${list.length} attached images`}`
131310
+ };
131311
+ src = att.dataUrl;
131312
+ } else if (args.image_url) {
131313
+ src = this.expandAttachmentUrls(String(args.image_url));
131314
+ if (!/^data:/i.test(src)) {
131315
+ let abs;
131316
+ try {
131317
+ abs = new URL(src, location.href);
131318
+ } catch (e) {
131319
+ return {
131320
+ error: 'image_url is not a valid URL'
131321
+ };
131322
+ }
131323
+ if (abs.origin === location.origin) {
131324
+ try {
131325
+ const blob = await (await fetch(abs.href, {
131326
+ signal
131327
+ })).blob();
131328
+ src = await new Promise((resolve, reject) => {
131329
+ const r = new FileReader();
131330
+ r.onload = () => resolve(r.result);
131331
+ r.onerror = reject;
131332
+ r.readAsDataURL(blob);
131333
+ });
131334
+ } catch (e) {
131335
+ return {
131336
+ error: 'could not read the image at image_url'
131337
+ };
131338
+ }
131339
+ } else {
131340
+ src = abs.href; // public URL — the provider fetches it
131341
+ }
131342
+ }
131343
+ } else {
131344
+ return {
131345
+ error: 'give image_url or attachment'
131346
+ };
131347
+ }
131348
+ const b = this.builder;
131349
+ const headers = {
131350
+ ...b.headers,
131351
+ ...b.defaultHeaders
131352
+ };
131353
+ const customData = b.editor && b.editor.customData || {};
131354
+ const post = async (url, body) => {
131355
+ const response = await fetch(url, {
131356
+ method: 'POST',
131357
+ headers,
131358
+ signal,
131359
+ body: JSON.stringify(body)
131360
+ });
131361
+ return await response.json();
131362
+ };
131363
+
131364
+ // Each model family names its input field differently
131365
+ const payload = /hunyuan/i.test(cfg.model) ? {
131366
+ input_image_url: src
131367
+ } : {
131368
+ image_urls: [src],
131369
+ geometry_file_format: 'glb'
131370
+ }; // Rodin-shaped default
131371
+
131372
+ const firstTwoParts = cfg.model.split('/').slice(0, 2).join('/');
131373
+ const req = await post(cfg.generateUrl, {
131374
+ model: cfg.model,
131375
+ customData,
131376
+ endpoint: `https://queue.fal.run/${cfg.model}`,
131377
+ payload
131378
+ });
131379
+ if (req.error || !req.request_id) {
131380
+ return {
131381
+ error: typeof req.error === 'string' ? req.error : '3D generation request failed'
131382
+ };
131383
+ }
131384
+ const request_id = req.request_id;
131385
+ const statusBody = {
131386
+ model: cfg.model,
131387
+ endpoint: `https://queue.fal.run/${firstTwoParts}/requests/${request_id}/status`,
131388
+ request_id,
131389
+ customData
131390
+ };
131391
+ let status = '';
131392
+ for (let i = 0; i < 150 && status !== 'COMPLETED'; i++) {
131393
+ const st = await post(cfg.statusUrl, statusBody);
131394
+ if (st.ok === false) return {
131395
+ error: st.error || '3D generation failed'
131396
+ };
131397
+ if (st.ok && st.status) {
131398
+ status = st.status;
131399
+ if (status === 'FAILED') return {
131400
+ error: '3D generation failed'
131401
+ };
131402
+ } else if (st.data) {
131403
+ // API-style status (mirrors the image flow's fallback)
131404
+ 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;
131405
+ }
131406
+ if (status !== 'COMPLETED') await new Promise(r => setTimeout(r, 2000));
131407
+ if (signal && signal.aborted) throw Object.assign(new Error('aborted'), {
131408
+ name: 'AbortError'
131409
+ });
131410
+ }
131411
+ if (status !== 'COMPLETED') return {
131412
+ error: '3D generation timed out'
131413
+ };
131414
+ const res = await post(cfg.resultUrl, {
131415
+ model: cfg.model,
131416
+ request_id,
131417
+ customData,
131418
+ endpoint: `https://queue.fal.run/${firstTwoParts}/requests/${request_id}`
131419
+ });
131420
+ if (res.error) return {
131421
+ error: typeof res.error === 'string' ? res.error : '3D result failed'
131422
+ };
131423
+ const entry = res.data && res.data.entries && res.data.entries[0];
131424
+ if (!entry || !entry.url) return {
131425
+ error: 'the generation returned no model file'
131426
+ };
131427
+ return {
131428
+ url: entry.url,
131429
+ format: 'glb'
131430
+ };
131431
+ }
130985
131432
  async generateImages(prompt, count = 1, size) {
130986
131433
  if (!this.v1) throw new Error('image generation is not configured');
130987
131434
  const signal = this.abort ? this.abort.signal : null;
@@ -131148,6 +131595,10 @@ function describeStep$1(tool, args, out, target) {
131148
131595
  return out('Reading the attachment') + '…';
131149
131596
  case 'generate_image':
131150
131597
  return out('Generating image') + '…';
131598
+ case 'generate_3d_model':
131599
+ return out('Generating 3D model') + '…';
131600
+ case 'view_render':
131601
+ return out('Checking the render') + '…';
131151
131602
  case 'edit_image':
131152
131603
  return out('Editing image') + '…';
131153
131604
  case '__waiting':
@@ -131409,6 +131860,55 @@ function attachCodeChatV2$1(codechat, builder, opts = {}) {
131409
131860
  v2.stop();
131410
131861
  if (origAbort) origAbort();
131411
131862
  };
131863
+
131864
+ /*
131865
+ Settings dialog: the 3D model picker. Injected here rather than written
131866
+ into the panel because the panel belongs to v1 and 3D generation is a v2
131867
+ feature — the same borrowing pattern as sendMessage above. The group
131868
+ exists only when the host configured models3D; with one entry it simply
131869
+ shows what is active.
131870
+ */
131871
+ const t3d = v2.threeDConfig && v2.threeDConfig();
131872
+ if (t3d && codechat.builderStuff && !codechat.builderStuff.querySelector('#model3DSelect')) {
131873
+ const anchor = codechat.builderStuff.querySelector('.div-image-generation-options');
131874
+ if (anchor) {
131875
+ const group = document.createElement('div');
131876
+ group.className = 'setting-group';
131877
+ group.innerHTML = `
131878
+ <label for="model3DSelect" class="setting-label">${out('3D Model')}</label>
131879
+ <p class="setting-description">${out('Select the AI model used for 3D model generation')}</p>
131880
+ <div class="cb-sel"><select id="model3DSelect"></select></div>`;
131881
+ const sel = group.querySelector('#model3DSelect');
131882
+ t3d.models.forEach(m => {
131883
+ const opt = document.createElement('option');
131884
+ opt.value = m.id;
131885
+ opt.textContent = m.label || m.id;
131886
+ sel.appendChild(opt);
131887
+ });
131888
+ anchor.parentNode.insertBefore(group, anchor.nextSibling);
131889
+
131890
+ // Current value on every open; snap to the first when stale
131891
+ const origOpen = codechat.openSettings ? codechat.openSettings.bind(codechat) : null;
131892
+ if (origOpen) {
131893
+ codechat.openSettings = function () {
131894
+ origOpen();
131895
+ const cfg = v2.threeDConfig();
131896
+ sel.value = cfg && cfg.model || '';
131897
+ if (sel.selectedIndex < 0 && sel.options.length) sel.selectedIndex = 0;
131898
+ };
131899
+ }
131900
+
131901
+ // Persist with the rest of the settings: set before the panel's
131902
+ // own save stringifies this.settings.
131903
+ const origSave = codechat.saveSettings ? codechat.saveSettings.bind(codechat) : null;
131904
+ if (origSave) {
131905
+ codechat.saveSettings = function () {
131906
+ codechat.settings.model3D = sel.value;
131907
+ origSave();
131908
+ };
131909
+ }
131910
+ }
131911
+ }
131412
131912
  return v2;
131413
131913
  }
131414
131914
 
@@ -131438,7 +131938,7 @@ All content lives in rows and columns — NEVER place content directly under a c
131438
131938
 
131439
131939
  - 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.
131440
131940
  - Weight: font-light font-normal font-medium font-semibold font-bold
131441
- - Tracking: tracking-tight tracking-normal tracking-wide tracking-wider tracking-150 (ideal for uppercase labels)
131941
+ - 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.
131442
131942
  - 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
131443
131943
  - 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.
131444
131944
 
@@ -131576,184 +132076,591 @@ Icons: use Bootstrap Icons (NOT emojis), wrapped in a div: <div class="text-cent
131576
132076
  - Minimalist: generous whitespace; palette of black/white/grays; light heading weights; hierarchy through size and spacing, not color.
131577
132077
  `;
131578
132078
 
131579
- const contextCodeBlock = `
131580
- # Custom Code Block
131581
-
131582
- Use Code Block for creating interactive elements that need JavaScript.
132079
+ /*
132080
+ * context-code.js — the code element reference for Code Chat.
132081
+ *
132082
+ * Documents the RELEASED code element ([data-cb-code], see
132083
+ * contentbuilder/docs/code-element-guide.md and codeelement.js). The previous
132084
+ * version of this file described an old experimental contract
132085
+ * (data-cb-type="code" + container.mount) that never shipped.
132086
+ *
132087
+ * Sent on every v1 request and served as the on-demand 'code' topic in v2,
132088
+ * so it is kept short. No backticks or ${ } — template literal.
132089
+ *
132090
+ * Takes the host's options because where a block may sit differs by product:
132091
+ * ContentBox also has overlay layers, ContentBuilder does not. One file, one
132092
+ * wording, and neither product is told about markup it has no use for.
132093
+ */
131583
132094
 
131584
- Code Block can only be placed:
132095
+ function getContextCodeBlock$1(opts = {}) {
132096
+ // Where a block may sit. ContentBox also has overlay layers; ContentBuilder
132097
+ // has columns only, and must not be told about markup it does not have.
132098
+ const placement = opts.overlay ? `It sits inside a column,
132099
+ beside other content, or inside '.is-overlay > .is-overlay-content'. To fill
132100
+ that layer, give the block 'position:absolute;inset:0' — the overlay is
132101
+ full-screen, the block is not.` : `It sits
132102
+ inside a column, beside other content.`;
132103
+ return `
132104
+ # Code Block
131585
132105
 
131586
- - in content (inside div.column)
131587
- - in overlay (must be in div.is-overlay-content)
132106
+ For elements that need JavaScript, use a code block: <div data-cb-code>
132107
+ (that attribute alone marks it — no class, no other bookkeeping). ${placement} The code runs live while editing and
132108
+ runs again on the saved page.
131588
132109
 
131589
- **In Content**
132110
+ A simple block is complete as written — no wrapper, no ceremony:
131590
132111
 
131591
- <div class="row">
131592
- <div class="column">
131593
- <div data-cb-type="code">
131594
- <!-- Your Code -->
131595
- </div>
131596
- </div>
132112
+ <div data-cb-code>
132113
+ <h1 id="greet7x">Lorem ipsum</h1>
132114
+ <p>This is a code block.</p>
132115
+ <script>
132116
+ document.querySelector('#greet7x').innerHTML = '<b>Hello World..!</b>';
132117
+ </script>
131597
132118
  </div>
131598
132119
 
131599
- **In Overlay**
132120
+ Ids are safe: when a block is duplicated, the editor gives the copy fresh ids
132121
+ and updates the references inside its script to match.
131600
132122
 
131601
- <div class="is-section is-box is-section-100 type-system-ui">
131602
- <div class="is-overlay">
131603
- <div class="is-overlay-content">
131604
- <div data-cb-type="code" style="position: absolute; top:0, left:0; width: 100%; height: 100%">
131605
- <!-- Your Code -->
131606
- </div>
131607
- </div>
131608
- </div>
131609
-
131610
- <!-- ... -->
131611
- </div>
132123
+ For blocks that do more, these rules keep them working through editing:
132124
+
132125
+ - The editor re-renders the block on edit, undo and duplicate, and each
132126
+ render runs the code again on FRESH elements. Anything that keeps running
132127
+ — a timer, an interval, a window/document listener, an animation loop —
132128
+ must stop by itself when its elements leave the page. Check isConnected:
131612
132129
 
131613
- ### 1. Basic Example
132130
+ const display = document.querySelector('#clock9k');
132131
+ const iv = setInterval(() => {
132132
+ if (!display.isConnected) { clearInterval(iv); return; }
132133
+ display.textContent = new Date().toLocaleTimeString();
132134
+ }, 1000);
131614
132135
 
131615
- **Important**: Always Use 'data-cb-type="code"' and create 'mount' function.
132136
+ - Plain classic <script> tags only — never type="module". To load a library,
132137
+ use dynamic import() inside ONE async IIFE wrapping the whole script,
132138
+ closed with exactly one })(); at the end — never nest a second wrapper:
131616
132139
 
131617
- <div data-cb-type="code">
131618
- <h3 class="code-block__title font-normal size-24">Code Block</h3>
131619
- <p>This is a custom code block. Open HTML code editor to edit this block.</p>
131620
132140
  <script>
131621
- (function() {
131622
- // Get reference to this specific script element
131623
- const currentScript = document.currentScript;
131624
- // Navigate to the parent container
131625
- const container = currentScript.parentElement;
131626
-
131627
- const initialize = async () => {
131628
- // Find the title within THIS container only
131629
- const title = container.querySelector('.code-block__title');
131630
- if (title) {
131631
- title.textContent = 'Code Block';
131632
- }
131633
- };
131634
-
131635
- // Create mount function and store it on the container
131636
- // for the Runtime to render the plugin
131637
- container.mount = function() {
131638
- initialize(); // Start initialization
131639
- };
131640
- // If no runtime, just start initialization
131641
- if (!window.builderRuntime) {
131642
- initialize();
131643
- }
132141
+ (async () => {
132142
+ const confetti = (await import('https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/+esm')).default;
132143
+ // ... the rest of the block's code ...
131644
132144
  })();
131645
132145
  </script>
131646
- </div>
131647
132146
 
131648
- ### 2. Inline '<style>' and Dynamic External Script Loading
131649
- ` + `
131650
- <div data-cb-type="code">
131651
- <style>
132147
+ A classic library that defines a global loads with a created <script>
132148
+ element (check for an existing one first); CSS files the same way with a
132149
+ <link rel="stylesheet">. If you read document.currentScript, do it on the
132150
+ first line — it is null after the first await.
131652
132151
 
131653
- </style>
131654
-
131655
- <div class="swiper swiper-initialized swiper-horizontal">
131656
-
131657
- </div>
132152
+ - Self-contained only: styles inline or in a <style> tag inside the block;
132153
+ never touch document-level styles or elements outside the block.
132154
+ `;
132155
+ }
131658
132156
 
131659
- <script>
131660
- (function() {
131661
- const currentScript = document.currentScript;
131662
- const container = currentScript.parentElement;
131663
-
131664
- // Dynamic loader for Swiper CSS
131665
- const loadSwiperCSS = () => {
131666
- return new Promise((resolve, reject) => {
131667
- // Check if CSS already loaded
131668
- const existingLink = document.querySelector('link[href*="swiper-bundle.min.css"]');
131669
- if (existingLink) {
131670
- resolve();
131671
- return;
131672
- }
132157
+ /*
132158
+ * context-3d.js — the "3d" on-demand doc topic for Code Chat v2.
132159
+ *
132160
+ * Loaded via get_framework_docs ONLY when a request involves real-time 3D:
132161
+ * a three.js scene, WebGL content, a 3D product view, or a browser game.
132162
+ * It is never part of the resident system prompt — 3D requests are rare and
132163
+ * this reference is long, so it rides the same on-demand rail as "animation".
132164
+ *
132165
+ * The recipes here were verified in the ContentBuilder public/test-3d
132166
+ * experiments (Sept 2026): the quality shell, sky-as-light, instanced grass,
132167
+ * and the glb normalize pattern are lifted from working pages, not invented.
132168
+ * Keep code samples free of backticks and ${ } — this is a template literal.
132169
+ */
131673
132170
 
131674
- const link = document.createElement('link');
131675
- link.rel = 'stylesheet';
131676
- link.href = 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css';
131677
- link.onload = () => resolve();
131678
- link.onerror = reject;
131679
- document.head.appendChild(link);
131680
- });
131681
- };
132171
+ const context3D$1 = `
132172
+ # Real-Time 3D & Games (three.js)
131682
132173
 
131683
- // Dynamic loader for Swiper JS
131684
- const loadSwiper = () => {
131685
- return new Promise((resolve, reject) => {
131686
- if (window.Swiper) {
131687
- resolve(window.Swiper);
131688
- return;
131689
- }
131690
- const script = document.createElement('script');
131691
- script.src = 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js';
131692
- script.onload = () => resolve(window.Swiper);
131693
- script.onerror = reject;
131694
- document.head.appendChild(script);
131695
- });
131696
- };
132174
+ Use this whenever the user asks for a 3D scene, 3D object, WebGL, a 3D
132175
+ product/hero visual, or a playable browser game. Everything lives in a code
132176
+ element (data-cb-code), which runs live in the editor and as plain HTML on
132177
+ the published page.
131697
132178
 
131698
- // Initialize plugin
131699
- const initializeSlider = async () => {
131700
- try {
131701
- // Load dependencies
131702
- await loadSwiperCSS();
131703
- const Swiper = await loadSwiper();
132179
+ A finished-looking scene is mostly LIGHTING, not geometry. Never ship the
132180
+ naive setup (ambient + directional light, no environment, no tone mapping) —
132181
+ it makes every material look like flat plastic. Always use the Scene Shell
132182
+ below.
131704
132183
 
131705
- // ...
132184
+ ## 1. The block
131706
132185
 
131707
- } catch (error) {
131708
- console.error('Failed to initialize slider:', error);
131709
- }
131710
- };
132186
+ 3D lives in a code element: <div data-cb-code> (that attribute alone marks
132187
+ it; no class, no other bookkeeping). It sits inside a normal column, beside
132188
+ other content. The code runs LIVE while editing and runs again on the saved
132189
+ page, so what the user sees in the editor is the real scene.
131711
132190
 
131712
- // Create mount function and store it on the container
131713
- // for the Runtime to render the plugin
131714
- container.mount = function() {
131715
- initializeSlider(); // Start initialization
131716
- };
131717
- // If no runtime, just start initialization
131718
- if (!window.builderRuntime) {
131719
- initializeSlider();
131720
- }
132191
+ <div data-cb-code>
132192
+ <div class="scene3d-stage" style="width:100%; aspect-ratio:16/9; position:relative; overflow:hidden; border-radius:8px; background:#101014;"></div>
132193
+ <script>
132194
+ (async () => {
132195
+ const container = document.currentScript.parentElement; // BEFORE any await
132196
+ const stage = container.querySelector('.scene3d-stage');
132197
+ // ... imports, then build the scene (see Scene Shell) ...
131721
132198
  })();
131722
132199
  </script>
131723
132200
  </div>
131724
132201
 
131725
- Remember: The goal is to create code blocks that are **self-contained**, **conflict-free**, and **easy to integrate** into any HTML environment. Follow these patterns for consistent, reliable code block development.
131726
-
131727
- ### Important:
131728
-
131729
- **Code Block can only be placed**:
131730
-
131731
- - in content (inside div.column)
131732
- - in overlay (must be in div.is-overlay-content)
132202
+ ONE async IIFE wraps the whole script — do not nest a second wrapper
132203
+ function, and close it with exactly one })(); at the end. Read
132204
+ document.currentScript on the first line: it is only set before the first
132205
+ await.
132206
+
132207
+ Rules that keep it healthy in the editor:
132208
+ - Plain classic <script> tags only — never type="module" (dynamic import()
132209
+ inside the classic script is the way to load modules).
132210
+ - Query elements from the container (document.currentScript.parentElement),
132211
+ not by id — the block can be duplicated, and container-scoped queries stay
132212
+ correct in every copy.
132213
+ - The editor re-renders the block on edit, undo and duplicate, and each
132214
+ render runs the code again on FRESH elements. Anything that keeps running
132215
+ — the render loop, timers, window/document listeners — must stop by itself
132216
+ when its elements leave the page. The shell's loop below does this with an
132217
+ isConnected check; apply the same rule to any setInterval or listener you
132218
+ add outside the loop.
132219
+ - The stage div is styled inline (width 100% + aspect-ratio); make it taller
132220
+ or full-bleed by changing that style, not by touching anything outside the
132221
+ block.
132222
+
132223
+ ## 2. Loading three.js
132224
+
132225
+ Load as ES modules with dynamic import inside the script. Use jsdelivr's
132226
+ +esm build and PIN the version — addons then resolve their internal 'three'
132227
+ import to the same module instance, so no import map is needed:
132228
+
132229
+ const THREE = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/+esm');
132230
+ const { OrbitControls } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/controls/OrbitControls.js/+esm');
132231
+ const { GLTFLoader } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/loaders/GLTFLoader.js/+esm');
132232
+ const { MeshoptDecoder } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/libs/meshopt_decoder.module.js/+esm');
132233
+ const { RoomEnvironment } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/environments/RoomEnvironment.js/+esm');
132234
+ const { Sky } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/objects/Sky.js/+esm');
132235
+
132236
+ Import only what the scene uses. Every addon lives under examples/jsm/ with
132237
+ the same /+esm suffix.
132238
+
132239
+ ## 3. The Scene Shell — always
132240
+
132241
+ const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); // preserveDrawingBuffer: required by the view_render self-check
132242
+ renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
132243
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
132244
+ renderer.shadowMap.enabled = true;
132245
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
132246
+ stage.appendChild(renderer.domElement);
132247
+
132248
+ const scene = new THREE.Scene();
132249
+ const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 200);
132250
+
132251
+ // ENVIRONMENT LIGHT — the single biggest quality lever. Pick one:
132252
+ // (a) Indoor / product / abstract: the built-in room, no files needed.
132253
+ const pmrem = new THREE.PMREMGenerator(renderer);
132254
+ scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
132255
+ // (b) Outdoor: a physical sky that also LIGHTS the scene — see Environments.
132256
+ // (c) A specific mood: an equirect .hdr URL via RGBELoader, same mapping.
132257
+
132258
+ // KEY LIGHT with soft shadows grounds the objects.
132259
+ const sun = new THREE.DirectionalLight(0xfff2e0, 2.2);
132260
+ sun.position.set(4, 6, 3);
132261
+ sun.castShadow = true;
132262
+ sun.shadow.mapSize.set(2048, 2048);
132263
+ sun.shadow.radius = 5;
132264
+ sun.shadow.bias = -0.0005;
132265
+ scene.add(sun);
132266
+
132267
+ // SIZE to the stage, not the window. Handles editor column resizes too.
132268
+ function resize() {
132269
+ const w = stage.clientWidth, h = stage.clientHeight;
132270
+ if (!w || !h) return;
132271
+ camera.aspect = w / h;
132272
+ camera.updateProjectionMatrix();
132273
+ renderer.setSize(w, h);
132274
+ }
132275
+ new ResizeObserver(resize).observe(stage);
132276
+ resize();
132277
+
132278
+ // LOOP — stops itself when this render is replaced (edit/undo/duplicate),
132279
+ // and pauses while the block is off-screen or the tab is hidden.
132280
+ let visible = true;
132281
+ new IntersectionObserver(function(e) { visible = e[0].isIntersecting; }).observe(stage);
132282
+ const clock = new THREE.Clock();
132283
+ renderer.setAnimationLoop(function() {
132284
+ if (!stage.isConnected) { // this render was discarded
132285
+ renderer.setAnimationLoop(null);
132286
+ renderer.dispose();
132287
+ return;
132288
+ }
132289
+ if (!visible || document.hidden) return;
132290
+ const dt = Math.min(clock.getDelta(), 0.05);
132291
+ // ... update(dt) ...
132292
+ renderer.render(scene, camera);
132293
+ });
131733
132294
 
131734
- **In Content**
132295
+ Materials: MeshStandardMaterial (metalness/roughness) or MeshPhysicalMaterial
132296
+ (clearcoat, transmission). With the environment set they pick up real
132297
+ reflections; without it, metals render black — that is the "flat plastic"
132298
+ failure. Shadows: castShadow/receiveShadow on meshes, receiveShadow on the
132299
+ ground. A ShadowMaterial plane (opacity ~0.35) gives a shadow-only floor for
132300
+ product shots.
132301
+
132302
+ ## 4. Objects: write code or load a .glb
132303
+
132304
+ - WRITE CODE for man-made, regular shapes: buildings, furniture, product
132305
+ mockups, abstract hero geometry. Compose primitives (Box, Cylinder,
132306
+ Extrude, Lathe, RoundedBoxGeometry addon), and add a subtle CanvasTexture
132307
+ for surface interest (wood planks, panel lines). Free, tiny, editable.
132308
+ - LOAD A .glb for organic or specific things: characters, animals, plants, a
132309
+ real product, anything that must match a reference image. Getting one:
132310
+ - When a generate_3d_model tool is offered, use it: give it ONE clear image
132311
+ of the object (a photo on the page, a generate_image result, or the
132312
+ user's attachment) and load the .glb URL it returns. For an object with
132313
+ no image yet, generate_image first, then feed that in — image quality
132314
+ decides model quality.
132315
+ - When no such tool is offered, build the object from primitives with
132316
+ real material craft instead — never fabricate a model URL.
132317
+
132318
+ Always set the meshopt decoder (generated models are usually
132319
+ meshopt-compressed; it is harmless for plain files), and normalize — glbs
132320
+ come in arbitrary scale:
132321
+
132322
+ const loader = new GLTFLoader();
132323
+ loader.setMeshoptDecoder(MeshoptDecoder);
132324
+ const gltf = await loader.loadAsync(url);
132325
+ const obj = gltf.scene;
132326
+ const box = new THREE.Box3().setFromObject(obj);
132327
+ const size = box.getSize(new THREE.Vector3());
132328
+ obj.scale.setScalar(TARGET_HEIGHT / Math.max(size.x, size.y, size.z));
132329
+ box.setFromObject(obj);
132330
+ const center = box.getCenter(new THREE.Vector3());
132331
+ obj.position.set(-center.x, -box.min.y, -center.z); // centered, on the floor
132332
+ obj.traverse(function(n) { if (n.isMesh) { n.castShadow = true; n.receiveShadow = true; } });
132333
+ scene.add(obj);
132334
+
132335
+ ## 5. Environments (outdoor scenes)
132336
+
132337
+ SKY THAT LIGHTS THE SCENE — golden hour in a few lines, no texture files:
132338
+
132339
+ const sky = new Sky();
132340
+ sky.scale.setScalar(4000);
132341
+ scene.add(sky);
132342
+ const sunDir = new THREE.Vector3().setFromSphericalCoords(1,
132343
+ THREE.MathUtils.degToRad(90 - 6), // elevation: 2-8 = sunset, 30+ = day
132344
+ THREE.MathUtils.degToRad(245)); // azimuth
132345
+ sky.material.uniforms.sunPosition.value.copy(sunDir);
132346
+ sky.material.uniforms.turbidity.value = 6;
132347
+ sky.material.uniforms.rayleigh.value = 2.2;
132348
+ scene.environment = pmrem.fromScene(sky, 0.02).texture; // the sky IS the light
132349
+ sun.position.copy(sunDir).multiplyScalar(120); // key light matches it
132350
+ renderer.toneMappingExposure = 0.55; // Sky is bright; tune 0.4-0.7
132351
+ scene.fog = new THREE.FogExp2(0xe8c8a0, 0.0035); // depth haze sells distance
132352
+
132353
+ TERRAIN — a displaced plane. Define ONE height function and reuse it for
132354
+ placing everything (grass, props) so nothing floats:
132355
+
132356
+ function ground(x, z) {
132357
+ return 1.1 * Math.sin(x * 0.045 + 1) * Math.cos(z * 0.05)
132358
+ + 0.5 * Math.sin(x * 0.11 + 2.3) * Math.sin(z * 0.13 + 1.1);
132359
+ }
132360
+ Build a PlaneGeometry(240, 240, 180, 180), rotateX(-PI/2), set each vertex y
132361
+ from ground(x, z), give vertices green colors varied by height, then
132362
+ computeVertexNormals() and a MeshStandardMaterial({ vertexColors: true,
132363
+ roughness: 1 }).
132364
+
132365
+ WATER — a large plane with a dark blue-grey MeshStandardMaterial and
132366
+ roughness around 0.15. With the sky environment set it reflects the sunset
132367
+ by itself. Place it below the terrain edge.
132368
+
132369
+ GRASS / vegetation — NEVER individual meshes, always ONE InstancedBufferGeometry
132370
+ draw call. One tapered blade (a 4-row triangle strip about 0.018 wide, 1 tall),
132371
+ instanced 50k-150k times with per-instance attributes: offset vec3 (position,
132372
+ y from ground()), data vec4 (rotation angle, height 0.35-0.9, wind phase,
132373
+ tint 0.8-1.2). A small ShaderMaterial does the rest:
132374
+ - vertex: scale by height, bend tip (p.z += t*t*0.1), rotate by angle, sway by
132375
+ sin(time * 1.5 + phase + offset.x * 0.15) * 0.14 * t * t, add offset.
132376
+ - fragment: mix a dark base green (0.07, 0.13, 0.03) to a light tip green
132377
+ (0.38, 0.46, 0.13) by the blade's v coordinate, times tint; optionally add a
132378
+ small warm term at the very tip for sunset scenes.
132379
+ Scatter on a disc around the focal point (radius * sqrt(random) keeps it
132380
+ even), skip blades under objects, set instanceCount, frustumCulled = false,
132381
+ side: THREE.DoubleSide. Update the time uniform in the loop. The same
132382
+ technique makes flowers, reeds and starfields — change the geometry and count.
132383
+
132384
+ ## 6. Interaction
132385
+
132386
+ HOVER + CLICK on 3D objects — a raycaster against the renderer bounds (never
132387
+ window coordinates; the canvas is inside a column):
132388
+
132389
+ const ray = new THREE.Raycaster();
132390
+ const pointer = new THREE.Vector2();
132391
+ function pick(e) {
132392
+ const r = renderer.domElement.getBoundingClientRect();
132393
+ pointer.set(((e.clientX - r.left) / r.width) * 2 - 1,
132394
+ -((e.clientY - r.top) / r.height) * 2 + 1);
132395
+ ray.setFromCamera(pointer, camera);
132396
+ const hit = ray.intersectObjects(pickables, true)[0];
132397
+ return hit ? hit.object : null;
132398
+ }
132399
+ renderer.domElement.addEventListener('pointermove', function(e) {
132400
+ const o = pick(e);
132401
+ renderer.domElement.style.cursor = o ? 'pointer' : '';
132402
+ // hover response: scale/emissive lerp in the loop, not a snap
132403
+ });
132404
+ renderer.domElement.addEventListener('click', function(e) {
132405
+ const o = pick(e); if (o) { /* select, open, score ... */ }
132406
+ });
131735
132407
 
131736
- <div class="row">
131737
- <div class="column">
131738
- <div data-cb-type="code">
131739
- <!-- Your Code -->
131740
- </div>
131741
- </div>
131742
- </div>
132408
+ Animate responses smoothly in the loop (lerp toward a target scale/rotation/
132409
+ color) instead of setting values on the event — that is what makes it feel
132410
+ polished. Camera orbit: OrbitControls with enableDamping = true; constrain
132411
+ (maxPolarAngle, min/maxDistance) so users cannot go under the floor.
132412
+
132413
+ REVEAL PATTERNS — a click should MEAN something, not just wobble. Four
132414
+ recurring shapes, all built from the same pieces (pick + lerp + HTML):
132415
+
132416
+ CAMERA FOCUS (product tour): click an object, fly the camera to frame it,
132417
+ and offer the way back.
132418
+
132419
+ let camTo = null, lookTo = null;
132420
+ function focus(obj) {
132421
+ const p = new THREE.Vector3();
132422
+ obj.getWorldPosition(p);
132423
+ camTo = p.clone().add(new THREE.Vector3(1.4, 0.8, 2.0)); // offset that frames it
132424
+ lookTo = p.clone();
132425
+ backBtn.style.display = 'block';
132426
+ }
132427
+ function overview() {
132428
+ camTo = HOME_POS.clone(); lookTo = HOME_TARGET.clone();
132429
+ backBtn.style.display = 'none'; card.style.display = 'none';
132430
+ }
132431
+ // in the loop:
132432
+ if (camTo) {
132433
+ const k = 1 - Math.exp(-dt * 4);
132434
+ camera.position.lerp(camTo, k);
132435
+ controls.target.lerp(lookTo, k);
132436
+ if (camera.position.distanceTo(camTo) < 0.01) { camTo = null; lookTo = null; }
132437
+ }
132438
+ controls.addEventListener('start', function() { camTo = null; lookTo = null; }); // user drag wins
132439
+
132440
+ INFO CARD: the revealed details are real HTML overlaid on the stage — never
132441
+ 3D text. A fixed-position card (absolute inside the stage, e.g. right side)
132442
+ reads better than one tracking the 3D point, and never clips. Fill it per
132443
+ object from a plain data object keyed by mesh name; give it a real <button>
132444
+ to close, and close on Escape too. Selectable, accessible, translatable.
132445
+
132446
+ STATE REVEAL: the click changes the scene itself — a lid opens, parts fan
132447
+ out into an exploded view, the chosen item stays lit while the rest dim.
132448
+ Record each part's home transform at build time, keep one 0..1 reveal value
132449
+ per group, and lerp every part between home and revealed each frame from
132450
+ that value. Dimming: lerp material color or opacity, never a CSS filter.
132451
+
132452
+ SCENE-TO-PAGE REVEAL: when the revealed content belongs in the page (specs,
132453
+ paragraphs, a price table), toggle a normal HTML element NEXT TO the stage
132454
+ instead of an overlay. The content stays selectable and readable without
132455
+ WebGL; the scene is the index, the page is the content.
132456
+
132457
+ Shared rules: one selection at a time; clicking empty space = Back; Back and
132458
+ close are real, keyboard-reachable buttons; everything moves through the
132459
+ loop's lerp, nothing snaps.
132460
+
132461
+ Touch: pointer events above already cover it; OrbitControls handles touch
132462
+ orbit. Do not call preventDefault on wheel/touch you do not use — the page
132463
+ must keep scrolling normally past the block.
132464
+
132465
+ SCROLL-DRIVEN scenes: read progress from the block's own position — works
132466
+ everywhere:
132467
+
132468
+ function scrollProgress() { // 0 when entering, 1 when leaving
132469
+ const r = stage.getBoundingClientRect();
132470
+ return THREE.MathUtils.clamp((innerHeight - r.top) / (innerHeight + r.height), 0, 1);
132471
+ }
132472
+ Call it in the loop and drive camera/objects from it (again through lerp for
132473
+ smoothness). On a ContentBox page whose section has data-fx-emit, prefer the
132474
+ host's event: container.closest('.is-section').addEventListener('fx:progress',
132475
+ function(e) { p = e.detail.progress; }).
132476
+
132477
+ ## 7. Games (3D)
132478
+
132479
+ Game craft — loop, input, state machine, HUD, sound, recycling, cleanup —
132480
+ lives in the "games" reference: load it for any game. This section is only
132481
+ what makes a THREE.JS game look and feel right:
132482
+
132483
+ - The Scene Shell applies to games in full. Environment lighting, real
132484
+ materials, fog and shadows are what separate a game from a tech demo —
132485
+ never fall back to flat MeshBasicMaterial arcade defaults. Emissive
132486
+ materials + bloom give the cheap "neon glow" so many game styles want.
132487
+ - HERO ASSETS: the 2-3 entities that carry the whole look — the player
132488
+ craft, the collectible, one signature obstacle — deserve generated models
132489
+ (generate_3d_model when offered). Load each .glb ONCE, then obj.clone()
132490
+ per spawn; cloning is as cheap as cloning boxes, so recycling pools work
132491
+ unchanged. This single choice is the difference between "colored cubes"
132492
+ and a game with an identity.
132493
+ - FILLER stays coded, but crafted: RoundedBoxGeometry over BoxGeometry,
132494
+ slight bevels, emissive rims, vertex-colored gradients — never bare
132495
+ saturated boxes.
132496
+ - Movement feel: lerp positions and tilt/bank the player toward its motion;
132497
+ camera follows with damping, never rigidly.
132498
+ - Update per frame from the update(dt) function; collisions at this scale
132499
+ are distance checks (player.position.distanceTo(e.position) < r1 + r2).
132500
+
132501
+ ## 8. Self-check — see your work
132502
+
132503
+ When a view_render tool is offered, ALWAYS call it after creating a scene or
132504
+ making a significant visual change, and LOOK at the screenshot before
132505
+ answering:
132506
+ - All black or flat: the environment/lighting is missing, or every object is
132507
+ outside the camera view.
132508
+ - Empty or nearly empty: camera position/target does not frame the content —
132509
+ check object positions and scale.
132510
+ - Clipped, tiny or washed out: adjust framing, scale or exposure.
132511
+ Fix what is wrong, re-check once, and only then answer. Do not describe a
132512
+ scene you have not seen. (The screenshot shows the canvas only — HTML
132513
+ overlays are not in it.)
132514
+
132515
+ Judge the render like an art director: the bar is FINISHED, not merely
132516
+ correct. A right-shaped scene rendered badly — glaring or flat light, dead
132517
+ materials, awkward framing, harsh exposure — is not done. Weigh the impact
132518
+ yourself, and reach for the cheap lever first:
132519
+ - Anything fixable in code — lighting, environment, exposure, camera,
132520
+ materials, scale — is FREE. If a change would noticeably improve the
132521
+ scene, make it.
132522
+ - Regenerating a model costs the user money. Do it only when the asset
132523
+ itself is the problem (mangled geometry, clearly the wrong object) and
132524
+ code cannot compensate — better light, exposure and framing rescue a
132525
+ mediocre asset more often than a re-roll does.
132526
+ When the scene genuinely reads well, it is done — stop; do not iterate on
132527
+ differences a viewer would not notice.
132528
+
132529
+ ## 9. Performance & hygiene
132530
+
132531
+ - One scene per block; reuse geometries/materials; instancing for anything
132532
+ repeated more than ~20 times.
132533
+ - Keep pixelRatio capped at 2 and shadow maps at 2048 or below.
132534
+ - The visibility pause in the shell is mandatory — a page can hold several
132535
+ blocks and background tabs must not burn CPU.
132536
+ - Respect prefers-reduced-motion: if it matches, skip autonomous camera
132537
+ motion and wind sway; render still frames on interaction only.
132538
+ - Do not touch document-level styles, and never assume the block is the only
132539
+ 3D on the page.
132540
+ `;
131743
132541
 
131744
- **In Overlay**
132542
+ /*
132543
+ * context-games.js — the "games" on-demand doc topic for Code Chat v2.
132544
+ *
132545
+ * Loaded via get_framework_docs for ANY game request, 2D or 3D. It owns the
132546
+ * craft every game shares (loop, input, state, HUD, sound, cleanup) plus the
132547
+ * 2D Canvas branch; a 3D game additionally loads the "3d" topic, whose games
132548
+ * section holds only the 3D-specific rendering rules.
132549
+ *
132550
+ * Asset reality check (user-verified, Sept 2026): most image models FAIL at
132551
+ * transparent PNGs — sprites are requested on solid backgrounds and handled
132552
+ * in code. Never instruct the model to ask for transparency.
132553
+ * Keep code samples free of backticks and ${ } — template literal.
132554
+ */
131745
132555
 
131746
- <div class="is-section is-box is-section-100 type-system-ui">
131747
- <div class="is-overlay">
131748
- <div class="is-overlay-content">
131749
- <div data-cb-type="code" style="position: absolute; top:0, left:0; width: 100%; height: 100%">
131750
- <!-- Your Code -->
131751
- </div>
131752
- </div>
132556
+ const contextGames$1 = `
132557
+ # Building Games
132558
+
132559
+ Use this for any playable game. First decide the dimension — it changes the
132560
+ whole toolchain:
132561
+
132562
+ - 2D (doodle, flat, sprite, puzzle, casual, "simple game"): the Canvas 2D
132563
+ API in a code block. Do NOT use three.js for these — it is heavier, slower
132564
+ to write, and fights the flat aesthetic.
132565
+ - 3D (depth, camera, "3D"): three.js — ALSO load the "3d" reference and
132566
+ follow its games section for rendering and assets.
132567
+
132568
+ Target scale either way: an arcade mini-game — one scene, one core mechanic,
132569
+ polished. Refine with the user over turns.
132570
+
132571
+ ## The skeleton every game shares
132572
+
132573
+ Everything lives in one code block (see the "code" reference for the block
132574
+ contract). Inside it:
132575
+
132576
+ - LOOP: requestAnimationFrame with a clamped delta; stop yourself when the
132577
+ block is re-rendered:
132578
+
132579
+ let last = performance.now();
132580
+ function frame(now) {
132581
+ if (!stage.isConnected) return; // this render was replaced
132582
+ const dt = Math.min((now - last) / 1000, 0.05);
132583
+ last = now;
132584
+ update(dt); draw();
132585
+ requestAnimationFrame(frame);
132586
+ }
132587
+ requestAnimationFrame(frame);
132588
+
132589
+ - INPUT: one flags object; keyboard listeners attach on game start (the
132590
+ start click also gives focus), ignore keys while the user types elsewhere,
132591
+ and detach themselves via the isConnected check. Map pointerdown /
132592
+ pointermove on the stage to the same flags so touch works. Do not
132593
+ preventDefault on events you do not use — the page must keep scrolling.
132594
+ - STATE: a small machine — ready / playing / over — with restart resetting
132595
+ state, never reloading. Show "click or tap to start".
132596
+ - HUD: HTML positioned over the stage (score, lives, messages) — real text,
132597
+ updated via textContent, never drawn glyph-by-glyph into the canvas.
132598
+ - SOUND: only if asked; short WebAudio oscillator beeps, no external files.
132599
+ - ENTITIES: recycle, never allocate per frame — spawn from a pool, move
132600
+ off-screen items back in.
132601
+ - SELF-CHECK: when a view_render tool is offered, screenshot after building
132602
+ and after big visual changes, judge it, fix, re-check once.
132603
+
132604
+ ## 2D games (Canvas 2D)
132605
+
132606
+ Setup — crisp on every screen:
132607
+
132608
+ <div data-cb-code>
132609
+ <div class="game-stage" style="position:relative; width:100%; aspect-ratio:16/10; overflow:hidden; border-radius:8px; background:#fdfdf8;">
132610
+ <canvas style="position:absolute; inset:0; width:100%; height:100%;"></canvas>
132611
+ <!-- HUD elements here -->
131753
132612
  </div>
131754
-
131755
- <!-- ... -->
132613
+ <script>
132614
+ (async () => {
132615
+ const container = document.currentScript.parentElement;
132616
+ const stage = container.querySelector('.game-stage');
132617
+ const canvas = stage.querySelector('canvas');
132618
+ const ctx = canvas.getContext('2d');
132619
+ function fit() {
132620
+ const dpr = Math.min(devicePixelRatio, 2);
132621
+ canvas.width = stage.clientWidth * dpr;
132622
+ canvas.height = stage.clientHeight * dpr;
132623
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // draw in CSS pixels
132624
+ }
132625
+ new ResizeObserver(fit).observe(stage);
132626
+ fit();
132627
+ // ... game ...
132628
+ })();
132629
+ </script>
131756
132630
  </div>
132631
+
132632
+ Collision at this scale is rectangles and circle distances — no physics
132633
+ library unless the game truly is physics.
132634
+
132635
+ ART DIRECTION — a 2D game must look intentional, not like debug rectangles:
132636
+ pick a style (hand-drawn doodle, flat pastel, neon minimal) and carry it
132637
+ through background, entities, HUD typography and even the game-over screen.
132638
+
132639
+ - DOODLE STYLE, pure code: draw with strokes, not fills — rounded lineJoin,
132640
+ slightly thick lines, and a small random jitter added to each vertex,
132641
+ re-rolled only a few times per second (a wobble timer, not every frame) so
132642
+ shapes shiver like a flipbook. Paper background (#fdfdf8, faint grid),
132643
+ handwritten-feel HUD (a casual system font, slight rotation).
132644
+ - SPRITES from generate_image, when the style needs real drawings: request
132645
+ each sprite "on a plain solid white background, no shadow" in one
132646
+ consistent style. Do NOT request transparent backgrounds — image models
132647
+ usually fail at true transparency, delivering fake checkerboards or dirty
132648
+ edges. Then either:
132649
+ - design the game on the SAME background color, so the sprite's white
132650
+ simply disappears (the doodle-on-paper trick — zero code); or
132651
+ - key it out at load: draw to an offscreen canvas, getImageData, and set
132652
+ alpha 0 on pixels close to the corner color (tolerance ~28); cache the
132653
+ result and draw that.
132654
+ - Parallax sells motion cheaply: two or three background layers scrolling at
132655
+ different speeds.
132656
+
132657
+ ## 3D games
132658
+
132659
+ Load the "3d" reference and follow its games section. The short version:
132660
+ the scene shell (environment light, real materials, fog) applies to games
132661
+ exactly as to scenes — no flat-material arcade fallback — and the 2-3 hero
132662
+ entities that carry the look (player, collectible, signature obstacle)
132663
+ deserve generated models, loaded once and cloned per spawn.
131757
132664
  `;
131758
132665
 
131759
132666
  const contextDesignGuide$1 = `
@@ -133361,7 +134268,7 @@ class ContentBuilder {
133361
134268
  this.ShortcutInfo = new ShortcutInfo(this);
133362
134269
  if (!this.opts.isContentBox) {
133363
134270
  this.codechat = new CodeChat$1({
133364
- context: contextContentFramework$1 + contextCodeBlock + contextDesignGuide$1
134271
+ context: contextContentFramework$1 + getContextCodeBlock$1() + contextDesignGuide$1
133365
134272
  }, this);
133366
134273
 
133367
134274
  /*
@@ -133376,10 +134283,16 @@ class ContentBuilder {
133376
134283
  attachCodeChatV2$1(this.codechat, this, {
133377
134284
  contextParts: {
133378
134285
  content: contextContentFramework$1,
133379
- designCore: contextDesignGuide$1
134286
+ designCore: contextDesignGuide$1,
134287
+ code: getContextCodeBlock$1(),
134288
+ // on-demand 'code' doc topic
134289
+ '3d': context3D$1,
134290
+ // on-demand '3d' doc topic
134291
+ games: contextGames$1 // on-demand 'games' doc topic
133380
134292
  }
133381
134293
  });
133382
134294
  }
134295
+
133383
134296
  if (this.startAIAssistant) {
133384
134297
  this.openAIAssistant();
133385
134298
  }
@@ -140460,52 +141373,22 @@ Please obtain a license at: https://innovastudio.com/contentbox`);
140460
141373
  }
140461
141374
 
140462
141375
  /*
140463
- codeelement.js
141376
+ codesource.js
140464
141377
  ------------------------------------------------------------------
140465
- Independent Code Element (template-based).
140466
-
140467
- Markup contract (in the editor):
140468
-
140469
- <div data-cb-code>
140470
- <template data-source>
140471
- ...pristine source (production-ready, ids resolved)...
140472
- </template>
140473
- ...live rendered DOM (disposable, may be mutated by scripts)...
140474
- </div>
140475
-
140476
- Saved output (produced by readHtml -> flatten):
140477
-
140478
- <div data-cb-code>
140479
- ...pristine source only (scripts run natively on page load)...
140480
- </div>
140481
-
140482
- Snippet form (inert, before first init):
140483
-
140484
- <div data-cb-code data-src="...encodeURIComponent(source with {id})..."></div>
140485
-
140486
- Principles:
140487
- - The <template data-source> is the single source of truth during editing.
140488
- - The rendered DOM is disposable and never trusted.
140489
- - [data-cb-code] is the only marker; no classes, no extra attributes in output.
140490
- - Scripts are removed from the live render right after they execute, so any
140491
- later clone/serialize of the rendered DOM (duplicate, copy/paste, undo
140492
- snapshot) carries nothing runnable. Only render() runs code.
140493
- - Duplicated blocks are detected by a duplicate-DOM-id scan over the content
140494
- area; conflicting blocks get fresh ids.
140495
- - Fully independent from the legacy [data-html] code block: it never touches
140496
- [data-html] and [data-html] logic never touches [data-cb-code].
141378
+ The two pure helpers ContentBox needs for [data-cb-code] blocks. Derived
141379
+ from ContentBuilder's codeelement.js, which also defines a CodeElement
141380
+ editor class — ContentBox never instantiates that, so it is not carried
141381
+ here. Named for what this file is, not for where it came from.
141382
+
141383
+ wrapCodeSource() is a string pre-pass. Call it on any HTML string about to
141384
+ be inserted with createContextualFragment (which executes scripts). It
141385
+ parses with DOMParser -- where scripts can never run -- and moves each
141386
+ [data-cb-code] block's source into an inert <template data-source>, so
141387
+ nothing has run by the time the content is in the DOM.
140497
141388
  */
140498
-
140499
141389
  const ATTR = 'data-cb-code';
140500
141390
  const SEL = '[' + ATTR + ']';
140501
141391
  const TPL_SEL = ':scope > template[data-source]';
140502
- /*
140503
- Prevent a literal </template> inside user code from prematurely closing the
140504
- <template data-source> wrapper when the document is serialized and re-parsed
140505
- (save -> load). Inside a JS string, <\/template> is identical to </template>
140506
- at runtime. (Same convention as the classic <\/script>.)
140507
- */
140508
-
140509
141392
  function escapeCodeSource(src) {
140510
141393
  return src.replace(/<\/template>/gi, '<\\/template>');
140511
141394
  }
@@ -145239,7 +146122,16 @@ class Section {
145239
146122
  html = html.replace(/{id}/g, this.uniqueId()); //Fill the block with original code
145240
146123
 
145241
146124
  this.html(element, html);
145242
- }); // Section Setup
146125
+ });
146126
+ /*
146127
+ A cloned code element carries the previous copy's rendered DOM and its
146128
+ ids. init() detects the conflict, gives the copy fresh ids and renders
146129
+ it again — which is also what re-runs its script. The .is-builder pass
146130
+ below covers blocks inside an editable area; this covers the rest, such
146131
+ as a full-bleed block in .is-overlay-content.
146132
+ */
146133
+
146134
+ if (this.builder.editor.codeElement) this.builder.editor.codeElement.init(section); // Section Setup
145243
146135
 
145244
146136
  this.builder.sectionSetup(section); // Box Setup
145245
146137
 
@@ -152582,6 +153474,25 @@ ${this.buildHistoryContext()}`; // Inlined document text can be large - keep it
152582
153474
  * ============================================================================
152583
153475
  */
152584
153476
 
153477
+ /* Attachments shown under a user message, matching what the composer
153478
+ * renders when a file is attached for real. Demo playback only. */
153479
+
153480
+
153481
+ addUserAttachmentPreview(images) {
153482
+ const previewDiv = document.createElement('div');
153483
+ previewDiv.className = 'message user';
153484
+ let html = '<div class="message-content" style="display:flex;flex-wrap:wrap;gap:8px;padding:10px;">';
153485
+ images.forEach(img => {
153486
+ const url = this.escapeHtml(img.url || '');
153487
+ if (!url) return;
153488
+ const alt = this.escapeHtml(img.context || url.split('/').pop() || 'Attachment');
153489
+ html += `<img src="${url}" alt="${alt}" style="width:64px;height:64px;object-fit:cover;border-radius:6px;" />`;
153490
+ });
153491
+ html += '</div>';
153492
+ previewDiv.innerHTML = html;
153493
+ this.messagesContainer.appendChild(previewDiv);
153494
+ this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
153495
+ }
152585
153496
 
152586
153497
  addImagePreview(images) {
152587
153498
  const out = s => this.out(s);
@@ -154164,10 +155075,12 @@ Return ONLY the edited section as a single <div class="is-section ...>. Do not o
154164
155075
  announcement.setAttribute('role', 'status');
154165
155076
  announcement.setAttribute('aria-live', 'polite');
154166
155077
  announcement.className = 'sr-only';
154167
- announcement.textContent = message;
154168
- document.body.appendChild(announcement);
155078
+ announcement.textContent = message; // Must live inside the builder container: the .sr-only rule is scoped
155079
+ // to [data-cb-ui], so elsewhere the text would be visible on screen.
155080
+
155081
+ this.builderStuff.appendChild(announcement);
154169
155082
  setTimeout(() => {
154170
- document.body.removeChild(announcement);
155083
+ announcement.remove();
154171
155084
  }, 1000);
154172
155085
  }
154173
155086
  /**
@@ -154182,7 +155095,11 @@ Return ONLY the edited section as a single <div class="is-section ...>. Do not o
154182
155095
  if (this.demoConversations && this.demoConversations.length > 0) {
154183
155096
  this.demoConversations.forEach(msg => {
154184
155097
  if (msg.role === 'user') {
154185
- this.addMessage('user', msg.content);
155098
+ this.addMessage('user', msg.content); // Attachments the user sent with the message
155099
+
155100
+ if (msg.imagePreview && msg.imagePreview.length > 0) {
155101
+ this.addUserAttachmentPreview(msg.imagePreview);
155102
+ }
154186
155103
  } else if (msg.role === 'assistant') {
154187
155104
  this.addMessage('assistant', msg.content, true); // Add image preview if available
154188
155105
 
@@ -154527,6 +155444,45 @@ Return ONLY the edited section as a single <div class="is-section ...>. Do not o
154527
155444
 
154528
155445
  }
154529
155446
 
155447
+ /**
155448
+ * repair-markup.js
155449
+ * ----------------------------------------------------------------------------
155450
+ * Fixes applied to AI-written HTML on its way to the page, in the targets'
155451
+ * normalize() step.
155452
+ *
155453
+ * Only for defects that are unambiguous — a rule the framework states, where
155454
+ * the result is visibly broken and there is exactly one correct repair. Taste
155455
+ * belongs in the context documentation, not here: this must never turn one
155456
+ * valid design into another.
155457
+ * ----------------------------------------------------------------------------
155458
+ */
155459
+
155460
+ /*
155461
+ An outline button needs 'hover:border-transparent'.
155462
+
155463
+ 'border-current' makes the 2px border follow the text colour, hover colour
155464
+ included, so on hover the border becomes a visible ring and the hover fill
155465
+ stops inside it — the button appears to shrink. The class lets the border take
155466
+ the hover background instead.
155467
+
155468
+ Left alone: a button that sets its own hover border (any 'hover:border-*'
155469
+ class, or 'border' listed in data-cb-hover), which is a deliberate choice the
155470
+ class would override.
155471
+ */
155472
+ function repairButtons(root) {
155473
+ if (!root || !root.querySelectorAll) return 0;
155474
+ let fixed = 0;
155475
+ root.querySelectorAll('a[role="button"], button').forEach(el => {
155476
+ if (!el.classList.contains('border-current')) return;
155477
+ if ([...el.classList].some(c => c.startsWith('hover:border-'))) return;
155478
+ const hover = (el.getAttribute('data-cb-hover') || '').split(/\s+/);
155479
+ if (hover.includes('border')) return;
155480
+ el.classList.add('hover:border-transparent');
155481
+ fixed++;
155482
+ });
155483
+ return fixed;
155484
+ }
155485
+
154530
155486
  /**
154531
155487
  * target-section.js
154532
155488
  * ----------------------------------------------------------------------------
@@ -154578,7 +155534,7 @@ function createSectionTarget(engine) {
154578
155534
 
154579
155535
 
154580
155536
  const docTopics = () => {
154581
- const t = ['box', 'animation'].filter(k => engine.contextParts[k]);
155537
+ const t = ['box', 'animation', '3d', 'games', 'code'].filter(k => engine.contextParts[k]);
154582
155538
  if (motionPathMode()) t.push('motionPath');
154583
155539
  return t;
154584
155540
  };
@@ -154597,6 +155553,7 @@ function createSectionTarget(engine) {
154597
155553
  const found = holder.querySelectorAll('.is-section');
154598
155554
  if (found.length !== 1) return null;
154599
155555
  const section = found[0];
155556
+ repairButtons(section);
154600
155557
  section.querySelectorAll('div.is-container').forEach(c => {
154601
155558
  if (![...c.classList].some(x => /^size-\d/.test(x))) c.classList.add('size-17');
154602
155559
  if (![...c.classList].some(x => /^leading-\d/.test(x))) c.classList.add('leading-17');
@@ -154740,7 +155697,7 @@ Sections are <div class="is-section ...">. Use ONLY documented classes — this
154740
155697
  ${onDemand.length ? `
154741
155698
  REFERENCE YOU MUST LOAD WHEN RELEVANT
154742
155699
  The documentation below covers grid, typography, spacing and colour. Further reference is available through get_framework_docs and is NOT included here:
154743
- ${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.` : ''}`;
155700
+ ${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.` : ''}`;
154744
155701
  },
154745
155702
 
154746
155703
  /* Structural tools. The engine adds the attachment and image tools. */
@@ -154813,7 +155770,7 @@ ${onDemand.includes('box') ? '- "box": section and box structure — load before
154813
155770
  }
154814
155771
  }, ...(topics.length ? [{
154815
155772
  name: 'get_framework_docs',
154816
- 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.`,
155773
+ 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.`,
154817
155774
  parameters: {
154818
155775
  type: 'object',
154819
155776
  properties: {
@@ -155028,7 +155985,6 @@ ${onDemand.includes('box') ? '- "box": section and box structure — load before
155028
155985
  const SPACER_HEIGHTS = [20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300];
155029
155986
 
155030
155987
  const spacerRow = h => `<div class="row"><div class="column"><div class="spacer height-${h}"></div></div></div>`;
155031
-
155032
155988
  function createRowTarget(engine) {
155033
155989
  const b = () => engine.builder;
155034
155990
 
@@ -155040,7 +155996,7 @@ function createRowTarget(engine) {
155040
155996
  /* Reference the host actually supplied, in the order it is offered. */
155041
155997
 
155042
155998
 
155043
- const docTopics = () => ['buttons', 'animation'].filter(k => engine.contextParts[k]);
155999
+ const docTopics = () => ['buttons', 'animation', '3d', 'games', 'code'].filter(k => engine.contextParts[k]);
155044
156000
  /*
155045
156001
  Exactly one .row, or null. Markup arrives fenced as often as not, and a
155046
156002
  model that returns two rows for a one-row tool has misunderstood the call
@@ -155055,6 +156011,7 @@ function createRowTarget(engine) {
155055
156011
  holder.innerHTML = html.replace(/```html\n?/g, '').replace(/```\n?/g, '').trim();
155056
156012
  const found = Array.from(holder.children).filter(el => el.classList.contains('row'));
155057
156013
  if (found.length !== 1) return null;
156014
+ repairButtons(found[0]);
155058
156015
  return found[0].outerHTML;
155059
156016
  }
155060
156017
 
@@ -155763,6 +156720,86 @@ ${docs}`;
155763
156720
  }
155764
156721
  });
155765
156722
  }
156723
+ /*
156724
+ 3D asset generation (image-to-3D). Offered only when the host lists
156725
+ models3D — the same opt-in convention as the image tools. One image in,
156726
+ one .glb URL out; the model then loads it with GLTFLoader per the "3d"
156727
+ reference doc.
156728
+ */
156729
+
156730
+
156731
+ if (this.threeDConfig()) {
156732
+ const threeDParams = {
156733
+ image_url: {
156734
+ type: 'string',
156735
+ description: 'URL of the source image: a photo on the page, or a generate_image result.'
156736
+ }
156737
+ };
156738
+
156739
+ if ((this.currentAttachments || []).some(a => a && a.kind === 'image')) {
156740
+ threeDParams.attachment = {
156741
+ type: 'integer',
156742
+ description: 'Use an attached image as the source instead of image_url, numbered from 1.'
156743
+ };
156744
+ }
156745
+
156746
+ t.push({
156747
+ name: 'generate_3d_model',
156748
+ 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>.',
156749
+ parameters: {
156750
+ type: 'object',
156751
+ properties: threeDParams,
156752
+ required: []
156753
+ }
156754
+ });
156755
+ }
156756
+ /*
156757
+ The critic loop: the model can SEE what a code block's canvas actually
156758
+ renders and fix visual problems before answering — the difference
156759
+ between shipping a black scene and noticing it. Offered only while the
156760
+ page holds a canvas inside a code block, so ordinary requests never
156761
+ see it.
156762
+ */
156763
+
156764
+ /* Gated on the block, not its canvas — right after an insert the
156765
+ scene is still importing modules and the canvas does not exist yet,
156766
+ which is exactly when the model needs the tool in its list.
156767
+ captureRender does the waiting. */
156768
+
156769
+ /*
156770
+ Only a model that reads images can act on the screenshot — for a
156771
+ text-only model the injected image is a provider error, so the tool is
156772
+ simply not offered and the docs' "when a view_render tool is offered"
156773
+ phrasing degrades the whole flow gracefully. Same capability registry
156774
+ the attach button uses.
156775
+ */
156776
+
156777
+ /*
156778
+ Off unless the host turns it on: the self-check costs an extra
156779
+ vision round-trip per scene, so it is opt-in per site.
156780
+ */
156781
+
156782
+
156783
+ const selfCheckOn = !!(this.builder && this.builder.renderSelfCheck);
156784
+ const seesImages = resolveFileCapability(this.model, this.v1 && this.v1.codeModels || this.builder && this.builder.codeModels || []).accepts.includes('image');
156785
+
156786
+ if (selfCheckOn && seesImages && this.builder && this.builder.doc && this.builder.doc.querySelector('[data-cb-code]')) {
156787
+ t.push({
156788
+ name: 'view_render',
156789
+ 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.',
156790
+ parameters: {
156791
+ type: 'object',
156792
+ properties: {
156793
+ block: {
156794
+ type: 'integer',
156795
+ description: '1-based index among the page\'s canvas-bearing code blocks; omit for the first',
156796
+ default: 1
156797
+ }
156798
+ },
156799
+ required: []
156800
+ }
156801
+ });
156802
+ }
155766
156803
 
155767
156804
  return t;
155768
156805
  }
@@ -155819,6 +156856,16 @@ ${docs}`;
155819
156856
  return await this.editImage(args);
155820
156857
  }
155821
156858
 
156859
+ case 'generate_3d_model':
156860
+ {
156861
+ return await this.generate3DModel(args);
156862
+ }
156863
+
156864
+ case 'view_render':
156865
+ {
156866
+ return await this.captureRender(args);
156867
+ }
156868
+
155822
156869
  default:
155823
156870
  return {
155824
156871
  error: `unknown tool ${name}`
@@ -155837,7 +156884,13 @@ ${docs}`;
155837
156884
 
155838
156885
 
155839
156886
  async send(text, attachments = []) {
155840
- this.abort = new AbortController();
156887
+ this.abort = new AbortController(); // Per-request ceilings on the expensive/loopable tools. MAX_STEPS
156888
+ // bounds the loop as a whole; these stop a model from burning paid
156889
+ // generations or endless self-checks chasing a critique it cannot
156890
+ // satisfy — past the cap the tool refuses and tells it to finish.
156891
+
156892
+ this.spent3D = 0;
156893
+ this.spentShots = 0;
155841
156894
  this.beginBatch(); // one undo entry for the whole request
155842
156895
 
155843
156896
  this.currentAttachments = attachments || [];
@@ -155905,6 +156958,31 @@ ${docs}`;
155905
156958
  content: JSON.stringify(result)
155906
156959
  });
155907
156960
  }
156961
+ /*
156962
+ view_render screenshots cannot ride in the tool message —
156963
+ tool results are text on every provider — so they follow as a
156964
+ user message. dropAttachmentPayloads compacts them on the next
156965
+ request like any other image, so they never accumulate.
156966
+ */
156967
+
156968
+
156969
+ if (this.pendingShots && this.pendingShots.length) {
156970
+ const parts = [{
156971
+ type: 'text',
156972
+ text: 'Screenshot of the current render:'
156973
+ }];
156974
+ this.pendingShots.forEach(u => parts.push({
156975
+ type: 'image_url',
156976
+ image_url: {
156977
+ url: u
156978
+ }
156979
+ }));
156980
+ messages.push({
156981
+ role: 'user',
156982
+ content: parts
156983
+ });
156984
+ this.pendingShots = [];
156985
+ }
155908
156986
  } // Ran out of steps. Not an error the user should see as a crash.
155909
156987
 
155910
156988
 
@@ -156381,6 +157459,292 @@ ${docs}`;
156381
157459
  freshOutline() {
156382
157460
  return this.target.outline();
156383
157461
  }
157462
+ /*
157463
+ Screenshot a code block's canvas for the critic loop. Waits for the canvas
157464
+ to exist and paint (module imports take a moment after an insert), then
157465
+ downscales to keep the image cheap. Requires the scene's renderer to have
157466
+ preserveDrawingBuffer: true (the 3d reference's shell sets it) — without
157467
+ it a WebGL canvas reads back blank between frames.
157468
+ */
157469
+
157470
+
157471
+ async captureRender(args = {}) {
157472
+ const doc = this.builder && this.builder.doc;
157473
+ if (!doc) return {
157474
+ error: 'no page'
157475
+ };
157476
+ this.spentShots = (this.spentShots || 0) + 1;
157477
+
157478
+ if (this.spentShots > 6) {
157479
+ return {
157480
+ 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.'
157481
+ };
157482
+ }
157483
+
157484
+ const n = Math.max(1, parseInt(args.block, 10) || 1);
157485
+ let canvas = null;
157486
+
157487
+ for (let i = 0; i < 24; i++) {
157488
+ const canvases = doc.querySelectorAll('[data-cb-code] canvas');
157489
+ canvas = canvases[n - 1] || null;
157490
+ if (canvas && canvas.width > 2 && canvas.height > 2) break;
157491
+ await new Promise(r => setTimeout(r, 500));
157492
+ if (this.abort && this.abort.signal.aborted) throw Object.assign(new Error('aborted'), {
157493
+ name: 'AbortError'
157494
+ });
157495
+ }
157496
+
157497
+ if (!canvas) return {
157498
+ error: 'no rendering canvas found in a code block (is the scene failing before it creates one?)'
157499
+ };
157500
+ await new Promise(r => setTimeout(r, 1500)); // let the first real frames draw
157501
+
157502
+ try {
157503
+ const scale = Math.min(1, 768 / canvas.width);
157504
+ const w = Math.max(1, Math.round(canvas.width * scale));
157505
+ const h = Math.max(1, Math.round(canvas.height * scale));
157506
+ const off = document.createElement('canvas');
157507
+ off.width = w;
157508
+ off.height = h;
157509
+ const ctx = off.getContext('2d');
157510
+ ctx.drawImage(canvas, 0, 0, w, h); // A uniform image means nothing drew (or the renderer lacks
157511
+ // preserveDrawingBuffer) — say so instead of sending a blank.
157512
+
157513
+ const d = ctx.getImageData(0, 0, w, h).data;
157514
+ let uniform = true;
157515
+
157516
+ for (let i = 4; i < d.length; i += 997 * 4) {
157517
+ if (d[i] !== d[0] || d[i + 1] !== d[1] || d[i + 2] !== d[2]) {
157518
+ uniform = false;
157519
+ break;
157520
+ }
157521
+ }
157522
+
157523
+ const dataUrl = off.toDataURL('image/jpeg', 0.8);
157524
+ this.pendingShots = this.pendingShots || [];
157525
+ this.pendingShots.push(dataUrl);
157526
+
157527
+ if (uniform) {
157528
+ return {
157529
+ ok: true,
157530
+ 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.'
157531
+ };
157532
+ }
157533
+
157534
+ return {
157535
+ ok: true,
157536
+ note: 'screenshot captured — it follows as an image in the next message. Look at it before answering.'
157537
+ };
157538
+ } catch (e) {
157539
+ return {
157540
+ error: 'could not capture the canvas: ' + String(e && e.message || e)
157541
+ };
157542
+ }
157543
+ }
157544
+ /*
157545
+ 3D generation configuration, resolved per call so a host can switch
157546
+ provider at runtime the same way image generation can. Mirrors v1's
157547
+ provider resolution: endpoint options are looked up as
157548
+ `<base>_<Provider>` in any casing, falling back to the _Fal names.
157549
+ Today every supported provider is queue-shaped (request/status/result);
157550
+ a future single-call 3D provider gets its own branch in generate3DModel,
157551
+ the way SINGLE_CALL_IMAGE_PROVIDERS did for images.
157552
+ */
157553
+
157554
+
157555
+ threeDConfig() {
157556
+ const b = this.builder;
157557
+ if (!b) return null;
157558
+ const models = Array.isArray(b.models3D) ? b.models3D.filter(m => m && m.id) : [];
157559
+ if (!models.length) return null;
157560
+ const provider = b.default3DGenerationProvider || 'fal';
157561
+
157562
+ const val = base => {
157563
+ const wanted = `${base}_${provider}`.toLowerCase();
157564
+ const key = Object.keys(b).find(k => k.toLowerCase() === wanted && b[k]);
157565
+ return key ? b[key] : undefined;
157566
+ };
157567
+
157568
+ const generateUrl = val('generateMediaUrl') || b.generateMediaUrl_Fal;
157569
+ const statusUrl = val('checkRequestStatusUrl') || b.checkRequestStatusUrl_Fal;
157570
+ const resultUrl = val('getResultUrl') || b.getResultUrl_Fal;
157571
+ if (!generateUrl || !statusUrl || !resultUrl) return null; // The saved selection, snapping to the first entry when it is not listed
157572
+
157573
+ const saved = this.v1 && this.v1.settings && this.v1.settings.model3D;
157574
+ const model = (models.find(m => m.id === saved) || models[0]).id;
157575
+ return {
157576
+ provider,
157577
+ models,
157578
+ model,
157579
+ generateUrl,
157580
+ statusUrl,
157581
+ resultUrl
157582
+ };
157583
+ }
157584
+ /*
157585
+ Image-to-3D. The image reaches the provider the same way reference images
157586
+ do for image editing: data URLs go through as-is, same-origin files are
157587
+ read into a data URL (the provider cannot fetch a private host), and
157588
+ public URLs are passed for the provider to fetch itself.
157589
+ */
157590
+
157591
+
157592
+ async generate3DModel(args = {}) {
157593
+ const cfg = this.threeDConfig();
157594
+ if (!cfg) return {
157595
+ error: '3D generation is not configured'
157596
+ };
157597
+ this.spent3D = (this.spent3D || 0) + 1;
157598
+
157599
+ if (this.spent3D > 4) {
157600
+ return {
157601
+ 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.'
157602
+ };
157603
+ }
157604
+
157605
+ const signal = this.abort ? this.abort.signal : null; // Resolve the source image
157606
+
157607
+ let src = null;
157608
+
157609
+ if (args.attachment !== undefined && args.attachment !== null) {
157610
+ const list = (this.currentAttachments || []).filter(a => a && a.kind === 'image');
157611
+ const att = list[(parseInt(args.attachment, 10) || 1) - 1];
157612
+ if (!att) return {
157613
+ error: `there ${list.length === 1 ? 'is 1 attached image' : `are ${list.length} attached images`}`
157614
+ };
157615
+ src = att.dataUrl;
157616
+ } else if (args.image_url) {
157617
+ src = this.expandAttachmentUrls(String(args.image_url));
157618
+
157619
+ if (!/^data:/i.test(src)) {
157620
+ let abs;
157621
+
157622
+ try {
157623
+ abs = new URL(src, location.href);
157624
+ } catch (e) {
157625
+ return {
157626
+ error: 'image_url is not a valid URL'
157627
+ };
157628
+ }
157629
+
157630
+ if (abs.origin === location.origin) {
157631
+ try {
157632
+ const blob = await (await fetch(abs.href, {
157633
+ signal
157634
+ })).blob();
157635
+ src = await new Promise((resolve, reject) => {
157636
+ const r = new FileReader();
157637
+
157638
+ r.onload = () => resolve(r.result);
157639
+
157640
+ r.onerror = reject;
157641
+ r.readAsDataURL(blob);
157642
+ });
157643
+ } catch (e) {
157644
+ return {
157645
+ error: 'could not read the image at image_url'
157646
+ };
157647
+ }
157648
+ } else {
157649
+ src = abs.href; // public URL — the provider fetches it
157650
+ }
157651
+ }
157652
+ } else {
157653
+ return {
157654
+ error: 'give image_url or attachment'
157655
+ };
157656
+ }
157657
+
157658
+ const b = this.builder;
157659
+ const headers = { ...b.headers,
157660
+ ...b.defaultHeaders
157661
+ };
157662
+ const customData = b.editor && b.editor.customData || {};
157663
+
157664
+ const post = async (url, body) => {
157665
+ const response = await fetch(url, {
157666
+ method: 'POST',
157667
+ headers,
157668
+ signal,
157669
+ body: JSON.stringify(body)
157670
+ });
157671
+ return await response.json();
157672
+ }; // Each model family names its input field differently
157673
+
157674
+
157675
+ const payload = /hunyuan/i.test(cfg.model) ? {
157676
+ input_image_url: src
157677
+ } : {
157678
+ image_urls: [src],
157679
+ geometry_file_format: 'glb'
157680
+ }; // Rodin-shaped default
157681
+
157682
+ const firstTwoParts = cfg.model.split('/').slice(0, 2).join('/');
157683
+ const req = await post(cfg.generateUrl, {
157684
+ model: cfg.model,
157685
+ customData,
157686
+ endpoint: `https://queue.fal.run/${cfg.model}`,
157687
+ payload
157688
+ });
157689
+
157690
+ if (req.error || !req.request_id) {
157691
+ return {
157692
+ error: typeof req.error === 'string' ? req.error : '3D generation request failed'
157693
+ };
157694
+ }
157695
+
157696
+ const request_id = req.request_id;
157697
+ const statusBody = {
157698
+ model: cfg.model,
157699
+ endpoint: `https://queue.fal.run/${firstTwoParts}/requests/${request_id}/status`,
157700
+ request_id,
157701
+ customData
157702
+ };
157703
+ let status = '';
157704
+
157705
+ for (let i = 0; i < 150 && status !== 'COMPLETED'; i++) {
157706
+ const st = await post(cfg.statusUrl, statusBody);
157707
+ if (st.ok === false) return {
157708
+ error: st.error || '3D generation failed'
157709
+ };
157710
+
157711
+ if (st.ok && st.status) {
157712
+ status = st.status;
157713
+ if (status === 'FAILED') return {
157714
+ error: '3D generation failed'
157715
+ };
157716
+ } else if (st.data) {
157717
+ // API-style status (mirrors the image flow's fallback)
157718
+ 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;
157719
+ }
157720
+
157721
+ if (status !== 'COMPLETED') await new Promise(r => setTimeout(r, 2000));
157722
+ if (signal && signal.aborted) throw Object.assign(new Error('aborted'), {
157723
+ name: 'AbortError'
157724
+ });
157725
+ }
157726
+
157727
+ if (status !== 'COMPLETED') return {
157728
+ error: '3D generation timed out'
157729
+ };
157730
+ const res = await post(cfg.resultUrl, {
157731
+ model: cfg.model,
157732
+ request_id,
157733
+ customData,
157734
+ endpoint: `https://queue.fal.run/${firstTwoParts}/requests/${request_id}`
157735
+ });
157736
+ if (res.error) return {
157737
+ error: typeof res.error === 'string' ? res.error : '3D result failed'
157738
+ };
157739
+ const entry = res.data && res.data.entries && res.data.entries[0];
157740
+ if (!entry || !entry.url) return {
157741
+ error: 'the generation returned no model file'
157742
+ };
157743
+ return {
157744
+ url: entry.url,
157745
+ format: 'glb'
157746
+ };
157747
+ }
156384
157748
 
156385
157749
  async generateImages(prompt, count = 1, size) {
156386
157750
  if (!this.v1) throw new Error('image generation is not configured');
@@ -156562,6 +157926,12 @@ function describeStep(tool, args, out, target) {
156562
157926
  case 'generate_image':
156563
157927
  return out('Generating image') + '…';
156564
157928
 
157929
+ case 'generate_3d_model':
157930
+ return out('Generating 3D model') + '…';
157931
+
157932
+ case 'view_render':
157933
+ return out('Checking the render') + '…';
157934
+
156565
157935
  case 'edit_image':
156566
157936
  return out('Editing image') + '…';
156567
157937
 
@@ -156860,6 +158230,59 @@ function attachCodeChatV2(codechat, builder, opts = {}) {
156860
158230
  v2.stop();
156861
158231
  if (origAbort) origAbort();
156862
158232
  };
158233
+ /*
158234
+ Settings dialog: the 3D model picker. Injected here rather than written
158235
+ into the panel because the panel belongs to v1 and 3D generation is a v2
158236
+ feature — the same borrowing pattern as sendMessage above. The group
158237
+ exists only when the host configured models3D; with one entry it simply
158238
+ shows what is active.
158239
+ */
158240
+
158241
+
158242
+ const t3d = v2.threeDConfig && v2.threeDConfig();
158243
+
158244
+ if (t3d && codechat.builderStuff && !codechat.builderStuff.querySelector('#model3DSelect')) {
158245
+ const anchor = codechat.builderStuff.querySelector('.div-image-generation-options');
158246
+
158247
+ if (anchor) {
158248
+ const group = document.createElement('div');
158249
+ group.className = 'setting-group';
158250
+ group.innerHTML = `
158251
+ <label for="model3DSelect" class="setting-label">${out('3D Model')}</label>
158252
+ <p class="setting-description">${out('Select the AI model used for 3D model generation')}</p>
158253
+ <div class="cb-sel"><select id="model3DSelect"></select></div>`;
158254
+ const sel = group.querySelector('#model3DSelect');
158255
+ t3d.models.forEach(m => {
158256
+ const opt = document.createElement('option');
158257
+ opt.value = m.id;
158258
+ opt.textContent = m.label || m.id;
158259
+ sel.appendChild(opt);
158260
+ });
158261
+ anchor.parentNode.insertBefore(group, anchor.nextSibling); // Current value on every open; snap to the first when stale
158262
+
158263
+ const origOpen = codechat.openSettings ? codechat.openSettings.bind(codechat) : null;
158264
+
158265
+ if (origOpen) {
158266
+ codechat.openSettings = function () {
158267
+ origOpen();
158268
+ const cfg = v2.threeDConfig();
158269
+ sel.value = cfg && cfg.model || '';
158270
+ if (sel.selectedIndex < 0 && sel.options.length) sel.selectedIndex = 0;
158271
+ };
158272
+ } // Persist with the rest of the settings: set before the panel's
158273
+ // own save stringifies this.settings.
158274
+
158275
+
158276
+ const origSave = codechat.saveSettings ? codechat.saveSettings.bind(codechat) : null;
158277
+
158278
+ if (origSave) {
158279
+ codechat.saveSettings = function () {
158280
+ codechat.settings.model3D = sel.value;
158281
+ origSave();
158282
+ };
158283
+ }
158284
+ }
158285
+ }
156863
158286
 
156864
158287
  return v2;
156865
158288
  }
@@ -156890,7 +158313,7 @@ All content lives in rows and columns — NEVER place content directly under a c
156890
158313
 
156891
158314
  - 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.
156892
158315
  - Weight: font-light font-normal font-medium font-semibold font-bold
156893
- - Tracking: tracking-tight tracking-normal tracking-wide tracking-wider tracking-150 (ideal for uppercase labels)
158316
+ - 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.
156894
158317
  - 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
156895
158318
  - 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.
156896
158319
 
@@ -157096,7 +158519,7 @@ Each box = optional '.is-overlay' (background) + '.is-container' (content):
157096
158519
  ## Overlay (Backgrounds)
157097
158520
 
157098
158521
  - Colour: '<div class="is-overlay" style="background-color: rgb(245,245,245);"></div>'
157099
- - Image: '<div class="is-overlay"><div class="is-overlay-bg" style="background-image:url(\\'image.jpg\\'); background-position:50% 60%;"></div></div>'
158522
+ - Image: '<div class="is-overlay"><div class="is-overlay-bg" style="background-image:url('image.jpg'); background-position:50% 60%;"></div></div>'
157100
158523
  - Dark overlay (for white text on an image): add '<div class="is-overlay-color opacity-30"></div>' inside '.is-overlay-bg' (opacity-30..-50).
157101
158524
  - 'background-position' sets the focal point — 'horizontal% vertical%' (e.g. '50% 60%', '67% 60%').
157102
158525
 
@@ -157195,7 +158618,7 @@ A normal section; its scene progresses **as the section arrives**. Progress '0'
157195
158618
  (To keep animating the whole time the section is on screen — a drift or parallax-like *through*-motion rather than an entrance — set 'data-fx-end="bottom top"'.)
157196
158619
 
157197
158620
 
157198
- <div class="is-section is-box is-section-90 type-poppins is-dark-text">
158621
+ <div class="is-section is-box is-section-90 type-poppins">
157199
158622
  <div class="is-overlay" style="background-color:#f4f2ec"></div>
157200
158623
  <div class="is-container is-content-1100">
157201
158624
  <div class="row"><div class="column">
@@ -157397,7 +158820,7 @@ Copy-and-fill skeletons. Entrance and basic-pinned are shown under "The two scen
157397
158820
 
157398
158821
  ### Split-text hero (content-path, non-pinned)
157399
158822
 
157400
- <div class="is-section is-box is-section-100 type-poppins is-dark-text">
158823
+ <div class="is-section is-box is-section-100 type-poppins">
157401
158824
  <div class="is-overlay" style="background-color:#eef1f5"></div>
157402
158825
  <div class="is-container is-content-1200"><div class="row"><div class="column">
157403
158826
  <p class="size-13 tracking-150 uppercase margin-0" style="color:#5b6779">Field camera</p>
@@ -157412,7 +158835,7 @@ Copy-and-fill skeletons. Entrance and basic-pinned are shown under "The two scen
157412
158835
  ### Pinned card stack — full-bleed layers, one 'is-container' per layer
157413
158836
  Each card is a full-bleed layer in 'is-overlay-content'. The first card scales back as the next slides up over it. Add more layers by repeating the block with later percentages and higher 'z-index'.
157414
158837
 
157415
- <div class="is-section is-box section-pin is-section-100 type-poppins is-light-text" data-fx-length="360" data-fx-scrub="0.3">
158838
+ <div class="is-section is-box section-pin is-section-100 type-poppins" data-fx-length="360" data-fx-scrub="0.3">
157416
158839
  <div class="is-overlay" style="background-color:#eef1f5">
157417
158840
  <div class="is-overlay-content" style="position:absolute;inset:0;overflow:hidden">
157418
158841
 
@@ -157441,7 +158864,7 @@ Each card is a full-bleed layer in 'is-overlay-content'. The first card scales b
157441
158864
  ### Pinned image crossfade — animated backdrop + caption floating above
157442
158865
  The image stack animates inside 'is-overlay-content'; a normal 'is-container' sits **above** it via 'z-index' for the caption.
157443
158866
 
157444
- <div class="is-section is-box section-pin is-section-100 type-poppins is-light-text" data-fx-length="400" data-fx-scrub="0.2">
158867
+ <div class="is-section is-box section-pin is-section-100 type-poppins" data-fx-length="400" data-fx-scrub="0.2">
157445
158868
  <div class="is-overlay" style="background-color:#10182b">
157446
158869
  <div class="is-overlay-content" style="position:absolute;inset:0;overflow:hidden">
157447
158870
  <img class="w-full h-full object-cover" src="a.jpg" alt="" style="position:absolute;inset:0;z-index:1">
@@ -157585,6 +159008,83 @@ propose a motion path as a solution — this effect is disabled for this site. I
157585
159008
  the request does not concern the existing path, leave its attributes untouched.
157586
159009
  `;
157587
159010
 
159011
+ /*
159012
+ * context-code.js — the code element reference for Code Chat.
159013
+ *
159014
+ * Documents the RELEASED code element ([data-cb-code], see
159015
+ * contentbuilder/docs/code-element-guide.md and codeelement.js). The previous
159016
+ * version of this file described an old experimental contract
159017
+ * (data-cb-type="code" + container.mount) that never shipped.
159018
+ *
159019
+ * Sent on every v1 request and served as the on-demand 'code' topic in v2,
159020
+ * so it is kept short. No backticks or ${ } — template literal.
159021
+ *
159022
+ * Takes the host's options because where a block may sit differs by product:
159023
+ * ContentBox also has overlay layers, ContentBuilder does not. One file, one
159024
+ * wording, and neither product is told about markup it has no use for.
159025
+ */
159026
+ function getContextCodeBlock(opts = {}) {
159027
+ // Where a block may sit. ContentBox also has overlay layers; ContentBuilder
159028
+ // has columns only, and must not be told about markup it does not have.
159029
+ const placement = opts.overlay ? `It sits inside a column,
159030
+ beside other content, or inside '.is-overlay > .is-overlay-content'. To fill
159031
+ that layer, give the block 'position:absolute;inset:0' — the overlay is
159032
+ full-screen, the block is not.` : `It sits
159033
+ inside a column, beside other content.`;
159034
+ return `
159035
+ # Code Block
159036
+
159037
+ For elements that need JavaScript, use a code block: <div data-cb-code>
159038
+ (that attribute alone marks it — no class, no other bookkeeping). ${placement} The code runs live while editing and
159039
+ runs again on the saved page.
159040
+
159041
+ A simple block is complete as written — no wrapper, no ceremony:
159042
+
159043
+ <div data-cb-code>
159044
+ <h1 id="greet7x">Lorem ipsum</h1>
159045
+ <p>This is a code block.</p>
159046
+ <script>
159047
+ document.querySelector('#greet7x').innerHTML = '<b>Hello World..!</b>';
159048
+ </script>
159049
+ </div>
159050
+
159051
+ Ids are safe: when a block is duplicated, the editor gives the copy fresh ids
159052
+ and updates the references inside its script to match.
159053
+
159054
+ For blocks that do more, these rules keep them working through editing:
159055
+
159056
+ - The editor re-renders the block on edit, undo and duplicate, and each
159057
+ render runs the code again on FRESH elements. Anything that keeps running
159058
+ — a timer, an interval, a window/document listener, an animation loop —
159059
+ must stop by itself when its elements leave the page. Check isConnected:
159060
+
159061
+ const display = document.querySelector('#clock9k');
159062
+ const iv = setInterval(() => {
159063
+ if (!display.isConnected) { clearInterval(iv); return; }
159064
+ display.textContent = new Date().toLocaleTimeString();
159065
+ }, 1000);
159066
+
159067
+ - Plain classic <script> tags only — never type="module". To load a library,
159068
+ use dynamic import() inside ONE async IIFE wrapping the whole script,
159069
+ closed with exactly one })(); at the end — never nest a second wrapper:
159070
+
159071
+ <script>
159072
+ (async () => {
159073
+ const confetti = (await import('https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/+esm')).default;
159074
+ // ... the rest of the block's code ...
159075
+ })();
159076
+ </script>
159077
+
159078
+ A classic library that defines a global loads with a created <script>
159079
+ element (check for an existing one first); CSS files the same way with a
159080
+ <link rel="stylesheet">. If you read document.currentScript, do it on the
159081
+ first line — it is null after the first await.
159082
+
159083
+ - Self-contained only: styles inline or in a <style> tag inside the block;
159084
+ never touch document-level styles or elements outside the block.
159085
+ `;
159086
+ }
159087
+
157588
159088
  // Design CORE — the output rules EVERY code request needs, including a single-
157589
159089
  // section edit. Minimal: how to output, no art direction, and deliberately NO
157590
159090
  // "<style> block for animations" nudge (the AI over-emits <style> when it's
@@ -157752,6 +159252,513 @@ const contextDesignGuide = `# Design Directive:
157752
159252
  - ❌ Cramped spacing (always err on the side of more whitespace)
157753
159253
  */
157754
159254
 
159255
+ /*
159256
+ * context-3d.js — the "3d" on-demand doc topic for Code Chat v2.
159257
+ *
159258
+ * Loaded via get_framework_docs ONLY when a request involves real-time 3D:
159259
+ * a three.js scene, WebGL content, a 3D product view, or a browser game.
159260
+ * It is never part of the resident system prompt — 3D requests are rare and
159261
+ * this reference is long, so it rides the same on-demand rail as "animation".
159262
+ *
159263
+ * The recipes here were verified in the ContentBuilder public/test-3d
159264
+ * experiments (Sept 2026): the quality shell, sky-as-light, instanced grass,
159265
+ * and the glb normalize pattern are lifted from working pages, not invented.
159266
+ * Keep code samples free of backticks and ${ } — this is a template literal.
159267
+ */
159268
+ const context3D = `
159269
+ # Real-Time 3D & Games (three.js)
159270
+
159271
+ Use this whenever the user asks for a 3D scene, 3D object, WebGL, a 3D
159272
+ product/hero visual, or a playable browser game. Everything lives in a code
159273
+ element (data-cb-code), which runs live in the editor and as plain HTML on
159274
+ the published page.
159275
+
159276
+ A finished-looking scene is mostly LIGHTING, not geometry. Never ship the
159277
+ naive setup (ambient + directional light, no environment, no tone mapping) —
159278
+ it makes every material look like flat plastic. Always use the Scene Shell
159279
+ below.
159280
+
159281
+ ## 1. The block
159282
+
159283
+ 3D lives in a code element: <div data-cb-code> (that attribute alone marks
159284
+ it; no class, no other bookkeeping). It sits inside a normal column, beside
159285
+ other content. The code runs LIVE while editing and runs again on the saved
159286
+ page, so what the user sees in the editor is the real scene.
159287
+
159288
+ <div data-cb-code>
159289
+ <div class="scene3d-stage" style="width:100%; aspect-ratio:16/9; position:relative; overflow:hidden; border-radius:8px; background:#101014;"></div>
159290
+ <script>
159291
+ (async () => {
159292
+ const container = document.currentScript.parentElement; // BEFORE any await
159293
+ const stage = container.querySelector('.scene3d-stage');
159294
+ // ... imports, then build the scene (see Scene Shell) ...
159295
+ })();
159296
+ </script>
159297
+ </div>
159298
+
159299
+ ONE async IIFE wraps the whole script — do not nest a second wrapper
159300
+ function, and close it with exactly one })(); at the end. Read
159301
+ document.currentScript on the first line: it is only set before the first
159302
+ await.
159303
+
159304
+ Rules that keep it healthy in the editor:
159305
+ - Plain classic <script> tags only — never type="module" (dynamic import()
159306
+ inside the classic script is the way to load modules).
159307
+ - Query elements from the container (document.currentScript.parentElement),
159308
+ not by id — the block can be duplicated, and container-scoped queries stay
159309
+ correct in every copy.
159310
+ - The editor re-renders the block on edit, undo and duplicate, and each
159311
+ render runs the code again on FRESH elements. Anything that keeps running
159312
+ — the render loop, timers, window/document listeners — must stop by itself
159313
+ when its elements leave the page. The shell's loop below does this with an
159314
+ isConnected check; apply the same rule to any setInterval or listener you
159315
+ add outside the loop.
159316
+ - The stage div is styled inline (width 100% + aspect-ratio); make it taller
159317
+ or full-bleed by changing that style, not by touching anything outside the
159318
+ block.
159319
+
159320
+ ## 2. Loading three.js
159321
+
159322
+ Load as ES modules with dynamic import inside the script. Use jsdelivr's
159323
+ +esm build and PIN the version — addons then resolve their internal 'three'
159324
+ import to the same module instance, so no import map is needed:
159325
+
159326
+ const THREE = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/+esm');
159327
+ const { OrbitControls } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/controls/OrbitControls.js/+esm');
159328
+ const { GLTFLoader } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/loaders/GLTFLoader.js/+esm');
159329
+ const { MeshoptDecoder } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/libs/meshopt_decoder.module.js/+esm');
159330
+ const { RoomEnvironment } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/environments/RoomEnvironment.js/+esm');
159331
+ const { Sky } = await import('https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/objects/Sky.js/+esm');
159332
+
159333
+ Import only what the scene uses. Every addon lives under examples/jsm/ with
159334
+ the same /+esm suffix.
159335
+
159336
+ ## 3. The Scene Shell — always
159337
+
159338
+ const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); // preserveDrawingBuffer: required by the view_render self-check
159339
+ renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
159340
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
159341
+ renderer.shadowMap.enabled = true;
159342
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
159343
+ stage.appendChild(renderer.domElement);
159344
+
159345
+ const scene = new THREE.Scene();
159346
+ const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 200);
159347
+
159348
+ // ENVIRONMENT LIGHT — the single biggest quality lever. Pick one:
159349
+ // (a) Indoor / product / abstract: the built-in room, no files needed.
159350
+ const pmrem = new THREE.PMREMGenerator(renderer);
159351
+ scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
159352
+ // (b) Outdoor: a physical sky that also LIGHTS the scene — see Environments.
159353
+ // (c) A specific mood: an equirect .hdr URL via RGBELoader, same mapping.
159354
+
159355
+ // KEY LIGHT with soft shadows grounds the objects.
159356
+ const sun = new THREE.DirectionalLight(0xfff2e0, 2.2);
159357
+ sun.position.set(4, 6, 3);
159358
+ sun.castShadow = true;
159359
+ sun.shadow.mapSize.set(2048, 2048);
159360
+ sun.shadow.radius = 5;
159361
+ sun.shadow.bias = -0.0005;
159362
+ scene.add(sun);
159363
+
159364
+ // SIZE to the stage, not the window. Handles editor column resizes too.
159365
+ function resize() {
159366
+ const w = stage.clientWidth, h = stage.clientHeight;
159367
+ if (!w || !h) return;
159368
+ camera.aspect = w / h;
159369
+ camera.updateProjectionMatrix();
159370
+ renderer.setSize(w, h);
159371
+ }
159372
+ new ResizeObserver(resize).observe(stage);
159373
+ resize();
159374
+
159375
+ // LOOP — stops itself when this render is replaced (edit/undo/duplicate),
159376
+ // and pauses while the block is off-screen or the tab is hidden.
159377
+ let visible = true;
159378
+ new IntersectionObserver(function(e) { visible = e[0].isIntersecting; }).observe(stage);
159379
+ const clock = new THREE.Clock();
159380
+ renderer.setAnimationLoop(function() {
159381
+ if (!stage.isConnected) { // this render was discarded
159382
+ renderer.setAnimationLoop(null);
159383
+ renderer.dispose();
159384
+ return;
159385
+ }
159386
+ if (!visible || document.hidden) return;
159387
+ const dt = Math.min(clock.getDelta(), 0.05);
159388
+ // ... update(dt) ...
159389
+ renderer.render(scene, camera);
159390
+ });
159391
+
159392
+ Materials: MeshStandardMaterial (metalness/roughness) or MeshPhysicalMaterial
159393
+ (clearcoat, transmission). With the environment set they pick up real
159394
+ reflections; without it, metals render black — that is the "flat plastic"
159395
+ failure. Shadows: castShadow/receiveShadow on meshes, receiveShadow on the
159396
+ ground. A ShadowMaterial plane (opacity ~0.35) gives a shadow-only floor for
159397
+ product shots.
159398
+
159399
+ ## 4. Objects: write code or load a .glb
159400
+
159401
+ - WRITE CODE for man-made, regular shapes: buildings, furniture, product
159402
+ mockups, abstract hero geometry. Compose primitives (Box, Cylinder,
159403
+ Extrude, Lathe, RoundedBoxGeometry addon), and add a subtle CanvasTexture
159404
+ for surface interest (wood planks, panel lines). Free, tiny, editable.
159405
+ - LOAD A .glb for organic or specific things: characters, animals, plants, a
159406
+ real product, anything that must match a reference image. Getting one:
159407
+ - When a generate_3d_model tool is offered, use it: give it ONE clear image
159408
+ of the object (a photo on the page, a generate_image result, or the
159409
+ user's attachment) and load the .glb URL it returns. For an object with
159410
+ no image yet, generate_image first, then feed that in — image quality
159411
+ decides model quality.
159412
+ - When no such tool is offered, build the object from primitives with
159413
+ real material craft instead — never fabricate a model URL.
159414
+
159415
+ Always set the meshopt decoder (generated models are usually
159416
+ meshopt-compressed; it is harmless for plain files), and normalize — glbs
159417
+ come in arbitrary scale:
159418
+
159419
+ const loader = new GLTFLoader();
159420
+ loader.setMeshoptDecoder(MeshoptDecoder);
159421
+ const gltf = await loader.loadAsync(url);
159422
+ const obj = gltf.scene;
159423
+ const box = new THREE.Box3().setFromObject(obj);
159424
+ const size = box.getSize(new THREE.Vector3());
159425
+ obj.scale.setScalar(TARGET_HEIGHT / Math.max(size.x, size.y, size.z));
159426
+ box.setFromObject(obj);
159427
+ const center = box.getCenter(new THREE.Vector3());
159428
+ obj.position.set(-center.x, -box.min.y, -center.z); // centered, on the floor
159429
+ obj.traverse(function(n) { if (n.isMesh) { n.castShadow = true; n.receiveShadow = true; } });
159430
+ scene.add(obj);
159431
+
159432
+ ## 5. Environments (outdoor scenes)
159433
+
159434
+ SKY THAT LIGHTS THE SCENE — golden hour in a few lines, no texture files:
159435
+
159436
+ const sky = new Sky();
159437
+ sky.scale.setScalar(4000);
159438
+ scene.add(sky);
159439
+ const sunDir = new THREE.Vector3().setFromSphericalCoords(1,
159440
+ THREE.MathUtils.degToRad(90 - 6), // elevation: 2-8 = sunset, 30+ = day
159441
+ THREE.MathUtils.degToRad(245)); // azimuth
159442
+ sky.material.uniforms.sunPosition.value.copy(sunDir);
159443
+ sky.material.uniforms.turbidity.value = 6;
159444
+ sky.material.uniforms.rayleigh.value = 2.2;
159445
+ scene.environment = pmrem.fromScene(sky, 0.02).texture; // the sky IS the light
159446
+ sun.position.copy(sunDir).multiplyScalar(120); // key light matches it
159447
+ renderer.toneMappingExposure = 0.55; // Sky is bright; tune 0.4-0.7
159448
+ scene.fog = new THREE.FogExp2(0xe8c8a0, 0.0035); // depth haze sells distance
159449
+
159450
+ TERRAIN — a displaced plane. Define ONE height function and reuse it for
159451
+ placing everything (grass, props) so nothing floats:
159452
+
159453
+ function ground(x, z) {
159454
+ return 1.1 * Math.sin(x * 0.045 + 1) * Math.cos(z * 0.05)
159455
+ + 0.5 * Math.sin(x * 0.11 + 2.3) * Math.sin(z * 0.13 + 1.1);
159456
+ }
159457
+ Build a PlaneGeometry(240, 240, 180, 180), rotateX(-PI/2), set each vertex y
159458
+ from ground(x, z), give vertices green colors varied by height, then
159459
+ computeVertexNormals() and a MeshStandardMaterial({ vertexColors: true,
159460
+ roughness: 1 }).
159461
+
159462
+ WATER — a large plane with a dark blue-grey MeshStandardMaterial and
159463
+ roughness around 0.15. With the sky environment set it reflects the sunset
159464
+ by itself. Place it below the terrain edge.
159465
+
159466
+ GRASS / vegetation — NEVER individual meshes, always ONE InstancedBufferGeometry
159467
+ draw call. One tapered blade (a 4-row triangle strip about 0.018 wide, 1 tall),
159468
+ instanced 50k-150k times with per-instance attributes: offset vec3 (position,
159469
+ y from ground()), data vec4 (rotation angle, height 0.35-0.9, wind phase,
159470
+ tint 0.8-1.2). A small ShaderMaterial does the rest:
159471
+ - vertex: scale by height, bend tip (p.z += t*t*0.1), rotate by angle, sway by
159472
+ sin(time * 1.5 + phase + offset.x * 0.15) * 0.14 * t * t, add offset.
159473
+ - fragment: mix a dark base green (0.07, 0.13, 0.03) to a light tip green
159474
+ (0.38, 0.46, 0.13) by the blade's v coordinate, times tint; optionally add a
159475
+ small warm term at the very tip for sunset scenes.
159476
+ Scatter on a disc around the focal point (radius * sqrt(random) keeps it
159477
+ even), skip blades under objects, set instanceCount, frustumCulled = false,
159478
+ side: THREE.DoubleSide. Update the time uniform in the loop. The same
159479
+ technique makes flowers, reeds and starfields — change the geometry and count.
159480
+
159481
+ ## 6. Interaction
159482
+
159483
+ HOVER + CLICK on 3D objects — a raycaster against the renderer bounds (never
159484
+ window coordinates; the canvas is inside a column):
159485
+
159486
+ const ray = new THREE.Raycaster();
159487
+ const pointer = new THREE.Vector2();
159488
+ function pick(e) {
159489
+ const r = renderer.domElement.getBoundingClientRect();
159490
+ pointer.set(((e.clientX - r.left) / r.width) * 2 - 1,
159491
+ -((e.clientY - r.top) / r.height) * 2 + 1);
159492
+ ray.setFromCamera(pointer, camera);
159493
+ const hit = ray.intersectObjects(pickables, true)[0];
159494
+ return hit ? hit.object : null;
159495
+ }
159496
+ renderer.domElement.addEventListener('pointermove', function(e) {
159497
+ const o = pick(e);
159498
+ renderer.domElement.style.cursor = o ? 'pointer' : '';
159499
+ // hover response: scale/emissive lerp in the loop, not a snap
159500
+ });
159501
+ renderer.domElement.addEventListener('click', function(e) {
159502
+ const o = pick(e); if (o) { /* select, open, score ... */ }
159503
+ });
159504
+
159505
+ Animate responses smoothly in the loop (lerp toward a target scale/rotation/
159506
+ color) instead of setting values on the event — that is what makes it feel
159507
+ polished. Camera orbit: OrbitControls with enableDamping = true; constrain
159508
+ (maxPolarAngle, min/maxDistance) so users cannot go under the floor.
159509
+
159510
+ REVEAL PATTERNS — a click should MEAN something, not just wobble. Four
159511
+ recurring shapes, all built from the same pieces (pick + lerp + HTML):
159512
+
159513
+ CAMERA FOCUS (product tour): click an object, fly the camera to frame it,
159514
+ and offer the way back.
159515
+
159516
+ let camTo = null, lookTo = null;
159517
+ function focus(obj) {
159518
+ const p = new THREE.Vector3();
159519
+ obj.getWorldPosition(p);
159520
+ camTo = p.clone().add(new THREE.Vector3(1.4, 0.8, 2.0)); // offset that frames it
159521
+ lookTo = p.clone();
159522
+ backBtn.style.display = 'block';
159523
+ }
159524
+ function overview() {
159525
+ camTo = HOME_POS.clone(); lookTo = HOME_TARGET.clone();
159526
+ backBtn.style.display = 'none'; card.style.display = 'none';
159527
+ }
159528
+ // in the loop:
159529
+ if (camTo) {
159530
+ const k = 1 - Math.exp(-dt * 4);
159531
+ camera.position.lerp(camTo, k);
159532
+ controls.target.lerp(lookTo, k);
159533
+ if (camera.position.distanceTo(camTo) < 0.01) { camTo = null; lookTo = null; }
159534
+ }
159535
+ controls.addEventListener('start', function() { camTo = null; lookTo = null; }); // user drag wins
159536
+
159537
+ INFO CARD: the revealed details are real HTML overlaid on the stage — never
159538
+ 3D text. A fixed-position card (absolute inside the stage, e.g. right side)
159539
+ reads better than one tracking the 3D point, and never clips. Fill it per
159540
+ object from a plain data object keyed by mesh name; give it a real <button>
159541
+ to close, and close on Escape too. Selectable, accessible, translatable.
159542
+
159543
+ STATE REVEAL: the click changes the scene itself — a lid opens, parts fan
159544
+ out into an exploded view, the chosen item stays lit while the rest dim.
159545
+ Record each part's home transform at build time, keep one 0..1 reveal value
159546
+ per group, and lerp every part between home and revealed each frame from
159547
+ that value. Dimming: lerp material color or opacity, never a CSS filter.
159548
+
159549
+ SCENE-TO-PAGE REVEAL: when the revealed content belongs in the page (specs,
159550
+ paragraphs, a price table), toggle a normal HTML element NEXT TO the stage
159551
+ instead of an overlay. The content stays selectable and readable without
159552
+ WebGL; the scene is the index, the page is the content.
159553
+
159554
+ Shared rules: one selection at a time; clicking empty space = Back; Back and
159555
+ close are real, keyboard-reachable buttons; everything moves through the
159556
+ loop's lerp, nothing snaps.
159557
+
159558
+ Touch: pointer events above already cover it; OrbitControls handles touch
159559
+ orbit. Do not call preventDefault on wheel/touch you do not use — the page
159560
+ must keep scrolling normally past the block.
159561
+
159562
+ SCROLL-DRIVEN scenes: read progress from the block's own position — works
159563
+ everywhere:
159564
+
159565
+ function scrollProgress() { // 0 when entering, 1 when leaving
159566
+ const r = stage.getBoundingClientRect();
159567
+ return THREE.MathUtils.clamp((innerHeight - r.top) / (innerHeight + r.height), 0, 1);
159568
+ }
159569
+ Call it in the loop and drive camera/objects from it (again through lerp for
159570
+ smoothness). On a ContentBox page whose section has data-fx-emit, prefer the
159571
+ host's event: container.closest('.is-section').addEventListener('fx:progress',
159572
+ function(e) { p = e.detail.progress; }).
159573
+
159574
+ ## 7. Games (3D)
159575
+
159576
+ Game craft — loop, input, state machine, HUD, sound, recycling, cleanup —
159577
+ lives in the "games" reference: load it for any game. This section is only
159578
+ what makes a THREE.JS game look and feel right:
159579
+
159580
+ - The Scene Shell applies to games in full. Environment lighting, real
159581
+ materials, fog and shadows are what separate a game from a tech demo —
159582
+ never fall back to flat MeshBasicMaterial arcade defaults. Emissive
159583
+ materials + bloom give the cheap "neon glow" so many game styles want.
159584
+ - HERO ASSETS: the 2-3 entities that carry the whole look — the player
159585
+ craft, the collectible, one signature obstacle — deserve generated models
159586
+ (generate_3d_model when offered). Load each .glb ONCE, then obj.clone()
159587
+ per spawn; cloning is as cheap as cloning boxes, so recycling pools work
159588
+ unchanged. This single choice is the difference between "colored cubes"
159589
+ and a game with an identity.
159590
+ - FILLER stays coded, but crafted: RoundedBoxGeometry over BoxGeometry,
159591
+ slight bevels, emissive rims, vertex-colored gradients — never bare
159592
+ saturated boxes.
159593
+ - Movement feel: lerp positions and tilt/bank the player toward its motion;
159594
+ camera follows with damping, never rigidly.
159595
+ - Update per frame from the update(dt) function; collisions at this scale
159596
+ are distance checks (player.position.distanceTo(e.position) < r1 + r2).
159597
+
159598
+ ## 8. Self-check — see your work
159599
+
159600
+ When a view_render tool is offered, ALWAYS call it after creating a scene or
159601
+ making a significant visual change, and LOOK at the screenshot before
159602
+ answering:
159603
+ - All black or flat: the environment/lighting is missing, or every object is
159604
+ outside the camera view.
159605
+ - Empty or nearly empty: camera position/target does not frame the content —
159606
+ check object positions and scale.
159607
+ - Clipped, tiny or washed out: adjust framing, scale or exposure.
159608
+ Fix what is wrong, re-check once, and only then answer. Do not describe a
159609
+ scene you have not seen. (The screenshot shows the canvas only — HTML
159610
+ overlays are not in it.)
159611
+
159612
+ Judge the render like an art director: the bar is FINISHED, not merely
159613
+ correct. A right-shaped scene rendered badly — glaring or flat light, dead
159614
+ materials, awkward framing, harsh exposure — is not done. Weigh the impact
159615
+ yourself, and reach for the cheap lever first:
159616
+ - Anything fixable in code — lighting, environment, exposure, camera,
159617
+ materials, scale — is FREE. If a change would noticeably improve the
159618
+ scene, make it.
159619
+ - Regenerating a model costs the user money. Do it only when the asset
159620
+ itself is the problem (mangled geometry, clearly the wrong object) and
159621
+ code cannot compensate — better light, exposure and framing rescue a
159622
+ mediocre asset more often than a re-roll does.
159623
+ When the scene genuinely reads well, it is done — stop; do not iterate on
159624
+ differences a viewer would not notice.
159625
+
159626
+ ## 9. Performance & hygiene
159627
+
159628
+ - One scene per block; reuse geometries/materials; instancing for anything
159629
+ repeated more than ~20 times.
159630
+ - Keep pixelRatio capped at 2 and shadow maps at 2048 or below.
159631
+ - The visibility pause in the shell is mandatory — a page can hold several
159632
+ blocks and background tabs must not burn CPU.
159633
+ - Respect prefers-reduced-motion: if it matches, skip autonomous camera
159634
+ motion and wind sway; render still frames on interaction only.
159635
+ - Do not touch document-level styles, and never assume the block is the only
159636
+ 3D on the page.
159637
+ `;
159638
+
159639
+ /*
159640
+ * context-games.js — the "games" on-demand doc topic for Code Chat v2.
159641
+ *
159642
+ * Loaded via get_framework_docs for ANY game request, 2D or 3D. It owns the
159643
+ * craft every game shares (loop, input, state, HUD, sound, cleanup) plus the
159644
+ * 2D Canvas branch; a 3D game additionally loads the "3d" topic, whose games
159645
+ * section holds only the 3D-specific rendering rules.
159646
+ *
159647
+ * Asset reality check (user-verified, Sept 2026): most image models FAIL at
159648
+ * transparent PNGs — sprites are requested on solid backgrounds and handled
159649
+ * in code. Never instruct the model to ask for transparency.
159650
+ * Keep code samples free of backticks and ${ } — template literal.
159651
+ */
159652
+ const contextGames = `
159653
+ # Building Games
159654
+
159655
+ Use this for any playable game. First decide the dimension — it changes the
159656
+ whole toolchain:
159657
+
159658
+ - 2D (doodle, flat, sprite, puzzle, casual, "simple game"): the Canvas 2D
159659
+ API in a code block. Do NOT use three.js for these — it is heavier, slower
159660
+ to write, and fights the flat aesthetic.
159661
+ - 3D (depth, camera, "3D"): three.js — ALSO load the "3d" reference and
159662
+ follow its games section for rendering and assets.
159663
+
159664
+ Target scale either way: an arcade mini-game — one scene, one core mechanic,
159665
+ polished. Refine with the user over turns.
159666
+
159667
+ ## The skeleton every game shares
159668
+
159669
+ Everything lives in one code block (see the "code" reference for the block
159670
+ contract). Inside it:
159671
+
159672
+ - LOOP: requestAnimationFrame with a clamped delta; stop yourself when the
159673
+ block is re-rendered:
159674
+
159675
+ let last = performance.now();
159676
+ function frame(now) {
159677
+ if (!stage.isConnected) return; // this render was replaced
159678
+ const dt = Math.min((now - last) / 1000, 0.05);
159679
+ last = now;
159680
+ update(dt); draw();
159681
+ requestAnimationFrame(frame);
159682
+ }
159683
+ requestAnimationFrame(frame);
159684
+
159685
+ - INPUT: one flags object; keyboard listeners attach on game start (the
159686
+ start click also gives focus), ignore keys while the user types elsewhere,
159687
+ and detach themselves via the isConnected check. Map pointerdown /
159688
+ pointermove on the stage to the same flags so touch works. Do not
159689
+ preventDefault on events you do not use — the page must keep scrolling.
159690
+ - STATE: a small machine — ready / playing / over — with restart resetting
159691
+ state, never reloading. Show "click or tap to start".
159692
+ - HUD: HTML positioned over the stage (score, lives, messages) — real text,
159693
+ updated via textContent, never drawn glyph-by-glyph into the canvas.
159694
+ - SOUND: only if asked; short WebAudio oscillator beeps, no external files.
159695
+ - ENTITIES: recycle, never allocate per frame — spawn from a pool, move
159696
+ off-screen items back in.
159697
+ - SELF-CHECK: when a view_render tool is offered, screenshot after building
159698
+ and after big visual changes, judge it, fix, re-check once.
159699
+
159700
+ ## 2D games (Canvas 2D)
159701
+
159702
+ Setup — crisp on every screen:
159703
+
159704
+ <div data-cb-code>
159705
+ <div class="game-stage" style="position:relative; width:100%; aspect-ratio:16/10; overflow:hidden; border-radius:8px; background:#fdfdf8;">
159706
+ <canvas style="position:absolute; inset:0; width:100%; height:100%;"></canvas>
159707
+ <!-- HUD elements here -->
159708
+ </div>
159709
+ <script>
159710
+ (async () => {
159711
+ const container = document.currentScript.parentElement;
159712
+ const stage = container.querySelector('.game-stage');
159713
+ const canvas = stage.querySelector('canvas');
159714
+ const ctx = canvas.getContext('2d');
159715
+ function fit() {
159716
+ const dpr = Math.min(devicePixelRatio, 2);
159717
+ canvas.width = stage.clientWidth * dpr;
159718
+ canvas.height = stage.clientHeight * dpr;
159719
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // draw in CSS pixels
159720
+ }
159721
+ new ResizeObserver(fit).observe(stage);
159722
+ fit();
159723
+ // ... game ...
159724
+ })();
159725
+ </script>
159726
+ </div>
159727
+
159728
+ Collision at this scale is rectangles and circle distances — no physics
159729
+ library unless the game truly is physics.
159730
+
159731
+ ART DIRECTION — a 2D game must look intentional, not like debug rectangles:
159732
+ pick a style (hand-drawn doodle, flat pastel, neon minimal) and carry it
159733
+ through background, entities, HUD typography and even the game-over screen.
159734
+
159735
+ - DOODLE STYLE, pure code: draw with strokes, not fills — rounded lineJoin,
159736
+ slightly thick lines, and a small random jitter added to each vertex,
159737
+ re-rolled only a few times per second (a wobble timer, not every frame) so
159738
+ shapes shiver like a flipbook. Paper background (#fdfdf8, faint grid),
159739
+ handwritten-feel HUD (a casual system font, slight rotation).
159740
+ - SPRITES from generate_image, when the style needs real drawings: request
159741
+ each sprite "on a plain solid white background, no shadow" in one
159742
+ consistent style. Do NOT request transparent backgrounds — image models
159743
+ usually fail at true transparency, delivering fake checkerboards or dirty
159744
+ edges. Then either:
159745
+ - design the game on the SAME background color, so the sprite's white
159746
+ simply disappears (the doodle-on-paper trick — zero code); or
159747
+ - key it out at load: draw to an offscreen canvas, getImageData, and set
159748
+ alpha 0 on pixels close to the corner color (tolerance ~28); cache the
159749
+ result and draw that.
159750
+ - Parallax sells motion cheaply: two or three background layers scrolling at
159751
+ different speeds.
159752
+
159753
+ ## 3D games
159754
+
159755
+ Load the "3d" reference and follow its games section. The short version:
159756
+ the scene shell (environment light, real materials, fog) applies to games
159757
+ exactly as to scenes — no flat-material arcade fallback — and the 2-3 hero
159758
+ entities that carry the look (player, collectible, signature obstacle)
159759
+ deserve generated models, loaded once and cloned per spawn.
159760
+ `;
159761
+
157755
159762
  class IframePanel {
157756
159763
  constructor(opts = {}, builder) {
157757
159764
  let defaults = {
@@ -166148,7 +168155,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
166148
168155
  motionPathPreserve: contextMotionPathPreserve + '\n---\n' + contextMotionPath
166149
168156
  };
166150
168157
  this.codechat = new CodeChat({
166151
- // context: contextContentFramework + contextBoxFramework + contextAnimFramework + contextCodeBlock + contextDesignGuide,
168158
+ // context: contextContentFramework + contextBoxFramework + contextAnimFramework + getContextCodeBlock({ overlay: true }) + contextDesignGuide,
166152
168159
  context: contextContentFramework + contextBoxFramework + contextAnimFramework + contextDesignGuide,
166153
168160
  // Separable pieces + mode let CodeChat include only the frameworks a
166154
168161
  // task needs when `context` is 'selection'/'optimized' (see defaults).
@@ -166182,6 +168189,11 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
166182
168189
  box: contextBoxFramework,
166183
168190
  animation: contextAnimFramework,
166184
168191
  designCore: contextDesignCore,
168192
+ '3d': context3D,
168193
+ games: contextGames,
168194
+ code: getContextCodeBlock({
168195
+ overlay: true
168196
+ }),
166185
168197
  ...motionPathParts
166186
168198
  }
166187
168199
  });
@@ -168887,6 +170899,15 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
168887
170899
  }
168888
170900
  });
168889
170901
  this.editor.applyBehavior();
170902
+ /*
170903
+ A code element can sit outside an editable area — a full-bleed block in
170904
+ .is-overlay-content, for instance. applyBehavior() only walks the
170905
+ .is-builder areas, so those blocks would keep their inert
170906
+ <template data-source> and never render or run. init() is idempotent:
170907
+ a block that is already rendered is left alone.
170908
+ */
170909
+
170910
+ if (this.editor.codeElement) this.editor.codeElement.init(this.wrapperEl);
168890
170911
  this.addSpace();
168891
170912
  /* If animation enabled on page, hide some tools during scroll/animate */
168892
170913
 
@@ -169936,7 +171957,10 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
169936
171957
  }
169937
171958
  }
169938
171959
  }
169939
- }); // Code Blocks Handling
171960
+ }); // Code elements ([data-cb-code]) anywhere in the section, including
171961
+ // an overlay layer, which applyBehavior() does not reach
171962
+
171963
+ if (this.editor.codeElement) this.editor.codeElement.init(section); // Code Blocks Handling
169940
171964
 
169941
171965
  let codeBlocks = section.querySelectorAll('[data-html]');
169942
171966
  codeBlocks.forEach(element => {