Aluode commited on
Commit
e85709c
·
verified ·
1 Parent(s): 0aedcbf

Upload 21 files

Browse files
Files changed (22) hide show
  1. .gitattributes +2 -0
  2. README.md +80 -13
  3. app.py +174 -0
  4. bet5_gabor_sds.py +604 -0
  5. build.js +11 -0
  6. builtins_preview.png +0 -0
  7. make_builtins.py +71 -0
  8. node +0 -0
  9. oracle.py +237 -0
  10. pic.png +3 -0
  11. pic2.png +3 -0
  12. python +0 -0
  13. requirements.txt +9 -0
  14. studio/build.js +11 -0
  15. studio/builtins.json +1 -0
  16. studio/core.js +412 -0
  17. studio/studio.html +1495 -0
  18. studio/studio_core.js +135 -0
  19. studio_core.js +135 -0
  20. studio_ui.html +962 -0
  21. tests_studio.js +150 -0
  22. trim_check.png +0 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ pic.png filter=lfs diff=lfs merge=lfs -text
37
+ pic2.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,15 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: SlapStackStudio
3
- emoji: 👀
4
- colorFrom: pink
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 6.19.0
8
- python_version: '3.13'
9
- app_file: app.py
10
- pinned: false
11
- license: mit
12
- short_description: '2D gabor splats that know borders via bp that can be moved. '
13
- ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Slapstack Studio (Bet 8 Playroom)
2
+
3
+ ![pic2](pic2.png)
4
+
5
+ An interactive, layered visual sandbox running multi-object Belief Propagation (BP) over Gabor packet representations. The system supports full 2D depth occlusion matting, spatial grab/drag/scale/rotate mechanics, semantic prompt interventions, and server-side neural layer distillation via Score Distillation Sampling (SDS).
6
+
7
+ ## Architecture & Division of Labor
8
+
9
+ - **The Server (Python):** Handles visual generation and heavy pixel-to-atom fitting. It uses an SDS pipeline with Stable Diffusion 2.1 or deterministic MSE optimization to turn pictures/prompts into compact, normalized Gabor-atom templates.
10
+ - **The Client (Browser JS):** Runs a highly optimized, loopy Belief Propagation factor graph entirely client-side. The frontend manages pose clustering, tracks identities through deep occlusion, calculates real-time alpha masks, and translates user gestures into exact group-algebra conditioning updates.
11
+
12
  ---
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ ## File Structure
15
+
16
+ ```text
17
+ slapstack-bet8/
18
+ ├── app.py # HuggingFace Space / Gradio server entry point
19
+ ├── oracle.py # Image-to-atom fit & text-to-atom SDS generation paths
20
+ ├── bet5_gabor_sds.py # Underlying Gabor Packet model & differentiable renderer
21
+ ├── requirements.txt # Python dependencies for GPU/CPU runtimes
22
+
23
+ ├── studio/ # Static folder mounted by FastAPI
24
+ │ ├── studio.html # Single-file production client (fully built)
25
+ │ └── builtins.json # Pre-trained compact templates (tractor, star, boat)
26
+
27
+ ├── studio_ui.html # Raw source markup for the client layout
28
+ ├── studio_core.js # Real-time alpha-matting & painter compositor logic
29
+ ├── build.js # Assembly script (combines UI + core logic into studio.html)
30
+ └── tests_studio.js # Headless test suite validating depth occlusion math
31
+ ```
32
+
33
+ ## Getting Started
34
+
35
+ ### Prerequisites
36
+
37
+ Make sure your environment has Python 3.10+ installed. For neural text-to-layer distillation, a CUDA-capable GPU is highly recommended.
38
+
39
+ ### Installation
40
+
41
+ 1. Clone or download the repository directory.
42
+ 2. Install the necessary dependencies directly via pip:
43
+
44
+ ```bash
45
+ pip install -r requirements.txt
46
+ ```
47
+
48
+ ### Running the App Locally
49
+
50
+ Launch the Gradio server by executing:
51
+
52
+ ```bash
53
+ python app.py
54
+ ```
55
+
56
+ Open your browser and navigate to http://127.0.0.1:7860. The unified workspace will load instantly without requiring any building or file compilation.
57
+
58
+ ## Feature Overview & Control Guide
59
+
60
+ ### Interactive Canvas
61
+
62
+ - **Scramble:** Randomizes object poses to throw all atoms into an unassigned "soup."
63
+ - **Bind:** Runs loopy BP live in the browser. You can watch the atoms settle, crystallize into boundaries, and snap back to their matching identity profiles.
64
+ - **Mouse Controls (Grabbable Posterior):**
65
+ - Click + Drag any recognized object to clamp its position. The environment dynamically re-settles around it.
66
+ - Mouse Wheel rotates the targeted object about its calculated center.
67
+ - Shift + Mouse Wheel scales the targeted object up or down.
68
+
69
+ ### Layer Manipulation & Occlusion Semantics
70
+
71
+ - Switch the scene blend setting from **Field** (additive summation) to **Painter** to observe full alpha-compositing across individual depths.
72
+ - Moving an object entirely behind another activates **Depth Occlusion**. Covered atoms cleanly release their assignments and revert back toward a state of uniform uncertainty (their marginal prior), while the shape's pose boundary coasts seamlessly behind the obstruction. Moving the object away re-establishes evidence and triggers immediate re-binding.
73
+
74
+ ### Spawning Custom Layers
75
+
76
+ - **Fit (CPU):** Upload any image containing a subject on a neutral, solid gray background. The optimizer will spend its entire atom budget crafting a tight, isolated layer template.
77
+ - **Distill (GPU):** Enter a descriptive text prompt to distill an atom map from Stable Diffusion.
78
+ - *Note for Local Deployment:* The system is pre-configured to look for `sd2-community/stable-diffusion-2-1-base` to efficiently reuse local caches located under `~/.cache/huggingface/hub/`.
79
+
80
+ ### Prompt Interventions
81
+
82
+ Enter clean, natural commands in the prompt box (e.g., *"move the boat left, send the star behind the tractor"*) to execute hard algebraic transformations. The graph instantly recalculates the new joint posterior distribution around your action.
app.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Slapstack Studio — HuggingFace Space server.
4
+
5
+ Layout:
6
+ / Gradio app: the Studio (iframe) + generation tabs + ledger
7
+ /studio/… static: the single-file interactive client (verified JS BP)
8
+ /gradio_api/… Gradio REST API, called by the client JS:
9
+ layer_from_image_b64(png_b64, n_atoms, iters) -> JSON
10
+ layer_from_text(prompt, negative, n_atoms, iters, cfg) -> JSON
11
+
12
+ Division of labor (the whole point):
13
+ the SERVER knows what things look like (SD oracle / image fitting),
14
+ the CLIENT knows what is where and how sure (verified BP in the browser).
15
+ """
16
+
17
+ import base64
18
+ import io
19
+ import json
20
+ import os
21
+
22
+ import gradio as gr
23
+ import numpy as np
24
+ from PIL import Image
25
+
26
+ from oracle import fit_image, sds_layer, preview_png_bytes
27
+
28
+ MAX_ATOMS = 256
29
+ MAX_ITERS_CPU = 800
30
+ MAX_ITERS_GPU = 1500
31
+
32
+
33
+ def _layer_payload(atoms, ledger):
34
+ png = preview_png_bytes(atoms, 192)
35
+ return json.dumps({
36
+ "atoms": np.asarray(atoms).round(5).tolist(),
37
+ "preview_png_b64": base64.b64encode(png).decode(),
38
+ "ledger": ledger,
39
+ })
40
+
41
+
42
+ # ---------------- endpoints (also used by the studio client JS) -------------
43
+
44
+ def layer_from_image_b64(png_b64: str, n_atoms: float, iters: float) -> str:
45
+ """b64 PNG/JPEG -> Gabor layer JSON. CPU path, verified."""
46
+ raw = base64.b64decode(png_b64.split(",")[-1])
47
+ img = Image.open(io.BytesIO(raw))
48
+ n_atoms = int(min(max(n_atoms, 16), MAX_ATOMS))
49
+ iters = int(min(max(iters, 50), MAX_ITERS_CPU))
50
+ atoms, ledger = fit_image(img, n_atoms=n_atoms, iters=iters)
51
+ return _layer_payload(atoms, ledger)
52
+
53
+
54
+ def layer_from_text(prompt: str, negative: str, n_atoms: float,
55
+ iters: float, cfg: float) -> str:
56
+ """text -> Gabor layer JSON via SDS. GPU only; honest error on CPU."""
57
+ n_atoms = int(min(max(n_atoms, 32), MAX_ATOMS))
58
+ iters = int(min(max(iters, 100), MAX_ITERS_GPU))
59
+ atoms, ledger = sds_layer(prompt, negative_prompt=negative or
60
+ "blurry, low quality, deformed",
61
+ n_atoms=n_atoms, iters=iters, cfg=float(cfg))
62
+ return _layer_payload(atoms, ledger)
63
+
64
+
65
+ # ---------------- human-facing wrappers for the Gradio tabs -----------------
66
+
67
+ def ui_from_image(img, n_atoms, iters):
68
+ if img is None:
69
+ raise gr.Error("upload an image first")
70
+ buf = io.BytesIO()
71
+ img.save(buf, "PNG")
72
+ out = layer_from_image_b64(base64.b64encode(buf.getvalue()).decode(),
73
+ n_atoms, iters)
74
+ d = json.loads(out)
75
+ prev = Image.open(io.BytesIO(base64.b64decode(d["preview_png_b64"])))
76
+ led = dict(d["ledger"]); led.pop("log", None)
77
+ return prev, json.dumps(led, indent=2), out
78
+
79
+
80
+ def ui_from_text(prompt, negative, n_atoms, iters, cfg):
81
+ if not (prompt or "").strip():
82
+ raise gr.Error("write a prompt first")
83
+ out = layer_from_text(prompt, negative, n_atoms, iters, cfg)
84
+ d = json.loads(out)
85
+ prev = Image.open(io.BytesIO(base64.b64decode(d["preview_png_b64"])))
86
+ led = dict(d["ledger"]); led.pop("log", None)
87
+ return prev, json.dumps(led, indent=2), out
88
+
89
+
90
+ CSS = """
91
+ .studio-frame iframe { width: 100%; height: 860px; border: 0; border-radius: 8px; }
92
+ """
93
+
94
+ with gr.Blocks(title="Slapstack Studio", css=CSS) as demo:
95
+ gr.Markdown(
96
+ "# Slapstack Studio\n"
97
+ "**Generate Gabor-atom layers with AI, then move them, occlude them, "
98
+ "and watch belief propagation keep track.** Every entity in the "
99
+ "studio is a posterior: layers are recovered from an unlabeled atom "
100
+ "soup by BP, a drag is a pose clamp, occlusion honestly widens the "
101
+ "belief. The interactive engine below is a JS port verified against "
102
+ "the SlapstackBet6 Python to 2e-16 (transform), 8.6e-8 (render MSE), "
103
+ "identical BP accuracy.")
104
+ with gr.Tab("Studio"):
105
+ gr.HTML('<div class="studio-frame">'
106
+ '<iframe src="/studio/studio.html"></iframe></div>')
107
+ with gr.Tab("Layer from image (CPU, verified)"):
108
+ with gr.Row():
109
+ with gr.Column():
110
+ in_img = gr.Image(type="pil", label="image")
111
+ in_na = gr.Slider(32, MAX_ATOMS, 140, step=4, label="atom budget")
112
+ in_it = gr.Slider(100, MAX_ITERS_CPU, 400, step=50, label="fit iterations")
113
+ btn_i = gr.Button("Fit layer", variant="primary")
114
+ with gr.Column():
115
+ out_prev_i = gr.Image(label="layer preview (atoms only)")
116
+ out_led_i = gr.Textbox(label="ledger", lines=8)
117
+ out_json_i = gr.Textbox(label="layer JSON (paste into the Studio)",
118
+ lines=4, max_lines=4)
119
+ btn_i.click(ui_from_image, [in_img, in_na, in_it],
120
+ [out_prev_i, out_led_i, out_json_i])
121
+ with gr.Tab("Layer from text (GPU, untested)"):
122
+ gr.Markdown(
123
+ "Score-distillation of a fresh atom population against Stable "
124
+ "Diffusion 2.1 — a line-for-line adaptation of the Bet-5 SDS "
125
+ "loop that was verified on GPU, but **this exact function has "
126
+ "not been executed yet**; the first run is a smoke test. On CPU "
127
+ "hardware this tab refuses honestly. Known carried-over risk: "
128
+ "SD2.1 mode-seeking oversaturation at high CFG.")
129
+ with gr.Row():
130
+ with gr.Column():
131
+ in_pr = gr.Textbox(label="prompt", placeholder="a red tractor, side view, flat background")
132
+ in_ng = gr.Textbox(label="negative prompt", value="blurry, low quality, deformed")
133
+ in_na2 = gr.Slider(32, MAX_ATOMS, 192, step=4, label="atom budget")
134
+ in_it2 = gr.Slider(100, MAX_ITERS_GPU, 900, step=50, label="SDS iterations")
135
+ in_cfg = gr.Slider(5, 60, 30, step=1, label="CFG")
136
+ btn_t = gr.Button("Distill layer", variant="primary")
137
+ with gr.Column():
138
+ out_prev_t = gr.Image(label="layer preview (atoms only)")
139
+ out_led_t = gr.Textbox(label="ledger", lines=8)
140
+ out_json_t = gr.Textbox(label="layer JSON (paste into the Studio)",
141
+ lines=4, max_lines=4)
142
+ btn_t.click(ui_from_text, [in_pr, in_ng, in_na2, in_it2, in_cfg],
143
+ [out_prev_t, out_led_t, out_json_t])
144
+
145
+ # API-only endpoints for the studio client (string in/out, no FileData)
146
+ api_b64_in = gr.Textbox(visible=False)
147
+ api_na = gr.Number(visible=False, value=140)
148
+ api_it = gr.Number(visible=False, value=400)
149
+ api_out = gr.Textbox(visible=False)
150
+ gr.Button(visible=False).click(layer_from_image_b64,
151
+ [api_b64_in, api_na, api_it], api_out,
152
+ api_name="layer_from_image_b64")
153
+ api_pr = gr.Textbox(visible=False)
154
+ api_ng = gr.Textbox(visible=False)
155
+ api_na2 = gr.Number(visible=False, value=192)
156
+ api_it2 = gr.Number(visible=False, value=900)
157
+ api_cfg = gr.Number(visible=False, value=30)
158
+ api_out2 = gr.Textbox(visible=False)
159
+ gr.Button(visible=False).click(layer_from_text,
160
+ [api_pr, api_ng, api_na2, api_it2, api_cfg],
161
+ api_out2, api_name="layer_from_text")
162
+
163
+ # ---------------- FastAPI mount: static studio + gradio ---------------------
164
+ from fastapi import FastAPI
165
+ from fastapi.staticfiles import StaticFiles
166
+
167
+ app = FastAPI()
168
+ app.mount("/studio", StaticFiles(directory=os.path.join(
169
+ os.path.dirname(__file__), "studio")), name="studio")
170
+ app = gr.mount_gradio_app(app, demo, path="/")
171
+
172
+ if __name__ == "__main__":
173
+ import uvicorn
174
+ uvicorn.run(app, host="0.0.0.0", port=7860)
bet5_gabor_sds.py ADDED
@@ -0,0 +1,604 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ BET 5 — Object permanence in Gabor packet space.
4
+ =================================================
5
+
6
+ Question: if we FREEZE the geometry channels of an existing atom set and let
7
+ SDS gradients touch only appearance (amp / color / phase / background), does
8
+ the object HOLD its shape while the scene relights?
9
+
10
+ "at dusk, it turns to dusk, the tractor stays."
11
+
12
+ This is the empirical test of the fiber-bundle claim: illumination acts on
13
+ the amplitude/chroma fibers; geometry (x, y, theta, sigma, f) is the base
14
+ space and should be untouchable by a relighting edit.
15
+
16
+ Carries forward everything verified in Bet 4 (GO: recognizable tractor from
17
+ 256 atoms under pure SDS) plus the fixes that run demanded:
18
+
19
+ * SOFT clamp pre = 4*tanh(pre/4) -- no zero-gradient dead zones
20
+ (Bet 4's hard clamp fixed saturation but killed the
21
+ escape gradient; this keeps the corridor AND the slope)
22
+ * Random Sim(2) cameras -- the DreamFusion trick, free in this
23
+ representation: zoom/shift/rotate are parameter
24
+ arithmetic on atoms. Kills the SDS zoom-crop trap:
25
+ only a complete, centered object scores well under
26
+ every random view.
27
+ * Normalized SDS loss + gate warmup -- in Bet 4 the SDS loss dwarfed the
28
+ L0 term so gates never closed (256/256 all run). Loss
29
+ is now per-element normalized and gates get a warmup
30
+ before pruning pressure engages.
31
+ * --init-atoms load any previous atoms.pt (Bet 4 recon or SDS,
32
+ N inferred from the file)
33
+ * --freeze channel groups excluded from optimization entirely
34
+ * --train-groups per-atom masks: only listed groups receive gradients
35
+ (the two-slot tractor/background experiment is a flag)
36
+
37
+ Modes
38
+ -----
39
+ recon : fit atoms to a target image (MSE). No diffusion model needed.
40
+ sds : score distillation from frozen Stable Diffusion.
41
+ render : load atoms and render -- identity view, a chosen camera, or a
42
+ camera sweep saved as GIF (the "glide" demo).
43
+
44
+ The permanence experiment (the actual bet)
45
+ ------------------------------------------
46
+ # 1. you already have runs/recon_tractor/atoms.pt (23.5 dB, 205 atoms)
47
+ # 2. relight it, geometry untouchable:
48
+ python bet5_gabor_sds.py --mode sds \
49
+ --init-atoms runs/recon_tractor/atoms.pt \
50
+ --freeze geometry,gates \
51
+ --prompt "a photo of a red tractor at dusk, golden hour, warm light" \
52
+ --iters 1500 --render-size 512 --cfg 50 \
53
+ --sd-model sd2-community/stable-diffusion-2-1-base \
54
+ --out runs/bet5_dusk
55
+
56
+ GO : final image reads as the SAME tractor, relit. Geometry channels are
57
+ bitwise identical (the script verifies and prints this).
58
+ NO-GO: appearance channels alone cannot express the edit (tractor holds by
59
+ construction -- geometry is frozen -- but the scene refuses to read
60
+ as dusk after a seed retry and a cfg bump).
61
+
62
+ Honesty note: recon/render/freeze/group/camera machinery executed and
63
+ verified on CPU before shipping. The sds path reuses Bet 4's verified-on-
64
+ your-GPU loop with the fixes above; the fixes themselves have not run under
65
+ a real SDS gradient yet. First run = smoke test.
66
+ """
67
+
68
+ import argparse
69
+ import json
70
+ import math
71
+ import os
72
+ import random
73
+ import time
74
+
75
+ import numpy as np
76
+ import torch
77
+ import torch.nn as nn
78
+ import torch.nn.functional as F
79
+
80
+ # ----------------------------------------------------------------------------
81
+ # Hard-concrete gates (Louizos et al.) — Slapstack two-way doors
82
+ # ----------------------------------------------------------------------------
83
+
84
+ class HardConcreteGates(nn.Module):
85
+ GAMMA, ZETA, BETA = -0.1, 1.1, 2.0 / 3.0
86
+
87
+ def __init__(self, n, init_logit=2.0):
88
+ super().__init__()
89
+ self.logits = nn.Parameter(torch.full((n,), float(init_logit)))
90
+
91
+ def forward(self, hard_eval=False):
92
+ if self.training and not hard_eval:
93
+ u = torch.rand_like(self.logits).clamp(1e-6, 1 - 1e-6)
94
+ s = torch.sigmoid((torch.log(u) - torch.log(1 - u) + self.logits) / self.BETA)
95
+ else:
96
+ s = torch.sigmoid(self.logits)
97
+ return (s * (self.ZETA - self.GAMMA) + self.GAMMA).clamp(0.0, 1.0)
98
+
99
+ def l0(self):
100
+ return torch.sigmoid(self.logits - self.BETA * math.log(-self.GAMMA / self.ZETA))
101
+
102
+ @torch.no_grad()
103
+ def hard_open(self):
104
+ z = torch.sigmoid(self.logits) * (self.ZETA - self.GAMMA) + self.GAMMA
105
+ return (z.clamp(0, 1) > 0.5)
106
+
107
+
108
+ # ----------------------------------------------------------------------------
109
+ # Differentiable Gabor packet image (Bet 4 renderer + soft clamp + cameras)
110
+ # ----------------------------------------------------------------------------
111
+
112
+ class GaborPacketImage(nn.Module):
113
+ """
114
+ Image = sigmoid( softclamp( bg_bias + sum_i g_i a_i c_i env_i carrier_i ) )
115
+
116
+ env_i = exp(-0.5 (u^2/su^2 + v^2/sv^2)), (u,v) atom-local rotated coords
117
+ carrier_i = cos(2*pi*f_i*u + phi_i) phi is ENVELOPE-RELATIVE:
118
+ pose never touches phase.
119
+ A camera g = (s, rho, tx, ty) in Sim(2) acts by parameter arithmetic:
120
+ xy -> s*R(rho)*xy + t, theta -> theta+rho, sigma -> s*sigma, f -> f/s
121
+ phase and color are untouched — that IS the identity/pose factorization.
122
+ """
123
+
124
+ # per-atom parameter names (used by freeze / group-mask machinery)
125
+ PER_ATOM = ["xy_raw", "theta", "log_sigma_u", "log_sigma_v",
126
+ "freq_raw", "phase", "amp", "color", "gates.logits"]
127
+
128
+ def __init__(self, n_atoms=256, coarse_frac=0.25, seed=0):
129
+ super().__init__()
130
+ g = torch.Generator().manual_seed(seed)
131
+ n = n_atoms
132
+ nc = int(n * coarse_frac)
133
+
134
+ self.xy_raw = nn.Parameter(torch.randn(n, 2, generator=g) * 0.7)
135
+ self.theta = nn.Parameter(torch.rand(n, generator=g) * math.pi)
136
+
137
+ log_s = torch.empty(n)
138
+ log_s[:nc] = math.log(0.45) + 0.25 * torch.randn(nc, generator=g)
139
+ log_s[nc:] = math.log(0.12) + 0.35 * torch.randn(n - nc, generator=g)
140
+ self.log_sigma_u = nn.Parameter(log_s.clone())
141
+ self.log_sigma_v = nn.Parameter(log_s + 0.2 * torch.randn(n, generator=g))
142
+
143
+ f = torch.empty(n)
144
+ f[:nc] = 0.25 + 0.5 * torch.rand(nc, generator=g)
145
+ f[nc:] = 0.75 + 2.0 * torch.rand(n - nc, generator=g)
146
+ self.freq_raw = nn.Parameter(torch.log(torch.expm1(f)))
147
+
148
+ self.phase = nn.Parameter(2 * math.pi * torch.rand(n, generator=g))
149
+ self.amp = nn.Parameter(0.35 + 0.15 * torch.randn(n, generator=g))
150
+ self.color = nn.Parameter(0.30 * torch.randn(n, 3, generator=g))
151
+ self.bg_bias = nn.Parameter(torch.zeros(3))
152
+
153
+ self.gates = HardConcreteGates(n)
154
+ # per-atom group id (0 = default). Buffer so it persists in atoms.pt.
155
+ self.register_buffer("group", torch.zeros(n, dtype=torch.long))
156
+ self.n_atoms = n
157
+
158
+ # -- derived ---------------------------------------------------------------
159
+ def xy(self):
160
+ return torch.tanh(self.xy_raw)
161
+
162
+ def freq(self):
163
+ return F.softplus(self.freq_raw)
164
+
165
+ def sigmas(self):
166
+ return (self.log_sigma_u.exp().clamp(5e-3, 2.0),
167
+ self.log_sigma_v.exp().clamp(5e-3, 2.0))
168
+
169
+ # -- render ------------------------------------------------------------------
170
+ def render(self, H, W, device, chunk=64, hard_gates=False, camera=None):
171
+ ys = torch.linspace(-1, 1, H, device=device)
172
+ xs = torch.linspace(-1, 1, W, device=device)
173
+ Y, X = torch.meshgrid(ys, xs, indexing="ij")
174
+
175
+ xy = self.xy().to(device)
176
+ theta = self.theta.to(device)
177
+ su, sv = self.sigmas()
178
+ su, sv = su.to(device), sv.to(device)
179
+ f = self.freq().to(device)
180
+ phi = self.phase.to(device)
181
+ amp = self.amp.to(device)
182
+ col = self.color.to(device)
183
+ z = self.gates(hard_eval=hard_gates).to(device)
184
+
185
+ if camera is not None: # Sim(2): parameter arithmetic
186
+ s, rho, tx, ty = camera
187
+ c, sn = math.cos(rho), math.sin(rho)
188
+ R = torch.tensor([[c, -sn], [sn, c]], device=device, dtype=xy.dtype)
189
+ xy = s * xy @ R.T + torch.tensor([tx, ty], device=device, dtype=xy.dtype)
190
+ theta = theta + rho
191
+ su, sv = s * su, s * sv
192
+ f = f / s
193
+ # phase untouched by construction — envelope-relative.
194
+
195
+ pre = torch.zeros(3, H, W, device=device) + self.bg_bias.to(device)[:, None, None]
196
+
197
+ for i0 in range(0, self.n_atoms, chunk):
198
+ sl = slice(i0, min(i0 + chunk, self.n_atoms))
199
+ dx = X[None] - xy[sl, 0, None, None]
200
+ dy = Y[None] - xy[sl, 1, None, None]
201
+ ct = torch.cos(theta[sl])[:, None, None]
202
+ st = torch.sin(theta[sl])[:, None, None]
203
+ u = ct * dx + st * dy
204
+ v = -st * dx + ct * dy
205
+ env = torch.exp(-0.5 * ((u / su[sl, None, None]) ** 2 +
206
+ (v / sv[sl, None, None]) ** 2))
207
+ carrier = torch.cos(2 * math.pi * f[sl, None, None] * u + phi[sl, None, None])
208
+ w = (z[sl] * amp[sl])[:, None, None] * env * carrier
209
+ pre = pre + torch.einsum("nhw,nc->chw", w, col[sl])
210
+
211
+ # Leaky soft clamp: tanh corridor + linear leak. Plain tanh still dies
212
+ # numerically in fp32 at |pre|>~40 (caught by shipped test 5); the
213
+ # 0.02*pre leak guarantees a nonzero escape gradient at ANY depth of
214
+ # saturation — the solid-red trap always has an exit ramp.
215
+ pre = 4.0 * torch.tanh(pre / 4.0) + 0.02 * pre
216
+ return torch.sigmoid(pre)
217
+
218
+ def ledger(self):
219
+ return {"atoms_total": self.n_atoms,
220
+ "atoms_open_hard": int(self.gates.hard_open().sum().item()),
221
+ "expected_L0": float(self.gates.l0().sum().item()),
222
+ "groups": {int(k): int(v) for k, v in
223
+ zip(*[t.tolist() for t in self.group.unique(return_counts=True)])}}
224
+
225
+
226
+ # ----------------------------------------------------------------------------
227
+ # Loading, freezing, group masks
228
+ # ----------------------------------------------------------------------------
229
+
230
+ FREEZE_MAP = {
231
+ "position": ["xy_raw"],
232
+ "orientation": ["theta"],
233
+ "scale": ["log_sigma_u", "log_sigma_v"],
234
+ "frequency": ["freq_raw"],
235
+ "phase": ["phase"],
236
+ "amp": ["amp"],
237
+ "color": ["color"],
238
+ "bg": ["bg_bias"],
239
+ "gates": ["gates.logits"],
240
+ }
241
+ FREEZE_MAP["geometry"] = (FREEZE_MAP["position"] + FREEZE_MAP["orientation"]
242
+ + FREEZE_MAP["scale"] + FREEZE_MAP["frequency"])
243
+ FREEZE_MAP["appearance"] = (FREEZE_MAP["phase"] + FREEZE_MAP["amp"]
244
+ + FREEZE_MAP["color"] + FREEZE_MAP["bg"])
245
+
246
+
247
+ def load_atoms(path):
248
+ sd = torch.load(path, map_location="cpu")
249
+ n = sd["phase"].shape[0]
250
+ model = GaborPacketImage(n_atoms=n)
251
+ missing, unexpected = model.load_state_dict(sd, strict=False)
252
+ # 'group' may be missing in Bet 4 files — that's fine (defaults to 0).
253
+ assert not unexpected, f"unexpected keys in {path}: {unexpected}"
254
+ print(f"loaded {path}: {n} atoms"
255
+ + (f" (new buffers defaulted: {missing})" if missing else ""))
256
+ return model
257
+
258
+
259
+ def resolve_frozen(freeze_arg):
260
+ frozen = set()
261
+ for tok in [t for t in (freeze_arg or "").split(",") if t.strip()]:
262
+ tok = tok.strip()
263
+ assert tok in FREEZE_MAP, f"unknown freeze channel '{tok}' " \
264
+ f"(choose from {sorted(FREEZE_MAP)})"
265
+ frozen.update(FREEZE_MAP[tok])
266
+ return frozen
267
+
268
+
269
+ def make_optimizer(model, frozen, mode, lr_scale=1.0):
270
+ # LRs that worked: recon = Bet 4 defaults; sds = the rebalance that
271
+ # escaped the solid-red saturation trap (color 2e-3, bg 5e-4).
272
+ lrs = {"xy_raw": 5e-3, "theta": 5e-3, "log_sigma_u": 5e-3,
273
+ "log_sigma_v": 5e-3, "freq_raw": 5e-3, "phase": 2e-2,
274
+ "amp": 1e-2, "color": 1e-2, "bg_bias": 1e-2, "gates.logits": 2e-2}
275
+ if mode == "sds":
276
+ lrs["color"], lrs["bg_bias"] = 2e-3, 5e-4
277
+ groups, trained = [], []
278
+ for name, lr in lrs.items():
279
+ if name in frozen:
280
+ continue
281
+ p = dict(model.named_parameters())[name]
282
+ groups.append({"params": [p], "lr": lr * lr_scale})
283
+ trained.append(name)
284
+ assert groups, "everything is frozen — nothing to optimize"
285
+ print(f"training channels: {trained}")
286
+ if frozen:
287
+ print(f"frozen channels: {sorted(frozen)}")
288
+ return torch.optim.Adam(groups, betas=(0.9, 0.99))
289
+
290
+
291
+ def group_mask(model, train_groups):
292
+ """None if all groups train; else float mask (n,) — 1 for trainable atoms."""
293
+ if train_groups is None:
294
+ return None
295
+ ids = torch.tensor([int(t) for t in train_groups.split(",")])
296
+ mask = torch.isin(model.group.cpu(), ids).float()
297
+ print(f"group mask: {int(mask.sum())}/{model.n_atoms} atoms trainable "
298
+ f"(groups {ids.tolist()})")
299
+ return mask
300
+
301
+
302
+ def apply_grad_masks(model, mask, device):
303
+ """Zero gradients of per-atom params for atoms outside trainable groups."""
304
+ if mask is None:
305
+ return
306
+ m = mask.to(device)
307
+ params = dict(model.named_parameters())
308
+ for name in GaborPacketImage.PER_ATOM:
309
+ p = params[name]
310
+ if p.grad is not None:
311
+ p.grad.mul_(m.view(-1, *([1] * (p.dim() - 1))))
312
+
313
+
314
+ def sample_camera(args):
315
+ if args.no_camera:
316
+ return None
317
+ return (math.exp(random.uniform(-args.cam_zoom, args.cam_zoom)),
318
+ random.uniform(-args.cam_rot, args.cam_rot),
319
+ random.uniform(-args.cam_shift, args.cam_shift),
320
+ random.uniform(-args.cam_shift, args.cam_shift))
321
+
322
+
323
+ def save_png(img_chw, path):
324
+ from PIL import Image
325
+ arr = (img_chw.detach().clamp(0, 1).cpu().numpy()
326
+ .transpose(1, 2, 0) * 255).astype(np.uint8)
327
+ Image.fromarray(arr).save(path)
328
+
329
+
330
+ def geometry_fingerprint(model):
331
+ """Hash of geometry channels — proves bitwise permanence after a run."""
332
+ import hashlib
333
+ h = hashlib.sha256()
334
+ for name in FREEZE_MAP["geometry"]:
335
+ h.update(dict(model.named_parameters())[name].detach().cpu()
336
+ .numpy().tobytes())
337
+ return h.hexdigest()[:16]
338
+
339
+
340
+ # ----------------------------------------------------------------------------
341
+ # Mode: recon
342
+ # ----------------------------------------------------------------------------
343
+
344
+ def run_recon(args, model, device):
345
+ from PIL import Image
346
+ tgt = Image.open(args.target).convert("RGB").resize((args.render_size,) * 2)
347
+ target = torch.from_numpy(np.asarray(tgt).copy()).float().permute(2, 0, 1) / 255.0
348
+ target = target.to(device)
349
+
350
+ frozen = resolve_frozen(args.freeze)
351
+ opt = make_optimizer(model, frozen, "recon")
352
+ mask = group_mask(model, args.train_groups)
353
+ model.train().to(device)
354
+
355
+ os.makedirs(args.out, exist_ok=True)
356
+ save_png(target, os.path.join(args.out, "target.png"))
357
+ log, t0 = [], time.time()
358
+ for it in range(args.iters):
359
+ opt.zero_grad()
360
+ img = model.render(args.render_size, args.render_size, device, chunk=args.chunk)
361
+ mse = F.mse_loss(img, target)
362
+ loss = mse + args.l0_weight * model.gates.l0().sum() / model.n_atoms
363
+ loss.backward()
364
+ if it < args.gate_warmup and model.gates.logits.grad is not None:
365
+ model.gates.logits.grad.zero_()
366
+ apply_grad_masks(model, mask, device)
367
+ opt.step()
368
+ if it % max(1, args.iters // 20) == 0 or it == args.iters - 1:
369
+ psnr = -10 * math.log10(max(mse.item(), 1e-12))
370
+ row = {"it": it, "mse": mse.item(), "psnr_db": psnr, **model.ledger()}
371
+ log.append(row)
372
+ print(f"[recon] it {it:5d} mse {mse.item():.5f} psnr {psnr:5.2f} dB "
373
+ f"open {row['atoms_open_hard']}/{model.n_atoms} ({time.time()-t0:.0f}s)")
374
+ save_png(img, os.path.join(args.out, f"it_{it:05d}.png"))
375
+ finish(model, args, device, log)
376
+
377
+
378
+ # ----------------------------------------------------------------------------
379
+ # Mode: sds
380
+ # ----------------------------------------------------------------------------
381
+
382
+ def run_sds(args, model, device):
383
+ from diffusers import StableDiffusionPipeline, DDPMScheduler
384
+
385
+ dtype = torch.float16 if device.type == "cuda" else torch.float32
386
+ pipe = StableDiffusionPipeline.from_pretrained(
387
+ args.sd_model, torch_dtype=dtype, safety_checker=None,
388
+ requires_safety_checker=False)
389
+ pipe.to(device)
390
+ vae, unet, tok, te = pipe.vae, pipe.unet, pipe.tokenizer, pipe.text_encoder
391
+ for m in (vae, unet, te):
392
+ m.requires_grad_(False)
393
+ sched = DDPMScheduler.from_pretrained(args.sd_model, subfolder="scheduler")
394
+ alphas = sched.alphas_cumprod.to(device)
395
+ T = sched.config.num_train_timesteps
396
+
397
+ def embed(text):
398
+ ids = tok(text, padding="max_length", max_length=tok.model_max_length,
399
+ truncation=True, return_tensors="pt").input_ids.to(device)
400
+ return te(ids)[0]
401
+
402
+ with torch.no_grad():
403
+ emb = torch.cat([embed(args.negative_prompt), embed(args.prompt)])
404
+
405
+ frozen = resolve_frozen(args.freeze)
406
+ opt = make_optimizer(model, frozen, "sds")
407
+ mask = group_mask(model, args.train_groups)
408
+ model.train().to(device)
409
+ fp_before = geometry_fingerprint(model)
410
+
411
+ os.makedirs(args.out, exist_ok=True)
412
+ log, t0 = [], time.time()
413
+ for it in range(args.iters):
414
+ opt.zero_grad()
415
+ cam = sample_camera(args)
416
+ img = model.render(args.render_size, args.render_size, device,
417
+ chunk=args.chunk, camera=cam)
418
+ x = img[None] * 2 - 1
419
+ if args.render_size != 512:
420
+ x = F.interpolate(x, (512, 512), mode="bilinear", align_corners=False)
421
+
422
+ latents = vae.encode(x.to(dtype)).latent_dist.sample() * vae.config.scaling_factor
423
+ latents = latents.float()
424
+
425
+ frac = it / max(1, args.iters - 1)
426
+ t_max = args.t_max_start + (args.t_max_end - args.t_max_start) * frac
427
+ t = torch.randint(int(args.t_min * T), int(t_max * T), (1,), device=device)
428
+
429
+ noise = torch.randn_like(latents)
430
+ noisy = sched.add_noise(latents, noise, t)
431
+ with torch.no_grad():
432
+ eps = unet(torch.cat([noisy] * 2).to(dtype), torch.cat([t] * 2),
433
+ encoder_hidden_states=emb).sample.float()
434
+ eps_un, eps_tx = eps.chunk(2)
435
+ eps_hat = eps_un + args.cfg * (eps_tx - eps_un)
436
+
437
+ w = (1 - alphas[t]).view(-1, 1, 1, 1)
438
+ grad = (w * (eps_hat - noise)).detach()
439
+ sds_loss = (grad * latents).sum() / latents.numel() # normalized (Bet 4 fix)
440
+ l0_loss = model.gates.l0().sum() / model.n_atoms
441
+ loss = sds_loss + args.l0_weight * l0_loss
442
+ loss.backward()
443
+ if it < args.gate_warmup and model.gates.logits.grad is not None:
444
+ model.gates.logits.grad.zero_()
445
+ apply_grad_masks(model, mask, device)
446
+ torch.nn.utils.clip_grad_norm_(
447
+ [p for g_ in opt.param_groups for p in g_["params"]], 1.0)
448
+ opt.step()
449
+
450
+ if it % max(1, args.iters // 30) == 0 or it == args.iters - 1:
451
+ row = {"it": it, "sds": float(sds_loss.item()),
452
+ "l0": float(l0_loss.item()), "t_max": t_max, **model.ledger()}
453
+ log.append(row)
454
+ print(f"[sds] it {it:5d} sds {sds_loss.item():+.4f} t_max {t_max:.2f} "
455
+ f"open {row['atoms_open_hard']}/{model.n_atoms} ({time.time()-t0:.0f}s)")
456
+ with torch.no_grad():
457
+ save_png(model.render(args.render_size, args.render_size, device,
458
+ chunk=args.chunk, hard_gates=True),
459
+ os.path.join(args.out, f"it_{it:05d}.png"))
460
+
461
+ fp_after = geometry_fingerprint(model)
462
+ if "geometry" in (args.freeze or ""):
463
+ verdict = "IDENTICAL — permanence held by construction" \
464
+ if fp_before == fp_after else "CHANGED — BUG, investigate"
465
+ print(f"geometry fingerprint before/after: {fp_before} / {fp_after} -> {verdict}")
466
+ finish(model, args, device, log,
467
+ extra={"geometry_fp_before": fp_before, "geometry_fp_after": fp_after})
468
+
469
+
470
+ # ----------------------------------------------------------------------------
471
+ # Mode: render (identity view, chosen camera, or a glide GIF)
472
+ # ----------------------------------------------------------------------------
473
+
474
+ def run_render(args, model, device):
475
+ model.eval().to(device)
476
+ os.makedirs(args.out, exist_ok=True)
477
+ S = args.render_size
478
+ with torch.no_grad():
479
+ save_png(model.render(S, S, device, chunk=args.chunk, hard_gates=True),
480
+ os.path.join(args.out, "identity.png"))
481
+ if args.camera:
482
+ cam = tuple(float(v) for v in args.camera.split(","))
483
+ assert len(cam) == 4, "--camera expects 's,rho,tx,ty'"
484
+ save_png(model.render(S, S, device, chunk=args.chunk,
485
+ hard_gates=True, camera=cam),
486
+ os.path.join(args.out, "camera.png"))
487
+ if args.gif:
488
+ from PIL import Image
489
+ frames = []
490
+ n = 40
491
+ for i in range(n):
492
+ p = i / (n - 1)
493
+ cam = (1.0 + 0.15 * math.sin(2 * math.pi * p), # gentle zoom breath
494
+ 0.0,
495
+ -0.45 + 0.9 * p, # glide left -> right
496
+ 0.10 * math.sin(4 * math.pi * p)) # slight bob
497
+ im = model.render(S, S, device, chunk=args.chunk,
498
+ hard_gates=True, camera=cam)
499
+ frames.append(Image.fromarray(
500
+ (im.clamp(0, 1).cpu().numpy().transpose(1, 2, 0) * 255)
501
+ .astype(np.uint8)))
502
+ frames[0].save(os.path.join(args.out, "glide.gif"), save_all=True,
503
+ append_images=frames[1:], duration=60, loop=0)
504
+ print("wrote glide.gif — texture rides the envelopes; phase never moves")
505
+ print(f"render -> {args.out}")
506
+
507
+
508
+ # ----------------------------------------------------------------------------
509
+
510
+ def finish(model, args, device, log, extra=None):
511
+ model.eval()
512
+ with torch.no_grad():
513
+ img = model.render(args.render_size, args.render_size, device,
514
+ chunk=args.chunk, hard_gates=True)
515
+ save_png(img, os.path.join(args.out, "final_hardgates.png"))
516
+ torch.save(model.state_dict(), os.path.join(args.out, "atoms.pt"))
517
+ ledger = {"mode": args.mode, "prompt": getattr(args, "prompt", None),
518
+ "freeze": args.freeze, "train_groups": args.train_groups,
519
+ "init_atoms": args.init_atoms,
520
+ "camera": None if args.no_camera else
521
+ {"zoom": args.cam_zoom, "shift": args.cam_shift,
522
+ "rot": args.cam_rot},
523
+ "final": model.ledger(), "log": log}
524
+ if extra:
525
+ ledger.update(extra)
526
+ with open(os.path.join(args.out, "ledger.json"), "w") as fh:
527
+ json.dump(ledger, fh, indent=2)
528
+ print(f"done -> {args.out} | open atoms: {model.ledger()['atoms_open_hard']}")
529
+
530
+
531
+ def assign_group_rect(model, spec):
532
+ """--assign-group-rect 'x0,y0,x1,y1:gid' — atoms with canonical xy inside
533
+ the rect get group gid. Coordinates in [-1,1]. Repeatable."""
534
+ box, gid = spec.split(":")
535
+ x0, y0, x1, y1 = (float(v) for v in box.split(","))
536
+ xy = model.xy().detach()
537
+ inside = ((xy[:, 0] >= x0) & (xy[:, 0] <= x1) &
538
+ (xy[:, 1] >= y0) & (xy[:, 1] <= y1))
539
+ model.group[inside] = int(gid)
540
+ print(f"assigned {int(inside.sum())} atoms in [{x0},{x1}]x[{y0},{y1}] "
541
+ f"to group {gid}")
542
+
543
+
544
+ def main():
545
+ p = argparse.ArgumentParser(description="Bet 5: permanence in Gabor packet space")
546
+ p.add_argument("--mode", choices=["recon", "sds", "render"], required=True)
547
+ p.add_argument("--out", default="runs/bet5")
548
+ p.add_argument("--atoms", type=int, default=256)
549
+ p.add_argument("--iters", type=int, default=2000)
550
+ p.add_argument("--render-size", type=int, default=512)
551
+ p.add_argument("--chunk", type=int, default=64)
552
+ p.add_argument("--seed", type=int, default=0)
553
+ p.add_argument("--l0-weight", type=float, default=5e-3)
554
+ p.add_argument("--gate-warmup", type=int, default=400,
555
+ help="iterations before L0 pruning pressure engages")
556
+ # init / freeze / groups
557
+ p.add_argument("--init-atoms", help="atoms.pt from any previous run (Bet 4 OK)")
558
+ p.add_argument("--freeze", default="",
559
+ help=f"comma list from {sorted(FREEZE_MAP)}")
560
+ p.add_argument("--train-groups",
561
+ help="comma list of group ids that receive gradients")
562
+ p.add_argument("--assign-group-rect", action="append", default=[],
563
+ help="'x0,y0,x1,y1:gid' assign atoms in rect to group (repeatable)")
564
+ # cameras
565
+ p.add_argument("--no-camera", action="store_true",
566
+ help="disable random Sim(2) cameras in sds mode")
567
+ p.add_argument("--cam-zoom", type=float, default=0.30, help="log-zoom range")
568
+ p.add_argument("--cam-shift", type=float, default=0.25)
569
+ p.add_argument("--cam-rot", type=float, default=0.15, help="radians")
570
+ p.add_argument("--camera", help="render mode: fixed 's,rho,tx,ty'")
571
+ p.add_argument("--gif", action="store_true", help="render mode: glide GIF")
572
+ # recon
573
+ p.add_argument("--target", help="target image (recon mode)")
574
+ # sds
575
+ p.add_argument("--prompt", default="a photo of a tractor")
576
+ p.add_argument("--negative-prompt", default="blurry, low quality, deformed")
577
+ p.add_argument("--sd-model", default="sd2-community/stable-diffusion-2-1-base")
578
+ p.add_argument("--cfg", type=float, default=50.0)
579
+ p.add_argument("--t-min", type=float, default=0.02)
580
+ p.add_argument("--t-max-start", type=float, default=0.98)
581
+ p.add_argument("--t-max-end", type=float, default=0.50)
582
+ args = p.parse_args()
583
+
584
+ random.seed(args.seed)
585
+ torch.manual_seed(args.seed)
586
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
587
+ print(f"device: {device}")
588
+
589
+ model = load_atoms(args.init_atoms) if args.init_atoms \
590
+ else GaborPacketImage(args.atoms, seed=args.seed)
591
+ for spec in args.assign_group_rect:
592
+ assign_group_rect(model, spec)
593
+
594
+ if args.mode == "recon":
595
+ assert args.target, "--target required in recon mode"
596
+ run_recon(args, model, device)
597
+ elif args.mode == "sds":
598
+ run_sds(args, model, device)
599
+ else:
600
+ run_render(args, model, device)
601
+
602
+
603
+ if __name__ == "__main__":
604
+ main()
build.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require("fs");
2
+ const strip = (src) => src.replace(/if \(typeof module[^]*$/m, "");
3
+ const core = strip(fs.readFileSync(__dirname + "/studio/core.js", "utf8"));
4
+ const scoreJs = strip(fs.readFileSync(__dirname + "/studio/studio_core.js", "utf8"));
5
+ const builtins = fs.readFileSync(__dirname + "/studio/builtins.json", "utf8");
6
+ let html = fs.readFileSync(__dirname + "/studio_ui.html", "utf8");
7
+ html = html.replace("/*CORE*/", core);
8
+ html = html.replace("/*STUDIO_CORE*/", scoreJs);
9
+ html = html.replace("/*BUILTINS*/null", builtins);
10
+ fs.writeFileSync(__dirname + "/studio/studio.html", html);
11
+ console.log("built studio/studio.html:", (html.length / 1024).toFixed(0), "KB");
builtins_preview.png ADDED
make_builtins.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Build the studio's built-in layers: the Bet-6 tractor and star, plus a boat,
4
+ drawn on NEUTRAL MID-GRAY (127,127,127). sigmoid(0) = 0.5, so a mid-gray
5
+ background needs NO atoms — the fit spends its whole budget on the object
6
+ and the layer comes out spatially compact by construction. This is the
7
+ object-layer recipe the Space UI also recommends for uploaded images.
8
+ """
9
+
10
+ import json
11
+ import math
12
+
13
+ import numpy as np
14
+ from PIL import Image, ImageDraw
15
+
16
+ from oracle import fit_image, render_atoms
17
+
18
+ GRAY = (127, 127, 127)
19
+
20
+
21
+ def draw_tractor(px=96):
22
+ # fills ~90% of the frame: template extent IS the object extent
23
+ im = Image.new("RGB", (px, px), GRAY)
24
+ d = ImageDraw.Draw(im)
25
+ d.rectangle([10, 34, 74, 72], fill=(200, 40, 30))
26
+ d.rectangle([42, 8, 74, 34], fill=(180, 30, 25))
27
+ d.ellipse([4, 58, 42, 92], fill=(20, 20, 20))
28
+ d.ellipse([57, 66, 82, 90], fill=(25, 25, 25))
29
+ return im
30
+
31
+
32
+ def draw_star(px=96):
33
+ im = Image.new("RGB", (px, px), GRAY)
34
+ d = ImageDraw.Draw(im)
35
+ c, r1, r2 = px / 2, px * 0.47, px * 0.19
36
+ pts = []
37
+ for i in range(10):
38
+ r = r1 if i % 2 == 0 else r2
39
+ a = math.pi / 2 + i * math.pi / 5
40
+ pts.append((c + r * math.cos(a), c - r * math.sin(a)))
41
+ d.polygon(pts, fill=(240, 200, 60))
42
+ d.ellipse([c - 9, c - 9, c + 9, c + 9], fill=(200, 90, 30))
43
+ return im
44
+
45
+
46
+ def draw_boat(px=96):
47
+ im = Image.new("RGB", (px, px), GRAY)
48
+ d = ImageDraw.Draw(im)
49
+ d.polygon([(6, 64), (90, 64), (74, 88), (20, 88)], fill=(160, 80, 40))
50
+ d.rectangle([45, 6, 50, 64], fill=(230, 220, 200))
51
+ d.polygon([(50, 8), (86, 56), (50, 56)], fill=(250, 245, 225))
52
+ return im
53
+
54
+
55
+ if __name__ == "__main__":
56
+ out, stats, panels = {}, {}, []
57
+ for name, img, seed in [("tractor", draw_tractor(), 0),
58
+ ("star", draw_star(), 1),
59
+ ("boat", draw_boat(), 3)]:
60
+ atoms, ledger = fit_image(img, n_atoms=140, iters=400, seed=seed)
61
+ r = np.hypot(atoms[:, 0], atoms[:, 1])
62
+ stats[name] = {"atoms": len(atoms),
63
+ "psnr_db": round(ledger["final_psnr_db"], 1),
64
+ "radius_p95": round(float(np.percentile(r, 95)), 2)}
65
+ out[name] = np.asarray(atoms).round(5).tolist()
66
+ panels.append((render_atoms(np.asarray(atoms), 160)
67
+ .transpose(1, 2, 0) * 255).astype(np.uint8))
68
+ print(name, stats[name])
69
+ json.dump(out, open("studio/builtins.json", "w"))
70
+ Image.fromarray(np.concatenate(panels, 1)).save("builtins_preview.png")
71
+ print("wrote studio/builtins.json")
node ADDED
File without changes
oracle.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Slapstack Studio oracle — turns pixels or text into Gabor-atom LAYERS.
4
+
5
+ Two paths, honestly separated:
6
+
7
+ fit_image(img) CPU, minutes. Bet-5 recon loop (verified math): fit a
8
+ GaborPacketImage to the image by MSE, export hard-open
9
+ atoms in the Bet-6 field layout. This is exactly
10
+ bet6_open._train_recon generalized to any image.
11
+
12
+ sds_layer(text) GPU, minutes. Bet-5 run_sds loop from scratch on a
13
+ fresh atom population, Stable Diffusion 2.1 as the
14
+ score oracle. UNTESTED on GPU in this build environment
15
+ (no CUDA, no SD weights here) — the loop is a line-for-
16
+ line adaptation of the Bet-5 SDS loop that WAS verified
17
+ on GPU, but treat the first run as a smoke test. Known
18
+ open risk carries over: SD 2.1 mode-seeking
19
+ oversaturation at high CFG.
20
+
21
+ Atom layout (FIELDS): [x, y, theta, sigma_u, sigma_v, freq, phase, r, g, b]
22
+ with amplitude folded into signed color and xy centered (canonical frame).
23
+ """
24
+
25
+ import io
26
+ import json
27
+ import math
28
+ import time
29
+
30
+ import numpy as np
31
+ import torch
32
+ import torch.nn.functional as F
33
+ from PIL import Image
34
+
35
+ from bet5_gabor_sds import GaborPacketImage
36
+
37
+ FIELDS = ["x", "y", "theta", "su", "sv", "f", "phase", "r", "g", "b"]
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # shared: model -> Bet-6 atom array (port of bet6_open.atoms_from_model)
42
+ # ---------------------------------------------------------------------------
43
+
44
+ def atoms_from_model(m):
45
+ keep = np.where(m.gates.hard_open().numpy())[0]
46
+ arr = np.zeros((len(keep), len(FIELDS)))
47
+ arr[:, 0:2] = torch.tanh(m.xy_raw).detach().numpy()[keep]
48
+ arr[:, 0:2] -= arr[:, 0:2].mean(0)
49
+ arr[:, 2] = m.theta.detach().numpy()[keep]
50
+ arr[:, 3] = np.clip(np.exp(m.log_sigma_u.detach().numpy()[keep]), 5e-3, 2.0)
51
+ arr[:, 4] = np.clip(np.exp(m.log_sigma_v.detach().numpy()[keep]), 5e-3, 2.0)
52
+ arr[:, 5] = np.log1p(np.exp(m.freq_raw.detach().numpy()[keep]))
53
+ arr[:, 6] = np.mod(m.phase.detach().numpy()[keep], 2 * math.pi)
54
+ amp = m.amp.detach().numpy()[keep]
55
+ arr[:, 7:10] = m.color.detach().numpy()[keep] * amp[:, None]
56
+ return arr
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # preview renderer — same formula the verified JS core uses
61
+ # (pre[c] += color_c * env * carrier ; sigmoid(2*pre))
62
+ # ---------------------------------------------------------------------------
63
+
64
+ def render_atoms(atoms, H=192):
65
+ ys = np.linspace(-1, 1, H)
66
+ X, Y = np.meshgrid(ys, ys)
67
+ pre = np.zeros((3, H, H), np.float32)
68
+ for a in atoms:
69
+ dx, dy = X - a[0], Y - a[1]
70
+ ct, st = math.cos(a[2]), math.sin(a[2])
71
+ u = ct * dx + st * dy
72
+ v = -st * dx + ct * dy
73
+ env = np.exp(-0.5 * ((u / a[3]) ** 2 + (v / a[4]) ** 2))
74
+ car = np.cos(2 * np.pi * a[5] * u + a[6])
75
+ ec = (env * car).astype(np.float32)
76
+ for c in range(3):
77
+ pre[c] += a[7 + c] * ec
78
+ return 1 / (1 + np.exp(-2 * pre))
79
+
80
+
81
+ def preview_png_bytes(atoms, H=192):
82
+ img = (render_atoms(atoms, H).transpose(1, 2, 0) * 255).astype(np.uint8)
83
+ buf = io.BytesIO()
84
+ Image.fromarray(img).save(buf, "PNG")
85
+ return buf.getvalue()
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # path 1: image -> layer (CPU, the Bet-5 recon loop)
90
+ # ---------------------------------------------------------------------------
91
+
92
+ def fit_image(pil_img, n_atoms=140, iters=400, size=96, seed=0,
93
+ l0_weight=1e-3, gate_warmup=60, progress=None):
94
+ """Fit a Gabor packet layer to an image by MSE. CPU, deterministic."""
95
+ dev = torch.device("cpu")
96
+ torch.manual_seed(seed)
97
+ pil_img = pil_img.convert("RGB")
98
+ tgt = torch.from_numpy(
99
+ np.asarray(pil_img.resize((size, size)), dtype=np.float32).copy()
100
+ ).permute(2, 0, 1) / 255.0
101
+
102
+ m = GaborPacketImage(n_atoms, seed=seed)
103
+ # bg_bias is DROPPED by atoms_from_model on export, so it must not be
104
+ # learned: atoms alone must carry the image, background stays sigmoid-
105
+ # neutral. (Letting it train and then dropping it leaves compensating
106
+ # haze smeared across the frame — found by the coverage battery.)
107
+ m.bg_bias.requires_grad_(False)
108
+ m.bg_bias.zero_()
109
+ m.train()
110
+ opt = torch.optim.Adam([p for p in m.parameters() if p.requires_grad],
111
+ lr=1e-2)
112
+ t0 = time.time()
113
+ log = []
114
+ for it in range(iters):
115
+ opt.zero_grad()
116
+ mse = F.mse_loss(m.render(size, size, dev), tgt)
117
+ loss = mse + l0_weight * m.gates.l0().sum() / n_atoms
118
+ loss.backward()
119
+ if it < gate_warmup and m.gates.logits.grad is not None:
120
+ m.gates.logits.grad.zero_()
121
+ opt.step()
122
+ if it % max(1, iters // 10) == 0 or it == iters - 1:
123
+ psnr = -10 * math.log10(max(mse.item(), 1e-12))
124
+ log.append({"it": it, "mse": float(mse.item()), "psnr_db": psnr})
125
+ if progress:
126
+ progress(it / iters, f"fit {it}/{iters} psnr {psnr:.1f} dB")
127
+ atoms = atoms_from_model(m)
128
+ ledger = {
129
+ "path": "fit_image", "status": "verified-CPU",
130
+ "n_atoms_model": n_atoms, "n_atoms_open": int(len(atoms)),
131
+ "iters": iters, "size": size, "seed": seed,
132
+ "final_psnr_db": log[-1]["psnr_db"], "seconds": round(time.time() - t0, 1),
133
+ "log": log,
134
+ }
135
+ return atoms, ledger
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # path 2: text -> layer (GPU, the Bet-5 SDS loop from scratch)
140
+ # ---------------------------------------------------------------------------
141
+
142
+ def sds_layer(prompt, negative_prompt="blurry, low quality, deformed",
143
+ n_atoms=192, iters=900, render_size=256, cfg=30.0, seed=0,
144
+ l0_weight=3e-3, gate_warmup=150,
145
+ t_min=0.02, t_max_start=0.98, t_max_end=0.50,
146
+ sd_model="sd2-community/stable-diffusion-2-1-base",
147
+ progress=None):
148
+ """Text -> Gabor layer via score distillation. Line-for-line adaptation
149
+ of the verified Bet-5 SDS loop, run FROM SCRATCH (no init atoms) and
150
+ without camera jitter (a layer is a single canonical view).
151
+
152
+ HONESTY: this function has NOT been executed in the build environment
153
+ (no GPU, no SD weights). The first run on a GPU Space is a smoke test."""
154
+ if not torch.cuda.is_available():
155
+ raise RuntimeError(
156
+ "text->layer needs a GPU (Stable Diffusion score distillation). "
157
+ "This Space is on CPU hardware: use image->layer instead, or "
158
+ "duplicate the Space onto GPU hardware.")
159
+ from diffusers import StableDiffusionPipeline, DDPMScheduler
160
+
161
+ device = torch.device("cuda")
162
+ torch.manual_seed(seed)
163
+ dtype = torch.float16
164
+
165
+ pipe = StableDiffusionPipeline.from_pretrained(
166
+ sd_model, torch_dtype=dtype, safety_checker=None,
167
+ requires_safety_checker=False)
168
+ pipe.to(device)
169
+ vae, unet, tok, te = pipe.vae, pipe.unet, pipe.tokenizer, pipe.text_encoder
170
+ for mod in (vae, unet, te):
171
+ mod.requires_grad_(False)
172
+ sched = DDPMScheduler.from_pretrained(sd_model, subfolder="scheduler")
173
+ alphas = sched.alphas_cumprod.to(device)
174
+ T = sched.config.num_train_timesteps
175
+
176
+ def embed(text):
177
+ ids = tok(text, padding="max_length", max_length=tok.model_max_length,
178
+ truncation=True, return_tensors="pt").input_ids.to(device)
179
+ return te(ids)[0]
180
+
181
+ with torch.no_grad():
182
+ emb = torch.cat([embed(negative_prompt), embed(prompt)])
183
+
184
+ m = GaborPacketImage(n_atoms, seed=seed)
185
+ m.train().to(device)
186
+ opt = torch.optim.Adam(m.parameters(), lr=1e-2)
187
+
188
+ t0 = time.time()
189
+ log = []
190
+ for it in range(iters):
191
+ opt.zero_grad()
192
+ img = m.render(render_size, render_size, device, chunk=64)
193
+ x = img[None] * 2 - 1
194
+ if render_size != 512:
195
+ x = F.interpolate(x, (512, 512), mode="bilinear",
196
+ align_corners=False)
197
+ latents = vae.encode(x.to(dtype)).latent_dist.sample() \
198
+ * vae.config.scaling_factor
199
+ latents = latents.float()
200
+
201
+ frac = it / max(1, iters - 1)
202
+ t_max = t_max_start + (t_max_end - t_max_start) * frac
203
+ t = torch.randint(int(t_min * T), int(t_max * T), (1,), device=device)
204
+ noise = torch.randn_like(latents)
205
+ noisy = sched.add_noise(latents, noise, t)
206
+ with torch.no_grad():
207
+ eps = unet(torch.cat([noisy] * 2).to(dtype), torch.cat([t] * 2),
208
+ encoder_hidden_states=emb).sample.float()
209
+ eps_un, eps_tx = eps.chunk(2)
210
+ eps_hat = eps_un + cfg * (eps_tx - eps_un)
211
+ w = (1 - alphas[t]).view(-1, 1, 1, 1)
212
+ grad = (w * (eps_hat - noise)).detach()
213
+ sds_loss = (grad * latents).sum() / latents.numel()
214
+ loss = sds_loss + l0_weight * m.gates.l0().sum() / n_atoms
215
+ loss.backward()
216
+ if it < gate_warmup and m.gates.logits.grad is not None:
217
+ m.gates.logits.grad.zero_()
218
+ torch.nn.utils.clip_grad_norm_(list(m.parameters()), 1.0)
219
+ opt.step()
220
+ if it % max(1, iters // 20) == 0 or it == iters - 1:
221
+ log.append({"it": it, "sds": float(sds_loss.item()),
222
+ "t_max": t_max})
223
+ if progress:
224
+ progress(it / iters, f"sds {it}/{iters}")
225
+
226
+ m.eval().cpu()
227
+ atoms = atoms_from_model(m)
228
+ ledger = {
229
+ "path": "sds_layer", "status": "UNTESTED-GPU (adapted from verified Bet-5 loop)",
230
+ "prompt": prompt, "negative_prompt": negative_prompt,
231
+ "n_atoms_model": n_atoms, "n_atoms_open": int(len(atoms)),
232
+ "iters": iters, "render_size": render_size, "cfg": cfg, "seed": seed,
233
+ "sd_model": sd_model, "seconds": round(time.time() - t0, 1),
234
+ "known_risk": "SD2.1 mode-seeking oversaturation at high CFG (Bet-5 ledger)",
235
+ "log": log,
236
+ }
237
+ return atoms, ledger
pic.png ADDED

Git LFS Details

  • SHA256: 3050b235ced1841020e062fbc3db38544cc8eca337c12d98f6c441b9b9bc8402
  • Pointer size: 131 Bytes
  • Size of remote file: 464 kB
pic2.png ADDED

Git LFS Details

  • SHA256: 3050b235ced1841020e062fbc3db38544cc8eca337c12d98f6c441b9b9bc8402
  • Pointer size: 131 Bytes
  • Size of remote file: 464 kB
python ADDED
File without changes
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ diffusers
3
+ transformers
4
+ torch
5
+ torchvision
6
+ numpy
7
+ Pillow
8
+ fastapi
9
+ uvicorn
studio/build.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require("fs");
2
+ const strip = (src) => src.replace(/if \(typeof module[^]*$/m, "");
3
+ const core = strip(fs.readFileSync(__dirname + "/studio/core.js", "utf8"));
4
+ const scoreJs = strip(fs.readFileSync(__dirname + "/studio/studio_core.js", "utf8"));
5
+ const builtins = fs.readFileSync(__dirname + "/studio/builtins.json", "utf8");
6
+ let html = fs.readFileSync(__dirname + "/studio_ui.html", "utf8");
7
+ html = html.replace("/*CORE*/", core);
8
+ html = html.replace("/*STUDIO_CORE*/", scoreJs);
9
+ html = html.replace("/*BUILTINS*/null", builtins);
10
+ fs.writeFileSync(__dirname + "/studio/studio.html", html);
11
+ console.log("built studio/studio.html:", (html.length / 1024).toFixed(0), "KB");
studio/builtins.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"tractor": [[-0.6824, -0.45011, 2.66975, 0.70848, 0.92606, 0.44334, 5.19345, -0.09237, 0.04058, -0.01663], [-0.08584, -0.04802, 0.61626, 0.49888, 0.51436, 0.28444, 2.98078, -0.10011, 0.19875, 0.39398], [0.74864, 0.1407, 2.20893, 0.54502, 0.36724, 0.58724, 1.0072, 0.08821, 0.17557, 0.02929], [0.16677, -0.78973, 0.94754, 0.31915, 0.39057, 0.59383, 1.7488, 0.04293, 0.29863, 0.19046], [0.13943, -0.4621, 0.34775, 0.43809, 0.50801, 0.30553, 2.8646, -0.17673, 0.21056, 0.20304], [-0.05499, 0.43338, 1.6514, 0.61284, 0.45687, 0.32725, 1.11694, 0.16899, -0.3567, -0.20526], [-0.34716, 0.78101, 1.2311, 0.74696, 0.43588, 0.47387, 4.34096, 0.2865, 0.32634, 0.40835], [0.38634, 0.15072, 1.59185, 0.38671, 0.80153, 0.2006, 3.93334, -0.038, 0.2346, 0.21658], [-0.24576, -0.6867, 1.53196, 0.713, 0.38647, 0.52652, 3.95442, 0.02936, -0.05187, 0.00121], [0.35205, 0.08337, 0.53545, 0.47834, 0.43423, 0.76246, 4.15911, -0.20329, -0.08114, 0.10186], [0.35364, -0.86473, 2.06911, 1.01878, 0.6701, 0.60244, 6.00887, -0.05083, 0.02103, 0.00718], [-0.46331, 0.69726, 1.8988, 0.44293, 0.3441, 0.55859, 4.08877, 0.28312, 0.04183, 0.30179], [0.73797, -0.72063, 2.42647, 0.56377, 0.93892, 0.57773, 0.71269, -0.00084, 0.10102, 0.07746], [-0.15339, 0.31724, 0.95726, 0.50801, 0.54256, 0.30858, 0.53938, 0.02523, -0.10313, -0.26048], [0.70309, 0.45386, -0.24644, 0.30297, 0.45741, 0.70925, 4.42917, -0.02503, 0.12105, 0.27342], [0.23943, -0.29863, 0.8422, 0.33233, 0.34051, 0.22202, 3.33993, -0.1503, 0.45788, 0.30974], [-0.56384, 0.25397, 2.42246, 0.96112, 1.21737, 0.23618, 4.36676, 0.05613, 0.13365, 0.11434], [-0.36512, -0.00686, 1.45209, 0.49457, 0.31707, 0.8374, 0.28444, 0.49811, 0.06634, -0.11866], [-0.03486, 0.01865, 1.8686, 0.4669, 0.33716, 0.43868, 0.29331, 0.29541, -0.03762, -0.31555], [0.62497, 0.27048, 2.18782, 0.3361, 0.40184, 0.5221, 5.30031, -0.00354, -0.13067, -0.12973], [-0.41317, 0.11105, 0.67898, 0.48606, 0.59036, 0.28682, 5.49711, -0.10229, -0.22111, -0.29212], [-0.27571, -0.1504, 1.77764, 0.69312, 0.48048, 0.49554, 0.54236, 0.18581, 0.06243, -0.04552], [0.4641, 0.77883, 2.26806, 0.4379, 1.14871, 0.39349, 4.6492, -0.06707, -0.03793, 0.11233], [-0.56653, -0.8675, 1.28471, 0.68397, 0.3365, 0.29354, 1.07811, 0.03291, 0.04284, 0.04632], [-0.08734, 0.59009, 1.70998, 0.5352, 0.32565, 0.59399, 5.31167, -0.02259, 0.03768, 0.00686], [-0.00891, -0.13649, 0.47363, 0.39481, 0.42164, 0.37599, 6.05673, 0.05368, -0.38657, -0.22384], [0.36102, -0.14155, 1.21607, 0.16981, 0.19346, 0.64559, 0.12328, 0.14206, -0.0136, -0.00415], [-0.45032, 0.49812, 2.35641, 0.38137, 0.27461, 0.56179, 6.26456, -0.56531, -0.34132, -0.1892], [0.66365, 0.91126, 2.85526, 0.78185, 0.62224, 0.62045, 4.89805, 0.05002, -0.02027, -0.00306], [-0.78133, -0.49309, 2.58298, 0.49602, 0.94653, 0.35357, 5.84621, -0.0226, -0.05471, 0.01001], [-0.23984, 0.22701, 2.64843, 0.37651, 0.42961, 0.25471, 5.58736, -0.05288, -0.16166, -0.17766], [0.55904, 0.56313, 0.8993, 0.30673, 0.17036, 0.47454, 0.48998, -0.20619, -0.03915, -0.40454], [-0.51254, 0.78367, -0.03254, 0.35668, 0.5844, 0.4059, 5.63151, -0.02093, -0.4402, -0.38576], [-0.49183, 0.16821, -0.10355, 0.13637, 0.01697, 1.68471, 6.20816, -2.19706, -0.06284, -0.29033], [-0.75865, -0.37915, 3.63614, 0.10401, 0.17838, 2.60281, 3.56605, 0.09073, -0.11981, -0.17238], [0.17649, -0.18151, 3.10036, 0.11139, 0.32721, 0.96393, 0.67937, 0.28827, -0.27353, -0.49037], [-0.48705, 0.35371, 1.2931, 0.24513, 0.16342, 2.2225, 1.28356, -1.2217, -0.46139, -0.37179], [-0.01754, 0.55558, 2.53858, 0.27124, 0.13969, 1.24224, 3.99958, 0.01855, -0.10741, -0.03076], [-0.25695, 0.4068, 2.89412, 0.23868, 0.29239, 1.08662, 1.92178, 0.45239, 0.34315, 0.5327], [-0.23956, 0.25592, 2.23383, 0.04539, 0.10912, 3.7646, 2.57708, 5.02682, 0.98909, 0.47807], [0.628, -0.22848, 3.15552, 0.00944, 0.50113, 4.66656, 0.93257, -0.90353, 1.54592, 1.79618], [0.49409, 0.54229, 2.89216, 0.21768, 0.21806, 0.8736, 6.24187, -0.72302, -0.646, -0.51336], [-0.47663, 0.8961, 1.59818, 0.08723, 0.12112, 3.82644, 3.48794, -0.86773, -1.11731, -1.16287], [-0.71023, 0.2454, 1.27084, 0.22689, 0.22869, 1.32049, 1.42892, 0.24752, 0.09838, 0.2392], [-0.6411, -0.32881, 2.22783, 0.10306, 0.05474, 2.24301, 1.94946, -0.72905, 0.94648, 1.07426], [-0.10236, 0.0571, 2.16689, 0.11368, 0.21031, 1.10909, 2.59632, -0.30966, 0.21218, 0.25668], [0.40293, 0.14539, 3.149, 0.16438, 0.29952, 0.80921, 0.18805, 0.0656, -0.28125, -0.43057], [-0.33196, 0.22949, 2.05313, 0.13608, 0.28771, 1.73023, 4.73087, -0.82913, 0.45633, 0.43345], [0.22227, 0.09197, 1.3324, 0.09975, 0.09312, 1.08971, 3.3524, -0.11906, 0.54156, 0.12403], [0.11793, 0.39258, 1.5849, 0.06849, 0.14492, 4.87662, 2.85658, -0.79292, 1.02084, 1.32427], [0.52823, -0.67727, 0.21753, 0.11492, 0.15154, 1.38408, 3.847, -0.26577, 0.63177, 0.75418], [0.59787, -0.90371, 2.38042, 0.07061, 0.07692, 3.31604, 3.69185, 0.2139, -0.53046, -0.54509], [-0.10296, -0.98696, 0.9158, 0.28364, 0.09523, 3.94613, 1.23748, 0.07247, -0.17598, -0.20063], [-0.42743, 0.12942, 1.74352, 0.02628, 0.13178, 2.05194, 3.66078, -1.69632, 0.02142, 0.37604], [0.43108, 0.31065, 1.26171, 0.04935, 0.11457, 3.65908, 0.71281, 1.81829, -0.01638, -0.1652], [-0.64551, 0.15029, 2.79823, 0.10818, 0.0286, 1.30001, 0.2492, 1.64158, -0.61879, -0.63809], [-0.2631, -0.22718, 1.08716, 0.14952, 0.32458, 1.25624, 6.07547, 0.25972, -0.30185, -0.5652], [0.64946, -0.76278, 2.69333, 0.15504, 0.25941, 1.33319, 3.44865, 0.01556, -0.14194, -0.15755], [-0.07011, 0.47643, 0.05323, 0.05179, 0.10503, 2.85549, 4.88342, 0.95629, 1.01843, 1.2136], [0.51219, 0.76466, 1.61491, 0.06949, 0.12168, 2.3771, 0.83677, -1.79585, -1.71946, -1.62417], [-0.65308, -0.0253, 0.05561, 0.12108, 0.24282, 1.96344, 2.99588, -0.63173, 0.8612, 0.96971], [-0.75393, 0.6394, -0.58857, 0.07367, 0.13364, 1.96551, 5.57205, -1.79448, -1.70385, -1.79319], [0.67947, 0.7066, 0.76501, 0.05174, 0.08365, 3.63395, 3.78731, 2.9312, 2.8337, 2.84961], [0.78374, -0.85168, 2.90191, 0.08335, 0.05401, 1.72051, 3.89837, 0.067, -0.14074, -0.13737], [-0.65644, 0.48538, 2.2971, 0.07061, 0.13933, 1.6288, 5.87677, -0.28017, -0.3744, -0.77021], [-0.07299, -0.56282, 2.95749, 0.18481, 0.15234, 1.14593, 4.68782, -0.06461, 0.37184, 0.25643], [0.11921, -0.59593, 2.97536, 0.28076, 0.2065, 0.65973, 0.82948, 0.25906, -0.57613, -0.79168], [-0.15672, 0.40291, -0.49942, 0.09191, 0.12886, 2.13207, 3.86166, 2.41648, 0.83947, 0.87012], [-0.41411, 0.13279, 0.71077, 0.13767, 0.22816, 1.17284, 4.23561, -0.01769, 0.48793, 0.43909], [0.47887, 0.44707, 1.43521, 0.23495, 0.19113, 1.11754, 5.76585, -0.91451, -0.41865, -0.21804], [0.2231, 0.39557, 0.52648, 0.34275, 0.29421, 1.20222, 0.59739, 0.44874, 0.14287, 0.14493], [-0.07979, -0.47271, 2.52165, 0.37694, 0.05142, 2.13301, 4.86104, -0.25241, 0.37106, 0.44839], [-0.06135, -0.67232, 0.00337, 0.06471, 0.16861, 2.13669, 2.27478, -0.64686, 1.07496, 1.29683], [-0.85238, 0.43497, 0.10646, 0.19737, 0.14188, 2.26408, 0.61625, 0.72284, 0.51988, 0.7092], [-0.51825, 0.43221, -0.10887, 0.13855, 0.23206, 1.18669, 3.31322, 1.01161, 0.7205, 0.53151], [-0.12745, 0.61162, 0.59207, 0.07377, 0.20372, 3.15871, 1.16311, -0.81843, -0.967, -1.00712], [0.56756, -0.5616, 3.11854, 0.05498, 0.22625, 2.42556, 3.04, -0.24507, 0.9606, 0.9633], [0.42554, -0.29332, 3.22275, 0.14231, 0.29628, 1.05207, 0.46887, 0.32789, -0.28524, -0.58433], [0.6174, -0.22144, 3.13034, 0.0889, 0.28451, 1.9402, 1.46343, -0.21528, 0.40253, 0.44874], [0.38453, 0.49359, 3.2826, 0.29519, 0.09099, 3.12073, 2.76174, 0.91117, 0.85602, 0.87243], [0.2619, 0.34869, 2.75335, 0.10021, 0.12359, 1.57645, 2.52743, -0.44928, 0.49646, 0.50057], [0.16446, 0.27981, 1.25228, 0.11339, 0.22752, 2.15115, 5.9815, 0.7878, -0.54335, -0.68222], [0.13585, -0.71683, 2.32833, 0.05899, 0.23033, 1.45565, 3.1888, -0.16916, 0.51429, 0.6052], [0.16468, -0.60197, 3.15185, 0.07581, 0.16841, 1.76863, 6.14408, 0.08836, -0.70628, -0.68243], [-0.35743, -0.28283, 1.54047, 0.03937, 0.19287, 1.5841, 3.32628, -0.36208, 0.43891, 0.36518], [-0.62251, -0.38234, 2.61567, 0.15456, 0.32413, 1.04598, 2.40596, -0.06698, 0.0187, 0.04889], [0.79814, 0.37616, 2.43606, 0.18443, 0.12374, 2.91468, 5.89122, 0.37607, 0.34732, 0.36404], [0.51853, 0.51633, 1.9986, 0.1856, 0.16773, 0.71136, 2.96926, 1.181, 0.3687, 0.52399], [-0.85179, -0.45927, 1.21504, 0.16331, 0.12762, 2.08546, 4.38702, -0.06148, 0.04644, 0.05241], [0.82346, -0.30704, 3.00411, 0.11575, 0.11242, 0.90949, 2.33484, 0.08476, -0.0924, -0.16091], [0.27188, -0.88797, 1.57825, 0.03127, 0.2239, 2.98966, 2.26823, -0.58261, 1.38576, 1.67028], [-0.46132, -0.65263, 1.91629, 0.13799, 0.14213, 0.84698, 2.25948, -0.08674, -0.07736, -0.01761], [-0.0886, -0.07273, 0.12768, 0.10024, 0.26852, 1.98655, 3.07155, -0.14939, 0.18659, 0.28131], [0.27019, 0.29867, 0.82501, 0.1431, 0.0816, 3.81301, 1.75651, -0.80205, -0.04131, 0.0189], [0.00722, 0.14136, 0.27264, 0.09149, 0.26244, 1.54872, 3.31034, -0.3603, 0.23216, 0.43633], [-0.13459, 0.52946, 3.39189, 0.04356, 0.13784, 1.61563, 6.20546, -0.968, -0.99753, -0.75933], [0.04028, -0.67528, 3.17229, 0.15631, 0.14623, 2.75131, 2.57373, -0.08824, 0.54121, 0.48411], [0.39384, -0.52184, 3.20355, 0.12168, 0.17843, 0.94272, 0.41448, -0.02293, -0.64596, -0.4227], [0.62453, 0.47184, 2.91956, 0.16326, 0.21354, 1.26663, 2.48642, 0.30674, 0.3324, 0.37472], [0.55621, 0.38204, 1.82273, 0.09645, 0.21059, 1.67487, 2.48445, 0.84619, 0.66986, 0.67577], [-0.81937, 0.493, -0.0036, 0.05319, 0.0932, 2.92422, 2.67774, 2.59517, 2.63921, 2.22331], [-0.09406, -0.6307, 3.13683, 0.01255, 0.21603, 2.82704, 3.40586, 0.63554, -1.39215, -1.53166], [0.64661, 0.49075, 2.50093, 0.15143, 0.13703, 0.80325, 5.39367, -0.41876, -1.24179, -0.74772], [0.52089, 0.00283, 3.20765, 0.15302, 0.20652, 1.36938, 2.85748, -0.45495, 0.55844, 0.44882], [-0.74526, -0.0199, 3.14128, 0.01984, 0.22471, 4.63492, 4.49001, -2.32443, 3.06353, 3.78369], [-0.32624, 0.77647, 1.11274, 0.05544, 0.12966, 2.98644, 0.48049, -2.01201, -1.79998, -2.19936], [0.35253, 0.66839, 2.45969, 0.0704, 0.10646, 2.89173, 3.52159, 2.50863, 2.51213, 2.52348], [-0.35928, 0.52445, 0.30938, 0.27926, 0.20368, 0.67397, 0.64618, -0.68931, -0.71052, -0.44067], [0.26325, -0.73415, 1.56602, 0.14298, 0.22818, 1.3569, 3.30544, -0.26757, 0.49021, 0.37976], [0.586, 0.26585, 2.14199, 0.03439, 0.14258, 1.46884, 5.34673, 0.96545, -0.18662, 0.04527], [-0.43325, 0.67077, 1.4374, 0.34834, 0.21183, 0.64381, 0.78005, -1.10733, -1.12475, -0.93446], [0.60188, -0.86897, 0.58349, 0.06483, 0.08777, 3.37717, 3.87752, -0.28056, 0.65673, 0.6977], [-0.82983, -0.80561, 1.16311, 0.09632, 0.14492, 1.60901, 2.93708, -0.03843, 0.02064, 0.03901], [0.27384, -0.9034, 1.56931, 0.01144, 0.25121, 2.51038, 5.78922, 0.83105, -1.51501, -1.32257], [-0.17077, 0.69291, 1.04495, 0.08344, 0.08726, 2.63368, 1.62487, -0.56447, -0.58933, -0.59421], [0.61545, -0.21363, 0.00674, 0.02712, 0.44163, 3.80444, 4.43907, -1.05569, 1.55318, 1.85023], [-0.45204, -0.24833, 1.67065, 0.15145, 0.20898, 1.1456, 5.80945, 0.25417, -0.58287, -0.44938], [0.91085, 0.30058, 2.55159, 0.32394, 0.11989, 4.42473, 3.81485, -0.19905, -0.20616, -0.19025], [-0.01544, -0.44709, 2.87273, 0.09374, 0.05014, 2.55513, 3.60033, -0.121, 0.72357, 1.00287], [-0.51983, -0.20343, -0.26748, 0.06577, 0.16795, 1.95382, 2.6593, -0.33308, 0.46895, 0.3713], [0.26357, -0.76429, 1.54218, 0.15758, 0.22336, 1.04113, 2.74606, -0.2448, 0.28127, 0.71722], [-0.3753, -0.36271, 1.57935, 0.02425, 0.2412, 3.96846, 1.94527, -1.9547, 2.67872, 3.41142], [-0.54932, -0.16918, 0.31415, 0.12651, 0.14755, 0.72287, 0.30894, 0.4183, -0.36801, -0.68576], [-0.23035, -0.49408, 1.66203, 0.23955, 0.24268, 1.84685, 2.22684, 0.09834, -0.12612, -0.15135], [-0.30076, -1.01231, 0.68756, 0.22471, 0.22201, 1.60389, 0.8549, 0.01592, -0.08469, -0.10949], [-0.62797, 0.69828, 2.19777, 0.09535, 0.15567, 2.33143, 6.10749, -1.68191, -2.18704, -2.31563], [0.54776, 0.16296, 3.13115, 0.07107, 0.14779, 3.33182, 0.19249, 0.53257, -0.48905, -0.70101], [0.28274, -0.85851, 1.56778, 0.07122, 0.22749, 3.08328, 5.4518, 0.32783, -1.029, -0.99974], [-0.75271, 0.29227, 0.74579, 0.03556, 0.08729, 4.60293, 2.32435, 4.08807, 3.24784, 3.005], [0.38325, 0.54477, 0.94102, 0.40818, 0.28921, 1.7405, 1.45806, -0.04071, 0.23743, 0.32516]], "star": [[-0.51053, -0.15273, 0.68805, 0.35961, 0.16414, 0.36307, 5.26169, 0.35404, 0.26297, -0.02025], [-0.07018, -0.79386, 1.46149, 0.27812, 0.34827, 0.66017, 0.5998, 0.02515, -0.03127, -0.01383], [-0.04242, -0.06369, 0.41342, 0.52432, 0.492, 0.32373, 2.82758, -0.0946, -0.04359, 0.19702], [-0.5847, -0.79101, 0.74844, 0.71803, 0.3309, 0.57434, 3.00178, 0.00359, -0.00168, -0.00322], [-0.08832, 0.12294, 1.53853, 0.459, 0.39519, 0.38586, 0.2186, 0.20673, 0.06584, -0.12705], [-0.60689, -0.33177, 1.17408, 0.96799, 0.62049, 0.69716, 5.23717, -0.01384, 0.00645, -0.01124], [0.11267, 0.28884, 1.11652, 0.39831, 0.30015, 0.46066, 4.32536, -0.14439, 0.02783, 0.16473], [0.32993, 0.06371, 0.41569, 0.16036, 0.30224, 0.78318, 6.03392, 0.31667, 0.33124, -0.08115], [0.46703, -0.0168, -0.08327, 0.40889, 0.45086, 0.59604, 4.14405, -0.04784, -0.04327, -0.01626], [0.61214, -0.16662, 2.05353, 0.39932, 0.47518, 0.62537, 6.12889, 0.00102, 0.02287, 0.0025], [-0.86329, -0.74587, 2.69009, 0.63739, 0.86173, 0.3242, 4.08681, -0.04421, -0.04775, 0.0649], [-0.01049, -0.1416, 1.38894, 0.47137, 0.34564, 0.30828, 2.61817, -0.42365, -0.03028, 0.21405], [-0.34958, 0.02443, 1.04418, 0.35261, 0.13573, 0.36934, 2.2475, -0.06532, -0.19908, 0.0011], [0.76035, -0.07882, 0.09598, 0.48621, 0.25489, 0.319, 4.09159, -0.1085, -0.11318, 0.15415], [0.28869, -0.26514, 0.83971, 0.49766, 0.42968, 0.42586, 4.07843, -0.00878, -0.0057, -0.00669], [0.09774, 0.02822, 2.87191, 0.62577, 0.52997, 0.44792, 0.1387, 0.03507, 0.06726, -0.03023], [-0.0697, 0.00158, 0.11391, 0.33412, 0.28107, 0.70131, 5.68176, 0.08611, -0.12642, -0.14202], [0.0488, 0.10928, 1.1197, 0.42092, 0.33285, 0.27918, 3.51575, -0.38059, -0.08911, 0.27614], [-0.0164, 0.04503, 2.89844, 0.54267, 0.40763, 0.33239, 0.07615, 0.26878, 0.18469, -0.06475], [0.62477, -0.25381, 2.3837, 0.37851, 0.37412, 0.23939, 4.97219, 0.17725, 0.07981, -0.00017], [-0.34377, 0.556, 1.43592, 0.30259, 0.52825, 0.61379, 4.12942, -0.10279, -0.05566, 0.04159], [0.03347, -0.16549, 1.55273, 0.26411, 0.54085, 0.52546, 5.74224, 0.38154, 0.11136, -0.06104], [0.57593, 0.35328, 1.28074, 0.52591, 0.59368, 0.28347, 3.16451, -0.0194, 0.00598, -0.00804], [0.56303, 0.91735, 1.15253, 0.38446, 0.53731, 0.41035, 0.65649, -0.01641, -0.00829, -0.03907], [-0.0675, 0.58166, 2.06245, 0.19321, 0.19147, 0.60727, 3.05455, 0.02441, -0.07817, -0.06201], [0.09489, -0.20717, 0.78309, 0.61948, 0.61941, 0.54474, 5.3332, 0.13564, 0.05384, -0.05197], [0.76998, -0.59085, 0.72889, 0.2626, 0.36886, 0.78807, 1.89753, -0.03333, -0.01984, -0.0037], [-0.92937, -0.2769, 1.48164, 0.16458, 0.51391, 0.88393, 4.68962, 0.11147, 0.02193, -0.12731], [-0.04073, -0.3637, 0.41262, 0.24729, 0.51547, 0.74959, 3.67937, -0.13205, -0.08886, 0.04567], [-0.1472, -0.41196, 1.58488, 0.58691, 0.55867, 0.38693, 2.37401, -0.03823, -0.02307, 0.02535], [-0.00614, 0.03833, 2.69916, 0.48144, 0.53005, 0.49706, 3.20669, -0.20773, 0.04657, 0.06422], [-0.20447, 0.05738, 2.97441, 0.15979, 0.22661, 2.44411, 1.39823, -0.30817, -0.09173, 0.12606], [-0.76942, -0.19783, 0.21829, 0.13352, 0.04064, 1.49619, 5.39481, 1.10947, 0.52703, -0.51469], [-0.46293, -0.23911, 1.61092, 0.04634, 0.33172, 3.03375, 5.41794, 1.26103, 0.78286, -0.49522], [0.06065, -0.83499, 1.1654, 0.14828, 0.005, 2.62333, 4.3315, -1.33619, -0.7626, 0.64493], [-0.31575, 0.31703, 0.19491, 0.13551, 0.27143, 1.33853, 6.08375, 0.8196, 0.30475, -0.20821], [-0.56296, -0.08599, 2.18115, 0.11669, 0.28658, 1.93389, 0.64648, 0.58107, 0.24135, -0.26829], [-0.112, 0.09234, 2.63498, 0.09472, 0.08023, 3.33312, 3.20024, 0.55321, 0.85241, 0.62275], [-0.42245, -0.08477, 0.01726, 0.22589, 0.12844, 1.69061, 0.16619, 0.23566, -0.00869, -0.03135], [0.06776, 0.6268, 2.58094, 0.06912, 0.05267, 2.03968, 1.49948, 0.11016, 0.04239, -0.04381], [0.28122, 0.59345, 2.17899, 0.01946, 0.24747, 3.82596, 4.42208, -3.84417, -2.18766, 2.01159], [0.31386, -0.36299, 1.20103, 0.13599, 0.07654, 1.6408, 0.16886, -0.29135, -0.0623, 0.17822], [0.55473, -0.31699, 1.59136, 0.11557, 0.16764, 2.06004, 1.14207, -0.62327, -0.31521, 0.27353], [-0.44106, 0.45828, 0.26594, 0.03281, 0.22193, 3.7483, 2.17469, -2.02698, -1.09346, 0.96396], [0.65717, 0.59846, -0.27109, 0.14513, 0.11559, 3.48087, 5.39501, 0.12239, 0.03585, -0.03452], [-0.14798, -0.45393, 0.32433, 0.01861, 0.79582, 4.4202, 4.92375, 3.28216, 1.43719, -1.25199], [-0.60536, -0.04913, 2.21412, 0.02314, 0.24465, 4.18919, 4.46277, -4.16041, -1.95161, 1.7603], [-0.62697, -0.5098, 1.45758, 0.08486, 0.15698, 1.52263, 1.89466, 0.12829, 0.10561, -0.03876], [0.51449, -0.28711, 1.60491, 0.06129, 0.2162, 3.89484, 3.87665, 0.95616, 0.46459, -0.42435], [0.32116, 0.38261, 2.63277, 0.18795, 0.14359, 1.17108, 0.03199, 0.37471, 0.26449, -0.44471], [0.29948, 0.06897, 2.81495, 0.04949, 0.39792, 2.97239, 5.91684, 1.89518, 0.56408, -0.23856], [-0.5663, 0.85365, 2.05972, 0.08528, 0.06723, 0.8634, 4.01119, 0.17046, 0.05918, -0.06136], [-0.03235, -0.1062, 1.30497, 0.0568, 0.10161, 1.98305, 2.61473, 0.27054, 0.64028, 0.97997], [0.22074, -0.61255, -0.17283, 0.21073, 0.19802, 1.33841, 5.61818, -0.11967, -0.07294, 0.12897], [0.11109, -0.54965, 2.90996, 0.05438, 0.19995, 2.67146, 5.49666, 0.89583, 0.30495, -0.30528], [0.4764, -0.2783, 1.57209, 0.01317, 0.20611, 3.7017, 3.85687, 1.86438, 0.81372, -0.68967], [-0.79551, -0.69126, 1.68869, 0.18938, 0.18063, 1.48869, 2.0417, -0.09177, -0.04576, 0.05076], [-0.69563, -0.39051, 1.65237, 0.17716, 0.12341, 3.1259, 4.8354, -0.17853, -0.09295, 0.08889], [0.4658, 0.20264, 1.3172, 0.11945, 0.05827, 0.84899, 2.47738, 0.35942, 0.24163, -0.24982], [0.12673, -0.61229, -0.30152, 0.02136, 0.30796, 3.50656, 4.4067, -2.95706, -1.8063, 1.69187], [0.01654, -0.88378, 1.64948, 0.13909, 0.02471, 2.18708, 4.81224, 1.36907, 0.71828, -0.64168], [0.52927, 0.0586, 0.94322, 0.05803, 0.13738, 3.00829, 1.65677, 1.20588, 0.70135, -0.64307], [0.11272, 0.53699, 1.59168, 0.27775, 0.20228, 1.15561, 4.67559, -0.23347, -0.19164, 0.09554], [0.19108, 0.06434, 0.15906, 0.05952, 0.08047, 2.55291, 0.86874, -0.65442, -1.18447, -1.1727], [-0.28411, -0.42135, 3.38232, 0.09539, 0.08902, 0.98512, 2.76018, 0.0392, 0.02729, -0.10188], [-0.42219, -0.22683, 2.16676, 0.17983, 0.29019, 0.84596, 2.02503, -0.16512, -0.1051, 0.16705], [0.24219, 0.4444, 2.16647, 0.13174, 0.20905, 1.64917, 0.1576, 0.92216, 0.24084, -0.18353], [0.14429, 0.51728, 1.87757, 0.09842, 0.12765, 2.3035, 0.3321, -0.21602, -0.00533, -0.04041], [0.00122, -0.42501, 3.22421, 0.14635, 0.15283, 1.00079, 3.56606, -0.95341, -0.46443, 0.19811], [0.01914, 0.02327, 1.68727, 0.21134, 0.13233, 1.49743, 6.22365, -0.51815, -0.83356, -0.67064], [0.28187, 0.36377, -0.38082, 0.10839, 0.14596, 0.8208, 5.92113, 1.09395, 0.3801, -0.17753], [0.22536, 0.49825, 2.20265, 0.05758, 0.25291, 2.76728, 0.46329, 1.42985, 0.51988, -0.41777], [-0.45783, -0.0778, 1.84375, 0.14555, 0.14505, 1.25657, 0.34373, 1.67452, 0.522, -0.42744], [0.13987, -0.2105, 2.01916, 0.13301, 0.19415, 1.81491, 0.56274, 1.00172, 0.68436, -0.27158], [0.50098, -0.05012, 0.76651, 0.19449, 0.2237, 1.15756, 3.65245, -0.66405, -0.35603, 0.28384], [0.45732, 0.61373, 1.1175, 0.15351, 0.04725, 0.83834, 0.31755, 1.27973, 0.78829, -0.76025], [-0.31064, 0.32627, 3.03941, 0.17271, 0.11314, 0.89206, 3.90119, -1.08711, -0.48405, 0.42314], [-0.05244, 0.46698, 1.28082, 0.41268, 0.19876, 1.82854, 1.96378, 0.14533, 0.08843, -0.04229], [0.65368, -0.04039, 0.91601, 0.01431, 0.2486, 2.64038, 0.37863, -1.53102, -0.6903, 0.61226], [0.11256, -0.49758, 2.90797, 0.08858, 0.20068, 1.66331, 1.9988, -0.95035, -0.3115, 0.39809], [-0.55115, -0.2651, 1.56625, 0.01558, 0.26992, 4.61937, 1.74074, -4.7308, -2.21314, 2.21696], [-0.57442, 0.29028, 2.07148, 0.07815, 0.11578, 1.77736, 3.97166, 0.24663, 0.10249, -0.12441], [-0.57351, -0.03527, 2.31236, 0.08946, 0.13378, 1.57572, 4.21907, -0.42142, -0.45787, 0.33859], [0.44745, -0.04054, 1.57304, 0.1578, 0.12902, 1.05371, 0.84855, 1.15497, 0.12029, -0.08756], [0.47798, 0.46267, -0.28408, 0.01747, 0.25291, 4.78872, 1.38827, 3.57491, 2.16954, -2.14804], [-0.39951, 0.56329, 3.60983, 0.06129, 0.12474, 2.31554, 0.20968, 1.7986, 0.71715, -0.67198], [0.2029, 0.94365, 1.51817, 0.12817, 0.15872, 0.90829, 5.2715, 0.09825, 0.00224, -0.02612], [-0.2578, 0.40853, 0.87754, 0.17928, 0.23013, 1.80534, 6.23272, 0.42828, 0.23362, -0.20952], [0.62698, -0.08746, 0.60216, 0.13076, 0.15134, 1.24408, 4.03669, -0.78655, -0.2753, 0.33121], [-0.11036, -0.36616, 0.44709, 0.11328, 0.21919, 1.53497, 2.63921, -1.00777, -0.44399, 0.4229], [0.57028, 0.65051, -0.01295, 0.18803, 0.20359, 1.5268, 2.61712, 0.20971, 0.08408, -0.06806], [0.91737, -0.25154, 3.13936, 0.36808, 0.01164, 0.55714, 4.98839, 3.41089, 1.68578, -1.47782], [0.00464, -0.67149, 0.51839, 0.07402, 0.08209, 1.77517, 6.14924, 1.13503, 0.69195, -0.43628], [0.45897, 0.66312, 1.85728, 0.07313, 0.11845, 1.14973, 3.4493, -0.06586, -0.09912, 0.17318], [0.03662, 0.17978, 2.97047, 0.11176, 0.035, 1.47455, 6.27172, -0.71513, -1.26976, -0.78411], [0.61477, -0.81121, 1.1419, 0.13361, 0.13146, 0.78051, 3.17935, -0.0222, 0.00013, -0.0463], [-0.47039, 0.16684, 2.52823, 0.13584, 0.09956, 2.82399, 0.4835, -0.37612, -0.15246, 0.13208], [0.0925, 0.27645, 0.13734, 0.26465, 0.1313, 1.96463, 3.63057, 0.45913, 0.11807, -0.09602], [0.74131, -0.17449, 2.55596, 0.14172, 0.03612, 0.77917, 3.09129, -2.06065, -1.08434, 0.96287], [-0.49518, 0.1883, 0.21456, 0.10268, 0.09144, 2.59442, 3.75846, -0.11141, -0.00155, -0.05859], [-0.08749, -0.04503, 0.36005, 0.16451, 0.14805, 1.75564, 1.96861, 0.37147, 0.61096, 0.57908], [-0.07382, -0.4671, 0.31571, 0.07188, 0.24398, 1.9183, 0.03077, 0.76225, 0.24352, -0.29235], [-0.41184, 0.73294, 4.04825, 0.01519, 0.12271, 2.8311, 3.22725, 0.54958, 0.21104, -0.20166], [0.05866, -0.48599, 1.87293, 0.06712, 0.14864, 1.88058, 2.60092, -0.29807, -0.2459, 0.00046], [-0.22666, 0.54185, 4.08157, 0.04808, 0.18162, 2.6353, 2.15601, -1.70536, -0.5344, 0.645], [0.11034, 0.555, 1.8767, 0.10493, 0.15919, 2.8434, 3.33574, 0.11173, 0.08681, -0.12987], [-0.00432, -0.59956, 0.68151, 0.21, 0.35642, 1.18027, 1.97778, -0.13986, -0.03979, 0.08981], [-0.22001, -0.41801, 1.9694, 0.2259, 0.02907, 0.6921, 0.39981, -0.62639, -0.23372, 0.2716], [-0.69313, 0.19448, 1.77782, 0.09053, 0.15994, 2.16316, 3.30231, 0.03186, 0.04356, -0.026], [-0.07556, 0.43344, 0.94755, 0.04052, 0.33977, 1.96234, 0.87423, 1.98507, 0.77476, -0.48832], [-0.25397, 0.58587, 0.97557, 0.01057, 0.2231, 2.65974, 3.26704, -2.58728, -1.3095, 1.18591], [0.51088, -0.39587, 1.97375, 0.26423, 0.20145, 1.12756, 1.03371, -0.17067, -0.13254, 0.15733], [-0.51523, 0.7042, 1.79156, 0.18224, 0.13081, 1.36296, 5.3965, -0.16144, -0.0463, -0.00371], [-0.32852, -0.24906, 1.52681, 0.08517, 0.13409, 2.13653, 1.6727, -0.87072, -0.15162, 0.25915], [0.00806, 0.84109, 0.15468, 0.10667, 0.14355, 0.93857, 6.05272, -0.02536, -0.02323, -0.01905]], "boat": [[-0.35803, -0.04712, 0.33676, 0.46106, 0.39276, 0.50475, 4.13232, 0.00283, 0.01229, -0.00976], [-0.41203, 0.10897, 2.52432, 0.40229, 0.57243, 0.23464, 5.08722, -0.00107, 0.00666, -0.07483], [0.12509, -0.17784, 2.45227, 0.25832, 0.32034, 0.71608, 3.56406, -0.55495, -0.81191, -0.24526], [-0.14167, -0.02618, 0.79131, 0.39298, 0.77199, 0.53086, 5.38417, 0.08289, -0.01826, -0.03796], [-0.01467, -0.16899, 0.41817, 0.36342, 0.33713, 0.73269, 5.66457, 0.22791, 0.20826, 0.12245], [0.11882, 0.17061, 1.5263, 0.42596, 0.48433, 0.64705, 4.39323, -0.0514, -0.11341, -0.10323], [0.38566, 0.39142, 0.93088, 0.41204, 0.58494, 0.39645, 0.64687, 0.12816, -0.03829, -0.08142], [0.14899, 0.39127, 1.63262, 0.57184, 0.2499, 0.65196, 5.76974, -0.0592, -0.16024, -0.1084], [-0.19028, -0.94504, 1.12366, 0.76464, 0.29497, 0.38367, 4.45984, 0.18743, 0.14662, 0.10917], [0.23686, 0.09142, 1.30661, 0.29763, 0.25272, 0.73147, 4.36912, -0.39693, -0.46958, -0.27099], [0.18348, 0.89217, 1.06554, 0.55058, 0.48248, 0.40182, 1.53431, 0.07829, 0.00204, -0.18879], [0.70699, -0.80219, 0.73109, 0.56942, 0.85824, 0.65426, 3.40796, -0.00738, -0.0733, -0.06328], [0.08463, 0.35836, 1.62099, 0.36608, 0.36466, 0.74301, 2.07875, -0.09204, 0.31805, 0.30015], [0.76143, 0.12624, 2.48314, 0.52285, 0.57768, 0.2519, 4.92292, 0.148, 0.04802, -0.05123], [-0.05321, -0.44144, 2.33538, 0.36346, 0.49199, 0.60418, 3.36667, -0.18666, -0.15261, -0.08766], [-0.49942, 0.39841, 2.1084, 0.46562, 0.37552, 0.79064, 4.52097, 0.01144, -0.00033, 0.00854], [0.12141, -0.21516, 0.92039, 0.34648, 0.19827, 0.3196, 6.14339, 0.22338, 0.23482, 0.45767], [0.11031, -0.58101, 1.14666, 0.51466, 0.84179, 0.23172, 2.30048, -0.1002, 0.06144, 0.03765], [0.03505, 0.09251, 1.79332, 0.55181, 0.44314, 0.35617, 1.28299, 0.10084, 0.28937, 0.17874], [0.19853, 0.07481, 3.03637, 0.23448, 0.52977, 0.37181, 5.88989, 0.19077, 0.22606, -0.15106], [0.05457, -0.27555, 0.96562, 0.33537, 0.23879, 0.41219, 2.59711, -0.4955, -0.37183, -0.08309], [0.24537, -0.88183, -0.01118, 0.39644, 0.76785, 0.60463, 0.83211, 0.08243, 0.07792, 0.08496], [0.35768, -0.15754, 0.02243, 0.40371, 0.34036, 0.4328, 0.9687, 0.27805, 0.11875, 0.05968], [0.45203, -0.6876, 2.51104, 0.64175, 0.47358, 0.9028, 2.91444, 0.03378, 0.01227, 0.00709], [-0.84711, 0.43104, 1.14221, 0.48062, 0.811, 0.58063, 1.42528, -0.01733, 0.03998, 0.02921], [-0.65691, -0.00547, 1.40793, 0.59829, 0.31731, 0.56019, 3.49149, -0.00514, 0.01508, -0.01819], [0.31537, 0.54621, 2.14771, 0.42914, 0.33822, 0.52531, 2.59495, -0.12871, 0.04671, 0.30871], [0.18389, -0.0192, 1.72127, 0.40317, 0.25492, 0.65319, 3.35443, -0.23199, -0.1824, -0.46588], [0.24018, -0.26153, -0.18801, 0.30371, 0.29881, 0.47301, 3.88628, -0.36114, -0.17004, -0.31887], [-0.51405, 0.71201, 1.76857, 0.34024, 0.31019, 0.61892, 3.98611, -0.1618, 0.17146, 0.45924], [-0.56883, 0.49773, 1.86237, 0.20481, 0.23447, 0.84602, 0.11957, 0.15594, -0.29436, -0.53234], [0.23848, 0.06412, 1.51014, 0.11924, 0.22533, 1.53357, 0.82589, 2.20238, 1.65043, 0.62457], [-0.21027, -0.54098, 2.46659, 0.22927, 0.1359, 1.27134, 5.19799, -0.11871, -0.0959, -0.13234], [-0.10351, -0.26834, 0.01181, 0.07839, 0.31304, 2.06626, 2.96916, -1.71056, -1.0688, -0.53551], [-0.0248, -0.55869, 2.72252, 0.17045, 0.16361, 1.86115, 0.88871, 0.67705, 0.66146, 0.1311], [0.72597, 0.40145, 3.83772, 0.0798, 0.09103, 2.35273, 4.93156, 0.25208, -0.42274, -0.58041], [0.34289, 0.39094, 1.38718, 0.16128, 0.19588, 1.19757, 5.685, 0.16747, -0.20192, -0.7312], [0.06644, -0.60577, -0.63073, 0.08736, 0.19748, 1.87261, 0.1431, 1.77121, 0.94503, 0.64713], [-0.21724, -0.33086, 0.47813, 0.08997, 0.08946, 0.89736, 6.21413, -0.23098, -0.24363, -0.16915], [-0.22979, -0.72439, 1.33041, 0.1924, 0.02362, 2.09594, 5.21617, -0.2779, -0.3288, -0.2743], [-0.28104, -0.15587, 1.23101, 0.13788, 0.09774, 0.8023, 3.10253, 0.0241, -0.01377, 0.13467], [0.52665, 0.56464, 0.60309, 0.11355, 0.19963, 1.72993, 0.41176, 0.25423, -0.35009, -1.00354], [-0.09769, 0.05324, 2.59468, 0.10418, 0.0784, 1.84332, 5.30456, -0.43615, -0.27265, -0.47339], [0.14923, 0.43003, 1.26489, 0.25233, 0.08449, 2.11129, 2.12432, -0.07108, -0.0937, -0.14313], [-0.79014, 0.61709, 1.90558, 0.37037, 0.11136, 1.64229, 5.18906, -0.13822, 0.16428, 0.3693], [-0.12659, 0.30183, 1.57897, 0.03235, 0.65149, 3.35957, 4.83163, 0.62631, -1.09254, -2.34957], [0.74631, -0.27895, 2.57092, 0.32236, 0.22899, 1.70782, 4.40745, -0.10511, -0.11866, -0.0974], [-0.4706, -1.01786, 1.562, 0.11467, 0.13857, 1.21133, 5.87333, 0.02135, -0.01299, 0.02737], [-0.05836, 0.04774, 0.62651, 0.22665, 0.15889, 1.20993, 2.69819, -0.14953, -0.32859, -0.33054], [0.48753, 0.18505, 1.40111, 0.09815, 0.21416, 1.35927, 0.28737, -0.22569, -0.15438, -0.10252], [0.19376, 0.78175, 0.86577, 0.232, 0.101, 1.39569, 2.35936, 0.06397, 0.01969, -0.15017], [-0.32573, 0.28337, 1.44029, 0.10177, 0.09071, 2.11344, 1.90905, -0.0877, 0.22625, 0.5373], [-0.10059, 0.20726, -0.15807, 0.04677, 0.10242, 1.83554, 3.26912, -2.11536, -1.82547, -1.57415], [0.84624, -0.47251, 1.47539, 0.20866, 0.22456, 1.01765, 2.08865, 0.01409, 0.01689, 0.01502], [0.50083, -0.29505, 1.39077, 0.08953, 0.07203, 1.88329, 0.12959, -0.05621, -0.08363, -0.1078], [-0.1445, -0.95257, 2.90764, 0.07775, 0.01977, 2.03443, 3.97165, 0.97008, 0.86927, 0.57117], [-0.47625, -0.61653, 2.43245, 0.11514, 0.11414, 1.41168, 3.49398, 0.02675, 0.04917, 0.00409], [0.15157, 0.15012, 1.47682, 0.03508, 0.16766, 3.63825, 3.18341, 0.92059, 0.83145, 0.42329], [0.14193, 0.25131, 2.49569, 0.17593, 0.13607, 2.08532, 0.75981, -0.11116, 0.00501, 0.01148], [-0.66478, 0.65038, 1.19491, 0.35746, 0.06058, 2.73093, 5.53523, 0.11769, -0.16289, -0.3357], [0.61342, 0.56992, 3.7336, 0.02526, 0.21197, 4.26954, 1.86086, -0.6535, 0.93151, 2.24838], [0.6787, -0.92989, 0.41269, 0.25026, 0.38995, 1.63202, 5.8353, -0.00703, -0.02194, -0.01415], [0.04638, -0.55831, 2.42571, 0.12093, 0.16263, 0.73856, 6.14543, 0.36226, 0.49601, 0.35012], [-0.21198, -0.14439, 1.58812, 0.15066, 0.05129, 0.70131, 6.16365, -0.33509, -0.32551, -0.12919], [-0.2144, 0.03032, 2.91624, 0.05753, 0.07852, 1.87215, 0.1741, -0.39673, -0.36386, -0.22987], [-0.13844, -0.71483, 0.07072, 0.07635, 0.33219, 2.26688, 4.82888, 0.2467, 0.18457, 0.16929], [0.15999, -0.29622, 2.69916, 0.20213, 0.24233, 1.002, 5.81437, 1.45309, 1.05946, 0.17348], [-0.07523, 0.37701, 0.46661, 0.06141, 0.03343, 1.56853, 0.70886, -1.05117, -1.23321, -0.96933], [-0.16682, -0.99045, 1.12432, 0.05948, 0.2007, 1.9382, 3.04478, 0.1796, 0.16536, 0.09781], [0.80351, -0.49286, 1.98354, 0.17694, 0.25954, 1.03301, 1.10691, 0.00779, 0.03798, 0.06703], [0.82117, 0.6799, 1.03574, 0.09622, 0.19606, 0.89922, 4.38744, 0.08018, -0.11594, -0.18364], [-0.13149, -0.81441, 0.15398, 0.07111, 0.12125, 1.76403, 2.42052, -0.97914, -1.07737, -0.75186], [-0.61828, -0.31465, 0.80308, 0.12261, 0.14744, 0.48786, 2.49841, -0.08821, -0.04903, 0.05376], [0.39459, -0.10493, 2.5056, 0.09057, 0.16075, 1.27382, 3.16278, -2.35689, -1.60934, -0.60862], [-0.09579, 0.81222, 1.57241, 0.01623, 0.37915, 5.3683, 1.47924, 0.88204, -1.30361, -2.71795], [-0.28004, 0.49948, 1.65494, 0.17664, 0.2661, 0.75148, 0.10037, 0.22502, -0.18689, -0.88598], [0.24841, -0.53404, 2.51898, 0.01783, 0.41616, 3.90649, 2.67916, 1.43219, 1.213, 0.7207], [0.11963, -0.67874, 0.9541, 0.60554, 0.00669, 0.856, 0.23229, -1.54654, -1.29281, -0.86237], [-0.52329, 0.77623, 1.58572, 0.03163, 0.13249, 1.86478, 3.20299, -0.22208, 0.35925, 0.72493], [0.0299, 0.77708, 1.54304, 0.06955, 0.29003, 2.61709, 3.92385, -0.28405, 0.4157, 1.04792], [-0.17194, 0.06405, 2.53053, 0.20492, 0.08149, 1.43345, 5.17384, -0.2267, 0.02543, 0.00314], [0.55317, 0.66661, 1.90108, 0.42554, 0.21636, 1.83571, 6.12928, -0.07583, 0.08871, 0.17245], [-0.44615, 0.12555, 3.12824, 0.23051, 0.18191, 1.53885, 4.09845, 0.04691, 0.01757, 0.05995], [-0.24918, -0.17663, 0.32413, 0.05639, 0.06614, 0.86147, 1.95925, 0.19675, 0.15027, 0.25504], [0.29911, 0.60437, 0.50524, 0.14357, 0.11689, 0.9624, 2.34493, -0.14827, 0.15435, 0.16659], [-0.41892, 0.7034, 1.53665, 0.09705, 0.16946, 1.9209, 3.17691, -0.1839, 0.26102, 0.75296], [-0.79271, 0.58647, 2.62031, 0.05162, 0.19227, 2.43745, 1.26147, 0.4843, -0.74477, -1.61327], [-0.60484, 0.03067, 1.28536, 0.17084, 0.14292, 1.52045, 3.6335, -0.04817, 0.02099, 0.0977], [-0.34671, 0.04527, 3.40945, 0.07248, 0.08656, 1.07657, 2.47549, 0.04936, 0.10153, 0.02661], [-0.22547, 0.15271, 0.20253, 0.16391, 0.13752, 1.71726, 4.2191, -0.02961, 0.15912, 0.14774], [-0.00687, -0.73161, 2.5028, 0.07599, 0.1783, 3.06414, 0.4712, 1.32528, 1.34062, 0.86031], [-0.15703, -0.95468, 3.25628, 0.1045, 0.01257, 2.11908, 1.13725, -0.45612, -0.42957, -0.29404], [-0.05372, 0.24665, 0.22127, 0.08319, 0.07884, 2.87674, 4.38322, -0.70704, -0.72359, -0.67824], [0.24342, 0.81571, 1.44644, 0.29137, 0.25659, 0.76395, 1.4982, 0.29314, -0.19535, -0.53801], [-0.68747, 0.54977, 0.22252, 0.25197, 0.1895, 0.88591, 2.94688, -0.12023, 0.23612, 0.56521], [0.47923, 0.30185, 1.57186, 0.00961, 0.2431, 1.65118, 0.45513, 0.60631, -0.81851, -1.97919], [0.04753, -0.49167, 2.745, 0.17775, 0.21465, 1.05867, 0.09611, 0.74664, 0.67871, 0.72987], [-0.91372, 0.37551, 2.54913, 0.04007, 0.06297, 3.36503, 3.85676, -0.40663, 0.58702, 1.40486], [0.409, 0.69172, 1.80022, 0.23277, 0.04852, 2.95077, 6.09774, 0.09222, -0.18616, -0.43305], [-0.96688, 0.90454, 1.94801, 0.08507, 0.17094, 1.69168, 6.06099, -0.06892, 0.06249, 0.09266], [-0.44291, -0.08531, -0.15556, 0.15896, 0.14925, 2.22345, 4.15054, -0.02685, -0.03191, -0.01925], [-0.22339, 0.07173, 0.92299, 0.05924, 0.04536, 1.11415, 5.24568, -0.18604, -0.34506, -0.07539], [0.56561, -0.04271, -0.6544, 0.02191, 0.16757, 3.69071, 4.20103, -4.09617, -4.05649, -2.88399], [0.35068, -0.35148, 2.51338, 0.05881, 0.30758, 2.72514, 1.79961, -1.26945, -1.30986, -1.07607], [-0.18379, -0.3334, 0.0015, 0.01045, 0.44674, 3.65024, 2.58839, 1.95094, 1.62632, 1.11477], [0.55417, 0.01449, 2.11492, 0.08191, 0.07838, 2.38535, 2.59567, -1.26342, -1.06981, -0.79691], [0.32377, 0.11651, 1.57898, 0.0271, 0.21155, 4.63407, 3.87996, -6.17162, -5.12337, -3.60569]]}
studio/core.js ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Slapstack Playroom core — faithful JS port of SlapstackBet6 math.
2
+ FIELDS: [x, y, theta, su, sv, f, phase, r, g, b]
3
+ Verified against bet6_open.py / bet6_bp_binding.py / bet6_multimodal.py
4
+ by tests_node.js before being embedded in the playroom. */
5
+ "use strict";
6
+
7
+ const TAU = Math.PI * 2;
8
+
9
+ function wrapPi(d) {
10
+ return ((d + Math.PI) % TAU + TAU) % TAU - Math.PI;
11
+ }
12
+
13
+ function rotApply(rho, x, y) {
14
+ const c = Math.cos(rho), s = Math.sin(rho);
15
+ return [c * x - s * y, s * x + c * y];
16
+ }
17
+
18
+ /* Exact Sim(2) action on atom parameters (the Bet 5 algebra):
19
+ xy -> s R xy + t, theta -> theta + rho, sigma -> s sigma, f -> f/s.
20
+ Envelope-relative phase and color are INVARIANT. */
21
+ function transformAtoms(atoms, xi) {
22
+ const [tx, ty, rho, lam] = xi;
23
+ const s = Math.exp(lam);
24
+ const out = new Array(atoms.length);
25
+ for (let i = 0; i < atoms.length; i++) {
26
+ const a = atoms[i];
27
+ const [rx, ry] = rotApply(rho, a[0], a[1]);
28
+ out[i] = [
29
+ s * rx + tx, s * ry + ty,
30
+ a[2] + rho,
31
+ a[3] * s, a[4] * s,
32
+ a[5] / s,
33
+ a[6],
34
+ a[7], a[8], a[9],
35
+ ];
36
+ }
37
+ return out;
38
+ }
39
+
40
+ /* Sim(2)-invariant intrinsic signature: identity lives here. */
41
+ function signature(atoms) {
42
+ return atoms.map(a => [
43
+ Math.log(a[3] * a[5]),
44
+ Math.log(a[3] / a[4]),
45
+ Math.cos(a[6]), Math.sin(a[6]),
46
+ a[7], a[8], a[9],
47
+ ]);
48
+ }
49
+
50
+ /* Two pose-vote hypotheses per correspondence (pi-ambiguity fix):
51
+ H0: rho = d_theta, phi_obs == phi_tmpl
52
+ H1: rho = d_theta + pi, phi_obs == -phi_tmpl */
53
+ function poseVotes2pi(obs, tmpl, sigPhase = 0.35) {
54
+ const s = Math.pow((obs[3] / tmpl[3]) * (obs[4] / tmpl[4]) * (tmpl[5] / obs[5]), 1 / 3);
55
+ const dTheta = obs[2] - tmpl[2];
56
+ const out = [];
57
+ for (let H = 0; H < 2; H++) {
58
+ const rho = wrapPi(dTheta + H * Math.PI);
59
+ const phiExp = H === 0 ? tmpl[6] : -tmpl[6];
60
+ const dphi = wrapPi(obs[6] - phiExp);
61
+ const pc = -0.5 * dphi * dphi / (sigPhase * sigPhase);
62
+ const [rx, ry] = rotApply(rho, tmpl[0], tmpl[1]);
63
+ out.push([[obs[0] - s * rx, obs[1] - s * ry, rho, Math.log(s)], pc]);
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /* ------- small dense linear algebra on 4x4 (row-major flat arrays) ------- */
69
+ function mat4Inv(m) {
70
+ // Gauss-Jordan, fine for well-conditioned SPD 4x4s here.
71
+ const a = m.map(r => r.slice());
72
+ const inv = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]];
73
+ for (let col = 0; col < 4; col++) {
74
+ let piv = col;
75
+ for (let r = col + 1; r < 4; r++)
76
+ if (Math.abs(a[r][col]) > Math.abs(a[piv][col])) piv = r;
77
+ [a[col], a[piv]] = [a[piv], a[col]];
78
+ [inv[col], inv[piv]] = [inv[piv], inv[col]];
79
+ const d = a[col][col];
80
+ for (let j = 0; j < 4; j++) { a[col][j] /= d; inv[col][j] /= d; }
81
+ for (let r = 0; r < 4; r++) {
82
+ if (r === col) continue;
83
+ const f = a[r][col];
84
+ for (let j = 0; j < 4; j++) { a[r][j] -= f * a[col][j]; inv[r][j] -= f * inv[col][j]; }
85
+ }
86
+ }
87
+ return inv;
88
+ }
89
+ function mat4Det(m) {
90
+ const a = m.map(r => r.slice());
91
+ let det = 1;
92
+ for (let col = 0; col < 4; col++) {
93
+ let piv = col;
94
+ for (let r = col + 1; r < 4; r++)
95
+ if (Math.abs(a[r][col]) > Math.abs(a[piv][col])) piv = r;
96
+ if (piv !== col) { [a[col], a[piv]] = [a[piv], a[col]]; det = -det; }
97
+ det *= a[col][col];
98
+ if (a[col][col] === 0) return 0;
99
+ for (let r = col + 1; r < 4; r++) {
100
+ const f = a[r][col] / a[col][col];
101
+ for (let j = col; j < 4; j++) a[r][j] -= f * a[col][j];
102
+ }
103
+ }
104
+ return det;
105
+ }
106
+ function mat4Vec(m, v) {
107
+ return [0,1,2,3].map(i => m[i][0]*v[0]+m[i][1]*v[1]+m[i][2]*v[2]+m[i][3]*v[3]);
108
+ }
109
+ function matAdd(A, B, wB = 1) {
110
+ return A.map((row, i) => row.map((x, j) => x + wB * B[i][j]));
111
+ }
112
+
113
+ /* Greedy mode-seeking init (angle-aware), port of _density_peaks. */
114
+ function densityPeaks(votes, weights, M, radius = 0.45) {
115
+ const scale = [0.15, 0.15, 0.30, 0.20];
116
+ const n = votes.length;
117
+ const dens = new Float64Array(n);
118
+ for (let i = 0; i < n; i++) {
119
+ let acc = 0;
120
+ for (let k = 0; k < n; k++) {
121
+ const d0 = (votes[k][0] - votes[i][0]) / scale[0];
122
+ const d1 = (votes[k][1] - votes[i][1]) / scale[1];
123
+ const d2 = wrapPi(votes[k][2] - votes[i][2]) / scale[2];
124
+ const d3 = (votes[k][3] - votes[i][3]) / scale[3];
125
+ acc += weights[k] * Math.exp(-0.5 * (d0*d0 + d1*d1 + d2*d2 + d3*d3));
126
+ }
127
+ dens[i] = acc;
128
+ }
129
+ const peaks = [];
130
+ const alive = new Uint8Array(n).fill(1);
131
+ for (let m = 0; m < M; m++) {
132
+ let best = -1, bestD = -Infinity;
133
+ for (let i = 0; i < n; i++)
134
+ if (alive[i] && dens[i] > bestD) { bestD = dens[i]; best = i; }
135
+ if (best < 0) break;
136
+ peaks.push(votes[best].slice());
137
+ for (let i = 0; i < n; i++) {
138
+ const dxy = Math.hypot(votes[i][0] - votes[best][0], votes[i][1] - votes[best][1]);
139
+ const dr = Math.abs(wrapPi(votes[i][2] - votes[best][2]));
140
+ if (dxy + dr <= radius) alive[i] = 0;
141
+ }
142
+ }
143
+ return peaks;
144
+ }
145
+
146
+ /* Loopy BP binding of obs atoms to K templates at unknown poses.
147
+ Port of bet6_open.bp_bind: candidates carry both pi-hypotheses,
148
+ cavity messages, damping, branch-aligned rotation fusion.
149
+ Options: clampPose — array of length K; if clampPose[k] is a pose xi,
150
+ object k's pose is held fixed (conditioning-as-intervention) and only
151
+ the assignment beliefs re-equilibrate around it. */
152
+ function bpBind(templates, obs, opts = {}) {
153
+ const iters = opts.iters ?? 40;
154
+ const damping = opts.damping ?? 0.5;
155
+ const cavity = opts.cavity ?? true;
156
+ const sigVar = opts.sigVar ?? 0.08;
157
+ const outLL = opts.outLL ?? -14.0;
158
+ const clampPose = opts.clampPose ?? null;
159
+ const hiddenMask = opts.hiddenMask ?? null; // per-atom: true = no evidence
160
+ const onIter = opts.onIter ?? null;
161
+
162
+ const K = templates.length;
163
+ const sigT = templates.map(signature);
164
+ const sigO = signature(obs);
165
+ const N = obs.length;
166
+
167
+ const Vdiag = [0.03 * 0.03, 0.03 * 0.03, 0.05 * 0.05, 0.05 * 0.05];
168
+ const Vinv = [[1/Vdiag[0],0,0,0],[0,1/Vdiag[1],0,0],[0,0,1/Vdiag[2],0],[0,0,0,1/Vdiag[3]]];
169
+ const Vmat = [[Vdiag[0],0,0,0],[0,Vdiag[1],0,0],[0,0,Vdiag[2],0],[0,0,0,Vdiag[3]]];
170
+ const P0inv = [[1e-2,0,0,0],[0,1e-2,0,0],[0,0,1e-2,0],[0,0,0,1e-2]];
171
+
172
+ // ---- candidate generation: 3 signature-nearest per template, 2 hypotheses
173
+ const cands = [], votes = [], base = [];
174
+ for (let i = 0; i < N; i++) {
175
+ const c = [], v = [], b = [];
176
+ if (!(hiddenMask && hiddenMask[i])) {
177
+ for (let k = 0; k < K; k++) {
178
+ const d2 = sigT[k].map(st => {
179
+ let acc = 0;
180
+ for (let q = 0; q < 7; q++) { const d = st[q] - sigO[i][q]; acc += d * d; }
181
+ return acc;
182
+ });
183
+ const order = d2.map((d, j) => [d, j]).sort((p, q) => p[0] - q[0]).slice(0, 3);
184
+ for (const [dj, j] of order) {
185
+ for (const [xi, pc] of poseVotes2pi(obs[i], templates[k][j])) {
186
+ c.push([k, j]); v.push(xi); b.push(-0.5 * dj / sigVar + pc);
187
+ }
188
+ }
189
+ }
190
+ }
191
+ cands.push(c); votes.push(v); base.push(b);
192
+ }
193
+
194
+ // ---- beliefs seeded from the identity+phase channel
195
+ let B = [];
196
+ for (let i = 0; i < N; i++) {
197
+ const ll = base[i].concat([outLL]);
198
+ const mx = Math.max(...ll);
199
+ let e = ll.map(x => Math.exp(x - mx));
200
+ const s = e.reduce((a, x) => a + x, 0);
201
+ B.push(e.map(x => x / s));
202
+ }
203
+
204
+ // ---- pose init: density peak of each object's votes (or the clamp)
205
+ let mu = [];
206
+ for (let k = 0; k < K; k++) {
207
+ if (clampPose && clampPose[k]) { mu.push(clampPose[k].slice()); continue; }
208
+ const vk = [], wk = [];
209
+ for (let i = 0; i < N; i++)
210
+ for (let ci = 0; ci < cands[i].length; ci++)
211
+ if (cands[i][ci][0] === k) { vk.push(votes[i][ci]); wk.push(B[i][ci]); }
212
+ mu.push(vk.length ? densityPeaks(vk, wk, 1)[0] : [0, 0, 0, 0]);
213
+ }
214
+ let Sig = mu.map(() => [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]);
215
+
216
+ for (let it = 0; it < iters; it++) {
217
+ // pose fusion
218
+ const Lam = [], eta = [];
219
+ for (let k = 0; k < K; k++) { Lam.push(P0inv.map(r => r.slice())); eta.push([0,0,0,0]); }
220
+ for (let i = 0; i < N; i++) {
221
+ for (let ci = 0; ci < cands[i].length; ci++) {
222
+ const k = cands[i][ci][0];
223
+ const v = votes[i][ci].slice();
224
+ v[2] = mu[k][2] + wrapPi(v[2] - mu[k][2]);
225
+ const w = B[i][ci];
226
+ Lam[k] = matAdd(Lam[k], Vinv, w);
227
+ const Vv = mat4Vec(Vinv, v);
228
+ for (let q = 0; q < 4; q++) eta[k][q] += w * Vv[q];
229
+ }
230
+ }
231
+ Sig = Lam.map(mat4Inv);
232
+ for (let k = 0; k < K; k++) {
233
+ if (clampPose && clampPose[k]) {
234
+ mu[k] = clampPose[k].slice();
235
+ Sig[k] = [[1e-6,0,0,0],[0,1e-6,0,0],[0,0,1e-6,0],[0,0,0,1e-6]];
236
+ } else {
237
+ mu[k] = mat4Vec(Sig[k], eta[k]);
238
+ mu[k][2] = wrapPi(mu[k][2]);
239
+ }
240
+ }
241
+
242
+ // assignment update with cavity
243
+ const newB = [];
244
+ for (let i = 0; i < N; i++) {
245
+ const nc = cands[i].length;
246
+ const ll = new Array(nc + 1);
247
+ for (let ci = 0; ci < nc; ci++) {
248
+ const k = cands[i][ci][0];
249
+ const v = votes[i][ci].slice();
250
+ v[2] = mu[k][2] + wrapPi(v[2] - mu[k][2]);
251
+ let mC, Sk;
252
+ if (cavity && !(clampPose && clampPose[k])) {
253
+ const Lc = matAdd(Lam[k], Vinv, -B[i][ci]);
254
+ const Vv = mat4Vec(Vinv, v);
255
+ const ecav = [0,1,2,3].map(q => eta[k][q] - B[i][ci] * Vv[q]);
256
+ const Sc = mat4Inv(Lc);
257
+ mC = mat4Vec(Sc, ecav);
258
+ Sk = Sc;
259
+ } else {
260
+ mC = mu[k]; Sk = Sig[k];
261
+ }
262
+ const r = [v[0] - mC[0], v[1] - mC[1], wrapPi(v[2] - mC[2]), v[3] - mC[3]];
263
+ const Cov = matAdd(Sk, Vmat, 1);
264
+ const Ci = mat4Inv(Cov);
265
+ const Cr = mat4Vec(Ci, r);
266
+ const quad = r[0]*Cr[0] + r[1]*Cr[1] + r[2]*Cr[2] + r[3]*Cr[3];
267
+ ll[ci] = base[i][ci] - 0.5 * quad - 0.5 * Math.log(mat4Det(Cov));
268
+ }
269
+ ll[nc] = outLL;
270
+ const mx = Math.max(...ll);
271
+ let e = ll.map(x => Math.exp(x - mx));
272
+ const s = e.reduce((a, x) => a + x, 0);
273
+ e = e.map(x => x / s);
274
+ newB.push(e.map((x, q) => damping * x + (1 - damping) * B[i][q]));
275
+ }
276
+ B = newB;
277
+ if (onIter) onIter(it, marginals(), mu, Sig);
278
+ }
279
+
280
+ function marginals() {
281
+ const marg = [];
282
+ for (let i = 0; i < N; i++) {
283
+ if (cands[i].length === 0) {
284
+ // no evidence: assignment belief reverts to the prior (uniform),
285
+ // not to a confident "outlier" — this is what permanence means.
286
+ marg.push(new Array(K + 1).fill(1 / (K + 1)));
287
+ continue;
288
+ }
289
+ const m = new Array(K + 1).fill(0);
290
+ for (let ci = 0; ci < cands[i].length; ci++) m[cands[i][ci][0]] += B[i][ci];
291
+ m[K] = B[i][cands[i].length];
292
+ marg.push(m);
293
+ }
294
+ return marg;
295
+ }
296
+
297
+ return { marg: marginals(), mu, Sig, cands, votes, B };
298
+ }
299
+
300
+ /* Numpy-matching reference render (verification + full-res compositor).
301
+ pre[c] += color_c * env * carrier ; out = sigmoid(2 * pre).
302
+ Returns {pre: Float32Array(3*H*H)} pre-sigmoid field. */
303
+ function renderPre(atoms, H, pre) {
304
+ pre = pre || new Float32Array(3 * H * H);
305
+ const lim = 3.2; // envelope support cut, in sigmas
306
+ for (const a of atoms) {
307
+ const [ax, ay, th, su, sv, f, ph, r, g, b] = a;
308
+ const ct = Math.cos(th), st = Math.sin(th);
309
+ const rad = lim * Math.max(su, sv);
310
+ // pixel bbox: x in [-1,1] maps to col (H-1)*(x+1)/2
311
+ const x0 = Math.max(0, Math.floor((ax - rad + 1) / 2 * (H - 1)));
312
+ const x1 = Math.min(H - 1, Math.ceil((ax + rad + 1) / 2 * (H - 1)));
313
+ const y0 = Math.max(0, Math.floor((ay - rad + 1) / 2 * (H - 1)));
314
+ const y1 = Math.min(H - 1, Math.ceil((ay + rad + 1) / 2 * (H - 1)));
315
+ for (let py = y0; py <= y1; py++) {
316
+ const Y = -1 + 2 * py / (H - 1);
317
+ const dy = Y - ay;
318
+ for (let px = x0; px <= x1; px++) {
319
+ const X = -1 + 2 * px / (H - 1);
320
+ const dx = X - ax;
321
+ const u = ct * dx + st * dy;
322
+ const v = -st * dx + ct * dy;
323
+ const eArg = 0.5 * ((u / su) * (u / su) + (v / sv) * (v / sv));
324
+ if (eArg > lim * lim / 2 * 1.6) continue;
325
+ const env = Math.exp(-eArg);
326
+ const car = Math.cos(TAU * f * u + ph);
327
+ const ec = env * car;
328
+ const idx = py * H + px;
329
+ pre[idx] += r * ec;
330
+ pre[H * H + idx] += g * ec;
331
+ pre[2 * H * H + idx] += b * ec;
332
+ }
333
+ }
334
+ }
335
+ return pre;
336
+ }
337
+
338
+ function sigmoidField(pre, H, out) {
339
+ out = out || new Uint8ClampedArray(4 * H * H);
340
+ const n = H * H;
341
+ for (let i = 0; i < n; i++) {
342
+ out[4 * i] = 255 / (1 + Math.exp(-2 * pre[i]));
343
+ out[4 * i + 1] = 255 / (1 + Math.exp(-2 * pre[n + i]));
344
+ out[4 * i + 2] = 255 / (1 + Math.exp(-2 * pre[2 * n + i]));
345
+ out[4 * i + 3] = 255;
346
+ }
347
+ return out;
348
+ }
349
+
350
+ /* Ownership field: P(k|pixel) through the atoms' actual envelopes,
351
+ energy-weighted. Port of bet6_open.ownership_field. */
352
+ function ownershipField(obs, marg, K, H) {
353
+ const O = [];
354
+ for (let k = 0; k <= K; k++) O.push(new Float32Array(H * H));
355
+ const lim = 3.2;
356
+ for (let i = 0; i < obs.length; i++) {
357
+ const a = obs[i];
358
+ const energy = Math.hypot(a[7], a[8], a[9]);
359
+ const ct = Math.cos(a[2]), st = Math.sin(a[2]);
360
+ const rad = lim * Math.max(a[3], a[4]);
361
+ const x0 = Math.max(0, Math.floor((a[0] - rad + 1) / 2 * (H - 1)));
362
+ const x1 = Math.min(H - 1, Math.ceil((a[0] + rad + 1) / 2 * (H - 1)));
363
+ const y0 = Math.max(0, Math.floor((a[1] - rad + 1) / 2 * (H - 1)));
364
+ const y1 = Math.min(H - 1, Math.ceil((a[1] + rad + 1) / 2 * (H - 1)));
365
+ for (let py = y0; py <= y1; py++) {
366
+ const Y = -1 + 2 * py / (H - 1);
367
+ const dy = Y - a[1];
368
+ for (let px = x0; px <= x1; px++) {
369
+ const X = -1 + 2 * px / (H - 1);
370
+ const dx = X - a[0];
371
+ const u = ct * dx + st * dy;
372
+ const v = -st * dx + ct * dy;
373
+ const env = Math.exp(-0.5 * ((u / a[3]) ** 2 + (v / a[4]) ** 2));
374
+ const idx = py * H + px;
375
+ for (let k = 0; k <= K; k++) O[k][idx] += marg[i][k] * energy * env;
376
+ }
377
+ }
378
+ }
379
+ const P = O.map(() => new Float32Array(H * H));
380
+ const ent = new Float32Array(H * H);
381
+ const support = new Uint8Array(H * H);
382
+ for (let idx = 0; idx < H * H; idx++) {
383
+ let tot = 0;
384
+ for (let k = 0; k <= K; k++) tot += O[k][idx];
385
+ support[idx] = tot > 0.05 ? 1 : 0;
386
+ let e = 0;
387
+ for (let k = 0; k <= K; k++) {
388
+ const p = O[k][idx] / (tot + 1e-6);
389
+ P[k][idx] = p;
390
+ e -= p * Math.log2(p + 1e-12);
391
+ }
392
+ ent[idx] = e;
393
+ }
394
+ return { P, ent, support };
395
+ }
396
+
397
+ /* Assignment entropy per atom, in bits. */
398
+ function atomEntropy(marg) {
399
+ return marg.map(m => {
400
+ let e = 0;
401
+ for (const p of m) e -= p * Math.log2(p + 1e-12);
402
+ return e;
403
+ });
404
+ }
405
+
406
+ if (typeof module !== "undefined") {
407
+ module.exports = {
408
+ wrapPi, transformAtoms, signature, poseVotes2pi, densityPeaks,
409
+ bpBind, renderPre, sigmoidField, ownershipField, atomEntropy,
410
+ mat4Inv, mat4Det,
411
+ };
412
+ }
studio/studio.html ADDED
@@ -0,0 +1,1495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Slapstack Studio — spawn objects, move them, in Gabor space</title>
7
+ <style>
8
+ :root {
9
+ --chassis:#1d222e; --panel:#262c3b; --panel-edge:#333b4f; --well:#10131b;
10
+ --ink:#e3e7f0; --muted:#8d96ab; --phosphor:#ffb454; --outlier:#78808f;
11
+ --entropy:#c86bff; --go:#7fd98a; --bad:#ff7a7a;
12
+ --mono: ui-monospace,"SF Mono","Cascadia Mono",Menlo,Consolas,monospace;
13
+ --cond:"Bahnschrift","Arial Narrow","Roboto Condensed",sans-serif;
14
+ }
15
+ *{box-sizing:border-box;margin:0}
16
+ html,body{height:100%}
17
+ body{background:var(--chassis);color:var(--ink);font-family:var(--mono);
18
+ font-size:13px;display:flex;flex-direction:column;overflow:hidden}
19
+ @media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
20
+ header{display:flex;align-items:baseline;gap:14px;padding:10px 16px 9px;
21
+ border-bottom:1px solid var(--panel-edge);flex-wrap:wrap}
22
+ header h1{font-family:var(--cond);font-size:20px;letter-spacing:2.5px;font-weight:700}
23
+ header h1 em{color:var(--phosphor);font-style:normal}
24
+ header .claim{color:var(--muted);font-size:11.5px}
25
+ #state-lamp{margin-left:auto;font-size:11px;letter-spacing:1.5px;padding:3px 10px;
26
+ border-radius:3px;border:1px solid var(--panel-edge);color:var(--muted)}
27
+ #state-lamp.soup{color:var(--outlier);border-color:var(--outlier)}
28
+ #state-lamp.binding{color:var(--entropy);border-color:var(--entropy)}
29
+ #state-lamp.bound{color:var(--go);border-color:var(--go)}
30
+ main{flex:1;display:flex;gap:12px;padding:12px 16px;min-height:0}
31
+ #stage-wrap{flex:1;display:flex;align-items:center;justify-content:center;min-width:0}
32
+ #stage{position:relative;background:var(--well);border:1px solid var(--panel-edge);
33
+ border-radius:6px;box-shadow:inset 0 0 60px rgba(0,0,0,.55)}
34
+ #stage canvas{position:absolute;inset:0;width:100%;height:100%;border-radius:6px}
35
+ #overlay-canvas{cursor:grab}
36
+ #overlay-canvas.dragging{cursor:grabbing}
37
+ #stage .hint{position:absolute;bottom:7px;left:0;right:0;text-align:center;
38
+ color:var(--muted);font-size:10.5px;pointer-events:none;text-shadow:0 1px 3px #000}
39
+ aside{width:330px;flex-shrink:0;display:flex;flex-direction:column;gap:9px;
40
+ overflow-y:auto;padding-right:2px}
41
+ .card{background:var(--panel);border:1px solid var(--panel-edge);border-radius:6px;
42
+ padding:10px 11px}
43
+ .card h2{font-family:var(--cond);font-size:12px;letter-spacing:2px;font-weight:700;
44
+ color:var(--muted);margin-bottom:8px}
45
+ button{font-family:var(--mono);font-size:12px;background:var(--well);color:var(--ink);
46
+ border:1px solid var(--panel-edge);border-radius:4px;padding:6px 11px;cursor:pointer}
47
+ button:hover{border-color:var(--phosphor);color:var(--phosphor)}
48
+ button:focus-visible{outline:2px solid var(--phosphor);outline-offset:1px}
49
+ button:disabled{opacity:.35;cursor:default}
50
+ button:disabled:hover{border-color:var(--panel-edge);color:var(--ink)}
51
+ button.primary{border-color:var(--phosphor);color:var(--phosphor)}
52
+ .row{display:flex;gap:7px;align-items:center}
53
+ .row+.row{margin-top:7px}
54
+ .readout{color:var(--phosphor)} .dim{color:var(--muted)} .spacer{flex:1}
55
+ #bind-meter{height:4px;background:var(--well);border-radius:2px;overflow:hidden;margin-top:8px}
56
+ #bind-meter>div{height:100%;width:0%;background:var(--entropy)}
57
+ #metrics{display:grid;grid-template-columns:1fr 1fr;gap:3px 10px;margin-top:8px;font-size:11.5px}
58
+ .layer{border-top:1px solid var(--panel-edge);padding:7px 0;border-radius:3px}
59
+ .layer:first-of-type{border-top:0}
60
+ .layer.sel{outline:1px solid var(--phosphor);outline-offset:2px}
61
+ .layer .name{display:flex;align-items:center;gap:7px;cursor:pointer}
62
+ .swatch{width:10px;height:10px;border-radius:2px;flex-shrink:0}
63
+ .layer .pose{color:var(--muted);font-size:10.5px;margin:4px 0 5px;min-height:13px}
64
+ .belief-bar{flex:1;height:5px;background:var(--well);border-radius:3px;overflow:hidden;max-width:80px}
65
+ .belief-bar>div{height:100%;background:var(--go)}
66
+ .layer .ctl{display:flex;gap:4px;flex-wrap:wrap;margin-top:5px}
67
+ .layer .ctl button{padding:3px 7px;font-size:10.5px}
68
+ #library{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
69
+ .chip{display:flex;align-items:center;gap:5px;background:var(--well);
70
+ border:1px solid var(--panel-edge);border-radius:12px;padding:3px 9px;font-size:11px}
71
+ .chip button{border:0;padding:0 2px;font-size:12px;background:none}
72
+ label.tog{display:flex;gap:8px;align-items:center;cursor:pointer;padding:2px 0;font-size:12px}
73
+ label.tog input{accent-color:var(--phosphor)}
74
+ input[type=range]{flex:1;accent-color:var(--phosphor)}
75
+ input[type=text],textarea{font-family:var(--mono);font-size:12px;background:var(--well);
76
+ color:var(--ink);border:1px solid var(--panel-edge);border-radius:4px;padding:6px 8px;width:100%}
77
+ input[type=text]:focus-visible,textarea:focus-visible{outline:2px solid var(--phosphor);border-color:transparent}
78
+ #spawn-status,#prompt-log{color:var(--muted);font-size:10.5px;margin-top:6px;
79
+ min-height:13px;white-space:pre-wrap}
80
+ #ledger p{font-size:10.5px;line-height:1.5;color:var(--muted);margin-bottom:6px}
81
+ #ledger b{color:var(--go);font-weight:400}
82
+ #ledger .no{color:var(--entropy)}
83
+ details summary{cursor:pointer;font-family:var(--cond);font-size:12px;letter-spacing:2px;color:var(--muted)}
84
+ details[open] summary{margin-bottom:7px}
85
+ input[type=file]{font-size:10.5px;color:var(--muted);max-width:170px}
86
+ @media (max-width:900px){main{flex-direction:column;overflow-y:auto}body{overflow:auto}aside{width:100%}}
87
+ </style>
88
+ </head>
89
+ <body>
90
+
91
+ <header>
92
+ <h1>SLAPSTACK <em>STUDIO</em></h1>
93
+ <span class="claim">spawn layers with AI &middot; scramble &middot; bind &middot; grab &middot; stack &middot; every entity is a posterior</span>
94
+ <span id="state-lamp" class="soup">ATOM SOUP</span>
95
+ </header>
96
+
97
+ <main>
98
+ <div id="stage-wrap">
99
+ <div id="stage">
100
+ <canvas id="scene-canvas"></canvas>
101
+ <canvas id="overlay-canvas"></canvas>
102
+ <div class="hint">drag = pose clamp &middot; wheel rotate &middot; shift+wheel scale &middot; shift+click = semantic hint for selected layer</div>
103
+ </div>
104
+ </div>
105
+
106
+ <aside>
107
+ <div class="card">
108
+ <h2>SCENE</h2>
109
+ <div class="row">
110
+ <button id="btn-scramble">Scramble</button>
111
+ <button id="btn-bind" class="primary">Bind</button>
112
+ <span class="spacer"></span>
113
+ <span class="dim">noise</span>
114
+ <input type="range" id="noise" min="0" max="10" value="1" style="width:60px" aria-label="observation noise">
115
+ </div>
116
+ <div class="row">
117
+ <span class="dim">composite</span>
118
+ <label class="tog"><input type="radio" name="comp" value="field" checked> field</label>
119
+ <label class="tog"><input type="radio" name="comp" value="painter"> painter (depth)</label>
120
+ <span class="spacer"></span>
121
+ <label class="tog"><input type="checkbox" id="auto-occ" checked> depth occlusion</label>
122
+ </div>
123
+ <div id="bind-meter"><div></div></div>
124
+ <div id="metrics">
125
+ <span class="dim">binding vs truth</span><span class="readout" id="m-acc">—</span>
126
+ <span class="dim">mean entropy</span><span class="readout" id="m-ent">—</span>
127
+ <span class="dim">BP iterations</span><span class="readout" id="m-it">—</span>
128
+ <span class="dim">atoms in scene</span><span class="readout" id="m-n">—</span>
129
+ </div>
130
+ </div>
131
+
132
+ <div class="card">
133
+ <h2>LAYERS <span class="dim" style="letter-spacing:0">(front is highest)</span></h2>
134
+ <div id="layer-list"></div>
135
+ <div class="dim" style="font-size:10.5px;margin-top:6px">library — place to add to the scene:</div>
136
+ <div id="library"></div>
137
+ </div>
138
+
139
+ <div class="card">
140
+ <h2>SPAWN A LAYER</h2>
141
+ <div class="row">
142
+ <input type="text" id="spawn-prompt" placeholder='prompt, e.g. "a red rocket, flat colors"'>
143
+ </div>
144
+ <div class="row">
145
+ <input type="text" id="spawn-token" placeholder="matte token (e.g. rocket)" style="width:55%">
146
+ <button id="btn-spawn-text">Distill (GPU)</button>
147
+ </div>
148
+ <div class="row">
149
+ <input type="file" id="spawn-file" accept="image/*">
150
+ <button id="btn-spawn-img">Fit (CPU)</button>
151
+ </div>
152
+ <div class="row">
153
+ <input type="text" id="spawn-json" placeholder="or paste layer JSON">
154
+ <button id="btn-spawn-json">Add</button>
155
+ </div>
156
+ <div id="spawn-status">Distill runs Stable Diffusion score distillation with a cross-attention matte (GPU Space only, untested — first run is a smoke test). Fit trains atoms to your image on CPU (~1 min; objects on plain mid-gray come out compact).</div>
157
+ </div>
158
+
159
+ <div class="card">
160
+ <h2>PROMPT AS INTERVENTION</h2>
161
+ <div class="row">
162
+ <input type="text" id="prompt" placeholder='e.g. "move the boat left, put the star behind the tractor"'>
163
+ <button id="btn-prompt">Do</button>
164
+ </div>
165
+ <div id="prompt-log">Commands become pose clamps and depth moves; BP re-settles. Free-form uses Claude when this runs inside claude.ai; a local grammar otherwise.</div>
166
+ </div>
167
+
168
+ <div class="card">
169
+ <h2>OVERLAYS</h2>
170
+ <label class="tog"><input type="checkbox" id="ov-atoms" checked> atoms, colored by belief</label>
171
+ <label class="tog"><input type="checkbox" id="ov-own"> ownership tint</label>
172
+ <label class="tog"><input type="checkbox" id="ov-ent"> entropy border glow</label>
173
+ <label class="tog"><input type="checkbox" id="ov-pose" checked> pose belief (crosshair + covariance)</label>
174
+ <label class="tog"><input type="checkbox" id="ov-hints" checked> semantic hints</label>
175
+ </div>
176
+
177
+ <div class="card" id="ledger">
178
+ <details>
179
+ <summary>HONEST LEDGER</summary>
180
+ <p><b>Verified in this build:</b> the BP core matches the SlapstackBet6 Python (transform 2e-16, render MSE 8.6e-8, identical binding); drag = pose clamp + re-settle; identity persists through occlusion; depth occlusion removes evidence honestly (marginals revert toward the prior, coverage releases when moved away); the semantic likelihood factor resolves the twins case (two identical stars: 0.05 acc on geometry alone, 0.99 with location priors) and a mildly wrong prior is overridden by evidence; image&rarr;layer fitting runs end to end on CPU.</p>
181
+ <p><span class="no">Not verified:</span> text&rarr;layer (SDS) and cross-attention matte are line-for-line adaptations of the verified Bet-5 GPU loop but have NOT been executed on a GPU — first run is a smoke test; SD attention maps flicker across seeds, which is why they enter BP as a prior with a weight, never as truth. Real-photo scene&rarr;atoms remains open. Painter compositing and the alpha matte are presentation choices, not the additive field model.</p>
182
+ </details>
183
+ </div>
184
+ </aside>
185
+ </main>
186
+
187
+ <script>
188
+ /* Slapstack Playroom core — faithful JS port of SlapstackBet6 math.
189
+ FIELDS: [x, y, theta, su, sv, f, phase, r, g, b]
190
+ Verified against bet6_open.py / bet6_bp_binding.py / bet6_multimodal.py
191
+ by tests_node.js before being embedded in the playroom. */
192
+ "use strict";
193
+
194
+ const TAU = Math.PI * 2;
195
+
196
+ function wrapPi(d) {
197
+ return ((d + Math.PI) % TAU + TAU) % TAU - Math.PI;
198
+ }
199
+
200
+ function rotApply(rho, x, y) {
201
+ const c = Math.cos(rho), s = Math.sin(rho);
202
+ return [c * x - s * y, s * x + c * y];
203
+ }
204
+
205
+ /* Exact Sim(2) action on atom parameters (the Bet 5 algebra):
206
+ xy -> s R xy + t, theta -> theta + rho, sigma -> s sigma, f -> f/s.
207
+ Envelope-relative phase and color are INVARIANT. */
208
+ function transformAtoms(atoms, xi) {
209
+ const [tx, ty, rho, lam] = xi;
210
+ const s = Math.exp(lam);
211
+ const out = new Array(atoms.length);
212
+ for (let i = 0; i < atoms.length; i++) {
213
+ const a = atoms[i];
214
+ const [rx, ry] = rotApply(rho, a[0], a[1]);
215
+ out[i] = [
216
+ s * rx + tx, s * ry + ty,
217
+ a[2] + rho,
218
+ a[3] * s, a[4] * s,
219
+ a[5] / s,
220
+ a[6],
221
+ a[7], a[8], a[9],
222
+ ];
223
+ }
224
+ return out;
225
+ }
226
+
227
+ /* Sim(2)-invariant intrinsic signature: identity lives here. */
228
+ function signature(atoms) {
229
+ return atoms.map(a => [
230
+ Math.log(a[3] * a[5]),
231
+ Math.log(a[3] / a[4]),
232
+ Math.cos(a[6]), Math.sin(a[6]),
233
+ a[7], a[8], a[9],
234
+ ]);
235
+ }
236
+
237
+ /* Two pose-vote hypotheses per correspondence (pi-ambiguity fix):
238
+ H0: rho = d_theta, phi_obs == phi_tmpl
239
+ H1: rho = d_theta + pi, phi_obs == -phi_tmpl */
240
+ function poseVotes2pi(obs, tmpl, sigPhase = 0.35) {
241
+ const s = Math.pow((obs[3] / tmpl[3]) * (obs[4] / tmpl[4]) * (tmpl[5] / obs[5]), 1 / 3);
242
+ const dTheta = obs[2] - tmpl[2];
243
+ const out = [];
244
+ for (let H = 0; H < 2; H++) {
245
+ const rho = wrapPi(dTheta + H * Math.PI);
246
+ const phiExp = H === 0 ? tmpl[6] : -tmpl[6];
247
+ const dphi = wrapPi(obs[6] - phiExp);
248
+ const pc = -0.5 * dphi * dphi / (sigPhase * sigPhase);
249
+ const [rx, ry] = rotApply(rho, tmpl[0], tmpl[1]);
250
+ out.push([[obs[0] - s * rx, obs[1] - s * ry, rho, Math.log(s)], pc]);
251
+ }
252
+ return out;
253
+ }
254
+
255
+ /* ------- small dense linear algebra on 4x4 (row-major flat arrays) ------- */
256
+ function mat4Inv(m) {
257
+ // Gauss-Jordan, fine for well-conditioned SPD 4x4s here.
258
+ const a = m.map(r => r.slice());
259
+ const inv = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]];
260
+ for (let col = 0; col < 4; col++) {
261
+ let piv = col;
262
+ for (let r = col + 1; r < 4; r++)
263
+ if (Math.abs(a[r][col]) > Math.abs(a[piv][col])) piv = r;
264
+ [a[col], a[piv]] = [a[piv], a[col]];
265
+ [inv[col], inv[piv]] = [inv[piv], inv[col]];
266
+ const d = a[col][col];
267
+ for (let j = 0; j < 4; j++) { a[col][j] /= d; inv[col][j] /= d; }
268
+ for (let r = 0; r < 4; r++) {
269
+ if (r === col) continue;
270
+ const f = a[r][col];
271
+ for (let j = 0; j < 4; j++) { a[r][j] -= f * a[col][j]; inv[r][j] -= f * inv[col][j]; }
272
+ }
273
+ }
274
+ return inv;
275
+ }
276
+ function mat4Det(m) {
277
+ const a = m.map(r => r.slice());
278
+ let det = 1;
279
+ for (let col = 0; col < 4; col++) {
280
+ let piv = col;
281
+ for (let r = col + 1; r < 4; r++)
282
+ if (Math.abs(a[r][col]) > Math.abs(a[piv][col])) piv = r;
283
+ if (piv !== col) { [a[col], a[piv]] = [a[piv], a[col]]; det = -det; }
284
+ det *= a[col][col];
285
+ if (a[col][col] === 0) return 0;
286
+ for (let r = col + 1; r < 4; r++) {
287
+ const f = a[r][col] / a[col][col];
288
+ for (let j = col; j < 4; j++) a[r][j] -= f * a[col][j];
289
+ }
290
+ }
291
+ return det;
292
+ }
293
+ function mat4Vec(m, v) {
294
+ return [0,1,2,3].map(i => m[i][0]*v[0]+m[i][1]*v[1]+m[i][2]*v[2]+m[i][3]*v[3]);
295
+ }
296
+ function matAdd(A, B, wB = 1) {
297
+ return A.map((row, i) => row.map((x, j) => x + wB * B[i][j]));
298
+ }
299
+
300
+ /* Greedy mode-seeking init (angle-aware), port of _density_peaks. */
301
+ function densityPeaks(votes, weights, M, radius = 0.45) {
302
+ const scale = [0.15, 0.15, 0.30, 0.20];
303
+ const n = votes.length;
304
+ const dens = new Float64Array(n);
305
+ for (let i = 0; i < n; i++) {
306
+ let acc = 0;
307
+ for (let k = 0; k < n; k++) {
308
+ const d0 = (votes[k][0] - votes[i][0]) / scale[0];
309
+ const d1 = (votes[k][1] - votes[i][1]) / scale[1];
310
+ const d2 = wrapPi(votes[k][2] - votes[i][2]) / scale[2];
311
+ const d3 = (votes[k][3] - votes[i][3]) / scale[3];
312
+ acc += weights[k] * Math.exp(-0.5 * (d0*d0 + d1*d1 + d2*d2 + d3*d3));
313
+ }
314
+ dens[i] = acc;
315
+ }
316
+ const peaks = [];
317
+ const alive = new Uint8Array(n).fill(1);
318
+ for (let m = 0; m < M; m++) {
319
+ let best = -1, bestD = -Infinity;
320
+ for (let i = 0; i < n; i++)
321
+ if (alive[i] && dens[i] > bestD) { bestD = dens[i]; best = i; }
322
+ if (best < 0) break;
323
+ peaks.push(votes[best].slice());
324
+ for (let i = 0; i < n; i++) {
325
+ const dxy = Math.hypot(votes[i][0] - votes[best][0], votes[i][1] - votes[best][1]);
326
+ const dr = Math.abs(wrapPi(votes[i][2] - votes[best][2]));
327
+ if (dxy + dr <= radius) alive[i] = 0;
328
+ }
329
+ }
330
+ return peaks;
331
+ }
332
+
333
+ /* Loopy BP binding of obs atoms to K templates at unknown poses.
334
+ Port of bet6_open.bp_bind: candidates carry both pi-hypotheses,
335
+ cavity messages, damping, branch-aligned rotation fusion.
336
+ Options: clampPose — array of length K; if clampPose[k] is a pose xi,
337
+ object k's pose is held fixed (conditioning-as-intervention) and only
338
+ the assignment beliefs re-equilibrate around it. */
339
+ function bpBind(templates, obs, opts = {}) {
340
+ const iters = opts.iters ?? 40;
341
+ const damping = opts.damping ?? 0.5;
342
+ const cavity = opts.cavity ?? true;
343
+ const sigVar = opts.sigVar ?? 0.08;
344
+ const outLL = opts.outLL ?? -14.0;
345
+ const clampPose = opts.clampPose ?? null;
346
+ const hiddenMask = opts.hiddenMask ?? null; // per-atom: true = no evidence
347
+ const onIter = opts.onIter ?? null;
348
+
349
+ const K = templates.length;
350
+ const sigT = templates.map(signature);
351
+ const sigO = signature(obs);
352
+ const N = obs.length;
353
+
354
+ const Vdiag = [0.03 * 0.03, 0.03 * 0.03, 0.05 * 0.05, 0.05 * 0.05];
355
+ const Vinv = [[1/Vdiag[0],0,0,0],[0,1/Vdiag[1],0,0],[0,0,1/Vdiag[2],0],[0,0,0,1/Vdiag[3]]];
356
+ const Vmat = [[Vdiag[0],0,0,0],[0,Vdiag[1],0,0],[0,0,Vdiag[2],0],[0,0,0,Vdiag[3]]];
357
+ const P0inv = [[1e-2,0,0,0],[0,1e-2,0,0],[0,0,1e-2,0],[0,0,0,1e-2]];
358
+
359
+ // ---- candidate generation: 3 signature-nearest per template, 2 hypotheses
360
+ const cands = [], votes = [], base = [];
361
+ for (let i = 0; i < N; i++) {
362
+ const c = [], v = [], b = [];
363
+ if (!(hiddenMask && hiddenMask[i])) {
364
+ for (let k = 0; k < K; k++) {
365
+ const d2 = sigT[k].map(st => {
366
+ let acc = 0;
367
+ for (let q = 0; q < 7; q++) { const d = st[q] - sigO[i][q]; acc += d * d; }
368
+ return acc;
369
+ });
370
+ const order = d2.map((d, j) => [d, j]).sort((p, q) => p[0] - q[0]).slice(0, 3);
371
+ for (const [dj, j] of order) {
372
+ for (const [xi, pc] of poseVotes2pi(obs[i], templates[k][j])) {
373
+ c.push([k, j]); v.push(xi); b.push(-0.5 * dj / sigVar + pc);
374
+ }
375
+ }
376
+ }
377
+ }
378
+ cands.push(c); votes.push(v); base.push(b);
379
+ }
380
+
381
+ // ---- beliefs seeded from the identity+phase channel
382
+ let B = [];
383
+ for (let i = 0; i < N; i++) {
384
+ const ll = base[i].concat([outLL]);
385
+ const mx = Math.max(...ll);
386
+ let e = ll.map(x => Math.exp(x - mx));
387
+ const s = e.reduce((a, x) => a + x, 0);
388
+ B.push(e.map(x => x / s));
389
+ }
390
+
391
+ // ---- pose init: density peak of each object's votes (or the clamp)
392
+ let mu = [];
393
+ for (let k = 0; k < K; k++) {
394
+ if (clampPose && clampPose[k]) { mu.push(clampPose[k].slice()); continue; }
395
+ const vk = [], wk = [];
396
+ for (let i = 0; i < N; i++)
397
+ for (let ci = 0; ci < cands[i].length; ci++)
398
+ if (cands[i][ci][0] === k) { vk.push(votes[i][ci]); wk.push(B[i][ci]); }
399
+ mu.push(vk.length ? densityPeaks(vk, wk, 1)[0] : [0, 0, 0, 0]);
400
+ }
401
+ let Sig = mu.map(() => [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]);
402
+
403
+ for (let it = 0; it < iters; it++) {
404
+ // pose fusion
405
+ const Lam = [], eta = [];
406
+ for (let k = 0; k < K; k++) { Lam.push(P0inv.map(r => r.slice())); eta.push([0,0,0,0]); }
407
+ for (let i = 0; i < N; i++) {
408
+ for (let ci = 0; ci < cands[i].length; ci++) {
409
+ const k = cands[i][ci][0];
410
+ const v = votes[i][ci].slice();
411
+ v[2] = mu[k][2] + wrapPi(v[2] - mu[k][2]);
412
+ const w = B[i][ci];
413
+ Lam[k] = matAdd(Lam[k], Vinv, w);
414
+ const Vv = mat4Vec(Vinv, v);
415
+ for (let q = 0; q < 4; q++) eta[k][q] += w * Vv[q];
416
+ }
417
+ }
418
+ Sig = Lam.map(mat4Inv);
419
+ for (let k = 0; k < K; k++) {
420
+ if (clampPose && clampPose[k]) {
421
+ mu[k] = clampPose[k].slice();
422
+ Sig[k] = [[1e-6,0,0,0],[0,1e-6,0,0],[0,0,1e-6,0],[0,0,0,1e-6]];
423
+ } else {
424
+ mu[k] = mat4Vec(Sig[k], eta[k]);
425
+ mu[k][2] = wrapPi(mu[k][2]);
426
+ }
427
+ }
428
+
429
+ // assignment update with cavity
430
+ const newB = [];
431
+ for (let i = 0; i < N; i++) {
432
+ const nc = cands[i].length;
433
+ const ll = new Array(nc + 1);
434
+ for (let ci = 0; ci < nc; ci++) {
435
+ const k = cands[i][ci][0];
436
+ const v = votes[i][ci].slice();
437
+ v[2] = mu[k][2] + wrapPi(v[2] - mu[k][2]);
438
+ let mC, Sk;
439
+ if (cavity && !(clampPose && clampPose[k])) {
440
+ const Lc = matAdd(Lam[k], Vinv, -B[i][ci]);
441
+ const Vv = mat4Vec(Vinv, v);
442
+ const ecav = [0,1,2,3].map(q => eta[k][q] - B[i][ci] * Vv[q]);
443
+ const Sc = mat4Inv(Lc);
444
+ mC = mat4Vec(Sc, ecav);
445
+ Sk = Sc;
446
+ } else {
447
+ mC = mu[k]; Sk = Sig[k];
448
+ }
449
+ const r = [v[0] - mC[0], v[1] - mC[1], wrapPi(v[2] - mC[2]), v[3] - mC[3]];
450
+ const Cov = matAdd(Sk, Vmat, 1);
451
+ const Ci = mat4Inv(Cov);
452
+ const Cr = mat4Vec(Ci, r);
453
+ const quad = r[0]*Cr[0] + r[1]*Cr[1] + r[2]*Cr[2] + r[3]*Cr[3];
454
+ ll[ci] = base[i][ci] - 0.5 * quad - 0.5 * Math.log(mat4Det(Cov));
455
+ }
456
+ ll[nc] = outLL;
457
+ const mx = Math.max(...ll);
458
+ let e = ll.map(x => Math.exp(x - mx));
459
+ const s = e.reduce((a, x) => a + x, 0);
460
+ e = e.map(x => x / s);
461
+ newB.push(e.map((x, q) => damping * x + (1 - damping) * B[i][q]));
462
+ }
463
+ B = newB;
464
+ if (onIter) onIter(it, marginals(), mu, Sig);
465
+ }
466
+
467
+ function marginals() {
468
+ const marg = [];
469
+ for (let i = 0; i < N; i++) {
470
+ if (cands[i].length === 0) {
471
+ // no evidence: assignment belief reverts to the prior (uniform),
472
+ // not to a confident "outlier" — this is what permanence means.
473
+ marg.push(new Array(K + 1).fill(1 / (K + 1)));
474
+ continue;
475
+ }
476
+ const m = new Array(K + 1).fill(0);
477
+ for (let ci = 0; ci < cands[i].length; ci++) m[cands[i][ci][0]] += B[i][ci];
478
+ m[K] = B[i][cands[i].length];
479
+ marg.push(m);
480
+ }
481
+ return marg;
482
+ }
483
+
484
+ return { marg: marginals(), mu, Sig, cands, votes, B };
485
+ }
486
+
487
+ /* Numpy-matching reference render (verification + full-res compositor).
488
+ pre[c] += color_c * env * carrier ; out = sigmoid(2 * pre).
489
+ Returns {pre: Float32Array(3*H*H)} pre-sigmoid field. */
490
+ function renderPre(atoms, H, pre) {
491
+ pre = pre || new Float32Array(3 * H * H);
492
+ const lim = 3.2; // envelope support cut, in sigmas
493
+ for (const a of atoms) {
494
+ const [ax, ay, th, su, sv, f, ph, r, g, b] = a;
495
+ const ct = Math.cos(th), st = Math.sin(th);
496
+ const rad = lim * Math.max(su, sv);
497
+ // pixel bbox: x in [-1,1] maps to col (H-1)*(x+1)/2
498
+ const x0 = Math.max(0, Math.floor((ax - rad + 1) / 2 * (H - 1)));
499
+ const x1 = Math.min(H - 1, Math.ceil((ax + rad + 1) / 2 * (H - 1)));
500
+ const y0 = Math.max(0, Math.floor((ay - rad + 1) / 2 * (H - 1)));
501
+ const y1 = Math.min(H - 1, Math.ceil((ay + rad + 1) / 2 * (H - 1)));
502
+ for (let py = y0; py <= y1; py++) {
503
+ const Y = -1 + 2 * py / (H - 1);
504
+ const dy = Y - ay;
505
+ for (let px = x0; px <= x1; px++) {
506
+ const X = -1 + 2 * px / (H - 1);
507
+ const dx = X - ax;
508
+ const u = ct * dx + st * dy;
509
+ const v = -st * dx + ct * dy;
510
+ const eArg = 0.5 * ((u / su) * (u / su) + (v / sv) * (v / sv));
511
+ if (eArg > lim * lim / 2 * 1.6) continue;
512
+ const env = Math.exp(-eArg);
513
+ const car = Math.cos(TAU * f * u + ph);
514
+ const ec = env * car;
515
+ const idx = py * H + px;
516
+ pre[idx] += r * ec;
517
+ pre[H * H + idx] += g * ec;
518
+ pre[2 * H * H + idx] += b * ec;
519
+ }
520
+ }
521
+ }
522
+ return pre;
523
+ }
524
+
525
+ function sigmoidField(pre, H, out) {
526
+ out = out || new Uint8ClampedArray(4 * H * H);
527
+ const n = H * H;
528
+ for (let i = 0; i < n; i++) {
529
+ out[4 * i] = 255 / (1 + Math.exp(-2 * pre[i]));
530
+ out[4 * i + 1] = 255 / (1 + Math.exp(-2 * pre[n + i]));
531
+ out[4 * i + 2] = 255 / (1 + Math.exp(-2 * pre[2 * n + i]));
532
+ out[4 * i + 3] = 255;
533
+ }
534
+ return out;
535
+ }
536
+
537
+ /* Ownership field: P(k|pixel) through the atoms' actual envelopes,
538
+ energy-weighted. Port of bet6_open.ownership_field. */
539
+ function ownershipField(obs, marg, K, H) {
540
+ const O = [];
541
+ for (let k = 0; k <= K; k++) O.push(new Float32Array(H * H));
542
+ const lim = 3.2;
543
+ for (let i = 0; i < obs.length; i++) {
544
+ const a = obs[i];
545
+ const energy = Math.hypot(a[7], a[8], a[9]);
546
+ const ct = Math.cos(a[2]), st = Math.sin(a[2]);
547
+ const rad = lim * Math.max(a[3], a[4]);
548
+ const x0 = Math.max(0, Math.floor((a[0] - rad + 1) / 2 * (H - 1)));
549
+ const x1 = Math.min(H - 1, Math.ceil((a[0] + rad + 1) / 2 * (H - 1)));
550
+ const y0 = Math.max(0, Math.floor((a[1] - rad + 1) / 2 * (H - 1)));
551
+ const y1 = Math.min(H - 1, Math.ceil((a[1] + rad + 1) / 2 * (H - 1)));
552
+ for (let py = y0; py <= y1; py++) {
553
+ const Y = -1 + 2 * py / (H - 1);
554
+ const dy = Y - a[1];
555
+ for (let px = x0; px <= x1; px++) {
556
+ const X = -1 + 2 * px / (H - 1);
557
+ const dx = X - a[0];
558
+ const u = ct * dx + st * dy;
559
+ const v = -st * dx + ct * dy;
560
+ const env = Math.exp(-0.5 * ((u / a[3]) ** 2 + (v / a[4]) ** 2));
561
+ const idx = py * H + px;
562
+ for (let k = 0; k <= K; k++) O[k][idx] += marg[i][k] * energy * env;
563
+ }
564
+ }
565
+ }
566
+ const P = O.map(() => new Float32Array(H * H));
567
+ const ent = new Float32Array(H * H);
568
+ const support = new Uint8Array(H * H);
569
+ for (let idx = 0; idx < H * H; idx++) {
570
+ let tot = 0;
571
+ for (let k = 0; k <= K; k++) tot += O[k][idx];
572
+ support[idx] = tot > 0.05 ? 1 : 0;
573
+ let e = 0;
574
+ for (let k = 0; k <= K; k++) {
575
+ const p = O[k][idx] / (tot + 1e-6);
576
+ P[k][idx] = p;
577
+ e -= p * Math.log2(p + 1e-12);
578
+ }
579
+ ent[idx] = e;
580
+ }
581
+ return { P, ent, support };
582
+ }
583
+
584
+ /* Assignment entropy per atom, in bits. */
585
+ function atomEntropy(marg) {
586
+ return marg.map(m => {
587
+ let e = 0;
588
+ for (const p of m) e -= p * Math.log2(p + 1e-12);
589
+ return e;
590
+ });
591
+ }
592
+
593
+
594
+ </script>
595
+ <script>
596
+ /* Slapstack Studio layer math — NEW mechanics on top of the verified core.
597
+ core.js is untouched; everything here is presentation-adjacent inference
598
+ plumbing, tested by tests_studio.js:
599
+ renderEnv per-layer envelope-energy buffer (for alpha + coverage)
600
+ coverageOf how much front-stack mass sits on each atom center
601
+ autoHidden coverage -> per-atom evidence mask (depth occlusion)
602
+ compositePainter back-to-front alpha compositing of layer buffers
603
+ composePose Sim(2) composition in vote coordinates [tx,ty,rho,lam]
604
+ */
605
+ "use strict";
606
+
607
+ const ALPHA_C = 2.0; // alpha = 1 - exp(-ALPHA_C * bounded envelope energy)
608
+ const COVER_THRESH = 0.55; // atom loses evidence when this covered
609
+
610
+ /* Opacity of a layer = where it actually PAINTS: per-pixel deviation of
611
+ the pre-sigmoid field from neutral. Cancelling atom pairs (high energy,
612
+ zero sum) are correctly transparent — envelope-based opacity is not used
613
+ because it saturates on exactly those invisible pairs.
614
+ a = 1 - exp(-ALPHA_C * max(0, |preR|+|preG|+|preB| - FLOOR)). */
615
+ const ALPHA_FLOOR = 0.08;
616
+ function alphaFromPre(pre, H, out) {
617
+ out = out || new Float32Array(H * H);
618
+ const n = H * H;
619
+ for (let i = 0; i < n; i++) {
620
+ const m = Math.abs(pre[i]) + Math.abs(pre[n + i]) + Math.abs(pre[2 * n + i]);
621
+ out[i] = m > ALPHA_FLOOR ? 1 - Math.exp(-ALPHA_C * (m - ALPHA_FLOOR)) : 0;
622
+ }
623
+ return out;
624
+ }
625
+
626
+ /* coverage of each atom center by a front stack of alpha buffers:
627
+ cov_i = 1 - prod_front (1 - a_m(x_i)). */
628
+ function coverageOf(atoms, frontAlphaBufs, H) {
629
+ const out = new Float32Array(atoms.length);
630
+ for (let i = 0; i < atoms.length; i++) {
631
+ const px = Math.max(0, Math.min(H - 1, Math.round((atoms[i][0] + 1) / 2 * (H - 1))));
632
+ const py = Math.max(0, Math.min(H - 1, Math.round((atoms[i][1] + 1) / 2 * (H - 1))));
633
+ let keep = 1;
634
+ for (const buf of frontAlphaBufs) keep *= 1 - buf[py * H + px];
635
+ out[i] = 1 - keep;
636
+ }
637
+ return out;
638
+ }
639
+
640
+ /* Per-atom evidence mask from the depth stack.
641
+ alphaBufs aligned with layers. An atom of layer k is evidence-free if the
642
+ strictly-in-front stack covers it beyond COVER_THRESH, or its layer is
643
+ user-hidden. */
644
+ function autoHidden(obs, layerOf, layers, alphaBufs, H) {
645
+ const mask = new Array(obs.length).fill(false);
646
+ const covFrac = layers.map(() => 0);
647
+ const counts = layers.map(() => 0);
648
+ for (let i = 0; i < obs.length; i++) {
649
+ const k = layerOf[i];
650
+ if (k < 0) continue; // clutter: always evidenced
651
+ counts[k]++;
652
+ if (layers[k].hidden) { mask[i] = true; covFrac[k]++; continue; }
653
+ const front = [];
654
+ for (let m = 0; m < layers.length; m++)
655
+ if (m !== k && !layers[m].hidden && layers[m].depth > layers[k].depth)
656
+ front.push(alphaBufs[m]);
657
+ if (!front.length) continue;
658
+ const cov = coverageOf([obs[i]], front, H)[0];
659
+ if (cov > COVER_THRESH) { mask[i] = true; covFrac[k]++; }
660
+ }
661
+ for (let k = 0; k < layers.length; k++)
662
+ covFrac[k] = counts[k] ? covFrac[k] / counts[k] : 0;
663
+ return { mask, covFrac };
664
+ }
665
+
666
+ /* Painter compositing: layers back-to-front by depth.
667
+ rgbBufs[k]: Float32Array(3*H*H) pre-sigmoid; alphaBufs[k]: from
668
+ alphaFromPre. Base is mid-gray (sigmoid(0)). */
669
+ function compositePainter(order, rgbBufs, alphaBufs, H, out) {
670
+ out = out || new Uint8ClampedArray(4 * H * H);
671
+ const n = H * H;
672
+ const acc = new Float32Array(3 * n);
673
+ for (let i = 0; i < n; i++) { acc[i] = 127.5; acc[n + i] = 127.5; acc[2 * n + i] = 127.5; }
674
+ for (const k of order) {
675
+ const pre = rgbBufs[k], al = alphaBufs[k];
676
+ for (let i = 0; i < n; i++) {
677
+ const a = al[i];
678
+ if (a < 1e-3) continue;
679
+ acc[i] += a * (255 / (1 + Math.exp(-2 * pre[i])) - acc[i]);
680
+ acc[n + i] += a * (255 / (1 + Math.exp(-2 * pre[n + i])) - acc[n + i]);
681
+ acc[2 * n + i] += a * (255 / (1 + Math.exp(-2 * pre[2 * n + i])) - acc[2 * n + i]);
682
+ }
683
+ }
684
+ for (let i = 0; i < n; i++) {
685
+ out[4 * i] = acc[i]; out[4 * i + 1] = acc[n + i];
686
+ out[4 * i + 2] = acc[2 * n + i]; out[4 * i + 3] = 255;
687
+ }
688
+ return out;
689
+ }
690
+
691
+ /* Backdrop trim: whole-image fits carry large-sigma background pads that
692
+ cover the full frame; for OBJECT layers, drop atoms whose envelope is
693
+ wider than sigMax. HEURISTIC, honestly: it also kills any genuinely
694
+ large object parts. The untrimmed layer is kept in the library. */
695
+ function trimAtoms(atoms, sigMax = 0.35) {
696
+ const kept = atoms.filter(a => Math.max(a[3], a[4]) <= sigMax);
697
+ if (!kept.length) return atoms.slice();
698
+ // re-center xy so the trimmed set is a canonical template again
699
+ let mx = 0, my = 0;
700
+ for (const a of kept) { mx += a[0]; my += a[1]; }
701
+ mx /= kept.length; my /= kept.length;
702
+ return kept.map(a => { const b = a.slice(); b[0] -= mx; b[1] -= my; return b; });
703
+ }
704
+
705
+ /* Sim(2) composition in vote coordinates: (g2 ∘ g1). */
706
+ function composePose(g2, g1) {
707
+ const s2 = Math.exp(g2[3]);
708
+ const c = Math.cos(g2[2]), s = Math.sin(g2[2]);
709
+ const wrap = d => ((d + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) - Math.PI;
710
+ return [s2 * (c * g1[0] - s * g1[1]) + g2[0],
711
+ s2 * (s * g1[0] + c * g1[1]) + g2[1],
712
+ wrap(g1[2] + g2[2]), g1[3] + g2[3]];
713
+ }
714
+ function gestureTranslate(dx, dy) { return [dx, dy, 0, 0]; }
715
+ function gestureRotateAbout(c, drho) {
716
+ const co = Math.cos(drho), si = Math.sin(drho);
717
+ return [c[0] - (co * c[0] - si * c[1]), c[1] - (si * c[0] + co * c[1]), drho, 0];
718
+ }
719
+ function gestureScaleAbout(c, ds) {
720
+ const s = Math.exp(ds);
721
+ return [c[0] - s * c[0], c[1] - s * c[1], 0, ds];
722
+ }
723
+
724
+
725
+ </script>
726
+ <script>
727
+ const BUILTINS = {"tractor": [[-0.6824, -0.45011, 2.66975, 0.70848, 0.92606, 0.44334, 5.19345, -0.09237, 0.04058, -0.01663], [-0.08584, -0.04802, 0.61626, 0.49888, 0.51436, 0.28444, 2.98078, -0.10011, 0.19875, 0.39398], [0.74864, 0.1407, 2.20893, 0.54502, 0.36724, 0.58724, 1.0072, 0.08821, 0.17557, 0.02929], [0.16677, -0.78973, 0.94754, 0.31915, 0.39057, 0.59383, 1.7488, 0.04293, 0.29863, 0.19046], [0.13943, -0.4621, 0.34775, 0.43809, 0.50801, 0.30553, 2.8646, -0.17673, 0.21056, 0.20304], [-0.05499, 0.43338, 1.6514, 0.61284, 0.45687, 0.32725, 1.11694, 0.16899, -0.3567, -0.20526], [-0.34716, 0.78101, 1.2311, 0.74696, 0.43588, 0.47387, 4.34096, 0.2865, 0.32634, 0.40835], [0.38634, 0.15072, 1.59185, 0.38671, 0.80153, 0.2006, 3.93334, -0.038, 0.2346, 0.21658], [-0.24576, -0.6867, 1.53196, 0.713, 0.38647, 0.52652, 3.95442, 0.02936, -0.05187, 0.00121], [0.35205, 0.08337, 0.53545, 0.47834, 0.43423, 0.76246, 4.15911, -0.20329, -0.08114, 0.10186], [0.35364, -0.86473, 2.06911, 1.01878, 0.6701, 0.60244, 6.00887, -0.05083, 0.02103, 0.00718], [-0.46331, 0.69726, 1.8988, 0.44293, 0.3441, 0.55859, 4.08877, 0.28312, 0.04183, 0.30179], [0.73797, -0.72063, 2.42647, 0.56377, 0.93892, 0.57773, 0.71269, -0.00084, 0.10102, 0.07746], [-0.15339, 0.31724, 0.95726, 0.50801, 0.54256, 0.30858, 0.53938, 0.02523, -0.10313, -0.26048], [0.70309, 0.45386, -0.24644, 0.30297, 0.45741, 0.70925, 4.42917, -0.02503, 0.12105, 0.27342], [0.23943, -0.29863, 0.8422, 0.33233, 0.34051, 0.22202, 3.33993, -0.1503, 0.45788, 0.30974], [-0.56384, 0.25397, 2.42246, 0.96112, 1.21737, 0.23618, 4.36676, 0.05613, 0.13365, 0.11434], [-0.36512, -0.00686, 1.45209, 0.49457, 0.31707, 0.8374, 0.28444, 0.49811, 0.06634, -0.11866], [-0.03486, 0.01865, 1.8686, 0.4669, 0.33716, 0.43868, 0.29331, 0.29541, -0.03762, -0.31555], [0.62497, 0.27048, 2.18782, 0.3361, 0.40184, 0.5221, 5.30031, -0.00354, -0.13067, -0.12973], [-0.41317, 0.11105, 0.67898, 0.48606, 0.59036, 0.28682, 5.49711, -0.10229, -0.22111, -0.29212], [-0.27571, -0.1504, 1.77764, 0.69312, 0.48048, 0.49554, 0.54236, 0.18581, 0.06243, -0.04552], [0.4641, 0.77883, 2.26806, 0.4379, 1.14871, 0.39349, 4.6492, -0.06707, -0.03793, 0.11233], [-0.56653, -0.8675, 1.28471, 0.68397, 0.3365, 0.29354, 1.07811, 0.03291, 0.04284, 0.04632], [-0.08734, 0.59009, 1.70998, 0.5352, 0.32565, 0.59399, 5.31167, -0.02259, 0.03768, 0.00686], [-0.00891, -0.13649, 0.47363, 0.39481, 0.42164, 0.37599, 6.05673, 0.05368, -0.38657, -0.22384], [0.36102, -0.14155, 1.21607, 0.16981, 0.19346, 0.64559, 0.12328, 0.14206, -0.0136, -0.00415], [-0.45032, 0.49812, 2.35641, 0.38137, 0.27461, 0.56179, 6.26456, -0.56531, -0.34132, -0.1892], [0.66365, 0.91126, 2.85526, 0.78185, 0.62224, 0.62045, 4.89805, 0.05002, -0.02027, -0.00306], [-0.78133, -0.49309, 2.58298, 0.49602, 0.94653, 0.35357, 5.84621, -0.0226, -0.05471, 0.01001], [-0.23984, 0.22701, 2.64843, 0.37651, 0.42961, 0.25471, 5.58736, -0.05288, -0.16166, -0.17766], [0.55904, 0.56313, 0.8993, 0.30673, 0.17036, 0.47454, 0.48998, -0.20619, -0.03915, -0.40454], [-0.51254, 0.78367, -0.03254, 0.35668, 0.5844, 0.4059, 5.63151, -0.02093, -0.4402, -0.38576], [-0.49183, 0.16821, -0.10355, 0.13637, 0.01697, 1.68471, 6.20816, -2.19706, -0.06284, -0.29033], [-0.75865, -0.37915, 3.63614, 0.10401, 0.17838, 2.60281, 3.56605, 0.09073, -0.11981, -0.17238], [0.17649, -0.18151, 3.10036, 0.11139, 0.32721, 0.96393, 0.67937, 0.28827, -0.27353, -0.49037], [-0.48705, 0.35371, 1.2931, 0.24513, 0.16342, 2.2225, 1.28356, -1.2217, -0.46139, -0.37179], [-0.01754, 0.55558, 2.53858, 0.27124, 0.13969, 1.24224, 3.99958, 0.01855, -0.10741, -0.03076], [-0.25695, 0.4068, 2.89412, 0.23868, 0.29239, 1.08662, 1.92178, 0.45239, 0.34315, 0.5327], [-0.23956, 0.25592, 2.23383, 0.04539, 0.10912, 3.7646, 2.57708, 5.02682, 0.98909, 0.47807], [0.628, -0.22848, 3.15552, 0.00944, 0.50113, 4.66656, 0.93257, -0.90353, 1.54592, 1.79618], [0.49409, 0.54229, 2.89216, 0.21768, 0.21806, 0.8736, 6.24187, -0.72302, -0.646, -0.51336], [-0.47663, 0.8961, 1.59818, 0.08723, 0.12112, 3.82644, 3.48794, -0.86773, -1.11731, -1.16287], [-0.71023, 0.2454, 1.27084, 0.22689, 0.22869, 1.32049, 1.42892, 0.24752, 0.09838, 0.2392], [-0.6411, -0.32881, 2.22783, 0.10306, 0.05474, 2.24301, 1.94946, -0.72905, 0.94648, 1.07426], [-0.10236, 0.0571, 2.16689, 0.11368, 0.21031, 1.10909, 2.59632, -0.30966, 0.21218, 0.25668], [0.40293, 0.14539, 3.149, 0.16438, 0.29952, 0.80921, 0.18805, 0.0656, -0.28125, -0.43057], [-0.33196, 0.22949, 2.05313, 0.13608, 0.28771, 1.73023, 4.73087, -0.82913, 0.45633, 0.43345], [0.22227, 0.09197, 1.3324, 0.09975, 0.09312, 1.08971, 3.3524, -0.11906, 0.54156, 0.12403], [0.11793, 0.39258, 1.5849, 0.06849, 0.14492, 4.87662, 2.85658, -0.79292, 1.02084, 1.32427], [0.52823, -0.67727, 0.21753, 0.11492, 0.15154, 1.38408, 3.847, -0.26577, 0.63177, 0.75418], [0.59787, -0.90371, 2.38042, 0.07061, 0.07692, 3.31604, 3.69185, 0.2139, -0.53046, -0.54509], [-0.10296, -0.98696, 0.9158, 0.28364, 0.09523, 3.94613, 1.23748, 0.07247, -0.17598, -0.20063], [-0.42743, 0.12942, 1.74352, 0.02628, 0.13178, 2.05194, 3.66078, -1.69632, 0.02142, 0.37604], [0.43108, 0.31065, 1.26171, 0.04935, 0.11457, 3.65908, 0.71281, 1.81829, -0.01638, -0.1652], [-0.64551, 0.15029, 2.79823, 0.10818, 0.0286, 1.30001, 0.2492, 1.64158, -0.61879, -0.63809], [-0.2631, -0.22718, 1.08716, 0.14952, 0.32458, 1.25624, 6.07547, 0.25972, -0.30185, -0.5652], [0.64946, -0.76278, 2.69333, 0.15504, 0.25941, 1.33319, 3.44865, 0.01556, -0.14194, -0.15755], [-0.07011, 0.47643, 0.05323, 0.05179, 0.10503, 2.85549, 4.88342, 0.95629, 1.01843, 1.2136], [0.51219, 0.76466, 1.61491, 0.06949, 0.12168, 2.3771, 0.83677, -1.79585, -1.71946, -1.62417], [-0.65308, -0.0253, 0.05561, 0.12108, 0.24282, 1.96344, 2.99588, -0.63173, 0.8612, 0.96971], [-0.75393, 0.6394, -0.58857, 0.07367, 0.13364, 1.96551, 5.57205, -1.79448, -1.70385, -1.79319], [0.67947, 0.7066, 0.76501, 0.05174, 0.08365, 3.63395, 3.78731, 2.9312, 2.8337, 2.84961], [0.78374, -0.85168, 2.90191, 0.08335, 0.05401, 1.72051, 3.89837, 0.067, -0.14074, -0.13737], [-0.65644, 0.48538, 2.2971, 0.07061, 0.13933, 1.6288, 5.87677, -0.28017, -0.3744, -0.77021], [-0.07299, -0.56282, 2.95749, 0.18481, 0.15234, 1.14593, 4.68782, -0.06461, 0.37184, 0.25643], [0.11921, -0.59593, 2.97536, 0.28076, 0.2065, 0.65973, 0.82948, 0.25906, -0.57613, -0.79168], [-0.15672, 0.40291, -0.49942, 0.09191, 0.12886, 2.13207, 3.86166, 2.41648, 0.83947, 0.87012], [-0.41411, 0.13279, 0.71077, 0.13767, 0.22816, 1.17284, 4.23561, -0.01769, 0.48793, 0.43909], [0.47887, 0.44707, 1.43521, 0.23495, 0.19113, 1.11754, 5.76585, -0.91451, -0.41865, -0.21804], [0.2231, 0.39557, 0.52648, 0.34275, 0.29421, 1.20222, 0.59739, 0.44874, 0.14287, 0.14493], [-0.07979, -0.47271, 2.52165, 0.37694, 0.05142, 2.13301, 4.86104, -0.25241, 0.37106, 0.44839], [-0.06135, -0.67232, 0.00337, 0.06471, 0.16861, 2.13669, 2.27478, -0.64686, 1.07496, 1.29683], [-0.85238, 0.43497, 0.10646, 0.19737, 0.14188, 2.26408, 0.61625, 0.72284, 0.51988, 0.7092], [-0.51825, 0.43221, -0.10887, 0.13855, 0.23206, 1.18669, 3.31322, 1.01161, 0.7205, 0.53151], [-0.12745, 0.61162, 0.59207, 0.07377, 0.20372, 3.15871, 1.16311, -0.81843, -0.967, -1.00712], [0.56756, -0.5616, 3.11854, 0.05498, 0.22625, 2.42556, 3.04, -0.24507, 0.9606, 0.9633], [0.42554, -0.29332, 3.22275, 0.14231, 0.29628, 1.05207, 0.46887, 0.32789, -0.28524, -0.58433], [0.6174, -0.22144, 3.13034, 0.0889, 0.28451, 1.9402, 1.46343, -0.21528, 0.40253, 0.44874], [0.38453, 0.49359, 3.2826, 0.29519, 0.09099, 3.12073, 2.76174, 0.91117, 0.85602, 0.87243], [0.2619, 0.34869, 2.75335, 0.10021, 0.12359, 1.57645, 2.52743, -0.44928, 0.49646, 0.50057], [0.16446, 0.27981, 1.25228, 0.11339, 0.22752, 2.15115, 5.9815, 0.7878, -0.54335, -0.68222], [0.13585, -0.71683, 2.32833, 0.05899, 0.23033, 1.45565, 3.1888, -0.16916, 0.51429, 0.6052], [0.16468, -0.60197, 3.15185, 0.07581, 0.16841, 1.76863, 6.14408, 0.08836, -0.70628, -0.68243], [-0.35743, -0.28283, 1.54047, 0.03937, 0.19287, 1.5841, 3.32628, -0.36208, 0.43891, 0.36518], [-0.62251, -0.38234, 2.61567, 0.15456, 0.32413, 1.04598, 2.40596, -0.06698, 0.0187, 0.04889], [0.79814, 0.37616, 2.43606, 0.18443, 0.12374, 2.91468, 5.89122, 0.37607, 0.34732, 0.36404], [0.51853, 0.51633, 1.9986, 0.1856, 0.16773, 0.71136, 2.96926, 1.181, 0.3687, 0.52399], [-0.85179, -0.45927, 1.21504, 0.16331, 0.12762, 2.08546, 4.38702, -0.06148, 0.04644, 0.05241], [0.82346, -0.30704, 3.00411, 0.11575, 0.11242, 0.90949, 2.33484, 0.08476, -0.0924, -0.16091], [0.27188, -0.88797, 1.57825, 0.03127, 0.2239, 2.98966, 2.26823, -0.58261, 1.38576, 1.67028], [-0.46132, -0.65263, 1.91629, 0.13799, 0.14213, 0.84698, 2.25948, -0.08674, -0.07736, -0.01761], [-0.0886, -0.07273, 0.12768, 0.10024, 0.26852, 1.98655, 3.07155, -0.14939, 0.18659, 0.28131], [0.27019, 0.29867, 0.82501, 0.1431, 0.0816, 3.81301, 1.75651, -0.80205, -0.04131, 0.0189], [0.00722, 0.14136, 0.27264, 0.09149, 0.26244, 1.54872, 3.31034, -0.3603, 0.23216, 0.43633], [-0.13459, 0.52946, 3.39189, 0.04356, 0.13784, 1.61563, 6.20546, -0.968, -0.99753, -0.75933], [0.04028, -0.67528, 3.17229, 0.15631, 0.14623, 2.75131, 2.57373, -0.08824, 0.54121, 0.48411], [0.39384, -0.52184, 3.20355, 0.12168, 0.17843, 0.94272, 0.41448, -0.02293, -0.64596, -0.4227], [0.62453, 0.47184, 2.91956, 0.16326, 0.21354, 1.26663, 2.48642, 0.30674, 0.3324, 0.37472], [0.55621, 0.38204, 1.82273, 0.09645, 0.21059, 1.67487, 2.48445, 0.84619, 0.66986, 0.67577], [-0.81937, 0.493, -0.0036, 0.05319, 0.0932, 2.92422, 2.67774, 2.59517, 2.63921, 2.22331], [-0.09406, -0.6307, 3.13683, 0.01255, 0.21603, 2.82704, 3.40586, 0.63554, -1.39215, -1.53166], [0.64661, 0.49075, 2.50093, 0.15143, 0.13703, 0.80325, 5.39367, -0.41876, -1.24179, -0.74772], [0.52089, 0.00283, 3.20765, 0.15302, 0.20652, 1.36938, 2.85748, -0.45495, 0.55844, 0.44882], [-0.74526, -0.0199, 3.14128, 0.01984, 0.22471, 4.63492, 4.49001, -2.32443, 3.06353, 3.78369], [-0.32624, 0.77647, 1.11274, 0.05544, 0.12966, 2.98644, 0.48049, -2.01201, -1.79998, -2.19936], [0.35253, 0.66839, 2.45969, 0.0704, 0.10646, 2.89173, 3.52159, 2.50863, 2.51213, 2.52348], [-0.35928, 0.52445, 0.30938, 0.27926, 0.20368, 0.67397, 0.64618, -0.68931, -0.71052, -0.44067], [0.26325, -0.73415, 1.56602, 0.14298, 0.22818, 1.3569, 3.30544, -0.26757, 0.49021, 0.37976], [0.586, 0.26585, 2.14199, 0.03439, 0.14258, 1.46884, 5.34673, 0.96545, -0.18662, 0.04527], [-0.43325, 0.67077, 1.4374, 0.34834, 0.21183, 0.64381, 0.78005, -1.10733, -1.12475, -0.93446], [0.60188, -0.86897, 0.58349, 0.06483, 0.08777, 3.37717, 3.87752, -0.28056, 0.65673, 0.6977], [-0.82983, -0.80561, 1.16311, 0.09632, 0.14492, 1.60901, 2.93708, -0.03843, 0.02064, 0.03901], [0.27384, -0.9034, 1.56931, 0.01144, 0.25121, 2.51038, 5.78922, 0.83105, -1.51501, -1.32257], [-0.17077, 0.69291, 1.04495, 0.08344, 0.08726, 2.63368, 1.62487, -0.56447, -0.58933, -0.59421], [0.61545, -0.21363, 0.00674, 0.02712, 0.44163, 3.80444, 4.43907, -1.05569, 1.55318, 1.85023], [-0.45204, -0.24833, 1.67065, 0.15145, 0.20898, 1.1456, 5.80945, 0.25417, -0.58287, -0.44938], [0.91085, 0.30058, 2.55159, 0.32394, 0.11989, 4.42473, 3.81485, -0.19905, -0.20616, -0.19025], [-0.01544, -0.44709, 2.87273, 0.09374, 0.05014, 2.55513, 3.60033, -0.121, 0.72357, 1.00287], [-0.51983, -0.20343, -0.26748, 0.06577, 0.16795, 1.95382, 2.6593, -0.33308, 0.46895, 0.3713], [0.26357, -0.76429, 1.54218, 0.15758, 0.22336, 1.04113, 2.74606, -0.2448, 0.28127, 0.71722], [-0.3753, -0.36271, 1.57935, 0.02425, 0.2412, 3.96846, 1.94527, -1.9547, 2.67872, 3.41142], [-0.54932, -0.16918, 0.31415, 0.12651, 0.14755, 0.72287, 0.30894, 0.4183, -0.36801, -0.68576], [-0.23035, -0.49408, 1.66203, 0.23955, 0.24268, 1.84685, 2.22684, 0.09834, -0.12612, -0.15135], [-0.30076, -1.01231, 0.68756, 0.22471, 0.22201, 1.60389, 0.8549, 0.01592, -0.08469, -0.10949], [-0.62797, 0.69828, 2.19777, 0.09535, 0.15567, 2.33143, 6.10749, -1.68191, -2.18704, -2.31563], [0.54776, 0.16296, 3.13115, 0.07107, 0.14779, 3.33182, 0.19249, 0.53257, -0.48905, -0.70101], [0.28274, -0.85851, 1.56778, 0.07122, 0.22749, 3.08328, 5.4518, 0.32783, -1.029, -0.99974], [-0.75271, 0.29227, 0.74579, 0.03556, 0.08729, 4.60293, 2.32435, 4.08807, 3.24784, 3.005], [0.38325, 0.54477, 0.94102, 0.40818, 0.28921, 1.7405, 1.45806, -0.04071, 0.23743, 0.32516]], "star": [[-0.51053, -0.15273, 0.68805, 0.35961, 0.16414, 0.36307, 5.26169, 0.35404, 0.26297, -0.02025], [-0.07018, -0.79386, 1.46149, 0.27812, 0.34827, 0.66017, 0.5998, 0.02515, -0.03127, -0.01383], [-0.04242, -0.06369, 0.41342, 0.52432, 0.492, 0.32373, 2.82758, -0.0946, -0.04359, 0.19702], [-0.5847, -0.79101, 0.74844, 0.71803, 0.3309, 0.57434, 3.00178, 0.00359, -0.00168, -0.00322], [-0.08832, 0.12294, 1.53853, 0.459, 0.39519, 0.38586, 0.2186, 0.20673, 0.06584, -0.12705], [-0.60689, -0.33177, 1.17408, 0.96799, 0.62049, 0.69716, 5.23717, -0.01384, 0.00645, -0.01124], [0.11267, 0.28884, 1.11652, 0.39831, 0.30015, 0.46066, 4.32536, -0.14439, 0.02783, 0.16473], [0.32993, 0.06371, 0.41569, 0.16036, 0.30224, 0.78318, 6.03392, 0.31667, 0.33124, -0.08115], [0.46703, -0.0168, -0.08327, 0.40889, 0.45086, 0.59604, 4.14405, -0.04784, -0.04327, -0.01626], [0.61214, -0.16662, 2.05353, 0.39932, 0.47518, 0.62537, 6.12889, 0.00102, 0.02287, 0.0025], [-0.86329, -0.74587, 2.69009, 0.63739, 0.86173, 0.3242, 4.08681, -0.04421, -0.04775, 0.0649], [-0.01049, -0.1416, 1.38894, 0.47137, 0.34564, 0.30828, 2.61817, -0.42365, -0.03028, 0.21405], [-0.34958, 0.02443, 1.04418, 0.35261, 0.13573, 0.36934, 2.2475, -0.06532, -0.19908, 0.0011], [0.76035, -0.07882, 0.09598, 0.48621, 0.25489, 0.319, 4.09159, -0.1085, -0.11318, 0.15415], [0.28869, -0.26514, 0.83971, 0.49766, 0.42968, 0.42586, 4.07843, -0.00878, -0.0057, -0.00669], [0.09774, 0.02822, 2.87191, 0.62577, 0.52997, 0.44792, 0.1387, 0.03507, 0.06726, -0.03023], [-0.0697, 0.00158, 0.11391, 0.33412, 0.28107, 0.70131, 5.68176, 0.08611, -0.12642, -0.14202], [0.0488, 0.10928, 1.1197, 0.42092, 0.33285, 0.27918, 3.51575, -0.38059, -0.08911, 0.27614], [-0.0164, 0.04503, 2.89844, 0.54267, 0.40763, 0.33239, 0.07615, 0.26878, 0.18469, -0.06475], [0.62477, -0.25381, 2.3837, 0.37851, 0.37412, 0.23939, 4.97219, 0.17725, 0.07981, -0.00017], [-0.34377, 0.556, 1.43592, 0.30259, 0.52825, 0.61379, 4.12942, -0.10279, -0.05566, 0.04159], [0.03347, -0.16549, 1.55273, 0.26411, 0.54085, 0.52546, 5.74224, 0.38154, 0.11136, -0.06104], [0.57593, 0.35328, 1.28074, 0.52591, 0.59368, 0.28347, 3.16451, -0.0194, 0.00598, -0.00804], [0.56303, 0.91735, 1.15253, 0.38446, 0.53731, 0.41035, 0.65649, -0.01641, -0.00829, -0.03907], [-0.0675, 0.58166, 2.06245, 0.19321, 0.19147, 0.60727, 3.05455, 0.02441, -0.07817, -0.06201], [0.09489, -0.20717, 0.78309, 0.61948, 0.61941, 0.54474, 5.3332, 0.13564, 0.05384, -0.05197], [0.76998, -0.59085, 0.72889, 0.2626, 0.36886, 0.78807, 1.89753, -0.03333, -0.01984, -0.0037], [-0.92937, -0.2769, 1.48164, 0.16458, 0.51391, 0.88393, 4.68962, 0.11147, 0.02193, -0.12731], [-0.04073, -0.3637, 0.41262, 0.24729, 0.51547, 0.74959, 3.67937, -0.13205, -0.08886, 0.04567], [-0.1472, -0.41196, 1.58488, 0.58691, 0.55867, 0.38693, 2.37401, -0.03823, -0.02307, 0.02535], [-0.00614, 0.03833, 2.69916, 0.48144, 0.53005, 0.49706, 3.20669, -0.20773, 0.04657, 0.06422], [-0.20447, 0.05738, 2.97441, 0.15979, 0.22661, 2.44411, 1.39823, -0.30817, -0.09173, 0.12606], [-0.76942, -0.19783, 0.21829, 0.13352, 0.04064, 1.49619, 5.39481, 1.10947, 0.52703, -0.51469], [-0.46293, -0.23911, 1.61092, 0.04634, 0.33172, 3.03375, 5.41794, 1.26103, 0.78286, -0.49522], [0.06065, -0.83499, 1.1654, 0.14828, 0.005, 2.62333, 4.3315, -1.33619, -0.7626, 0.64493], [-0.31575, 0.31703, 0.19491, 0.13551, 0.27143, 1.33853, 6.08375, 0.8196, 0.30475, -0.20821], [-0.56296, -0.08599, 2.18115, 0.11669, 0.28658, 1.93389, 0.64648, 0.58107, 0.24135, -0.26829], [-0.112, 0.09234, 2.63498, 0.09472, 0.08023, 3.33312, 3.20024, 0.55321, 0.85241, 0.62275], [-0.42245, -0.08477, 0.01726, 0.22589, 0.12844, 1.69061, 0.16619, 0.23566, -0.00869, -0.03135], [0.06776, 0.6268, 2.58094, 0.06912, 0.05267, 2.03968, 1.49948, 0.11016, 0.04239, -0.04381], [0.28122, 0.59345, 2.17899, 0.01946, 0.24747, 3.82596, 4.42208, -3.84417, -2.18766, 2.01159], [0.31386, -0.36299, 1.20103, 0.13599, 0.07654, 1.6408, 0.16886, -0.29135, -0.0623, 0.17822], [0.55473, -0.31699, 1.59136, 0.11557, 0.16764, 2.06004, 1.14207, -0.62327, -0.31521, 0.27353], [-0.44106, 0.45828, 0.26594, 0.03281, 0.22193, 3.7483, 2.17469, -2.02698, -1.09346, 0.96396], [0.65717, 0.59846, -0.27109, 0.14513, 0.11559, 3.48087, 5.39501, 0.12239, 0.03585, -0.03452], [-0.14798, -0.45393, 0.32433, 0.01861, 0.79582, 4.4202, 4.92375, 3.28216, 1.43719, -1.25199], [-0.60536, -0.04913, 2.21412, 0.02314, 0.24465, 4.18919, 4.46277, -4.16041, -1.95161, 1.7603], [-0.62697, -0.5098, 1.45758, 0.08486, 0.15698, 1.52263, 1.89466, 0.12829, 0.10561, -0.03876], [0.51449, -0.28711, 1.60491, 0.06129, 0.2162, 3.89484, 3.87665, 0.95616, 0.46459, -0.42435], [0.32116, 0.38261, 2.63277, 0.18795, 0.14359, 1.17108, 0.03199, 0.37471, 0.26449, -0.44471], [0.29948, 0.06897, 2.81495, 0.04949, 0.39792, 2.97239, 5.91684, 1.89518, 0.56408, -0.23856], [-0.5663, 0.85365, 2.05972, 0.08528, 0.06723, 0.8634, 4.01119, 0.17046, 0.05918, -0.06136], [-0.03235, -0.1062, 1.30497, 0.0568, 0.10161, 1.98305, 2.61473, 0.27054, 0.64028, 0.97997], [0.22074, -0.61255, -0.17283, 0.21073, 0.19802, 1.33841, 5.61818, -0.11967, -0.07294, 0.12897], [0.11109, -0.54965, 2.90996, 0.05438, 0.19995, 2.67146, 5.49666, 0.89583, 0.30495, -0.30528], [0.4764, -0.2783, 1.57209, 0.01317, 0.20611, 3.7017, 3.85687, 1.86438, 0.81372, -0.68967], [-0.79551, -0.69126, 1.68869, 0.18938, 0.18063, 1.48869, 2.0417, -0.09177, -0.04576, 0.05076], [-0.69563, -0.39051, 1.65237, 0.17716, 0.12341, 3.1259, 4.8354, -0.17853, -0.09295, 0.08889], [0.4658, 0.20264, 1.3172, 0.11945, 0.05827, 0.84899, 2.47738, 0.35942, 0.24163, -0.24982], [0.12673, -0.61229, -0.30152, 0.02136, 0.30796, 3.50656, 4.4067, -2.95706, -1.8063, 1.69187], [0.01654, -0.88378, 1.64948, 0.13909, 0.02471, 2.18708, 4.81224, 1.36907, 0.71828, -0.64168], [0.52927, 0.0586, 0.94322, 0.05803, 0.13738, 3.00829, 1.65677, 1.20588, 0.70135, -0.64307], [0.11272, 0.53699, 1.59168, 0.27775, 0.20228, 1.15561, 4.67559, -0.23347, -0.19164, 0.09554], [0.19108, 0.06434, 0.15906, 0.05952, 0.08047, 2.55291, 0.86874, -0.65442, -1.18447, -1.1727], [-0.28411, -0.42135, 3.38232, 0.09539, 0.08902, 0.98512, 2.76018, 0.0392, 0.02729, -0.10188], [-0.42219, -0.22683, 2.16676, 0.17983, 0.29019, 0.84596, 2.02503, -0.16512, -0.1051, 0.16705], [0.24219, 0.4444, 2.16647, 0.13174, 0.20905, 1.64917, 0.1576, 0.92216, 0.24084, -0.18353], [0.14429, 0.51728, 1.87757, 0.09842, 0.12765, 2.3035, 0.3321, -0.21602, -0.00533, -0.04041], [0.00122, -0.42501, 3.22421, 0.14635, 0.15283, 1.00079, 3.56606, -0.95341, -0.46443, 0.19811], [0.01914, 0.02327, 1.68727, 0.21134, 0.13233, 1.49743, 6.22365, -0.51815, -0.83356, -0.67064], [0.28187, 0.36377, -0.38082, 0.10839, 0.14596, 0.8208, 5.92113, 1.09395, 0.3801, -0.17753], [0.22536, 0.49825, 2.20265, 0.05758, 0.25291, 2.76728, 0.46329, 1.42985, 0.51988, -0.41777], [-0.45783, -0.0778, 1.84375, 0.14555, 0.14505, 1.25657, 0.34373, 1.67452, 0.522, -0.42744], [0.13987, -0.2105, 2.01916, 0.13301, 0.19415, 1.81491, 0.56274, 1.00172, 0.68436, -0.27158], [0.50098, -0.05012, 0.76651, 0.19449, 0.2237, 1.15756, 3.65245, -0.66405, -0.35603, 0.28384], [0.45732, 0.61373, 1.1175, 0.15351, 0.04725, 0.83834, 0.31755, 1.27973, 0.78829, -0.76025], [-0.31064, 0.32627, 3.03941, 0.17271, 0.11314, 0.89206, 3.90119, -1.08711, -0.48405, 0.42314], [-0.05244, 0.46698, 1.28082, 0.41268, 0.19876, 1.82854, 1.96378, 0.14533, 0.08843, -0.04229], [0.65368, -0.04039, 0.91601, 0.01431, 0.2486, 2.64038, 0.37863, -1.53102, -0.6903, 0.61226], [0.11256, -0.49758, 2.90797, 0.08858, 0.20068, 1.66331, 1.9988, -0.95035, -0.3115, 0.39809], [-0.55115, -0.2651, 1.56625, 0.01558, 0.26992, 4.61937, 1.74074, -4.7308, -2.21314, 2.21696], [-0.57442, 0.29028, 2.07148, 0.07815, 0.11578, 1.77736, 3.97166, 0.24663, 0.10249, -0.12441], [-0.57351, -0.03527, 2.31236, 0.08946, 0.13378, 1.57572, 4.21907, -0.42142, -0.45787, 0.33859], [0.44745, -0.04054, 1.57304, 0.1578, 0.12902, 1.05371, 0.84855, 1.15497, 0.12029, -0.08756], [0.47798, 0.46267, -0.28408, 0.01747, 0.25291, 4.78872, 1.38827, 3.57491, 2.16954, -2.14804], [-0.39951, 0.56329, 3.60983, 0.06129, 0.12474, 2.31554, 0.20968, 1.7986, 0.71715, -0.67198], [0.2029, 0.94365, 1.51817, 0.12817, 0.15872, 0.90829, 5.2715, 0.09825, 0.00224, -0.02612], [-0.2578, 0.40853, 0.87754, 0.17928, 0.23013, 1.80534, 6.23272, 0.42828, 0.23362, -0.20952], [0.62698, -0.08746, 0.60216, 0.13076, 0.15134, 1.24408, 4.03669, -0.78655, -0.2753, 0.33121], [-0.11036, -0.36616, 0.44709, 0.11328, 0.21919, 1.53497, 2.63921, -1.00777, -0.44399, 0.4229], [0.57028, 0.65051, -0.01295, 0.18803, 0.20359, 1.5268, 2.61712, 0.20971, 0.08408, -0.06806], [0.91737, -0.25154, 3.13936, 0.36808, 0.01164, 0.55714, 4.98839, 3.41089, 1.68578, -1.47782], [0.00464, -0.67149, 0.51839, 0.07402, 0.08209, 1.77517, 6.14924, 1.13503, 0.69195, -0.43628], [0.45897, 0.66312, 1.85728, 0.07313, 0.11845, 1.14973, 3.4493, -0.06586, -0.09912, 0.17318], [0.03662, 0.17978, 2.97047, 0.11176, 0.035, 1.47455, 6.27172, -0.71513, -1.26976, -0.78411], [0.61477, -0.81121, 1.1419, 0.13361, 0.13146, 0.78051, 3.17935, -0.0222, 0.00013, -0.0463], [-0.47039, 0.16684, 2.52823, 0.13584, 0.09956, 2.82399, 0.4835, -0.37612, -0.15246, 0.13208], [0.0925, 0.27645, 0.13734, 0.26465, 0.1313, 1.96463, 3.63057, 0.45913, 0.11807, -0.09602], [0.74131, -0.17449, 2.55596, 0.14172, 0.03612, 0.77917, 3.09129, -2.06065, -1.08434, 0.96287], [-0.49518, 0.1883, 0.21456, 0.10268, 0.09144, 2.59442, 3.75846, -0.11141, -0.00155, -0.05859], [-0.08749, -0.04503, 0.36005, 0.16451, 0.14805, 1.75564, 1.96861, 0.37147, 0.61096, 0.57908], [-0.07382, -0.4671, 0.31571, 0.07188, 0.24398, 1.9183, 0.03077, 0.76225, 0.24352, -0.29235], [-0.41184, 0.73294, 4.04825, 0.01519, 0.12271, 2.8311, 3.22725, 0.54958, 0.21104, -0.20166], [0.05866, -0.48599, 1.87293, 0.06712, 0.14864, 1.88058, 2.60092, -0.29807, -0.2459, 0.00046], [-0.22666, 0.54185, 4.08157, 0.04808, 0.18162, 2.6353, 2.15601, -1.70536, -0.5344, 0.645], [0.11034, 0.555, 1.8767, 0.10493, 0.15919, 2.8434, 3.33574, 0.11173, 0.08681, -0.12987], [-0.00432, -0.59956, 0.68151, 0.21, 0.35642, 1.18027, 1.97778, -0.13986, -0.03979, 0.08981], [-0.22001, -0.41801, 1.9694, 0.2259, 0.02907, 0.6921, 0.39981, -0.62639, -0.23372, 0.2716], [-0.69313, 0.19448, 1.77782, 0.09053, 0.15994, 2.16316, 3.30231, 0.03186, 0.04356, -0.026], [-0.07556, 0.43344, 0.94755, 0.04052, 0.33977, 1.96234, 0.87423, 1.98507, 0.77476, -0.48832], [-0.25397, 0.58587, 0.97557, 0.01057, 0.2231, 2.65974, 3.26704, -2.58728, -1.3095, 1.18591], [0.51088, -0.39587, 1.97375, 0.26423, 0.20145, 1.12756, 1.03371, -0.17067, -0.13254, 0.15733], [-0.51523, 0.7042, 1.79156, 0.18224, 0.13081, 1.36296, 5.3965, -0.16144, -0.0463, -0.00371], [-0.32852, -0.24906, 1.52681, 0.08517, 0.13409, 2.13653, 1.6727, -0.87072, -0.15162, 0.25915], [0.00806, 0.84109, 0.15468, 0.10667, 0.14355, 0.93857, 6.05272, -0.02536, -0.02323, -0.01905]], "boat": [[-0.35803, -0.04712, 0.33676, 0.46106, 0.39276, 0.50475, 4.13232, 0.00283, 0.01229, -0.00976], [-0.41203, 0.10897, 2.52432, 0.40229, 0.57243, 0.23464, 5.08722, -0.00107, 0.00666, -0.07483], [0.12509, -0.17784, 2.45227, 0.25832, 0.32034, 0.71608, 3.56406, -0.55495, -0.81191, -0.24526], [-0.14167, -0.02618, 0.79131, 0.39298, 0.77199, 0.53086, 5.38417, 0.08289, -0.01826, -0.03796], [-0.01467, -0.16899, 0.41817, 0.36342, 0.33713, 0.73269, 5.66457, 0.22791, 0.20826, 0.12245], [0.11882, 0.17061, 1.5263, 0.42596, 0.48433, 0.64705, 4.39323, -0.0514, -0.11341, -0.10323], [0.38566, 0.39142, 0.93088, 0.41204, 0.58494, 0.39645, 0.64687, 0.12816, -0.03829, -0.08142], [0.14899, 0.39127, 1.63262, 0.57184, 0.2499, 0.65196, 5.76974, -0.0592, -0.16024, -0.1084], [-0.19028, -0.94504, 1.12366, 0.76464, 0.29497, 0.38367, 4.45984, 0.18743, 0.14662, 0.10917], [0.23686, 0.09142, 1.30661, 0.29763, 0.25272, 0.73147, 4.36912, -0.39693, -0.46958, -0.27099], [0.18348, 0.89217, 1.06554, 0.55058, 0.48248, 0.40182, 1.53431, 0.07829, 0.00204, -0.18879], [0.70699, -0.80219, 0.73109, 0.56942, 0.85824, 0.65426, 3.40796, -0.00738, -0.0733, -0.06328], [0.08463, 0.35836, 1.62099, 0.36608, 0.36466, 0.74301, 2.07875, -0.09204, 0.31805, 0.30015], [0.76143, 0.12624, 2.48314, 0.52285, 0.57768, 0.2519, 4.92292, 0.148, 0.04802, -0.05123], [-0.05321, -0.44144, 2.33538, 0.36346, 0.49199, 0.60418, 3.36667, -0.18666, -0.15261, -0.08766], [-0.49942, 0.39841, 2.1084, 0.46562, 0.37552, 0.79064, 4.52097, 0.01144, -0.00033, 0.00854], [0.12141, -0.21516, 0.92039, 0.34648, 0.19827, 0.3196, 6.14339, 0.22338, 0.23482, 0.45767], [0.11031, -0.58101, 1.14666, 0.51466, 0.84179, 0.23172, 2.30048, -0.1002, 0.06144, 0.03765], [0.03505, 0.09251, 1.79332, 0.55181, 0.44314, 0.35617, 1.28299, 0.10084, 0.28937, 0.17874], [0.19853, 0.07481, 3.03637, 0.23448, 0.52977, 0.37181, 5.88989, 0.19077, 0.22606, -0.15106], [0.05457, -0.27555, 0.96562, 0.33537, 0.23879, 0.41219, 2.59711, -0.4955, -0.37183, -0.08309], [0.24537, -0.88183, -0.01118, 0.39644, 0.76785, 0.60463, 0.83211, 0.08243, 0.07792, 0.08496], [0.35768, -0.15754, 0.02243, 0.40371, 0.34036, 0.4328, 0.9687, 0.27805, 0.11875, 0.05968], [0.45203, -0.6876, 2.51104, 0.64175, 0.47358, 0.9028, 2.91444, 0.03378, 0.01227, 0.00709], [-0.84711, 0.43104, 1.14221, 0.48062, 0.811, 0.58063, 1.42528, -0.01733, 0.03998, 0.02921], [-0.65691, -0.00547, 1.40793, 0.59829, 0.31731, 0.56019, 3.49149, -0.00514, 0.01508, -0.01819], [0.31537, 0.54621, 2.14771, 0.42914, 0.33822, 0.52531, 2.59495, -0.12871, 0.04671, 0.30871], [0.18389, -0.0192, 1.72127, 0.40317, 0.25492, 0.65319, 3.35443, -0.23199, -0.1824, -0.46588], [0.24018, -0.26153, -0.18801, 0.30371, 0.29881, 0.47301, 3.88628, -0.36114, -0.17004, -0.31887], [-0.51405, 0.71201, 1.76857, 0.34024, 0.31019, 0.61892, 3.98611, -0.1618, 0.17146, 0.45924], [-0.56883, 0.49773, 1.86237, 0.20481, 0.23447, 0.84602, 0.11957, 0.15594, -0.29436, -0.53234], [0.23848, 0.06412, 1.51014, 0.11924, 0.22533, 1.53357, 0.82589, 2.20238, 1.65043, 0.62457], [-0.21027, -0.54098, 2.46659, 0.22927, 0.1359, 1.27134, 5.19799, -0.11871, -0.0959, -0.13234], [-0.10351, -0.26834, 0.01181, 0.07839, 0.31304, 2.06626, 2.96916, -1.71056, -1.0688, -0.53551], [-0.0248, -0.55869, 2.72252, 0.17045, 0.16361, 1.86115, 0.88871, 0.67705, 0.66146, 0.1311], [0.72597, 0.40145, 3.83772, 0.0798, 0.09103, 2.35273, 4.93156, 0.25208, -0.42274, -0.58041], [0.34289, 0.39094, 1.38718, 0.16128, 0.19588, 1.19757, 5.685, 0.16747, -0.20192, -0.7312], [0.06644, -0.60577, -0.63073, 0.08736, 0.19748, 1.87261, 0.1431, 1.77121, 0.94503, 0.64713], [-0.21724, -0.33086, 0.47813, 0.08997, 0.08946, 0.89736, 6.21413, -0.23098, -0.24363, -0.16915], [-0.22979, -0.72439, 1.33041, 0.1924, 0.02362, 2.09594, 5.21617, -0.2779, -0.3288, -0.2743], [-0.28104, -0.15587, 1.23101, 0.13788, 0.09774, 0.8023, 3.10253, 0.0241, -0.01377, 0.13467], [0.52665, 0.56464, 0.60309, 0.11355, 0.19963, 1.72993, 0.41176, 0.25423, -0.35009, -1.00354], [-0.09769, 0.05324, 2.59468, 0.10418, 0.0784, 1.84332, 5.30456, -0.43615, -0.27265, -0.47339], [0.14923, 0.43003, 1.26489, 0.25233, 0.08449, 2.11129, 2.12432, -0.07108, -0.0937, -0.14313], [-0.79014, 0.61709, 1.90558, 0.37037, 0.11136, 1.64229, 5.18906, -0.13822, 0.16428, 0.3693], [-0.12659, 0.30183, 1.57897, 0.03235, 0.65149, 3.35957, 4.83163, 0.62631, -1.09254, -2.34957], [0.74631, -0.27895, 2.57092, 0.32236, 0.22899, 1.70782, 4.40745, -0.10511, -0.11866, -0.0974], [-0.4706, -1.01786, 1.562, 0.11467, 0.13857, 1.21133, 5.87333, 0.02135, -0.01299, 0.02737], [-0.05836, 0.04774, 0.62651, 0.22665, 0.15889, 1.20993, 2.69819, -0.14953, -0.32859, -0.33054], [0.48753, 0.18505, 1.40111, 0.09815, 0.21416, 1.35927, 0.28737, -0.22569, -0.15438, -0.10252], [0.19376, 0.78175, 0.86577, 0.232, 0.101, 1.39569, 2.35936, 0.06397, 0.01969, -0.15017], [-0.32573, 0.28337, 1.44029, 0.10177, 0.09071, 2.11344, 1.90905, -0.0877, 0.22625, 0.5373], [-0.10059, 0.20726, -0.15807, 0.04677, 0.10242, 1.83554, 3.26912, -2.11536, -1.82547, -1.57415], [0.84624, -0.47251, 1.47539, 0.20866, 0.22456, 1.01765, 2.08865, 0.01409, 0.01689, 0.01502], [0.50083, -0.29505, 1.39077, 0.08953, 0.07203, 1.88329, 0.12959, -0.05621, -0.08363, -0.1078], [-0.1445, -0.95257, 2.90764, 0.07775, 0.01977, 2.03443, 3.97165, 0.97008, 0.86927, 0.57117], [-0.47625, -0.61653, 2.43245, 0.11514, 0.11414, 1.41168, 3.49398, 0.02675, 0.04917, 0.00409], [0.15157, 0.15012, 1.47682, 0.03508, 0.16766, 3.63825, 3.18341, 0.92059, 0.83145, 0.42329], [0.14193, 0.25131, 2.49569, 0.17593, 0.13607, 2.08532, 0.75981, -0.11116, 0.00501, 0.01148], [-0.66478, 0.65038, 1.19491, 0.35746, 0.06058, 2.73093, 5.53523, 0.11769, -0.16289, -0.3357], [0.61342, 0.56992, 3.7336, 0.02526, 0.21197, 4.26954, 1.86086, -0.6535, 0.93151, 2.24838], [0.6787, -0.92989, 0.41269, 0.25026, 0.38995, 1.63202, 5.8353, -0.00703, -0.02194, -0.01415], [0.04638, -0.55831, 2.42571, 0.12093, 0.16263, 0.73856, 6.14543, 0.36226, 0.49601, 0.35012], [-0.21198, -0.14439, 1.58812, 0.15066, 0.05129, 0.70131, 6.16365, -0.33509, -0.32551, -0.12919], [-0.2144, 0.03032, 2.91624, 0.05753, 0.07852, 1.87215, 0.1741, -0.39673, -0.36386, -0.22987], [-0.13844, -0.71483, 0.07072, 0.07635, 0.33219, 2.26688, 4.82888, 0.2467, 0.18457, 0.16929], [0.15999, -0.29622, 2.69916, 0.20213, 0.24233, 1.002, 5.81437, 1.45309, 1.05946, 0.17348], [-0.07523, 0.37701, 0.46661, 0.06141, 0.03343, 1.56853, 0.70886, -1.05117, -1.23321, -0.96933], [-0.16682, -0.99045, 1.12432, 0.05948, 0.2007, 1.9382, 3.04478, 0.1796, 0.16536, 0.09781], [0.80351, -0.49286, 1.98354, 0.17694, 0.25954, 1.03301, 1.10691, 0.00779, 0.03798, 0.06703], [0.82117, 0.6799, 1.03574, 0.09622, 0.19606, 0.89922, 4.38744, 0.08018, -0.11594, -0.18364], [-0.13149, -0.81441, 0.15398, 0.07111, 0.12125, 1.76403, 2.42052, -0.97914, -1.07737, -0.75186], [-0.61828, -0.31465, 0.80308, 0.12261, 0.14744, 0.48786, 2.49841, -0.08821, -0.04903, 0.05376], [0.39459, -0.10493, 2.5056, 0.09057, 0.16075, 1.27382, 3.16278, -2.35689, -1.60934, -0.60862], [-0.09579, 0.81222, 1.57241, 0.01623, 0.37915, 5.3683, 1.47924, 0.88204, -1.30361, -2.71795], [-0.28004, 0.49948, 1.65494, 0.17664, 0.2661, 0.75148, 0.10037, 0.22502, -0.18689, -0.88598], [0.24841, -0.53404, 2.51898, 0.01783, 0.41616, 3.90649, 2.67916, 1.43219, 1.213, 0.7207], [0.11963, -0.67874, 0.9541, 0.60554, 0.00669, 0.856, 0.23229, -1.54654, -1.29281, -0.86237], [-0.52329, 0.77623, 1.58572, 0.03163, 0.13249, 1.86478, 3.20299, -0.22208, 0.35925, 0.72493], [0.0299, 0.77708, 1.54304, 0.06955, 0.29003, 2.61709, 3.92385, -0.28405, 0.4157, 1.04792], [-0.17194, 0.06405, 2.53053, 0.20492, 0.08149, 1.43345, 5.17384, -0.2267, 0.02543, 0.00314], [0.55317, 0.66661, 1.90108, 0.42554, 0.21636, 1.83571, 6.12928, -0.07583, 0.08871, 0.17245], [-0.44615, 0.12555, 3.12824, 0.23051, 0.18191, 1.53885, 4.09845, 0.04691, 0.01757, 0.05995], [-0.24918, -0.17663, 0.32413, 0.05639, 0.06614, 0.86147, 1.95925, 0.19675, 0.15027, 0.25504], [0.29911, 0.60437, 0.50524, 0.14357, 0.11689, 0.9624, 2.34493, -0.14827, 0.15435, 0.16659], [-0.41892, 0.7034, 1.53665, 0.09705, 0.16946, 1.9209, 3.17691, -0.1839, 0.26102, 0.75296], [-0.79271, 0.58647, 2.62031, 0.05162, 0.19227, 2.43745, 1.26147, 0.4843, -0.74477, -1.61327], [-0.60484, 0.03067, 1.28536, 0.17084, 0.14292, 1.52045, 3.6335, -0.04817, 0.02099, 0.0977], [-0.34671, 0.04527, 3.40945, 0.07248, 0.08656, 1.07657, 2.47549, 0.04936, 0.10153, 0.02661], [-0.22547, 0.15271, 0.20253, 0.16391, 0.13752, 1.71726, 4.2191, -0.02961, 0.15912, 0.14774], [-0.00687, -0.73161, 2.5028, 0.07599, 0.1783, 3.06414, 0.4712, 1.32528, 1.34062, 0.86031], [-0.15703, -0.95468, 3.25628, 0.1045, 0.01257, 2.11908, 1.13725, -0.45612, -0.42957, -0.29404], [-0.05372, 0.24665, 0.22127, 0.08319, 0.07884, 2.87674, 4.38322, -0.70704, -0.72359, -0.67824], [0.24342, 0.81571, 1.44644, 0.29137, 0.25659, 0.76395, 1.4982, 0.29314, -0.19535, -0.53801], [-0.68747, 0.54977, 0.22252, 0.25197, 0.1895, 0.88591, 2.94688, -0.12023, 0.23612, 0.56521], [0.47923, 0.30185, 1.57186, 0.00961, 0.2431, 1.65118, 0.45513, 0.60631, -0.81851, -1.97919], [0.04753, -0.49167, 2.745, 0.17775, 0.21465, 1.05867, 0.09611, 0.74664, 0.67871, 0.72987], [-0.91372, 0.37551, 2.54913, 0.04007, 0.06297, 3.36503, 3.85676, -0.40663, 0.58702, 1.40486], [0.409, 0.69172, 1.80022, 0.23277, 0.04852, 2.95077, 6.09774, 0.09222, -0.18616, -0.43305], [-0.96688, 0.90454, 1.94801, 0.08507, 0.17094, 1.69168, 6.06099, -0.06892, 0.06249, 0.09266], [-0.44291, -0.08531, -0.15556, 0.15896, 0.14925, 2.22345, 4.15054, -0.02685, -0.03191, -0.01925], [-0.22339, 0.07173, 0.92299, 0.05924, 0.04536, 1.11415, 5.24568, -0.18604, -0.34506, -0.07539], [0.56561, -0.04271, -0.6544, 0.02191, 0.16757, 3.69071, 4.20103, -4.09617, -4.05649, -2.88399], [0.35068, -0.35148, 2.51338, 0.05881, 0.30758, 2.72514, 1.79961, -1.26945, -1.30986, -1.07607], [-0.18379, -0.3334, 0.0015, 0.01045, 0.44674, 3.65024, 2.58839, 1.95094, 1.62632, 1.11477], [0.55417, 0.01449, 2.11492, 0.08191, 0.07838, 2.38535, 2.59567, -1.26342, -1.06981, -0.79691], [0.32377, 0.11651, 1.57898, 0.0271, 0.21155, 4.63407, 3.87996, -6.17162, -5.12337, -3.60569]]};
728
+ </script>
729
+ <script>
730
+ "use strict";
731
+ /* ============================ studio app ============================ */
732
+
733
+ const PALETTE = [
734
+ ["#ff6b5e",[255,107,94]], ["#5ec8ff",[94,200,255]], ["#ffd35e",[255,211,94]],
735
+ ["#7fd98a",[127,217,138]], ["#c86bff",[200,107,255]], ["#5effd0",[94,255,208]],
736
+ ["#ff8fc7",[255,143,199]], ["#b0b8ff",[176,184,255]],
737
+ ];
738
+ const OUTLIER_RGB = [120,128,143];
739
+ const RES = 192, OWN_RES = 128;
740
+ const NOISE_SCALE = [1,1,1,.3,.3,20,3,.5,.5,.5];
741
+ const HINT_W = 6.0, HINT_SIG = 0.4;
742
+
743
+ /* ------------- state ------------- */
744
+ const S = {
745
+ phase: "soup",
746
+ layers: [], // {id,name,color,rgb,template,placed,pose,hidden,depth,hints:[]}
747
+ selected: -1, // index into layers
748
+ obs: [], gt: [], // gt = placed-order index, -1 clutter
749
+ placed: [], // layer indices in placed order (BP object order)
750
+ marg: null, mu: null, Sig: null, assign: null,
751
+ evid: [], covFrac: [],
752
+ groupPre: [], groupAlpha: [], // per placed group + clutter last
753
+ ownField: null, coastT: [],
754
+ };
755
+ let nextDepth = 1, nextId = 1;
756
+
757
+ function addLayer(name, template, autoPlace) {
758
+ const [color, rgb] = PALETTE[(S.layers.length) % PALETTE.length];
759
+ const L = { id: nextId++, name, color, rgb, template, placed: false,
760
+ pose: [0,0,0,Math.log(0.5)], hidden: false, depth: 0, hints: [] };
761
+ S.layers.push(L);
762
+ if (autoPlace) placeLayer(S.layers.length - 1);
763
+ else { renderLibrary(); }
764
+ return L;
765
+ }
766
+ function placeLayer(li) {
767
+ const L = S.layers[li];
768
+ L.placed = true; L.hidden = false; L.depth = nextDepth++;
769
+ L.pose = [ -0.3 + 0.6*rand(), -0.3 + 0.6*rand(), -Math.PI + TAU*rand(),
770
+ Math.log(0.45 + 0.2*rand()) ];
771
+ rebuildScene();
772
+ }
773
+ function removeLayer(li) {
774
+ S.layers[li].placed = false;
775
+ rebuildScene();
776
+ }
777
+
778
+ /* ------------- scene construction ------------- */
779
+ let rngState = 22222;
780
+ function rand(){ rngState=(rngState*1103515245+12345)&0x7fffffff; return rngState/0x7fffffff; }
781
+
782
+ function rebuildScene() {
783
+ S.placed = S.layers.map((L,i)=>L.placed?i:-1).filter(i=>i>=0);
784
+ const noiseAmt = 0.006 * Math.pow(3, +document.getElementById("noise").value / 3);
785
+ S.obs = []; S.gt = [];
786
+ S.placed.forEach((li,k)=>{
787
+ for (const a of transformAtoms(S.layers[li].template, S.layers[li].pose)) {
788
+ S.obs.push(a); S.gt.push(k);
789
+ }
790
+ });
791
+ for (let i=0;i<12;i++){
792
+ const su=0.04+0.08*rand();
793
+ S.obs.push([-0.95+1.9*rand(),-0.95+1.9*rand(),-Math.PI/2+Math.PI*rand(),
794
+ su,su*(0.4+0.5*rand()),4+10*rand(),TAU*rand(),rand(),rand(),rand()]);
795
+ S.gt.push(-1);
796
+ }
797
+ for (const a of S.obs) for(let q=0;q<10;q++){
798
+ const g=Math.sqrt(-2*Math.log(rand()+1e-9))*Math.cos(TAU*rand());
799
+ a[q]+=g*noiseAmt*NOISE_SCALE[q];
800
+ }
801
+ for (const a of S.obs){a[3]=Math.max(a[3],.012);a[4]=Math.max(a[4],.008);a[5]=Math.max(a[5],.5);}
802
+ S.marg=null; S.mu=null; S.Sig=null; S.assign=null;
803
+ S.evid=S.obs.map(()=>true);
804
+ S.covFrac=S.placed.map(()=>0);
805
+ S.coastT=S.placed.map(()=>0);
806
+ S.ownField=null;
807
+ setPhase("soup");
808
+ rebuildGroupBuffers();
809
+ renderScene(); drawOverlay(); renderLayerList(); renderLibrary(); updatePanels();
810
+ }
811
+ function scramble(){
812
+ for (const li of S.placed){
813
+ S.layers[li].pose=[-0.35+0.7*rand(),-0.35+0.7*rand(),-Math.PI+TAU*rand(),
814
+ Math.log(0.4+0.25*rand())];
815
+ }
816
+ rebuildScene();
817
+ }
818
+
819
+ /* ------------- believed grouping (persistent through occlusion) ------------- */
820
+ function believedGroup(i){ return S.assign ? S.assign[i] : -2; }
821
+ function updateAssign(){
822
+ if(!S.marg){S.assign=null;return;}
823
+ const K=S.placed.length;
824
+ if(!S.assign) S.assign=new Array(S.obs.length).fill(-1);
825
+ for(let i=0;i<S.obs.length;i++){
826
+ if(S.assign[i]>=0 && !S.evid[i]) continue; // no evidence: identity persists
827
+ const m=S.marg[i]; let b=0;
828
+ for(let k=1;k<=K;k++) if(m[k]>m[b]) b=k;
829
+ S.assign[i]= b===K ? -1 : b;
830
+ }
831
+ }
832
+ function groupAtomIdx(k){
833
+ const out=[];
834
+ for(let i=0;i<S.obs.length;i++) if(believedGroup(i)===k) out.push(i);
835
+ return out;
836
+ }
837
+
838
+ /* ------------- rendering ------------- */
839
+ const stage=document.getElementById("stage");
840
+ const sceneC=document.getElementById("scene-canvas");
841
+ const overC=document.getElementById("overlay-canvas");
842
+ sceneC.width=sceneC.height=RES;
843
+ let CSS=600;
844
+ function fitStage(){
845
+ const wrap=document.getElementById("stage-wrap");
846
+ CSS=Math.max(280,Math.floor(Math.min(wrap.clientWidth,wrap.clientHeight)-8));
847
+ stage.style.width=stage.style.height=CSS+"px";
848
+ overC.width=overC.height=CSS*(window.devicePixelRatio||1);
849
+ drawOverlay();
850
+ }
851
+ window.addEventListener("resize",fitStage);
852
+
853
+ function rebuildGroupBuffers(){
854
+ const K=S.placed.length;
855
+ S.groupPre=[]; S.groupAlpha=[];
856
+ for(let g=0;g<=K;g++) S.groupPre.push(new Float32Array(3*RES*RES));
857
+ const groupOf=i=>{ const g=S.marg?believedGroup(i):S.gt[i]; return g>=0?g:K; };
858
+ for(let i=0;i<S.obs.length;i++){
859
+ const g=groupOf(i);
860
+ if(g<K && S.layers[S.placed[g]].hidden) continue;
861
+ renderPre([S.obs[i]],RES,S.groupPre[g]);
862
+ }
863
+ for(let g=0;g<=K;g++) S.groupAlpha.push(alphaFromPre(S.groupPre[g],RES));
864
+ }
865
+ function rebuildOneGroup(k){
866
+ S.groupPre[k]=new Float32Array(3*RES*RES);
867
+ if(!S.layers[S.placed[k]].hidden)
868
+ renderPre(groupAtomIdx(k).map(i=>S.obs[i]),RES,S.groupPre[k]);
869
+ S.groupAlpha[k]=alphaFromPre(S.groupPre[k],RES);
870
+ }
871
+ const sceneCtx=sceneC.getContext("2d");
872
+ const compositePre=new Float32Array(3*RES*RES);
873
+ const sceneImg=new ImageData(RES,RES);
874
+ function compMode(){ return document.querySelector('input[name=comp]:checked').value; }
875
+ function renderScene(){
876
+ const K=S.placed.length;
877
+ if(compMode()==="field"){
878
+ compositePre.fill(0);
879
+ for(const buf of S.groupPre)
880
+ for(let i=0;i<compositePre.length;i++) compositePre[i]+=buf[i];
881
+ sigmoidField(compositePre,RES,sceneImg.data);
882
+ } else {
883
+ // painter: clutter at the back, then placed layers by depth
884
+ const order=[K, ...S.placed.map((li,k)=>k)
885
+ .sort((a,b)=>S.layers[S.placed[a]].depth-S.layers[S.placed[b]].depth)];
886
+ compositePainter(order,S.groupPre,S.groupAlpha,RES,sceneImg.data);
887
+ }
888
+ sceneCtx.putImageData(sceneImg,0,0);
889
+ }
890
+
891
+ function w2p(x){return (x+1)/2*overC.width;}
892
+ function p2w(px){return px/overC.width*2-1;}
893
+
894
+ const ovAtoms=document.getElementById("ov-atoms"),ovOwn=document.getElementById("ov-own"),
895
+ ovEnt=document.getElementById("ov-ent"),ovPose=document.getElementById("ov-pose"),
896
+ ovHints=document.getElementById("ov-hints");
897
+ for(const el of [ovAtoms,ovOwn,ovEnt,ovPose,ovHints]) el.addEventListener("change",drawOverlay);
898
+ document.querySelectorAll('input[name=comp]').forEach(el=>
899
+ el.addEventListener("change",()=>{renderScene();}));
900
+
901
+ function drawOverlay(){
902
+ const ctx=overC.getContext("2d");
903
+ ctx.clearRect(0,0,overC.width,overC.height);
904
+ const px1=overC.width/CSS;
905
+ const K=S.placed.length;
906
+ const colOf=k=>S.layers[S.placed[k]];
907
+
908
+ if(S.ownField && (ovOwn.checked||ovEnt.checked)){
909
+ const {P,ent,support}=S.ownField;
910
+ const im=ctx.createImageData(OWN_RES,OWN_RES);
911
+ for(let i=0;i<OWN_RES*OWN_RES;i++){
912
+ if(!support[i]) continue;
913
+ let r=0,g=0,b=0,a=0;
914
+ if(ovOwn.checked){
915
+ let best=0;
916
+ for(let k=1;k<=K;k++) if(P[k][i]>P[best][i]) best=k;
917
+ const rgb=best===K?OUTLIER_RGB:colOf(best).rgb;
918
+ r=rgb[0];g=rgb[1];b=rgb[2];a=90*P[best][i];
919
+ }
920
+ if(ovEnt.checked){
921
+ const maxE=Math.log2(K+1);
922
+ const e=Math.min(1,ent[i]/maxE), ea=160*e*e;
923
+ if(ea>a){r=200;g=107;b=255;a=ea;}
924
+ }
925
+ im.data[4*i]=r;im.data[4*i+1]=g;im.data[4*i+2]=b;im.data[4*i+3]=a;
926
+ }
927
+ const tmp=document.createElement("canvas");
928
+ tmp.width=tmp.height=OWN_RES;
929
+ tmp.getContext("2d").putImageData(im,0,0);
930
+ ctx.imageSmoothingEnabled=true;
931
+ ctx.drawImage(tmp,0,0,overC.width,overC.height);
932
+ }
933
+
934
+ if(ovAtoms.checked){
935
+ for(let i=0;i<S.obs.length;i++){
936
+ const a=S.obs[i], g=believedGroup(i);
937
+ if(g>=0 && S.layers[S.placed[g]].hidden) continue;
938
+ let rgb=OUTLIER_RGB, alpha=0.5;
939
+ if(g===-2){rgb=[150,155,168];alpha=0.55;}
940
+ else if(g>=0){rgb=colOf(g).rgb;alpha=S.evid[i]?0.25+0.75*S.marg[i][g]:0.35;}
941
+ else if(S.marg){alpha=0.25+0.55*S.marg[i][K];}
942
+ const len=Math.max(3*px1,a[3]*overC.width*0.5);
943
+ const cx=w2p(a[0]),cy=w2p(a[1]);
944
+ const dx=Math.cos(a[2])*len/2,dy=Math.sin(a[2])*len/2;
945
+ ctx.strokeStyle=`rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha})`;
946
+ ctx.lineWidth=1.6*px1;
947
+ ctx.setLineDash(g>=0&&!S.evid[i]?[3*px1,3*px1]:[]);
948
+ ctx.beginPath();ctx.moveTo(cx-dx,cy-dy);ctx.lineTo(cx+dx,cy+dy);ctx.stroke();
949
+ }
950
+ ctx.setLineDash([]);
951
+ }
952
+
953
+ if(ovHints.checked){
954
+ for(let k=0;k<K;k++){
955
+ const L=colOf(k);
956
+ for(const h of L.hints){
957
+ ctx.strokeStyle=L.color; ctx.lineWidth=1.2*px1; ctx.setLineDash([2*px1,3*px1]);
958
+ ctx.beginPath();ctx.arc(w2p(h[0]),w2p(h[1]),HINT_SIG*overC.width/2,0,TAU);ctx.stroke();
959
+ ctx.setLineDash([]);
960
+ ctx.beginPath();ctx.arc(w2p(h[0]),w2p(h[1]),3*px1,0,TAU);
961
+ ctx.fillStyle=L.color;ctx.fill();
962
+ }
963
+ }
964
+ }
965
+
966
+ if(ovPose.checked && S.mu){
967
+ for(let k=0;k<K;k++){
968
+ const mu=S.mu[k],Sg=S.Sig[k],L=colOf(k);
969
+ const cx=w2p(mu[0]),cy=w2p(mu[1]);
970
+ ctx.strokeStyle=L.color;ctx.lineWidth=1.2*px1;
971
+ ctx.beginPath();
972
+ ctx.moveTo(cx-7*px1,cy);ctx.lineTo(cx+7*px1,cy);
973
+ ctx.moveTo(cx,cy-7*px1);ctx.lineTo(cx,cy+7*px1);ctx.stroke();
974
+ const Ln=26*px1*Math.exp(mu[3]);
975
+ ctx.beginPath();ctx.moveTo(cx,cy);
976
+ ctx.lineTo(cx+Math.cos(mu[2])*Ln,cy+Math.sin(mu[2])*Ln);ctx.stroke();
977
+ const coast=S.coastT[k], q=coast*coast*0.0012;
978
+ const a11=Sg[0][0]+q,a12=Sg[0][1],a22=Sg[1][1]+q;
979
+ const tr=a11+a22,det=a11*a22-a12*a12;
980
+ const disc=Math.sqrt(Math.max(0,tr*tr/4-det));
981
+ const l1=tr/2+disc,l2=Math.max(1e-12,tr/2-disc);
982
+ const ang=Math.atan2(l1-a11,a12||1e-12);
983
+ const ghost=L.hidden||S.covFrac[k]>0.7;
984
+ ctx.save();ctx.translate(cx,cy);ctx.rotate(-ang+Math.PI/2);
985
+ ctx.setLineDash(ghost?[5*px1,4*px1]:[]);
986
+ ctx.beginPath();
987
+ ctx.ellipse(0,0,2*Math.sqrt(l1)*overC.width/2,2*Math.sqrt(l2)*overC.width/2,0,0,TAU);
988
+ ctx.stroke();ctx.restore();ctx.setLineDash([]);
989
+ if(ghost){
990
+ ctx.fillStyle=L.color;
991
+ ctx.font=`${11*px1}px ${getComputedStyle(document.body).fontFamily}`;
992
+ ctx.fillText("believed, unseen",cx+10*px1,cy-10*px1);
993
+ }
994
+ }
995
+ }
996
+ }
997
+
998
+ /* ------------- semantic factor from hints ------------- */
999
+ function makeSemLL(){
1000
+ const K=S.placed.length;
1001
+ let any=false;
1002
+ for(const li of S.placed) if(S.layers[li].hints.length) any=true;
1003
+ if(!any) return null;
1004
+ return (i,k)=>{
1005
+ const hs=S.layers[S.placed[k]].hints;
1006
+ if(!hs.length) return 0;
1007
+ let best=0;
1008
+ for(const h of hs){
1009
+ const d2=(S.obs[i][0]-h[0])**2+(S.obs[i][1]-h[1])**2;
1010
+ best=Math.max(best,Math.exp(-0.5*d2/(HINT_SIG*HINT_SIG)));
1011
+ }
1012
+ return HINT_W*(best-0.5);
1013
+ };
1014
+ }
1015
+
1016
+ /* ------------- BP wiring + depth occlusion ------------- */
1017
+ function currentTemplates(){ return S.placed.map(li=>S.layers[li].template); }
1018
+
1019
+ function computeEvidence(){
1020
+ const K=S.placed.length;
1021
+ if(!document.getElementById("auto-occ").checked || !S.assign){
1022
+ S.evid=S.obs.map((_,i)=>{
1023
+ const g=believedGroup(i);
1024
+ return !(g>=0 && S.layers[S.placed[g]].hidden);
1025
+ });
1026
+ S.covFrac=S.placed.map((li,k)=>S.layers[li].hidden?1:0);
1027
+ return;
1028
+ }
1029
+ const layersMeta=S.placed.map(li=>({depth:S.layers[li].depth,hidden:S.layers[li].hidden}));
1030
+ const {mask,covFrac}=autoHidden(S.obs,S.assign,layersMeta,S.groupAlpha,RES);
1031
+ S.evid=mask.map(m=>!m);
1032
+ S.covFrac=covFrac;
1033
+ }
1034
+
1035
+ function runBind(opts={}){
1036
+ const hiddenMask=S.evid.map(e=>!e);
1037
+ const frames=[];
1038
+ const res=bpBind(currentTemplates(),S.obs,{
1039
+ iters:opts.iters??40,
1040
+ clampPose:opts.clampPose??null,
1041
+ hiddenMask,
1042
+ semLL:makeSemLL(),
1043
+ onIter:(it,marg,mu,Sig)=>{ if(opts.animate) frames.push({
1044
+ marg,mu:mu.map(m=>m.slice()),Sig:Sig.map(M=>M.map(r=>r.slice()))}); },
1045
+ });
1046
+ return {res,frames};
1047
+ }
1048
+
1049
+ function bind(animate=true){
1050
+ if(S.phase==="binding"||!S.placed.length) return;
1051
+ S.assign=null;
1052
+ S.evid=S.obs.map(()=>true); // full re-bind: everything is evidence again
1053
+ for(const li of S.placed) if(S.layers[li].hidden){
1054
+ // user-hidden layers stay evidence-free
1055
+ // (their atoms are excluded per-atom below via gt at soup time)
1056
+ }
1057
+ setPhase("binding");
1058
+ setTimeout(()=>{
1059
+ const {res,frames}=runBind({iters:40,animate});
1060
+ if(!animate){finishBind(res);return;}
1061
+ let f=0;
1062
+ const meter=document.querySelector("#bind-meter > div");
1063
+ const step=()=>{
1064
+ if(f<frames.length){
1065
+ S.marg=frames[f].marg;S.mu=frames[f].mu;S.Sig=frames[f].Sig;
1066
+ updateAssign();
1067
+ meter.style.width=(100*(f+1)/frames.length)+"%";
1068
+ document.getElementById("m-it").textContent=(f+1);
1069
+ drawOverlay();updatePanels();
1070
+ f++;requestAnimationFrame(step);
1071
+ } else finishBind(res);
1072
+ };
1073
+ step();
1074
+ },30);
1075
+ }
1076
+ function finishBind(res){
1077
+ S.marg=res.marg;S.mu=res.mu;S.Sig=res.Sig;
1078
+ updateAssign();
1079
+ rebuildGroupBuffers();
1080
+ occlusionRefresh(true);
1081
+ renderScene();refreshOwnership();
1082
+ setPhase("bound");
1083
+ drawOverlay();updatePanels();renderLayerList();
1084
+ }
1085
+
1086
+ /* recompute evidence from the depth stack; if it changed, refresh marginals
1087
+ with all poses clamped (occlusion changes evidence, not where things are) */
1088
+ function occlusionRefresh(silent){
1089
+ const before=S.evid.join("");
1090
+ computeEvidence();
1091
+ if(S.evid.join("")===before) return;
1092
+ const clamp=S.mu.map(m=>m.slice());
1093
+ const {res}=runBind({iters:8,clampPose:clamp});
1094
+ S.marg=res.marg;
1095
+ updateAssign();
1096
+ // coasting for heavily covered layers
1097
+ for(let k=0;k<S.placed.length;k++)
1098
+ S.coastT[k]=(S.covFrac[k]>0.7||S.layers[S.placed[k]].hidden)?S.coastT[k]+1:0;
1099
+ if(!silent){drawOverlay();updatePanels();renderLayerList();}
1100
+ }
1101
+
1102
+ function refreshOwnership(){
1103
+ if(!S.marg){S.ownField=null;return;}
1104
+ const K=S.placed.length;
1105
+ const vis=[],visMarg=[];
1106
+ for(let i=0;i<S.obs.length;i++){
1107
+ const g=believedGroup(i);
1108
+ if(g>=0&&S.layers[S.placed[g]].hidden) continue;
1109
+ vis.push(S.obs[i]);visMarg.push(S.marg[i]);
1110
+ }
1111
+ S.ownField=ownershipField(vis,visMarg,K,OWN_RES);
1112
+ }
1113
+
1114
+ function resettle(k,clampXi){
1115
+ const clamp=S.placed.map(()=>null);clamp[k]=clampXi;
1116
+ const {res}=runBind({iters:10,clampPose:clamp});
1117
+ S.marg=res.marg;S.mu=res.mu;S.Sig=res.Sig;
1118
+ S.mu[k]=clampXi.slice();
1119
+ updateAssign();
1120
+ rebuildGroupBuffers();
1121
+ occlusionRefresh(true);
1122
+ renderScene();refreshOwnership();
1123
+ drawOverlay();updatePanels();renderLayerList();
1124
+ document.getElementById("m-it").textContent="10 (re-settle)";
1125
+ }
1126
+
1127
+ /* ------------- interventions ------------- */
1128
+ function applyGesture(k,g){
1129
+ for(const i of groupAtomIdx(k)) S.obs[i]=transformAtoms([S.obs[i]],g)[0];
1130
+ S.mu[k]=composePose(g,S.mu[k]);
1131
+ rebuildOneGroup(k);
1132
+ renderScene();drawOverlay();updatePanels();
1133
+ }
1134
+
1135
+ /* ------------- pointer interaction ------------- */
1136
+ let drag=null;
1137
+ overC.addEventListener("pointerdown",e=>{
1138
+ const rect=overC.getBoundingClientRect();
1139
+ const wx=p2w((e.clientX-rect.left)*overC.width/rect.width);
1140
+ const wy=p2w((e.clientY-rect.top)*overC.width/rect.width);
1141
+ if(e.shiftKey){ // semantic hint for the selected layer
1142
+ const li=S.selected;
1143
+ if(li>=0&&S.layers[li].placed){
1144
+ S.layers[li].hints.push([wx,wy]);
1145
+ drawOverlay();renderLayerList();
1146
+ document.getElementById("prompt-log").textContent=
1147
+ `hint added for ${S.layers[li].name} — Bind applies it as a location prior.`;
1148
+ }
1149
+ return;
1150
+ }
1151
+ if(S.phase!=="bound") return;
1152
+ let best=-1,bd=0.12*0.12;
1153
+ for(let i=0;i<S.obs.length;i++){
1154
+ const g=believedGroup(i);
1155
+ if(g<0) continue;
1156
+ if(S.layers[S.placed[g]].hidden) continue;
1157
+ const d=(S.obs[i][0]-wx)**2+(S.obs[i][1]-wy)**2;
1158
+ if(d<bd){bd=d;best=i;}
1159
+ }
1160
+ let k=best>=0?believedGroup(best):-1;
1161
+ if(k<0) for(let kk=0;kk<S.placed.length;kk++)
1162
+ if((S.layers[S.placed[kk]].hidden||S.covFrac[kk]>0.7)&&
1163
+ (S.mu[kk][0]-wx)**2+(S.mu[kk][1]-wy)**2<0.05*0.05) k=kk;
1164
+ if(k<0) return;
1165
+ drag={k,lx:wx,ly:wy,moved:false};
1166
+ overC.classList.add("dragging");
1167
+ overC.setPointerCapture(e.pointerId);
1168
+ });
1169
+ overC.addEventListener("pointermove",e=>{
1170
+ if(!drag) return;
1171
+ const rect=overC.getBoundingClientRect();
1172
+ const wx=p2w((e.clientX-rect.left)*overC.width/rect.width);
1173
+ const wy=p2w((e.clientY-rect.top)*overC.width/rect.width);
1174
+ const dx=wx-drag.lx,dy=wy-drag.ly;
1175
+ if(dx||dy){applyGesture(drag.k,gestureTranslate(dx,dy));drag.lx=wx;drag.ly=wy;drag.moved=true;}
1176
+ });
1177
+ overC.addEventListener("pointerup",e=>{
1178
+ if(!drag) return;
1179
+ overC.classList.remove("dragging");
1180
+ if(drag.moved) resettle(drag.k,S.mu[drag.k]);
1181
+ drag=null;
1182
+ });
1183
+ overC.addEventListener("wheel",e=>{
1184
+ if(S.phase!=="bound") return;
1185
+ const rect=overC.getBoundingClientRect();
1186
+ const wx=p2w((e.clientX-rect.left)*overC.width/rect.width);
1187
+ const wy=p2w((e.clientY-rect.top)*overC.width/rect.width);
1188
+ let k=-1,bd=0.16*0.16;
1189
+ for(let kk=0;kk<S.placed.length;kk++){
1190
+ const d=(S.mu[kk][0]-wx)**2+(S.mu[kk][1]-wy)**2;
1191
+ if(d<bd){bd=d;k=kk;}
1192
+ }
1193
+ if(k<0) return;
1194
+ e.preventDefault();
1195
+ const c=[S.mu[k][0],S.mu[k][1]];
1196
+ const g=e.shiftKey?gestureScaleAbout(c,e.deltaY>0?-0.05:0.05)
1197
+ :gestureRotateAbout(c,e.deltaY>0?0.08:-0.08);
1198
+ applyGesture(k,g);
1199
+ clearTimeout(overC._wt);
1200
+ overC._wt=setTimeout(()=>resettle(k,S.mu[k]),350);
1201
+ },{passive:false});
1202
+
1203
+ /* ------------- panels ------------- */
1204
+ function setPhase(p){
1205
+ S.phase=p;
1206
+ const lamp=document.getElementById("state-lamp");
1207
+ lamp.className=p==="soup"?"soup":p==="binding"?"binding":"bound";
1208
+ lamp.textContent=p==="soup"?"ATOM SOUP":p==="binding"?"BELIEFS SETTLING":"BOUND — GRABBABLE";
1209
+ document.getElementById("btn-bind").disabled=p==="binding"||!S.placed.length;
1210
+ }
1211
+
1212
+ function renderLayerList(){
1213
+ const el=document.getElementById("layer-list");
1214
+ el.innerHTML="";
1215
+ const byDepth=[...S.placed].map((li,k)=>({li,k}))
1216
+ .sort((a,b)=>S.layers[b.li].depth-S.layers[a.li].depth);
1217
+ for(const {li,k} of byDepth){
1218
+ const L=S.layers[li];
1219
+ const idx=S.assign?groupAtomIdx(k):[];
1220
+ const conf=idx.length?idx.reduce((a,i)=>a+(S.evid[i]?S.marg[i][k]:0),0)/Math.max(1,idx.filter(i=>S.evid[i]).length):0;
1221
+ const cov=S.covFrac[k]||0;
1222
+ const d=document.createElement("div");
1223
+ d.className="layer"+(S.selected===li?" sel":"");
1224
+ d.innerHTML=`
1225
+ <div class="name" data-li="${li}">
1226
+ <span class="swatch" style="background:${L.color}"></span>
1227
+ <span>${L.name}</span>
1228
+ <span class="dim">${idx.length||L.template.length} atoms${L.hints.length?` · ${L.hints.length} hint${L.hints.length>1?"s":""}`:""}</span>
1229
+ <span class="spacer"></span>
1230
+ <div class="belief-bar"><div style="width:${100*conf}%"></div></div>
1231
+ </div>
1232
+ <div class="pose">${S.mu?`t=(${S.mu[k][0].toFixed(2)}, ${S.mu[k][1].toFixed(2)}) \u03c1=${(S.mu[k][2]*180/Math.PI).toFixed(0)}\u00b0 s=${Math.exp(S.mu[k][3]).toFixed(2)}`:"pose: unbound"}${cov>0.05?` · <span style="color:var(--entropy)">${(100*cov).toFixed(0)}% occluded${S.coastT[k]?`, coasting ${S.coastT[k]}`:""}</span>`:""}</div>
1233
+ <div class="ctl">
1234
+ <button data-li="${li}" data-op="rotl">&#8634;</button>
1235
+ <button data-li="${li}" data-op="rotr">&#8635;</button>
1236
+ <button data-li="${li}" data-op="grow">+</button>
1237
+ <button data-li="${li}" data-op="shrink">&minus;</button>
1238
+ <button data-li="${li}" data-op="front">front</button>
1239
+ <button data-li="${li}" data-op="back">back</button>
1240
+ <button data-li="${li}" data-op="hide">${L.hidden?"reveal":"hide"}</button>
1241
+ ${L.hints.length?`<button data-li="${li}" data-op="clearhints">hints&nbsp;&#10005;</button>`:""}
1242
+ <button data-li="${li}" data-op="remove">&#10005;</button>
1243
+ </div>`;
1244
+ el.appendChild(d);
1245
+ }
1246
+ if(!byDepth.length)
1247
+ el.innerHTML='<div class="dim" style="font-size:11px">no layers placed — place from the library or spawn one</div>';
1248
+ }
1249
+ document.getElementById("layer-list").addEventListener("click",e=>{
1250
+ const nm=e.target.closest(".name");
1251
+ if(nm&&!e.target.closest("button")){
1252
+ S.selected=+nm.dataset.li===S.selected?-1:+nm.dataset.li;
1253
+ renderLayerList();return;
1254
+ }
1255
+ const b=e.target.closest("button");if(!b)return;
1256
+ const li=+b.dataset.li,op=b.dataset.op;
1257
+ const k=S.placed.indexOf(li);
1258
+ if(op==="remove"){removeLayer(li);return;}
1259
+ if(op==="clearhints"){S.layers[li].hints=[];drawOverlay();renderLayerList();return;}
1260
+ if(op==="front"||op==="back"){
1261
+ S.layers[li].depth=op==="front"?nextDepth++:(Math.min(...S.placed.map(x=>S.layers[x].depth))-1);
1262
+ if(S.phase==="bound"){rebuildGroupBuffers();occlusionRefresh(false);renderScene();refreshOwnership();}
1263
+ renderLayerList();return;
1264
+ }
1265
+ if(S.phase!=="bound")return;
1266
+ if(op==="hide"){
1267
+ S.layers[li].hidden=!S.layers[li].hidden;
1268
+ rebuildGroupBuffers();occlusionRefresh(false);renderScene();refreshOwnership();drawOverlay();renderLayerList();
1269
+ return;
1270
+ }
1271
+ const c=[S.mu[k][0],S.mu[k][1]];
1272
+ if(op==="rotl"){applyGesture(k,gestureRotateAbout(c,-Math.PI/12));resettle(k,S.mu[k]);}
1273
+ if(op==="rotr"){applyGesture(k,gestureRotateAbout(c,Math.PI/12));resettle(k,S.mu[k]);}
1274
+ if(op==="grow"){applyGesture(k,gestureScaleAbout(c,0.12));resettle(k,S.mu[k]);}
1275
+ if(op==="shrink"){applyGesture(k,gestureScaleAbout(c,-0.12));resettle(k,S.mu[k]);}
1276
+ });
1277
+
1278
+ function renderLibrary(){
1279
+ const el=document.getElementById("library");
1280
+ el.innerHTML="";
1281
+ S.layers.forEach((L,li)=>{
1282
+ if(L.placed) return;
1283
+ const c=document.createElement("span");
1284
+ c.className="chip";
1285
+ c.innerHTML=`<span class="swatch" style="background:${L.color}"></span>${L.name}
1286
+ <span class="dim">${L.template.length}</span>
1287
+ <button data-li="${li}" data-op="place" title="place">+</button>`;
1288
+ el.appendChild(c);
1289
+ });
1290
+ }
1291
+ document.getElementById("library").addEventListener("click",e=>{
1292
+ const b=e.target.closest("button");if(!b)return;
1293
+ placeLayer(+b.dataset.li);
1294
+ });
1295
+
1296
+ function updatePanels(){
1297
+ document.getElementById("m-n").textContent=S.obs.length;
1298
+ const K=S.placed.length;
1299
+ if(!S.marg){
1300
+ document.getElementById("m-acc").textContent="—";
1301
+ document.getElementById("m-ent").textContent="—";
1302
+ return;
1303
+ }
1304
+ let correct=0;
1305
+ for(let i=0;i<S.gt.length;i++) if(believedGroup(i)===S.gt[i]) correct++;
1306
+ document.getElementById("m-acc").textContent=(100*correct/S.gt.length).toFixed(1)+"%";
1307
+ const ent=atomEntropy(S.marg);
1308
+ document.getElementById("m-ent").textContent=
1309
+ (ent.reduce((a,x)=>a+x,0)/ent.length).toFixed(2)+" bits";
1310
+ }
1311
+
1312
+ /* ------------- spawn: server endpoints ------------- */
1313
+ const spawnStatus=document.getElementById("spawn-status");
1314
+ async function gradioCall(name,data){
1315
+ const base=location.pathname.includes("/studio/")?"..":"";
1316
+ const r=await fetch(`${base}/gradio_api/call/${name}`,{
1317
+ method:"POST",headers:{"Content-Type":"application/json"},
1318
+ body:JSON.stringify({data})});
1319
+ const {event_id}=await r.json();
1320
+ const resp=await fetch(`${base}/gradio_api/call/${name}/${event_id}`);
1321
+ const text=await resp.text();
1322
+ const lines=text.split("\n").filter(l=>l.startsWith("data:"));
1323
+ if(!lines.length) throw new Error("no data from server");
1324
+ const payload=JSON.parse(lines[lines.length-1].slice(5));
1325
+ if(payload==null||payload.error) throw new Error(payload?.error||"server error");
1326
+ return JSON.parse(payload[0]);
1327
+ }
1328
+ function addGenerated(layer,fallbackName){
1329
+ const name=(layer.ledger&&layer.ledger.prompt)
1330
+ ? (layer.ledger.attention&&layer.ledger.attention.token!=="(all content tokens)"
1331
+ ? layer.ledger.attention.token : layer.ledger.prompt.split(/[ ,]/)[1]||fallbackName)
1332
+ : fallbackName;
1333
+ addLayer(name,layer.atoms,true);
1334
+ spawnStatus.textContent=`layer "${name}": ${layer.atoms.length} atoms · ${layer.ledger.status}`+
1335
+ (layer.ledger.attention?` · matte kept ${layer.ledger.attention.atoms_after_matte}/${layer.ledger.attention.atoms_before_matte}`:"");
1336
+ }
1337
+ document.getElementById("btn-spawn-text").addEventListener("click",async()=>{
1338
+ const prompt=document.getElementById("spawn-prompt").value.trim();
1339
+ const token=document.getElementById("spawn-token").value.trim();
1340
+ if(!prompt){spawnStatus.textContent="write a prompt first";return;}
1341
+ spawnStatus.textContent="distilling on the server (GPU, minutes)\u2026";
1342
+ try{
1343
+ const layer=await gradioCall("layer_from_text",[prompt,"",192,900,30,token,0.35]);
1344
+ addGenerated(layer,"generated");
1345
+ }catch(err){spawnStatus.textContent="distill failed: "+err.message+
1346
+ " (needs a GPU Space; on CPU use Fit or paste JSON)";}
1347
+ });
1348
+ document.getElementById("btn-spawn-img").addEventListener("click",async()=>{
1349
+ const f=document.getElementById("spawn-file").files[0];
1350
+ if(!f){spawnStatus.textContent="choose an image first";return;}
1351
+ spawnStatus.textContent="fitting atoms to the image (CPU, ~1 min)\u2026";
1352
+ try{
1353
+ const b64=await new Promise((res,rej)=>{
1354
+ const img=new Image();
1355
+ img.onload=()=>{
1356
+ const c=document.createElement("canvas");
1357
+ const s=Math.min(1,160/Math.max(img.width,img.height));
1358
+ c.width=Math.round(img.width*s);c.height=Math.round(img.height*s);
1359
+ c.getContext("2d").drawImage(img,0,0,c.width,c.height);
1360
+ res(c.toDataURL("image/png").split(",")[1]);
1361
+ };
1362
+ img.onerror=rej;
1363
+ img.src=URL.createObjectURL(f);
1364
+ });
1365
+ const layer=await gradioCall("layer_from_image_b64",[b64,140,400]);
1366
+ addGenerated(layer,f.name.replace(/\.[^.]+$/,""));
1367
+ }catch(err){spawnStatus.textContent="fit failed: "+err.message+
1368
+ " (is this page running from the Space? standalone files can only paste JSON)";}
1369
+ });
1370
+ document.getElementById("btn-spawn-json").addEventListener("click",()=>{
1371
+ try{
1372
+ const raw=JSON.parse(document.getElementById("spawn-json").value);
1373
+ const atoms=raw.atoms||raw;
1374
+ if(!Array.isArray(atoms)||atoms[0].length!==10) throw new Error("expect [[10 fields]...]");
1375
+ addLayer("pasted-"+nextId,atoms,true);
1376
+ spawnStatus.textContent=`pasted layer: ${atoms.length} atoms`;
1377
+ document.getElementById("spawn-json").value="";
1378
+ }catch(err){spawnStatus.textContent="bad JSON: "+err.message;}
1379
+ });
1380
+
1381
+ /* ------------- prompt as intervention ------------- */
1382
+ const promptLog=document.getElementById("prompt-log");
1383
+ function layerByName(part){
1384
+ for(let k=0;k<S.placed.length;k++)
1385
+ if(part.includes(S.layers[S.placed[k]].name.toLowerCase())) return k;
1386
+ return -1;
1387
+ }
1388
+ function localParse(text){
1389
+ const t=text.toLowerCase(),ops=[];
1390
+ for(const part of t.split(/,|;| and /)){
1391
+ const k=layerByName(part);
1392
+ if(k<0) continue;
1393
+ if(/hide|occlude/.test(part)){ops.push({op:"hide",k});continue;}
1394
+ if(/show|reveal|unhide/.test(part)){ops.push({op:"show",k});continue;}
1395
+ if(/front|top|forward/.test(part)&&/bring|put|move|send/.test(part)){ops.push({op:"front",k});continue;}
1396
+ if(/back|behind|bottom/.test(part)&&/send|put|move|push/.test(part)){ops.push({op:"back",k});continue;}
1397
+ const deg=part.match(/(-?\d+)\s*(deg|\u00b0)?/);
1398
+ if(/rotate|turn|spin/.test(part))
1399
+ ops.push({op:"rotate",k,deg:deg?+deg[1]:30*(/left|counter|ccw/.test(part)?-1:1)});
1400
+ if(/scale|grow|bigger|larger|shrink|smaller/.test(part))
1401
+ ops.push({op:"scale",k,ds:/shrink|smaller/.test(part)?-0.15:0.15});
1402
+ if(/move|push|slide|put|drag/.test(part)){
1403
+ let dx=0,dy=0;const step=0.22;
1404
+ if(/left/.test(part))dx-=step;
1405
+ if(/right/.test(part))dx+=step;
1406
+ if(/up|top/.test(part))dy-=step;
1407
+ if(/down|bottom/.test(part))dy+=step;
1408
+ if(/center|middle/.test(part))ops.push({op:"moveto",k,x:0,y:0});
1409
+ else if(dx||dy)ops.push({op:"move",k,dx,dy});
1410
+ }
1411
+ }
1412
+ return ops;
1413
+ }
1414
+ async function claudeParse(text){
1415
+ const names=S.placed.map(li=>S.layers[li].name);
1416
+ const sceneDesc=S.placed.map((li,k)=>
1417
+ `${S.layers[li].name}: t=(${S.mu[k][0].toFixed(2)},${S.mu[k][1].toFixed(2)}) rho=${S.mu[k][2].toFixed(2)} scale=${Math.exp(S.mu[k][3]).toFixed(2)} depth=${S.layers[li].depth} hidden=${S.layers[li].hidden}`).join("; ");
1418
+ const resp=await fetch("https://api.anthropic.com/v1/messages",{
1419
+ method:"POST",headers:{"Content-Type":"application/json"},
1420
+ body:JSON.stringify({model:"claude-sonnet-4-6",max_tokens:1000,
1421
+ messages:[{role:"user",content:
1422
+ `You control a probabilistic layered scene editor. Coordinates x,y in [-1,1], y grows DOWNWARD. Higher depth = in front. Layers: ${sceneDesc}.
1423
+ User command: "${text}"
1424
+ Respond ONLY with a JSON array (no prose, no fences) of ops:
1425
+ {"op":"move","object":<name>,"dx":n,"dy":n} {"op":"moveto","object":<name>,"x":n,"y":n}
1426
+ {"op":"rotate","object":<name>,"deg":n} {"op":"scale","object":<name>,"factor":n}
1427
+ {"op":"front","object":<name>} {"op":"back","object":<name>}
1428
+ {"op":"hide","object":<name>} {"op":"show","object":<name>}
1429
+ Valid names: ${JSON.stringify(names)}`}]})});
1430
+ const data=await resp.json();
1431
+ const txt=data.content.filter(c=>c.type==="text").map(c=>c.text).join("");
1432
+ const arr=JSON.parse(txt.replace(/```json|```/g,"").trim());
1433
+ return arr.map(o=>{
1434
+ const k=S.placed.findIndex(li=>S.layers[li].name===o.object);
1435
+ if(k<0)return null;
1436
+ if(o.op==="move")return{op:"move",k,dx:o.dx,dy:o.dy};
1437
+ if(o.op==="moveto")return{op:"moveto",k,x:o.x,y:o.y};
1438
+ if(o.op==="rotate")return{op:"rotate",k,deg:o.deg};
1439
+ if(o.op==="scale")return{op:"scale",k,ds:Math.log(o.factor)};
1440
+ return{op:o.op,k};
1441
+ }).filter(Boolean);
1442
+ }
1443
+ function execOps(ops){
1444
+ if(!ops.length){promptLog.textContent="Parsed no intervention. Layers: "+
1445
+ S.placed.map(li=>S.layers[li].name).join(", ")+". Verbs: move/rotate/scale/hide/show/front/back.";return;}
1446
+ const touched=new Set();
1447
+ for(const o of ops){
1448
+ const li=S.placed[o.k];
1449
+ if(o.op==="hide"&&!S.layers[li].hidden){S.layers[li].hidden=true;continue;}
1450
+ if(o.op==="show"&&S.layers[li].hidden){S.layers[li].hidden=false;continue;}
1451
+ if(o.op==="front"){S.layers[li].depth=nextDepth++;continue;}
1452
+ if(o.op==="back"){S.layers[li].depth=Math.min(...S.placed.map(x=>S.layers[x].depth))-1;continue;}
1453
+ const c=[S.mu[o.k][0],S.mu[o.k][1]];
1454
+ if(o.op==="move"){applyGesture(o.k,gestureTranslate(o.dx,o.dy));touched.add(o.k);}
1455
+ if(o.op==="moveto"){applyGesture(o.k,gestureTranslate(o.x-c[0],o.y-c[1]));touched.add(o.k);}
1456
+ if(o.op==="rotate"){applyGesture(o.k,gestureRotateAbout(c,o.deg*Math.PI/180));touched.add(o.k);}
1457
+ if(o.op==="scale"){applyGesture(o.k,gestureScaleAbout(c,o.ds));touched.add(o.k);}
1458
+ }
1459
+ for(const k of touched) resettle(k,S.mu[k]);
1460
+ rebuildGroupBuffers();occlusionRefresh(true);renderScene();refreshOwnership();
1461
+ drawOverlay();updatePanels();renderLayerList();
1462
+ promptLog.textContent="Clamped: "+ops.map(o=>`${o.op} ${S.layers[S.placed[o.k]].name}`).join(", ")+
1463
+ " — beliefs re-settled around the intervention.";
1464
+ }
1465
+ async function doPrompt(){
1466
+ if(S.phase!=="bound"){promptLog.textContent="Bind first — there is nothing to address in a soup.";return;}
1467
+ const text=document.getElementById("prompt").value.trim();
1468
+ if(!text)return;
1469
+ let ops=localParse(text);
1470
+ if(!ops.length){
1471
+ promptLog.textContent="Local grammar found nothing; asking Claude\u2026";
1472
+ try{ops=await claudeParse(text);}
1473
+ catch{promptLog.textContent="Claude unavailable outside claude.ai — try: move/rotate/scale/hide/front/back + a layer name.";return;}
1474
+ }
1475
+ execOps(ops);
1476
+ }
1477
+ document.getElementById("btn-prompt").addEventListener("click",doPrompt);
1478
+ document.getElementById("prompt").addEventListener("keydown",e=>{if(e.key==="Enter")doPrompt();});
1479
+ document.getElementById("auto-occ").addEventListener("change",()=>{
1480
+ if(S.phase==="bound"){occlusionRefresh(false);renderScene();}
1481
+ });
1482
+
1483
+ /* ------------- boot ------------- */
1484
+ document.getElementById("btn-scramble").addEventListener("click",scramble);
1485
+ document.getElementById("btn-bind").addEventListener("click",()=>bind(true));
1486
+ addLayer("tractor",BUILTINS.tractor,false);
1487
+ addLayer("star",BUILTINS.star,false);
1488
+ addLayer("boat",BUILTINS.boat,false);
1489
+ S.layers[0].placed=true;S.layers[0].depth=nextDepth++;S.layers[0].pose=[0.3,-0.25,0.3,Math.log(0.55)];
1490
+ S.layers[1].placed=true;S.layers[1].depth=nextDepth++;S.layers[1].pose=[-0.4,0.35,-0.9,Math.log(0.5)];
1491
+ fitStage();
1492
+ rebuildScene();
1493
+ </script>
1494
+ </body>
1495
+ </html>
studio/studio_core.js ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Slapstack Studio layer math — NEW mechanics on top of the verified core.
2
+ core.js is untouched; everything here is presentation-adjacent inference
3
+ plumbing, tested by tests_studio.js:
4
+ renderEnv per-layer envelope-energy buffer (for alpha + coverage)
5
+ coverageOf how much front-stack mass sits on each atom center
6
+ autoHidden coverage -> per-atom evidence mask (depth occlusion)
7
+ compositePainter back-to-front alpha compositing of layer buffers
8
+ composePose Sim(2) composition in vote coordinates [tx,ty,rho,lam]
9
+ */
10
+ "use strict";
11
+
12
+ const ALPHA_C = 2.0; // alpha = 1 - exp(-ALPHA_C * bounded envelope energy)
13
+ const COVER_THRESH = 0.55; // atom loses evidence when this covered
14
+
15
+ /* Opacity of a layer = where it actually PAINTS: per-pixel deviation of
16
+ the pre-sigmoid field from neutral. Cancelling atom pairs (high energy,
17
+ zero sum) are correctly transparent — envelope-based opacity is not used
18
+ because it saturates on exactly those invisible pairs.
19
+ a = 1 - exp(-ALPHA_C * max(0, |preR|+|preG|+|preB| - FLOOR)). */
20
+ const ALPHA_FLOOR = 0.08;
21
+ function alphaFromPre(pre, H, out) {
22
+ out = out || new Float32Array(H * H);
23
+ const n = H * H;
24
+ for (let i = 0; i < n; i++) {
25
+ const m = Math.abs(pre[i]) + Math.abs(pre[n + i]) + Math.abs(pre[2 * n + i]);
26
+ out[i] = m > ALPHA_FLOOR ? 1 - Math.exp(-ALPHA_C * (m - ALPHA_FLOOR)) : 0;
27
+ }
28
+ return out;
29
+ }
30
+
31
+ /* coverage of each atom center by a front stack of alpha buffers:
32
+ cov_i = 1 - prod_front (1 - a_m(x_i)). */
33
+ function coverageOf(atoms, frontAlphaBufs, H) {
34
+ const out = new Float32Array(atoms.length);
35
+ for (let i = 0; i < atoms.length; i++) {
36
+ const px = Math.max(0, Math.min(H - 1, Math.round((atoms[i][0] + 1) / 2 * (H - 1))));
37
+ const py = Math.max(0, Math.min(H - 1, Math.round((atoms[i][1] + 1) / 2 * (H - 1))));
38
+ let keep = 1;
39
+ for (const buf of frontAlphaBufs) keep *= 1 - buf[py * H + px];
40
+ out[i] = 1 - keep;
41
+ }
42
+ return out;
43
+ }
44
+
45
+ /* Per-atom evidence mask from the depth stack.
46
+ alphaBufs aligned with layers. An atom of layer k is evidence-free if the
47
+ strictly-in-front stack covers it beyond COVER_THRESH, or its layer is
48
+ user-hidden. */
49
+ function autoHidden(obs, layerOf, layers, alphaBufs, H) {
50
+ const mask = new Array(obs.length).fill(false);
51
+ const covFrac = layers.map(() => 0);
52
+ const counts = layers.map(() => 0);
53
+ for (let i = 0; i < obs.length; i++) {
54
+ const k = layerOf[i];
55
+ if (k < 0) continue; // clutter: always evidenced
56
+ counts[k]++;
57
+ if (layers[k].hidden) { mask[i] = true; covFrac[k]++; continue; }
58
+ const front = [];
59
+ for (let m = 0; m < layers.length; m++)
60
+ if (m !== k && !layers[m].hidden && layers[m].depth > layers[k].depth)
61
+ front.push(alphaBufs[m]);
62
+ if (!front.length) continue;
63
+ const cov = coverageOf([obs[i]], front, H)[0];
64
+ if (cov > COVER_THRESH) { mask[i] = true; covFrac[k]++; }
65
+ }
66
+ for (let k = 0; k < layers.length; k++)
67
+ covFrac[k] = counts[k] ? covFrac[k] / counts[k] : 0;
68
+ return { mask, covFrac };
69
+ }
70
+
71
+ /* Painter compositing: layers back-to-front by depth.
72
+ rgbBufs[k]: Float32Array(3*H*H) pre-sigmoid; alphaBufs[k]: from
73
+ alphaFromPre. Base is mid-gray (sigmoid(0)). */
74
+ function compositePainter(order, rgbBufs, alphaBufs, H, out) {
75
+ out = out || new Uint8ClampedArray(4 * H * H);
76
+ const n = H * H;
77
+ const acc = new Float32Array(3 * n);
78
+ for (let i = 0; i < n; i++) { acc[i] = 127.5; acc[n + i] = 127.5; acc[2 * n + i] = 127.5; }
79
+ for (const k of order) {
80
+ const pre = rgbBufs[k], al = alphaBufs[k];
81
+ for (let i = 0; i < n; i++) {
82
+ const a = al[i];
83
+ if (a < 1e-3) continue;
84
+ acc[i] += a * (255 / (1 + Math.exp(-2 * pre[i])) - acc[i]);
85
+ acc[n + i] += a * (255 / (1 + Math.exp(-2 * pre[n + i])) - acc[n + i]);
86
+ acc[2 * n + i] += a * (255 / (1 + Math.exp(-2 * pre[2 * n + i])) - acc[2 * n + i]);
87
+ }
88
+ }
89
+ for (let i = 0; i < n; i++) {
90
+ out[4 * i] = acc[i]; out[4 * i + 1] = acc[n + i];
91
+ out[4 * i + 2] = acc[2 * n + i]; out[4 * i + 3] = 255;
92
+ }
93
+ return out;
94
+ }
95
+
96
+ /* Backdrop trim: whole-image fits carry large-sigma background pads that
97
+ cover the full frame; for OBJECT layers, drop atoms whose envelope is
98
+ wider than sigMax. HEURISTIC, honestly: it also kills any genuinely
99
+ large object parts. The untrimmed layer is kept in the library. */
100
+ function trimAtoms(atoms, sigMax = 0.35) {
101
+ const kept = atoms.filter(a => Math.max(a[3], a[4]) <= sigMax);
102
+ if (!kept.length) return atoms.slice();
103
+ // re-center xy so the trimmed set is a canonical template again
104
+ let mx = 0, my = 0;
105
+ for (const a of kept) { mx += a[0]; my += a[1]; }
106
+ mx /= kept.length; my /= kept.length;
107
+ return kept.map(a => { const b = a.slice(); b[0] -= mx; b[1] -= my; return b; });
108
+ }
109
+
110
+ /* Sim(2) composition in vote coordinates: (g2 ∘ g1). */
111
+ function composePose(g2, g1) {
112
+ const s2 = Math.exp(g2[3]);
113
+ const c = Math.cos(g2[2]), s = Math.sin(g2[2]);
114
+ const wrap = d => ((d + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) - Math.PI;
115
+ return [s2 * (c * g1[0] - s * g1[1]) + g2[0],
116
+ s2 * (s * g1[0] + c * g1[1]) + g2[1],
117
+ wrap(g1[2] + g2[2]), g1[3] + g2[3]];
118
+ }
119
+ function gestureTranslate(dx, dy) { return [dx, dy, 0, 0]; }
120
+ function gestureRotateAbout(c, drho) {
121
+ const co = Math.cos(drho), si = Math.sin(drho);
122
+ return [c[0] - (co * c[0] - si * c[1]), c[1] - (si * c[0] + co * c[1]), drho, 0];
123
+ }
124
+ function gestureScaleAbout(c, ds) {
125
+ const s = Math.exp(ds);
126
+ return [c[0] - s * c[0], c[1] - s * c[1], 0, ds];
127
+ }
128
+
129
+ if (typeof module !== "undefined") {
130
+ module.exports = {
131
+ ALPHA_C, COVER_THRESH, alphaFromPre, coverageOf, autoHidden,
132
+ compositePainter, composePose, gestureTranslate, gestureRotateAbout,
133
+ gestureScaleAbout, trimAtoms,
134
+ };
135
+ }
studio_core.js ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Slapstack Studio layer math — NEW mechanics on top of the verified core.
2
+ core.js is untouched; everything here is presentation-adjacent inference
3
+ plumbing, tested by tests_studio.js:
4
+ renderEnv per-layer envelope-energy buffer (for alpha + coverage)
5
+ coverageOf how much front-stack mass sits on each atom center
6
+ autoHidden coverage -> per-atom evidence mask (depth occlusion)
7
+ compositePainter back-to-front alpha compositing of layer buffers
8
+ composePose Sim(2) composition in vote coordinates [tx,ty,rho,lam]
9
+ */
10
+ "use strict";
11
+
12
+ const ALPHA_C = 2.0; // alpha = 1 - exp(-ALPHA_C * bounded envelope energy)
13
+ const COVER_THRESH = 0.55; // atom loses evidence when this covered
14
+
15
+ /* Opacity of a layer = where it actually PAINTS: per-pixel deviation of
16
+ the pre-sigmoid field from neutral. Cancelling atom pairs (high energy,
17
+ zero sum) are correctly transparent — envelope-based opacity is not used
18
+ because it saturates on exactly those invisible pairs.
19
+ a = 1 - exp(-ALPHA_C * max(0, |preR|+|preG|+|preB| - FLOOR)). */
20
+ const ALPHA_FLOOR = 0.08;
21
+ function alphaFromPre(pre, H, out) {
22
+ out = out || new Float32Array(H * H);
23
+ const n = H * H;
24
+ for (let i = 0; i < n; i++) {
25
+ const m = Math.abs(pre[i]) + Math.abs(pre[n + i]) + Math.abs(pre[2 * n + i]);
26
+ out[i] = m > ALPHA_FLOOR ? 1 - Math.exp(-ALPHA_C * (m - ALPHA_FLOOR)) : 0;
27
+ }
28
+ return out;
29
+ }
30
+
31
+ /* coverage of each atom center by a front stack of alpha buffers:
32
+ cov_i = 1 - prod_front (1 - a_m(x_i)). */
33
+ function coverageOf(atoms, frontAlphaBufs, H) {
34
+ const out = new Float32Array(atoms.length);
35
+ for (let i = 0; i < atoms.length; i++) {
36
+ const px = Math.max(0, Math.min(H - 1, Math.round((atoms[i][0] + 1) / 2 * (H - 1))));
37
+ const py = Math.max(0, Math.min(H - 1, Math.round((atoms[i][1] + 1) / 2 * (H - 1))));
38
+ let keep = 1;
39
+ for (const buf of frontAlphaBufs) keep *= 1 - buf[py * H + px];
40
+ out[i] = 1 - keep;
41
+ }
42
+ return out;
43
+ }
44
+
45
+ /* Per-atom evidence mask from the depth stack.
46
+ alphaBufs aligned with layers. An atom of layer k is evidence-free if the
47
+ strictly-in-front stack covers it beyond COVER_THRESH, or its layer is
48
+ user-hidden. */
49
+ function autoHidden(obs, layerOf, layers, alphaBufs, H) {
50
+ const mask = new Array(obs.length).fill(false);
51
+ const covFrac = layers.map(() => 0);
52
+ const counts = layers.map(() => 0);
53
+ for (let i = 0; i < obs.length; i++) {
54
+ const k = layerOf[i];
55
+ if (k < 0) continue; // clutter: always evidenced
56
+ counts[k]++;
57
+ if (layers[k].hidden) { mask[i] = true; covFrac[k]++; continue; }
58
+ const front = [];
59
+ for (let m = 0; m < layers.length; m++)
60
+ if (m !== k && !layers[m].hidden && layers[m].depth > layers[k].depth)
61
+ front.push(alphaBufs[m]);
62
+ if (!front.length) continue;
63
+ const cov = coverageOf([obs[i]], front, H)[0];
64
+ if (cov > COVER_THRESH) { mask[i] = true; covFrac[k]++; }
65
+ }
66
+ for (let k = 0; k < layers.length; k++)
67
+ covFrac[k] = counts[k] ? covFrac[k] / counts[k] : 0;
68
+ return { mask, covFrac };
69
+ }
70
+
71
+ /* Painter compositing: layers back-to-front by depth.
72
+ rgbBufs[k]: Float32Array(3*H*H) pre-sigmoid; alphaBufs[k]: from
73
+ alphaFromPre. Base is mid-gray (sigmoid(0)). */
74
+ function compositePainter(order, rgbBufs, alphaBufs, H, out) {
75
+ out = out || new Uint8ClampedArray(4 * H * H);
76
+ const n = H * H;
77
+ const acc = new Float32Array(3 * n);
78
+ for (let i = 0; i < n; i++) { acc[i] = 127.5; acc[n + i] = 127.5; acc[2 * n + i] = 127.5; }
79
+ for (const k of order) {
80
+ const pre = rgbBufs[k], al = alphaBufs[k];
81
+ for (let i = 0; i < n; i++) {
82
+ const a = al[i];
83
+ if (a < 1e-3) continue;
84
+ acc[i] += a * (255 / (1 + Math.exp(-2 * pre[i])) - acc[i]);
85
+ acc[n + i] += a * (255 / (1 + Math.exp(-2 * pre[n + i])) - acc[n + i]);
86
+ acc[2 * n + i] += a * (255 / (1 + Math.exp(-2 * pre[2 * n + i])) - acc[2 * n + i]);
87
+ }
88
+ }
89
+ for (let i = 0; i < n; i++) {
90
+ out[4 * i] = acc[i]; out[4 * i + 1] = acc[n + i];
91
+ out[4 * i + 2] = acc[2 * n + i]; out[4 * i + 3] = 255;
92
+ }
93
+ return out;
94
+ }
95
+
96
+ /* Backdrop trim: whole-image fits carry large-sigma background pads that
97
+ cover the full frame; for OBJECT layers, drop atoms whose envelope is
98
+ wider than sigMax. HEURISTIC, honestly: it also kills any genuinely
99
+ large object parts. The untrimmed layer is kept in the library. */
100
+ function trimAtoms(atoms, sigMax = 0.35) {
101
+ const kept = atoms.filter(a => Math.max(a[3], a[4]) <= sigMax);
102
+ if (!kept.length) return atoms.slice();
103
+ // re-center xy so the trimmed set is a canonical template again
104
+ let mx = 0, my = 0;
105
+ for (const a of kept) { mx += a[0]; my += a[1]; }
106
+ mx /= kept.length; my /= kept.length;
107
+ return kept.map(a => { const b = a.slice(); b[0] -= mx; b[1] -= my; return b; });
108
+ }
109
+
110
+ /* Sim(2) composition in vote coordinates: (g2 ∘ g1). */
111
+ function composePose(g2, g1) {
112
+ const s2 = Math.exp(g2[3]);
113
+ const c = Math.cos(g2[2]), s = Math.sin(g2[2]);
114
+ const wrap = d => ((d + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) - Math.PI;
115
+ return [s2 * (c * g1[0] - s * g1[1]) + g2[0],
116
+ s2 * (s * g1[0] + c * g1[1]) + g2[1],
117
+ wrap(g1[2] + g2[2]), g1[3] + g2[3]];
118
+ }
119
+ function gestureTranslate(dx, dy) { return [dx, dy, 0, 0]; }
120
+ function gestureRotateAbout(c, drho) {
121
+ const co = Math.cos(drho), si = Math.sin(drho);
122
+ return [c[0] - (co * c[0] - si * c[1]), c[1] - (si * c[0] + co * c[1]), drho, 0];
123
+ }
124
+ function gestureScaleAbout(c, ds) {
125
+ const s = Math.exp(ds);
126
+ return [c[0] - s * c[0], c[1] - s * c[1], 0, ds];
127
+ }
128
+
129
+ if (typeof module !== "undefined") {
130
+ module.exports = {
131
+ ALPHA_C, COVER_THRESH, alphaFromPre, coverageOf, autoHidden,
132
+ compositePainter, composePose, gestureTranslate, gestureRotateAbout,
133
+ gestureScaleAbout, trimAtoms,
134
+ };
135
+ }
studio_ui.html ADDED
@@ -0,0 +1,962 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Slapstack Studio — spawn objects, move them, in Gabor space</title>
7
+ <style>
8
+ :root {
9
+ --chassis:#1d222e; --panel:#262c3b; --panel-edge:#333b4f; --well:#10131b;
10
+ --ink:#e3e7f0; --muted:#8d96ab; --phosphor:#ffb454; --outlier:#78808f;
11
+ --entropy:#c86bff; --go:#7fd98a; --bad:#ff7a7a;
12
+ --mono: ui-monospace,"SF Mono","Cascadia Mono",Menlo,Consolas,monospace;
13
+ --cond:"Bahnschrift","Arial Narrow","Roboto Condensed",sans-serif;
14
+ }
15
+ *{box-sizing:border-box;margin:0}
16
+ html,body{height:100%}
17
+ body{background:var(--chassis);color:var(--ink);font-family:var(--mono);
18
+ font-size:13px;display:flex;flex-direction:column;overflow:hidden}
19
+ @media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
20
+ header{display:flex;align-items:baseline;gap:14px;padding:10px 16px 9px;
21
+ border-bottom:1px solid var(--panel-edge);flex-wrap:wrap}
22
+ header h1{font-family:var(--cond);font-size:20px;letter-spacing:2.5px;font-weight:700}
23
+ header h1 em{color:var(--phosphor);font-style:normal}
24
+ header .claim{color:var(--muted);font-size:11.5px}
25
+ #state-lamp{margin-left:auto;font-size:11px;letter-spacing:1.5px;padding:3px 10px;
26
+ border-radius:3px;border:1px solid var(--panel-edge);color:var(--muted)}
27
+ #state-lamp.soup{color:var(--outlier);border-color:var(--outlier)}
28
+ #state-lamp.binding{color:var(--entropy);border-color:var(--entropy)}
29
+ #state-lamp.bound{color:var(--go);border-color:var(--go)}
30
+ main{flex:1;display:flex;gap:12px;padding:12px 16px;min-height:0}
31
+ #stage-wrap{flex:1;display:flex;align-items:center;justify-content:center;min-width:0}
32
+ #stage{position:relative;background:var(--well);border:1px solid var(--panel-edge);
33
+ border-radius:6px;box-shadow:inset 0 0 60px rgba(0,0,0,.55)}
34
+ #stage canvas{position:absolute;inset:0;width:100%;height:100%;border-radius:6px}
35
+ #overlay-canvas{cursor:grab}
36
+ #overlay-canvas.dragging{cursor:grabbing}
37
+ #stage .hint{position:absolute;bottom:7px;left:0;right:0;text-align:center;
38
+ color:var(--muted);font-size:10.5px;pointer-events:none;text-shadow:0 1px 3px #000}
39
+ aside{width:330px;flex-shrink:0;display:flex;flex-direction:column;gap:9px;
40
+ overflow-y:auto;padding-right:2px}
41
+ .card{background:var(--panel);border:1px solid var(--panel-edge);border-radius:6px;
42
+ padding:10px 11px}
43
+ .card h2{font-family:var(--cond);font-size:12px;letter-spacing:2px;font-weight:700;
44
+ color:var(--muted);margin-bottom:8px}
45
+ button{font-family:var(--mono);font-size:12px;background:var(--well);color:var(--ink);
46
+ border:1px solid var(--panel-edge);border-radius:4px;padding:6px 11px;cursor:pointer}
47
+ button:hover{border-color:var(--phosphor);color:var(--phosphor)}
48
+ button:focus-visible{outline:2px solid var(--phosphor);outline-offset:1px}
49
+ button:disabled{opacity:.35;cursor:default}
50
+ button:disabled:hover{border-color:var(--panel-edge);color:var(--ink)}
51
+ button.primary{border-color:var(--phosphor);color:var(--phosphor)}
52
+ .row{display:flex;gap:7px;align-items:center}
53
+ .row+.row{margin-top:7px}
54
+ .readout{color:var(--phosphor)} .dim{color:var(--muted)} .spacer{flex:1}
55
+ #bind-meter{height:4px;background:var(--well);border-radius:2px;overflow:hidden;margin-top:8px}
56
+ #bind-meter>div{height:100%;width:0%;background:var(--entropy)}
57
+ #metrics{display:grid;grid-template-columns:1fr 1fr;gap:3px 10px;margin-top:8px;font-size:11.5px}
58
+ .layer{border-top:1px solid var(--panel-edge);padding:7px 0;border-radius:3px}
59
+ .layer:first-of-type{border-top:0}
60
+ .layer.sel{outline:1px solid var(--phosphor);outline-offset:2px}
61
+ .layer .name{display:flex;align-items:center;gap:7px;cursor:pointer}
62
+ .swatch{width:10px;height:10px;border-radius:2px;flex-shrink:0}
63
+ .layer .pose{color:var(--muted);font-size:10.5px;margin:4px 0 5px;min-height:13px}
64
+ .belief-bar{flex:1;height:5px;background:var(--well);border-radius:3px;overflow:hidden;max-width:80px}
65
+ .belief-bar>div{height:100%;background:var(--go)}
66
+ .layer .ctl{display:flex;gap:4px;flex-wrap:wrap;margin-top:5px}
67
+ .layer .ctl button{padding:3px 7px;font-size:10.5px}
68
+ #library{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
69
+ .chip{display:flex;align-items:center;gap:5px;background:var(--well);
70
+ border:1px solid var(--panel-edge);border-radius:12px;padding:3px 9px;font-size:11px}
71
+ .chip button{border:0;padding:0 2px;font-size:12px;background:none}
72
+ label.tog{display:flex;gap:8px;align-items:center;cursor:pointer;padding:2px 0;font-size:12px}
73
+ label.tog input{accent-color:var(--phosphor)}
74
+ input[type=range]{flex:1;accent-color:var(--phosphor)}
75
+ input[type=text],textarea{font-family:var(--mono);font-size:12px;background:var(--well);
76
+ color:var(--ink);border:1px solid var(--panel-edge);border-radius:4px;padding:6px 8px;width:100%}
77
+ input[type=text]:focus-visible,textarea:focus-visible{outline:2px solid var(--phosphor);border-color:transparent}
78
+ #spawn-status,#prompt-log{color:var(--muted);font-size:10.5px;margin-top:6px;
79
+ min-height:13px;white-space:pre-wrap}
80
+ #ledger p{font-size:10.5px;line-height:1.5;color:var(--muted);margin-bottom:6px}
81
+ #ledger b{color:var(--go);font-weight:400}
82
+ #ledger .no{color:var(--entropy)}
83
+ details summary{cursor:pointer;font-family:var(--cond);font-size:12px;letter-spacing:2px;color:var(--muted)}
84
+ details[open] summary{margin-bottom:7px}
85
+ input[type=file]{font-size:10.5px;color:var(--muted);max-width:170px}
86
+ @media (max-width:900px){main{flex-direction:column;overflow-y:auto}body{overflow:auto}aside{width:100%}}
87
+ </style>
88
+ </head>
89
+ <body>
90
+
91
+ <header>
92
+ <h1>SLAPSTACK <em>STUDIO</em></h1>
93
+ <span class="claim">spawn layers with AI &middot; scramble &middot; bind &middot; grab &middot; stack &middot; every entity is a posterior</span>
94
+ <span id="state-lamp" class="soup">ATOM SOUP</span>
95
+ </header>
96
+
97
+ <main>
98
+ <div id="stage-wrap">
99
+ <div id="stage">
100
+ <canvas id="scene-canvas"></canvas>
101
+ <canvas id="overlay-canvas"></canvas>
102
+ <div class="hint">drag = pose clamp &middot; wheel rotate &middot; shift+wheel scale &middot; shift+click = semantic hint for selected layer</div>
103
+ </div>
104
+ </div>
105
+
106
+ <aside>
107
+ <div class="card">
108
+ <h2>SCENE</h2>
109
+ <div class="row">
110
+ <button id="btn-scramble">Scramble</button>
111
+ <button id="btn-bind" class="primary">Bind</button>
112
+ <span class="spacer"></span>
113
+ <span class="dim">noise</span>
114
+ <input type="range" id="noise" min="0" max="10" value="1" style="width:60px" aria-label="observation noise">
115
+ </div>
116
+ <div class="row">
117
+ <span class="dim">composite</span>
118
+ <label class="tog"><input type="radio" name="comp" value="field" checked> field</label>
119
+ <label class="tog"><input type="radio" name="comp" value="painter"> painter (depth)</label>
120
+ <span class="spacer"></span>
121
+ <label class="tog"><input type="checkbox" id="auto-occ" checked> depth occlusion</label>
122
+ </div>
123
+ <div id="bind-meter"><div></div></div>
124
+ <div id="metrics">
125
+ <span class="dim">binding vs truth</span><span class="readout" id="m-acc">—</span>
126
+ <span class="dim">mean entropy</span><span class="readout" id="m-ent">—</span>
127
+ <span class="dim">BP iterations</span><span class="readout" id="m-it">—</span>
128
+ <span class="dim">atoms in scene</span><span class="readout" id="m-n">—</span>
129
+ </div>
130
+ </div>
131
+
132
+ <div class="card">
133
+ <h2>LAYERS <span class="dim" style="letter-spacing:0">(front is highest)</span></h2>
134
+ <div id="layer-list"></div>
135
+ <div class="dim" style="font-size:10.5px;margin-top:6px">library — place to add to the scene:</div>
136
+ <div id="library"></div>
137
+ </div>
138
+
139
+ <div class="card">
140
+ <h2>SPAWN A LAYER</h2>
141
+ <div class="row">
142
+ <input type="text" id="spawn-prompt" placeholder='prompt, e.g. "a red rocket, flat colors"'>
143
+ </div>
144
+ <div class="row">
145
+ <input type="text" id="spawn-token" placeholder="matte token (e.g. rocket)" style="width:55%">
146
+ <button id="btn-spawn-text">Distill (GPU)</button>
147
+ </div>
148
+ <div class="row">
149
+ <input type="file" id="spawn-file" accept="image/*">
150
+ <button id="btn-spawn-img">Fit (CPU)</button>
151
+ </div>
152
+ <div class="row">
153
+ <input type="text" id="spawn-json" placeholder="or paste layer JSON">
154
+ <button id="btn-spawn-json">Add</button>
155
+ </div>
156
+ <div id="spawn-status">Distill runs Stable Diffusion score distillation with a cross-attention matte (GPU Space only, untested — first run is a smoke test). Fit trains atoms to your image on CPU (~1 min; objects on plain mid-gray come out compact).</div>
157
+ </div>
158
+
159
+ <div class="card">
160
+ <h2>PROMPT AS INTERVENTION</h2>
161
+ <div class="row">
162
+ <input type="text" id="prompt" placeholder='e.g. "move the boat left, put the star behind the tractor"'>
163
+ <button id="btn-prompt">Do</button>
164
+ </div>
165
+ <div id="prompt-log">Commands become pose clamps and depth moves; BP re-settles. Free-form uses Claude when this runs inside claude.ai; a local grammar otherwise.</div>
166
+ </div>
167
+
168
+ <div class="card">
169
+ <h2>OVERLAYS</h2>
170
+ <label class="tog"><input type="checkbox" id="ov-atoms" checked> atoms, colored by belief</label>
171
+ <label class="tog"><input type="checkbox" id="ov-own"> ownership tint</label>
172
+ <label class="tog"><input type="checkbox" id="ov-ent"> entropy border glow</label>
173
+ <label class="tog"><input type="checkbox" id="ov-pose" checked> pose belief (crosshair + covariance)</label>
174
+ <label class="tog"><input type="checkbox" id="ov-hints" checked> semantic hints</label>
175
+ </div>
176
+
177
+ <div class="card" id="ledger">
178
+ <details>
179
+ <summary>HONEST LEDGER</summary>
180
+ <p><b>Verified in this build:</b> the BP core matches the SlapstackBet6 Python (transform 2e-16, render MSE 8.6e-8, identical binding); drag = pose clamp + re-settle; identity persists through occlusion; depth occlusion removes evidence honestly (marginals revert toward the prior, coverage releases when moved away); the semantic likelihood factor resolves the twins case (two identical stars: 0.05 acc on geometry alone, 0.99 with location priors) and a mildly wrong prior is overridden by evidence; image&rarr;layer fitting runs end to end on CPU.</p>
181
+ <p><span class="no">Not verified:</span> text&rarr;layer (SDS) and cross-attention matte are line-for-line adaptations of the verified Bet-5 GPU loop but have NOT been executed on a GPU — first run is a smoke test; SD attention maps flicker across seeds, which is why they enter BP as a prior with a weight, never as truth. Real-photo scene&rarr;atoms remains open. Painter compositing and the alpha matte are presentation choices, not the additive field model.</p>
182
+ </details>
183
+ </div>
184
+ </aside>
185
+ </main>
186
+
187
+ <script>
188
+ /*CORE*/
189
+ </script>
190
+ <script>
191
+ /*STUDIO_CORE*/
192
+ </script>
193
+ <script>
194
+ const BUILTINS = /*BUILTINS*/null;
195
+ </script>
196
+ <script>
197
+ "use strict";
198
+ /* ============================ studio app ============================ */
199
+
200
+ const PALETTE = [
201
+ ["#ff6b5e",[255,107,94]], ["#5ec8ff",[94,200,255]], ["#ffd35e",[255,211,94]],
202
+ ["#7fd98a",[127,217,138]], ["#c86bff",[200,107,255]], ["#5effd0",[94,255,208]],
203
+ ["#ff8fc7",[255,143,199]], ["#b0b8ff",[176,184,255]],
204
+ ];
205
+ const OUTLIER_RGB = [120,128,143];
206
+ const RES = 192, OWN_RES = 128;
207
+ const NOISE_SCALE = [1,1,1,.3,.3,20,3,.5,.5,.5];
208
+ const HINT_W = 6.0, HINT_SIG = 0.4;
209
+
210
+ /* ------------- state ------------- */
211
+ const S = {
212
+ phase: "soup",
213
+ layers: [], // {id,name,color,rgb,template,placed,pose,hidden,depth,hints:[]}
214
+ selected: -1, // index into layers
215
+ obs: [], gt: [], // gt = placed-order index, -1 clutter
216
+ placed: [], // layer indices in placed order (BP object order)
217
+ marg: null, mu: null, Sig: null, assign: null,
218
+ evid: [], covFrac: [],
219
+ groupPre: [], groupAlpha: [], // per placed group + clutter last
220
+ ownField: null, coastT: [],
221
+ };
222
+ let nextDepth = 1, nextId = 1;
223
+
224
+ function addLayer(name, template, autoPlace) {
225
+ const [color, rgb] = PALETTE[(S.layers.length) % PALETTE.length];
226
+ const L = { id: nextId++, name, color, rgb, template, placed: false,
227
+ pose: [0,0,0,Math.log(0.5)], hidden: false, depth: 0, hints: [] };
228
+ S.layers.push(L);
229
+ if (autoPlace) placeLayer(S.layers.length - 1);
230
+ else { renderLibrary(); }
231
+ return L;
232
+ }
233
+ function placeLayer(li) {
234
+ const L = S.layers[li];
235
+ L.placed = true; L.hidden = false; L.depth = nextDepth++;
236
+ L.pose = [ -0.3 + 0.6*rand(), -0.3 + 0.6*rand(), -Math.PI + TAU*rand(),
237
+ Math.log(0.45 + 0.2*rand()) ];
238
+ rebuildScene();
239
+ }
240
+ function removeLayer(li) {
241
+ S.layers[li].placed = false;
242
+ rebuildScene();
243
+ }
244
+
245
+ /* ------------- scene construction ------------- */
246
+ let rngState = 22222;
247
+ function rand(){ rngState=(rngState*1103515245+12345)&0x7fffffff; return rngState/0x7fffffff; }
248
+
249
+ function rebuildScene() {
250
+ S.placed = S.layers.map((L,i)=>L.placed?i:-1).filter(i=>i>=0);
251
+ const noiseAmt = 0.006 * Math.pow(3, +document.getElementById("noise").value / 3);
252
+ S.obs = []; S.gt = [];
253
+ S.placed.forEach((li,k)=>{
254
+ for (const a of transformAtoms(S.layers[li].template, S.layers[li].pose)) {
255
+ S.obs.push(a); S.gt.push(k);
256
+ }
257
+ });
258
+ for (let i=0;i<12;i++){
259
+ const su=0.04+0.08*rand();
260
+ S.obs.push([-0.95+1.9*rand(),-0.95+1.9*rand(),-Math.PI/2+Math.PI*rand(),
261
+ su,su*(0.4+0.5*rand()),4+10*rand(),TAU*rand(),rand(),rand(),rand()]);
262
+ S.gt.push(-1);
263
+ }
264
+ for (const a of S.obs) for(let q=0;q<10;q++){
265
+ const g=Math.sqrt(-2*Math.log(rand()+1e-9))*Math.cos(TAU*rand());
266
+ a[q]+=g*noiseAmt*NOISE_SCALE[q];
267
+ }
268
+ for (const a of S.obs){a[3]=Math.max(a[3],.012);a[4]=Math.max(a[4],.008);a[5]=Math.max(a[5],.5);}
269
+ S.marg=null; S.mu=null; S.Sig=null; S.assign=null;
270
+ S.evid=S.obs.map(()=>true);
271
+ S.covFrac=S.placed.map(()=>0);
272
+ S.coastT=S.placed.map(()=>0);
273
+ S.ownField=null;
274
+ setPhase("soup");
275
+ rebuildGroupBuffers();
276
+ renderScene(); drawOverlay(); renderLayerList(); renderLibrary(); updatePanels();
277
+ }
278
+ function scramble(){
279
+ for (const li of S.placed){
280
+ S.layers[li].pose=[-0.35+0.7*rand(),-0.35+0.7*rand(),-Math.PI+TAU*rand(),
281
+ Math.log(0.4+0.25*rand())];
282
+ }
283
+ rebuildScene();
284
+ }
285
+
286
+ /* ------------- believed grouping (persistent through occlusion) ------------- */
287
+ function believedGroup(i){ return S.assign ? S.assign[i] : -2; }
288
+ function updateAssign(){
289
+ if(!S.marg){S.assign=null;return;}
290
+ const K=S.placed.length;
291
+ if(!S.assign) S.assign=new Array(S.obs.length).fill(-1);
292
+ for(let i=0;i<S.obs.length;i++){
293
+ if(S.assign[i]>=0 && !S.evid[i]) continue; // no evidence: identity persists
294
+ const m=S.marg[i]; let b=0;
295
+ for(let k=1;k<=K;k++) if(m[k]>m[b]) b=k;
296
+ S.assign[i]= b===K ? -1 : b;
297
+ }
298
+ }
299
+ function groupAtomIdx(k){
300
+ const out=[];
301
+ for(let i=0;i<S.obs.length;i++) if(believedGroup(i)===k) out.push(i);
302
+ return out;
303
+ }
304
+
305
+ /* ------------- rendering ------------- */
306
+ const stage=document.getElementById("stage");
307
+ const sceneC=document.getElementById("scene-canvas");
308
+ const overC=document.getElementById("overlay-canvas");
309
+ sceneC.width=sceneC.height=RES;
310
+ let CSS=600;
311
+ function fitStage(){
312
+ const wrap=document.getElementById("stage-wrap");
313
+ CSS=Math.max(280,Math.floor(Math.min(wrap.clientWidth,wrap.clientHeight)-8));
314
+ stage.style.width=stage.style.height=CSS+"px";
315
+ overC.width=overC.height=CSS*(window.devicePixelRatio||1);
316
+ drawOverlay();
317
+ }
318
+ window.addEventListener("resize",fitStage);
319
+
320
+ function rebuildGroupBuffers(){
321
+ const K=S.placed.length;
322
+ S.groupPre=[]; S.groupAlpha=[];
323
+ for(let g=0;g<=K;g++) S.groupPre.push(new Float32Array(3*RES*RES));
324
+ const groupOf=i=>{ const g=S.marg?believedGroup(i):S.gt[i]; return g>=0?g:K; };
325
+ for(let i=0;i<S.obs.length;i++){
326
+ const g=groupOf(i);
327
+ if(g<K && S.layers[S.placed[g]].hidden) continue;
328
+ renderPre([S.obs[i]],RES,S.groupPre[g]);
329
+ }
330
+ for(let g=0;g<=K;g++) S.groupAlpha.push(alphaFromPre(S.groupPre[g],RES));
331
+ }
332
+ function rebuildOneGroup(k){
333
+ S.groupPre[k]=new Float32Array(3*RES*RES);
334
+ if(!S.layers[S.placed[k]].hidden)
335
+ renderPre(groupAtomIdx(k).map(i=>S.obs[i]),RES,S.groupPre[k]);
336
+ S.groupAlpha[k]=alphaFromPre(S.groupPre[k],RES);
337
+ }
338
+ const sceneCtx=sceneC.getContext("2d");
339
+ const compositePre=new Float32Array(3*RES*RES);
340
+ const sceneImg=new ImageData(RES,RES);
341
+ function compMode(){ return document.querySelector('input[name=comp]:checked').value; }
342
+ function renderScene(){
343
+ const K=S.placed.length;
344
+ if(compMode()==="field"){
345
+ compositePre.fill(0);
346
+ for(const buf of S.groupPre)
347
+ for(let i=0;i<compositePre.length;i++) compositePre[i]+=buf[i];
348
+ sigmoidField(compositePre,RES,sceneImg.data);
349
+ } else {
350
+ // painter: clutter at the back, then placed layers by depth
351
+ const order=[K, ...S.placed.map((li,k)=>k)
352
+ .sort((a,b)=>S.layers[S.placed[a]].depth-S.layers[S.placed[b]].depth)];
353
+ compositePainter(order,S.groupPre,S.groupAlpha,RES,sceneImg.data);
354
+ }
355
+ sceneCtx.putImageData(sceneImg,0,0);
356
+ }
357
+
358
+ function w2p(x){return (x+1)/2*overC.width;}
359
+ function p2w(px){return px/overC.width*2-1;}
360
+
361
+ const ovAtoms=document.getElementById("ov-atoms"),ovOwn=document.getElementById("ov-own"),
362
+ ovEnt=document.getElementById("ov-ent"),ovPose=document.getElementById("ov-pose"),
363
+ ovHints=document.getElementById("ov-hints");
364
+ for(const el of [ovAtoms,ovOwn,ovEnt,ovPose,ovHints]) el.addEventListener("change",drawOverlay);
365
+ document.querySelectorAll('input[name=comp]').forEach(el=>
366
+ el.addEventListener("change",()=>{renderScene();}));
367
+
368
+ function drawOverlay(){
369
+ const ctx=overC.getContext("2d");
370
+ ctx.clearRect(0,0,overC.width,overC.height);
371
+ const px1=overC.width/CSS;
372
+ const K=S.placed.length;
373
+ const colOf=k=>S.layers[S.placed[k]];
374
+
375
+ if(S.ownField && (ovOwn.checked||ovEnt.checked)){
376
+ const {P,ent,support}=S.ownField;
377
+ const im=ctx.createImageData(OWN_RES,OWN_RES);
378
+ for(let i=0;i<OWN_RES*OWN_RES;i++){
379
+ if(!support[i]) continue;
380
+ let r=0,g=0,b=0,a=0;
381
+ if(ovOwn.checked){
382
+ let best=0;
383
+ for(let k=1;k<=K;k++) if(P[k][i]>P[best][i]) best=k;
384
+ const rgb=best===K?OUTLIER_RGB:colOf(best).rgb;
385
+ r=rgb[0];g=rgb[1];b=rgb[2];a=90*P[best][i];
386
+ }
387
+ if(ovEnt.checked){
388
+ const maxE=Math.log2(K+1);
389
+ const e=Math.min(1,ent[i]/maxE), ea=160*e*e;
390
+ if(ea>a){r=200;g=107;b=255;a=ea;}
391
+ }
392
+ im.data[4*i]=r;im.data[4*i+1]=g;im.data[4*i+2]=b;im.data[4*i+3]=a;
393
+ }
394
+ const tmp=document.createElement("canvas");
395
+ tmp.width=tmp.height=OWN_RES;
396
+ tmp.getContext("2d").putImageData(im,0,0);
397
+ ctx.imageSmoothingEnabled=true;
398
+ ctx.drawImage(tmp,0,0,overC.width,overC.height);
399
+ }
400
+
401
+ if(ovAtoms.checked){
402
+ for(let i=0;i<S.obs.length;i++){
403
+ const a=S.obs[i], g=believedGroup(i);
404
+ if(g>=0 && S.layers[S.placed[g]].hidden) continue;
405
+ let rgb=OUTLIER_RGB, alpha=0.5;
406
+ if(g===-2){rgb=[150,155,168];alpha=0.55;}
407
+ else if(g>=0){rgb=colOf(g).rgb;alpha=S.evid[i]?0.25+0.75*S.marg[i][g]:0.35;}
408
+ else if(S.marg){alpha=0.25+0.55*S.marg[i][K];}
409
+ const len=Math.max(3*px1,a[3]*overC.width*0.5);
410
+ const cx=w2p(a[0]),cy=w2p(a[1]);
411
+ const dx=Math.cos(a[2])*len/2,dy=Math.sin(a[2])*len/2;
412
+ ctx.strokeStyle=`rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha})`;
413
+ ctx.lineWidth=1.6*px1;
414
+ ctx.setLineDash(g>=0&&!S.evid[i]?[3*px1,3*px1]:[]);
415
+ ctx.beginPath();ctx.moveTo(cx-dx,cy-dy);ctx.lineTo(cx+dx,cy+dy);ctx.stroke();
416
+ }
417
+ ctx.setLineDash([]);
418
+ }
419
+
420
+ if(ovHints.checked){
421
+ for(let k=0;k<K;k++){
422
+ const L=colOf(k);
423
+ for(const h of L.hints){
424
+ ctx.strokeStyle=L.color; ctx.lineWidth=1.2*px1; ctx.setLineDash([2*px1,3*px1]);
425
+ ctx.beginPath();ctx.arc(w2p(h[0]),w2p(h[1]),HINT_SIG*overC.width/2,0,TAU);ctx.stroke();
426
+ ctx.setLineDash([]);
427
+ ctx.beginPath();ctx.arc(w2p(h[0]),w2p(h[1]),3*px1,0,TAU);
428
+ ctx.fillStyle=L.color;ctx.fill();
429
+ }
430
+ }
431
+ }
432
+
433
+ if(ovPose.checked && S.mu){
434
+ for(let k=0;k<K;k++){
435
+ const mu=S.mu[k],Sg=S.Sig[k],L=colOf(k);
436
+ const cx=w2p(mu[0]),cy=w2p(mu[1]);
437
+ ctx.strokeStyle=L.color;ctx.lineWidth=1.2*px1;
438
+ ctx.beginPath();
439
+ ctx.moveTo(cx-7*px1,cy);ctx.lineTo(cx+7*px1,cy);
440
+ ctx.moveTo(cx,cy-7*px1);ctx.lineTo(cx,cy+7*px1);ctx.stroke();
441
+ const Ln=26*px1*Math.exp(mu[3]);
442
+ ctx.beginPath();ctx.moveTo(cx,cy);
443
+ ctx.lineTo(cx+Math.cos(mu[2])*Ln,cy+Math.sin(mu[2])*Ln);ctx.stroke();
444
+ const coast=S.coastT[k], q=coast*coast*0.0012;
445
+ const a11=Sg[0][0]+q,a12=Sg[0][1],a22=Sg[1][1]+q;
446
+ const tr=a11+a22,det=a11*a22-a12*a12;
447
+ const disc=Math.sqrt(Math.max(0,tr*tr/4-det));
448
+ const l1=tr/2+disc,l2=Math.max(1e-12,tr/2-disc);
449
+ const ang=Math.atan2(l1-a11,a12||1e-12);
450
+ const ghost=L.hidden||S.covFrac[k]>0.7;
451
+ ctx.save();ctx.translate(cx,cy);ctx.rotate(-ang+Math.PI/2);
452
+ ctx.setLineDash(ghost?[5*px1,4*px1]:[]);
453
+ ctx.beginPath();
454
+ ctx.ellipse(0,0,2*Math.sqrt(l1)*overC.width/2,2*Math.sqrt(l2)*overC.width/2,0,0,TAU);
455
+ ctx.stroke();ctx.restore();ctx.setLineDash([]);
456
+ if(ghost){
457
+ ctx.fillStyle=L.color;
458
+ ctx.font=`${11*px1}px ${getComputedStyle(document.body).fontFamily}`;
459
+ ctx.fillText("believed, unseen",cx+10*px1,cy-10*px1);
460
+ }
461
+ }
462
+ }
463
+ }
464
+
465
+ /* ------------- semantic factor from hints ------------- */
466
+ function makeSemLL(){
467
+ const K=S.placed.length;
468
+ let any=false;
469
+ for(const li of S.placed) if(S.layers[li].hints.length) any=true;
470
+ if(!any) return null;
471
+ return (i,k)=>{
472
+ const hs=S.layers[S.placed[k]].hints;
473
+ if(!hs.length) return 0;
474
+ let best=0;
475
+ for(const h of hs){
476
+ const d2=(S.obs[i][0]-h[0])**2+(S.obs[i][1]-h[1])**2;
477
+ best=Math.max(best,Math.exp(-0.5*d2/(HINT_SIG*HINT_SIG)));
478
+ }
479
+ return HINT_W*(best-0.5);
480
+ };
481
+ }
482
+
483
+ /* ------------- BP wiring + depth occlusion ------------- */
484
+ function currentTemplates(){ return S.placed.map(li=>S.layers[li].template); }
485
+
486
+ function computeEvidence(){
487
+ const K=S.placed.length;
488
+ if(!document.getElementById("auto-occ").checked || !S.assign){
489
+ S.evid=S.obs.map((_,i)=>{
490
+ const g=believedGroup(i);
491
+ return !(g>=0 && S.layers[S.placed[g]].hidden);
492
+ });
493
+ S.covFrac=S.placed.map((li,k)=>S.layers[li].hidden?1:0);
494
+ return;
495
+ }
496
+ const layersMeta=S.placed.map(li=>({depth:S.layers[li].depth,hidden:S.layers[li].hidden}));
497
+ const {mask,covFrac}=autoHidden(S.obs,S.assign,layersMeta,S.groupAlpha,RES);
498
+ S.evid=mask.map(m=>!m);
499
+ S.covFrac=covFrac;
500
+ }
501
+
502
+ function runBind(opts={}){
503
+ const hiddenMask=S.evid.map(e=>!e);
504
+ const frames=[];
505
+ const res=bpBind(currentTemplates(),S.obs,{
506
+ iters:opts.iters??40,
507
+ clampPose:opts.clampPose??null,
508
+ hiddenMask,
509
+ semLL:makeSemLL(),
510
+ onIter:(it,marg,mu,Sig)=>{ if(opts.animate) frames.push({
511
+ marg,mu:mu.map(m=>m.slice()),Sig:Sig.map(M=>M.map(r=>r.slice()))}); },
512
+ });
513
+ return {res,frames};
514
+ }
515
+
516
+ function bind(animate=true){
517
+ if(S.phase==="binding"||!S.placed.length) return;
518
+ S.assign=null;
519
+ S.evid=S.obs.map(()=>true); // full re-bind: everything is evidence again
520
+ for(const li of S.placed) if(S.layers[li].hidden){
521
+ // user-hidden layers stay evidence-free
522
+ // (their atoms are excluded per-atom below via gt at soup time)
523
+ }
524
+ setPhase("binding");
525
+ setTimeout(()=>{
526
+ const {res,frames}=runBind({iters:40,animate});
527
+ if(!animate){finishBind(res);return;}
528
+ let f=0;
529
+ const meter=document.querySelector("#bind-meter > div");
530
+ const step=()=>{
531
+ if(f<frames.length){
532
+ S.marg=frames[f].marg;S.mu=frames[f].mu;S.Sig=frames[f].Sig;
533
+ updateAssign();
534
+ meter.style.width=(100*(f+1)/frames.length)+"%";
535
+ document.getElementById("m-it").textContent=(f+1);
536
+ drawOverlay();updatePanels();
537
+ f++;requestAnimationFrame(step);
538
+ } else finishBind(res);
539
+ };
540
+ step();
541
+ },30);
542
+ }
543
+ function finishBind(res){
544
+ S.marg=res.marg;S.mu=res.mu;S.Sig=res.Sig;
545
+ updateAssign();
546
+ rebuildGroupBuffers();
547
+ occlusionRefresh(true);
548
+ renderScene();refreshOwnership();
549
+ setPhase("bound");
550
+ drawOverlay();updatePanels();renderLayerList();
551
+ }
552
+
553
+ /* recompute evidence from the depth stack; if it changed, refresh marginals
554
+ with all poses clamped (occlusion changes evidence, not where things are) */
555
+ function occlusionRefresh(silent){
556
+ const before=S.evid.join("");
557
+ computeEvidence();
558
+ if(S.evid.join("")===before) return;
559
+ const clamp=S.mu.map(m=>m.slice());
560
+ const {res}=runBind({iters:8,clampPose:clamp});
561
+ S.marg=res.marg;
562
+ updateAssign();
563
+ // coasting for heavily covered layers
564
+ for(let k=0;k<S.placed.length;k++)
565
+ S.coastT[k]=(S.covFrac[k]>0.7||S.layers[S.placed[k]].hidden)?S.coastT[k]+1:0;
566
+ if(!silent){drawOverlay();updatePanels();renderLayerList();}
567
+ }
568
+
569
+ function refreshOwnership(){
570
+ if(!S.marg){S.ownField=null;return;}
571
+ const K=S.placed.length;
572
+ const vis=[],visMarg=[];
573
+ for(let i=0;i<S.obs.length;i++){
574
+ const g=believedGroup(i);
575
+ if(g>=0&&S.layers[S.placed[g]].hidden) continue;
576
+ vis.push(S.obs[i]);visMarg.push(S.marg[i]);
577
+ }
578
+ S.ownField=ownershipField(vis,visMarg,K,OWN_RES);
579
+ }
580
+
581
+ function resettle(k,clampXi){
582
+ const clamp=S.placed.map(()=>null);clamp[k]=clampXi;
583
+ const {res}=runBind({iters:10,clampPose:clamp});
584
+ S.marg=res.marg;S.mu=res.mu;S.Sig=res.Sig;
585
+ S.mu[k]=clampXi.slice();
586
+ updateAssign();
587
+ rebuildGroupBuffers();
588
+ occlusionRefresh(true);
589
+ renderScene();refreshOwnership();
590
+ drawOverlay();updatePanels();renderLayerList();
591
+ document.getElementById("m-it").textContent="10 (re-settle)";
592
+ }
593
+
594
+ /* ------------- interventions ------------- */
595
+ function applyGesture(k,g){
596
+ for(const i of groupAtomIdx(k)) S.obs[i]=transformAtoms([S.obs[i]],g)[0];
597
+ S.mu[k]=composePose(g,S.mu[k]);
598
+ rebuildOneGroup(k);
599
+ renderScene();drawOverlay();updatePanels();
600
+ }
601
+
602
+ /* ------------- pointer interaction ------------- */
603
+ let drag=null;
604
+ overC.addEventListener("pointerdown",e=>{
605
+ const rect=overC.getBoundingClientRect();
606
+ const wx=p2w((e.clientX-rect.left)*overC.width/rect.width);
607
+ const wy=p2w((e.clientY-rect.top)*overC.width/rect.width);
608
+ if(e.shiftKey){ // semantic hint for the selected layer
609
+ const li=S.selected;
610
+ if(li>=0&&S.layers[li].placed){
611
+ S.layers[li].hints.push([wx,wy]);
612
+ drawOverlay();renderLayerList();
613
+ document.getElementById("prompt-log").textContent=
614
+ `hint added for ${S.layers[li].name} — Bind applies it as a location prior.`;
615
+ }
616
+ return;
617
+ }
618
+ if(S.phase!=="bound") return;
619
+ let best=-1,bd=0.12*0.12;
620
+ for(let i=0;i<S.obs.length;i++){
621
+ const g=believedGroup(i);
622
+ if(g<0) continue;
623
+ if(S.layers[S.placed[g]].hidden) continue;
624
+ const d=(S.obs[i][0]-wx)**2+(S.obs[i][1]-wy)**2;
625
+ if(d<bd){bd=d;best=i;}
626
+ }
627
+ let k=best>=0?believedGroup(best):-1;
628
+ if(k<0) for(let kk=0;kk<S.placed.length;kk++)
629
+ if((S.layers[S.placed[kk]].hidden||S.covFrac[kk]>0.7)&&
630
+ (S.mu[kk][0]-wx)**2+(S.mu[kk][1]-wy)**2<0.05*0.05) k=kk;
631
+ if(k<0) return;
632
+ drag={k,lx:wx,ly:wy,moved:false};
633
+ overC.classList.add("dragging");
634
+ overC.setPointerCapture(e.pointerId);
635
+ });
636
+ overC.addEventListener("pointermove",e=>{
637
+ if(!drag) return;
638
+ const rect=overC.getBoundingClientRect();
639
+ const wx=p2w((e.clientX-rect.left)*overC.width/rect.width);
640
+ const wy=p2w((e.clientY-rect.top)*overC.width/rect.width);
641
+ const dx=wx-drag.lx,dy=wy-drag.ly;
642
+ if(dx||dy){applyGesture(drag.k,gestureTranslate(dx,dy));drag.lx=wx;drag.ly=wy;drag.moved=true;}
643
+ });
644
+ overC.addEventListener("pointerup",e=>{
645
+ if(!drag) return;
646
+ overC.classList.remove("dragging");
647
+ if(drag.moved) resettle(drag.k,S.mu[drag.k]);
648
+ drag=null;
649
+ });
650
+ overC.addEventListener("wheel",e=>{
651
+ if(S.phase!=="bound") return;
652
+ const rect=overC.getBoundingClientRect();
653
+ const wx=p2w((e.clientX-rect.left)*overC.width/rect.width);
654
+ const wy=p2w((e.clientY-rect.top)*overC.width/rect.width);
655
+ let k=-1,bd=0.16*0.16;
656
+ for(let kk=0;kk<S.placed.length;kk++){
657
+ const d=(S.mu[kk][0]-wx)**2+(S.mu[kk][1]-wy)**2;
658
+ if(d<bd){bd=d;k=kk;}
659
+ }
660
+ if(k<0) return;
661
+ e.preventDefault();
662
+ const c=[S.mu[k][0],S.mu[k][1]];
663
+ const g=e.shiftKey?gestureScaleAbout(c,e.deltaY>0?-0.05:0.05)
664
+ :gestureRotateAbout(c,e.deltaY>0?0.08:-0.08);
665
+ applyGesture(k,g);
666
+ clearTimeout(overC._wt);
667
+ overC._wt=setTimeout(()=>resettle(k,S.mu[k]),350);
668
+ },{passive:false});
669
+
670
+ /* ------------- panels ------------- */
671
+ function setPhase(p){
672
+ S.phase=p;
673
+ const lamp=document.getElementById("state-lamp");
674
+ lamp.className=p==="soup"?"soup":p==="binding"?"binding":"bound";
675
+ lamp.textContent=p==="soup"?"ATOM SOUP":p==="binding"?"BELIEFS SETTLING":"BOUND — GRABBABLE";
676
+ document.getElementById("btn-bind").disabled=p==="binding"||!S.placed.length;
677
+ }
678
+
679
+ function renderLayerList(){
680
+ const el=document.getElementById("layer-list");
681
+ el.innerHTML="";
682
+ const byDepth=[...S.placed].map((li,k)=>({li,k}))
683
+ .sort((a,b)=>S.layers[b.li].depth-S.layers[a.li].depth);
684
+ for(const {li,k} of byDepth){
685
+ const L=S.layers[li];
686
+ const idx=S.assign?groupAtomIdx(k):[];
687
+ const conf=idx.length?idx.reduce((a,i)=>a+(S.evid[i]?S.marg[i][k]:0),0)/Math.max(1,idx.filter(i=>S.evid[i]).length):0;
688
+ const cov=S.covFrac[k]||0;
689
+ const d=document.createElement("div");
690
+ d.className="layer"+(S.selected===li?" sel":"");
691
+ d.innerHTML=`
692
+ <div class="name" data-li="${li}">
693
+ <span class="swatch" style="background:${L.color}"></span>
694
+ <span>${L.name}</span>
695
+ <span class="dim">${idx.length||L.template.length} atoms${L.hints.length?` · ${L.hints.length} hint${L.hints.length>1?"s":""}`:""}</span>
696
+ <span class="spacer"></span>
697
+ <div class="belief-bar"><div style="width:${100*conf}%"></div></div>
698
+ </div>
699
+ <div class="pose">${S.mu?`t=(${S.mu[k][0].toFixed(2)}, ${S.mu[k][1].toFixed(2)}) \u03c1=${(S.mu[k][2]*180/Math.PI).toFixed(0)}\u00b0 s=${Math.exp(S.mu[k][3]).toFixed(2)}`:"pose: unbound"}${cov>0.05?` · <span style="color:var(--entropy)">${(100*cov).toFixed(0)}% occluded${S.coastT[k]?`, coasting ${S.coastT[k]}`:""}</span>`:""}</div>
700
+ <div class="ctl">
701
+ <button data-li="${li}" data-op="rotl">&#8634;</button>
702
+ <button data-li="${li}" data-op="rotr">&#8635;</button>
703
+ <button data-li="${li}" data-op="grow">+</button>
704
+ <button data-li="${li}" data-op="shrink">&minus;</button>
705
+ <button data-li="${li}" data-op="front">front</button>
706
+ <button data-li="${li}" data-op="back">back</button>
707
+ <button data-li="${li}" data-op="hide">${L.hidden?"reveal":"hide"}</button>
708
+ ${L.hints.length?`<button data-li="${li}" data-op="clearhints">hints&nbsp;&#10005;</button>`:""}
709
+ <button data-li="${li}" data-op="remove">&#10005;</button>
710
+ </div>`;
711
+ el.appendChild(d);
712
+ }
713
+ if(!byDepth.length)
714
+ el.innerHTML='<div class="dim" style="font-size:11px">no layers placed — place from the library or spawn one</div>';
715
+ }
716
+ document.getElementById("layer-list").addEventListener("click",e=>{
717
+ const nm=e.target.closest(".name");
718
+ if(nm&&!e.target.closest("button")){
719
+ S.selected=+nm.dataset.li===S.selected?-1:+nm.dataset.li;
720
+ renderLayerList();return;
721
+ }
722
+ const b=e.target.closest("button");if(!b)return;
723
+ const li=+b.dataset.li,op=b.dataset.op;
724
+ const k=S.placed.indexOf(li);
725
+ if(op==="remove"){removeLayer(li);return;}
726
+ if(op==="clearhints"){S.layers[li].hints=[];drawOverlay();renderLayerList();return;}
727
+ if(op==="front"||op==="back"){
728
+ S.layers[li].depth=op==="front"?nextDepth++:(Math.min(...S.placed.map(x=>S.layers[x].depth))-1);
729
+ if(S.phase==="bound"){rebuildGroupBuffers();occlusionRefresh(false);renderScene();refreshOwnership();}
730
+ renderLayerList();return;
731
+ }
732
+ if(S.phase!=="bound")return;
733
+ if(op==="hide"){
734
+ S.layers[li].hidden=!S.layers[li].hidden;
735
+ rebuildGroupBuffers();occlusionRefresh(false);renderScene();refreshOwnership();drawOverlay();renderLayerList();
736
+ return;
737
+ }
738
+ const c=[S.mu[k][0],S.mu[k][1]];
739
+ if(op==="rotl"){applyGesture(k,gestureRotateAbout(c,-Math.PI/12));resettle(k,S.mu[k]);}
740
+ if(op==="rotr"){applyGesture(k,gestureRotateAbout(c,Math.PI/12));resettle(k,S.mu[k]);}
741
+ if(op==="grow"){applyGesture(k,gestureScaleAbout(c,0.12));resettle(k,S.mu[k]);}
742
+ if(op==="shrink"){applyGesture(k,gestureScaleAbout(c,-0.12));resettle(k,S.mu[k]);}
743
+ });
744
+
745
+ function renderLibrary(){
746
+ const el=document.getElementById("library");
747
+ el.innerHTML="";
748
+ S.layers.forEach((L,li)=>{
749
+ if(L.placed) return;
750
+ const c=document.createElement("span");
751
+ c.className="chip";
752
+ c.innerHTML=`<span class="swatch" style="background:${L.color}"></span>${L.name}
753
+ <span class="dim">${L.template.length}</span>
754
+ <button data-li="${li}" data-op="place" title="place">+</button>`;
755
+ el.appendChild(c);
756
+ });
757
+ }
758
+ document.getElementById("library").addEventListener("click",e=>{
759
+ const b=e.target.closest("button");if(!b)return;
760
+ placeLayer(+b.dataset.li);
761
+ });
762
+
763
+ function updatePanels(){
764
+ document.getElementById("m-n").textContent=S.obs.length;
765
+ const K=S.placed.length;
766
+ if(!S.marg){
767
+ document.getElementById("m-acc").textContent="—";
768
+ document.getElementById("m-ent").textContent="—";
769
+ return;
770
+ }
771
+ let correct=0;
772
+ for(let i=0;i<S.gt.length;i++) if(believedGroup(i)===S.gt[i]) correct++;
773
+ document.getElementById("m-acc").textContent=(100*correct/S.gt.length).toFixed(1)+"%";
774
+ const ent=atomEntropy(S.marg);
775
+ document.getElementById("m-ent").textContent=
776
+ (ent.reduce((a,x)=>a+x,0)/ent.length).toFixed(2)+" bits";
777
+ }
778
+
779
+ /* ------------- spawn: server endpoints ------------- */
780
+ const spawnStatus=document.getElementById("spawn-status");
781
+ async function gradioCall(name,data){
782
+ const base=location.pathname.includes("/studio/")?"..":"";
783
+ const r=await fetch(`${base}/gradio_api/call/${name}`,{
784
+ method:"POST",headers:{"Content-Type":"application/json"},
785
+ body:JSON.stringify({data})});
786
+ const {event_id}=await r.json();
787
+ const resp=await fetch(`${base}/gradio_api/call/${name}/${event_id}`);
788
+ const text=await resp.text();
789
+ const lines=text.split("\n").filter(l=>l.startsWith("data:"));
790
+ if(!lines.length) throw new Error("no data from server");
791
+ const payload=JSON.parse(lines[lines.length-1].slice(5));
792
+ if(payload==null||payload.error) throw new Error(payload?.error||"server error");
793
+ return JSON.parse(payload[0]);
794
+ }
795
+ function addGenerated(layer,fallbackName){
796
+ const name=(layer.ledger&&layer.ledger.prompt)
797
+ ? (layer.ledger.attention&&layer.ledger.attention.token!=="(all content tokens)"
798
+ ? layer.ledger.attention.token : layer.ledger.prompt.split(/[ ,]/)[1]||fallbackName)
799
+ : fallbackName;
800
+ addLayer(name,layer.atoms,true);
801
+ spawnStatus.textContent=`layer "${name}": ${layer.atoms.length} atoms · ${layer.ledger.status}`+
802
+ (layer.ledger.attention?` · matte kept ${layer.ledger.attention.atoms_after_matte}/${layer.ledger.attention.atoms_before_matte}`:"");
803
+ }
804
+ document.getElementById("btn-spawn-text").addEventListener("click",async()=>{
805
+ const prompt=document.getElementById("spawn-prompt").value.trim();
806
+ const token=document.getElementById("spawn-token").value.trim();
807
+ if(!prompt){spawnStatus.textContent="write a prompt first";return;}
808
+ spawnStatus.textContent="distilling on the server (GPU, minutes)\u2026";
809
+ try{
810
+ const layer=await gradioCall("layer_from_text",[prompt,"",192,900,30,token,0.35]);
811
+ addGenerated(layer,"generated");
812
+ }catch(err){spawnStatus.textContent="distill failed: "+err.message+
813
+ " (needs a GPU Space; on CPU use Fit or paste JSON)";}
814
+ });
815
+ document.getElementById("btn-spawn-img").addEventListener("click",async()=>{
816
+ const f=document.getElementById("spawn-file").files[0];
817
+ if(!f){spawnStatus.textContent="choose an image first";return;}
818
+ spawnStatus.textContent="fitting atoms to the image (CPU, ~1 min)\u2026";
819
+ try{
820
+ const b64=await new Promise((res,rej)=>{
821
+ const img=new Image();
822
+ img.onload=()=>{
823
+ const c=document.createElement("canvas");
824
+ const s=Math.min(1,160/Math.max(img.width,img.height));
825
+ c.width=Math.round(img.width*s);c.height=Math.round(img.height*s);
826
+ c.getContext("2d").drawImage(img,0,0,c.width,c.height);
827
+ res(c.toDataURL("image/png").split(",")[1]);
828
+ };
829
+ img.onerror=rej;
830
+ img.src=URL.createObjectURL(f);
831
+ });
832
+ const layer=await gradioCall("layer_from_image_b64",[b64,140,400]);
833
+ addGenerated(layer,f.name.replace(/\.[^.]+$/,""));
834
+ }catch(err){spawnStatus.textContent="fit failed: "+err.message+
835
+ " (is this page running from the Space? standalone files can only paste JSON)";}
836
+ });
837
+ document.getElementById("btn-spawn-json").addEventListener("click",()=>{
838
+ try{
839
+ const raw=JSON.parse(document.getElementById("spawn-json").value);
840
+ const atoms=raw.atoms||raw;
841
+ if(!Array.isArray(atoms)||atoms[0].length!==10) throw new Error("expect [[10 fields]...]");
842
+ addLayer("pasted-"+nextId,atoms,true);
843
+ spawnStatus.textContent=`pasted layer: ${atoms.length} atoms`;
844
+ document.getElementById("spawn-json").value="";
845
+ }catch(err){spawnStatus.textContent="bad JSON: "+err.message;}
846
+ });
847
+
848
+ /* ------------- prompt as intervention ------------- */
849
+ const promptLog=document.getElementById("prompt-log");
850
+ function layerByName(part){
851
+ for(let k=0;k<S.placed.length;k++)
852
+ if(part.includes(S.layers[S.placed[k]].name.toLowerCase())) return k;
853
+ return -1;
854
+ }
855
+ function localParse(text){
856
+ const t=text.toLowerCase(),ops=[];
857
+ for(const part of t.split(/,|;| and /)){
858
+ const k=layerByName(part);
859
+ if(k<0) continue;
860
+ if(/hide|occlude/.test(part)){ops.push({op:"hide",k});continue;}
861
+ if(/show|reveal|unhide/.test(part)){ops.push({op:"show",k});continue;}
862
+ if(/front|top|forward/.test(part)&&/bring|put|move|send/.test(part)){ops.push({op:"front",k});continue;}
863
+ if(/back|behind|bottom/.test(part)&&/send|put|move|push/.test(part)){ops.push({op:"back",k});continue;}
864
+ const deg=part.match(/(-?\d+)\s*(deg|\u00b0)?/);
865
+ if(/rotate|turn|spin/.test(part))
866
+ ops.push({op:"rotate",k,deg:deg?+deg[1]:30*(/left|counter|ccw/.test(part)?-1:1)});
867
+ if(/scale|grow|bigger|larger|shrink|smaller/.test(part))
868
+ ops.push({op:"scale",k,ds:/shrink|smaller/.test(part)?-0.15:0.15});
869
+ if(/move|push|slide|put|drag/.test(part)){
870
+ let dx=0,dy=0;const step=0.22;
871
+ if(/left/.test(part))dx-=step;
872
+ if(/right/.test(part))dx+=step;
873
+ if(/up|top/.test(part))dy-=step;
874
+ if(/down|bottom/.test(part))dy+=step;
875
+ if(/center|middle/.test(part))ops.push({op:"moveto",k,x:0,y:0});
876
+ else if(dx||dy)ops.push({op:"move",k,dx,dy});
877
+ }
878
+ }
879
+ return ops;
880
+ }
881
+ async function claudeParse(text){
882
+ const names=S.placed.map(li=>S.layers[li].name);
883
+ const sceneDesc=S.placed.map((li,k)=>
884
+ `${S.layers[li].name}: t=(${S.mu[k][0].toFixed(2)},${S.mu[k][1].toFixed(2)}) rho=${S.mu[k][2].toFixed(2)} scale=${Math.exp(S.mu[k][3]).toFixed(2)} depth=${S.layers[li].depth} hidden=${S.layers[li].hidden}`).join("; ");
885
+ const resp=await fetch("https://api.anthropic.com/v1/messages",{
886
+ method:"POST",headers:{"Content-Type":"application/json"},
887
+ body:JSON.stringify({model:"claude-sonnet-4-6",max_tokens:1000,
888
+ messages:[{role:"user",content:
889
+ `You control a probabilistic layered scene editor. Coordinates x,y in [-1,1], y grows DOWNWARD. Higher depth = in front. Layers: ${sceneDesc}.
890
+ User command: "${text}"
891
+ Respond ONLY with a JSON array (no prose, no fences) of ops:
892
+ {"op":"move","object":<name>,"dx":n,"dy":n} {"op":"moveto","object":<name>,"x":n,"y":n}
893
+ {"op":"rotate","object":<name>,"deg":n} {"op":"scale","object":<name>,"factor":n}
894
+ {"op":"front","object":<name>} {"op":"back","object":<name>}
895
+ {"op":"hide","object":<name>} {"op":"show","object":<name>}
896
+ Valid names: ${JSON.stringify(names)}`}]})});
897
+ const data=await resp.json();
898
+ const txt=data.content.filter(c=>c.type==="text").map(c=>c.text).join("");
899
+ const arr=JSON.parse(txt.replace(/```json|```/g,"").trim());
900
+ return arr.map(o=>{
901
+ const k=S.placed.findIndex(li=>S.layers[li].name===o.object);
902
+ if(k<0)return null;
903
+ if(o.op==="move")return{op:"move",k,dx:o.dx,dy:o.dy};
904
+ if(o.op==="moveto")return{op:"moveto",k,x:o.x,y:o.y};
905
+ if(o.op==="rotate")return{op:"rotate",k,deg:o.deg};
906
+ if(o.op==="scale")return{op:"scale",k,ds:Math.log(o.factor)};
907
+ return{op:o.op,k};
908
+ }).filter(Boolean);
909
+ }
910
+ function execOps(ops){
911
+ if(!ops.length){promptLog.textContent="Parsed no intervention. Layers: "+
912
+ S.placed.map(li=>S.layers[li].name).join(", ")+". Verbs: move/rotate/scale/hide/show/front/back.";return;}
913
+ const touched=new Set();
914
+ for(const o of ops){
915
+ const li=S.placed[o.k];
916
+ if(o.op==="hide"&&!S.layers[li].hidden){S.layers[li].hidden=true;continue;}
917
+ if(o.op==="show"&&S.layers[li].hidden){S.layers[li].hidden=false;continue;}
918
+ if(o.op==="front"){S.layers[li].depth=nextDepth++;continue;}
919
+ if(o.op==="back"){S.layers[li].depth=Math.min(...S.placed.map(x=>S.layers[x].depth))-1;continue;}
920
+ const c=[S.mu[o.k][0],S.mu[o.k][1]];
921
+ if(o.op==="move"){applyGesture(o.k,gestureTranslate(o.dx,o.dy));touched.add(o.k);}
922
+ if(o.op==="moveto"){applyGesture(o.k,gestureTranslate(o.x-c[0],o.y-c[1]));touched.add(o.k);}
923
+ if(o.op==="rotate"){applyGesture(o.k,gestureRotateAbout(c,o.deg*Math.PI/180));touched.add(o.k);}
924
+ if(o.op==="scale"){applyGesture(o.k,gestureScaleAbout(c,o.ds));touched.add(o.k);}
925
+ }
926
+ for(const k of touched) resettle(k,S.mu[k]);
927
+ rebuildGroupBuffers();occlusionRefresh(true);renderScene();refreshOwnership();
928
+ drawOverlay();updatePanels();renderLayerList();
929
+ promptLog.textContent="Clamped: "+ops.map(o=>`${o.op} ${S.layers[S.placed[o.k]].name}`).join(", ")+
930
+ " — beliefs re-settled around the intervention.";
931
+ }
932
+ async function doPrompt(){
933
+ if(S.phase!=="bound"){promptLog.textContent="Bind first — there is nothing to address in a soup.";return;}
934
+ const text=document.getElementById("prompt").value.trim();
935
+ if(!text)return;
936
+ let ops=localParse(text);
937
+ if(!ops.length){
938
+ promptLog.textContent="Local grammar found nothing; asking Claude\u2026";
939
+ try{ops=await claudeParse(text);}
940
+ catch{promptLog.textContent="Claude unavailable outside claude.ai — try: move/rotate/scale/hide/front/back + a layer name.";return;}
941
+ }
942
+ execOps(ops);
943
+ }
944
+ document.getElementById("btn-prompt").addEventListener("click",doPrompt);
945
+ document.getElementById("prompt").addEventListener("keydown",e=>{if(e.key==="Enter")doPrompt();});
946
+ document.getElementById("auto-occ").addEventListener("change",()=>{
947
+ if(S.phase==="bound"){occlusionRefresh(false);renderScene();}
948
+ });
949
+
950
+ /* ------------- boot ------------- */
951
+ document.getElementById("btn-scramble").addEventListener("click",scramble);
952
+ document.getElementById("btn-bind").addEventListener("click",()=>bind(true));
953
+ addLayer("tractor",BUILTINS.tractor,false);
954
+ addLayer("star",BUILTINS.star,false);
955
+ addLayer("boat",BUILTINS.boat,false);
956
+ S.layers[0].placed=true;S.layers[0].depth=nextDepth++;S.layers[0].pose=[0.3,-0.25,0.3,Math.log(0.55)];
957
+ S.layers[1].placed=true;S.layers[1].depth=nextDepth++;S.layers[1].pose=[-0.4,0.35,-0.9,Math.log(0.5)];
958
+ fitStage();
959
+ rebuildScene();
960
+ </script>
961
+ </body>
962
+ </html>
tests_studio.js ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Studio battery — the big-prize claims, tested headless:
2
+ 1. K=3 bind where the third layer came from the LIVE SERVER (image->layer)
3
+ 2. depth auto-occlusion: drag star behind tractor -> atoms lose evidence,
4
+ marginals honestly uncertain, identity persists; move away -> rebinds
5
+ 3. painter compositor: the front layer wins where it covers
6
+ */
7
+ "use strict";
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const C = require("./studio/core.js");
11
+ const S = require("./studio/studio_core.js");
12
+ const B = JSON.parse(fs.readFileSync(__dirname + "/studio/builtins.json"));
13
+ let pass = 0, fail = 0;
14
+ const check = (n, ok, d) => { console.log((ok ? "GO " : "FAIL ") + n + (d ? " [" + d + "]" : "")); ok ? pass++ : fail++; };
15
+
16
+ const templates = [B.tractor, B.star, B.boat];
17
+ const K = 3;
18
+
19
+ function argAssign(marg, prev, evid) {
20
+ return marg.map((m, i) => {
21
+ if (prev && prev[i] >= 0 && !evid[i]) return prev[i];
22
+ let b = 0;
23
+ for (let k = 1; k <= K; k++) if (m[k] > m[b]) b = k;
24
+ return b === K ? -1 : b;
25
+ });
26
+ }
27
+
28
+ // ---- scene: three layers + clutter, seeded
29
+ let st = 777; const rand = () => { st = (st * 1103515245 + 12345) & 0x7fffffff; return st / 0x7fffffff; };
30
+ const TAU = Math.PI * 2;
31
+ const xis = [[0.35, -0.3, 0.9, 0.0], [-0.45, 0.35, -2.1, -0.12], [0.05, 0.45, 0.4, -0.25]];
32
+ const obs = [], gt = [];
33
+ for (let k = 0; k < K; k++)
34
+ for (const a of C.transformAtoms(templates[k], xis[k])) { obs.push(a); gt.push(k); }
35
+ for (let i = 0; i < 15; i++) {
36
+ const su = 0.04 + 0.08 * rand();
37
+ obs.push([-0.95 + 1.9 * rand(), -0.95 + 1.9 * rand(), -Math.PI / 2 + Math.PI * rand(),
38
+ su, su * (0.4 + 0.5 * rand()), 4 + 10 * rand(), TAU * rand(), rand(), rand(), rand()]);
39
+ gt.push(-1);
40
+ }
41
+ const NS = [1, 1, 1, .3, .3, 20, 3, .5, .5, .5];
42
+ for (const a of obs) for (let q = 0; q < 10; q++) {
43
+ const g = Math.sqrt(-2 * Math.log(rand() + 1e-9)) * Math.cos(TAU * rand());
44
+ a[q] += g * 0.006 * NS[q];
45
+ }
46
+ for (const a of obs) { a[3] = Math.max(a[3], .012); a[4] = Math.max(a[4], .008); a[5] = Math.max(a[5], .5); }
47
+
48
+ // ---- 1. dynamic K=3 bind, third template fresh from the generation path
49
+ let r = C.bpBind(templates, obs, { iters: 40 });
50
+ let evid = obs.map(() => true);
51
+ let assign = argAssign(r.marg, null, evid);
52
+ {
53
+ let ok = 0, okBoat = 0, nBoat = 0;
54
+ for (let i = 0; i < gt.length; i++) {
55
+ if (assign[i] === gt[i]) ok++;
56
+ if (gt[i] === 2) { nBoat++; if (assign[i] === 2) okBoat++; }
57
+ }
58
+ check("K=3 bind with generated boat layer", ok / gt.length > 0.85,
59
+ (ok / gt.length).toFixed(3) + " overall");
60
+ check("boat atoms bind to boat", okBoat / nBoat > 0.85, okBoat + "/" + nBoat);
61
+ }
62
+
63
+ // ---- 2. depth auto-occlusion on TRIMMED object layers (studio semantics)
64
+ {
65
+ const trimmed = templates; // builtins are compact by construction (gray-fit)
66
+ const xisT = [[0.3, -0.25, 0.4, -0.6], [0.3, -0.25, -1.0, -0.6], [-0.45, 0.45, 0.2, -0.6]];
67
+ const obsT = [], gtT = [];
68
+ for (let k = 0; k < K; k++)
69
+ for (const a of C.transformAtoms(trimmed[k], xisT[k])) { obsT.push(a); gtT.push(k); }
70
+ // star placed ON the tractor; tractor in front by depth
71
+ const rT = C.bpBind(trimmed, obsT, { iters: 30 });
72
+ let evT = obsT.map(() => true);
73
+ let asT = argAssign(rT.marg, null, evT);
74
+
75
+ const H = 128;
76
+ const layers = [
77
+ { depth: 2, hidden: false }, // tractor front
78
+ { depth: 1, hidden: false }, // star behind
79
+ { depth: 0, hidden: false }, // boat back, far away
80
+ ];
81
+ const alphaBufs = layers.map((_, k) =>
82
+ S.alphaFromPre(C.renderPre(obsT.filter((_, i) => asT[i] === k), H), H));
83
+ const { mask, covFrac } = S.autoHidden(obsT, asT, layers, alphaBufs, H);
84
+
85
+ check("star (behind, overlapped) loses evidence", covFrac[1] > 0.5,
86
+ (100 * covFrac[1]).toFixed(0) + "% of star atoms covered");
87
+ check("tractor (front) keeps evidence", covFrac[0] < 0.25,
88
+ (100 * covFrac[0]).toFixed(0) + "% covered");
89
+ check("boat (far away) keeps evidence", covFrac[2] < 0.1,
90
+ (100 * covFrac[2]).toFixed(0) + "% covered");
91
+
92
+ const wasStar = asT.map(a => a === 1);
93
+ const r2 = C.bpBind(trimmed, obsT, {
94
+ iters: 15, hiddenMask: mask, clampPose: [null, rT.mu[1], null] });
95
+ evT = mask.map(m => !m);
96
+ const as2 = argAssign(r2.marg, asT, evT);
97
+ const ent = C.atomEntropy(r2.marg);
98
+ let hEnt = 0, nH = 0, kept = 0, nWas = 0;
99
+ for (let i = 0; i < gtT.length; i++) {
100
+ if (mask[i]) { hEnt += ent[i]; nH++; }
101
+ if (wasStar[i] && mask[i]) { nWas++; if (as2[i] === 1) kept++; }
102
+ }
103
+ check("covered atoms honestly uncertain", hEnt / nH > 1.5,
104
+ (hEnt / nH).toFixed(2) + " bits");
105
+ check("identity persists under the tractor", kept === nWas,
106
+ kept + "/" + nWas + " covered star atoms stay star");
107
+
108
+ // move the star away: coverage releases, full rebind recovers
109
+ const g2 = S.gestureTranslate(-0.7, -0.35);
110
+ for (let i = 0; i < obsT.length; i++)
111
+ if (as2[i] === 1) obsT[i] = C.transformAtoms([obsT[i]], g2)[0];
112
+ const alphaBufs2 = layers.map((_, k) =>
113
+ S.alphaFromPre(C.renderPre(obsT.filter((_, i) => as2[i] === k), H), H));
114
+ const { covFrac: cf2 } = S.autoHidden(obsT, as2, layers, alphaBufs2, H);
115
+ check("coverage releases when moved away", cf2[1] < 0.15,
116
+ (100 * cf2[1]).toFixed(0) + "%");
117
+ const r3 = C.bpBind(trimmed, obsT, { iters: 25 });
118
+ const as3 = argAssign(r3.marg, null, obsT.map(() => true));
119
+ let ok = 0;
120
+ for (let i = 0; i < gtT.length; i++) if (as3[i] === gtT[i]) ok++;
121
+ check("re-emergence from behind rebinds", ok / gtT.length > 0.85,
122
+ (ok / gtT.length).toFixed(3));
123
+ }
124
+
125
+ // ---- 3. painter compositor: depth order decides the overlap
126
+ {
127
+ const H = 96;
128
+ const red = [[-0.3, 0, 0, 0.28, 0.28, 0.05, 0, 1.5, -1.5, -1.5]];
129
+ const blue = [[ 0.3, 0, 0, 0.28, 0.28, 0.05, 0, -1.5, -1.5, 1.5]];
130
+ const rgb = [C.renderPre(red, H), C.renderPre(blue, H)];
131
+ const env = [S.alphaFromPre(rgb[0], H), S.alphaFromPre(rgb[1], H)];
132
+ const pick = (out, x, y) => {
133
+ const px = Math.round((x + 1) / 2 * (H - 1)), py = Math.round((y + 1) / 2 * (H - 1));
134
+ const i = 4 * (py * H + px);
135
+ return [out[i], out[i + 1], out[i + 2]];
136
+ };
137
+ const blueFront = S.compositePainter([0, 1], rgb, env, H);
138
+ const redFront = S.compositePainter([1, 0], rgb, env, H);
139
+ const a = pick(blueFront, 0.3, 0), b = pick(blueFront, -0.3, 0);
140
+ check("front layer wins at its center", a[2] > 170 && a[0] < 90,
141
+ "rgb(" + a.join(",") + ")");
142
+ check("back layer shows where uncovered", b[0] > 170 && b[2] < 90,
143
+ "rgb(" + b.join(",") + ")");
144
+ const m1 = pick(blueFront, 0, 0), m2 = pick(redFront, 0, 0);
145
+ check("depth order decides the overlap", m1[2] > m2[2] && m2[0] > m1[0],
146
+ "blue-front rgb(" + m1.join(",") + ") vs red-front rgb(" + m2.join(",") + ")");
147
+ }
148
+
149
+ console.log("\n" + pass + " GO, " + fail + " FAIL");
150
+ process.exit(fail ? 1 : 0);
trim_check.png ADDED