tag-builder Wave A: backend stability + cleanup + customizer/timer frontend fixes
Backend: - H4 get_db reconnect storm: gate the one-time schema/normalize/reconcile helpers behind a process-level flag so the ~11,529-row reconcile stops re-running on the asyncio loop on every spurious mtime reopen. - H5 tag_search write-on-read: cache last-good fingerprint (return it on transient lock instead of zeros -> no spurious 5-10 min re-embed) + mtime-gate the query. - Remove dead /clothing/assemble + /furniture/assemble routes. - Escape LIKE %/_ in 3 character-prefix lookups; NULL-guard /props/assemble. - New POST /tag-builder/maintenance (idempotent orphan cleanup + alias dedup). Frontend: - Customizer output-mode: thread real isNaturalMode into the 3 customizers; preview matches the actual emit per mode; focus checkbox disabled (not silent no-op) in tag mode. - Event-driven timers: drop 3 setTimeout(0) click-outside deferrals + the 1100ms flash timer (animationend instead).
This commit is contained in:
+106
-143
@@ -23,6 +23,10 @@ _dbg = logging.getLogger("promptchain.ai.debug")
|
||||
|
||||
DB_PATH = Path(__file__).parent.parent / "data" / "tag-builder" / "tag-builder.db"
|
||||
_local = threading.local()
|
||||
# True once the one-time schema/normalize/reconcile helpers have run this
|
||||
# process. Gates get_db's mtime-triggered reconnects so the expensive
|
||||
# ~11,529-row reconcile doesn't re-run on every spurious reopen.
|
||||
_schema_ready = False
|
||||
|
||||
|
||||
def get_db() -> sqlite3.Connection:
|
||||
@@ -50,11 +54,28 @@ def get_db() -> sqlite3.Connection:
|
||||
# FK enforcement is off by default in sqlite, so CASCADE deletes were
|
||||
# inert and orphan preset rows accumulated. Per-connection pragma.
|
||||
conn.execute("PRAGMA foreign_keys=ON")
|
||||
_ensure_generic_outfits_schema(conn)
|
||||
_normalize_full_outfits_sort_order(conn)
|
||||
_normalize_scene_locations_sort_order(conn)
|
||||
_ensure_character_appearance_chip_columns(conn)
|
||||
_reconcile_character_base_tags(conn)
|
||||
# These schema/normalize/reconcile helpers are idempotent but
|
||||
# expensive — the reconcile walks ~11,529 character rows. get_db
|
||||
# reconnects on every DB-file mtime change, and sqlite's delete
|
||||
# journal self-bumps that mtime on internal commits, so without
|
||||
# gating they re-ran on the asyncio loop on roughly every other
|
||||
# request while editing. Run them once per process; the request-
|
||||
# time appearance-chip fallback covers a char added externally
|
||||
# mid-session, and a restart re-runs them to refresh the cache.
|
||||
global _schema_ready
|
||||
if not _schema_ready:
|
||||
_ensure_generic_outfits_schema(conn)
|
||||
_normalize_full_outfits_sort_order(conn)
|
||||
_normalize_scene_locations_sort_order(conn)
|
||||
_ensure_character_appearance_chip_columns(conn)
|
||||
_reconcile_character_base_tags(conn)
|
||||
_schema_ready = True
|
||||
# The helpers commit, bumping the watched mtime; re-read it so
|
||||
# their own write doesn't immediately trigger another reconnect.
|
||||
try:
|
||||
current_mtime = DB_PATH.stat().st_mtime_ns
|
||||
except OSError:
|
||||
pass
|
||||
_local.conn = conn
|
||||
_local.mtime = current_mtime
|
||||
return _local.conn
|
||||
@@ -3110,6 +3131,62 @@ def _assert_allowed_table(name: str) -> str:
|
||||
return name
|
||||
|
||||
|
||||
def _run_data_health(db: sqlite3.Connection) -> dict:
|
||||
"""Idempotent maintenance: drop orphaned preset rows (FK enforcement
|
||||
was historically off) and de-duplicate tag_aliases. Returns per-step
|
||||
removal counts. Each step is independent and guarded so a missing
|
||||
table or schema drift skips that step instead of failing the run."""
|
||||
cleaned: dict[str, int] = {}
|
||||
|
||||
def _exists(table: str) -> bool:
|
||||
return db.execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,)
|
||||
).fetchone() is not None
|
||||
|
||||
# (child, fk_col, parent, parent_key) — parents first so a parent
|
||||
# delete's now-stranded children are caught in the same pass.
|
||||
orphan_rules = [
|
||||
("outfits", "character_tag", "characters", "tag"),
|
||||
("poses", "character_tag", "characters", "tag"),
|
||||
("character_chip_overrides", "character_tag", "characters", "tag"),
|
||||
("outfit_tag_slots", "outfit_id", "outfits", "id"),
|
||||
("outfit_chip_overrides", "outfit_id", "outfits", "id"),
|
||||
("pose_chip_overrides", "pose_id", "poses", "id"),
|
||||
]
|
||||
for child, fk, parent, key in orphan_rules:
|
||||
if not (_exists(child) and _exists(parent)):
|
||||
continue
|
||||
try:
|
||||
cur = db.execute(
|
||||
f"DELETE FROM {child} WHERE {fk} IS NOT NULL "
|
||||
f"AND {fk} NOT IN (SELECT {key} FROM {parent})"
|
||||
)
|
||||
cleaned[f"orphan_{child}"] = cur.rowcount
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
|
||||
# tag_aliases has a (tag, alias) primary key so exact dupes can't exist,
|
||||
# but keep the dedup as a cheap self-heal in case the PK was ever absent.
|
||||
if _exists("tag_aliases"):
|
||||
try:
|
||||
cur = db.execute(
|
||||
"DELETE FROM tag_aliases WHERE rowid NOT IN "
|
||||
"(SELECT MIN(rowid) FROM tag_aliases GROUP BY tag, alias)"
|
||||
)
|
||||
cleaned["tag_alias_duplicates"] = cur.rowcount
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
|
||||
db.commit()
|
||||
return cleaned
|
||||
|
||||
|
||||
@routes.post("/promptchain/tag-builder/maintenance")
|
||||
async def _api_tag_builder_maintenance(request):
|
||||
"""Run idempotent data-health maintenance and report what was removed."""
|
||||
return web.json_response({"cleaned": _run_data_health(get_db())})
|
||||
|
||||
|
||||
@routes.get("/promptchain/tag-builder/buckets")
|
||||
async def _api_buckets(request):
|
||||
db = get_db()
|
||||
@@ -4795,7 +4872,9 @@ def match_characters_inner(
|
||||
name_prefix_candidates.setdefault(token.lower(), original)
|
||||
if name_prefix_candidates:
|
||||
for prefix, original in name_prefix_candidates.items():
|
||||
like_pattern = prefix + r"\_%"
|
||||
# Escape LIKE metacharacters so a name with % or _ doesn't wildcard-match.
|
||||
esc = prefix.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
like_pattern = esc + r"\_%"
|
||||
prefix_rows = db.execute(
|
||||
"SELECT tag, display, series, base_tags, base_natlang FROM characters "
|
||||
"WHERE LOWER(tag) LIKE ? ESCAPE '\\' "
|
||||
@@ -4845,7 +4924,9 @@ def match_characters_inner(
|
||||
paren_suffix_candidates.setdefault(bare_name.lower(), original)
|
||||
if paren_suffix_candidates:
|
||||
for prefix, original in paren_suffix_candidates.items():
|
||||
like_pattern = prefix + r"\_%"
|
||||
# Escape LIKE metacharacters so a name with % or _ doesn't wildcard-match.
|
||||
esc = prefix.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
like_pattern = esc + r"\_%"
|
||||
prefix_rows = db.execute(
|
||||
"SELECT tag, display, series, base_tags, base_natlang FROM characters "
|
||||
"WHERE LOWER(tag) = ? OR LOWER(tag) LIKE ? ESCAPE '\\' "
|
||||
@@ -4889,7 +4970,9 @@ def match_characters_inner(
|
||||
first_segment_candidates.setdefault(bare_name.lower(), original)
|
||||
if first_segment_candidates:
|
||||
for prefix, original in first_segment_candidates.items():
|
||||
like_pattern = prefix + r"\_%"
|
||||
# Escape LIKE metacharacters so a name with % or _ doesn't wildcard-match.
|
||||
esc = prefix.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
like_pattern = esc + r"\_%"
|
||||
prefix_rows = db.execute(
|
||||
"SELECT tag, display, series, base_tags, base_natlang FROM characters "
|
||||
"WHERE LOWER(tag) = ? OR LOWER(tag) LIKE ? ESCAPE '\\' "
|
||||
@@ -5374,28 +5457,34 @@ async def _api_props_assemble(request):
|
||||
tags_parts = []
|
||||
natlang_parts = []
|
||||
|
||||
if color:
|
||||
if color and color.get("tag"):
|
||||
tags_parts.append(color["tag"])
|
||||
if color.get("prefix"):
|
||||
natlang_parts.append(color["prefix"])
|
||||
if pattern and pattern["tag"] != "solid":
|
||||
if pattern and pattern.get("tag") and pattern["tag"] != "solid":
|
||||
tags_parts.append(pattern["tag"])
|
||||
if pattern.get("prefix"):
|
||||
natlang_parts.append(pattern["prefix"])
|
||||
if material:
|
||||
if material and material.get("tag"):
|
||||
tags_parts.append(material["tag"])
|
||||
if material.get("prefix"):
|
||||
natlang_parts.append(material["prefix"])
|
||||
|
||||
tags_parts.append(prop["prop_tag"])
|
||||
natlang_parts.append(prop.get("base_natlang") or prop["display_name"].lower())
|
||||
if prop.get("prop_tag"):
|
||||
tags_parts.append(prop["prop_tag"])
|
||||
_display = prop.get("display_name")
|
||||
_phrase = prop.get("base_natlang") or (_display.lower() if isinstance(_display, str) else None)
|
||||
if _phrase:
|
||||
natlang_parts.append(_phrase)
|
||||
|
||||
prop_tags = " ".join(tags_parts)
|
||||
prop_natlang = " ".join(natlang_parts)
|
||||
prop_tags = " ".join(str(p) for p in tags_parts if p)
|
||||
prop_natlang = " ".join(str(p) for p in natlang_parts if p)
|
||||
|
||||
if action:
|
||||
final_tags = f"{action['action_prefix_tags']} {prop_tags}"
|
||||
final_natlang = f"{action['action_prefix_natlang']} {prop_natlang}"
|
||||
prefix_tags = action.get("action_prefix_tags") or ""
|
||||
prefix_natlang = action.get("action_prefix_natlang") or ""
|
||||
final_tags = f"{prefix_tags} {prop_tags}".strip()
|
||||
final_natlang = f"{prefix_natlang} {prop_natlang}".strip()
|
||||
else:
|
||||
final_tags = prop_tags
|
||||
final_natlang = prop_natlang
|
||||
@@ -5534,74 +5623,6 @@ async def _api_clothing_customizer_data(request):
|
||||
})
|
||||
|
||||
|
||||
@routes.post("/promptchain/clothing/assemble")
|
||||
async def _api_clothing_assemble(request):
|
||||
"""Build tag and natlang strings from selected clothing parts."""
|
||||
data, err = await parse_json(request)
|
||||
if err: return err
|
||||
|
||||
item_tag = data.get("item_tag", "")
|
||||
color_tag = data.get("color_tag", "")
|
||||
material_tag = data.get("material_tag", "")
|
||||
pattern_tag = data.get("pattern_tag", "")
|
||||
condition_tag = data.get("condition_tag", "")
|
||||
|
||||
db = get_db()
|
||||
|
||||
tag_parts = []
|
||||
natlang_parts = []
|
||||
|
||||
# Color
|
||||
if color_tag:
|
||||
color = db.execute(
|
||||
"SELECT * FROM clothing_colors WHERE tag = ?", (color_tag,)
|
||||
).fetchone()
|
||||
if color:
|
||||
tag_parts.append(color["prefix"])
|
||||
natlang_parts.append(color["prefix"])
|
||||
|
||||
# Material
|
||||
if material_tag:
|
||||
material = db.execute(
|
||||
"SELECT * FROM clothing_materials WHERE tag = ?", (material_tag,)
|
||||
).fetchone()
|
||||
if material:
|
||||
tag_parts.append(material["prefix"])
|
||||
natlang_parts.append(material["prefix"])
|
||||
|
||||
# Pattern
|
||||
if pattern_tag:
|
||||
pattern = db.execute(
|
||||
"SELECT * FROM clothing_patterns WHERE tag = ?", (pattern_tag,)
|
||||
).fetchone()
|
||||
if pattern:
|
||||
tag_parts.append(pattern["prefix"])
|
||||
natlang_parts.append(pattern["prefix"])
|
||||
|
||||
# Condition
|
||||
if condition_tag:
|
||||
condition = db.execute(
|
||||
"SELECT * FROM clothing_conditions WHERE tag = ?", (condition_tag,)
|
||||
).fetchone()
|
||||
if condition:
|
||||
tag_parts.append(condition["prefix"])
|
||||
natlang_parts.append(condition["prefix"])
|
||||
|
||||
# Clothing item itself
|
||||
if item_tag:
|
||||
item = db.execute(
|
||||
"SELECT * FROM clothing_items WHERE item_tag = ?", (item_tag,)
|
||||
).fetchone()
|
||||
if item:
|
||||
tag_parts.append(item["base_tags"])
|
||||
natlang_parts.append(item["base_natlang"])
|
||||
|
||||
tag = ", ".join(p for p in tag_parts if p)
|
||||
natlang = " ".join(p for p in natlang_parts if p)
|
||||
|
||||
return web.json_response({"tag": tag, "natlang": natlang})
|
||||
|
||||
|
||||
@routes.get("/promptchain/clothing/customizable-groups")
|
||||
async def _api_clothing_customizable_groups(request):
|
||||
"""Return clothing groups and items for customizable groups only."""
|
||||
@@ -5821,61 +5842,3 @@ async def _api_furniture_all(request):
|
||||
"patterns": patterns,
|
||||
"colors": colors,
|
||||
})
|
||||
|
||||
|
||||
@routes.post("/promptchain/furniture/assemble")
|
||||
async def _api_furniture_assemble(request):
|
||||
"""Build tag and natlang strings from selected furniture parts."""
|
||||
data, err = await parse_json(request)
|
||||
if err: return err
|
||||
|
||||
furniture_tag = data.get("furniture_tag", "")
|
||||
material_tag = data.get("material_tag", "")
|
||||
pattern_tag = data.get("pattern_tag", "")
|
||||
color_tag = data.get("color_tag", "")
|
||||
|
||||
db = get_db()
|
||||
|
||||
tag_parts = []
|
||||
natlang_parts = []
|
||||
|
||||
# Color
|
||||
if color_tag:
|
||||
color = db.execute(
|
||||
"SELECT * FROM furniture_colors WHERE tag = ?", (color_tag,)
|
||||
).fetchone()
|
||||
if color:
|
||||
tag_parts.append(color["prefix"])
|
||||
natlang_parts.append(color["prefix"])
|
||||
|
||||
# Material
|
||||
if material_tag:
|
||||
material = db.execute(
|
||||
"SELECT * FROM furniture_materials WHERE tag = ?", (material_tag,)
|
||||
).fetchone()
|
||||
if material:
|
||||
tag_parts.append(material["prefix"])
|
||||
natlang_parts.append(material["prefix"])
|
||||
|
||||
# Pattern
|
||||
if pattern_tag:
|
||||
pattern = db.execute(
|
||||
"SELECT * FROM furniture_patterns WHERE tag = ?", (pattern_tag,)
|
||||
).fetchone()
|
||||
if pattern:
|
||||
tag_parts.append(pattern["prefix"])
|
||||
natlang_parts.append(pattern["prefix"])
|
||||
|
||||
# Furniture item itself
|
||||
if furniture_tag:
|
||||
item = db.execute(
|
||||
"SELECT * FROM furniture WHERE tag = ?", (furniture_tag,)
|
||||
).fetchone()
|
||||
if item:
|
||||
tag_parts.append(item["base_tags"])
|
||||
natlang_parts.append(item["base_natlang"])
|
||||
|
||||
tag = ", ".join(p for p in tag_parts if p)
|
||||
natlang = " ".join(p for p in natlang_parts if p)
|
||||
|
||||
return web.json_response({"tag": tag, "natlang": natlang})
|
||||
|
||||
+47
-5
@@ -68,6 +68,14 @@ _state: dict[str, Any] = {
|
||||
"fingerprint": None,
|
||||
}
|
||||
|
||||
# A transient DB lock made _fingerprint() return zeros, which read as
|
||||
# "table changed" and kicked off a 5–10 min CPU re-embed. Cache the last
|
||||
# good fingerprint + the DB mtime it was read at: return the cached value
|
||||
# on error (never treat a read failure as a change), and skip the recompute
|
||||
# entirely while the file is unchanged.
|
||||
_last_successful_fingerprint: tuple | None = None
|
||||
_last_db_mtime: float | None = None
|
||||
|
||||
|
||||
def _open_db() -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(str(DB_PATH))
|
||||
@@ -151,7 +159,12 @@ def _fingerprint() -> tuple:
|
||||
table state. Changes when wiki rows are added/removed/refreshed
|
||||
(body_hash sum) OR when aliases are added/removed/edited. Either
|
||||
forces a rebuild — alias changes shift the embedded body_full
|
||||
text so cached embeddings need to be regenerated."""
|
||||
text so cached embeddings need to be regenerated.
|
||||
|
||||
On a transient DB lock / read error, returns the last successful
|
||||
fingerprint instead of zeros, so a momentary lock can't masquerade
|
||||
as a table change and trigger a spurious rebuild."""
|
||||
global _last_successful_fingerprint
|
||||
try:
|
||||
conn = _open_db()
|
||||
_ensure_aliases_schema(conn)
|
||||
@@ -167,11 +180,15 @@ def _fingerprint() -> tuple:
|
||||
"FROM tag_aliases"
|
||||
).fetchone()
|
||||
conn.close()
|
||||
return (
|
||||
result = (
|
||||
wiki_row["m"], wiki_row["c"], wiki_row["h"],
|
||||
alias_row["c"], alias_row["h"],
|
||||
)
|
||||
_last_successful_fingerprint = result
|
||||
return result
|
||||
except Exception:
|
||||
if _last_successful_fingerprint is not None:
|
||||
return _last_successful_fingerprint
|
||||
return (0, 0, 0, 0, 0)
|
||||
|
||||
|
||||
@@ -292,7 +309,7 @@ def _load_index_from_disk() -> bool:
|
||||
logger.info("tag_search: cache model mismatch — rebuilding")
|
||||
return False
|
||||
|
||||
live_fp = _fingerprint()
|
||||
live_fp = _get_fingerprint_cached()
|
||||
cached_fp = tuple(manifest.get("fingerprint") or ())
|
||||
if cached_fp != live_fp:
|
||||
logger.info(
|
||||
@@ -365,10 +382,26 @@ def _rebuild_index(on_status: Callable[[str], None] | None = None) -> None:
|
||||
_persist_index_to_disk()
|
||||
|
||||
|
||||
def _get_fingerprint_cached() -> tuple:
|
||||
"""Gate the expensive _fingerprint() (DDL + alias seed-sync + two
|
||||
aggregate scans) on the DB file mtime. The fingerprint only changes
|
||||
when the file changes, so while mtime is steady — the common case for
|
||||
a search — return the cached value and skip the query entirely."""
|
||||
global _last_db_mtime
|
||||
try:
|
||||
current_mtime = DB_PATH.stat().st_mtime
|
||||
except Exception:
|
||||
return _fingerprint()
|
||||
if _last_db_mtime == current_mtime and _last_successful_fingerprint is not None:
|
||||
return _last_successful_fingerprint
|
||||
_last_db_mtime = current_mtime
|
||||
return _fingerprint()
|
||||
|
||||
|
||||
def _ensure_index_fresh(on_status: Callable[[str], None] | None = None) -> None:
|
||||
"""Try disk first, then rebuild. Disk load is fast; rebuild is the
|
||||
minutes-long path."""
|
||||
if _state["embeddings"] is not None and _state["fingerprint"] == _fingerprint():
|
||||
if _state["embeddings"] is not None and _state["fingerprint"] == _get_fingerprint_cached():
|
||||
return
|
||||
if _load_index_from_disk():
|
||||
return
|
||||
@@ -501,8 +534,17 @@ def _row_for_tag(tag: str) -> dict | None:
|
||||
def warmup() -> None:
|
||||
"""Best-effort eager load on server boot. Disk-cache hit is < 1s;
|
||||
cold rebuild is ~5–10 min on CPU and is the only thing that takes
|
||||
real time here."""
|
||||
real time here.
|
||||
|
||||
Seeds the alias lookup cache up front so the literal alias_scan path
|
||||
doesn't hit the DB on every search, and warms the fingerprint cache so
|
||||
early searches can't trip a spurious rebuild on a transient lock."""
|
||||
import time
|
||||
try:
|
||||
with _lock:
|
||||
_alias_lookup_list()
|
||||
except Exception:
|
||||
logger.warning("tag_search: alias cache seed failed", exc_info=True)
|
||||
for attempt in range(3):
|
||||
with _lock:
|
||||
if _embed_model.get() is not None:
|
||||
|
||||
@@ -5458,6 +5458,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.pcr-atb2-cust-focus.svelte-1mwxie1 input:where(.svelte-1mwxie1) { accent-color: #7c3aed; }
|
||||
.pcr-atb2-cust-focus-disabled.svelte-1mwxie1 { opacity: 0.5; cursor: default; }
|
||||
.pcr-atb2-cust-focus-hint.svelte-1mwxie1 {
|
||||
font-size: 11px; color: #888; margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ var root_14$3 = from_html(`<button class="pcr-atb2-cust-rowclear svelte-1mwxie1"
|
||||
var root_17$2 = from_html(`<div><span class="pcr-atb2-cust-icon-bullet svelte-1mwxie1"></span> <span> </span></div>`);
|
||||
var root_16$2 = from_html(`<div class="pcr-atb2-cust-popgroup svelte-1mwxie1"> </div> <!>`, 1);
|
||||
var root_15$3 = from_html(`<div class="pcr-atb2-cust-popover svelte-1mwxie1"></div>`);
|
||||
var root_2$3 = from_html(`<div class="pcr-atb2-cust-body svelte-1mwxie1"><div><button class="pcr-atb2-cust-rowbtn svelte-1mwxie1" type="button"><span class="pcr-atb2-cust-icon pcr-atb2-cust-icon-color svelte-1mwxie1"></span> <span class="pcr-atb2-cust-rowlabel svelte-1mwxie1"> </span> <!> <span class="pcr-atb2-cust-chev svelte-1mwxie1">▾</span></button> <!></div> <div><button class="pcr-atb2-cust-rowbtn svelte-1mwxie1" type="button"><span class="pcr-atb2-cust-icon pcr-atb2-cust-icon-pattern svelte-1mwxie1" aria-hidden="true"><svg viewBox="0 0 16 16" width="16" height="16"><rect x="0" y="0" width="4" height="4" fill="currentColor"></rect><rect x="8" y="0" width="4" height="4" fill="currentColor"></rect><rect x="4" y="4" width="4" height="4" fill="currentColor"></rect><rect x="12" y="4" width="4" height="4" fill="currentColor"></rect><rect x="0" y="8" width="4" height="4" fill="currentColor"></rect><rect x="8" y="8" width="4" height="4" fill="currentColor"></rect><rect x="4" y="12" width="4" height="4" fill="currentColor"></rect><rect x="12" y="12" width="4" height="4" fill="currentColor"></rect></svg></span> <span class="pcr-atb2-cust-rowlabel svelte-1mwxie1"> </span> <!> <span class="pcr-atb2-cust-chev svelte-1mwxie1">▾</span></button> <!></div> <div><button class="pcr-atb2-cust-rowbtn svelte-1mwxie1" type="button"><span class="pcr-atb2-cust-icon pcr-atb2-cust-icon-material svelte-1mwxie1" aria-hidden="true"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M1 5 Q 4 2, 8 5 T 15 5"></path><path d="M1 11 Q 4 8, 8 11 T 15 11"></path></svg></span> <span class="pcr-atb2-cust-rowlabel svelte-1mwxie1"> </span> <!> <span class="pcr-atb2-cust-chev svelte-1mwxie1">▾</span></button> <!></div> <div><button class="pcr-atb2-cust-rowbtn svelte-1mwxie1" type="button"><span class="pcr-atb2-cust-icon pcr-atb2-cust-icon-condition svelte-1mwxie1" aria-hidden="true"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><polyline points="2,4 5,7 3,9 6,12 4,14"></polyline><polyline points="10,2 13,5 11,8 14,11 12,14"></polyline></svg></span> <span class="pcr-atb2-cust-rowlabel svelte-1mwxie1"> </span> <!> <span class="pcr-atb2-cust-chev svelte-1mwxie1">▾</span></button> <!></div> <label class="pcr-atb2-cust-focus svelte-1mwxie1"><input type="checkbox" class="svelte-1mwxie1"/> <span>Add focus tags</span> <span class="pcr-atb2-cust-focus-hint svelte-1mwxie1">Camera focuses on this item</span></label> <div class="pcr-atb2-cust-preview svelte-1mwxie1"><div class="pcr-atb2-cust-preview-label svelte-1mwxie1">Preview</div> <div class="pcr-atb2-cust-preview-text svelte-1mwxie1"> </div></div></div> <div class="pcr-atb2-cust-footer svelte-1mwxie1"><button class="pcr-atb2-cust-btn pcr-atb2-cust-cancel svelte-1mwxie1">Cancel</button> <button class="pcr-atb2-cust-btn pcr-atb2-cust-ok svelte-1mwxie1">Apply</button></div>`, 1);
|
||||
var root_2$3 = from_html(`<div class="pcr-atb2-cust-body svelte-1mwxie1"><div><button class="pcr-atb2-cust-rowbtn svelte-1mwxie1" type="button"><span class="pcr-atb2-cust-icon pcr-atb2-cust-icon-color svelte-1mwxie1"></span> <span class="pcr-atb2-cust-rowlabel svelte-1mwxie1"> </span> <!> <span class="pcr-atb2-cust-chev svelte-1mwxie1">▾</span></button> <!></div> <div><button class="pcr-atb2-cust-rowbtn svelte-1mwxie1" type="button"><span class="pcr-atb2-cust-icon pcr-atb2-cust-icon-pattern svelte-1mwxie1" aria-hidden="true"><svg viewBox="0 0 16 16" width="16" height="16"><rect x="0" y="0" width="4" height="4" fill="currentColor"></rect><rect x="8" y="0" width="4" height="4" fill="currentColor"></rect><rect x="4" y="4" width="4" height="4" fill="currentColor"></rect><rect x="12" y="4" width="4" height="4" fill="currentColor"></rect><rect x="0" y="8" width="4" height="4" fill="currentColor"></rect><rect x="8" y="8" width="4" height="4" fill="currentColor"></rect><rect x="4" y="12" width="4" height="4" fill="currentColor"></rect><rect x="12" y="12" width="4" height="4" fill="currentColor"></rect></svg></span> <span class="pcr-atb2-cust-rowlabel svelte-1mwxie1"> </span> <!> <span class="pcr-atb2-cust-chev svelte-1mwxie1">▾</span></button> <!></div> <div><button class="pcr-atb2-cust-rowbtn svelte-1mwxie1" type="button"><span class="pcr-atb2-cust-icon pcr-atb2-cust-icon-material svelte-1mwxie1" aria-hidden="true"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M1 5 Q 4 2, 8 5 T 15 5"></path><path d="M1 11 Q 4 8, 8 11 T 15 11"></path></svg></span> <span class="pcr-atb2-cust-rowlabel svelte-1mwxie1"> </span> <!> <span class="pcr-atb2-cust-chev svelte-1mwxie1">▾</span></button> <!></div> <div><button class="pcr-atb2-cust-rowbtn svelte-1mwxie1" type="button"><span class="pcr-atb2-cust-icon pcr-atb2-cust-icon-condition svelte-1mwxie1" aria-hidden="true"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><polyline points="2,4 5,7 3,9 6,12 4,14"></polyline><polyline points="10,2 13,5 11,8 14,11 12,14"></polyline></svg></span> <span class="pcr-atb2-cust-rowlabel svelte-1mwxie1"> </span> <!> <span class="pcr-atb2-cust-chev svelte-1mwxie1">▾</span></button> <!></div> <label><input type="checkbox" class="svelte-1mwxie1"/> <span>Add focus tags</span> <span class="pcr-atb2-cust-focus-hint svelte-1mwxie1"> </span></label> <div class="pcr-atb2-cust-preview svelte-1mwxie1"><div class="pcr-atb2-cust-preview-label svelte-1mwxie1">Preview</div> <div class="pcr-atb2-cust-preview-text svelte-1mwxie1"> </div></div></div> <div class="pcr-atb2-cust-footer svelte-1mwxie1"><button class="pcr-atb2-cust-btn pcr-atb2-cust-cancel svelte-1mwxie1">Cancel</button> <button class="pcr-atb2-cust-btn pcr-atb2-cust-ok svelte-1mwxie1">Apply</button></div>`, 1);
|
||||
var root$2 = from_html(`<div class="pcr-atb2-cust-overlay svelte-1mwxie1"><div class="pcr-atb2-cust-modal svelte-1mwxie1"><div class="pcr-atb2-cust-header svelte-1mwxie1"><span class="pcr-atb2-cust-title svelte-1mwxie1">Customize <strong class="svelte-1mwxie1"> </strong></span> <button class="pcr-atb2-cust-close svelte-1mwxie1" aria-label="Close">×</button></div> <!></div></div>`);
|
||||
function Customizer($$anchor, $$props) {
|
||||
var _a, _b, _c, _d, _e;
|
||||
@@ -32,6 +32,12 @@ function Customizer($$anchor, $$props) {
|
||||
3,
|
||||
null
|
||||
// { color, pattern, material, condition, focus } — pre-fill when editing
|
||||
), isNaturalMode = prop(
|
||||
$$props,
|
||||
"isNaturalMode",
|
||||
3,
|
||||
false
|
||||
// current output mode, so the preview + focus match what the parent emits
|
||||
), onConfirm = prop($$props, "onConfirm", 3, () => {
|
||||
}), onCancel = prop($$props, "onCancel", 3, () => {
|
||||
});
|
||||
@@ -157,13 +163,20 @@ function Customizer($$anchor, $$props) {
|
||||
let preview = user_derived(() => {
|
||||
var _a2, _b2, _c2, _d2;
|
||||
if (!get(data)) return ($$props.item.display_name || $$props.item.item_tag).toLowerCase();
|
||||
const parts = [];
|
||||
if ((_a2 = get(conditionOpt)) == null ? void 0 : _a2.prefix) parts.push(get(conditionOpt).prefix);
|
||||
if ((_b2 = get(colorOpt)) == null ? void 0 : _b2.prefix) parts.push(get(colorOpt).prefix);
|
||||
if ((_c2 = get(patternOpt)) == null ? void 0 : _c2.prefix) parts.push(get(patternOpt).prefix);
|
||||
if ((_d2 = get(materialOpt)) == null ? void 0 : _d2.prefix) parts.push(get(materialOpt).prefix);
|
||||
parts.push(($$props.item.display_name || $$props.item.item_tag).toLowerCase());
|
||||
const phrase = parts.join(" ");
|
||||
const prefixes = [
|
||||
(_a2 = get(conditionOpt)) == null ? void 0 : _a2.prefix,
|
||||
(_b2 = get(colorOpt)) == null ? void 0 : _b2.prefix,
|
||||
(_c2 = get(patternOpt)) == null ? void 0 : _c2.prefix,
|
||||
(_d2 = get(materialOpt)) == null ? void 0 : _d2.prefix
|
||||
].filter(Boolean);
|
||||
if (!isNaturalMode()) {
|
||||
const slug = (s) => s.replace(/\s+/g, "_");
|
||||
return [...prefixes.map(slug), $$props.item.item_tag || ""].filter(Boolean).join("_");
|
||||
}
|
||||
const phrase = [
|
||||
...prefixes,
|
||||
($$props.item.display_name || $$props.item.item_tag).toLowerCase()
|
||||
].join(" ");
|
||||
return get(focus) ? `${phrase}, presenting ${phrase} to viewer, ${phrase} focus` : phrase;
|
||||
});
|
||||
let isCustomized = user_derived(() => !!get(color) || get(pattern) && get(pattern) !== "solid" || !!get(material) || get(condition) && get(condition) !== "default" || get(focus));
|
||||
@@ -436,10 +449,13 @@ function Customizer($$anchor, $$props) {
|
||||
});
|
||||
}
|
||||
var label = sibling(div_18, 2);
|
||||
let classes_10;
|
||||
var input = child(label);
|
||||
var span_11 = sibling(input, 4);
|
||||
var text_12 = child(span_11);
|
||||
var div_22 = sibling(label, 2);
|
||||
var div_23 = sibling(child(div_22), 2);
|
||||
var text_12 = child(div_23);
|
||||
var text_13 = child(div_23);
|
||||
var div_24 = sibling(div_4, 2);
|
||||
var button_9 = child(div_24);
|
||||
var button_10 = sibling(button_9, 2);
|
||||
@@ -455,7 +471,10 @@ function Customizer($$anchor, $$props) {
|
||||
set_text(text_7, ((_c2 = get(materialOpt)) == null ? void 0 : _c2.display) || "Material");
|
||||
classes_8 = set_class(div_18, 1, "pcr-atb2-cust-row svelte-1mwxie1", null, classes_8, { open: get(openRow) === "condition" });
|
||||
set_text(text_9, ((_d2 = get(conditionOpt)) == null ? void 0 : _d2.display) || "Default");
|
||||
set_text(text_12, get(preview));
|
||||
classes_10 = set_class(label, 1, "pcr-atb2-cust-focus svelte-1mwxie1", null, classes_10, { "pcr-atb2-cust-focus-disabled": !isNaturalMode() });
|
||||
input.disabled = !isNaturalMode();
|
||||
set_text(text_12, isNaturalMode() ? "Camera focuses on this item" : "Natural-language mode only");
|
||||
set_text(text_13, get(preview));
|
||||
},
|
||||
[() => colorSwatch(get(color))]
|
||||
);
|
||||
@@ -521,6 +540,12 @@ function FurnitureCustomizer($$anchor, $$props) {
|
||||
3,
|
||||
null
|
||||
// { material, pattern, color, action } when editing
|
||||
), isNaturalMode = prop(
|
||||
$$props,
|
||||
"isNaturalMode",
|
||||
3,
|
||||
false
|
||||
// current output mode — preview shows tag vs natlang form to match emit
|
||||
), onConfirm = prop($$props, "onConfirm", 3, () => {
|
||||
}), onCancel = prop($$props, "onCancel", 3, () => {
|
||||
});
|
||||
@@ -979,7 +1004,7 @@ function FurnitureCustomizer($$anchor, $$props) {
|
||||
set_text(text_12, contextSubject() && get(action2) ? "Add Interaction" : "Add to Scene");
|
||||
},
|
||||
[
|
||||
() => get(previewNat) || get(previewTag) || ($$props.item.display_name || $$props.item.item_tag).toLowerCase()
|
||||
() => isNaturalMode() ? get(previewNat) || get(previewTag) || ($$props.item.display_name || $$props.item.item_tag).toLowerCase() : get(previewTag) || get(previewNat) || ($$props.item.display_name || $$props.item.item_tag).toLowerCase()
|
||||
]
|
||||
);
|
||||
delegated("click", div, handleOverlayClick);
|
||||
@@ -1029,7 +1054,15 @@ function FantasyCustomizer($$anchor, $$props) {
|
||||
3,
|
||||
null
|
||||
// { shape, color, type } when re-editing
|
||||
), onConfirm = prop($$props, "onConfirm", 3, () => {
|
||||
);
|
||||
prop(
|
||||
$$props,
|
||||
"isNaturalMode",
|
||||
3,
|
||||
false
|
||||
// current output mode (declared so the parent can pass it through)
|
||||
);
|
||||
let onConfirm = prop($$props, "onConfirm", 3, () => {
|
||||
}), onCancel = prop($$props, "onCancel", 3, () => {
|
||||
});
|
||||
let fetched = state(null);
|
||||
@@ -3142,11 +3175,8 @@ function TagBuilder2($$anchor, $$props) {
|
||||
if (e.target.closest(".pcr-atb2-identity-trigger")) return;
|
||||
closeIdentityPicker();
|
||||
}
|
||||
const id = setTimeout(() => document.addEventListener("click", onDocClick, true), 0);
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
document.removeEventListener("click", onDocClick, true);
|
||||
};
|
||||
document.addEventListener("click", onDocClick, true);
|
||||
return () => document.removeEventListener("click", onDocClick, true);
|
||||
});
|
||||
function togglePresetPicker(kind, subjId, triggerEl) {
|
||||
var _a2, _b;
|
||||
@@ -3256,11 +3286,8 @@ function TagBuilder2($$anchor, $$props) {
|
||||
if (e.target.closest(".pcr-atb2-preset-trigger")) return;
|
||||
closePresetPicker();
|
||||
}
|
||||
const id = setTimeout(() => document.addEventListener("click", onDocClick, true), 0);
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
document.removeEventListener("click", onDocClick, true);
|
||||
};
|
||||
document.addEventListener("click", onDocClick, true);
|
||||
return () => document.removeEventListener("click", onDocClick, true);
|
||||
});
|
||||
function removePreset(subjId, kind) {
|
||||
set(
|
||||
@@ -4387,11 +4414,8 @@ function TagBuilder2($$anchor, $$props) {
|
||||
if (e.target.closest(".pcr-atb2-modifier-trigger")) return;
|
||||
closeModifierPicker();
|
||||
}
|
||||
const id = setTimeout(() => document.addEventListener("click", onDocClick, true), 0);
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
document.removeEventListener("click", onDocClick, true);
|
||||
};
|
||||
document.addEventListener("click", onDocClick, true);
|
||||
return () => document.removeEventListener("click", onDocClick, true);
|
||||
});
|
||||
function identityMatchToken(option) {
|
||||
if ((option == null ? void 0 : option.kind) === "cast") {
|
||||
@@ -4469,7 +4493,7 @@ function TagBuilder2($$anchor, $$props) {
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
el.classList.add("pcr-atb2-flash");
|
||||
setTimeout(() => el.classList.remove("pcr-atb2-flash"), 1100);
|
||||
el.addEventListener("animationend", () => el.classList.remove("pcr-atb2-flash"), { once: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -7545,6 +7569,9 @@ ${filtered}` : filtered;
|
||||
get initial() {
|
||||
return get(customizerOpen).initial;
|
||||
},
|
||||
get isNaturalMode() {
|
||||
return get(isNaturalMode);
|
||||
},
|
||||
onConfirm: commitCustomizer,
|
||||
onCancel: cancelCustomizer
|
||||
});
|
||||
@@ -7584,6 +7611,9 @@ ${filtered}` : filtered;
|
||||
get initial() {
|
||||
return get(furnitureCustomizerOpen).initial;
|
||||
},
|
||||
get isNaturalMode() {
|
||||
return get(isNaturalMode);
|
||||
},
|
||||
onConfirm: commitFurnitureCustomizer,
|
||||
onCancel: cancelFurnitureCustomizer
|
||||
});
|
||||
@@ -7607,6 +7637,9 @@ ${filtered}` : filtered;
|
||||
get initial() {
|
||||
return get(fantasyCustomizerOpen).initial;
|
||||
},
|
||||
get isNaturalMode() {
|
||||
return get(isNaturalMode);
|
||||
},
|
||||
onConfirm: commitFantasyCustomizer,
|
||||
onCancel: cancelFantasyCustomizer
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
let {
|
||||
item, // chip the user is customizing: { item_tag, display_name, base_tags, base_natlang, item_group }
|
||||
initial = null, // { color, pattern, material, condition, focus } — pre-fill when editing
|
||||
isNaturalMode = false, // current output mode, so the preview + focus match what the parent emits
|
||||
onConfirm = () => {},
|
||||
onCancel = () => {},
|
||||
} = $props();
|
||||
@@ -124,13 +125,16 @@
|
||||
|
||||
let preview = $derived.by(() => {
|
||||
if (!data) return (item.display_name || item.item_tag).toLowerCase();
|
||||
const parts = [];
|
||||
if (conditionOpt?.prefix) parts.push(conditionOpt.prefix);
|
||||
if (colorOpt?.prefix) parts.push(colorOpt.prefix);
|
||||
if (patternOpt?.prefix) parts.push(patternOpt.prefix);
|
||||
if (materialOpt?.prefix) parts.push(materialOpt.prefix);
|
||||
parts.push((item.display_name || item.item_tag).toLowerCase());
|
||||
const phrase = parts.join(" ");
|
||||
// Mirror the parent's buildModifiedPhrase so the preview matches what is
|
||||
// actually emitted: condition, color, pattern, material, then the item.
|
||||
const prefixes = [conditionOpt?.prefix, colorOpt?.prefix, patternOpt?.prefix, materialOpt?.prefix].filter(Boolean);
|
||||
if (!isNaturalMode) {
|
||||
// Tag mode: underscore-joined prefix slugs + item_tag. The focus
|
||||
// phrase is natural-language only and isn't part of the tag emit.
|
||||
const slug = (s) => s.replace(/\s+/g, "_");
|
||||
return [...prefixes.map(slug), item.item_tag || ""].filter(Boolean).join("_");
|
||||
}
|
||||
const phrase = [...prefixes, (item.display_name || item.item_tag).toLowerCase()].join(" ");
|
||||
return focus ? `${phrase}, presenting ${phrase} to viewer, ${phrase} focus` : phrase;
|
||||
});
|
||||
|
||||
@@ -322,11 +326,11 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- FOCUS -->
|
||||
<label class="pcr-atb2-cust-focus">
|
||||
<input type="checkbox" bind:checked={focus} />
|
||||
<!-- FOCUS — natural-language only; the tag-mode emit has no focus phrase -->
|
||||
<label class="pcr-atb2-cust-focus" class:pcr-atb2-cust-focus-disabled={!isNaturalMode}>
|
||||
<input type="checkbox" bind:checked={focus} disabled={!isNaturalMode} />
|
||||
<span>Add focus tags</span>
|
||||
<span class="pcr-atb2-cust-focus-hint">Camera focuses on this item</span>
|
||||
<span class="pcr-atb2-cust-focus-hint">{isNaturalMode ? "Camera focuses on this item" : "Natural-language mode only"}</span>
|
||||
</label>
|
||||
|
||||
<div class="pcr-atb2-cust-preview">
|
||||
@@ -458,6 +462,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.pcr-atb2-cust-focus input { accent-color: #7c3aed; }
|
||||
.pcr-atb2-cust-focus-disabled { opacity: 0.5; cursor: default; }
|
||||
.pcr-atb2-cust-focus-hint {
|
||||
font-size: 11px; color: #888; margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
item, // { item_tag, display_name, base_tags, base_natlang, item_group }
|
||||
data = null, // { shapes, colors, types } — when null, self-fetches
|
||||
initial = null, // { shape, color, type } when re-editing
|
||||
isNaturalMode = false, // current output mode (declared so the parent can pass it through)
|
||||
onConfirm = () => {},
|
||||
onCancel = () => {},
|
||||
} = $props();
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
actionOverrides = {}, // { prop_tag: [action_tag, ...] } — narrows verbs when present
|
||||
contextSubject = null, // null = scene scope; truthy = subject interaction
|
||||
initial = null, // { material, pattern, color, action } when editing
|
||||
isNaturalMode = false, // current output mode — preview shows tag vs natlang form to match emit
|
||||
onConfirm = () => {},
|
||||
onCancel = () => {},
|
||||
} = $props();
|
||||
@@ -319,7 +320,7 @@
|
||||
|
||||
<div class="pcr-atb2-cust-preview">
|
||||
<div class="pcr-atb2-cust-preview-label">Preview</div>
|
||||
<div class="pcr-atb2-cust-preview-text">{previewNat || previewTag || (item.display_name || item.item_tag).toLowerCase()}</div>
|
||||
<div class="pcr-atb2-cust-preview-text">{isNaturalMode ? (previewNat || previewTag || (item.display_name || item.item_tag).toLowerCase()) : (previewTag || previewNat || (item.display_name || item.item_tag).toLowerCase())}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1672,11 +1672,11 @@
|
||||
if (e.target.closest('.pcr-atb2-identity-trigger')) return;
|
||||
closeIdentityPicker();
|
||||
}
|
||||
const id = setTimeout(() => document.addEventListener('click', onDocClick, true), 0);
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
document.removeEventListener('click', onDocClick, true);
|
||||
};
|
||||
// Add directly — this $effect runs after the opening click has finished
|
||||
// dispatching, so the capture-phase listener can't catch that same click
|
||||
// (no setTimeout(0) deferral needed).
|
||||
document.addEventListener('click', onDocClick, true);
|
||||
return () => document.removeEventListener('click', onDocClick, true);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
@@ -1800,11 +1800,11 @@
|
||||
if (e.target.closest('.pcr-atb2-preset-trigger')) return;
|
||||
closePresetPicker();
|
||||
}
|
||||
const id = setTimeout(() => document.addEventListener('click', onDocClick, true), 0);
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
document.removeEventListener('click', onDocClick, true);
|
||||
};
|
||||
// Add directly — this $effect runs after the opening click has finished
|
||||
// dispatching, so the capture-phase listener can't catch that same click
|
||||
// (no setTimeout(0) deferral needed).
|
||||
document.addEventListener('click', onDocClick, true);
|
||||
return () => document.removeEventListener('click', onDocClick, true);
|
||||
});
|
||||
|
||||
// Remove an active outfit or pose: subtract the snapshot's contribution
|
||||
@@ -3306,11 +3306,11 @@
|
||||
if (e.target.closest('.pcr-atb2-modifier-trigger')) return;
|
||||
closeModifierPicker();
|
||||
}
|
||||
const id = setTimeout(() => document.addEventListener('click', onDocClick, true), 0);
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
document.removeEventListener('click', onDocClick, true);
|
||||
};
|
||||
// Add directly — this $effect runs after the opening click has finished
|
||||
// dispatching, so the capture-phase listener can't catch that same click
|
||||
// (no setTimeout(0) deferral needed).
|
||||
document.addEventListener('click', onDocClick, true);
|
||||
return () => document.removeEventListener('click', onDocClick, true);
|
||||
});
|
||||
|
||||
// What token should partitionTokens treat as the identity assertion in
|
||||
@@ -3416,7 +3416,9 @@
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
el.classList.add("pcr-atb2-flash");
|
||||
setTimeout(() => el.classList.remove("pcr-atb2-flash"), 1100);
|
||||
// Remove on animationend (the .pcr-atb2-flash rule runs pcrAtb2Flash
|
||||
// 1s once) so re-jumping to the same card re-triggers it — no timer.
|
||||
el.addEventListener("animationend", () => el.classList.remove("pcr-atb2-flash"), { once: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -5986,6 +5988,7 @@
|
||||
<Customizer
|
||||
item={customizerOpen.item}
|
||||
initial={customizerOpen.initial}
|
||||
isNaturalMode={isNaturalMode}
|
||||
onConfirm={commitCustomizer}
|
||||
onCancel={cancelCustomizer}
|
||||
/>
|
||||
@@ -6004,6 +6007,7 @@
|
||||
? subjects.find(s => s.id === furnitureCustomizerOpen.subjId)
|
||||
: null}
|
||||
initial={furnitureCustomizerOpen.initial}
|
||||
isNaturalMode={isNaturalMode}
|
||||
onConfirm={commitFurnitureCustomizer}
|
||||
onCancel={cancelFurnitureCustomizer}
|
||||
/>
|
||||
@@ -6015,6 +6019,7 @@
|
||||
item={fantasyCustomizerOpen.item}
|
||||
data={fantasyModData}
|
||||
initial={fantasyCustomizerOpen.initial}
|
||||
isNaturalMode={isNaturalMode}
|
||||
onConfirm={commitFantasyCustomizer}
|
||||
onCancel={cancelFantasyCustomizer}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user