Spaces:
Running on Zero
Running on Zero
Deploy Karate Wiener (kimodo kata maker)
Browse files- app.py +51 -0
- comic_shots.py +79 -0
- tabs/dojo.drawer.html +63 -63
- tabs/dojo.tab.css +138 -4
- tabs/dojo.tab.js +292 -139
app.py
CHANGED
|
@@ -133,6 +133,8 @@ _DEFAULT_SCENE_PATH = Path(__file__).parent / "assets" / "default_scene.webp"
|
|
| 133 |
DIT360_SPACE = os.environ.get("KIMODO_DIT360_SPACE", "Insta360-Research/DiT360").strip()
|
| 134 |
KLEIN_SPACE = os.environ.get("KIMODO_KLEIN_SPACE", "polats/tiny-army-klein-zerogpu").strip()
|
| 135 |
TRIPOSPLAT_SPACE = os.environ.get("KIMODO_TRIPOSPLAT_SPACE", "polats/kimodo-triposplat-zerogpu").strip()
|
|
|
|
|
|
|
| 136 |
|
| 137 |
# --- Pluggable side tabs ---------------------------------------------------
|
| 138 |
# A NEW side tab is added by DROPPING FILES into ./tabs — NO app.py edit needed,
|
|
@@ -3571,6 +3573,45 @@ def generate_dojo_scene(payload_json: str | dict | None) -> str:
|
|
| 3571 |
return
|
| 3572 |
|
| 3573 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3574 |
def _copy_creator_fields(dst: dict, src: dict | None) -> dict:
|
| 3575 |
if not src:
|
| 3576 |
return dst
|
|
@@ -6466,6 +6507,10 @@ with gr.Blocks(title="Kimodo ZeroGPU", css=_BLOCKS_CSS) as demo:
|
|
| 6466 |
dojo_payload = gr.Textbox(value="", label="Dojo payload", elem_id="dojo-payload", container=False)
|
| 6467 |
dojo_result = gr.Textbox(value="", label="Dojo result", elem_id="dojo-result", container=False)
|
| 6468 |
dojo_btn = gr.Button("Generate dojo scene", elem_id="dojo-btn")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6469 |
# Viewer is the only visible component — it fills the screen. Everything else
|
| 6470 |
# is wrapped in .kimodo-chrome and hidden (kept in the DOM so the drawers can
|
| 6471 |
# still read its inputs / fire its hidden buttons).
|
|
@@ -6608,6 +6653,12 @@ with gr.Blocks(title="Kimodo ZeroGPU", css=_BLOCKS_CSS) as demo:
|
|
| 6608 |
outputs=[dojo_result],
|
| 6609 |
api_name=False,
|
| 6610 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6611 |
|
| 6612 |
# Display model drives the persistent viewer live via postMessage (no reload).
|
| 6613 |
# Clothing is handled by the fixed browser drawer so it can behave like kata's drawers.
|
|
|
|
| 133 |
DIT360_SPACE = os.environ.get("KIMODO_DIT360_SPACE", "Insta360-Research/DiT360").strip()
|
| 134 |
KLEIN_SPACE = os.environ.get("KIMODO_KLEIN_SPACE", "polats/tiny-army-klein-zerogpu").strip()
|
| 135 |
TRIPOSPLAT_SPACE = os.environ.get("KIMODO_TRIPOSPLAT_SPACE", "polats/kimodo-triposplat-zerogpu").strip()
|
| 136 |
+
# tiny-aya (Cohere Tiny Aya) text model — used to suggest/improve dojo scene prompts.
|
| 137 |
+
TINY_AYA_SPACE = os.environ.get("KIMODO_TINY_AYA_SPACE", "polats/tiny-army-tiny-aya-zerogpu").strip()
|
| 138 |
|
| 139 |
# --- Pluggable side tabs ---------------------------------------------------
|
| 140 |
# A NEW side tab is added by DROPPING FILES into ./tabs — NO app.py edit needed,
|
|
|
|
| 3573 |
return
|
| 3574 |
|
| 3575 |
|
| 3576 |
+
def improve_dojo_prompt(payload_json: str | dict | None) -> str:
|
| 3577 |
+
"""Use tiny-aya to turn a few keywords (or nothing) into a full dojo scene prompt.
|
| 3578 |
+
Returns JSON: {"ok": true, "prompt": "..."} or {"ok": false, "error": "..."}."""
|
| 3579 |
+
raw = payload_json
|
| 3580 |
+
if not isinstance(raw, dict):
|
| 3581 |
+
try:
|
| 3582 |
+
raw = json.loads(raw or "{}")
|
| 3583 |
+
except Exception:
|
| 3584 |
+
raw = {"words": str(payload_json or "")}
|
| 3585 |
+
words = (raw.get("words") or raw.get("prompt") or "").strip()
|
| 3586 |
+
system = (
|
| 3587 |
+
"You write a single vivid text-to-image prompt for a 360-degree room used as a "
|
| 3588 |
+
"martial-arts dojo backdrop. Reply with ONLY the prompt on one line: no preamble, "
|
| 3589 |
+
"no quotes, under 55 words. Describe a full room interior (floor, walls, lighting) "
|
| 3590 |
+
"and end with: full room visible, high detail, no people, no text."
|
| 3591 |
+
)
|
| 3592 |
+
user = (f"Turn these ideas into a dojo scene prompt: {words}" if words
|
| 3593 |
+
else "Invent a creative, unexpected dojo scene prompt.")
|
| 3594 |
+
if not TINY_AYA_SPACE:
|
| 3595 |
+
return json.dumps({"ok": False, "error": "tiny-aya space not configured"})
|
| 3596 |
+
try:
|
| 3597 |
+
from gradio_client import Client
|
| 3598 |
+
client = Client(TINY_AYA_SPACE, token=_hf_token())
|
| 3599 |
+
out = client.predict(system, user, 220, 0.9, api_name="/generate")
|
| 3600 |
+
text = (out or "").strip()
|
| 3601 |
+
# Prefer the first substantial line (drops any stray preamble the model adds).
|
| 3602 |
+
best = ""
|
| 3603 |
+
for line in text.splitlines():
|
| 3604 |
+
cand = line.strip().strip('"').strip("'").lstrip("-*•0123456789. ").strip()
|
| 3605 |
+
if len(cand) > len(best):
|
| 3606 |
+
best = cand
|
| 3607 |
+
text = " ".join((best or text).split())[:500]
|
| 3608 |
+
if not text:
|
| 3609 |
+
return json.dumps({"ok": False, "error": "empty suggestion"})
|
| 3610 |
+
return json.dumps({"ok": True, "prompt": text})
|
| 3611 |
+
except Exception as exc:
|
| 3612 |
+
return json.dumps({"ok": False, "error": str(exc)[:180]})
|
| 3613 |
+
|
| 3614 |
+
|
| 3615 |
def _copy_creator_fields(dst: dict, src: dict | None) -> dict:
|
| 3616 |
if not src:
|
| 3617 |
return dst
|
|
|
|
| 6507 |
dojo_payload = gr.Textbox(value="", label="Dojo payload", elem_id="dojo-payload", container=False)
|
| 6508 |
dojo_result = gr.Textbox(value="", label="Dojo result", elem_id="dojo-result", container=False)
|
| 6509 |
dojo_btn = gr.Button("Generate dojo scene", elem_id="dojo-btn")
|
| 6510 |
+
# tiny-aya prompt helper: JSON {"words": "..."} -> JSON {"ok", "prompt"}.
|
| 6511 |
+
dojo_suggest_payload = gr.Textbox(value="", label="Dojo suggest payload", elem_id="dojo-suggest-payload", container=False)
|
| 6512 |
+
dojo_suggest_result = gr.Textbox(value="", label="Dojo suggest result", elem_id="dojo-suggest-result", container=False)
|
| 6513 |
+
dojo_suggest_btn = gr.Button("Suggest dojo prompt", elem_id="dojo-suggest-btn")
|
| 6514 |
# Viewer is the only visible component — it fills the screen. Everything else
|
| 6515 |
# is wrapped in .kimodo-chrome and hidden (kept in the DOM so the drawers can
|
| 6516 |
# still read its inputs / fire its hidden buttons).
|
|
|
|
| 6653 |
outputs=[dojo_result],
|
| 6654 |
api_name=False,
|
| 6655 |
)
|
| 6656 |
+
dojo_suggest_btn.click(
|
| 6657 |
+
fn=improve_dojo_prompt,
|
| 6658 |
+
inputs=[dojo_suggest_payload],
|
| 6659 |
+
outputs=[dojo_suggest_result],
|
| 6660 |
+
api_name=False,
|
| 6661 |
+
)
|
| 6662 |
|
| 6663 |
# Display model drives the persistent viewer live via postMessage (no reload).
|
| 6664 |
# Clothing is handled by the fixed browser drawer so it can behave like kata's drawers.
|
comic_shots.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shot grammar for the 4-koma stance comics: named shots -> deterministic camera,
|
| 2 |
+
computed purely from the skeleton frame (bbox + hip heading). The LLM never sees any
|
| 3 |
+
of this; it only ever picks captions. Cameras are plain pinhole (eye/target/fov/roll)
|
| 4 |
+
so the same parameters port 1:1 to the three.js viewer later."""
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
PARENTS = [-1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 12, 13, 14, 16, 17, 18, 19]
|
| 8 |
+
PEL, LHIP, RHIP, HEAD = 0, 1, 2, 15
|
| 9 |
+
LEFTJ = {1, 4, 7, 10, 13, 16, 18, 20}
|
| 10 |
+
RIGHTJ = {2, 5, 8, 11, 14, 17, 19, 21}
|
| 11 |
+
|
| 12 |
+
# az = horizontal angle of the camera relative to the character's facing direction
|
| 13 |
+
# (0 = head-on). el = elevation in degrees (negative = camera low, looking up).
|
| 14 |
+
# margin = how much air around the joint bbox. roll = dutch tilt.
|
| 15 |
+
SHOTS = {
|
| 16 |
+
"establishing": dict(az=38.0, el=10.0, margin=1.55, roll=0.0, fov=40.0),
|
| 17 |
+
"action": dict(az=55.0, el=4.0, margin=1.22, roll=-4.0, fov=46.0),
|
| 18 |
+
"gag": dict(az=22.0, el=16.0, margin=1.12, roll=13.0, fov=50.0),
|
| 19 |
+
"hero": dict(az=32.0, el=-16.0, margin=1.42, roll=0.0, fov=38.0),
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def heading(Pf):
|
| 24 |
+
"""Facing angle about Y from the hip line (0 = facing +Z), as in _build_stances."""
|
| 25 |
+
d = Pf[RHIP] - Pf[LHIP]
|
| 26 |
+
return float(np.arctan2(d[2], -d[0]))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def make_camera(shot_name, Pf):
|
| 30 |
+
"""Camera dict for one skeleton frame [22,3]. Frames the joint bbox with the
|
| 31 |
+
shot's margin, oriented relative to the character's heading."""
|
| 32 |
+
s = SHOTS[shot_name]
|
| 33 |
+
lo, hi = Pf.min(axis=0), Pf.max(axis=0)
|
| 34 |
+
target = (lo + hi) / 2.0
|
| 35 |
+
radius = float(np.linalg.norm(Pf - target, axis=1).max())
|
| 36 |
+
fov = np.radians(s["fov"])
|
| 37 |
+
dist = s["margin"] * radius / np.tan(fov / 2.0)
|
| 38 |
+
theta = heading(Pf) + np.radians(s["az"])
|
| 39 |
+
el = np.radians(s["el"])
|
| 40 |
+
direction = np.array([np.sin(theta) * np.cos(el), np.sin(el), np.cos(theta) * np.cos(el)])
|
| 41 |
+
eye = target + direction * dist
|
| 42 |
+
# Don't let the camera dip below the floor (hero shots on grounded poses).
|
| 43 |
+
eye[1] = max(eye[1], 0.12)
|
| 44 |
+
return dict(eye=eye, target=target, fov=fov, roll=np.radians(s["roll"]))
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def project(points, cam, W, H):
|
| 48 |
+
"""Pinhole-project [N,3] world points -> ([N,2] pixel coords, [N] view depth)."""
|
| 49 |
+
pts = np.asarray(points, np.float64).reshape(-1, 3)
|
| 50 |
+
eye, target = cam["eye"], cam["target"]
|
| 51 |
+
fwd = target - eye
|
| 52 |
+
fwd = fwd / np.linalg.norm(fwd)
|
| 53 |
+
right = np.cross(fwd, np.array([0.0, 1.0, 0.0]))
|
| 54 |
+
right = right / (np.linalg.norm(right) + 1e-9)
|
| 55 |
+
up = np.cross(right, fwd)
|
| 56 |
+
r = cam["roll"]
|
| 57 |
+
if abs(r) > 1e-6: # dutch tilt: rotate the basis about the view axis
|
| 58 |
+
c, s = np.cos(r), np.sin(r)
|
| 59 |
+
right, up = c * right + s * up, -s * right + c * up
|
| 60 |
+
rel = pts - eye
|
| 61 |
+
x = rel @ right
|
| 62 |
+
y = rel @ up
|
| 63 |
+
z = rel @ fwd
|
| 64 |
+
z = np.maximum(z, 1e-4)
|
| 65 |
+
f = (H / 2.0) / np.tan(cam["fov"] / 2.0)
|
| 66 |
+
px = W / 2.0 + f * x / z
|
| 67 |
+
py = H / 2.0 - f * y / z
|
| 68 |
+
return np.stack([px, py], axis=1), z
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def ground_grid(center, span=2.6, step=0.65):
|
| 72 |
+
"""3D segments of a floor grid (y=0) around the character, for scene depth."""
|
| 73 |
+
segs = []
|
| 74 |
+
cx, cz = float(center[0]), float(center[2])
|
| 75 |
+
ticks = np.arange(-span, span + 1e-6, step)
|
| 76 |
+
for t in ticks:
|
| 77 |
+
segs.append(((cx - span, 0.0, cz + t), (cx + span, 0.0, cz + t)))
|
| 78 |
+
segs.append(((cx + t, 0.0, cz - span), (cx + t, 0.0, cz + span)))
|
| 79 |
+
return segs
|
tabs/dojo.drawer.html
CHANGED
|
@@ -1,73 +1,73 @@
|
|
| 1 |
-
<aside id="kimodo-dojo-drawer" class="kimodo-drawer" aria-label="Dojo
|
| 2 |
<h2>Dojo</h2>
|
| 3 |
-
<div class="kimodo-drawer-sub">Create a room scene for the viewer.</div>
|
| 4 |
|
| 5 |
-
<
|
| 6 |
-
|
| 7 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
</div>
|
| 9 |
|
| 10 |
-
<
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
</label>
|
| 15 |
-
<label class="kimodo-dojo-range" for="kimodo-dojo-room-size">
|
| 16 |
-
<span>Room size <output id="kimodo-dojo-room-size-value">8.0</output>m</span>
|
| 17 |
-
<input id="kimodo-dojo-room-size" type="range" min="3" max="14" value="8" step="0.5" />
|
| 18 |
-
</label>
|
| 19 |
-
<label class="kimodo-dojo-range" for="kimodo-dojo-room-height">
|
| 20 |
-
<span>Room height <output id="kimodo-dojo-room-height-value">3.2</output>m</span>
|
| 21 |
-
<input id="kimodo-dojo-room-height" type="range" min="2" max="6" value="3.2" step="0.2" />
|
| 22 |
-
</label>
|
| 23 |
-
</div>
|
| 24 |
-
|
| 25 |
-
<label class="kimodo-dojo-label" for="kimodo-dojo-prompt">Scene prompt</label>
|
| 26 |
-
<textarea id="kimodo-dojo-prompt" class="kimodo-dojo-prompt" rows="5"
|
| 27 |
-
spellcheck="true">A traditional martial arts dojo room, polished wooden floor, training mats, wooden wall panels, warm lantern light, full room visible, high detail, no people, no text.</textarea>
|
| 28 |
|
| 29 |
-
|
| 30 |
-
<
|
| 31 |
-
|
| 32 |
-
<button class="kimodo-dojo-
|
| 33 |
-
|
| 34 |
-
<button class="kimodo-dojo-preset" type="button"
|
| 35 |
-
data-prompt="A futuristic neon martial arts training room, glossy floor, holographic wall panels, dramatic rim light, full room visible, no people, no text.">Neon dojo</button>
|
| 36 |
-
</div>
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
<
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
<figcaption>
|
| 65 |
-
|
| 66 |
-
</
|
| 67 |
-
<figure>
|
| 68 |
-
<figcaption>360</figcaption>
|
| 69 |
-
<img id="kimodo-dojo-preview-pano" alt="Flux 360 source result" />
|
| 70 |
-
</figure>
|
| 71 |
</div>
|
| 72 |
-
<div id="kimodo-dojo-status" class="kimodo-drawer-sub kimodo-dojo-status">Ready.</div>
|
| 73 |
</aside>
|
|
|
|
| 1 |
+
<aside id="kimodo-dojo-drawer" class="kimodo-drawer" aria-label="Dojo drawer">
|
| 2 |
<h2>Dojo</h2>
|
|
|
|
| 3 |
|
| 4 |
+
<!-- ============ LIBRARY VIEW: list of dojos (Mine / Community) ============ -->
|
| 5 |
+
<div id="kimodo-dojo-library" class="kimodo-dojo-view">
|
| 6 |
+
<div class="kimodo-cz-seg kimodo-cz-seg--sub" id="kimodo-dojo-scope" aria-label="Dojo scope">
|
| 7 |
+
<button type="button" class="kimodo-cz-segbtn active" data-scope="mine">Mine</button>
|
| 8 |
+
<button type="button" class="kimodo-cz-segbtn" data-scope="community">Community</button>
|
| 9 |
+
</div>
|
| 10 |
+
<button id="kimodo-dojo-add" class="kimodo-dojo-addbtn" type="button">+ Add dojo</button>
|
| 11 |
+
<div id="kimodo-dojo-grid" class="kimodo-dojo-cards" aria-label="Dojo list"></div>
|
| 12 |
+
<div id="kimodo-dojo-empty" class="kimodo-drawer-sub kimodo-dojo-empty"></div>
|
| 13 |
</div>
|
| 14 |
|
| 15 |
+
<!-- ============ CREATE VIEW: prompt + suggest + generate + progress ======= -->
|
| 16 |
+
<div id="kimodo-dojo-create" class="kimodo-dojo-view" hidden>
|
| 17 |
+
<button id="kimodo-dojo-back" class="kimodo-dojo-back" type="button" title="Back to dojos">‹ Dojos</button>
|
| 18 |
+
<div class="kimodo-drawer-sub">Describe a room scene for the viewer.</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
<label class="kimodo-dojo-label" for="kimodo-dojo-prompt">Scene prompt</label>
|
| 21 |
+
<textarea id="kimodo-dojo-prompt" class="kimodo-dojo-prompt" rows="5"
|
| 22 |
+
spellcheck="true" placeholder="A few words, or leave blank and tap Suggest…">A traditional martial arts dojo room, polished wooden floor, training mats, wooden wall panels, warm lantern light, full room visible, high detail, no people, no text.</textarea>
|
| 23 |
+
<button id="kimodo-dojo-improve" class="kimodo-dojo-improve" type="button"
|
| 24 |
+
title="Improve the prompt with tiny-aya (or suggest one if empty)">✦ Improve with tiny-aya</button>
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
<details class="kimodo-dojo-adv">
|
| 27 |
+
<summary>Advanced settings</summary>
|
| 28 |
+
<div class="kimodo-dojo-adv-body">
|
| 29 |
+
<div class="kimodo-cz-seg kimodo-dojo-mode" id="kimodo-dojo-mode" aria-label="Dojo scene type">
|
| 30 |
+
<button type="button" class="kimodo-cz-segbtn active" data-mode="splat">Splat</button>
|
| 31 |
+
<button type="button" class="kimodo-cz-segbtn" data-mode="panorama">Panorama</button>
|
| 32 |
+
</div>
|
| 33 |
+
<div class="kimodo-dojo-settings" aria-label="Dojo room settings">
|
| 34 |
+
<label class="kimodo-dojo-check">
|
| 35 |
+
<input id="kimodo-dojo-skybox" type="checkbox" />
|
| 36 |
+
<span>Use panorama as skybox in splat</span>
|
| 37 |
+
</label>
|
| 38 |
+
<label class="kimodo-dojo-range" for="kimodo-dojo-room-size">
|
| 39 |
+
<span>Room size <output id="kimodo-dojo-room-size-value">8.0</output>m</span>
|
| 40 |
+
<input id="kimodo-dojo-room-size" type="range" min="3" max="14" value="8" step="0.5" />
|
| 41 |
+
</label>
|
| 42 |
+
<label class="kimodo-dojo-range" for="kimodo-dojo-room-height">
|
| 43 |
+
<span>Room height <output id="kimodo-dojo-room-height-value">3.2</output>m</span>
|
| 44 |
+
<input id="kimodo-dojo-room-height" type="range" min="2" max="6" value="3.2" step="0.2" />
|
| 45 |
+
</label>
|
| 46 |
+
</div>
|
| 47 |
+
<div class="kimodo-dojo-grid2">
|
| 48 |
+
<label><span>Seed</span><input id="kimodo-dojo-seed" type="number" value="-1" step="1" /></label>
|
| 49 |
+
<label><span>Steps</span><input id="kimodo-dojo-steps" type="number" value="28" min="1" max="100" step="1" /></label>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
</details>
|
| 53 |
|
| 54 |
+
<div class="kimodo-dojo-actions">
|
| 55 |
+
<button id="kimodo-dojo-generate" class="kimodo-dojo-primary" type="button">Generate Scene</button>
|
| 56 |
+
<button id="kimodo-dojo-clear" type="button">Clear</button>
|
| 57 |
+
</div>
|
| 58 |
|
| 59 |
+
<div id="kimodo-dojo-progress" class="kimodo-dojo-progress" aria-label="Dojo generation progress">
|
| 60 |
+
<div class="kimodo-dojo-step" data-step="flux-iso"><span></span>Flux isometric</div>
|
| 61 |
+
<div class="kimodo-dojo-step" data-step="flux-pano"><span></span>Flux 360</div>
|
| 62 |
+
<div class="kimodo-dojo-step" data-step="splat"><span></span>TripoSplat</div>
|
| 63 |
+
<div class="kimodo-dojo-step" data-step="viewer"><span></span>Viewer load</div>
|
| 64 |
+
</div>
|
| 65 |
+
<div id="kimodo-dojo-time" class="kimodo-dojo-time" aria-live="polite"></div>
|
| 66 |
+
<div id="kimodo-dojo-detail" class="kimodo-dojo-detail" aria-live="polite"></div>
|
| 67 |
+
<div id="kimodo-dojo-preview-wrap" class="kimodo-dojo-preview-wrap">
|
| 68 |
+
<figure><figcaption>Isometric</figcaption><img id="kimodo-dojo-preview" alt="Flux isometric result" /></figure>
|
| 69 |
+
<figure><figcaption>360</figcaption><img id="kimodo-dojo-preview-pano" alt="Flux 360 result" /></figure>
|
| 70 |
+
</div>
|
| 71 |
+
<div id="kimodo-dojo-status" class="kimodo-drawer-sub kimodo-dojo-status">Ready.</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
</div>
|
|
|
|
| 73 |
</aside>
|
tabs/dojo.tab.css
CHANGED
|
@@ -101,19 +101,19 @@
|
|
| 101 |
.kimodo-dojo-actions button:hover:not(:disabled) {
|
| 102 |
background: #3f3f48 !important;
|
| 103 |
}
|
| 104 |
-
.kimodo-dojo-
|
| 105 |
display: grid;
|
| 106 |
grid-template-columns: 1fr 1fr;
|
| 107 |
gap: 8px;
|
| 108 |
-
margin: 10px 0;
|
| 109 |
}
|
| 110 |
-
.kimodo-dojo-
|
| 111 |
display: grid;
|
| 112 |
gap: 4px;
|
| 113 |
font-size: 11px;
|
| 114 |
color: #b8bec6;
|
| 115 |
}
|
| 116 |
-
.kimodo-dojo-
|
| 117 |
width: 100%;
|
| 118 |
box-sizing: border-box;
|
| 119 |
padding: 6px 7px;
|
|
@@ -226,3 +226,137 @@
|
|
| 226 |
aspect-ratio: 1 / 1;
|
| 227 |
object-fit: cover;
|
| 228 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
.kimodo-dojo-actions button:hover:not(:disabled) {
|
| 102 |
background: #3f3f48 !important;
|
| 103 |
}
|
| 104 |
+
.kimodo-dojo-grid2 {
|
| 105 |
display: grid;
|
| 106 |
grid-template-columns: 1fr 1fr;
|
| 107 |
gap: 8px;
|
| 108 |
+
margin: 10px 0 2px;
|
| 109 |
}
|
| 110 |
+
.kimodo-dojo-grid2 label {
|
| 111 |
display: grid;
|
| 112 |
gap: 4px;
|
| 113 |
font-size: 11px;
|
| 114 |
color: #b8bec6;
|
| 115 |
}
|
| 116 |
+
.kimodo-dojo-grid2 input {
|
| 117 |
width: 100%;
|
| 118 |
box-sizing: border-box;
|
| 119 |
padding: 6px 7px;
|
|
|
|
| 226 |
aspect-ratio: 1 / 1;
|
| 227 |
object-fit: cover;
|
| 228 |
}
|
| 229 |
+
|
| 230 |
+
/* ============ Library / create restructure ============ */
|
| 231 |
+
.kimodo-dojo-view[hidden] { display: none; }
|
| 232 |
+
.kimodo-dojo-addbtn {
|
| 233 |
+
width: 100%;
|
| 234 |
+
padding: 9px;
|
| 235 |
+
margin-bottom: 10px;
|
| 236 |
+
font-size: 12.5px;
|
| 237 |
+
font-weight: 600;
|
| 238 |
+
cursor: pointer;
|
| 239 |
+
border-radius: 8px;
|
| 240 |
+
background: #4a3320 !important;
|
| 241 |
+
color: #ffe0ad !important;
|
| 242 |
+
border: 1px solid #b97736 !important;
|
| 243 |
+
box-shadow: none !important;
|
| 244 |
+
}
|
| 245 |
+
.kimodo-dojo-addbtn:hover { background: #5a3f27 !important; }
|
| 246 |
+
.kimodo-dojo-cards {
|
| 247 |
+
display: grid;
|
| 248 |
+
grid-template-columns: 1fr 1fr;
|
| 249 |
+
gap: 8px;
|
| 250 |
+
}
|
| 251 |
+
.kimodo-dojo-card {
|
| 252 |
+
position: relative;
|
| 253 |
+
cursor: pointer;
|
| 254 |
+
border-radius: 8px;
|
| 255 |
+
overflow: hidden;
|
| 256 |
+
background: #202026;
|
| 257 |
+
border: 1px solid #34343c;
|
| 258 |
+
}
|
| 259 |
+
.kimodo-dojo-card:hover { border-color: #b97736; }
|
| 260 |
+
.kimodo-dojo-card .thumb {
|
| 261 |
+
display: block;
|
| 262 |
+
width: 100%;
|
| 263 |
+
aspect-ratio: 1 / 1;
|
| 264 |
+
object-fit: cover;
|
| 265 |
+
background: #15151a;
|
| 266 |
+
}
|
| 267 |
+
.kimodo-dojo-card .ph {
|
| 268 |
+
display: flex;
|
| 269 |
+
align-items: center;
|
| 270 |
+
justify-content: center;
|
| 271 |
+
width: 100%;
|
| 272 |
+
aspect-ratio: 1 / 1;
|
| 273 |
+
font-size: 26px;
|
| 274 |
+
color: #6a5a3a;
|
| 275 |
+
background: #1b1b20;
|
| 276 |
+
}
|
| 277 |
+
.kimodo-dojo-card .nm {
|
| 278 |
+
padding: 5px 7px;
|
| 279 |
+
font-size: 10.5px;
|
| 280 |
+
line-height: 1.25;
|
| 281 |
+
color: #d7d3c8;
|
| 282 |
+
white-space: nowrap;
|
| 283 |
+
overflow: hidden;
|
| 284 |
+
text-overflow: ellipsis;
|
| 285 |
+
}
|
| 286 |
+
.kimodo-dojo-card .badge {
|
| 287 |
+
position: absolute;
|
| 288 |
+
left: 5px;
|
| 289 |
+
top: 5px;
|
| 290 |
+
padding: 1px 5px;
|
| 291 |
+
border-radius: 4px;
|
| 292 |
+
font-size: 8.5px;
|
| 293 |
+
font-weight: 700;
|
| 294 |
+
letter-spacing: .03em;
|
| 295 |
+
background: rgba(0, 0, 0, 0.6);
|
| 296 |
+
color: #ffd28a;
|
| 297 |
+
}
|
| 298 |
+
.kimodo-dojo-card .del {
|
| 299 |
+
position: absolute;
|
| 300 |
+
top: 4px;
|
| 301 |
+
right: 5px;
|
| 302 |
+
padding: 1px 5px;
|
| 303 |
+
font-size: 12px;
|
| 304 |
+
line-height: 1;
|
| 305 |
+
cursor: pointer;
|
| 306 |
+
color: #ccc !important;
|
| 307 |
+
background: rgba(0, 0, 0, 0.55) !important;
|
| 308 |
+
border: 0 !important;
|
| 309 |
+
border-radius: 5px;
|
| 310 |
+
box-shadow: none !important;
|
| 311 |
+
}
|
| 312 |
+
.kimodo-dojo-card .del:hover { color: #ff9a9a !important; }
|
| 313 |
+
.kimodo-dojo-empty {
|
| 314 |
+
margin-top: 12px;
|
| 315 |
+
text-align: center;
|
| 316 |
+
color: #87909b;
|
| 317 |
+
}
|
| 318 |
+
.kimodo-dojo-back {
|
| 319 |
+
padding: 4px 2px;
|
| 320 |
+
margin-bottom: 4px;
|
| 321 |
+
font-size: 13px;
|
| 322 |
+
cursor: pointer;
|
| 323 |
+
color: #ffd28a !important;
|
| 324 |
+
background: none !important;
|
| 325 |
+
border: 0 !important;
|
| 326 |
+
box-shadow: none !important;
|
| 327 |
+
}
|
| 328 |
+
.kimodo-dojo-improve {
|
| 329 |
+
width: 100%;
|
| 330 |
+
margin: 7px 0 4px;
|
| 331 |
+
padding: 7px;
|
| 332 |
+
font-size: 11.5px;
|
| 333 |
+
font-weight: 600;
|
| 334 |
+
cursor: pointer;
|
| 335 |
+
border-radius: 7px;
|
| 336 |
+
background: #2a2440 !important;
|
| 337 |
+
color: #cbb6ff !important;
|
| 338 |
+
border: 1px solid #4a3a6a !important;
|
| 339 |
+
box-shadow: none !important;
|
| 340 |
+
}
|
| 341 |
+
.kimodo-dojo-improve:hover:not(:disabled) { background: #342c52 !important; }
|
| 342 |
+
.kimodo-dojo-improve:disabled { opacity: .55; cursor: default; }
|
| 343 |
+
.kimodo-dojo-adv {
|
| 344 |
+
margin: 8px 0;
|
| 345 |
+
border: 1px solid #34343c;
|
| 346 |
+
border-radius: 7px;
|
| 347 |
+
background: #1d1d22;
|
| 348 |
+
overflow: hidden;
|
| 349 |
+
}
|
| 350 |
+
.kimodo-dojo-adv > summary {
|
| 351 |
+
padding: 8px 10px;
|
| 352 |
+
font-size: 11.5px;
|
| 353 |
+
font-weight: 600;
|
| 354 |
+
color: #b8bec6;
|
| 355 |
+
cursor: pointer;
|
| 356 |
+
list-style: none;
|
| 357 |
+
}
|
| 358 |
+
.kimodo-dojo-adv > summary::-webkit-details-marker { display: none; }
|
| 359 |
+
.kimodo-dojo-adv > summary::after { content: " ▾"; color: #7f8792; }
|
| 360 |
+
.kimodo-dojo-adv[open] > summary::after { content: " ▴"; }
|
| 361 |
+
.kimodo-dojo-adv-body { padding: 0 10px 8px; }
|
| 362 |
+
.kimodo-dojo-adv-body .kimodo-dojo-mode { margin-top: 2px; }
|
tabs/dojo.tab.js
CHANGED
|
@@ -2,9 +2,29 @@
|
|
| 2 |
let busy = false;
|
| 3 |
let dojoMode = 'splat';
|
| 4 |
let lastScene = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
function dojoEls() {
|
| 7 |
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
prompt: document.getElementById('kimodo-dojo-prompt'),
|
| 9 |
mode: document.getElementById('kimodo-dojo-mode'),
|
| 10 |
roomSize: document.getElementById('kimodo-dojo-room-size'),
|
|
@@ -91,10 +111,7 @@
|
|
| 91 |
if (els.detail) els.detail.textContent = bestProgressText(stage);
|
| 92 |
}
|
| 93 |
|
| 94 |
-
function hiddenInput(id) {
|
| 95 |
-
return inputInside(id);
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
function writeHidden(id, value) {
|
| 99 |
const input = hiddenInput(id);
|
| 100 |
if (!input) return false;
|
|
@@ -103,12 +120,20 @@
|
|
| 103 |
input.dispatchEvent(new Event('change', { bubbles: true }));
|
| 104 |
return true;
|
| 105 |
}
|
| 106 |
-
|
| 107 |
function readHidden(id) {
|
| 108 |
const input = hiddenInput(id);
|
| 109 |
return input ? input.value || '' : '';
|
| 110 |
}
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
function sendScene(scene) {
|
| 113 |
const frame = previewFrame();
|
| 114 |
if (!scene) return;
|
|
@@ -119,15 +144,6 @@
|
|
| 119 |
if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-scene', scene: payload, image: payload.image }, '*');
|
| 120 |
}
|
| 121 |
|
| 122 |
-
function roomSettings() {
|
| 123 |
-
const els = dojoEls();
|
| 124 |
-
return {
|
| 125 |
-
room_size: Math.max(1, Number(els.roomSize && els.roomSize.value) || 8),
|
| 126 |
-
room_height: Math.max(1, Number(els.roomHeight && els.roomHeight.value) || 3.2),
|
| 127 |
-
use_panorama_skybox: !!(els.skybox && els.skybox.checked)
|
| 128 |
-
};
|
| 129 |
-
}
|
| 130 |
-
|
| 131 |
function updateRoomLabels() {
|
| 132 |
const els = dojoEls();
|
| 133 |
const settings = roomSettings();
|
|
@@ -172,6 +188,7 @@
|
|
| 172 |
const els = dojoEls();
|
| 173 |
if (els.generate) els.generate.disabled = busy;
|
| 174 |
if (els.clear) els.clear.disabled = busy;
|
|
|
|
| 175 |
}
|
| 176 |
|
| 177 |
function waitForResult(previousValue, onStage) {
|
|
@@ -213,144 +230,280 @@
|
|
| 213 |
};
|
| 214 |
}
|
| 215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
function initDojo() {
|
| 217 |
const drawer = document.getElementById('kimodo-dojo-drawer');
|
| 218 |
const els = dojoEls();
|
| 219 |
-
if (!drawer || drawer.dataset.ready === '1') return;
|
| 220 |
drawer.dataset.ready = '1';
|
| 221 |
updateRoomLabels();
|
|
|
|
| 222 |
|
| 223 |
-
if (els.
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
['roomSize', 'roomHeight', 'skybox'].forEach((key) => {
|
| 235 |
const input = els[key];
|
| 236 |
if (!input) return;
|
| 237 |
-
input.addEventListener('input', () => {
|
| 238 |
-
|
| 239 |
-
sendRoomSettings();
|
| 240 |
-
});
|
| 241 |
-
input.addEventListener('change', () => {
|
| 242 |
-
updateRoomLabels();
|
| 243 |
-
if (lastScene) sendScene(lastScene);
|
| 244 |
-
});
|
| 245 |
});
|
| 246 |
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
setStatus('Preset loaded.');
|
| 254 |
-
});
|
| 255 |
});
|
| 256 |
-
|
| 257 |
-
if (els.generate) {
|
| 258 |
-
els.generate.addEventListener('click', async () => {
|
| 259 |
-
if (busy) return;
|
| 260 |
-
const cfg = currentConfig();
|
| 261 |
-
if (!cfg.prompt) {
|
| 262 |
-
setStatus('Add a scene prompt first.');
|
| 263 |
-
return;
|
| 264 |
-
}
|
| 265 |
-
const payloadInput = hiddenInput('dojo-payload');
|
| 266 |
-
const trigger = document.querySelector('#dojo-btn button, #dojo-btn');
|
| 267 |
-
if (!payloadInput || !trigger) {
|
| 268 |
-
setStatus('Dojo backend controls are missing.');
|
| 269 |
-
return;
|
| 270 |
-
}
|
| 271 |
-
setBusy(true);
|
| 272 |
-
resetProgress();
|
| 273 |
-
setStep('flux-iso', 'active');
|
| 274 |
-
setStatus('Starting isometric Flux generation...');
|
| 275 |
-
writeHidden('dojo-result', '');
|
| 276 |
-
const previous = '';
|
| 277 |
-
writeHidden('dojo-payload', JSON.stringify(cfg));
|
| 278 |
-
try {
|
| 279 |
-
trigger.click();
|
| 280 |
-
const value = await waitForResult(previous, (stage) => {
|
| 281 |
-
setTiming(stage);
|
| 282 |
-
if (stage.source_image || stage.isometric_image) showSourceImage(stage.source_image || stage.isometric_image, 'isometric');
|
| 283 |
-
if (stage.panorama_image) showSourceImage(stage.panorama_image, 'panorama');
|
| 284 |
-
if (stage.stage === 'flux-iso-start') {
|
| 285 |
-
setStep('flux-iso', 'active');
|
| 286 |
-
setStatus(stage.status || 'Generating isometric Flux image...');
|
| 287 |
-
} else if (stage.stage === 'flux-iso-progress') {
|
| 288 |
-
setStep('flux-iso', 'active');
|
| 289 |
-
setStatus(stage.status || 'Generating isometric Flux image...');
|
| 290 |
-
} else if (stage.stage === 'flux-iso-done') {
|
| 291 |
-
setStep('flux-iso', 'done');
|
| 292 |
-
setStep('flux-pano', 'active');
|
| 293 |
-
showSourceImage(stage.source_image || stage.isometric_image, 'isometric');
|
| 294 |
-
setStatus(stage.status || 'Isometric Flux image ready. Starting 360 Flux image...');
|
| 295 |
-
} else if (stage.stage === 'flux-pano-start') {
|
| 296 |
-
setStep('flux-iso', 'done');
|
| 297 |
-
setStep('flux-pano', 'active');
|
| 298 |
-
setStatus(stage.status || 'Generating 360 Flux image...');
|
| 299 |
-
} else if (stage.stage === 'flux-pano-progress') {
|
| 300 |
-
setStep('flux-pano', 'active');
|
| 301 |
-
setStatus(stage.status || 'Generating 360 Flux image...');
|
| 302 |
-
} else if (stage.stage === 'flux-pano-done') {
|
| 303 |
-
setStep('flux-pano', 'done');
|
| 304 |
-
setStep('splat', 'active');
|
| 305 |
-
showSourceImage(stage.panorama_image, 'panorama');
|
| 306 |
-
setStatus(stage.status || '360 Flux image ready. Starting TripoSplat...');
|
| 307 |
-
} else if (stage.stage === 'splat-start') {
|
| 308 |
-
setStep('splat', 'active');
|
| 309 |
-
setStatus(stage.status || 'Generating TripoSplat...');
|
| 310 |
-
} else if (stage.stage === 'splat-progress') {
|
| 311 |
-
setStep('flux-iso', 'done');
|
| 312 |
-
setStep('flux-pano', 'done');
|
| 313 |
-
setStep('splat', 'active');
|
| 314 |
-
setStatus(bestProgressText(stage) || stage.status || 'Generating TripoSplat...');
|
| 315 |
-
} else if (stage.stage === 'splat-done') {
|
| 316 |
-
setStep('splat', 'done');
|
| 317 |
-
setStep('viewer', 'active');
|
| 318 |
-
setStatus(stage.status || 'TripoSplat ready. Loading viewer...');
|
| 319 |
-
} else if (stage.stage === 'fallback') {
|
| 320 |
-
setStep('flux-iso', 'fail');
|
| 321 |
-
setStep('flux-pano', 'fail');
|
| 322 |
-
setStep('splat', 'fail');
|
| 323 |
-
setStatus(stage.warning || stage.error || 'Fallback generated.');
|
| 324 |
-
}
|
| 325 |
-
});
|
| 326 |
-
const scene = JSON.parse(value);
|
| 327 |
-
if (scene && (scene.image || scene.splat || scene.splat_url)) {
|
| 328 |
-
Object.assign(scene, roomSettings());
|
| 329 |
-
lastScene = scene;
|
| 330 |
-
sendScene(scene);
|
| 331 |
-
if (scene.source === 'klein-triposplat') setStatus((dojoMode === 'panorama' ? 'Panorama applied. ' : 'Splat applied. ') + (scene.info || ''));
|
| 332 |
-
else if (scene.source === 'dit360') setStatus(scene.stage === 'fallback' ? 'Panorama fallback applied.' : 'Panorama applied.');
|
| 333 |
-
else setStatus((scene.warning || 'Using default scene.') + ' Default scene applied.');
|
| 334 |
-
} else {
|
| 335 |
-
setStatus((scene && scene.error) || 'Scene generation failed.');
|
| 336 |
-
}
|
| 337 |
-
} catch (e) {
|
| 338 |
-
console.error('dojo scene generation failed', e);
|
| 339 |
-
setStatus('Scene generation failed: ' + e.message);
|
| 340 |
-
} finally {
|
| 341 |
-
setBusy(false);
|
| 342 |
-
}
|
| 343 |
-
});
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
if (els.clear) {
|
| 347 |
-
els.clear.addEventListener('click', () => {
|
| 348 |
-
const frame = previewFrame();
|
| 349 |
-
if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-clear' }, '*');
|
| 350 |
-
resetProgress();
|
| 351 |
-
setStatus('Scene cleared.');
|
| 352 |
-
});
|
| 353 |
-
}
|
| 354 |
}
|
| 355 |
|
| 356 |
window.addEventListener('kimodo-drawer-open', (e) => {
|
|
|
|
| 2 |
let busy = false;
|
| 3 |
let dojoMode = 'splat';
|
| 4 |
let lastScene = null;
|
| 5 |
+
let dojoScope = 'mine';
|
| 6 |
+
|
| 7 |
+
// Community starters — tapping one opens the create view prefilled, so generating
|
| 8 |
+
// produces a real flux + splat scene (which then lands under "Mine").
|
| 9 |
+
const COMMUNITY_SEED = [
|
| 10 |
+
{ name: 'Classic dojo', mode: 'splat', settings: { room_size: 8, room_height: 3.2 },
|
| 11 |
+
prompt: 'A traditional martial arts dojo room, polished wooden floor, training mats, wooden wall panels, warm lantern light, full room visible, high detail, no people, no text.' },
|
| 12 |
+
{ name: 'Mountain dojo', mode: 'panorama', settings: { room_size: 10, room_height: 4 },
|
| 13 |
+
prompt: 'An outdoor mountain dojo courtyard at sunrise, stone tiles, wooden gates, misty trees, full environment visible, high detail, no people, no text.' },
|
| 14 |
+
{ name: 'Neon dojo', mode: 'splat', settings: { room_size: 8, room_height: 3.2 },
|
| 15 |
+
prompt: 'A futuristic neon martial arts training room, glossy floor, holographic wall panels, dramatic rim light, full room visible, no people, no text.' }
|
| 16 |
+
];
|
| 17 |
|
| 18 |
function dojoEls() {
|
| 19 |
return {
|
| 20 |
+
library: document.getElementById('kimodo-dojo-library'),
|
| 21 |
+
create: document.getElementById('kimodo-dojo-create'),
|
| 22 |
+
scope: document.getElementById('kimodo-dojo-scope'),
|
| 23 |
+
add: document.getElementById('kimodo-dojo-add'),
|
| 24 |
+
back: document.getElementById('kimodo-dojo-back'),
|
| 25 |
+
grid: document.getElementById('kimodo-dojo-grid'),
|
| 26 |
+
emptyMsg: document.getElementById('kimodo-dojo-empty'),
|
| 27 |
+
improve: document.getElementById('kimodo-dojo-improve'),
|
| 28 |
prompt: document.getElementById('kimodo-dojo-prompt'),
|
| 29 |
mode: document.getElementById('kimodo-dojo-mode'),
|
| 30 |
roomSize: document.getElementById('kimodo-dojo-room-size'),
|
|
|
|
| 111 |
if (els.detail) els.detail.textContent = bestProgressText(stage);
|
| 112 |
}
|
| 113 |
|
| 114 |
+
function hiddenInput(id) { return inputInside(id); }
|
|
|
|
|
|
|
|
|
|
| 115 |
function writeHidden(id, value) {
|
| 116 |
const input = hiddenInput(id);
|
| 117 |
if (!input) return false;
|
|
|
|
| 120 |
input.dispatchEvent(new Event('change', { bubbles: true }));
|
| 121 |
return true;
|
| 122 |
}
|
|
|
|
| 123 |
function readHidden(id) {
|
| 124 |
const input = hiddenInput(id);
|
| 125 |
return input ? input.value || '' : '';
|
| 126 |
}
|
| 127 |
|
| 128 |
+
function roomSettings() {
|
| 129 |
+
const els = dojoEls();
|
| 130 |
+
return {
|
| 131 |
+
room_size: Math.max(1, Number(els.roomSize && els.roomSize.value) || 8),
|
| 132 |
+
room_height: Math.max(1, Number(els.roomHeight && els.roomHeight.value) || 3.2),
|
| 133 |
+
use_panorama_skybox: !!(els.skybox && els.skybox.checked)
|
| 134 |
+
};
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
function sendScene(scene) {
|
| 138 |
const frame = previewFrame();
|
| 139 |
if (!scene) return;
|
|
|
|
| 144 |
if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-scene', scene: payload, image: payload.image }, '*');
|
| 145 |
}
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
function updateRoomLabels() {
|
| 148 |
const els = dojoEls();
|
| 149 |
const settings = roomSettings();
|
|
|
|
| 188 |
const els = dojoEls();
|
| 189 |
if (els.generate) els.generate.disabled = busy;
|
| 190 |
if (els.clear) els.clear.disabled = busy;
|
| 191 |
+
if (els.improve) els.improve.disabled = busy;
|
| 192 |
}
|
| 193 |
|
| 194 |
function waitForResult(previousValue, onStage) {
|
|
|
|
| 230 |
};
|
| 231 |
}
|
| 232 |
|
| 233 |
+
// ---------- Saved dojos (localStorage) ----------
|
| 234 |
+
function loadMyDojos() { try { return JSON.parse(localStorage.getItem('kimodoMyDojos') || '[]'); } catch (e) { return []; } }
|
| 235 |
+
function saveMyDojos(list) {
|
| 236 |
+
let arr = (list || []).slice(0, 16);
|
| 237 |
+
// Scene payloads carry data-URL splats (can be MBs); drop the oldest until it fits.
|
| 238 |
+
while (arr.length) {
|
| 239 |
+
try { localStorage.setItem('kimodoMyDojos', JSON.stringify(arr)); return true; }
|
| 240 |
+
catch (e) { arr = arr.slice(0, arr.length - 1); }
|
| 241 |
+
}
|
| 242 |
+
try { localStorage.setItem('kimodoMyDojos', '[]'); } catch (e) {}
|
| 243 |
+
return false;
|
| 244 |
+
}
|
| 245 |
+
function dojoName(prompt) {
|
| 246 |
+
const head = (prompt || '').replace(/[.,].*$/, '').trim().replace(/^(a|an|the)\s+/i, '');
|
| 247 |
+
const name = head.split(/\s+/).slice(0, 4).join(' ');
|
| 248 |
+
return (name || 'Dojo').slice(0, 30);
|
| 249 |
+
}
|
| 250 |
+
function saveGeneratedDojo(scene, cfg) {
|
| 251 |
+
const thumb = scene.isometric_image || scene.source_image || scene.image || '';
|
| 252 |
+
const entry = {
|
| 253 |
+
id: 'd' + Date.now().toString(36),
|
| 254 |
+
name: dojoName(cfg.prompt),
|
| 255 |
+
prompt: cfg.prompt,
|
| 256 |
+
mode: cfg.mode,
|
| 257 |
+
settings: { room_size: cfg.room_size, room_height: cfg.room_height, use_panorama_skybox: !!cfg.use_panorama_skybox, seed: cfg.seed, steps: cfg.steps },
|
| 258 |
+
scene: scene,
|
| 259 |
+
thumb: thumb,
|
| 260 |
+
ts: Date.now()
|
| 261 |
+
};
|
| 262 |
+
const arr = loadMyDojos();
|
| 263 |
+
arr.unshift(entry);
|
| 264 |
+
saveMyDojos(arr);
|
| 265 |
+
return entry;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
// ---------- View + library ----------
|
| 269 |
+
function showView(view) {
|
| 270 |
+
const els = dojoEls();
|
| 271 |
+
if (els.library) els.library.hidden = (view !== 'library');
|
| 272 |
+
if (els.create) els.create.hidden = (view !== 'create');
|
| 273 |
+
if (view === 'library') renderLibrary();
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
function syncModeButtons() {
|
| 277 |
+
const els = dojoEls();
|
| 278 |
+
if (!els.mode) return;
|
| 279 |
+
els.mode.querySelectorAll('.kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b.dataset.mode === dojoMode));
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
function applySettings(settings) {
|
| 283 |
+
const els = dojoEls();
|
| 284 |
+
if (!settings) return;
|
| 285 |
+
if (els.roomSize && settings.room_size) els.roomSize.value = settings.room_size;
|
| 286 |
+
if (els.roomHeight && settings.room_height) els.roomHeight.value = settings.room_height;
|
| 287 |
+
if (els.skybox) els.skybox.checked = !!settings.use_panorama_skybox;
|
| 288 |
+
if (els.seed && settings.seed != null) els.seed.value = settings.seed;
|
| 289 |
+
if (els.steps && settings.steps != null) els.steps.value = settings.steps;
|
| 290 |
+
updateRoomLabels();
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
function openCreate(seed) {
|
| 294 |
+
showView('create');
|
| 295 |
+
resetProgress();
|
| 296 |
+
const els = dojoEls();
|
| 297 |
+
if (seed) {
|
| 298 |
+
if (els.prompt) { els.prompt.value = seed.prompt || ''; els.prompt.dispatchEvent(new Event('input', { bubbles: true })); }
|
| 299 |
+
dojoMode = seed.mode || 'splat';
|
| 300 |
+
syncModeButtons();
|
| 301 |
+
applySettings(seed.settings);
|
| 302 |
+
setStatus('Loaded "' + (seed.name || 'starter') + '". Tweak and Generate.');
|
| 303 |
+
} else {
|
| 304 |
+
setStatus('Ready.');
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
function mineCard(entry) {
|
| 309 |
+
const card = document.createElement('div');
|
| 310 |
+
card.className = 'kimodo-dojo-card';
|
| 311 |
+
card.title = entry.name || 'dojo';
|
| 312 |
+
if (entry.thumb) {
|
| 313 |
+
const img = document.createElement('img');
|
| 314 |
+
img.className = 'thumb'; img.src = entry.thumb; img.alt = entry.name || 'dojo';
|
| 315 |
+
card.appendChild(img);
|
| 316 |
+
} else {
|
| 317 |
+
const ph = document.createElement('div'); ph.className = 'ph'; ph.textContent = '🏯'; card.appendChild(ph);
|
| 318 |
+
}
|
| 319 |
+
const tag = document.createElement('span'); tag.className = 'badge'; tag.textContent = (entry.mode === 'panorama' ? '360' : 'SPLAT'); card.appendChild(tag);
|
| 320 |
+
const del = document.createElement('button'); del.className = 'del'; del.type = 'button'; del.textContent = '✕'; del.title = 'Delete';
|
| 321 |
+
del.onclick = (e) => { e.stopPropagation(); if (!window.confirm('Delete this dojo?')) return; saveMyDojos(loadMyDojos().filter((x) => x.id !== entry.id)); renderLibrary(); };
|
| 322 |
+
card.appendChild(del);
|
| 323 |
+
const nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = entry.name || 'Dojo'; card.appendChild(nm);
|
| 324 |
+
card.onclick = () => applySavedDojo(entry);
|
| 325 |
+
return card;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
function communityCard(seed) {
|
| 329 |
+
const card = document.createElement('div');
|
| 330 |
+
card.className = 'kimodo-dojo-card';
|
| 331 |
+
card.title = seed.name;
|
| 332 |
+
const ph = document.createElement('div'); ph.className = 'ph'; ph.textContent = '🏯'; card.appendChild(ph);
|
| 333 |
+
const tag = document.createElement('span'); tag.className = 'badge'; tag.textContent = 'STARTER'; card.appendChild(tag);
|
| 334 |
+
const nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = seed.name; card.appendChild(nm);
|
| 335 |
+
card.onclick = () => openCreate(seed);
|
| 336 |
+
return card;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
function renderLibrary() {
|
| 340 |
+
const els = dojoEls();
|
| 341 |
+
if (els.scope) els.scope.querySelectorAll('.kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b.dataset.scope === dojoScope));
|
| 342 |
+
if (!els.grid) return;
|
| 343 |
+
els.grid.innerHTML = '';
|
| 344 |
+
if (dojoScope === 'mine') {
|
| 345 |
+
const arr = loadMyDojos();
|
| 346 |
+
arr.forEach((e) => els.grid.appendChild(mineCard(e)));
|
| 347 |
+
if (els.emptyMsg) els.emptyMsg.textContent = arr.length ? '' : 'No dojos yet — tap + Add dojo to create one.';
|
| 348 |
+
} else {
|
| 349 |
+
COMMUNITY_SEED.forEach((s) => els.grid.appendChild(communityCard(s)));
|
| 350 |
+
if (els.emptyMsg) els.emptyMsg.textContent = '';
|
| 351 |
+
}
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
function applySavedDojo(entry) {
|
| 355 |
+
const scene = entry.scene;
|
| 356 |
+
if (!scene) { openCreate(entry); return; } // recipe-only fallback → re-generate
|
| 357 |
+
dojoMode = scene.render_mode || (scene.splat || scene.splat_url ? 'splat' : 'panorama');
|
| 358 |
+
syncModeButtons();
|
| 359 |
+
applySettings(entry.settings);
|
| 360 |
+
lastScene = scene;
|
| 361 |
+
sendScene(scene);
|
| 362 |
+
setStatus('Applied "' + (entry.name || 'dojo') + '".');
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
// ---------- tiny-aya prompt suggestion ----------
|
| 366 |
+
function improvePrompt() {
|
| 367 |
+
const els = dojoEls();
|
| 368 |
+
const payloadInput = hiddenInput('dojo-suggest-payload');
|
| 369 |
+
const trigger = document.querySelector('#dojo-suggest-btn button, #dojo-suggest-btn');
|
| 370 |
+
if (!payloadInput || !trigger) { setStatus('Suggest backend missing.'); return; }
|
| 371 |
+
const words = (els.prompt && els.prompt.value || '').trim();
|
| 372 |
+
if (els.improve) { els.improve.disabled = true; els.improve.textContent = '✦ Thinking…'; }
|
| 373 |
+
setStatus(words ? 'Improving your prompt with tiny-aya…' : 'Suggesting a prompt with tiny-aya…');
|
| 374 |
+
writeHidden('dojo-suggest-result', '');
|
| 375 |
+
writeHidden('dojo-suggest-payload', JSON.stringify({ words: words }));
|
| 376 |
+
const started = Date.now();
|
| 377 |
+
const timer = window.setInterval(() => {
|
| 378 |
+
const val = readHidden('dojo-suggest-result');
|
| 379 |
+
if (val) { window.clearInterval(timer); finishImprove(val); }
|
| 380 |
+
else if (Date.now() - started > 150000) { window.clearInterval(timer); finishImprove('{"ok":false,"error":"timed out"}'); }
|
| 381 |
+
}, 300);
|
| 382 |
+
window.setTimeout(() => trigger.click(), 40);
|
| 383 |
+
|
| 384 |
+
function finishImprove(val) {
|
| 385 |
+
if (els.improve) { els.improve.disabled = busy; els.improve.textContent = '✦ Improve with tiny-aya'; }
|
| 386 |
+
try {
|
| 387 |
+
const r = JSON.parse(val);
|
| 388 |
+
if (r.ok && r.prompt) {
|
| 389 |
+
if (els.prompt) { els.prompt.value = r.prompt; els.prompt.dispatchEvent(new Event('input', { bubbles: true })); }
|
| 390 |
+
setStatus('Prompt suggested ✦');
|
| 391 |
+
} else {
|
| 392 |
+
setStatus('Suggest failed: ' + (r.error || 'unknown'));
|
| 393 |
+
}
|
| 394 |
+
} catch (e) { setStatus('Suggest failed.'); }
|
| 395 |
+
}
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
// ---------- Generate ----------
|
| 399 |
+
async function runGenerate() {
|
| 400 |
+
if (busy) return;
|
| 401 |
+
const cfg = currentConfig();
|
| 402 |
+
if (!cfg.prompt) { setStatus('Add a scene prompt first (or tap Improve).'); return; }
|
| 403 |
+
const payloadInput = hiddenInput('dojo-payload');
|
| 404 |
+
const trigger = document.querySelector('#dojo-btn button, #dojo-btn');
|
| 405 |
+
if (!payloadInput || !trigger) { setStatus('Dojo backend controls are missing.'); return; }
|
| 406 |
+
setBusy(true);
|
| 407 |
+
resetProgress();
|
| 408 |
+
setStep('flux-iso', 'active');
|
| 409 |
+
setStatus('Starting isometric Flux generation...');
|
| 410 |
+
writeHidden('dojo-result', '');
|
| 411 |
+
writeHidden('dojo-payload', JSON.stringify(cfg));
|
| 412 |
+
try {
|
| 413 |
+
trigger.click();
|
| 414 |
+
const value = await waitForResult('', (stage) => {
|
| 415 |
+
setTiming(stage);
|
| 416 |
+
if (stage.source_image || stage.isometric_image) showSourceImage(stage.source_image || stage.isometric_image, 'isometric');
|
| 417 |
+
if (stage.panorama_image) showSourceImage(stage.panorama_image, 'panorama');
|
| 418 |
+
if (stage.stage === 'flux-iso-start' || stage.stage === 'flux-iso-progress') {
|
| 419 |
+
setStep('flux-iso', 'active');
|
| 420 |
+
setStatus(stage.status || 'Generating isometric Flux image...');
|
| 421 |
+
} else if (stage.stage === 'flux-iso-done') {
|
| 422 |
+
setStep('flux-iso', 'done'); setStep('flux-pano', 'active');
|
| 423 |
+
showSourceImage(stage.source_image || stage.isometric_image, 'isometric');
|
| 424 |
+
setStatus(stage.status || 'Isometric ready. Starting 360 Flux...');
|
| 425 |
+
} else if (stage.stage === 'flux-pano-start' || stage.stage === 'flux-pano-progress') {
|
| 426 |
+
setStep('flux-iso', 'done'); setStep('flux-pano', 'active');
|
| 427 |
+
setStatus(stage.status || 'Generating 360 Flux image...');
|
| 428 |
+
} else if (stage.stage === 'flux-pano-done') {
|
| 429 |
+
setStep('flux-pano', 'done'); setStep('splat', 'active');
|
| 430 |
+
showSourceImage(stage.panorama_image, 'panorama');
|
| 431 |
+
setStatus(stage.status || '360 ready. Starting TripoSplat...');
|
| 432 |
+
} else if (stage.stage === 'splat-start' || stage.stage === 'splat-progress') {
|
| 433 |
+
setStep('flux-iso', 'done'); setStep('flux-pano', 'done'); setStep('splat', 'active');
|
| 434 |
+
setStatus(bestProgressText(stage) || stage.status || 'Generating TripoSplat...');
|
| 435 |
+
} else if (stage.stage === 'splat-done') {
|
| 436 |
+
setStep('splat', 'done'); setStep('viewer', 'active');
|
| 437 |
+
setStatus(stage.status || 'TripoSplat ready. Loading viewer...');
|
| 438 |
+
} else if (stage.stage === 'fallback') {
|
| 439 |
+
setStep('flux-iso', 'fail'); setStep('flux-pano', 'fail'); setStep('splat', 'fail');
|
| 440 |
+
setStatus(stage.warning || stage.error || 'Fallback generated.');
|
| 441 |
+
}
|
| 442 |
+
});
|
| 443 |
+
const scene = JSON.parse(value);
|
| 444 |
+
if (scene && (scene.image || scene.splat || scene.splat_url)) {
|
| 445 |
+
Object.assign(scene, roomSettings());
|
| 446 |
+
lastScene = scene;
|
| 447 |
+
sendScene(scene);
|
| 448 |
+
// A real scene (klein-triposplat / dit360) is saved under "Mine".
|
| 449 |
+
if (scene.source === 'klein-triposplat' || scene.source === 'dit360') {
|
| 450 |
+
const entry = saveGeneratedDojo(scene, cfg);
|
| 451 |
+
setStatus('Saved "' + entry.name + '" to My dojos. ' + (scene.info || ''));
|
| 452 |
+
} else {
|
| 453 |
+
setStatus((scene.warning || 'Using default scene.') + ' Default scene applied.');
|
| 454 |
+
}
|
| 455 |
+
} else {
|
| 456 |
+
setStatus((scene && scene.error) || 'Scene generation failed.');
|
| 457 |
+
}
|
| 458 |
+
} catch (e) {
|
| 459 |
+
console.error('dojo scene generation failed', e);
|
| 460 |
+
setStatus('Scene generation failed: ' + e.message);
|
| 461 |
+
} finally {
|
| 462 |
+
setBusy(false);
|
| 463 |
+
}
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
function initDojo() {
|
| 467 |
const drawer = document.getElementById('kimodo-dojo-drawer');
|
| 468 |
const els = dojoEls();
|
| 469 |
+
if (!drawer || drawer.dataset.ready === '1') { renderLibrary(); return; }
|
| 470 |
drawer.dataset.ready = '1';
|
| 471 |
updateRoomLabels();
|
| 472 |
+
showView('library');
|
| 473 |
|
| 474 |
+
if (els.scope) els.scope.addEventListener('click', (e) => {
|
| 475 |
+
const b = e.target.closest('.kimodo-cz-segbtn[data-scope]');
|
| 476 |
+
if (!b) return;
|
| 477 |
+
dojoScope = b.dataset.scope || 'mine';
|
| 478 |
+
renderLibrary();
|
| 479 |
+
});
|
| 480 |
+
if (els.add) els.add.addEventListener('click', () => openCreate(null));
|
| 481 |
+
if (els.back) els.back.addEventListener('click', () => showView('library'));
|
| 482 |
+
if (els.improve) els.improve.addEventListener('click', () => { if (!busy) improvePrompt(); });
|
| 483 |
+
|
| 484 |
+
if (els.mode) els.mode.addEventListener('click', (e) => {
|
| 485 |
+
const button = e.target.closest('.kimodo-cz-segbtn[data-mode]');
|
| 486 |
+
if (!button || busy) return;
|
| 487 |
+
dojoMode = button.dataset.mode || 'splat';
|
| 488 |
+
syncModeButtons();
|
| 489 |
+
setStatus(dojoMode === 'panorama' ? 'Panorama mode selected.' : 'Splat mode selected.');
|
| 490 |
+
if (lastScene) sendScene(lastScene);
|
| 491 |
+
});
|
| 492 |
|
| 493 |
['roomSize', 'roomHeight', 'skybox'].forEach((key) => {
|
| 494 |
const input = els[key];
|
| 495 |
if (!input) return;
|
| 496 |
+
input.addEventListener('input', () => { updateRoomLabels(); sendRoomSettings(); });
|
| 497 |
+
input.addEventListener('change', () => { updateRoomLabels(); if (lastScene) sendScene(lastScene); });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
});
|
| 499 |
|
| 500 |
+
if (els.generate) els.generate.addEventListener('click', runGenerate);
|
| 501 |
+
if (els.clear) els.clear.addEventListener('click', () => {
|
| 502 |
+
const frame = previewFrame();
|
| 503 |
+
if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-clear' }, '*');
|
| 504 |
+
resetProgress();
|
| 505 |
+
setStatus('Scene cleared.');
|
|
|
|
|
|
|
| 506 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
}
|
| 508 |
|
| 509 |
window.addEventListener('kimodo-drawer-open', (e) => {
|