From a0510e640512d4c932a00832eb769c76b7128ee7 Mon Sep 17 00:00:00 2001 From: Ryan Kegel Date: Sat, 17 Oct 2026 12:53:38 -0400 Subject: [PATCH] Add new test file for phase 19 scenery with unique identifier --- .codex/agents/artist.toml | 3 + .codex/skills/asset-creation/SKILL.md | 2 +- .gitignore | 5 + content/props/art_bookshelf-v1.prop | 712 + content/props/art_city_skyline-v1.prop | 366 + content/props/art_fireplace_mantel-v2.prop | 26890 ++++++++++++++++ content/props/art_framed_picture-v1.prop | 274 + content/props/art_potted_plant-v1.prop | 236 + content/props/art_rug-v1.prop | 304 + content/props/art_storefront-v1.prop | 546 + content/props/art_town_house-v1.prop | 473 + content/props/art_tree-v2.prop | 1749 + content/props/art_wall_clock-v1.prop | 430 + content/props/art_window_frame-v1.prop | 246 + content/props/prop_armchair-v1.prop | 276 + content/props/prop_coffee_table-v1.prop | 204 + content/props/prop_couch-v1.prop | 298 + content/stages/background01.scene | 641 + content/stages/background02.scene | 659 + content/stages/background03.scene | 319 + content/stages/starters/blank.scene | 9 + content/stages/starters/living_room.scene | 30 + content/stages/starters/town_street.scene | 17 + .../tech_debt_and_optimizations.md | 5 + docs/ideas/feature_backlog.md | 4 + docs/plans/phase_19_handoff.md | 34 + ...19_scenery_backgrounds_surface_finishes.md | 72 +- ...19_scenery_backgrounds_surface_finishes.md | 252 + .../phase_19_scenery_backgrounds_ui_ux.md | 277 + ...ase_19_selected_artwork_depth_inspector.md | 82 + scenes/stage/asset_selector.tscn | 35 + scripts/editors/prop/prop_canvas.gd | 254 +- scripts/editors/prop/prop_editor.gd | 395 +- scripts/editors/prop/prop_file.gd | 23 +- scripts/editors/stage/sandbox_stage.gd | 999 +- scripts/editors/stage/stage_asset_browser.gd | 70 +- scripts/editors/stage/stage_chrome.gd | 9 +- scripts/editors/stage/stage_modal_views.gd | 11 + scripts/editors/stage/stage_scene_adapter.gd | 65 +- .../editors/stage/stage_scenery_controller.gd | 801 + .../stage/stage_scenery_controller.gd.uid | 1 + .../editors/stage/stage_terrain_painter.gd | 17 + .../stage/stage_use_target_capture_bar.gd | 103 +- scripts/props/prop_library.gd | 51 +- scripts/shared/ui/asset_card.gd | 73 +- scripts/shared/ui/asset_selector.gd | 155 +- .../scenery/stage_background_card_preview.gd | 45 + .../stage_background_card_preview.gd.uid | 1 + .../stage/scenery/stage_scenery_artwork.gd | 134 + .../scenery/stage_scenery_artwork.gd.uid | 1 + scripts/stage/scenery/stage_sky_view.gd | 323 + scripts/stage/scenery/stage_sky_view.gd.uid | 1 + .../scenery/stage_starter_card_preview.gd | 116 + .../scenery/stage_starter_card_preview.gd.uid | 1 + .../stage/scenery/stage_surface_renderer.gd | 267 + .../scenery/stage_surface_renderer.gd.uid | 1 + scripts/stage/scenery/stage_wall_panel.gd | 36 + scripts/stage/scenery/stage_wall_panel.gd.uid | 1 + scripts/stage/stage_draw_order.gd | 21 + scripts/stage/stage_draw_order.gd.uid | 1 + scripts/stage/stage_gizmos.gd | 67 +- scripts/stage/stage_scene_io.gd | 76 +- scripts/stage/stage_selection.gd | 18 +- scripts/stage/stage_spawner.gd | 10 +- scripts/terrain/terrain_block.gd | 25 + .../phase19_depth_inspector_visual_capture.gd | 54 + ...se19_depth_inspector_visual_capture.gd.uid | 1 + tests/phase19_gui_visual_capture.gd | 146 + tests/phase19_gui_visual_capture.gd.uid | 1 + tests/test_phase10_prop_integration.gd | 6 +- tests/test_phase17a_prop_roles.gd | 2 +- tests/test_phase19_scenery.gd | 1248 + tests/test_phase19_scenery.gd.uid | 1 + tests/test_phase7_palette.gd | 16 +- 74 files changed, 40948 insertions(+), 149 deletions(-) create mode 100644 content/props/art_bookshelf-v1.prop create mode 100644 content/props/art_city_skyline-v1.prop create mode 100644 content/props/art_fireplace_mantel-v2.prop create mode 100644 content/props/art_framed_picture-v1.prop create mode 100644 content/props/art_potted_plant-v1.prop create mode 100644 content/props/art_rug-v1.prop create mode 100644 content/props/art_storefront-v1.prop create mode 100644 content/props/art_town_house-v1.prop create mode 100644 content/props/art_tree-v2.prop create mode 100644 content/props/art_wall_clock-v1.prop create mode 100644 content/props/art_window_frame-v1.prop create mode 100644 content/props/prop_armchair-v1.prop create mode 100644 content/props/prop_coffee_table-v1.prop create mode 100644 content/props/prop_couch-v1.prop create mode 100644 content/stages/background01.scene create mode 100644 content/stages/background02.scene create mode 100644 content/stages/background03.scene create mode 100644 content/stages/starters/blank.scene create mode 100644 content/stages/starters/living_room.scene create mode 100644 content/stages/starters/town_street.scene create mode 100644 docs/plans/phase_19_handoff.md create mode 100644 docs/specs/phase_19_scenery_backgrounds_surface_finishes.md create mode 100644 docs/specs/phase_19_scenery_backgrounds_ui_ux.md create mode 100644 docs/specs/phase_19_selected_artwork_depth_inspector.md create mode 100644 scripts/editors/stage/stage_scenery_controller.gd create mode 100644 scripts/editors/stage/stage_scenery_controller.gd.uid create mode 100644 scripts/stage/scenery/stage_background_card_preview.gd create mode 100644 scripts/stage/scenery/stage_background_card_preview.gd.uid create mode 100644 scripts/stage/scenery/stage_scenery_artwork.gd create mode 100644 scripts/stage/scenery/stage_scenery_artwork.gd.uid create mode 100644 scripts/stage/scenery/stage_sky_view.gd create mode 100644 scripts/stage/scenery/stage_sky_view.gd.uid create mode 100644 scripts/stage/scenery/stage_starter_card_preview.gd create mode 100644 scripts/stage/scenery/stage_starter_card_preview.gd.uid create mode 100644 scripts/stage/scenery/stage_surface_renderer.gd create mode 100644 scripts/stage/scenery/stage_surface_renderer.gd.uid create mode 100644 scripts/stage/scenery/stage_wall_panel.gd create mode 100644 scripts/stage/scenery/stage_wall_panel.gd.uid create mode 100644 scripts/stage/stage_draw_order.gd create mode 100644 scripts/stage/stage_draw_order.gd.uid create mode 100644 tests/phase19_depth_inspector_visual_capture.gd create mode 100644 tests/phase19_depth_inspector_visual_capture.gd.uid create mode 100644 tests/phase19_gui_visual_capture.gd create mode 100644 tests/phase19_gui_visual_capture.gd.uid create mode 100644 tests/test_phase19_scenery.gd create mode 100644 tests/test_phase19_scenery.gd.uid diff --git a/.codex/agents/artist.toml b/.codex/agents/artist.toml index cf19ac8..ba9138f 100644 --- a/.codex/agents/artist.toml +++ b/.codex/agents/artist.toml @@ -12,6 +12,7 @@ You are the art and animation specialist for Stickman Studio, a kid-friendly God - Create native `.prop` and `.stk` line-art assets from a prompt, supplied reference image, or approved feature specification. - Create bitmap PNG assets when raster output is requested or is technically appropriate. Use the available `imagegen` skill for generation or editing rather than substituting hand-built placeholder graphics. - Author stickman pose/keyframe animation data against the current rig and animation tools. Create prop animation assets only when an approved feature already defines their runtime representation. +- For Stage-usable `.prop` artwork, infer a sensible everyday size from the named subject and intended use, using the current standard stickman guide as the ruler. For example, an ordinary refrigerator is roughly 0.85–1 standard stickman height; a toy, giant, or distant version is an intentional exception. Author the geometry at its intended Stage size so a new instance reads correctly at scale 1; do not treat the Prop Editor's fixed 96-unit Normalize Size target as a general Stage size. Ask about scale only when the intent is ambiguous or deliberately unusual. - Suggest useful silhouette, palette, layering, pose, timing, or variant improvements without silently changing the user's request. - Browse for additional visual references when useful. Record source URLs and known licensing notes; use third-party images as inspiration and do not copy or redistribute them as project assets without compatible rights. @@ -40,6 +41,8 @@ Each review bundle must contain: - `review.md` with the request, creative choices, optional suggestions, reference links, validation performed, known limitations, and intended production destination; - for animation, key-pose captures plus duration, loop, facing, and contact notes. +For Stage-usable `.prop` artwork, the representative preview must include a scale check against the current standard stickman guide, composited visibly in front of the art, plus a ground/contact reference where relevant. Record the inferred intended size in `review.md` as an approximate fraction of standard stickman height, along with any assumptions or intentional perspective/oversize choices. Check outline and detail readability at normal Stage zoom after sizing; geometry scaling alone may leave line weights too thin or too heavy. + Do not delete rejected variants automatically. Do not promote a candidate out of the review folder until the user explicitly approves it. On approval, promote `.prop` to `content/props/`, `.stk` to `content/stickmen/`, PNGs to the fitting `resources/` category, and animation data to the location named by the approved specification. Use a new filename unless replacement was explicitly approved. ## Boundaries and validation diff --git a/.codex/skills/asset-creation/SKILL.md b/.codex/skills/asset-creation/SKILL.md index 75426ee..af92a63 100644 --- a/.codex/skills/asset-creation/SKILL.md +++ b/.codex/skills/asset-creation/SKILL.md @@ -11,7 +11,7 @@ Use the `artist` subagent for project graphical-asset and animation work. This s Spawn `artist` with `agent_type: "artist"`. Reuse the same Artist for revisions while it is available. Tell it that other agents may share the workspace and that it must preserve unrelated changes. -Pass the user's request, intended use, supplied image roles (reference versus edit target), relevant approved specification, requested format, and any style or technical constraints. Do not require the user to repeat information already available in the conversation or repository. +Pass the user's request, intended use, supplied image roles (reference versus edit target), relevant approved specification, requested format, and any style or technical constraints. For Stage-usable `.prop` art, include a requested scale when one is specified; otherwise have Artist infer a conventional size from the named subject and intended use against the standard stickman guide, and record that estimate in the review bundle. Do not require the user to repeat ordinary size information or clarify unless the requested scale is ambiguous or intentionally unusual. Before delegating, read [references/project-formats.md](references/project-formats.md) for routing and source-of-truth locations. For production or revision work, also read [references/review-workflow.md](references/review-workflow.md). diff --git a/.gitignore b/.gitignore index bd35f8f..53eb418 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,8 @@ addons/ .opencode/skills/playwright-cli *.import content/review/artist/ +/tests/godot_test_user_data/ +/tests/*_runtime.log +/tests/phase19_*.log +/tests/phase19_visual_*.png +/tests/phase19_selected_artwork_depth_inspector.png diff --git a/content/props/art_bookshelf-v1.prop b/content/props/art_bookshelf-v1.prop new file mode 100644 index 0000000..af21e90 --- /dev/null +++ b/content/props/art_bookshelf-v1.prop @@ -0,0 +1,712 @@ +{ + "version": "1.1", + "prop_name": "Bookshelf", + "material_preset": "wood", + "tags": ["objects", "walls"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": -102, + "y": -188 + }, + { + "x": 102, + "y": -188 + }, + { + "x": 102, + "y": 123 + }, + { + "x": -102, + "y": 123 + } + ], + "fill_color": "#AB795E", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -87, + "y": -170 + }, + { + "x": 87, + "y": -170 + }, + { + "x": 87, + "y": 99 + }, + { + "x": -87, + "y": 99 + } + ], + "fill_color": "#F5D9AA", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -87, + "y": -87 + }, + { + "x": 87, + "y": -87 + }, + { + "x": 87, + "y": -75 + }, + { + "x": -87, + "y": -75 + } + ], + "fill_color": "#AB795E", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -87, + "y": 2 + }, + { + "x": 87, + "y": 2 + }, + { + "x": 87, + "y": 14 + }, + { + "x": -87, + "y": 14 + } + ], + "fill_color": "#AB795E", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -76, + "y": -163 + }, + { + "x": -58, + "y": -163 + }, + { + "x": -58, + "y": -91 + }, + { + "x": -76, + "y": -91 + } + ], + "fill_color": "#C87375", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -54, + "y": -163 + }, + { + "x": -36, + "y": -163 + }, + { + "x": -36, + "y": -91 + }, + { + "x": -54, + "y": -91 + } + ], + "fill_color": "#77A7B2", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -32, + "y": -163 + }, + { + "x": -14, + "y": -163 + }, + { + "x": -14, + "y": -91 + }, + { + "x": -32, + "y": -91 + } + ], + "fill_color": "#E3B765", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -9, + "y": -163 + }, + { + "x": 9, + "y": -163 + }, + { + "x": 9, + "y": -91 + }, + { + "x": -9, + "y": -91 + } + ], + "fill_color": "#82B38D", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 17, + "y": -163 + }, + { + "x": 35, + "y": -163 + }, + { + "x": 35, + "y": -91 + }, + { + "x": 17, + "y": -91 + } + ], + "fill_color": "#C87375", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 43, + "y": -163 + }, + { + "x": 61, + "y": -163 + }, + { + "x": 61, + "y": -91 + }, + { + "x": 43, + "y": -91 + } + ], + "fill_color": "#77A7B2", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -74, + "y": -69 + }, + { + "x": -57, + "y": -69 + }, + { + "x": -57, + "y": -5 + }, + { + "x": -74, + "y": -5 + } + ], + "fill_color": "#78A7B2", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -53, + "y": -69 + }, + { + "x": -36, + "y": -69 + }, + { + "x": -36, + "y": -5 + }, + { + "x": -53, + "y": -5 + } + ], + "fill_color": "#E5B869", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -32, + "y": -69 + }, + { + "x": -15, + "y": -69 + }, + { + "x": -15, + "y": -5 + }, + { + "x": -32, + "y": -5 + } + ], + "fill_color": "#C97375", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 4, + "y": -69 + }, + { + "x": 21, + "y": -69 + }, + { + "x": 21, + "y": -5 + }, + { + "x": 4, + "y": -5 + } + ], + "fill_color": "#9BB586", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 27, + "y": -69 + }, + { + "x": 44, + "y": -69 + }, + { + "x": 44, + "y": -5 + }, + { + "x": 27, + "y": -5 + } + ], + "fill_color": "#83AABB", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 48, + "y": -69 + }, + { + "x": 65, + "y": -69 + }, + { + "x": 65, + "y": -5 + }, + { + "x": 48, + "y": -5 + } + ], + "fill_color": "#DEAA72", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -68, + "y": 31 + }, + { + "x": -12, + "y": 31 + }, + { + "x": -12, + "y": 86 + }, + { + "x": -68, + "y": 86 + } + ], + "fill_color": "#8CB98E", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -9.0, + "y": 35.0 + }, + { + "x": -11.36, + "y": 44.18 + }, + { + "x": -18.08, + "y": 51.97 + }, + { + "x": -28.14, + "y": 57.17 + }, + { + "x": -40.0, + "y": 59.0 + }, + { + "x": -51.86, + "y": 57.17 + }, + { + "x": -61.92, + "y": 51.97 + }, + { + "x": -68.64, + "y": 44.18 + }, + { + "x": -71.0, + "y": 35.0 + }, + { + "x": -68.64, + "y": 25.82 + }, + { + "x": -61.92, + "y": 18.03 + }, + { + "x": -51.86, + "y": 12.83 + }, + { + "x": -40.0, + "y": 11.0 + }, + { + "x": -28.14, + "y": 12.83 + }, + { + "x": -18.08, + "y": 18.03 + }, + { + "x": -11.36, + "y": 25.82 + } + ], + "fill_color": "#79AF81", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 10, + "y": 48 + }, + { + "x": 66, + "y": 48 + }, + { + "x": 66, + "y": 93 + }, + { + "x": 10, + "y": 93 + } + ], + "fill_color": "#C17476", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 10, + "y": 67 + }, + { + "x": 66, + "y": 67 + } + ], + "fill_color": "#00000000", + "outline_color": "#F4CE9E", + "outline_width": 3, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/props/art_city_skyline-v1.prop b/content/props/art_city_skyline-v1.prop new file mode 100644 index 0000000..f7821a9 --- /dev/null +++ b/content/props/art_city_skyline-v1.prop @@ -0,0 +1,366 @@ +{ + "version": "1.1", + "prop_name": "City Skyline", + "material_preset": "none", + "tags": ["buildings"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": -320, + "y": 70 + }, + { + "x": -320, + "y": -25 + }, + { + "x": -278, + "y": -25 + }, + { + "x": -278, + "y": -75 + }, + { + "x": -220, + "y": -75 + }, + { + "x": -220, + "y": -30 + }, + { + "x": -184, + "y": -30 + }, + { + "x": -184, + "y": -112 + }, + { + "x": -136, + "y": -112 + }, + { + "x": -136, + "y": -55 + }, + { + "x": -86, + "y": -55 + }, + { + "x": -86, + "y": -88 + }, + { + "x": -38, + "y": -88 + }, + { + "x": -38, + "y": -35 + }, + { + "x": 15, + "y": -35 + }, + { + "x": 15, + "y": -125 + }, + { + "x": 62, + "y": -125 + }, + { + "x": 62, + "y": -52 + }, + { + "x": 110, + "y": -52 + }, + { + "x": 110, + "y": -92 + }, + { + "x": 160, + "y": -92 + }, + { + "x": 160, + "y": -34 + }, + { + "x": 207, + "y": -34 + }, + { + "x": 207, + "y": -70 + }, + { + "x": 259, + "y": -70 + }, + { + "x": 259, + "y": -20 + }, + { + "x": 320, + "y": -20 + }, + { + "x": 320, + "y": 70 + } + ], + "fill_color": "#879DB0", + "outline_color": "#687F97", + "outline_width": 3, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -298, + "y": -56 + }, + { + "x": -286, + "y": -56 + }, + { + "x": -286, + "y": -37 + }, + { + "x": -298, + "y": -37 + } + ], + "fill_color": "#DDE7E8", + "outline_color": "#DDE7E8", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -258, + "y": -52 + }, + { + "x": -246, + "y": -52 + }, + { + "x": -246, + "y": -34 + }, + { + "x": -258, + "y": -34 + } + ], + "fill_color": "#DDE7E8", + "outline_color": "#DDE7E8", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -161, + "y": -93 + }, + { + "x": -149, + "y": -93 + }, + { + "x": -149, + "y": -75 + }, + { + "x": -161, + "y": -75 + } + ], + "fill_color": "#DDE7E8", + "outline_color": "#DDE7E8", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 30, + "y": -107 + }, + { + "x": 42, + "y": -107 + }, + { + "x": 42, + "y": -87 + }, + { + "x": 30, + "y": -87 + } + ], + "fill_color": "#DDE7E8", + "outline_color": "#DDE7E8", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 127, + "y": -74 + }, + { + "x": 139, + "y": -74 + }, + { + "x": 139, + "y": -56 + }, + { + "x": 127, + "y": -56 + } + ], + "fill_color": "#DDE7E8", + "outline_color": "#DDE7E8", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 226, + "y": -54 + }, + { + "x": 238, + "y": -54 + }, + { + "x": 238, + "y": -37 + }, + { + "x": 226, + "y": -37 + } + ], + "fill_color": "#DDE7E8", + "outline_color": "#DDE7E8", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/props/art_fireplace_mantel-v2.prop b/content/props/art_fireplace_mantel-v2.prop new file mode 100644 index 0000000..4c234bf --- /dev/null +++ b/content/props/art_fireplace_mantel-v2.prop @@ -0,0 +1,26890 @@ +{ + "anchors": { + "origin": { + "allowed_hands": "both", + "role": "pivot", + "rotation_deg": 0.0, + "x": 0.0, + "y": 0.0, + "z_index_offset": 0 + } + }, + "material_preset": "none", + "metadata": { + "created_at": "2026-10-16T15:08:21", + "modified_at": "2026-10-16T15:08:21" + }, + "prop_name": "Cozy Brick Fireplace Mantel", + "shapes": [ + { + "closed": true, + "fill_color": "#AA947B", + "outline_color": "#594132", + "outline_width": 3.89061369015, + "points": [ + { + "x": -334.5927734375, + "y": -236.030548095703 + }, + { + "x": 334.5927734375, + "y": -236.030548095703 + }, + { + "x": 334.5927734375, + "y": 256.780517578125 + }, + { + "x": -334.5927734375, + "y": 256.780517578125 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": -230.843048095703 + }, + { + "x": -261.967987060547, + "y": -230.064926147461 + }, + { + "x": -259.374237060547, + "y": -227.211807250977 + }, + { + "x": -260.152374267578, + "y": -207.499374389648 + }, + { + "x": -263.264862060547, + "y": -204.905639648438 + }, + { + "x": -326.8115234375, + "y": -205.943130493164 + }, + { + "x": -329.4052734375, + "y": -209.055618286133 + }, + { + "x": -328.627136230469, + "y": -228.249313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": -227.989944458008 + }, + { + "x": -264.561706542969, + "y": -227.989944458008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": -207.499374389648 + }, + { + "x": -261.967987060547, + "y": -207.499374389648 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -320.179351806641, + "y": -213.444229125977 + }, + { + "x": -309.804382324219, + "y": -215.259841918945 + }, + { + "x": -299.429412841797, + "y": -212.666107177734 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -250.296142578125, + "y": -230.843048095703 + }, + { + "x": -184.155700683594, + "y": -230.064926147461 + }, + { + "x": -181.561965942383, + "y": -227.211807250977 + }, + { + "x": -182.340103149414, + "y": -207.499374389648 + }, + { + "x": -185.452575683594, + "y": -204.905639648438 + }, + { + "x": -251.593017578125, + "y": -205.943130493164 + }, + { + "x": -254.186752319336, + "y": -209.055618286133 + }, + { + "x": -253.408615112305, + "y": -228.249313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -248.999267578125, + "y": -227.989944458008 + }, + { + "x": -186.749450683594, + "y": -227.989944458008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -248.999267578125, + "y": -207.499374389648 + }, + { + "x": -184.155700683594, + "y": -207.499374389648 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -243.51091003418, + "y": -213.511672973633 + }, + { + "x": -233.135940551758, + "y": -215.327285766602 + }, + { + "x": -222.760971069336, + "y": -212.733535766602 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -172.483871459961, + "y": -230.843048095703 + }, + { + "x": -106.343437194824, + "y": -230.064926147461 + }, + { + "x": -103.749694824219, + "y": -227.211807250977 + }, + { + "x": -104.527816772461, + "y": -207.499374389648 + }, + { + "x": -107.64030456543, + "y": -204.905639648438 + }, + { + "x": -173.780731201172, + "y": -205.943130493164 + }, + { + "x": -176.374481201172, + "y": -209.055618286133 + }, + { + "x": -175.596343994141, + "y": -228.249313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -171.186996459961, + "y": -227.989944458008 + }, + { + "x": -108.93717956543, + "y": -227.989944458008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -171.186996459961, + "y": -207.499374389648 + }, + { + "x": -106.343437194824, + "y": -207.499374389648 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -158.988632202148, + "y": -215.988693237305 + }, + { + "x": -148.613662719727, + "y": -217.804306030273 + }, + { + "x": -138.238693237305, + "y": -215.210571289063 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -94.6716003417969, + "y": -230.843048095703 + }, + { + "x": -28.5311660766602, + "y": -230.064926147461 + }, + { + "x": -25.9374237060547, + "y": -227.211807250977 + }, + { + "x": -26.7155475616455, + "y": -207.499374389648 + }, + { + "x": -29.8280372619629, + "y": -204.905639648438 + }, + { + "x": -95.9684677124023, + "y": -205.943130493164 + }, + { + "x": -98.5622100830078, + "y": -209.055618286133 + }, + { + "x": -97.7840881347656, + "y": -228.249313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -93.3747253417969, + "y": -227.989944458008 + }, + { + "x": -31.1249084472656, + "y": -227.989944458008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -93.3747253417969, + "y": -207.499374389648 + }, + { + "x": -28.5311660766602, + "y": -207.499374389648 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -85.3081817626953, + "y": -222.675354003906 + }, + { + "x": -74.9332122802734, + "y": -224.490982055664 + }, + { + "x": -64.5582427978516, + "y": -221.897247314453 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -16.8593254089355, + "y": -230.843048095703 + }, + { + "x": 49.2811050415039, + "y": -230.064926147461 + }, + { + "x": 51.8748474121094, + "y": -227.211807250977 + }, + { + "x": 51.0967254638672, + "y": -207.499374389648 + }, + { + "x": 47.9842338562012, + "y": -204.905639648438 + }, + { + "x": -18.1561965942383, + "y": -205.943130493164 + }, + { + "x": -20.7499389648438, + "y": -209.055618286133 + }, + { + "x": -19.9718151092529, + "y": -228.249313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -15.5624542236328, + "y": -227.989944458008 + }, + { + "x": 46.6873626708984, + "y": -227.989944458008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -15.5624542236328, + "y": -207.499374389648 + }, + { + "x": 49.2811050415039, + "y": -207.499374389648 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -9.95218944549561, + "y": -215.254653930664 + }, + { + "x": 0.422780007123947, + "y": -217.070297241211 + }, + { + "x": 10.7977495193481, + "y": -214.476547241211 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 60.9529457092285, + "y": -230.843048095703 + }, + { + "x": 127.093376159668, + "y": -230.064926147461 + }, + { + "x": 129.687118530273, + "y": -227.211807250977 + }, + { + "x": 128.908996582031, + "y": -207.499374389648 + }, + { + "x": 125.796508789063, + "y": -204.905639648438 + }, + { + "x": 59.6560745239258, + "y": -205.943130493164 + }, + { + "x": 57.0623321533203, + "y": -209.055618286133 + }, + { + "x": 57.8404541015625, + "y": -228.249313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 62.2498168945313, + "y": -227.989944458008 + }, + { + "x": 124.499633789063, + "y": -227.989944458008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 62.2498168945313, + "y": -207.499374389648 + }, + { + "x": 127.093376159668, + "y": -207.499374389648 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 67.4217376708984, + "y": -214.634765625 + }, + { + "x": 77.7967071533203, + "y": -216.45036315918 + }, + { + "x": 88.1716766357422, + "y": -213.856628417969 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 138.765213012695, + "y": -230.843048095703 + }, + { + "x": 204.905639648438, + "y": -230.064926147461 + }, + { + "x": 207.499389648438, + "y": -227.211807250977 + }, + { + "x": 206.721252441406, + "y": -207.499374389648 + }, + { + "x": 203.608779907227, + "y": -204.905639648438 + }, + { + "x": 137.468353271484, + "y": -205.943130493164 + }, + { + "x": 134.874603271484, + "y": -209.055618286133 + }, + { + "x": 135.652725219727, + "y": -228.249313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 140.062088012695, + "y": -227.989944458008 + }, + { + "x": 202.311904907227, + "y": -227.989944458008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 140.062088012695, + "y": -207.499374389648 + }, + { + "x": 204.905639648438, + "y": -207.499374389648 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 148.097503662109, + "y": -217.236282348633 + }, + { + "x": 158.472473144531, + "y": -219.051910400391 + }, + { + "x": 168.847442626953, + "y": -216.458160400391 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 216.577484130859, + "y": -230.843048095703 + }, + { + "x": 282.717926025391, + "y": -230.064926147461 + }, + { + "x": 285.311645507813, + "y": -227.211807250977 + }, + { + "x": 284.533538818359, + "y": -207.499374389648 + }, + { + "x": 281.421051025391, + "y": -204.905639648438 + }, + { + "x": 215.280609130859, + "y": -205.943130493164 + }, + { + "x": 212.686874389648, + "y": -209.055618286133 + }, + { + "x": 213.46501159668, + "y": -228.249313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 217.874359130859, + "y": -227.989944458008 + }, + { + "x": 280.124176025391, + "y": -227.989944458008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 217.874359130859, + "y": -207.499374389648 + }, + { + "x": 282.717926025391, + "y": -207.499374389648 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 230.964965820313, + "y": -222.97883605957 + }, + { + "x": 241.339935302734, + "y": -224.794448852539 + }, + { + "x": 251.714904785156, + "y": -222.200698852539 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 294.389770507813, + "y": -230.843048095703 + }, + { + "x": 326.8115234375, + "y": -230.064926147461 + }, + { + "x": 329.4052734375, + "y": -227.211807250977 + }, + { + "x": 328.627136230469, + "y": -207.499374389648 + }, + { + "x": 325.514678955078, + "y": -204.905639648438 + }, + { + "x": 293.092895507813, + "y": -205.943130493164 + }, + { + "x": 290.499145507813, + "y": -209.055618286133 + }, + { + "x": 291.277282714844, + "y": -228.249313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 295.686645507813, + "y": -227.989944458008 + }, + { + "x": 324.217803955078, + "y": -227.989944458008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 295.686645507813, + "y": -207.499374389648 + }, + { + "x": 326.8115234375, + "y": -207.499374389648 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 300.350158691406, + "y": -217.581253051758 + }, + { + "x": 310.725128173828, + "y": -219.396865844727 + }, + { + "x": 321.10009765625, + "y": -216.803115844727 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": -199.718139648438 + }, + { + "x": -306.061584472656, + "y": -198.940017700195 + }, + { + "x": -303.467864990234, + "y": -196.086898803711 + }, + { + "x": -304.246002197266, + "y": -176.374465942383 + }, + { + "x": -307.358459472656, + "y": -173.780731201172 + }, + { + "x": -326.8115234375, + "y": -174.818222045898 + }, + { + "x": -329.4052734375, + "y": -177.930709838867 + }, + { + "x": -328.627136230469, + "y": -197.124404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": -196.865036010742 + }, + { + "x": -308.655334472656, + "y": -196.865036010742 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": -176.374465942383 + }, + { + "x": -306.061584472656, + "y": -176.374465942383 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -294.389770507813, + "y": -199.718139648438 + }, + { + "x": -228.249328613281, + "y": -198.940017700195 + }, + { + "x": -225.655578613281, + "y": -196.086898803711 + }, + { + "x": -226.433715820313, + "y": -176.374465942383 + }, + { + "x": -229.546203613281, + "y": -173.780731201172 + }, + { + "x": -295.686645507813, + "y": -174.818222045898 + }, + { + "x": -298.280364990234, + "y": -177.930709838867 + }, + { + "x": -297.502227783203, + "y": -197.124404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -293.092895507813, + "y": -196.865036010742 + }, + { + "x": -230.843078613281, + "y": -196.865036010742 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -293.092895507813, + "y": -176.374465942383 + }, + { + "x": -228.249328613281, + "y": -176.374465942383 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -287.705688476563, + "y": -182.036605834961 + }, + { + "x": -277.330718994141, + "y": -183.852233886719 + }, + { + "x": -266.955749511719, + "y": -181.258483886719 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -216.577484130859, + "y": -199.718139648438 + }, + { + "x": -150.437057495117, + "y": -198.940017700195 + }, + { + "x": -147.843322753906, + "y": -196.086898803711 + }, + { + "x": -148.621429443359, + "y": -176.374465942383 + }, + { + "x": -151.733932495117, + "y": -173.780731201172 + }, + { + "x": -217.874359130859, + "y": -174.818222045898 + }, + { + "x": -220.468109130859, + "y": -177.930709838867 + }, + { + "x": -219.689971923828, + "y": -197.124404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -215.280609130859, + "y": -196.865036010742 + }, + { + "x": -153.030792236328, + "y": -196.865036010742 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -215.280609130859, + "y": -176.374465942383 + }, + { + "x": -150.437057495117, + "y": -176.374465942383 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -211.483383178711, + "y": -190.681549072266 + }, + { + "x": -201.108413696289, + "y": -192.497177124023 + }, + { + "x": -190.733444213867, + "y": -189.903442382813 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -138.765213012695, + "y": -199.718139648438 + }, + { + "x": -72.6247863769531, + "y": -198.940017700195 + }, + { + "x": -70.0310440063477, + "y": -196.086898803711 + }, + { + "x": -70.8091659545898, + "y": -176.374465942383 + }, + { + "x": -73.9216613769531, + "y": -173.780731201172 + }, + { + "x": -140.062088012695, + "y": -174.818222045898 + }, + { + "x": -142.655822753906, + "y": -177.930709838867 + }, + { + "x": -141.877716064453, + "y": -197.124404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -137.468353271484, + "y": -196.865036010742 + }, + { + "x": -75.2185287475586, + "y": -196.865036010742 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -137.468353271484, + "y": -176.374465942383 + }, + { + "x": -72.6247863769531, + "y": -176.374465942383 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -130.413360595703, + "y": -188.505401611328 + }, + { + "x": -120.038391113281, + "y": -190.321014404297 + }, + { + "x": -109.663421630859, + "y": -187.727279663086 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -60.9529457092285, + "y": -199.718139648438 + }, + { + "x": 5.18748474121094, + "y": -198.940017700195 + }, + { + "x": 7.78122711181641, + "y": -196.086898803711 + }, + { + "x": 7.00310468673706, + "y": -176.374465942383 + }, + { + "x": 3.8906135559082, + "y": -173.780731201172 + }, + { + "x": -62.2498168945313, + "y": -174.818222045898 + }, + { + "x": -64.8435592651367, + "y": -177.930709838867 + }, + { + "x": -64.0654373168945, + "y": -197.124404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -59.6560745239258, + "y": -196.865036010742 + }, + { + "x": 2.59374237060547, + "y": -196.865036010742 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -59.6560745239258, + "y": -176.374465942383 + }, + { + "x": 5.18748474121094, + "y": -176.374465942383 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -47.5977668762207, + "y": -184.983093261719 + }, + { + "x": -37.2227973937988, + "y": -186.798706054688 + }, + { + "x": -26.8478260040283, + "y": -184.204971313477 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 16.8593254089355, + "y": -199.718139648438 + }, + { + "x": 82.999755859375, + "y": -198.940017700195 + }, + { + "x": 85.5934982299805, + "y": -196.086898803711 + }, + { + "x": 84.8153762817383, + "y": -176.374465942383 + }, + { + "x": 81.702880859375, + "y": -173.780731201172 + }, + { + "x": 15.5624542236328, + "y": -174.818222045898 + }, + { + "x": 12.9687118530273, + "y": -177.930709838867 + }, + { + "x": 13.7468347549438, + "y": -197.124404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 18.1561965942383, + "y": -196.865036010742 + }, + { + "x": 80.4060134887695, + "y": -196.865036010742 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 18.1561965942383, + "y": -176.374465942383 + }, + { + "x": 82.999755859375, + "y": -176.374465942383 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 21.7718734741211, + "y": -188.134490966797 + }, + { + "x": 32.146842956543, + "y": -189.950119018555 + }, + { + "x": 42.5218086242676, + "y": -187.356369018555 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 94.6716003417969, + "y": -199.718139648438 + }, + { + "x": 160.812026977539, + "y": -198.940017700195 + }, + { + "x": 163.40576171875, + "y": -196.086898803711 + }, + { + "x": 162.627655029297, + "y": -176.374465942383 + }, + { + "x": 159.515151977539, + "y": -173.780731201172 + }, + { + "x": 93.3747253417969, + "y": -174.818222045898 + }, + { + "x": 90.7809829711914, + "y": -177.930709838867 + }, + { + "x": 91.5591049194336, + "y": -197.124404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 95.9684677124023, + "y": -196.865036010742 + }, + { + "x": 158.218292236328, + "y": -196.865036010742 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 95.9684677124023, + "y": -176.374465942383 + }, + { + "x": 160.812026977539, + "y": -176.374465942383 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 106.584648132324, + "y": -180.760482788086 + }, + { + "x": 116.959617614746, + "y": -182.576095581055 + }, + { + "x": 127.334587097168, + "y": -179.982360839844 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 172.483871459961, + "y": -199.718139648438 + }, + { + "x": 238.624298095703, + "y": -198.940017700195 + }, + { + "x": 241.218048095703, + "y": -196.086898803711 + }, + { + "x": 240.439910888672, + "y": -176.374465942383 + }, + { + "x": 237.327423095703, + "y": -173.780731201172 + }, + { + "x": 171.186996459961, + "y": -174.818222045898 + }, + { + "x": 168.59326171875, + "y": -177.930709838867 + }, + { + "x": 169.371383666992, + "y": -197.124404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 173.780731201172, + "y": -196.865036010742 + }, + { + "x": 236.030548095703, + "y": -196.865036010742 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 173.780731201172, + "y": -176.374465942383 + }, + { + "x": 238.624298095703, + "y": -176.374465942383 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 178.867065429688, + "y": -191.034301757813 + }, + { + "x": 189.242034912109, + "y": -192.849914550781 + }, + { + "x": 199.617004394531, + "y": -190.256164550781 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 250.296142578125, + "y": -199.718139648438 + }, + { + "x": 316.436584472656, + "y": -198.940017700195 + }, + { + "x": 319.030303955078, + "y": -196.086898803711 + }, + { + "x": 318.252166748047, + "y": -176.374465942383 + }, + { + "x": 315.139709472656, + "y": -173.780731201172 + }, + { + "x": 248.999267578125, + "y": -174.818222045898 + }, + { + "x": 246.405517578125, + "y": -177.930709838867 + }, + { + "x": 247.183654785156, + "y": -197.124404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 251.593017578125, + "y": -196.865036010742 + }, + { + "x": 313.842834472656, + "y": -196.865036010742 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 251.593017578125, + "y": -176.374465942383 + }, + { + "x": 316.436584472656, + "y": -176.374465942383 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 257.141021728516, + "y": -188.183776855469 + }, + { + "x": 267.515991210938, + "y": -189.999389648438 + }, + { + "x": 277.890960693359, + "y": -187.405654907227 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": -168.593231201172 + }, + { + "x": -261.967987060547, + "y": -167.815124511719 + }, + { + "x": -259.374237060547, + "y": -164.961990356445 + }, + { + "x": -260.152374267578, + "y": -145.249557495117 + }, + { + "x": -263.264862060547, + "y": -142.655822753906 + }, + { + "x": -326.8115234375, + "y": -143.693313598633 + }, + { + "x": -329.4052734375, + "y": -146.805801391602 + }, + { + "x": -328.627136230469, + "y": -165.999496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": -165.740127563477 + }, + { + "x": -264.561706542969, + "y": -165.740127563477 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": -145.249557495117 + }, + { + "x": -261.967987060547, + "y": -145.249557495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -310.530609130859, + "y": -149.575912475586 + }, + { + "x": -300.155639648438, + "y": -151.391540527344 + }, + { + "x": -289.780670166016, + "y": -148.797790527344 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -250.296142578125, + "y": -168.593231201172 + }, + { + "x": -184.155700683594, + "y": -167.815124511719 + }, + { + "x": -181.561965942383, + "y": -164.961990356445 + }, + { + "x": -182.340103149414, + "y": -145.249557495117 + }, + { + "x": -185.452575683594, + "y": -142.655822753906 + }, + { + "x": -251.593017578125, + "y": -143.693313598633 + }, + { + "x": -254.186752319336, + "y": -146.805801391602 + }, + { + "x": -253.408615112305, + "y": -165.999496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -248.999267578125, + "y": -165.740127563477 + }, + { + "x": -186.749450683594, + "y": -165.740127563477 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -248.999267578125, + "y": -145.249557495117 + }, + { + "x": -184.155700683594, + "y": -145.249557495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -242.800231933594, + "y": -152.569091796875 + }, + { + "x": -232.425262451172, + "y": -154.384719848633 + }, + { + "x": -222.05029296875, + "y": -151.790969848633 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -172.483871459961, + "y": -168.593231201172 + }, + { + "x": -106.343437194824, + "y": -167.815124511719 + }, + { + "x": -103.749694824219, + "y": -164.961990356445 + }, + { + "x": -104.527816772461, + "y": -145.249557495117 + }, + { + "x": -107.64030456543, + "y": -142.655822753906 + }, + { + "x": -173.780731201172, + "y": -143.693313598633 + }, + { + "x": -176.374481201172, + "y": -146.805801391602 + }, + { + "x": -175.596343994141, + "y": -165.999496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -171.186996459961, + "y": -165.740127563477 + }, + { + "x": -108.93717956543, + "y": -165.740127563477 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -171.186996459961, + "y": -145.249557495117 + }, + { + "x": -106.343437194824, + "y": -145.249557495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -166.484527587891, + "y": -156.205520629883 + }, + { + "x": -156.109573364258, + "y": -158.021148681641 + }, + { + "x": -145.734603881836, + "y": -155.427398681641 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -94.6716003417969, + "y": -168.593231201172 + }, + { + "x": -28.5311660766602, + "y": -167.815124511719 + }, + { + "x": -25.9374237060547, + "y": -164.961990356445 + }, + { + "x": -26.7155475616455, + "y": -145.249557495117 + }, + { + "x": -29.8280372619629, + "y": -142.655822753906 + }, + { + "x": -95.9684677124023, + "y": -143.693313598633 + }, + { + "x": -98.5622100830078, + "y": -146.805801391602 + }, + { + "x": -97.7840881347656, + "y": -165.999496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -93.3747253417969, + "y": -165.740127563477 + }, + { + "x": -31.1249084472656, + "y": -165.740127563477 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -93.3747253417969, + "y": -145.249557495117 + }, + { + "x": -28.5311660766602, + "y": -145.249557495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -87.3183364868164, + "y": -148.665512084961 + }, + { + "x": -76.9433670043945, + "y": -150.48112487793 + }, + { + "x": -66.5683975219727, + "y": -147.887390136719 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -16.8593254089355, + "y": -168.593231201172 + }, + { + "x": 49.2811050415039, + "y": -167.815124511719 + }, + { + "x": 51.8748474121094, + "y": -164.961990356445 + }, + { + "x": 51.0967254638672, + "y": -145.249557495117 + }, + { + "x": 47.9842338562012, + "y": -142.655822753906 + }, + { + "x": -18.1561965942383, + "y": -143.693313598633 + }, + { + "x": -20.7499389648438, + "y": -146.805801391602 + }, + { + "x": -19.9718151092529, + "y": -165.999496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -15.5624542236328, + "y": -165.740127563477 + }, + { + "x": 46.6873626708984, + "y": -165.740127563477 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -15.5624542236328, + "y": -145.249557495117 + }, + { + "x": 49.2811050415039, + "y": -145.249557495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -12.0427465438843, + "y": -152.576889038086 + }, + { + "x": -1.66777634620667, + "y": -154.392501831055 + }, + { + "x": 8.70719337463379, + "y": -151.798767089844 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 60.9529457092285, + "y": -168.593231201172 + }, + { + "x": 127.093376159668, + "y": -167.815124511719 + }, + { + "x": 129.687118530273, + "y": -164.961990356445 + }, + { + "x": 128.908996582031, + "y": -145.249557495117 + }, + { + "x": 125.796508789063, + "y": -142.655822753906 + }, + { + "x": 59.6560745239258, + "y": -143.693313598633 + }, + { + "x": 57.0623321533203, + "y": -146.805801391602 + }, + { + "x": 57.8404541015625, + "y": -165.999496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 62.2498168945313, + "y": -165.740127563477 + }, + { + "x": 124.499633789063, + "y": -165.740127563477 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 62.2498168945313, + "y": -145.249557495117 + }, + { + "x": 127.093376159668, + "y": -145.249557495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 74.3003463745117, + "y": -158.692916870117 + }, + { + "x": 84.6753158569336, + "y": -160.508544921875 + }, + { + "x": 95.0502853393555, + "y": -157.914794921875 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 138.765213012695, + "y": -168.593231201172 + }, + { + "x": 204.905639648438, + "y": -167.815124511719 + }, + { + "x": 207.499389648438, + "y": -164.961990356445 + }, + { + "x": 206.721252441406, + "y": -145.249557495117 + }, + { + "x": 203.608779907227, + "y": -142.655822753906 + }, + { + "x": 137.468353271484, + "y": -143.693313598633 + }, + { + "x": 134.874603271484, + "y": -146.805801391602 + }, + { + "x": 135.652725219727, + "y": -165.999496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 140.062088012695, + "y": -165.740127563477 + }, + { + "x": 202.311904907227, + "y": -165.740127563477 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 140.062088012695, + "y": -145.249557495117 + }, + { + "x": 204.905639648438, + "y": -145.249557495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 146.157379150391, + "y": -148.810775756836 + }, + { + "x": 156.532348632813, + "y": -150.626388549805 + }, + { + "x": 166.907318115234, + "y": -148.032653808594 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 216.577484130859, + "y": -168.593231201172 + }, + { + "x": 282.717926025391, + "y": -167.815124511719 + }, + { + "x": 285.311645507813, + "y": -164.961990356445 + }, + { + "x": 284.533538818359, + "y": -145.249557495117 + }, + { + "x": 281.421051025391, + "y": -142.655822753906 + }, + { + "x": 215.280609130859, + "y": -143.693313598633 + }, + { + "x": 212.686874389648, + "y": -146.805801391602 + }, + { + "x": 213.46501159668, + "y": -165.999496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 217.874359130859, + "y": -165.740127563477 + }, + { + "x": 280.124176025391, + "y": -165.740127563477 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 217.874359130859, + "y": -145.249557495117 + }, + { + "x": 282.717926025391, + "y": -145.249557495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 227.691680908203, + "y": -151.933624267578 + }, + { + "x": 238.066650390625, + "y": -153.749252319336 + }, + { + "x": 248.441619873047, + "y": -151.155517578125 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 294.389770507813, + "y": -168.593231201172 + }, + { + "x": 326.8115234375, + "y": -167.815124511719 + }, + { + "x": 329.4052734375, + "y": -164.961990356445 + }, + { + "x": 328.627136230469, + "y": -145.249557495117 + }, + { + "x": 325.514678955078, + "y": -142.655822753906 + }, + { + "x": 293.092895507813, + "y": -143.693313598633 + }, + { + "x": 290.499145507813, + "y": -146.805801391602 + }, + { + "x": 291.277282714844, + "y": -165.999496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 295.686645507813, + "y": -165.740127563477 + }, + { + "x": 324.217803955078, + "y": -165.740127563477 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 295.686645507813, + "y": -145.249557495117 + }, + { + "x": 326.8115234375, + "y": -145.249557495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 308.245544433594, + "y": -160.08576965332 + }, + { + "x": 318.620513916016, + "y": -161.901382446289 + }, + { + "x": 324.217803955078, + "y": -159.307632446289 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": -137.468322753906 + }, + { + "x": -306.061584472656, + "y": -136.690216064453 + }, + { + "x": -303.467864990234, + "y": -133.837097167969 + }, + { + "x": -304.246002197266, + "y": -114.124649047852 + }, + { + "x": -307.358459472656, + "y": -111.530906677246 + }, + { + "x": -326.8115234375, + "y": -112.568405151367 + }, + { + "x": -329.4052734375, + "y": -115.680892944336 + }, + { + "x": -328.627136230469, + "y": -134.874588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": -134.615219116211 + }, + { + "x": -308.655334472656, + "y": -134.615219116211 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": -114.124649047852 + }, + { + "x": -306.061584472656, + "y": -114.124649047852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -294.389770507813, + "y": -137.468322753906 + }, + { + "x": -228.249328613281, + "y": -136.690216064453 + }, + { + "x": -225.655578613281, + "y": -133.837097167969 + }, + { + "x": -226.433715820313, + "y": -114.124649047852 + }, + { + "x": -229.546203613281, + "y": -111.530906677246 + }, + { + "x": -295.686645507813, + "y": -112.568405151367 + }, + { + "x": -298.280364990234, + "y": -115.680892944336 + }, + { + "x": -297.502227783203, + "y": -134.874588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -293.092895507813, + "y": -134.615219116211 + }, + { + "x": -230.843078613281, + "y": -134.615219116211 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -293.092895507813, + "y": -114.124649047852 + }, + { + "x": -228.249328613281, + "y": -114.124649047852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -280.059326171875, + "y": -124.48405456543 + }, + { + "x": -269.684356689453, + "y": -126.299674987793 + }, + { + "x": -259.309387207031, + "y": -123.705932617188 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -216.577484130859, + "y": -137.468322753906 + }, + { + "x": -150.437057495117, + "y": -136.690216064453 + }, + { + "x": -147.843322753906, + "y": -133.837097167969 + }, + { + "x": -148.621429443359, + "y": -114.124649047852 + }, + { + "x": -151.733932495117, + "y": -111.530906677246 + }, + { + "x": -217.874359130859, + "y": -112.568405151367 + }, + { + "x": -220.468109130859, + "y": -115.680892944336 + }, + { + "x": -219.689971923828, + "y": -134.874588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -215.280609130859, + "y": -134.615219116211 + }, + { + "x": -153.030792236328, + "y": -134.615219116211 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -215.280609130859, + "y": -114.124649047852 + }, + { + "x": -150.437057495117, + "y": -114.124649047852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -209.950469970703, + "y": -117.457611083984 + }, + { + "x": -199.575500488281, + "y": -119.273231506348 + }, + { + "x": -189.200531005859, + "y": -116.679489135742 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -138.765213012695, + "y": -137.468322753906 + }, + { + "x": -72.6247863769531, + "y": -136.690216064453 + }, + { + "x": -70.0310440063477, + "y": -133.837097167969 + }, + { + "x": -70.8091659545898, + "y": -114.124649047852 + }, + { + "x": -73.9216613769531, + "y": -111.530906677246 + }, + { + "x": -140.062088012695, + "y": -112.568405151367 + }, + { + "x": -142.655822753906, + "y": -115.680892944336 + }, + { + "x": -141.877716064453, + "y": -134.874588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -137.468353271484, + "y": -134.615219116211 + }, + { + "x": -75.2185287475586, + "y": -134.615219116211 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -137.468353271484, + "y": -114.124649047852 + }, + { + "x": -72.6247863769531, + "y": -114.124649047852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -130.794647216797, + "y": -121.187408447266 + }, + { + "x": -120.41967010498, + "y": -123.003028869629 + }, + { + "x": -110.044700622559, + "y": -120.409286499023 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -60.9529457092285, + "y": -137.468322753906 + }, + { + "x": 5.18748474121094, + "y": -136.690216064453 + }, + { + "x": 7.78122711181641, + "y": -133.837097167969 + }, + { + "x": 7.00310468673706, + "y": -114.124649047852 + }, + { + "x": 3.8906135559082, + "y": -111.530906677246 + }, + { + "x": -62.2498168945313, + "y": -112.568405151367 + }, + { + "x": -64.8435592651367, + "y": -115.680892944336 + }, + { + "x": -64.0654373168945, + "y": -134.874588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -59.6560745239258, + "y": -134.615219116211 + }, + { + "x": 2.59374237060547, + "y": -134.615219116211 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -59.6560745239258, + "y": -114.124649047852 + }, + { + "x": 5.18748474121094, + "y": -114.124649047852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -45.5902099609375, + "y": -121.350814819336 + }, + { + "x": -35.2152404785156, + "y": -123.166435241699 + }, + { + "x": -24.8402690887451, + "y": -120.572692871094 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 16.8593254089355, + "y": -137.468322753906 + }, + { + "x": 82.999755859375, + "y": -136.690216064453 + }, + { + "x": 85.5934982299805, + "y": -133.837097167969 + }, + { + "x": 84.8153762817383, + "y": -114.124649047852 + }, + { + "x": 81.702880859375, + "y": -111.530906677246 + }, + { + "x": 15.5624542236328, + "y": -112.568405151367 + }, + { + "x": 12.9687118530273, + "y": -115.680892944336 + }, + { + "x": 13.7468347549438, + "y": -134.874588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 18.1561965942383, + "y": -134.615219116211 + }, + { + "x": 80.4060134887695, + "y": -134.615219116211 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 18.1561965942383, + "y": -114.124649047852 + }, + { + "x": 82.999755859375, + "y": -114.124649047852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 24.1840553283691, + "y": -126.489013671875 + }, + { + "x": 34.559024810791, + "y": -128.304626464844 + }, + { + "x": 44.9339904785156, + "y": -125.710891723633 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 94.6716003417969, + "y": -137.468322753906 + }, + { + "x": 160.812026977539, + "y": -136.690216064453 + }, + { + "x": 163.40576171875, + "y": -133.837097167969 + }, + { + "x": 162.627655029297, + "y": -114.124649047852 + }, + { + "x": 159.515151977539, + "y": -111.530906677246 + }, + { + "x": 93.3747253417969, + "y": -112.568405151367 + }, + { + "x": 90.7809829711914, + "y": -115.680892944336 + }, + { + "x": 91.5591049194336, + "y": -134.874588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 95.9684677124023, + "y": -134.615219116211 + }, + { + "x": 158.218292236328, + "y": -134.615219116211 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 95.9684677124023, + "y": -114.124649047852 + }, + { + "x": 160.812026977539, + "y": -114.124649047852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 105.552345275879, + "y": -119.999473571777 + }, + { + "x": 115.927314758301, + "y": -121.815093994141 + }, + { + "x": 126.302284240723, + "y": -119.221351623535 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 172.483871459961, + "y": -137.468322753906 + }, + { + "x": 238.624298095703, + "y": -136.690216064453 + }, + { + "x": 241.218048095703, + "y": -133.837097167969 + }, + { + "x": 240.439910888672, + "y": -114.124649047852 + }, + { + "x": 237.327423095703, + "y": -111.530906677246 + }, + { + "x": 171.186996459961, + "y": -112.568405151367 + }, + { + "x": 168.59326171875, + "y": -115.680892944336 + }, + { + "x": 169.371383666992, + "y": -134.874588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 173.780731201172, + "y": -134.615219116211 + }, + { + "x": 236.030548095703, + "y": -134.615219116211 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 173.780731201172, + "y": -114.124649047852 + }, + { + "x": 238.624298095703, + "y": -114.124649047852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 188.017791748047, + "y": -124.507392883301 + }, + { + "x": 198.392761230469, + "y": -126.323020935059 + }, + { + "x": 208.767730712891, + "y": -123.729278564453 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 250.296142578125, + "y": -137.468322753906 + }, + { + "x": 316.436584472656, + "y": -136.690216064453 + }, + { + "x": 319.030303955078, + "y": -133.837097167969 + }, + { + "x": 318.252166748047, + "y": -114.124649047852 + }, + { + "x": 315.139709472656, + "y": -111.530906677246 + }, + { + "x": 248.999267578125, + "y": -112.568405151367 + }, + { + "x": 246.405517578125, + "y": -115.680892944336 + }, + { + "x": 247.183654785156, + "y": -134.874588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 251.593017578125, + "y": -134.615219116211 + }, + { + "x": 313.842834472656, + "y": -134.615219116211 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 251.593017578125, + "y": -114.124649047852 + }, + { + "x": 316.436584472656, + "y": -114.124649047852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 267.121734619141, + "y": -127.892227172852 + }, + { + "x": 277.496704101563, + "y": -129.707855224609 + }, + { + "x": 287.871673583984, + "y": -127.114112854004 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": -106.343421936035 + }, + { + "x": -261.967987060547, + "y": -105.565299987793 + }, + { + "x": -259.374237060547, + "y": -102.712181091309 + }, + { + "x": -260.152374267578, + "y": -82.9997406005859 + }, + { + "x": -263.264862060547, + "y": -80.4059982299805 + }, + { + "x": -326.8115234375, + "y": -81.4434967041016 + }, + { + "x": -329.4052734375, + "y": -84.5559844970703 + }, + { + "x": -328.627136230469, + "y": -103.74967956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": -103.490310668945 + }, + { + "x": -264.561706542969, + "y": -103.490310668945 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": -82.9997406005859 + }, + { + "x": -261.967987060547, + "y": -82.9997406005859 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -320.443908691406, + "y": -97.9500732421875 + }, + { + "x": -310.068939208984, + "y": -99.7656936645508 + }, + { + "x": -299.693969726563, + "y": -97.1719512939453 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -250.296142578125, + "y": -106.343421936035 + }, + { + "x": -184.155700683594, + "y": -105.565299987793 + }, + { + "x": -181.561965942383, + "y": -102.712181091309 + }, + { + "x": -182.340103149414, + "y": -82.9997406005859 + }, + { + "x": -185.452575683594, + "y": -80.4059982299805 + }, + { + "x": -251.593017578125, + "y": -81.4434967041016 + }, + { + "x": -254.186752319336, + "y": -84.5559844970703 + }, + { + "x": -253.408615112305, + "y": -103.74967956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -248.999267578125, + "y": -103.490310668945 + }, + { + "x": -186.749450683594, + "y": -103.490310668945 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -248.999267578125, + "y": -82.9997406005859 + }, + { + "x": -184.155700683594, + "y": -82.9997406005859 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -244.423904418945, + "y": -96.4872055053711 + }, + { + "x": -234.048934936523, + "y": -98.3028182983398 + }, + { + "x": -223.673965454102, + "y": -95.7090759277344 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -172.483871459961, + "y": -106.343421936035 + }, + { + "x": -106.343437194824, + "y": -105.565299987793 + }, + { + "x": -103.749694824219, + "y": -102.712181091309 + }, + { + "x": -104.527816772461, + "y": -82.9997406005859 + }, + { + "x": -107.64030456543, + "y": -80.4059982299805 + }, + { + "x": -173.780731201172, + "y": -81.4434967041016 + }, + { + "x": -176.374481201172, + "y": -84.5559844970703 + }, + { + "x": -175.596343994141, + "y": -103.74967956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -171.186996459961, + "y": -103.490310668945 + }, + { + "x": -108.93717956543, + "y": -103.490310668945 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -171.186996459961, + "y": -82.9997406005859 + }, + { + "x": -106.343437194824, + "y": -82.9997406005859 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -163.117858886719, + "y": -97.7373809814453 + }, + { + "x": -152.742889404297, + "y": -99.5530014038086 + }, + { + "x": -142.367919921875, + "y": -96.9592590332031 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -94.6716003417969, + "y": -106.343421936035 + }, + { + "x": -28.5311660766602, + "y": -105.565299987793 + }, + { + "x": -25.9374237060547, + "y": -102.712181091309 + }, + { + "x": -26.7155475616455, + "y": -82.9997406005859 + }, + { + "x": -29.8280372619629, + "y": -80.4059982299805 + }, + { + "x": -95.9684677124023, + "y": -81.4434967041016 + }, + { + "x": -98.5622100830078, + "y": -84.5559844970703 + }, + { + "x": -97.7840881347656, + "y": -103.74967956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -93.3747253417969, + "y": -103.490310668945 + }, + { + "x": -31.1249084472656, + "y": -103.490310668945 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -93.3747253417969, + "y": -82.9997406005859 + }, + { + "x": -28.5311660766602, + "y": -82.9997406005859 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -83.0542221069336, + "y": -92.4928359985352 + }, + { + "x": -72.6792526245117, + "y": -94.3084564208984 + }, + { + "x": -62.3042869567871, + "y": -91.714714050293 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -16.8593254089355, + "y": -106.343421936035 + }, + { + "x": 49.2811050415039, + "y": -105.565299987793 + }, + { + "x": 51.8748474121094, + "y": -102.712181091309 + }, + { + "x": 51.0967254638672, + "y": -82.9997406005859 + }, + { + "x": 47.9842338562012, + "y": -80.4059982299805 + }, + { + "x": -18.1561965942383, + "y": -81.4434967041016 + }, + { + "x": -20.7499389648438, + "y": -84.5559844970703 + }, + { + "x": -19.9718151092529, + "y": -103.74967956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -15.5624542236328, + "y": -103.490310668945 + }, + { + "x": 46.6873626708984, + "y": -103.490310668945 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -15.5624542236328, + "y": -82.9997406005859 + }, + { + "x": 49.2811050415039, + "y": -82.9997406005859 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -0.3760926425457, + "y": -87.3987274169922 + }, + { + "x": 9.99887657165527, + "y": -89.2143478393555 + }, + { + "x": 20.3738460540771, + "y": -86.62060546875 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 60.9529457092285, + "y": -106.343421936035 + }, + { + "x": 127.093376159668, + "y": -105.565299987793 + }, + { + "x": 129.687118530273, + "y": -102.712181091309 + }, + { + "x": 128.908996582031, + "y": -82.9997406005859 + }, + { + "x": 125.796508789063, + "y": -80.4059982299805 + }, + { + "x": 59.6560745239258, + "y": -81.4434967041016 + }, + { + "x": 57.0623321533203, + "y": -84.5559844970703 + }, + { + "x": 57.8404541015625, + "y": -103.74967956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 62.2498168945313, + "y": -103.490310668945 + }, + { + "x": 124.499633789063, + "y": -103.490310668945 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 62.2498168945313, + "y": -82.9997406005859 + }, + { + "x": 127.093376159668, + "y": -82.9997406005859 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 73.6648712158203, + "y": -97.0759811401367 + }, + { + "x": 84.0398483276367, + "y": -98.8915939331055 + }, + { + "x": 94.4148178100586, + "y": -96.2978591918945 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 138.765213012695, + "y": -106.343421936035 + }, + { + "x": 204.905639648438, + "y": -105.565299987793 + }, + { + "x": 207.499389648438, + "y": -102.712181091309 + }, + { + "x": 206.721252441406, + "y": -82.9997406005859 + }, + { + "x": 203.608779907227, + "y": -80.4059982299805 + }, + { + "x": 137.468353271484, + "y": -81.4434967041016 + }, + { + "x": 134.874603271484, + "y": -84.5559844970703 + }, + { + "x": 135.652725219727, + "y": -103.74967956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 140.062088012695, + "y": -103.490310668945 + }, + { + "x": 202.311904907227, + "y": -103.490310668945 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 140.062088012695, + "y": -82.9997406005859 + }, + { + "x": 204.905639648438, + "y": -82.9997406005859 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 152.39274597168, + "y": -88.5944442749023 + }, + { + "x": 162.767715454102, + "y": -90.4100646972656 + }, + { + "x": 173.142669677734, + "y": -87.8163223266602 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 216.577484130859, + "y": -106.343421936035 + }, + { + "x": 282.717926025391, + "y": -105.565299987793 + }, + { + "x": 285.311645507813, + "y": -102.712181091309 + }, + { + "x": 284.533538818359, + "y": -82.9997406005859 + }, + { + "x": 281.421051025391, + "y": -80.4059982299805 + }, + { + "x": 215.280609130859, + "y": -81.4434967041016 + }, + { + "x": 212.686874389648, + "y": -84.5559844970703 + }, + { + "x": 213.46501159668, + "y": -103.74967956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 217.874359130859, + "y": -103.490310668945 + }, + { + "x": 280.124176025391, + "y": -103.490310668945 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 217.874359130859, + "y": -82.9997406005859 + }, + { + "x": 282.717926025391, + "y": -82.9997406005859 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 221.915405273438, + "y": -92.9234008789063 + }, + { + "x": 232.290374755859, + "y": -94.739013671875 + }, + { + "x": 242.665344238281, + "y": -92.1452713012695 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 294.389770507813, + "y": -106.343421936035 + }, + { + "x": 326.8115234375, + "y": -105.565299987793 + }, + { + "x": 329.4052734375, + "y": -102.712181091309 + }, + { + "x": 328.627136230469, + "y": -82.9997406005859 + }, + { + "x": 325.514678955078, + "y": -80.4059982299805 + }, + { + "x": 293.092895507813, + "y": -81.4434967041016 + }, + { + "x": 290.499145507813, + "y": -84.5559844970703 + }, + { + "x": 291.277282714844, + "y": -103.74967956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 295.686645507813, + "y": -103.490310668945 + }, + { + "x": 324.217803955078, + "y": -103.490310668945 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 295.686645507813, + "y": -82.9997406005859 + }, + { + "x": 326.8115234375, + "y": -82.9997406005859 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 298.321868896484, + "y": -89.1987838745117 + }, + { + "x": 308.696838378906, + "y": -91.014404296875 + }, + { + "x": 319.071807861328, + "y": -88.4206619262695 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": -75.2185134887695 + }, + { + "x": -306.061584472656, + "y": -74.4403915405273 + }, + { + "x": -303.467864990234, + "y": -71.5872802734375 + }, + { + "x": -304.246002197266, + "y": -51.8748321533203 + }, + { + "x": -307.358459472656, + "y": -49.2810897827148 + }, + { + "x": -326.8115234375, + "y": -50.3185844421387 + }, + { + "x": -329.4052734375, + "y": -53.431079864502 + }, + { + "x": -328.627136230469, + "y": -72.6247711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": -72.3653945922852 + }, + { + "x": -308.655334472656, + "y": -72.3653945922852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": -51.8748321533203 + }, + { + "x": -306.061584472656, + "y": -51.8748321533203 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -294.389770507813, + "y": -75.2185134887695 + }, + { + "x": -228.249328613281, + "y": -74.4403915405273 + }, + { + "x": -225.655578613281, + "y": -71.5872802734375 + }, + { + "x": -226.433715820313, + "y": -51.8748321533203 + }, + { + "x": -229.546203613281, + "y": -49.2810897827148 + }, + { + "x": -295.686645507813, + "y": -50.3185844421387 + }, + { + "x": -298.280364990234, + "y": -53.431079864502 + }, + { + "x": -297.502227783203, + "y": -72.6247711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -293.092895507813, + "y": -72.3653945922852 + }, + { + "x": -230.843078613281, + "y": -72.3653945922852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -293.092895507813, + "y": -51.8748321533203 + }, + { + "x": -228.249328613281, + "y": -51.8748321533203 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -277.896148681641, + "y": -57.5551300048828 + }, + { + "x": -267.521179199219, + "y": -59.3707466125488 + }, + { + "x": -257.146209716797, + "y": -56.7770042419434 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -216.577484130859, + "y": -75.2185134887695 + }, + { + "x": -150.437057495117, + "y": -74.4403915405273 + }, + { + "x": -147.843322753906, + "y": -71.5872802734375 + }, + { + "x": -148.621429443359, + "y": -51.8748321533203 + }, + { + "x": -151.733932495117, + "y": -49.2810897827148 + }, + { + "x": -217.874359130859, + "y": -50.3185844421387 + }, + { + "x": -220.468109130859, + "y": -53.431079864502 + }, + { + "x": -219.689971923828, + "y": -72.6247711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -215.280609130859, + "y": -72.3653945922852 + }, + { + "x": -153.030792236328, + "y": -72.3653945922852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -215.280609130859, + "y": -51.8748321533203 + }, + { + "x": -150.437057495117, + "y": -51.8748321533203 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -200.851623535156, + "y": -56.146728515625 + }, + { + "x": -190.476654052734, + "y": -57.962345123291 + }, + { + "x": -180.101684570313, + "y": -55.3686027526855 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -138.765213012695, + "y": -75.2185134887695 + }, + { + "x": -72.6247863769531, + "y": -74.4403915405273 + }, + { + "x": -70.0310440063477, + "y": -71.5872802734375 + }, + { + "x": -70.8091659545898, + "y": -51.8748321533203 + }, + { + "x": -73.9216613769531, + "y": -49.2810897827148 + }, + { + "x": -140.062088012695, + "y": -50.3185844421387 + }, + { + "x": -142.655822753906, + "y": -53.431079864502 + }, + { + "x": -141.877716064453, + "y": -72.6247711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -137.468353271484, + "y": -72.3653945922852 + }, + { + "x": -75.2185287475586, + "y": -72.3653945922852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -137.468353271484, + "y": -51.8748321533203 + }, + { + "x": -72.6247863769531, + "y": -51.8748321533203 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -123.960136413574, + "y": -60.8543663024902 + }, + { + "x": -113.585166931152, + "y": -62.6699905395508 + }, + { + "x": -103.21019744873, + "y": -60.0762481689453 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -60.9529457092285, + "y": -75.2185134887695 + }, + { + "x": 5.18748474121094, + "y": -74.4403915405273 + }, + { + "x": 7.78122711181641, + "y": -71.5872802734375 + }, + { + "x": 7.00310468673706, + "y": -51.8748321533203 + }, + { + "x": 3.8906135559082, + "y": -49.2810897827148 + }, + { + "x": -62.2498168945313, + "y": -50.3185844421387 + }, + { + "x": -64.8435592651367, + "y": -53.431079864502 + }, + { + "x": -64.0654373168945, + "y": -72.6247711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -59.6560745239258, + "y": -72.3653945922852 + }, + { + "x": 2.59374237060547, + "y": -72.3653945922852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -59.6560745239258, + "y": -51.8748321533203 + }, + { + "x": 5.18748474121094, + "y": -51.8748321533203 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -55.0807113647461, + "y": -57.5473480224609 + }, + { + "x": -44.7057418823242, + "y": -59.362964630127 + }, + { + "x": -34.3307723999023, + "y": -56.7692260742188 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 16.8593254089355, + "y": -75.2185134887695 + }, + { + "x": 82.999755859375, + "y": -74.4403915405273 + }, + { + "x": 85.5934982299805, + "y": -71.5872802734375 + }, + { + "x": 84.8153762817383, + "y": -51.8748321533203 + }, + { + "x": 81.702880859375, + "y": -49.2810897827148 + }, + { + "x": 15.5624542236328, + "y": -50.3185844421387 + }, + { + "x": 12.9687118530273, + "y": -53.431079864502 + }, + { + "x": 13.7468347549438, + "y": -72.6247711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 18.1561965942383, + "y": -72.3653945922852 + }, + { + "x": 80.4060134887695, + "y": -72.3653945922852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 18.1561965942383, + "y": -51.8748321533203 + }, + { + "x": 82.999755859375, + "y": -51.8748321533203 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 33.1143112182617, + "y": -55.4749450683594 + }, + { + "x": 43.4892807006836, + "y": -57.2905654907227 + }, + { + "x": 53.8642501831055, + "y": -54.6968231201172 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 94.6716003417969, + "y": -75.2185134887695 + }, + { + "x": 160.812026977539, + "y": -74.4403915405273 + }, + { + "x": 163.40576171875, + "y": -71.5872802734375 + }, + { + "x": 162.627655029297, + "y": -51.8748321533203 + }, + { + "x": 159.515151977539, + "y": -49.2810897827148 + }, + { + "x": 93.3747253417969, + "y": -50.3185844421387 + }, + { + "x": 90.7809829711914, + "y": -53.431079864502 + }, + { + "x": 91.5591049194336, + "y": -72.6247711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 95.9684677124023, + "y": -72.3653945922852 + }, + { + "x": 158.218292236328, + "y": -72.3653945922852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 95.9684677124023, + "y": -51.8748321533203 + }, + { + "x": 160.812026977539, + "y": -51.8748321533203 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 106.330467224121, + "y": -61.100772857666 + }, + { + "x": 116.705436706543, + "y": -62.9163932800293 + }, + { + "x": 127.080406188965, + "y": -60.3226509094238 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 172.483871459961, + "y": -75.2185134887695 + }, + { + "x": 238.624298095703, + "y": -74.4403915405273 + }, + { + "x": 241.218048095703, + "y": -71.5872802734375 + }, + { + "x": 240.439910888672, + "y": -51.8748321533203 + }, + { + "x": 237.327423095703, + "y": -49.2810897827148 + }, + { + "x": 171.186996459961, + "y": -50.3185844421387 + }, + { + "x": 168.59326171875, + "y": -53.431079864502 + }, + { + "x": 169.371383666992, + "y": -72.6247711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 173.780731201172, + "y": -72.3653945922852 + }, + { + "x": 236.030548095703, + "y": -72.3653945922852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 173.780731201172, + "y": -51.8748321533203 + }, + { + "x": 238.624298095703, + "y": -51.8748321533203 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 188.422409057617, + "y": -56.0403823852539 + }, + { + "x": 198.797378540039, + "y": -57.8560028076172 + }, + { + "x": 209.172348022461, + "y": -55.2622604370117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 250.296142578125, + "y": -75.2185134887695 + }, + { + "x": 316.436584472656, + "y": -74.4403915405273 + }, + { + "x": 319.030303955078, + "y": -71.5872802734375 + }, + { + "x": 318.252166748047, + "y": -51.8748321533203 + }, + { + "x": 315.139709472656, + "y": -49.2810897827148 + }, + { + "x": 248.999267578125, + "y": -50.3185844421387 + }, + { + "x": 246.405517578125, + "y": -53.431079864502 + }, + { + "x": 247.183654785156, + "y": -72.6247711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 251.593017578125, + "y": -72.3653945922852 + }, + { + "x": 313.842834472656, + "y": -72.3653945922852 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 251.593017578125, + "y": -51.8748321533203 + }, + { + "x": 316.436584472656, + "y": -51.8748321533203 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 264.64990234375, + "y": -64.3325805664063 + }, + { + "x": 275.024871826172, + "y": -66.148193359375 + }, + { + "x": 285.399841308594, + "y": -63.5544548034668 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": -44.0936050415039 + }, + { + "x": -261.967987060547, + "y": -43.3154830932617 + }, + { + "x": -259.374237060547, + "y": -40.4623641967773 + }, + { + "x": -260.152374267578, + "y": -20.7499237060547 + }, + { + "x": -263.264862060547, + "y": -18.1561813354492 + }, + { + "x": -326.8115234375, + "y": -19.1936779022217 + }, + { + "x": -329.4052734375, + "y": -22.3061695098877 + }, + { + "x": -328.627136230469, + "y": -41.4998626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": -41.2404861450195 + }, + { + "x": -264.561706542969, + "y": -41.2404861450195 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": -20.7499237060547 + }, + { + "x": -261.967987060547, + "y": -20.7499237060547 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -310.237518310547, + "y": -24.0310077667236 + }, + { + "x": -299.862548828125, + "y": -25.8466262817383 + }, + { + "x": -289.487579345703, + "y": -23.2528839111328 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -250.296142578125, + "y": -44.0936050415039 + }, + { + "x": -184.155700683594, + "y": -43.3154830932617 + }, + { + "x": -181.561965942383, + "y": -40.4623641967773 + }, + { + "x": -182.340103149414, + "y": -20.7499237060547 + }, + { + "x": -185.452575683594, + "y": -18.1561813354492 + }, + { + "x": -251.593017578125, + "y": -19.1936779022217 + }, + { + "x": -254.186752319336, + "y": -22.3061695098877 + }, + { + "x": -253.408615112305, + "y": -41.4998626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -248.999267578125, + "y": -41.2404861450195 + }, + { + "x": -186.749450683594, + "y": -41.2404861450195 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -248.999267578125, + "y": -20.7499237060547 + }, + { + "x": -184.155700683594, + "y": -20.7499237060547 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -236.912414550781, + "y": -25.729907989502 + }, + { + "x": -226.537445068359, + "y": -27.5455284118652 + }, + { + "x": -216.162475585938, + "y": -24.9517860412598 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -172.483871459961, + "y": -44.0936050415039 + }, + { + "x": -106.343437194824, + "y": -43.3154830932617 + }, + { + "x": -103.749694824219, + "y": -40.4623641967773 + }, + { + "x": -104.527816772461, + "y": -20.7499237060547 + }, + { + "x": -107.64030456543, + "y": -18.1561813354492 + }, + { + "x": -173.780731201172, + "y": -19.1936779022217 + }, + { + "x": -176.374481201172, + "y": -22.3061695098877 + }, + { + "x": -175.596343994141, + "y": -41.4998626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -171.186996459961, + "y": -41.2404861450195 + }, + { + "x": -108.93717956543, + "y": -41.2404861450195 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -171.186996459961, + "y": -20.7499237060547 + }, + { + "x": -106.343437194824, + "y": -20.7499237060547 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -161.683517456055, + "y": -34.6809158325195 + }, + { + "x": -151.308547973633, + "y": -36.4965362548828 + }, + { + "x": -140.933578491211, + "y": -33.9027938842773 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -94.6716003417969, + "y": -44.0936050415039 + }, + { + "x": -28.5311660766602, + "y": -43.3154830932617 + }, + { + "x": -25.9374237060547, + "y": -40.4623641967773 + }, + { + "x": -26.7155475616455, + "y": -20.7499237060547 + }, + { + "x": -29.8280372619629, + "y": -18.1561813354492 + }, + { + "x": -95.9684677124023, + "y": -19.1936779022217 + }, + { + "x": -98.5622100830078, + "y": -22.3061695098877 + }, + { + "x": -97.7840881347656, + "y": -41.4998626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -93.3747253417969, + "y": -41.2404861450195 + }, + { + "x": -31.1249084472656, + "y": -41.2404861450195 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -93.3747253417969, + "y": -20.7499237060547 + }, + { + "x": -28.5311660766602, + "y": -20.7499237060547 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -84.6182556152344, + "y": -29.1821804046631 + }, + { + "x": -74.2432861328125, + "y": -30.9978008270264 + }, + { + "x": -63.8683128356934, + "y": -28.4040584564209 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -16.8593254089355, + "y": -44.0936050415039 + }, + { + "x": 49.2811050415039, + "y": -43.3154830932617 + }, + { + "x": 51.8748474121094, + "y": -40.4623641967773 + }, + { + "x": 51.0967254638672, + "y": -20.7499237060547 + }, + { + "x": 47.9842338562012, + "y": -18.1561813354492 + }, + { + "x": -18.1561965942383, + "y": -19.1936779022217 + }, + { + "x": -20.7499389648438, + "y": -22.3061695098877 + }, + { + "x": -19.9718151092529, + "y": -41.4998626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -15.5624542236328, + "y": -41.2404861450195 + }, + { + "x": 46.6873626708984, + "y": -41.2404861450195 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -15.5624542236328, + "y": -20.7499237060547 + }, + { + "x": 49.2811050415039, + "y": -20.7499237060547 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -6.12641954421997, + "y": -28.445556640625 + }, + { + "x": 4.2485499382019, + "y": -30.2611770629883 + }, + { + "x": 14.6235198974609, + "y": -27.6674346923828 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 60.9529457092285, + "y": -44.0936050415039 + }, + { + "x": 127.093376159668, + "y": -43.3154830932617 + }, + { + "x": 129.687118530273, + "y": -40.4623641967773 + }, + { + "x": 128.908996582031, + "y": -20.7499237060547 + }, + { + "x": 125.796508789063, + "y": -18.1561813354492 + }, + { + "x": 59.6560745239258, + "y": -19.1936779022217 + }, + { + "x": 57.0623321533203, + "y": -22.3061695098877 + }, + { + "x": 57.8404541015625, + "y": -41.4998626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 62.2498168945313, + "y": -41.2404861450195 + }, + { + "x": 124.499633789063, + "y": -41.2404861450195 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 62.2498168945313, + "y": -20.7499237060547 + }, + { + "x": 127.093376159668, + "y": -20.7499237060547 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 76.8914947509766, + "y": -26.5184059143066 + }, + { + "x": 87.2664642333984, + "y": -28.3340263366699 + }, + { + "x": 97.6414337158203, + "y": -25.7402839660645 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 138.765213012695, + "y": -44.0936050415039 + }, + { + "x": 204.905639648438, + "y": -43.3154830932617 + }, + { + "x": 207.499389648438, + "y": -40.4623641967773 + }, + { + "x": 206.721252441406, + "y": -20.7499237060547 + }, + { + "x": 203.608779907227, + "y": -18.1561813354492 + }, + { + "x": 137.468353271484, + "y": -19.1936779022217 + }, + { + "x": 134.874603271484, + "y": -22.3061695098877 + }, + { + "x": 135.652725219727, + "y": -41.4998626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 140.062088012695, + "y": -41.2404861450195 + }, + { + "x": 202.311904907227, + "y": -41.2404861450195 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 140.062088012695, + "y": -20.7499237060547 + }, + { + "x": 204.905639648438, + "y": -20.7499237060547 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 145.26513671875, + "y": -32.2687339782715 + }, + { + "x": 155.640106201172, + "y": -34.0843505859375 + }, + { + "x": 166.015060424805, + "y": -31.4906101226807 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 216.577484130859, + "y": -44.0936050415039 + }, + { + "x": 282.717926025391, + "y": -43.3154830932617 + }, + { + "x": 285.311645507813, + "y": -40.4623641967773 + }, + { + "x": 284.533538818359, + "y": -20.7499237060547 + }, + { + "x": 281.421051025391, + "y": -18.1561813354492 + }, + { + "x": 215.280609130859, + "y": -19.1936779022217 + }, + { + "x": 212.686874389648, + "y": -22.3061695098877 + }, + { + "x": 213.46501159668, + "y": -41.4998626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 217.874359130859, + "y": -41.2404861450195 + }, + { + "x": 280.124176025391, + "y": -41.2404861450195 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 217.874359130859, + "y": -20.7499237060547 + }, + { + "x": 282.717926025391, + "y": -20.7499237060547 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 228.895172119141, + "y": -30.629487991333 + }, + { + "x": 239.270141601563, + "y": -32.4451065063477 + }, + { + "x": 249.645111083984, + "y": -29.8513641357422 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 294.389770507813, + "y": -44.0936050415039 + }, + { + "x": 326.8115234375, + "y": -43.3154830932617 + }, + { + "x": 329.4052734375, + "y": -40.4623641967773 + }, + { + "x": 328.627136230469, + "y": -20.7499237060547 + }, + { + "x": 325.514678955078, + "y": -18.1561813354492 + }, + { + "x": 293.092895507813, + "y": -19.1936779022217 + }, + { + "x": 290.499145507813, + "y": -22.3061695098877 + }, + { + "x": 291.277282714844, + "y": -41.4998626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 295.686645507813, + "y": -41.2404861450195 + }, + { + "x": 324.217803955078, + "y": -41.2404861450195 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 295.686645507813, + "y": -20.7499237060547 + }, + { + "x": 326.8115234375, + "y": -20.7499237060547 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 304.134460449219, + "y": -33.0961380004883 + }, + { + "x": 314.509429931641, + "y": -34.9117584228516 + }, + { + "x": 324.217803955078, + "y": -32.3180160522461 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": -12.9686965942383 + }, + { + "x": -306.061584472656, + "y": -12.1905736923218 + }, + { + "x": -303.467864990234, + "y": -9.33745765686035 + }, + { + "x": -304.246002197266, + "y": 10.3749847412109 + }, + { + "x": -307.358459472656, + "y": 12.9687271118164 + }, + { + "x": -326.8115234375, + "y": 11.9312305450439 + }, + { + "x": -329.4052734375, + "y": 8.81873893737793 + }, + { + "x": -328.627136230469, + "y": -10.3749542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": -10.1155796051025 + }, + { + "x": -308.655334472656, + "y": -10.1155796051025 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": 10.3749847412109 + }, + { + "x": -306.061584472656, + "y": 10.3749847412109 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -294.389770507813, + "y": -12.9686965942383 + }, + { + "x": -228.249328613281, + "y": -12.1905736923218 + }, + { + "x": -225.655578613281, + "y": -9.33745765686035 + }, + { + "x": -226.433715820313, + "y": 10.3749847412109 + }, + { + "x": -229.546203613281, + "y": 12.9687271118164 + }, + { + "x": -295.686645507813, + "y": 11.9312305450439 + }, + { + "x": -298.280364990234, + "y": 8.81873893737793 + }, + { + "x": -297.502227783203, + "y": -10.3749542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -293.092895507813, + "y": -10.1155796051025 + }, + { + "x": -230.843078613281, + "y": -10.1155796051025 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -293.092895507813, + "y": 10.3749847412109 + }, + { + "x": -228.249328613281, + "y": 10.3749847412109 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -290.226806640625, + "y": 1.99719619750977 + }, + { + "x": -279.851837158203, + "y": 0.181577682495117 + }, + { + "x": -269.476867675781, + "y": 2.77532005310059 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -216.577484130859, + "y": -12.9686965942383 + }, + { + "x": -150.437057495117, + "y": -12.1905736923218 + }, + { + "x": -147.843322753906, + "y": -9.33745765686035 + }, + { + "x": -148.621429443359, + "y": 10.3749847412109 + }, + { + "x": -151.733932495117, + "y": 12.9687271118164 + }, + { + "x": -217.874359130859, + "y": 11.9312305450439 + }, + { + "x": -220.468109130859, + "y": 8.81873893737793 + }, + { + "x": -219.689971923828, + "y": -10.3749542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -215.280609130859, + "y": -10.1155796051025 + }, + { + "x": -153.030792236328, + "y": -10.1155796051025 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -215.280609130859, + "y": 10.3749847412109 + }, + { + "x": -150.437057495117, + "y": 10.3749847412109 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -209.831176757813, + "y": -4.26669120788574 + }, + { + "x": -199.456207275391, + "y": -6.08231067657471 + }, + { + "x": -189.081237792969, + "y": -3.48856735229492 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -138.765213012695, + "y": -12.9686965942383 + }, + { + "x": -72.6247863769531, + "y": -12.1905736923218 + }, + { + "x": -70.0310440063477, + "y": -9.33745765686035 + }, + { + "x": -70.8091659545898, + "y": 10.3749847412109 + }, + { + "x": -73.9216613769531, + "y": 12.9687271118164 + }, + { + "x": -140.062088012695, + "y": 11.9312305450439 + }, + { + "x": -142.655822753906, + "y": 8.81873893737793 + }, + { + "x": -141.877716064453, + "y": -10.3749542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -137.468353271484, + "y": -10.1155796051025 + }, + { + "x": -75.2185287475586, + "y": -10.1155796051025 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -137.468353271484, + "y": 10.3749847412109 + }, + { + "x": -72.6247863769531, + "y": 10.3749847412109 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -134.112045288086, + "y": -4.31337833404541 + }, + { + "x": -123.737075805664, + "y": -6.12899780273438 + }, + { + "x": -113.362106323242, + "y": -3.53525543212891 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -60.9529457092285, + "y": -12.9686965942383 + }, + { + "x": 5.18748474121094, + "y": -12.1905736923218 + }, + { + "x": 7.78122711181641, + "y": -9.33745765686035 + }, + { + "x": 7.00310468673706, + "y": 10.3749847412109 + }, + { + "x": 3.8906135559082, + "y": 12.9687271118164 + }, + { + "x": -62.2498168945313, + "y": 11.9312305450439 + }, + { + "x": -64.8435592651367, + "y": 8.81873893737793 + }, + { + "x": -64.0654373168945, + "y": -10.3749542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -59.6560745239258, + "y": -10.1155796051025 + }, + { + "x": 2.59374237060547, + "y": -10.1155796051025 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -59.6560745239258, + "y": 10.3749847412109 + }, + { + "x": 5.18748474121094, + "y": 10.3749847412109 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -45.9144248962402, + "y": -4.26928424835205 + }, + { + "x": -35.5394554138184, + "y": -6.08490467071533 + }, + { + "x": -25.1644878387451, + "y": -3.49116230010986 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 16.8593254089355, + "y": -12.9686965942383 + }, + { + "x": 82.999755859375, + "y": -12.1905736923218 + }, + { + "x": 85.5934982299805, + "y": -9.33745765686035 + }, + { + "x": 84.8153762817383, + "y": 10.3749847412109 + }, + { + "x": 81.702880859375, + "y": 12.9687271118164 + }, + { + "x": 15.5624542236328, + "y": 11.9312305450439 + }, + { + "x": 12.9687118530273, + "y": 8.81873893737793 + }, + { + "x": 13.7468347549438, + "y": -10.3749542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 18.1561965942383, + "y": -10.1155796051025 + }, + { + "x": 80.4060134887695, + "y": -10.1155796051025 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 18.1561965942383, + "y": 10.3749847412109 + }, + { + "x": 82.999755859375, + "y": 10.3749847412109 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 24.3604297637939, + "y": 1.1075439453125 + }, + { + "x": 34.7353973388672, + "y": -0.708075523376465 + }, + { + "x": 45.1103668212891, + "y": 1.88566589355469 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 94.6716003417969, + "y": -12.9686965942383 + }, + { + "x": 160.812026977539, + "y": -12.1905736923218 + }, + { + "x": 163.40576171875, + "y": -9.33745765686035 + }, + { + "x": 162.627655029297, + "y": 10.3749847412109 + }, + { + "x": 159.515151977539, + "y": 12.9687271118164 + }, + { + "x": 93.3747253417969, + "y": 11.9312305450439 + }, + { + "x": 90.7809829711914, + "y": 8.81873893737793 + }, + { + "x": 91.5591049194336, + "y": -10.3749542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 95.9684677124023, + "y": -10.1155796051025 + }, + { + "x": 158.218292236328, + "y": -10.1155796051025 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 95.9684677124023, + "y": 10.3749847412109 + }, + { + "x": 160.812026977539, + "y": 10.3749847412109 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 100.279266357422, + "y": 6.94605827331543 + }, + { + "x": 110.654235839844, + "y": 5.13043785095215 + }, + { + "x": 121.029205322266, + "y": 7.72417831420898 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 172.483871459961, + "y": -12.9686965942383 + }, + { + "x": 238.624298095703, + "y": -12.1905736923218 + }, + { + "x": 241.218048095703, + "y": -9.33745765686035 + }, + { + "x": 240.439910888672, + "y": 10.3749847412109 + }, + { + "x": 237.327423095703, + "y": 12.9687271118164 + }, + { + "x": 171.186996459961, + "y": 11.9312305450439 + }, + { + "x": 168.59326171875, + "y": 8.81873893737793 + }, + { + "x": 169.371383666992, + "y": -10.3749542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 173.780731201172, + "y": -10.1155796051025 + }, + { + "x": 236.030548095703, + "y": -10.1155796051025 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 173.780731201172, + "y": 10.3749847412109 + }, + { + "x": 238.624298095703, + "y": 10.3749847412109 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 183.847045898438, + "y": -2.03607177734375 + }, + { + "x": 194.222015380859, + "y": -3.85169219970703 + }, + { + "x": 204.596984863281, + "y": -1.25794982910156 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 250.296142578125, + "y": -12.9686965942383 + }, + { + "x": 316.436584472656, + "y": -12.1905736923218 + }, + { + "x": 319.030303955078, + "y": -9.33745765686035 + }, + { + "x": 318.252166748047, + "y": 10.3749847412109 + }, + { + "x": 315.139709472656, + "y": 12.9687271118164 + }, + { + "x": 248.999267578125, + "y": 11.9312305450439 + }, + { + "x": 246.405517578125, + "y": 8.81873893737793 + }, + { + "x": 247.183654785156, + "y": -10.3749542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 251.593017578125, + "y": -10.1155796051025 + }, + { + "x": 313.842834472656, + "y": -10.1155796051025 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 251.593017578125, + "y": 10.3749847412109 + }, + { + "x": 316.436584472656, + "y": 10.3749847412109 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 264.657684326172, + "y": -2.71822643280029 + }, + { + "x": 275.032653808594, + "y": -4.53384685516357 + }, + { + "x": 285.407623291016, + "y": -1.94010448455811 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": 18.1562118530273 + }, + { + "x": -261.967987060547, + "y": 18.9343338012695 + }, + { + "x": -259.374237060547, + "y": 21.7874488830566 + }, + { + "x": -260.152374267578, + "y": 41.4998931884766 + }, + { + "x": -263.264862060547, + "y": 44.093635559082 + }, + { + "x": -326.8115234375, + "y": 43.0561408996582 + }, + { + "x": -329.4052734375, + "y": 39.9436454772949 + }, + { + "x": -328.627136230469, + "y": 20.7499542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": 21.0093307495117 + }, + { + "x": -264.561706542969, + "y": 21.0093307495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": 41.4998931884766 + }, + { + "x": -261.967987060547, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -313.077667236328, + "y": 30.5309562683105 + }, + { + "x": -302.702697753906, + "y": 28.7153358459473 + }, + { + "x": -292.327728271484, + "y": 31.3090782165527 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -250.296142578125, + "y": 18.1562118530273 + }, + { + "x": -184.155700683594, + "y": 18.9343338012695 + }, + { + "x": -181.561965942383, + "y": 21.7874488830566 + }, + { + "x": -182.340103149414, + "y": 41.4998931884766 + }, + { + "x": -185.452575683594, + "y": 44.093635559082 + }, + { + "x": -251.593017578125, + "y": 43.0561408996582 + }, + { + "x": -254.186752319336, + "y": 39.9436454772949 + }, + { + "x": -253.408615112305, + "y": 20.7499542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -248.999267578125, + "y": 21.0093307495117 + }, + { + "x": -186.749450683594, + "y": 21.0093307495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -248.999267578125, + "y": 41.4998931884766 + }, + { + "x": -184.155700683594, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -240.349136352539, + "y": 35.3916320800781 + }, + { + "x": -229.974166870117, + "y": 33.5760078430176 + }, + { + "x": -219.599197387695, + "y": 36.169750213623 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -172.483871459961, + "y": 18.1562118530273 + }, + { + "x": -106.343437194824, + "y": 18.9343338012695 + }, + { + "x": -103.749694824219, + "y": 21.7874488830566 + }, + { + "x": -104.527816772461, + "y": 41.4998931884766 + }, + { + "x": -107.64030456543, + "y": 44.093635559082 + }, + { + "x": -173.780731201172, + "y": 43.0561408996582 + }, + { + "x": -176.374481201172, + "y": 39.9436454772949 + }, + { + "x": -175.596343994141, + "y": 20.7499542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -171.186996459961, + "y": 21.0093307495117 + }, + { + "x": -108.93717956543, + "y": 21.0093307495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -171.186996459961, + "y": 41.4998931884766 + }, + { + "x": -106.343437194824, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -160.119506835938, + "y": 34.3178215026855 + }, + { + "x": -149.744537353516, + "y": 32.5022010803223 + }, + { + "x": -139.369567871094, + "y": 35.0959434509277 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -94.6716003417969, + "y": 18.1562118530273 + }, + { + "x": -28.5311660766602, + "y": 18.9343338012695 + }, + { + "x": -25.9374237060547, + "y": 21.7874488830566 + }, + { + "x": -26.7155475616455, + "y": 41.4998931884766 + }, + { + "x": -29.8280372619629, + "y": 44.093635559082 + }, + { + "x": -95.9684677124023, + "y": 43.0561408996582 + }, + { + "x": -98.5622100830078, + "y": 39.9436454772949 + }, + { + "x": -97.7840881347656, + "y": 20.7499542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -93.3747253417969, + "y": 21.0093307495117 + }, + { + "x": -31.1249084472656, + "y": 21.0093307495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -93.3747253417969, + "y": 41.4998931884766 + }, + { + "x": -28.5311660766602, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -85.5597763061523, + "y": 30.3390197753906 + }, + { + "x": -75.1848068237305, + "y": 28.5233993530273 + }, + { + "x": -64.8098373413086, + "y": 31.1171417236328 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -16.8593254089355, + "y": 18.1562118530273 + }, + { + "x": 49.2811050415039, + "y": 18.9343338012695 + }, + { + "x": 51.8748474121094, + "y": 21.7874488830566 + }, + { + "x": 51.0967254638672, + "y": 41.4998931884766 + }, + { + "x": 47.9842338562012, + "y": 44.093635559082 + }, + { + "x": -18.1561965942383, + "y": 43.0561408996582 + }, + { + "x": -20.7499389648438, + "y": 39.9436454772949 + }, + { + "x": -19.9718151092529, + "y": 20.7499542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -15.5624542236328, + "y": 21.0093307495117 + }, + { + "x": 46.6873626708984, + "y": 21.0093307495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -15.5624542236328, + "y": 41.4998931884766 + }, + { + "x": 49.2811050415039, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -11.5525283813477, + "y": 28.4222450256348 + }, + { + "x": -1.17755901813507, + "y": 26.6066246032715 + }, + { + "x": 9.19741058349609, + "y": 29.200366973877 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 60.9529457092285, + "y": 18.1562118530273 + }, + { + "x": 127.093376159668, + "y": 18.9343338012695 + }, + { + "x": 129.687118530273, + "y": 21.7874488830566 + }, + { + "x": 128.908996582031, + "y": 41.4998931884766 + }, + { + "x": 125.796508789063, + "y": 44.093635559082 + }, + { + "x": 59.6560745239258, + "y": 43.0561408996582 + }, + { + "x": 57.0623321533203, + "y": 39.9436454772949 + }, + { + "x": 57.8404541015625, + "y": 20.7499542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 62.2498168945313, + "y": 21.0093307495117 + }, + { + "x": 124.499633789063, + "y": 21.0093307495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 62.2498168945313, + "y": 41.4998931884766 + }, + { + "x": 127.093376159668, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 70.6613235473633, + "y": 31.4076385498047 + }, + { + "x": 81.0362930297852, + "y": 29.5920219421387 + }, + { + "x": 91.411262512207, + "y": 32.1857643127441 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 138.765213012695, + "y": 18.1562118530273 + }, + { + "x": 204.905639648438, + "y": 18.9343338012695 + }, + { + "x": 207.499389648438, + "y": 21.7874488830566 + }, + { + "x": 206.721252441406, + "y": 41.4998931884766 + }, + { + "x": 203.608779907227, + "y": 44.093635559082 + }, + { + "x": 137.468353271484, + "y": 43.0561408996582 + }, + { + "x": 134.874603271484, + "y": 39.9436454772949 + }, + { + "x": 135.652725219727, + "y": 20.7499542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 140.062088012695, + "y": 21.0093307495117 + }, + { + "x": 202.311904907227, + "y": 21.0093307495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 140.062088012695, + "y": 41.4998931884766 + }, + { + "x": 204.905639648438, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 148.655151367188, + "y": 37.2772789001465 + }, + { + "x": 159.030120849609, + "y": 35.4616622924805 + }, + { + "x": 169.40510559082, + "y": 38.0554046630859 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 216.577484130859, + "y": 18.1562118530273 + }, + { + "x": 282.717926025391, + "y": 18.9343338012695 + }, + { + "x": 285.311645507813, + "y": 21.7874488830566 + }, + { + "x": 284.533538818359, + "y": 41.4998931884766 + }, + { + "x": 281.421051025391, + "y": 44.093635559082 + }, + { + "x": 215.280609130859, + "y": 43.0561408996582 + }, + { + "x": 212.686874389648, + "y": 39.9436454772949 + }, + { + "x": 213.46501159668, + "y": 20.7499542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 217.874359130859, + "y": 21.0093307495117 + }, + { + "x": 280.124176025391, + "y": 21.0093307495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 217.874359130859, + "y": 41.4998931884766 + }, + { + "x": 282.717926025391, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 226.215850830078, + "y": 37.1086845397949 + }, + { + "x": 236.5908203125, + "y": 35.2930679321289 + }, + { + "x": 246.965789794922, + "y": 37.8868103027344 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 294.389770507813, + "y": 18.1562118530273 + }, + { + "x": 326.8115234375, + "y": 18.9343338012695 + }, + { + "x": 329.4052734375, + "y": 21.7874488830566 + }, + { + "x": 328.627136230469, + "y": 41.4998931884766 + }, + { + "x": 325.514678955078, + "y": 44.093635559082 + }, + { + "x": 293.092895507813, + "y": 43.0561408996582 + }, + { + "x": 290.499145507813, + "y": 39.9436454772949 + }, + { + "x": 291.277282714844, + "y": 20.7499542236328 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 295.686645507813, + "y": 21.0093307495117 + }, + { + "x": 324.217803955078, + "y": 21.0093307495117 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 295.686645507813, + "y": 41.4998931884766 + }, + { + "x": 326.8115234375, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 299.551300048828, + "y": 34.3956336975098 + }, + { + "x": 309.92626953125, + "y": 32.5800170898438 + }, + { + "x": 320.301239013672, + "y": 35.1737594604492 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": 49.281120300293 + }, + { + "x": -306.061584472656, + "y": 50.0592422485352 + }, + { + "x": -303.467864990234, + "y": 52.9123611450195 + }, + { + "x": -304.246002197266, + "y": 72.6248016357422 + }, + { + "x": -307.358459472656, + "y": 75.2185440063477 + }, + { + "x": -326.8115234375, + "y": 74.1810455322266 + }, + { + "x": -329.4052734375, + "y": 71.0685577392578 + }, + { + "x": -328.627136230469, + "y": 51.8748626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": 52.1342391967773 + }, + { + "x": -308.655334472656, + "y": 52.1342391967773 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": 72.6248016357422 + }, + { + "x": -306.061584472656, + "y": 72.6248016357422 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -294.389770507813, + "y": 49.281120300293 + }, + { + "x": -228.249328613281, + "y": 50.0592422485352 + }, + { + "x": -225.655578613281, + "y": 52.9123611450195 + }, + { + "x": -226.433715820313, + "y": 72.6248016357422 + }, + { + "x": -229.546203613281, + "y": 75.2185440063477 + }, + { + "x": -295.686645507813, + "y": 74.1810455322266 + }, + { + "x": -298.280364990234, + "y": 71.0685577392578 + }, + { + "x": -297.502227783203, + "y": 51.8748626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -293.092895507813, + "y": 52.1342391967773 + }, + { + "x": -230.843078613281, + "y": 52.1342391967773 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -293.092895507813, + "y": 72.6248016357422 + }, + { + "x": -228.249328613281, + "y": 72.6248016357422 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -290.307220458984, + "y": 58.2710266113281 + }, + { + "x": -279.932250976563, + "y": 56.4554138183594 + }, + { + "x": -269.557281494141, + "y": 59.0491561889648 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -216.577484130859, + "y": 49.281120300293 + }, + { + "x": -150.437057495117, + "y": 50.0592422485352 + }, + { + "x": -147.843322753906, + "y": 52.9123611450195 + }, + { + "x": -148.621429443359, + "y": 72.6248016357422 + }, + { + "x": -151.733932495117, + "y": 75.2185440063477 + }, + { + "x": -217.874359130859, + "y": 74.1810455322266 + }, + { + "x": -220.468109130859, + "y": 71.0685577392578 + }, + { + "x": -219.689971923828, + "y": 51.8748626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -215.280609130859, + "y": 52.1342391967773 + }, + { + "x": -153.030792236328, + "y": 52.1342391967773 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -215.280609130859, + "y": 72.6248016357422 + }, + { + "x": -150.437057495117, + "y": 72.6248016357422 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -201.681640625, + "y": 59.220344543457 + }, + { + "x": -191.306671142578, + "y": 57.4047241210938 + }, + { + "x": -180.931701660156, + "y": 59.9984664916992 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -138.765213012695, + "y": 49.281120300293 + }, + { + "x": -72.6247863769531, + "y": 50.0592422485352 + }, + { + "x": -70.0310440063477, + "y": 52.9123611450195 + }, + { + "x": -70.8091659545898, + "y": 72.6248016357422 + }, + { + "x": -73.9216613769531, + "y": 75.2185440063477 + }, + { + "x": -140.062088012695, + "y": 74.1810455322266 + }, + { + "x": -142.655822753906, + "y": 71.0685577392578 + }, + { + "x": -141.877716064453, + "y": 51.8748626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -137.468353271484, + "y": 52.1342391967773 + }, + { + "x": -75.2185287475586, + "y": 52.1342391967773 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -137.468353271484, + "y": 72.6248016357422 + }, + { + "x": -72.6247863769531, + "y": 72.6248016357422 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -128.631454467773, + "y": 68.2751007080078 + }, + { + "x": -118.256492614746, + "y": 66.45947265625 + }, + { + "x": -107.881523132324, + "y": 69.05322265625 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -60.9529457092285, + "y": 49.281120300293 + }, + { + "x": 5.18748474121094, + "y": 50.0592422485352 + }, + { + "x": 7.78122711181641, + "y": 52.9123611450195 + }, + { + "x": 7.00310468673706, + "y": 72.6248016357422 + }, + { + "x": 3.8906135559082, + "y": 75.2185440063477 + }, + { + "x": -62.2498168945313, + "y": 74.1810455322266 + }, + { + "x": -64.8435592651367, + "y": 71.0685577392578 + }, + { + "x": -64.0654373168945, + "y": 51.8748626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -59.6560745239258, + "y": 52.1342391967773 + }, + { + "x": 2.59374237060547, + "y": 52.1342391967773 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -59.6560745239258, + "y": 72.6248016357422 + }, + { + "x": 5.18748474121094, + "y": 72.6248016357422 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -54.9276809692383, + "y": 57.0908813476563 + }, + { + "x": -44.5527114868164, + "y": 55.275260925293 + }, + { + "x": -34.1777420043945, + "y": 57.8690032958984 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 16.8593254089355, + "y": 49.281120300293 + }, + { + "x": 82.999755859375, + "y": 50.0592422485352 + }, + { + "x": 85.5934982299805, + "y": 52.9123611450195 + }, + { + "x": 84.8153762817383, + "y": 72.6248016357422 + }, + { + "x": 81.702880859375, + "y": 75.2185440063477 + }, + { + "x": 15.5624542236328, + "y": 74.1810455322266 + }, + { + "x": 12.9687118530273, + "y": 71.0685577392578 + }, + { + "x": 13.7468347549438, + "y": 51.8748626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 18.1561965942383, + "y": 52.1342391967773 + }, + { + "x": 80.4060134887695, + "y": 52.1342391967773 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 18.1561965942383, + "y": 72.6248016357422 + }, + { + "x": 82.999755859375, + "y": 72.6248016357422 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 24.1892414093018, + "y": 67.2298202514648 + }, + { + "x": 34.5642127990723, + "y": 65.4141998291016 + }, + { + "x": 44.9391822814941, + "y": 68.007942199707 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 94.6716003417969, + "y": 49.281120300293 + }, + { + "x": 160.812026977539, + "y": 50.0592422485352 + }, + { + "x": 163.40576171875, + "y": 52.9123611450195 + }, + { + "x": 162.627655029297, + "y": 72.6248016357422 + }, + { + "x": 159.515151977539, + "y": 75.2185440063477 + }, + { + "x": 93.3747253417969, + "y": 74.1810455322266 + }, + { + "x": 90.7809829711914, + "y": 71.0685577392578 + }, + { + "x": 91.5591049194336, + "y": 51.8748626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 95.9684677124023, + "y": 52.1342391967773 + }, + { + "x": 158.218292236328, + "y": 52.1342391967773 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 95.9684677124023, + "y": 72.6248016357422 + }, + { + "x": 160.812026977539, + "y": 72.6248016357422 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 102.260879516602, + "y": 66.0963516235352 + }, + { + "x": 112.635848999023, + "y": 64.2807312011719 + }, + { + "x": 123.010818481445, + "y": 66.8744735717773 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 172.483871459961, + "y": 49.281120300293 + }, + { + "x": 238.624298095703, + "y": 50.0592422485352 + }, + { + "x": 241.218048095703, + "y": 52.9123611450195 + }, + { + "x": 240.439910888672, + "y": 72.6248016357422 + }, + { + "x": 237.327423095703, + "y": 75.2185440063477 + }, + { + "x": 171.186996459961, + "y": 74.1810455322266 + }, + { + "x": 168.59326171875, + "y": 71.0685577392578 + }, + { + "x": 169.371383666992, + "y": 51.8748626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 173.780731201172, + "y": 52.1342391967773 + }, + { + "x": 236.030548095703, + "y": 52.1342391967773 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 173.780731201172, + "y": 72.6248016357422 + }, + { + "x": 238.624298095703, + "y": 72.6248016357422 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 189.28352355957, + "y": 64.2703552246094 + }, + { + "x": 199.658493041992, + "y": 62.4547424316406 + }, + { + "x": 210.033462524414, + "y": 65.0484848022461 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 250.296142578125, + "y": 49.281120300293 + }, + { + "x": 316.436584472656, + "y": 50.0592422485352 + }, + { + "x": 319.030303955078, + "y": 52.9123611450195 + }, + { + "x": 318.252166748047, + "y": 72.6248016357422 + }, + { + "x": 315.139709472656, + "y": 75.2185440063477 + }, + { + "x": 248.999267578125, + "y": 74.1810455322266 + }, + { + "x": 246.405517578125, + "y": 71.0685577392578 + }, + { + "x": 247.183654785156, + "y": 51.8748626708984 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 251.593017578125, + "y": 52.1342391967773 + }, + { + "x": 313.842834472656, + "y": 52.1342391967773 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 251.593017578125, + "y": 72.6248016357422 + }, + { + "x": 316.436584472656, + "y": 72.6248016357422 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 260.497314453125, + "y": 59.5238037109375 + }, + { + "x": 270.872283935547, + "y": 57.7081909179688 + }, + { + "x": 281.247253417969, + "y": 60.3019332885742 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": 80.4060287475586 + }, + { + "x": -261.967987060547, + "y": 81.1841506958008 + }, + { + "x": -259.374237060547, + "y": 84.0372695922852 + }, + { + "x": -260.152374267578, + "y": 103.749710083008 + }, + { + "x": -263.264862060547, + "y": 106.343452453613 + }, + { + "x": -326.8115234375, + "y": 105.305953979492 + }, + { + "x": -329.4052734375, + "y": 102.193466186523 + }, + { + "x": -328.627136230469, + "y": 82.9997711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": 83.2591400146484 + }, + { + "x": -264.561706542969, + "y": 83.2591400146484 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": 103.749710083008 + }, + { + "x": -261.967987060547, + "y": 103.749710083008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -309.059967041016, + "y": 95.69873046875 + }, + { + "x": -298.684997558594, + "y": 93.8831100463867 + }, + { + "x": -288.310028076172, + "y": 96.4768524169922 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -250.296142578125, + "y": 80.4060287475586 + }, + { + "x": -184.155700683594, + "y": 81.1841506958008 + }, + { + "x": -181.561965942383, + "y": 84.0372695922852 + }, + { + "x": -182.340103149414, + "y": 103.749710083008 + }, + { + "x": -185.452575683594, + "y": 106.343452453613 + }, + { + "x": -251.593017578125, + "y": 105.305953979492 + }, + { + "x": -254.186752319336, + "y": 102.193466186523 + }, + { + "x": -253.408615112305, + "y": 82.9997711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -248.999267578125, + "y": 83.2591400146484 + }, + { + "x": -186.749450683594, + "y": 83.2591400146484 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -248.999267578125, + "y": 103.749710083008 + }, + { + "x": -184.155700683594, + "y": 103.749710083008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -245.61442565918, + "y": 92.2542495727539 + }, + { + "x": -235.239456176758, + "y": 90.4386215209961 + }, + { + "x": -224.864486694336, + "y": 93.0323638916016 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -172.483871459961, + "y": 80.4060287475586 + }, + { + "x": -106.343437194824, + "y": 81.1841506958008 + }, + { + "x": -103.749694824219, + "y": 84.0372695922852 + }, + { + "x": -104.527816772461, + "y": 103.749710083008 + }, + { + "x": -107.64030456543, + "y": 106.343452453613 + }, + { + "x": -173.780731201172, + "y": 105.305953979492 + }, + { + "x": -176.374481201172, + "y": 102.193466186523 + }, + { + "x": -175.596343994141, + "y": 82.9997711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -171.186996459961, + "y": 83.2591400146484 + }, + { + "x": -108.93717956543, + "y": 83.2591400146484 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -171.186996459961, + "y": 103.749710083008 + }, + { + "x": -106.343437194824, + "y": 103.749710083008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -161.01692199707, + "y": 94.7079238891602 + }, + { + "x": -150.641952514648, + "y": 92.8923034667969 + }, + { + "x": -140.266983032227, + "y": 95.4860458374023 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -94.6716003417969, + "y": 80.4060287475586 + }, + { + "x": -28.5311660766602, + "y": 81.1841506958008 + }, + { + "x": -25.9374237060547, + "y": 84.0372695922852 + }, + { + "x": -26.7155475616455, + "y": 103.749710083008 + }, + { + "x": -29.8280372619629, + "y": 106.343452453613 + }, + { + "x": -95.9684677124023, + "y": 105.305953979492 + }, + { + "x": -98.5622100830078, + "y": 102.193466186523 + }, + { + "x": -97.7840881347656, + "y": 82.9997711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -93.3747253417969, + "y": 83.2591400146484 + }, + { + "x": -31.1249084472656, + "y": 83.2591400146484 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -93.3747253417969, + "y": 103.749710083008 + }, + { + "x": -28.5311660766602, + "y": 103.749710083008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -83.8920059204102, + "y": 88.9238815307617 + }, + { + "x": -73.5170364379883, + "y": 87.1082611083984 + }, + { + "x": -63.1420631408691, + "y": 89.7020034790039 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -16.8593254089355, + "y": 80.4060287475586 + }, + { + "x": 49.2811050415039, + "y": 81.1841506958008 + }, + { + "x": 51.8748474121094, + "y": 84.0372695922852 + }, + { + "x": 51.0967254638672, + "y": 103.749710083008 + }, + { + "x": 47.9842338562012, + "y": 106.343452453613 + }, + { + "x": -18.1561965942383, + "y": 105.305953979492 + }, + { + "x": -20.7499389648438, + "y": 102.193466186523 + }, + { + "x": -19.9718151092529, + "y": 82.9997711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -15.5624542236328, + "y": 83.2591400146484 + }, + { + "x": 46.6873626708984, + "y": 83.2591400146484 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -15.5624542236328, + "y": 103.749710083008 + }, + { + "x": 49.2811050415039, + "y": 103.749710083008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -11.9286212921143, + "y": 90.5968475341797 + }, + { + "x": -1.5536515712738, + "y": 88.7812194824219 + }, + { + "x": 8.82131767272949, + "y": 91.3749618530273 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 60.9529457092285, + "y": 80.4060287475586 + }, + { + "x": 127.093376159668, + "y": 81.1841506958008 + }, + { + "x": 129.687118530273, + "y": 84.0372695922852 + }, + { + "x": 128.908996582031, + "y": 103.749710083008 + }, + { + "x": 125.796508789063, + "y": 106.343452453613 + }, + { + "x": 59.6560745239258, + "y": 105.305953979492 + }, + { + "x": 57.0623321533203, + "y": 102.193466186523 + }, + { + "x": 57.8404541015625, + "y": 82.9997711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 62.2498168945313, + "y": 83.2591400146484 + }, + { + "x": 124.499633789063, + "y": 83.2591400146484 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 62.2498168945313, + "y": 103.749710083008 + }, + { + "x": 127.093376159668, + "y": 103.749710083008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 65.7254333496094, + "y": 99.3636856079102 + }, + { + "x": 76.1004028320313, + "y": 97.5480728149414 + }, + { + "x": 86.4753723144531, + "y": 100.141815185547 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 138.765213012695, + "y": 80.4060287475586 + }, + { + "x": 204.905639648438, + "y": 81.1841506958008 + }, + { + "x": 207.499389648438, + "y": 84.0372695922852 + }, + { + "x": 206.721252441406, + "y": 103.749710083008 + }, + { + "x": 203.608779907227, + "y": 106.343452453613 + }, + { + "x": 137.468353271484, + "y": 105.305953979492 + }, + { + "x": 134.874603271484, + "y": 102.193466186523 + }, + { + "x": 135.652725219727, + "y": 82.9997711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 140.062088012695, + "y": 83.2591400146484 + }, + { + "x": 202.311904907227, + "y": 83.2591400146484 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 140.062088012695, + "y": 103.749710083008 + }, + { + "x": 204.905639648438, + "y": 103.749710083008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 144.20947265625, + "y": 99.7320022583008 + }, + { + "x": 154.584442138672, + "y": 97.9163818359375 + }, + { + "x": 164.959411621094, + "y": 100.510124206543 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 216.577484130859, + "y": 80.4060287475586 + }, + { + "x": 282.717926025391, + "y": 81.1841506958008 + }, + { + "x": 285.311645507813, + "y": 84.0372695922852 + }, + { + "x": 284.533538818359, + "y": 103.749710083008 + }, + { + "x": 281.421051025391, + "y": 106.343452453613 + }, + { + "x": 215.280609130859, + "y": 105.305953979492 + }, + { + "x": 212.686874389648, + "y": 102.193466186523 + }, + { + "x": 213.46501159668, + "y": 82.9997711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 217.874359130859, + "y": 83.2591400146484 + }, + { + "x": 280.124176025391, + "y": 83.2591400146484 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 217.874359130859, + "y": 103.749710083008 + }, + { + "x": 282.717926025391, + "y": 103.749710083008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 227.976974487305, + "y": 88.7034149169922 + }, + { + "x": 238.351943969727, + "y": 86.8877944946289 + }, + { + "x": 248.726913452148, + "y": 89.4815368652344 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 294.389770507813, + "y": 80.4060287475586 + }, + { + "x": 326.8115234375, + "y": 81.1841506958008 + }, + { + "x": 329.4052734375, + "y": 84.0372695922852 + }, + { + "x": 328.627136230469, + "y": 103.749710083008 + }, + { + "x": 325.514678955078, + "y": 106.343452453613 + }, + { + "x": 293.092895507813, + "y": 105.305953979492 + }, + { + "x": 290.499145507813, + "y": 102.193466186523 + }, + { + "x": 291.277282714844, + "y": 82.9997711181641 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 295.686645507813, + "y": 83.2591400146484 + }, + { + "x": 324.217803955078, + "y": 83.2591400146484 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 295.686645507813, + "y": 103.749710083008 + }, + { + "x": 326.8115234375, + "y": 103.749710083008 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 303.716857910156, + "y": 95.7557907104492 + }, + { + "x": 314.091827392578, + "y": 93.9401702880859 + }, + { + "x": 324.217803955078, + "y": 96.5339126586914 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": 111.530937194824 + }, + { + "x": -306.061584472656, + "y": 112.309059143066 + }, + { + "x": -303.467864990234, + "y": 115.162185668945 + }, + { + "x": -304.246002197266, + "y": 134.874618530273 + }, + { + "x": -307.358459472656, + "y": 137.468353271484 + }, + { + "x": -326.8115234375, + "y": 136.430862426758 + }, + { + "x": -329.4052734375, + "y": 133.318374633789 + }, + { + "x": -328.627136230469, + "y": 114.12467956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": 114.384048461914 + }, + { + "x": -308.655334472656, + "y": 114.384048461914 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": 134.874618530273 + }, + { + "x": -306.061584472656, + "y": 134.874618530273 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -294.389770507813, + "y": 111.530937194824 + }, + { + "x": -228.249328613281, + "y": 112.309059143066 + }, + { + "x": -225.655578613281, + "y": 115.162185668945 + }, + { + "x": -226.433715820313, + "y": 134.874618530273 + }, + { + "x": -229.546203613281, + "y": 137.468353271484 + }, + { + "x": -295.686645507813, + "y": 136.430862426758 + }, + { + "x": -298.280364990234, + "y": 133.318374633789 + }, + { + "x": -297.502227783203, + "y": 114.12467956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -293.092895507813, + "y": 114.384048461914 + }, + { + "x": -230.843078613281, + "y": 114.384048461914 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -293.092895507813, + "y": 134.874618530273 + }, + { + "x": -228.249328613281, + "y": 134.874618530273 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -278.3759765625, + "y": 121.960372924805 + }, + { + "x": -268.001007080078, + "y": 120.144760131836 + }, + { + "x": -257.626037597656, + "y": 122.738494873047 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -216.577484130859, + "y": 111.530937194824 + }, + { + "x": -150.437057495117, + "y": 112.309059143066 + }, + { + "x": -147.843322753906, + "y": 115.162185668945 + }, + { + "x": -148.621429443359, + "y": 134.874618530273 + }, + { + "x": -151.733932495117, + "y": 137.468353271484 + }, + { + "x": -217.874359130859, + "y": 136.430862426758 + }, + { + "x": -220.468109130859, + "y": 133.318374633789 + }, + { + "x": -219.689971923828, + "y": 114.12467956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -215.280609130859, + "y": 114.384048461914 + }, + { + "x": -153.030792236328, + "y": 114.384048461914 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -215.280609130859, + "y": 134.874618530273 + }, + { + "x": -150.437057495117, + "y": 134.874618530273 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -208.612106323242, + "y": 122.756652832031 + }, + { + "x": -198.23713684082, + "y": 120.941024780273 + }, + { + "x": -187.862167358398, + "y": 123.534774780273 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -138.765213012695, + "y": 111.530937194824 + }, + { + "x": -72.6247863769531, + "y": 112.309059143066 + }, + { + "x": -70.0310440063477, + "y": 115.162185668945 + }, + { + "x": -70.8091659545898, + "y": 134.874618530273 + }, + { + "x": -73.9216613769531, + "y": 137.468353271484 + }, + { + "x": -140.062088012695, + "y": 136.430862426758 + }, + { + "x": -142.655822753906, + "y": 133.318374633789 + }, + { + "x": -141.877716064453, + "y": 114.12467956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -137.468353271484, + "y": 114.384048461914 + }, + { + "x": -75.2185287475586, + "y": 114.384048461914 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -137.468353271484, + "y": 134.874618530273 + }, + { + "x": -72.6247863769531, + "y": 134.874618530273 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -126.164810180664, + "y": 123.202774047852 + }, + { + "x": -115.789840698242, + "y": 121.387161254883 + }, + { + "x": -105.41487121582, + "y": 123.980895996094 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -60.9529457092285, + "y": 111.530937194824 + }, + { + "x": 5.18748474121094, + "y": 112.309059143066 + }, + { + "x": 7.78122711181641, + "y": 115.162185668945 + }, + { + "x": 7.00310468673706, + "y": 134.874618530273 + }, + { + "x": 3.8906135559082, + "y": 137.468353271484 + }, + { + "x": -62.2498168945313, + "y": 136.430862426758 + }, + { + "x": -64.8435592651367, + "y": 133.318374633789 + }, + { + "x": -64.0654373168945, + "y": 114.12467956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -59.6560745239258, + "y": 114.384048461914 + }, + { + "x": 2.59374237060547, + "y": 114.384048461914 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -59.6560745239258, + "y": 134.874618530273 + }, + { + "x": 5.18748474121094, + "y": 134.874618530273 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -45.0143966674805, + "y": 131.448287963867 + }, + { + "x": -34.6394271850586, + "y": 129.632659912109 + }, + { + "x": -24.2644596099854, + "y": 132.226409912109 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 16.8593254089355, + "y": 111.530937194824 + }, + { + "x": 82.999755859375, + "y": 112.309059143066 + }, + { + "x": 85.5934982299805, + "y": 115.162185668945 + }, + { + "x": 84.8153762817383, + "y": 134.874618530273 + }, + { + "x": 81.702880859375, + "y": 137.468353271484 + }, + { + "x": 15.5624542236328, + "y": 136.430862426758 + }, + { + "x": 12.9687118530273, + "y": 133.318374633789 + }, + { + "x": 13.7468347549438, + "y": 114.12467956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 18.1561965942383, + "y": 114.384048461914 + }, + { + "x": 80.4060134887695, + "y": 114.384048461914 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 18.1561965942383, + "y": 134.874618530273 + }, + { + "x": 82.999755859375, + "y": 134.874618530273 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 26.6922035217285, + "y": 132.260131835938 + }, + { + "x": 37.0671730041504, + "y": 130.44450378418 + }, + { + "x": 47.4421424865723, + "y": 133.03825378418 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 94.6716003417969, + "y": 111.530937194824 + }, + { + "x": 160.812026977539, + "y": 112.309059143066 + }, + { + "x": 163.40576171875, + "y": 115.162185668945 + }, + { + "x": 162.627655029297, + "y": 134.874618530273 + }, + { + "x": 159.515151977539, + "y": 137.468353271484 + }, + { + "x": 93.3747253417969, + "y": 136.430862426758 + }, + { + "x": 90.7809829711914, + "y": 133.318374633789 + }, + { + "x": 91.5591049194336, + "y": 114.12467956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 95.9684677124023, + "y": 114.384048461914 + }, + { + "x": 158.218292236328, + "y": 114.384048461914 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 95.9684677124023, + "y": 134.874618530273 + }, + { + "x": 160.812026977539, + "y": 134.874618530273 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 99.5115203857422, + "y": 122.077102661133 + }, + { + "x": 109.886489868164, + "y": 120.261474609375 + }, + { + "x": 120.261459350586, + "y": 122.855224609375 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 172.483871459961, + "y": 111.530937194824 + }, + { + "x": 238.624298095703, + "y": 112.309059143066 + }, + { + "x": 241.218048095703, + "y": 115.162185668945 + }, + { + "x": 240.439910888672, + "y": 134.874618530273 + }, + { + "x": 237.327423095703, + "y": 137.468353271484 + }, + { + "x": 171.186996459961, + "y": 136.430862426758 + }, + { + "x": 168.59326171875, + "y": 133.318374633789 + }, + { + "x": 169.371383666992, + "y": 114.12467956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 173.780731201172, + "y": 114.384048461914 + }, + { + "x": 236.030548095703, + "y": 114.384048461914 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 173.780731201172, + "y": 134.874618530273 + }, + { + "x": 238.624298095703, + "y": 134.874618530273 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 178.091552734375, + "y": 123.838241577148 + }, + { + "x": 188.466522216797, + "y": 122.022613525391 + }, + { + "x": 198.841491699219, + "y": 124.616363525391 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 250.296142578125, + "y": 111.530937194824 + }, + { + "x": 316.436584472656, + "y": 112.309059143066 + }, + { + "x": 319.030303955078, + "y": 115.162185668945 + }, + { + "x": 318.252166748047, + "y": 134.874618530273 + }, + { + "x": 315.139709472656, + "y": 137.468353271484 + }, + { + "x": 248.999267578125, + "y": 136.430862426758 + }, + { + "x": 246.405517578125, + "y": 133.318374633789 + }, + { + "x": 247.183654785156, + "y": 114.12467956543 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 251.593017578125, + "y": 114.384048461914 + }, + { + "x": 313.842834472656, + "y": 114.384048461914 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 251.593017578125, + "y": 134.874618530273 + }, + { + "x": 316.436584472656, + "y": 134.874618530273 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 265.5888671875, + "y": 124.105392456055 + }, + { + "x": 275.963836669922, + "y": 122.289779663086 + }, + { + "x": 286.338806152344, + "y": 124.883514404297 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": 142.655853271484 + }, + { + "x": -261.967987060547, + "y": 143.433959960938 + }, + { + "x": -259.374237060547, + "y": 146.287094116211 + }, + { + "x": -260.152374267578, + "y": 165.999526977539 + }, + { + "x": -263.264862060547, + "y": 168.59326171875 + }, + { + "x": -326.8115234375, + "y": 167.555770874023 + }, + { + "x": -329.4052734375, + "y": 164.443283081055 + }, + { + "x": -328.627136230469, + "y": 145.249588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": 145.50895690918 + }, + { + "x": -264.561706542969, + "y": 145.50895690918 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": 165.999526977539 + }, + { + "x": -261.967987060547, + "y": 165.999526977539 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -315.941162109375, + "y": 157.481689453125 + }, + { + "x": -305.566192626953, + "y": 155.666061401367 + }, + { + "x": -295.191223144531, + "y": 158.259796142578 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -250.296142578125, + "y": 142.655853271484 + }, + { + "x": -184.155700683594, + "y": 143.433959960938 + }, + { + "x": -181.561965942383, + "y": 146.287094116211 + }, + { + "x": -182.340103149414, + "y": 165.999526977539 + }, + { + "x": -185.452575683594, + "y": 168.59326171875 + }, + { + "x": -251.593017578125, + "y": 167.555770874023 + }, + { + "x": -254.186752319336, + "y": 164.443283081055 + }, + { + "x": -253.408615112305, + "y": 145.249588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -248.999267578125, + "y": 145.50895690918 + }, + { + "x": -186.749450683594, + "y": 145.50895690918 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -248.999267578125, + "y": 165.999526977539 + }, + { + "x": -184.155700683594, + "y": 165.999526977539 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -238.473861694336, + "y": 163.239776611328 + }, + { + "x": -228.098892211914, + "y": 161.424163818359 + }, + { + "x": -217.723922729492, + "y": 164.017913818359 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -172.483871459961, + "y": 142.655853271484 + }, + { + "x": -106.343437194824, + "y": 143.433959960938 + }, + { + "x": -103.749694824219, + "y": 146.287094116211 + }, + { + "x": -104.527816772461, + "y": 165.999526977539 + }, + { + "x": -107.64030456543, + "y": 168.59326171875 + }, + { + "x": -173.780731201172, + "y": 167.555770874023 + }, + { + "x": -176.374481201172, + "y": 164.443283081055 + }, + { + "x": -175.596343994141, + "y": 145.249588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -171.186996459961, + "y": 145.50895690918 + }, + { + "x": -108.93717956543, + "y": 145.50895690918 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -171.186996459961, + "y": 165.999526977539 + }, + { + "x": -106.343437194824, + "y": 165.999526977539 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -161.732803344727, + "y": 159.097564697266 + }, + { + "x": -151.357833862305, + "y": 157.281951904297 + }, + { + "x": -140.982864379883, + "y": 159.875701904297 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -94.6716003417969, + "y": 142.655853271484 + }, + { + "x": -28.5311660766602, + "y": 143.433959960938 + }, + { + "x": -25.9374237060547, + "y": 146.287094116211 + }, + { + "x": -26.7155475616455, + "y": 165.999526977539 + }, + { + "x": -29.8280372619629, + "y": 168.59326171875 + }, + { + "x": -95.9684677124023, + "y": 167.555770874023 + }, + { + "x": -98.5622100830078, + "y": 164.443283081055 + }, + { + "x": -97.7840881347656, + "y": 145.249588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -93.3747253417969, + "y": 145.50895690918 + }, + { + "x": -31.1249084472656, + "y": 145.50895690918 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -93.3747253417969, + "y": 165.999526977539 + }, + { + "x": -28.5311660766602, + "y": 165.999526977539 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -86.8981475830078, + "y": 159.040512084961 + }, + { + "x": -76.5231781005859, + "y": 157.224884033203 + }, + { + "x": -66.1482086181641, + "y": 159.818634033203 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -16.8593254089355, + "y": 142.655853271484 + }, + { + "x": 49.2811050415039, + "y": 143.433959960938 + }, + { + "x": 51.8748474121094, + "y": 146.287094116211 + }, + { + "x": 51.0967254638672, + "y": 165.999526977539 + }, + { + "x": 47.9842338562012, + "y": 168.59326171875 + }, + { + "x": -18.1561965942383, + "y": 167.555770874023 + }, + { + "x": -20.7499389648438, + "y": 164.443283081055 + }, + { + "x": -19.9718151092529, + "y": 145.249588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -15.5624542236328, + "y": 145.50895690918 + }, + { + "x": 46.6873626708984, + "y": 145.50895690918 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -15.5624542236328, + "y": 165.999526977539 + }, + { + "x": 49.2811050415039, + "y": 165.999526977539 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -9.53978443145752, + "y": 161.976623535156 + }, + { + "x": 0.835185050964355, + "y": 160.161010742188 + }, + { + "x": 11.2101545333862, + "y": 162.754760742188 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 60.9529457092285, + "y": 142.655853271484 + }, + { + "x": 127.093376159668, + "y": 143.433959960938 + }, + { + "x": 129.687118530273, + "y": 146.287094116211 + }, + { + "x": 128.908996582031, + "y": 165.999526977539 + }, + { + "x": 125.796508789063, + "y": 168.59326171875 + }, + { + "x": 59.6560745239258, + "y": 167.555770874023 + }, + { + "x": 57.0623321533203, + "y": 164.443283081055 + }, + { + "x": 57.8404541015625, + "y": 145.249588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 62.2498168945313, + "y": 145.50895690918 + }, + { + "x": 124.499633789063, + "y": 145.50895690918 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 62.2498168945313, + "y": 165.999526977539 + }, + { + "x": 127.093376159668, + "y": 165.999526977539 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 72.0178527832031, + "y": 153.969741821289 + }, + { + "x": 82.392822265625, + "y": 152.154144287109 + }, + { + "x": 92.7677841186523, + "y": 154.74787902832 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 138.765213012695, + "y": 142.655853271484 + }, + { + "x": 204.905639648438, + "y": 143.433959960938 + }, + { + "x": 207.499389648438, + "y": 146.287094116211 + }, + { + "x": 206.721252441406, + "y": 165.999526977539 + }, + { + "x": 203.608779907227, + "y": 168.59326171875 + }, + { + "x": 137.468353271484, + "y": 167.555770874023 + }, + { + "x": 134.874603271484, + "y": 164.443283081055 + }, + { + "x": 135.652725219727, + "y": 145.249588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 140.062088012695, + "y": 145.50895690918 + }, + { + "x": 202.311904907227, + "y": 145.50895690918 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 140.062088012695, + "y": 165.999526977539 + }, + { + "x": 204.905639648438, + "y": 165.999526977539 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 145.38703918457, + "y": 154.882736206055 + }, + { + "x": 155.762008666992, + "y": 153.067123413086 + }, + { + "x": 166.136978149414, + "y": 155.660873413086 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 216.577484130859, + "y": 142.655853271484 + }, + { + "x": 282.717926025391, + "y": 143.433959960938 + }, + { + "x": 285.311645507813, + "y": 146.287094116211 + }, + { + "x": 284.533538818359, + "y": 165.999526977539 + }, + { + "x": 281.421051025391, + "y": 168.59326171875 + }, + { + "x": 215.280609130859, + "y": 167.555770874023 + }, + { + "x": 212.686874389648, + "y": 164.443283081055 + }, + { + "x": 213.46501159668, + "y": 145.249588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 217.874359130859, + "y": 145.50895690918 + }, + { + "x": 280.124176025391, + "y": 145.50895690918 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 217.874359130859, + "y": 165.999526977539 + }, + { + "x": 282.717926025391, + "y": 165.999526977539 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 228.692855834961, + "y": 153.860809326172 + }, + { + "x": 239.067825317383, + "y": 152.045196533203 + }, + { + "x": 249.442794799805, + "y": 154.638946533203 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 294.389770507813, + "y": 142.655853271484 + }, + { + "x": 326.8115234375, + "y": 143.433959960938 + }, + { + "x": 329.4052734375, + "y": 146.287094116211 + }, + { + "x": 328.627136230469, + "y": 165.999526977539 + }, + { + "x": 325.514678955078, + "y": 168.59326171875 + }, + { + "x": 293.092895507813, + "y": 167.555770874023 + }, + { + "x": 290.499145507813, + "y": 164.443283081055 + }, + { + "x": 291.277282714844, + "y": 145.249588012695 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 295.686645507813, + "y": 145.50895690918 + }, + { + "x": 324.217803955078, + "y": 145.50895690918 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 295.686645507813, + "y": 165.999526977539 + }, + { + "x": 326.8115234375, + "y": 165.999526977539 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 301.538116455078, + "y": 162.215255737305 + }, + { + "x": 311.9130859375, + "y": 160.399642944336 + }, + { + "x": 322.288055419922, + "y": 162.993392944336 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": 173.78076171875 + }, + { + "x": -306.061584472656, + "y": 174.558883666992 + }, + { + "x": -303.467864990234, + "y": 177.412002563477 + }, + { + "x": -304.246002197266, + "y": 197.124435424805 + }, + { + "x": -307.358459472656, + "y": 199.718170166016 + }, + { + "x": -326.8115234375, + "y": 198.680679321289 + }, + { + "x": -329.4052734375, + "y": 195.56819152832 + }, + { + "x": -328.627136230469, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": 176.633865356445 + }, + { + "x": -308.655334472656, + "y": 176.633865356445 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": 197.124435424805 + }, + { + "x": -306.061584472656, + "y": 197.124435424805 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -294.389770507813, + "y": 173.78076171875 + }, + { + "x": -228.249328613281, + "y": 174.558883666992 + }, + { + "x": -225.655578613281, + "y": 177.412002563477 + }, + { + "x": -226.433715820313, + "y": 197.124435424805 + }, + { + "x": -229.546203613281, + "y": 199.718170166016 + }, + { + "x": -295.686645507813, + "y": 198.680679321289 + }, + { + "x": -298.280364990234, + "y": 195.56819152832 + }, + { + "x": -297.502227783203, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -293.092895507813, + "y": 176.633865356445 + }, + { + "x": -230.843078613281, + "y": 176.633865356445 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -293.092895507813, + "y": 197.124435424805 + }, + { + "x": -228.249328613281, + "y": 197.124435424805 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -282.274383544922, + "y": 192.318237304688 + }, + { + "x": -271.8994140625, + "y": 190.50260925293 + }, + { + "x": -261.524444580078, + "y": 193.096343994141 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -216.577484130859, + "y": 173.78076171875 + }, + { + "x": -150.437057495117, + "y": 174.558883666992 + }, + { + "x": -147.843322753906, + "y": 177.412002563477 + }, + { + "x": -148.621429443359, + "y": 197.124435424805 + }, + { + "x": -151.733932495117, + "y": 199.718170166016 + }, + { + "x": -217.874359130859, + "y": 198.680679321289 + }, + { + "x": -220.468109130859, + "y": 195.56819152832 + }, + { + "x": -219.689971923828, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -215.280609130859, + "y": 176.633865356445 + }, + { + "x": -153.030792236328, + "y": 176.633865356445 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -215.280609130859, + "y": 197.124435424805 + }, + { + "x": -150.437057495117, + "y": 197.124435424805 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -212.640197753906, + "y": 191.563461303711 + }, + { + "x": -202.265228271484, + "y": 189.747833251953 + }, + { + "x": -191.890258789063, + "y": 192.341567993164 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -138.765213012695, + "y": 173.78076171875 + }, + { + "x": -72.6247863769531, + "y": 174.558883666992 + }, + { + "x": -70.0310440063477, + "y": 177.412002563477 + }, + { + "x": -70.8091659545898, + "y": 197.124435424805 + }, + { + "x": -73.9216613769531, + "y": 199.718170166016 + }, + { + "x": -140.062088012695, + "y": 198.680679321289 + }, + { + "x": -142.655822753906, + "y": 195.56819152832 + }, + { + "x": -141.877716064453, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -137.468353271484, + "y": 176.633865356445 + }, + { + "x": -75.2185287475586, + "y": 176.633865356445 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -137.468353271484, + "y": 197.124435424805 + }, + { + "x": -72.6247863769531, + "y": 197.124435424805 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -122.167861938477, + "y": 183.65771484375 + }, + { + "x": -111.792892456055, + "y": 181.84211730957 + }, + { + "x": -101.417922973633, + "y": 184.435852050781 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -60.9529457092285, + "y": 173.78076171875 + }, + { + "x": 5.18748474121094, + "y": 174.558883666992 + }, + { + "x": 7.78122711181641, + "y": 177.412002563477 + }, + { + "x": 7.00310468673706, + "y": 197.124435424805 + }, + { + "x": 3.8906135559082, + "y": 199.718170166016 + }, + { + "x": -62.2498168945313, + "y": 198.680679321289 + }, + { + "x": -64.8435592651367, + "y": 195.56819152832 + }, + { + "x": -64.0654373168945, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -59.6560745239258, + "y": 176.633865356445 + }, + { + "x": 2.59374237060547, + "y": 176.633865356445 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -59.6560745239258, + "y": 197.124435424805 + }, + { + "x": 5.18748474121094, + "y": 197.124435424805 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -49.9087905883789, + "y": 187.107406616211 + }, + { + "x": -39.533821105957, + "y": 185.291793823242 + }, + { + "x": -29.1588516235352, + "y": 187.885528564453 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 16.8593254089355, + "y": 173.78076171875 + }, + { + "x": 82.999755859375, + "y": 174.558883666992 + }, + { + "x": 85.5934982299805, + "y": 177.412002563477 + }, + { + "x": 84.8153762817383, + "y": 197.124435424805 + }, + { + "x": 81.702880859375, + "y": 199.718170166016 + }, + { + "x": 15.5624542236328, + "y": 198.680679321289 + }, + { + "x": 12.9687118530273, + "y": 195.56819152832 + }, + { + "x": 13.7468347549438, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 18.1561965942383, + "y": 176.633865356445 + }, + { + "x": 80.4060134887695, + "y": 176.633865356445 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 18.1561965942383, + "y": 197.124435424805 + }, + { + "x": 82.999755859375, + "y": 197.124435424805 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 22.1998405456543, + "y": 193.817413330078 + }, + { + "x": 32.5748100280762, + "y": 192.001800537109 + }, + { + "x": 42.949779510498, + "y": 194.59553527832 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 94.6716003417969, + "y": 173.78076171875 + }, + { + "x": 160.812026977539, + "y": 174.558883666992 + }, + { + "x": 163.40576171875, + "y": 177.412002563477 + }, + { + "x": 162.627655029297, + "y": 197.124435424805 + }, + { + "x": 159.515151977539, + "y": 199.718170166016 + }, + { + "x": 93.3747253417969, + "y": 198.680679321289 + }, + { + "x": 90.7809829711914, + "y": 195.56819152832 + }, + { + "x": 91.5591049194336, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 95.9684677124023, + "y": 176.633865356445 + }, + { + "x": 158.218292236328, + "y": 176.633865356445 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 95.9684677124023, + "y": 197.124435424805 + }, + { + "x": 160.812026977539, + "y": 197.124435424805 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 110.285926818848, + "y": 188.6376953125 + }, + { + "x": 120.66089630127, + "y": 186.82209777832 + }, + { + "x": 131.035858154297, + "y": 189.415832519531 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 172.483871459961, + "y": 173.78076171875 + }, + { + "x": 238.624298095703, + "y": 174.558883666992 + }, + { + "x": 241.218048095703, + "y": 177.412002563477 + }, + { + "x": 240.439910888672, + "y": 197.124435424805 + }, + { + "x": 237.327423095703, + "y": 199.718170166016 + }, + { + "x": 171.186996459961, + "y": 198.680679321289 + }, + { + "x": 168.59326171875, + "y": 195.56819152832 + }, + { + "x": 169.371383666992, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 173.780731201172, + "y": 176.633865356445 + }, + { + "x": 236.030548095703, + "y": 176.633865356445 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 173.780731201172, + "y": 197.124435424805 + }, + { + "x": 238.624298095703, + "y": 197.124435424805 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 181.162536621094, + "y": 188.728485107422 + }, + { + "x": 191.537506103516, + "y": 186.912887573242 + }, + { + "x": 201.912475585938, + "y": 189.506622314453 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 250.296142578125, + "y": 173.78076171875 + }, + { + "x": 316.436584472656, + "y": 174.558883666992 + }, + { + "x": 319.030303955078, + "y": 177.412002563477 + }, + { + "x": 318.252166748047, + "y": 197.124435424805 + }, + { + "x": 315.139709472656, + "y": 199.718170166016 + }, + { + "x": 248.999267578125, + "y": 198.680679321289 + }, + { + "x": 246.405517578125, + "y": 195.56819152832 + }, + { + "x": 247.183654785156, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 251.593017578125, + "y": 176.633865356445 + }, + { + "x": 313.842834472656, + "y": 176.633865356445 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 251.593017578125, + "y": 197.124435424805 + }, + { + "x": 316.436584472656, + "y": 197.124435424805 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 255.838973999023, + "y": 185.558944702148 + }, + { + "x": 266.213928222656, + "y": 183.74333190918 + }, + { + "x": 276.588897705078, + "y": 186.337066650391 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": 204.905670166016 + }, + { + "x": -261.967987060547, + "y": 205.683792114258 + }, + { + "x": -259.374237060547, + "y": 208.536911010742 + }, + { + "x": -260.152374267578, + "y": 228.24934387207 + }, + { + "x": -263.264862060547, + "y": 230.843078613281 + }, + { + "x": -326.8115234375, + "y": 229.805587768555 + }, + { + "x": -329.4052734375, + "y": 226.693099975586 + }, + { + "x": -328.627136230469, + "y": 207.499404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": 207.758773803711 + }, + { + "x": -264.561706542969, + "y": 207.758773803711 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": 228.24934387207 + }, + { + "x": -261.967987060547, + "y": 228.24934387207 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -309.015869140625, + "y": 213.203033447266 + }, + { + "x": -298.640899658203, + "y": 211.387420654297 + }, + { + "x": -288.265930175781, + "y": 213.981170654297 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -250.296142578125, + "y": 204.905670166016 + }, + { + "x": -184.155700683594, + "y": 205.683792114258 + }, + { + "x": -181.561965942383, + "y": 208.536911010742 + }, + { + "x": -182.340103149414, + "y": 228.24934387207 + }, + { + "x": -185.452575683594, + "y": 230.843078613281 + }, + { + "x": -251.593017578125, + "y": 229.805587768555 + }, + { + "x": -254.186752319336, + "y": 226.693099975586 + }, + { + "x": -253.408615112305, + "y": 207.499404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -248.999267578125, + "y": 207.758773803711 + }, + { + "x": -186.749450683594, + "y": 207.758773803711 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -248.999267578125, + "y": 228.24934387207 + }, + { + "x": -184.155700683594, + "y": 228.24934387207 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -243.682083129883, + "y": 215.923889160156 + }, + { + "x": -233.307113647461, + "y": 214.108245849609 + }, + { + "x": -222.932144165039, + "y": 216.701995849609 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -172.483871459961, + "y": 204.905670166016 + }, + { + "x": -106.343437194824, + "y": 205.683792114258 + }, + { + "x": -103.749694824219, + "y": 208.536911010742 + }, + { + "x": -104.527816772461, + "y": 228.24934387207 + }, + { + "x": -107.64030456543, + "y": 230.843078613281 + }, + { + "x": -173.780731201172, + "y": 229.805587768555 + }, + { + "x": -176.374481201172, + "y": 226.693099975586 + }, + { + "x": -175.596343994141, + "y": 207.499404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -171.186996459961, + "y": 207.758773803711 + }, + { + "x": -108.93717956543, + "y": 207.758773803711 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -171.186996459961, + "y": 228.24934387207 + }, + { + "x": -106.343437194824, + "y": 228.24934387207 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -164.007507324219, + "y": 219.94677734375 + }, + { + "x": -153.632537841797, + "y": 218.131149291992 + }, + { + "x": -143.257568359375, + "y": 220.724899291992 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -94.6716003417969, + "y": 204.905670166016 + }, + { + "x": -28.5311660766602, + "y": 205.683792114258 + }, + { + "x": -25.9374237060547, + "y": 208.536911010742 + }, + { + "x": -26.7155475616455, + "y": 228.24934387207 + }, + { + "x": -29.8280372619629, + "y": 230.843078613281 + }, + { + "x": -95.9684677124023, + "y": 229.805587768555 + }, + { + "x": -98.5622100830078, + "y": 226.693099975586 + }, + { + "x": -97.7840881347656, + "y": 207.499404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -93.3747253417969, + "y": 207.758773803711 + }, + { + "x": -31.1249084472656, + "y": 207.758773803711 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -93.3747253417969, + "y": 228.24934387207 + }, + { + "x": -28.5311660766602, + "y": 228.24934387207 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -85.510498046875, + "y": 220.735260009766 + }, + { + "x": -75.1355285644531, + "y": 218.919647216797 + }, + { + "x": -64.7605590820313, + "y": 221.513397216797 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -16.8593254089355, + "y": 204.905670166016 + }, + { + "x": 49.2811050415039, + "y": 205.683792114258 + }, + { + "x": 51.8748474121094, + "y": 208.536911010742 + }, + { + "x": 51.0967254638672, + "y": 228.24934387207 + }, + { + "x": 47.9842338562012, + "y": 230.843078613281 + }, + { + "x": -18.1561965942383, + "y": 229.805587768555 + }, + { + "x": -20.7499389648438, + "y": 226.693099975586 + }, + { + "x": -19.9718151092529, + "y": 207.499404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -15.5624542236328, + "y": 207.758773803711 + }, + { + "x": 46.6873626708984, + "y": 207.758773803711 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -15.5624542236328, + "y": 228.24934387207 + }, + { + "x": 49.2811050415039, + "y": 228.24934387207 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -0.962278425693512, + "y": 215.757873535156 + }, + { + "x": 9.41269111633301, + "y": 213.942260742188 + }, + { + "x": 19.7876605987549, + "y": 216.536010742188 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 60.9529457092285, + "y": 204.905670166016 + }, + { + "x": 127.093376159668, + "y": 205.683792114258 + }, + { + "x": 129.687118530273, + "y": 208.536911010742 + }, + { + "x": 128.908996582031, + "y": 228.24934387207 + }, + { + "x": 125.796508789063, + "y": 230.843078613281 + }, + { + "x": 59.6560745239258, + "y": 229.805587768555 + }, + { + "x": 57.0623321533203, + "y": 226.693099975586 + }, + { + "x": 57.8404541015625, + "y": 207.499404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 62.2498168945313, + "y": 207.758773803711 + }, + { + "x": 124.499633789063, + "y": 207.758773803711 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 62.2498168945313, + "y": 228.24934387207 + }, + { + "x": 127.093376159668, + "y": 228.24934387207 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 77.5036163330078, + "y": 223.201919555664 + }, + { + "x": 87.8785858154297, + "y": 221.386291503906 + }, + { + "x": 98.2535552978516, + "y": 223.980041503906 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 138.765213012695, + "y": 204.905670166016 + }, + { + "x": 204.905639648438, + "y": 205.683792114258 + }, + { + "x": 207.499389648438, + "y": 208.536911010742 + }, + { + "x": 206.721252441406, + "y": 228.24934387207 + }, + { + "x": 203.608779907227, + "y": 230.843078613281 + }, + { + "x": 137.468353271484, + "y": 229.805587768555 + }, + { + "x": 134.874603271484, + "y": 226.693099975586 + }, + { + "x": 135.652725219727, + "y": 207.499404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 140.062088012695, + "y": 207.758773803711 + }, + { + "x": 202.311904907227, + "y": 207.758773803711 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 140.062088012695, + "y": 228.24934387207 + }, + { + "x": 204.905639648438, + "y": 228.24934387207 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 142.977462768555, + "y": 222.239639282227 + }, + { + "x": 153.352432250977, + "y": 220.424026489258 + }, + { + "x": 163.727401733398, + "y": 223.017776489258 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 216.577484130859, + "y": 204.905670166016 + }, + { + "x": 282.717926025391, + "y": 205.683792114258 + }, + { + "x": 285.311645507813, + "y": 208.536911010742 + }, + { + "x": 284.533538818359, + "y": 228.24934387207 + }, + { + "x": 281.421051025391, + "y": 230.843078613281 + }, + { + "x": 215.280609130859, + "y": 229.805587768555 + }, + { + "x": 212.686874389648, + "y": 226.693099975586 + }, + { + "x": 213.46501159668, + "y": 207.499404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 217.874359130859, + "y": 207.758773803711 + }, + { + "x": 280.124176025391, + "y": 207.758773803711 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 217.874359130859, + "y": 228.24934387207 + }, + { + "x": 282.717926025391, + "y": 228.24934387207 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 230.614822387695, + "y": 218.027404785156 + }, + { + "x": 240.989791870117, + "y": 216.211791992188 + }, + { + "x": 251.364761352539, + "y": 218.805526733398 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 294.389770507813, + "y": 204.905670166016 + }, + { + "x": 326.8115234375, + "y": 205.683792114258 + }, + { + "x": 329.4052734375, + "y": 208.536911010742 + }, + { + "x": 328.627136230469, + "y": 228.24934387207 + }, + { + "x": 325.514678955078, + "y": 230.843078613281 + }, + { + "x": 293.092895507813, + "y": 229.805587768555 + }, + { + "x": 290.499145507813, + "y": 226.693099975586 + }, + { + "x": 291.277282714844, + "y": 207.499404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 295.686645507813, + "y": 207.758773803711 + }, + { + "x": 324.217803955078, + "y": 207.758773803711 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 295.686645507813, + "y": 228.24934387207 + }, + { + "x": 326.8115234375, + "y": 228.24934387207 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 309.4853515625, + "y": 223.201919555664 + }, + { + "x": 319.860321044922, + "y": 221.386291503906 + }, + { + "x": 324.217803955078, + "y": 223.980041503906 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -325.514678955078, + "y": 236.030578613281 + }, + { + "x": -306.061584472656, + "y": 236.808700561523 + }, + { + "x": -303.467864990234, + "y": 239.661819458008 + }, + { + "x": -304.246002197266, + "y": 251.593017578125 + }, + { + "x": -307.358459472656, + "y": 254.186767578125 + }, + { + "x": -326.8115234375, + "y": 253.149261474609 + }, + { + "x": -329.4052734375, + "y": 250.036773681641 + }, + { + "x": -328.627136230469, + "y": 238.624313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -324.217803955078, + "y": 238.883682250977 + }, + { + "x": -308.655334472656, + "y": 238.883682250977 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -324.217803955078, + "y": 251.593017578125 + }, + { + "x": -306.061584472656, + "y": 251.593017578125 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -294.389770507813, + "y": 236.030578613281 + }, + { + "x": -228.249328613281, + "y": 236.808700561523 + }, + { + "x": -225.655578613281, + "y": 239.661819458008 + }, + { + "x": -226.433715820313, + "y": 251.593017578125 + }, + { + "x": -229.546203613281, + "y": 254.186767578125 + }, + { + "x": -295.686645507813, + "y": 253.149261474609 + }, + { + "x": -298.280364990234, + "y": 250.036773681641 + }, + { + "x": -297.502227783203, + "y": 238.624313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -293.092895507813, + "y": 238.883682250977 + }, + { + "x": -230.843078613281, + "y": 238.883682250977 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -293.092895507813, + "y": 251.593017578125 + }, + { + "x": -228.249328613281, + "y": 251.593017578125 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -280.28759765625, + "y": 244.3720703125 + }, + { + "x": -269.912628173828, + "y": 242.556427001953 + }, + { + "x": -259.537658691406, + "y": 245.150177001953 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -216.577484130859, + "y": 236.030578613281 + }, + { + "x": -150.437057495117, + "y": 236.808700561523 + }, + { + "x": -147.843322753906, + "y": 239.661819458008 + }, + { + "x": -148.621429443359, + "y": 251.593017578125 + }, + { + "x": -151.733932495117, + "y": 254.186767578125 + }, + { + "x": -217.874359130859, + "y": 253.149261474609 + }, + { + "x": -220.468109130859, + "y": 250.036773681641 + }, + { + "x": -219.689971923828, + "y": 238.624313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -215.280609130859, + "y": 238.883682250977 + }, + { + "x": -153.030792236328, + "y": 238.883682250977 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -215.280609130859, + "y": 251.593017578125 + }, + { + "x": -150.437057495117, + "y": 251.593017578125 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -212.183685302734, + "y": 246.252502441406 + }, + { + "x": -201.808715820313, + "y": 244.436889648438 + }, + { + "x": -191.433746337891, + "y": 247.030639648438 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -138.765213012695, + "y": 236.030578613281 + }, + { + "x": -72.6247863769531, + "y": 236.808700561523 + }, + { + "x": -70.0310440063477, + "y": 239.661819458008 + }, + { + "x": -70.8091659545898, + "y": 251.593017578125 + }, + { + "x": -73.9216613769531, + "y": 254.186767578125 + }, + { + "x": -140.062088012695, + "y": 253.149261474609 + }, + { + "x": -142.655822753906, + "y": 250.036773681641 + }, + { + "x": -141.877716064453, + "y": 238.624313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -137.468353271484, + "y": 238.883682250977 + }, + { + "x": -75.2185287475586, + "y": 238.883682250977 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -137.468353271484, + "y": 251.593017578125 + }, + { + "x": -72.6247863769531, + "y": 251.593017578125 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -124.284355163574, + "y": 246.200622558594 + }, + { + "x": -113.909385681152, + "y": 244.385009765625 + }, + { + "x": -103.53441619873, + "y": 246.978759765625 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -60.9529457092285, + "y": 236.030578613281 + }, + { + "x": 5.18748474121094, + "y": 236.808700561523 + }, + { + "x": 7.78122711181641, + "y": 239.661819458008 + }, + { + "x": 7.00310468673706, + "y": 251.593017578125 + }, + { + "x": 3.8906135559082, + "y": 254.186767578125 + }, + { + "x": -62.2498168945313, + "y": 253.149261474609 + }, + { + "x": -64.8435592651367, + "y": 250.036773681641 + }, + { + "x": -64.0654373168945, + "y": 238.624313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -59.6560745239258, + "y": 238.883682250977 + }, + { + "x": 2.59374237060547, + "y": 238.883682250977 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -59.6560745239258, + "y": 251.593017578125 + }, + { + "x": 5.18748474121094, + "y": 251.593017578125 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -46.4202041625977, + "y": 248.332672119141 + }, + { + "x": -36.045238494873, + "y": 246.517059326172 + }, + { + "x": -25.6702690124512, + "y": 249.110809326172 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 16.8593254089355, + "y": 236.030578613281 + }, + { + "x": 82.999755859375, + "y": 236.808700561523 + }, + { + "x": 85.5934982299805, + "y": 239.661819458008 + }, + { + "x": 84.8153762817383, + "y": 251.593017578125 + }, + { + "x": 81.702880859375, + "y": 254.186767578125 + }, + { + "x": 15.5624542236328, + "y": 253.149261474609 + }, + { + "x": 12.9687118530273, + "y": 250.036773681641 + }, + { + "x": 13.7468347549438, + "y": 238.624313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 18.1561965942383, + "y": 238.883682250977 + }, + { + "x": 80.4060134887695, + "y": 238.883682250977 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 18.1561965942383, + "y": 251.593017578125 + }, + { + "x": 82.999755859375, + "y": 251.593017578125 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 21.0559997558594, + "y": 244.81298828125 + }, + { + "x": 31.4309692382813, + "y": 242.997344970703 + }, + { + "x": 41.8059387207031, + "y": 245.591094970703 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 94.6716003417969, + "y": 236.030578613281 + }, + { + "x": 160.812026977539, + "y": 236.808700561523 + }, + { + "x": 163.40576171875, + "y": 239.661819458008 + }, + { + "x": 162.627655029297, + "y": 251.593017578125 + }, + { + "x": 159.515151977539, + "y": 254.186767578125 + }, + { + "x": 93.3747253417969, + "y": 253.149261474609 + }, + { + "x": 90.7809829711914, + "y": 250.036773681641 + }, + { + "x": 91.5591049194336, + "y": 238.624313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 95.9684677124023, + "y": 238.883682250977 + }, + { + "x": 158.218292236328, + "y": 238.883682250977 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 95.9684677124023, + "y": 251.593017578125 + }, + { + "x": 160.812026977539, + "y": 251.593017578125 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 102.175300598145, + "y": 244.172332763672 + }, + { + "x": 112.550270080566, + "y": 242.356719970703 + }, + { + "x": 122.925239562988, + "y": 244.950469970703 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 172.483871459961, + "y": 236.030578613281 + }, + { + "x": 238.624298095703, + "y": 236.808700561523 + }, + { + "x": 241.218048095703, + "y": 239.661819458008 + }, + { + "x": 240.439910888672, + "y": 251.593017578125 + }, + { + "x": 237.327423095703, + "y": 254.186767578125 + }, + { + "x": 171.186996459961, + "y": 253.149261474609 + }, + { + "x": 168.59326171875, + "y": 250.036773681641 + }, + { + "x": 169.371383666992, + "y": 238.624313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 173.780731201172, + "y": 238.883682250977 + }, + { + "x": 236.030548095703, + "y": 238.883682250977 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 173.780731201172, + "y": 251.593017578125 + }, + { + "x": 238.624298095703, + "y": 251.593017578125 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 179.995361328125, + "y": 247.139556884766 + }, + { + "x": 190.370330810547, + "y": 245.323944091797 + }, + { + "x": 200.745300292969, + "y": 247.917694091797 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 250.296142578125, + "y": 236.030578613281 + }, + { + "x": 316.436584472656, + "y": 236.808700561523 + }, + { + "x": 319.030303955078, + "y": 239.661819458008 + }, + { + "x": 318.252166748047, + "y": 251.593017578125 + }, + { + "x": 315.139709472656, + "y": 254.186767578125 + }, + { + "x": 248.999267578125, + "y": 253.149261474609 + }, + { + "x": 246.405517578125, + "y": 250.036773681641 + }, + { + "x": 247.183654785156, + "y": 238.624313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 251.593017578125, + "y": 238.883682250977 + }, + { + "x": 313.842834472656, + "y": 238.883682250977 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 251.593017578125, + "y": 251.593017578125 + }, + { + "x": 316.436584472656, + "y": 251.593017578125 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 263.000305175781, + "y": 248.187438964844 + }, + { + "x": 273.375274658203, + "y": 246.371826171875 + }, + { + "x": 283.750244140625, + "y": 248.965576171875 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#241B18", + "outline_color": "#523B2D", + "outline_width": 5.1874849202, + "points": [ + { + "x": -199.718170166016, + "y": 254.186767578125 + }, + { + "x": -199.718170166016, + "y": 15.5624694824219 + }, + { + "x": -199.718170166016, + "y": 15.5624694824219 + }, + { + "x": -199.103439331055, + "y": 3.5560359954834 + }, + { + "x": -197.259292602539, + "y": -8.37777328491211 + }, + { + "x": -194.198684692383, + "y": -20.1611442565918 + }, + { + "x": -189.942352294922, + "y": -31.7266407012939 + }, + { + "x": -184.516235351563, + "y": -42.9990463256836 + }, + { + "x": -177.951477050781, + "y": -53.9109191894531 + }, + { + "x": -170.286972045898, + "y": -64.3948211669922 + }, + { + "x": -161.574584960938, + "y": -74.3859252929688 + }, + { + "x": -151.8662109375, + "y": -83.8219528198242 + }, + { + "x": -141.221481323242, + "y": -92.645866394043 + }, + { + "x": -129.705276489258, + "y": -100.803184509277 + }, + { + "x": -117.390182495117, + "y": -108.242034912109 + }, + { + "x": -104.351440429688, + "y": -114.918334960938 + }, + { + "x": -90.6694564819336, + "y": -120.787971496582 + }, + { + "x": -76.429801940918, + "y": -125.819831848145 + }, + { + "x": -61.7155075073242, + "y": -129.977600097656 + }, + { + "x": -46.6225204467773, + "y": -133.240524291992 + }, + { + "x": -31.241626739502, + "y": -135.585266113281 + }, + { + "x": -15.668797492981, + "y": -136.996276855469 + }, + { + "x": 0.0, + "y": -137.468322753906 + }, + { + "x": 15.668797492981, + "y": -136.996276855469 + }, + { + "x": 31.241626739502, + "y": -135.585266113281 + }, + { + "x": 46.6225204467773, + "y": -133.240524291992 + }, + { + "x": 61.7155075073242, + "y": -129.977600097656 + }, + { + "x": 76.429801940918, + "y": -125.819831848145 + }, + { + "x": 90.6694564819336, + "y": -120.787971496582 + }, + { + "x": 104.351440429688, + "y": -114.918334960938 + }, + { + "x": 117.390182495117, + "y": -108.242034912109 + }, + { + "x": 129.705276489258, + "y": -100.803184509277 + }, + { + "x": 141.221481323242, + "y": -92.645866394043 + }, + { + "x": 151.8662109375, + "y": -83.8219528198242 + }, + { + "x": 161.574584960938, + "y": -74.3859252929688 + }, + { + "x": 170.286972045898, + "y": -64.3948211669922 + }, + { + "x": 177.951477050781, + "y": -53.9109191894531 + }, + { + "x": 184.516235351563, + "y": -42.9990463256836 + }, + { + "x": 189.942352294922, + "y": -31.7266407012939 + }, + { + "x": 194.198684692383, + "y": -20.1611442565918 + }, + { + "x": 197.259292602539, + "y": -8.37777328491211 + }, + { + "x": 199.103439331055, + "y": 3.5560359954834 + }, + { + "x": 199.718170166016, + "y": 15.5624694824219 + }, + { + "x": 199.718170166016, + "y": 254.186767578125 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -82.999755859375, + "y": -98.5621948242188 + }, + { + "x": -82.999755859375, + "y": -123.498428344727 + }, + { + "x": -90.6694564819336, + "y": -120.787971496582 + }, + { + "x": -104.351440429688, + "y": -114.918334960938 + }, + { + "x": -117.390182495117, + "y": -108.242034912109 + }, + { + "x": -129.705276489258, + "y": -100.803184509277 + }, + { + "x": -132.869644165039, + "y": -98.5621948242188 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -77.8122711181641, + "y": -98.5621948242188 + }, + { + "x": 0.0, + "y": -98.5621948242188 + }, + { + "x": 0.0, + "y": -127.093360900879 + }, + { + "x": -71.9218826293945, + "y": -127.093360900879 + }, + { + "x": -76.429801940918, + "y": -125.819831848145 + }, + { + "x": -77.8122711181641, + "y": -125.329612731934 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 5.18748474121094, + "y": -127.093360900879 + }, + { + "x": 5.18748474121094, + "y": -98.5621948242188 + }, + { + "x": 82.999755859375, + "y": -98.5621948242188 + }, + { + "x": 82.999755859375, + "y": -123.498428344727 + }, + { + "x": 76.429801940918, + "y": -125.819831848145 + }, + { + "x": 71.9218826293945, + "y": -127.093360900879 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 88.1872406005859, + "y": -98.5621948242188 + }, + { + "x": 132.869644165039, + "y": -98.5621948242188 + }, + { + "x": 129.705276489258, + "y": -100.803184509277 + }, + { + "x": 117.390182495117, + "y": -108.242034912109 + }, + { + "x": 104.351440429688, + "y": -114.918334960938 + }, + { + "x": 90.6694564819336, + "y": -120.787971496582 + }, + { + "x": 88.1872406005859, + "y": -121.667251586914 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -124.499633789063, + "y": -64.8435440063477 + }, + { + "x": -124.499633789063, + "y": -93.3747100830078 + }, + { + "x": -140.194366455078, + "y": -93.3747100830078 + }, + { + "x": -141.221481323242, + "y": -92.645866394043 + }, + { + "x": -151.8662109375, + "y": -83.8219528198242 + }, + { + "x": -161.574584960938, + "y": -74.3859252929688 + }, + { + "x": -169.897903442383, + "y": -64.8435440063477 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -119.312149047852, + "y": -93.3747100830078 + }, + { + "x": -119.312149047852, + "y": -64.8435440063477 + }, + { + "x": -41.4998779296875, + "y": -64.8435440063477 + }, + { + "x": -41.4998779296875, + "y": -93.3747100830078 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -36.3123931884766, + "y": -93.3747100830078 + }, + { + "x": -36.3123931884766, + "y": -64.8435440063477 + }, + { + "x": 41.4998779296875, + "y": -64.8435440063477 + }, + { + "x": 41.4998779296875, + "y": -93.3747100830078 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 46.6873626708984, + "y": -93.3747100830078 + }, + { + "x": 46.6873626708984, + "y": -64.8435440063477 + }, + { + "x": 124.499633789063, + "y": -64.8435440063477 + }, + { + "x": 124.499633789063, + "y": -93.3747100830078 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 129.687118530273, + "y": -93.3747100830078 + }, + { + "x": 129.687118530273, + "y": -64.8435440063477 + }, + { + "x": 169.897903442383, + "y": -64.8435440063477 + }, + { + "x": 161.574584960938, + "y": -74.3859252929688 + }, + { + "x": 151.8662109375, + "y": -83.8219528198242 + }, + { + "x": 141.221481323242, + "y": -92.645866394043 + }, + { + "x": 140.194366455078, + "y": -93.3747100830078 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -165.99951171875, + "y": -31.1248931884766 + }, + { + "x": -165.99951171875, + "y": -59.6560592651367 + }, + { + "x": -173.752197265625, + "y": -59.6560592651367 + }, + { + "x": -177.951477050781, + "y": -53.9109191894531 + }, + { + "x": -184.516235351563, + "y": -42.9990463256836 + }, + { + "x": -189.942352294922, + "y": -31.7266407012939 + }, + { + "x": -190.165405273438, + "y": -31.1248931884766 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -160.812026977539, + "y": -59.6560592651367 + }, + { + "x": -160.812026977539, + "y": -31.1248931884766 + }, + { + "x": -82.999755859375, + "y": -31.1248931884766 + }, + { + "x": -82.999755859375, + "y": -59.6560592651367 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -77.8122711181641, + "y": -59.6560592651367 + }, + { + "x": -77.8122711181641, + "y": -31.1248931884766 + }, + { + "x": 0.0, + "y": -31.1248931884766 + }, + { + "x": 0.0, + "y": -59.6560592651367 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 5.18748474121094, + "y": -59.6560592651367 + }, + { + "x": 5.18748474121094, + "y": -31.1248931884766 + }, + { + "x": 82.999755859375, + "y": -31.1248931884766 + }, + { + "x": 82.999755859375, + "y": -59.6560592651367 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 88.1872406005859, + "y": -59.6560592651367 + }, + { + "x": 88.1872406005859, + "y": -31.1248931884766 + }, + { + "x": 165.99951171875, + "y": -31.1248931884766 + }, + { + "x": 165.99951171875, + "y": -59.6560592651367 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 171.186996459961, + "y": -59.6560592651367 + }, + { + "x": 171.186996459961, + "y": -31.1248931884766 + }, + { + "x": 190.165405273438, + "y": -31.1248931884766 + }, + { + "x": 189.942352294922, + "y": -31.7266407012939 + }, + { + "x": 184.516235351563, + "y": -42.9990463256836 + }, + { + "x": 177.951477050781, + "y": -53.9109191894531 + }, + { + "x": 173.752197265625, + "y": -59.6560592651367 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -124.499633789063, + "y": 2.59375762939453 + }, + { + "x": -124.499633789063, + "y": -25.9374084472656 + }, + { + "x": -192.074401855469, + "y": -25.9374084472656 + }, + { + "x": -194.198684692383, + "y": -20.1611442565918 + }, + { + "x": -197.259292602539, + "y": -8.37777328491211 + }, + { + "x": -198.953018188477, + "y": 2.59375762939453 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -119.312149047852, + "y": -25.9374084472656 + }, + { + "x": -119.312149047852, + "y": 2.59375762939453 + }, + { + "x": -41.4998779296875, + "y": 2.59375762939453 + }, + { + "x": -41.4998779296875, + "y": -25.9374084472656 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -36.3123931884766, + "y": -25.9374084472656 + }, + { + "x": -36.3123931884766, + "y": 2.59375762939453 + }, + { + "x": 41.4998779296875, + "y": 2.59375762939453 + }, + { + "x": 41.4998779296875, + "y": -25.9374084472656 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 46.6873626708984, + "y": -25.9374084472656 + }, + { + "x": 46.6873626708984, + "y": 2.59375762939453 + }, + { + "x": 124.499633789063, + "y": 2.59375762939453 + }, + { + "x": 124.499633789063, + "y": -25.9374084472656 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 129.687118530273, + "y": -25.9374084472656 + }, + { + "x": 129.687118530273, + "y": 2.59375762939453 + }, + { + "x": 198.953018188477, + "y": 2.59375762939453 + }, + { + "x": 197.259292602539, + "y": -8.37777328491211 + }, + { + "x": 194.198684692383, + "y": -20.1611442565918 + }, + { + "x": 192.074401855469, + "y": -25.9374084472656 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -165.99951171875, + "y": 36.3124084472656 + }, + { + "x": -165.99951171875, + "y": 7.78124237060547 + }, + { + "x": -199.318725585938, + "y": 7.78124237060547 + }, + { + "x": -199.718170166016, + "y": 15.5624694824219 + }, + { + "x": -199.718170166016, + "y": 15.5624694824219 + }, + { + "x": -199.718170166016, + "y": 36.3124084472656 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -160.812026977539, + "y": 7.78124237060547 + }, + { + "x": -160.812026977539, + "y": 36.3124084472656 + }, + { + "x": -82.999755859375, + "y": 36.3124084472656 + }, + { + "x": -82.999755859375, + "y": 7.78124237060547 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -77.8122711181641, + "y": 7.78124237060547 + }, + { + "x": -77.8122711181641, + "y": 36.3124084472656 + }, + { + "x": 0.0, + "y": 36.3124084472656 + }, + { + "x": 0.0, + "y": 7.78124237060547 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 5.18748474121094, + "y": 7.78124237060547 + }, + { + "x": 5.18748474121094, + "y": 36.3124084472656 + }, + { + "x": 82.999755859375, + "y": 36.3124084472656 + }, + { + "x": 82.999755859375, + "y": 7.78124237060547 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 88.1872406005859, + "y": 7.78124237060547 + }, + { + "x": 88.1872406005859, + "y": 36.3124084472656 + }, + { + "x": 165.99951171875, + "y": 36.3124084472656 + }, + { + "x": 165.99951171875, + "y": 7.78124237060547 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 171.186996459961, + "y": 7.78124237060547 + }, + { + "x": 171.186996459961, + "y": 36.3124084472656 + }, + { + "x": 199.718170166016, + "y": 36.3124084472656 + }, + { + "x": 199.718170166016, + "y": 15.5624694824219 + }, + { + "x": 199.318725585938, + "y": 7.78124237060547 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -124.499633789063, + "y": 70.0310592651367 + }, + { + "x": -124.499633789063, + "y": 41.4998931884766 + }, + { + "x": -199.718170166016, + "y": 41.4998931884766 + }, + { + "x": -199.718170166016, + "y": 70.0310592651367 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -119.312149047852, + "y": 41.4998931884766 + }, + { + "x": -119.312149047852, + "y": 70.0310592651367 + }, + { + "x": -41.4998779296875, + "y": 70.0310592651367 + }, + { + "x": -41.4998779296875, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -36.3123931884766, + "y": 41.4998931884766 + }, + { + "x": -36.3123931884766, + "y": 70.0310592651367 + }, + { + "x": 41.4998779296875, + "y": 70.0310592651367 + }, + { + "x": 41.4998779296875, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 46.6873626708984, + "y": 41.4998931884766 + }, + { + "x": 46.6873626708984, + "y": 70.0310592651367 + }, + { + "x": 124.499633789063, + "y": 70.0310592651367 + }, + { + "x": 124.499633789063, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 129.687118530273, + "y": 41.4998931884766 + }, + { + "x": 129.687118530273, + "y": 70.0310592651367 + }, + { + "x": 199.718170166016, + "y": 70.0310592651367 + }, + { + "x": 199.718170166016, + "y": 41.4998931884766 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -165.99951171875, + "y": 103.749710083008 + }, + { + "x": -165.99951171875, + "y": 75.2185440063477 + }, + { + "x": -199.718170166016, + "y": 75.2185440063477 + }, + { + "x": -199.718170166016, + "y": 103.749710083008 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -160.812026977539, + "y": 75.2185440063477 + }, + { + "x": -160.812026977539, + "y": 103.749710083008 + }, + { + "x": -82.999755859375, + "y": 103.749710083008 + }, + { + "x": -82.999755859375, + "y": 75.2185440063477 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -77.8122711181641, + "y": 75.2185440063477 + }, + { + "x": -77.8122711181641, + "y": 103.749710083008 + }, + { + "x": 0.0, + "y": 103.749710083008 + }, + { + "x": 0.0, + "y": 75.2185440063477 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 5.18748474121094, + "y": 75.2185440063477 + }, + { + "x": 5.18748474121094, + "y": 103.749710083008 + }, + { + "x": 82.999755859375, + "y": 103.749710083008 + }, + { + "x": 82.999755859375, + "y": 75.2185440063477 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 88.1872406005859, + "y": 75.2185440063477 + }, + { + "x": 88.1872406005859, + "y": 103.749710083008 + }, + { + "x": 165.99951171875, + "y": 103.749710083008 + }, + { + "x": 165.99951171875, + "y": 75.2185440063477 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 171.186996459961, + "y": 75.2185440063477 + }, + { + "x": 171.186996459961, + "y": 103.749710083008 + }, + { + "x": 199.718170166016, + "y": 103.749710083008 + }, + { + "x": 199.718170166016, + "y": 75.2185440063477 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -124.499633789063, + "y": 137.468353271484 + }, + { + "x": -124.499633789063, + "y": 108.937194824219 + }, + { + "x": -199.718170166016, + "y": 108.937194824219 + }, + { + "x": -199.718170166016, + "y": 137.468353271484 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -119.312149047852, + "y": 108.937194824219 + }, + { + "x": -119.312149047852, + "y": 137.468353271484 + }, + { + "x": -41.4998779296875, + "y": 137.468353271484 + }, + { + "x": -41.4998779296875, + "y": 108.937194824219 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -36.3123931884766, + "y": 108.937194824219 + }, + { + "x": -36.3123931884766, + "y": 137.468353271484 + }, + { + "x": 41.4998779296875, + "y": 137.468353271484 + }, + { + "x": 41.4998779296875, + "y": 108.937194824219 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 46.6873626708984, + "y": 108.937194824219 + }, + { + "x": 46.6873626708984, + "y": 137.468353271484 + }, + { + "x": 124.499633789063, + "y": 137.468353271484 + }, + { + "x": 124.499633789063, + "y": 108.937194824219 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 129.687118530273, + "y": 108.937194824219 + }, + { + "x": 129.687118530273, + "y": 137.468353271484 + }, + { + "x": 199.718170166016, + "y": 137.468353271484 + }, + { + "x": 199.718170166016, + "y": 108.937194824219 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -165.99951171875, + "y": 171.18701171875 + }, + { + "x": -165.99951171875, + "y": 142.655853271484 + }, + { + "x": -199.718170166016, + "y": 142.655853271484 + }, + { + "x": -199.718170166016, + "y": 171.18701171875 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -160.812026977539, + "y": 142.655853271484 + }, + { + "x": -160.812026977539, + "y": 171.18701171875 + }, + { + "x": -82.999755859375, + "y": 171.18701171875 + }, + { + "x": -82.999755859375, + "y": 142.655853271484 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -77.8122711181641, + "y": 142.655853271484 + }, + { + "x": -77.8122711181641, + "y": 171.18701171875 + }, + { + "x": 0.0, + "y": 171.18701171875 + }, + { + "x": 0.0, + "y": 142.655853271484 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 5.18748474121094, + "y": 142.655853271484 + }, + { + "x": 5.18748474121094, + "y": 171.18701171875 + }, + { + "x": 82.999755859375, + "y": 171.18701171875 + }, + { + "x": 82.999755859375, + "y": 142.655853271484 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 88.1872406005859, + "y": 142.655853271484 + }, + { + "x": 88.1872406005859, + "y": 171.18701171875 + }, + { + "x": 165.99951171875, + "y": 171.18701171875 + }, + { + "x": 165.99951171875, + "y": 142.655853271484 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 171.186996459961, + "y": 142.655853271484 + }, + { + "x": 171.186996459961, + "y": 171.18701171875 + }, + { + "x": 199.718170166016, + "y": 171.18701171875 + }, + { + "x": 199.718170166016, + "y": 142.655853271484 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -124.499633789063, + "y": 204.905670166016 + }, + { + "x": -124.499633789063, + "y": 176.374496459961 + }, + { + "x": -199.718170166016, + "y": 176.374496459961 + }, + { + "x": -199.718170166016, + "y": 204.905670166016 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -119.312149047852, + "y": 176.374496459961 + }, + { + "x": -119.312149047852, + "y": 204.905670166016 + }, + { + "x": -41.4998779296875, + "y": 204.905670166016 + }, + { + "x": -41.4998779296875, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -36.3123931884766, + "y": 176.374496459961 + }, + { + "x": -36.3123931884766, + "y": 204.905670166016 + }, + { + "x": 41.4998779296875, + "y": 204.905670166016 + }, + { + "x": 41.4998779296875, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 46.6873626708984, + "y": 176.374496459961 + }, + { + "x": 46.6873626708984, + "y": 204.905670166016 + }, + { + "x": 124.499633789063, + "y": 204.905670166016 + }, + { + "x": 124.499633789063, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 129.687118530273, + "y": 176.374496459961 + }, + { + "x": 129.687118530273, + "y": 204.905670166016 + }, + { + "x": 199.718170166016, + "y": 204.905670166016 + }, + { + "x": 199.718170166016, + "y": 176.374496459961 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -165.99951171875, + "y": 238.624313354492 + }, + { + "x": -165.99951171875, + "y": 210.093139648438 + }, + { + "x": -199.718170166016, + "y": 210.093139648438 + }, + { + "x": -199.718170166016, + "y": 238.624313354492 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -160.812026977539, + "y": 210.093139648438 + }, + { + "x": -160.812026977539, + "y": 238.624313354492 + }, + { + "x": -82.999755859375, + "y": 238.624313354492 + }, + { + "x": -82.999755859375, + "y": 210.093139648438 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -77.8122711181641, + "y": 210.093139648438 + }, + { + "x": -77.8122711181641, + "y": 238.624313354492 + }, + { + "x": 0.0, + "y": 238.624313354492 + }, + { + "x": 0.0, + "y": 210.093139648438 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#35251F", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 5.18748474121094, + "y": 210.093139648438 + }, + { + "x": 5.18748474121094, + "y": 238.624313354492 + }, + { + "x": 82.999755859375, + "y": 238.624313354492 + }, + { + "x": 82.999755859375, + "y": 210.093139648438 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 88.1872406005859, + "y": 210.093139648438 + }, + { + "x": 88.1872406005859, + "y": 238.624313354492 + }, + { + "x": 165.99951171875, + "y": 238.624313354492 + }, + { + "x": 165.99951171875, + "y": 210.093139648438 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 171.186996459961, + "y": 210.093139648438 + }, + { + "x": 171.186996459961, + "y": 238.624313354492 + }, + { + "x": 199.718170166016, + "y": 238.624313354492 + }, + { + "x": 199.718170166016, + "y": 210.093139648438 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -124.499633789063, + "y": 243.811798095703 + }, + { + "x": -199.718170166016, + "y": 243.811798095703 + }, + { + "x": -199.718170166016, + "y": 254.186767578125 + }, + { + "x": -124.499633789063, + "y": 254.186767578125 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": -41.4998779296875, + "y": 243.811798095703 + }, + { + "x": -119.312149047852, + "y": 243.811798095703 + }, + { + "x": -119.312149047852, + "y": 254.186767578125 + }, + { + "x": -41.4998779296875, + "y": 254.186767578125 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#3B2921", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 41.4998779296875, + "y": 243.811798095703 + }, + { + "x": -36.3123931884766, + "y": 243.811798095703 + }, + { + "x": -36.3123931884766, + "y": 254.186767578125 + }, + { + "x": 41.4998779296875, + "y": 254.186767578125 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#412B21", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 124.499633789063, + "y": 243.811798095703 + }, + { + "x": 46.6873626708984, + "y": 243.811798095703 + }, + { + "x": 46.6873626708984, + "y": 254.186767578125 + }, + { + "x": 124.499633789063, + "y": 254.186767578125 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#483025", + "outline_color": "#201A17", + "outline_width": 1.685932599065, + "points": [ + { + "x": 129.687118530273, + "y": 243.811798095703 + }, + { + "x": 129.687118530273, + "y": 254.186767578125 + }, + { + "x": 199.718170166016, + "y": 254.186767578125 + }, + { + "x": 199.718170166016, + "y": 243.811798095703 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": -204.890090942383, + "y": 13.669038772583 + }, + { + "x": -248.981094360352, + "y": 13.2618198394775 + }, + { + "x": -242.460433959961, + "y": -28.0850276947021 + }, + { + "x": -199.526229858398, + "y": -20.3556747436523 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": -198.348663330078, + "y": -24.0310077667236 + }, + { + "x": -241.031295776367, + "y": -32.5514526367188 + }, + { + "x": -221.850555419922, + "y": -71.493896484375 + }, + { + "x": -182.56575012207, + "y": -56.079288482666 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": -180.278060913086, + "y": -59.4304046630859 + }, + { + "x": -219.072662353516, + "y": -75.5686645507813 + }, + { + "x": -188.347183227539, + "y": -109.84757232666 + }, + { + "x": -154.994262695313, + "y": -87.6373519897461 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": -151.731338500977, + "y": -90.4723129272461 + }, + { + "x": -184.38395690918, + "y": -113.28946685791 + }, + { + "x": -143.895645141602, + "y": -140.910217285156 + }, + { + "x": -118.414710998535, + "y": -113.201278686523 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": -114.368476867676, + "y": -115.351486206055 + }, + { + "x": -138.977905273438, + "y": -143.522125244141 + }, + { + "x": -91.0844497680664, + "y": -162.87922668457 + }, + { + "x": -74.953971862793, + "y": -131.279663085938 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": -70.3552627563477, + "y": -132.623229980469 + }, + { + "x": -85.4975357055664, + "y": -164.510681152344 + }, + { + "x": -32.9768409729004, + "y": -174.478439331055 + }, + { + "x": -27.1383266448975, + "y": -140.824630737305 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": -22.2569046020508, + "y": -141.281127929688 + }, + { + "x": -27.0449523925781, + "y": -175.033508300781 + }, + { + "x": 27.0449523925781, + "y": -175.033508300781 + }, + { + "x": 22.2569046020508, + "y": -141.281127929688 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": 27.1383266448975, + "y": -140.824630737305 + }, + { + "x": 32.9768409729004, + "y": -174.478439331055 + }, + { + "x": 85.4975357055664, + "y": -164.510681152344 + }, + { + "x": 70.3552627563477, + "y": -132.623229980469 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": 74.953971862793, + "y": -131.279663085938 + }, + { + "x": 91.0844497680664, + "y": -162.87922668457 + }, + { + "x": 138.977905273438, + "y": -143.522125244141 + }, + { + "x": 114.368476867676, + "y": -115.351486206055 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": 118.414710998535, + "y": -113.201278686523 + }, + { + "x": 143.895645141602, + "y": -140.910217285156 + }, + { + "x": 184.38395690918, + "y": -113.28946685791 + }, + { + "x": 151.731338500977, + "y": -90.4723129272461 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": 154.994262695313, + "y": -87.6373519897461 + }, + { + "x": 188.347183227539, + "y": -109.84757232666 + }, + { + "x": 219.072662353516, + "y": -75.5686645507813 + }, + { + "x": 180.278060913086, + "y": -59.4304046630859 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": 182.56575012207, + "y": -56.079288482666 + }, + { + "x": 221.850555419922, + "y": -71.493896484375 + }, + { + "x": 241.031295776367, + "y": -32.5514526367188 + }, + { + "x": 198.348663330078, + "y": -24.0310077667236 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#B39B7E", + "outline_width": 2.5937424601, + "points": [ + { + "x": 199.526229858398, + "y": -20.3556747436523 + }, + { + "x": 242.460433959961, + "y": -28.0850276947021 + }, + { + "x": 248.981094360352, + "y": 13.2618198394775 + }, + { + "x": 204.890090942383, + "y": 13.669038772583 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -242.514907836914, + "y": 20.7499542236328 + }, + { + "x": -207.499389648438, + "y": 21.528076171875 + }, + { + "x": -204.905639648438, + "y": 24.3811912536621 + }, + { + "x": -205.683776855469, + "y": 51.8748626708984 + }, + { + "x": -208.796264648438, + "y": 54.4686050415039 + }, + { + "x": -243.811782836914, + "y": 53.4311065673828 + }, + { + "x": -246.405517578125, + "y": 50.3186187744141 + }, + { + "x": -245.627395629883, + "y": 23.3436965942383 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -241.218048095703, + "y": 23.6030731201172 + }, + { + "x": -210.093139648438, + "y": 23.6030731201172 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -241.218048095703, + "y": 51.8748626708984 + }, + { + "x": -207.499389648438, + "y": 51.8748626708984 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -235.459930419922, + "y": 42.143138885498 + }, + { + "x": -225.0849609375, + "y": 40.327522277832 + }, + { + "x": -214.709991455078, + "y": 42.9212646484375 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -242.514907836914, + "y": 59.6560897827148 + }, + { + "x": -207.499389648438, + "y": 60.434211730957 + }, + { + "x": -204.905639648438, + "y": 63.2873306274414 + }, + { + "x": -205.683776855469, + "y": 90.7809982299805 + }, + { + "x": -208.796264648438, + "y": 93.3747406005859 + }, + { + "x": -243.811782836914, + "y": 92.3372421264648 + }, + { + "x": -246.405517578125, + "y": 89.2247543334961 + }, + { + "x": -245.627395629883, + "y": 62.2498321533203 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -241.218048095703, + "y": 62.5092086791992 + }, + { + "x": -210.093139648438, + "y": 62.5092086791992 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -241.218048095703, + "y": 90.7809982299805 + }, + { + "x": -207.499389648438, + "y": 90.7809982299805 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -227.048431396484, + "y": 85.2926330566406 + }, + { + "x": -216.673461914063, + "y": 83.4770126342773 + }, + { + "x": -210.093139648438, + "y": 86.0707550048828 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -242.514907836914, + "y": 98.5622253417969 + }, + { + "x": -207.499389648438, + "y": 99.3403472900391 + }, + { + "x": -204.905639648438, + "y": 102.193466186523 + }, + { + "x": -205.683776855469, + "y": 129.687133789063 + }, + { + "x": -208.796264648438, + "y": 132.280883789063 + }, + { + "x": -243.811782836914, + "y": 131.243377685547 + }, + { + "x": -246.405517578125, + "y": 128.130889892578 + }, + { + "x": -245.627395629883, + "y": 101.155967712402 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -241.218048095703, + "y": 101.415336608887 + }, + { + "x": -210.093139648438, + "y": 101.415336608887 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -241.218048095703, + "y": 129.687133789063 + }, + { + "x": -207.499389648438, + "y": 129.687133789063 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -228.52946472168, + "y": 110.885093688965 + }, + { + "x": -218.154495239258, + "y": 109.069473266602 + }, + { + "x": -210.093139648438, + "y": 111.663215637207 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AF745A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -242.514907836914, + "y": 137.468353271484 + }, + { + "x": -207.499389648438, + "y": 138.246475219727 + }, + { + "x": -204.905639648438, + "y": 141.099609375 + }, + { + "x": -205.683776855469, + "y": 168.59326171875 + }, + { + "x": -208.796264648438, + "y": 171.18701171875 + }, + { + "x": -243.811782836914, + "y": 170.149505615234 + }, + { + "x": -246.405517578125, + "y": 167.037033081055 + }, + { + "x": -245.627395629883, + "y": 140.062103271484 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -241.218048095703, + "y": 140.321472167969 + }, + { + "x": -210.093139648438, + "y": 140.321472167969 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -241.218048095703, + "y": 168.59326171875 + }, + { + "x": -207.499389648438, + "y": 168.59326171875 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -226.200271606445, + "y": 155.998046875 + }, + { + "x": -215.825302124023, + "y": 154.182434082031 + }, + { + "x": -210.093139648438, + "y": 156.776184082031 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -242.514907836914, + "y": 176.374496459961 + }, + { + "x": -207.499389648438, + "y": 177.152633666992 + }, + { + "x": -204.905639648438, + "y": 180.005737304688 + }, + { + "x": -205.683776855469, + "y": 207.499404907227 + }, + { + "x": -208.796264648438, + "y": 210.093139648438 + }, + { + "x": -243.811782836914, + "y": 209.055648803711 + }, + { + "x": -246.405517578125, + "y": 205.943161010742 + }, + { + "x": -245.627395629883, + "y": 178.968231201172 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -241.218048095703, + "y": 179.227615356445 + }, + { + "x": -210.093139648438, + "y": 179.227615356445 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -241.218048095703, + "y": 207.499404907227 + }, + { + "x": -207.499389648438, + "y": 207.499404907227 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -231.395538330078, + "y": 203.193786621094 + }, + { + "x": -221.020568847656, + "y": 201.378173828125 + }, + { + "x": -210.645599365234, + "y": 203.971908569336 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B77B5C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": -242.514907836914, + "y": 215.280639648438 + }, + { + "x": -207.499389648438, + "y": 216.05876159668 + }, + { + "x": -204.905639648438, + "y": 218.911880493164 + }, + { + "x": -205.683776855469, + "y": 246.405548095703 + }, + { + "x": -208.796264648438, + "y": 248.999267578125 + }, + { + "x": -243.811782836914, + "y": 247.961791992188 + }, + { + "x": -246.405517578125, + "y": 244.849304199219 + }, + { + "x": -245.627395629883, + "y": 217.874374389648 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": -241.218048095703, + "y": 218.133743286133 + }, + { + "x": -210.093139648438, + "y": 218.133743286133 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": -241.218048095703, + "y": 246.405548095703 + }, + { + "x": -207.499389648438, + "y": 246.405548095703 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": -232.705383300781, + "y": 239.690353393555 + }, + { + "x": -222.330413818359, + "y": 237.874725341797 + }, + { + "x": -211.955444335938, + "y": 240.468475341797 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9D624A", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 208.796264648438, + "y": 20.7499542236328 + }, + { + "x": 243.811782836914, + "y": 21.528076171875 + }, + { + "x": 246.405517578125, + "y": 24.3811912536621 + }, + { + "x": 245.627395629883, + "y": 51.8748626708984 + }, + { + "x": 242.514907836914, + "y": 54.4686050415039 + }, + { + "x": 207.499389648438, + "y": 53.4311065673828 + }, + { + "x": 204.905639648438, + "y": 50.3186187744141 + }, + { + "x": 205.683776855469, + "y": 23.3436965942383 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 210.093139648438, + "y": 23.6030731201172 + }, + { + "x": 241.218048095703, + "y": 23.6030731201172 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 210.093139648438, + "y": 51.8748626708984 + }, + { + "x": 243.811782836914, + "y": 51.8748626708984 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 219.57585144043, + "y": 40.1278038024902 + }, + { + "x": 229.950820922852, + "y": 38.312183380127 + }, + { + "x": 240.325790405273, + "y": 40.9059257507324 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 208.796264648438, + "y": 59.6560897827148 + }, + { + "x": 243.811782836914, + "y": 60.434211730957 + }, + { + "x": 246.405517578125, + "y": 63.2873306274414 + }, + { + "x": 245.627395629883, + "y": 90.7809982299805 + }, + { + "x": 242.514907836914, + "y": 93.3747406005859 + }, + { + "x": 207.499389648438, + "y": 92.3372421264648 + }, + { + "x": 204.905639648438, + "y": 89.2247543334961 + }, + { + "x": 205.683776855469, + "y": 62.2498321533203 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 210.093139648438, + "y": 62.5092086791992 + }, + { + "x": 241.218048095703, + "y": 62.5092086791992 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 210.093139648438, + "y": 90.7809982299805 + }, + { + "x": 243.811782836914, + "y": 90.7809982299805 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 225.00715637207, + "y": 85.9332962036133 + }, + { + "x": 235.382125854492, + "y": 84.11767578125 + }, + { + "x": 241.218048095703, + "y": 86.7114181518555 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18B6C", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 208.796264648438, + "y": 98.5622253417969 + }, + { + "x": 243.811782836914, + "y": 99.3403472900391 + }, + { + "x": 246.405517578125, + "y": 102.193466186523 + }, + { + "x": 245.627395629883, + "y": 129.687133789063 + }, + { + "x": 242.514907836914, + "y": 132.280883789063 + }, + { + "x": 207.499389648438, + "y": 131.243377685547 + }, + { + "x": 204.905639648438, + "y": 128.130889892578 + }, + { + "x": 205.683776855469, + "y": 101.155967712402 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 210.093139648438, + "y": 101.415336608887 + }, + { + "x": 241.218048095703, + "y": 101.415336608887 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 210.093139648438, + "y": 129.687133789063 + }, + { + "x": 243.811782836914, + "y": 129.687133789063 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 223.256362915039, + "y": 124.852401733398 + }, + { + "x": 233.631332397461, + "y": 123.036773681641 + }, + { + "x": 241.218048095703, + "y": 125.630523681641 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A97057", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 208.796264648438, + "y": 137.468353271484 + }, + { + "x": 243.811782836914, + "y": 138.246475219727 + }, + { + "x": 246.405517578125, + "y": 141.099609375 + }, + { + "x": 245.627395629883, + "y": 168.59326171875 + }, + { + "x": 242.514907836914, + "y": 171.18701171875 + }, + { + "x": 207.499389648438, + "y": 170.149505615234 + }, + { + "x": 204.905639648438, + "y": 167.037033081055 + }, + { + "x": 205.683776855469, + "y": 140.062103271484 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 210.093139648438, + "y": 140.321472167969 + }, + { + "x": 241.218048095703, + "y": 140.321472167969 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 210.093139648438, + "y": 168.59326171875 + }, + { + "x": 243.811782836914, + "y": 168.59326171875 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 223.458679199219, + "y": 156.617965698242 + }, + { + "x": 233.833648681641, + "y": 154.802337646484 + }, + { + "x": 241.218048095703, + "y": 157.396087646484 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A9684F", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 208.796264648438, + "y": 176.374496459961 + }, + { + "x": 243.811782836914, + "y": 177.152633666992 + }, + { + "x": 246.405517578125, + "y": 180.005737304688 + }, + { + "x": 245.627395629883, + "y": 207.499404907227 + }, + { + "x": 242.514907836914, + "y": 210.093139648438 + }, + { + "x": 207.499389648438, + "y": 209.055648803711 + }, + { + "x": 204.905639648438, + "y": 205.943161010742 + }, + { + "x": 205.683776855469, + "y": 178.968231201172 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 210.093139648438, + "y": 179.227615356445 + }, + { + "x": 241.218048095703, + "y": 179.227615356445 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 210.093139648438, + "y": 207.499404907227 + }, + { + "x": 243.811782836914, + "y": 207.499404907227 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 222.330413818359, + "y": 194.004150390625 + }, + { + "x": 232.705383300781, + "y": 192.188537597656 + }, + { + "x": 241.218048095703, + "y": 194.782287597656 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BF8263", + "outline_color": "#714838", + "outline_width": 1.167184107045, + "points": [ + { + "x": 208.796264648438, + "y": 215.280639648438 + }, + { + "x": 243.811782836914, + "y": 216.05876159668 + }, + { + "x": 246.405517578125, + "y": 218.911880493164 + }, + { + "x": 245.627395629883, + "y": 246.405548095703 + }, + { + "x": 242.514907836914, + "y": 248.999267578125 + }, + { + "x": 207.499389648438, + "y": 247.961791992188 + }, + { + "x": 204.905639648438, + "y": 244.849304199219 + }, + { + "x": 205.683776855469, + "y": 217.874374389648 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D49A77", + "outline_width": 1.55624547606, + "points": [ + { + "x": 210.093139648438, + "y": 218.133743286133 + }, + { + "x": 241.218048095703, + "y": 218.133743286133 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#79503F", + "outline_width": 1.685932599065, + "points": [ + { + "x": 210.093139648438, + "y": 246.405548095703 + }, + { + "x": 243.811782836914, + "y": 246.405548095703 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A86850", + "outline_width": 1.167184107045, + "points": [ + { + "x": 222.436752319336, + "y": 240.489227294922 + }, + { + "x": 232.811721801758, + "y": 238.673583984375 + }, + { + "x": 241.218048095703, + "y": 241.267333984375 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#493021", + "outline_color": "#493021", + "outline_width": 0.0, + "points": [ + { + "x": 171.186996459961, + "y": 228.24934387207 + }, + { + "x": 168.585479736328, + "y": 235.90608215332 + }, + { + "x": 160.863906860352, + "y": 243.329376220703 + }, + { + "x": 148.253128051758, + "y": 250.296142578125 + }, + { + "x": 131.137008666992, + "y": 256.591186523438 + }, + { + "x": 110.036926269531, + "y": 262.027648925781 + }, + { + "x": 85.5934982299805, + "y": 266.434417724609 + }, + { + "x": 58.5485458374023, + "y": 269.684387207031 + }, + { + "x": 29.7268829345703, + "y": 271.673767089844 + }, + { + "x": 0.0, + "y": 272.342956542969 + }, + { + "x": -29.7268829345703, + "y": 271.673767089844 + }, + { + "x": -58.5485458374023, + "y": 269.684387207031 + }, + { + "x": -85.5934982299805, + "y": 266.434417724609 + }, + { + "x": -110.036926269531, + "y": 262.027648925781 + }, + { + "x": -131.137008666992, + "y": 256.591186523438 + }, + { + "x": -148.253128051758, + "y": 250.296142578125 + }, + { + "x": -160.863906860352, + "y": 243.329376220703 + }, + { + "x": -168.585479736328, + "y": 235.90608215332 + }, + { + "x": -171.186996459961, + "y": 228.24934387207 + }, + { + "x": -168.585479736328, + "y": 220.59260559082 + }, + { + "x": -160.863906860352, + "y": 213.169311523438 + }, + { + "x": -148.253128051758, + "y": 206.202529907227 + }, + { + "x": -131.137008666992, + "y": 199.907516479492 + }, + { + "x": -110.036926269531, + "y": 194.47102355957 + }, + { + "x": -85.5934982299805, + "y": 190.064270019531 + }, + { + "x": -58.5485458374023, + "y": 186.814315795898 + }, + { + "x": -29.7268829345703, + "y": 184.824920654297 + }, + { + "x": 0.0, + "y": 184.155731201172 + }, + { + "x": 29.7268829345703, + "y": 184.824920654297 + }, + { + "x": 58.5485458374023, + "y": 186.814315795898 + }, + { + "x": 85.5934982299805, + "y": 190.064270019531 + }, + { + "x": 110.036926269531, + "y": 194.47102355957 + }, + { + "x": 131.137008666992, + "y": 199.907516479492 + }, + { + "x": 148.253128051758, + "y": 206.202529907227 + }, + { + "x": 160.863906860352, + "y": 213.169311523438 + }, + { + "x": 168.585479736328, + "y": 220.59260559082 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#633B23", + "outline_color": "#633B23", + "outline_width": 0.0, + "points": [ + { + "x": 145.249572753906, + "y": 228.24934387207 + }, + { + "x": 143.042297363281, + "y": 234.103424072266 + }, + { + "x": 136.490509033203, + "y": 239.781112670898 + }, + { + "x": 125.788726806641, + "y": 245.108673095703 + }, + { + "x": 111.266357421875, + "y": 249.922668457031 + }, + { + "x": 93.3643493652344, + "y": 254.080413818359 + }, + { + "x": 72.6247863769531, + "y": 257.44970703125 + }, + { + "x": 49.6779479980469, + "y": 259.934509277344 + }, + { + "x": 25.221549987793, + "y": 261.45703125 + }, + { + "x": 0.0, + "y": 261.967987060547 + }, + { + "x": -25.221549987793, + "y": 261.45703125 + }, + { + "x": -49.6779479980469, + "y": 259.934509277344 + }, + { + "x": -72.6247863769531, + "y": 257.44970703125 + }, + { + "x": -93.3643493652344, + "y": 254.080413818359 + }, + { + "x": -111.266357421875, + "y": 249.922668457031 + }, + { + "x": -125.788726806641, + "y": 245.108673095703 + }, + { + "x": -136.490509033203, + "y": 239.781112670898 + }, + { + "x": -143.042297363281, + "y": 234.103424072266 + }, + { + "x": -145.249572753906, + "y": 228.24934387207 + }, + { + "x": -143.042297363281, + "y": 222.395263671875 + }, + { + "x": -136.490509033203, + "y": 216.717575073242 + }, + { + "x": -125.788726806641, + "y": 211.390014648438 + }, + { + "x": -111.266357421875, + "y": 206.576019287109 + }, + { + "x": -93.3643493652344, + "y": 202.418258666992 + }, + { + "x": -72.6247863769531, + "y": 199.048980712891 + }, + { + "x": -49.6779479980469, + "y": 196.564178466797 + }, + { + "x": -25.221549987793, + "y": 195.041656494141 + }, + { + "x": 0.0, + "y": 194.530700683594 + }, + { + "x": 25.221549987793, + "y": 195.041656494141 + }, + { + "x": 49.6779479980469, + "y": 196.564178466797 + }, + { + "x": 72.6247863769531, + "y": 199.048980712891 + }, + { + "x": 93.3643493652344, + "y": 202.418258666992 + }, + { + "x": 111.266357421875, + "y": 206.576019287109 + }, + { + "x": 125.788726806641, + "y": 211.390014648438 + }, + { + "x": 136.490509033203, + "y": 216.717575073242 + }, + { + "x": 143.042297363281, + "y": 222.395263671875 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#8B4725", + "outline_color": "#8B4725", + "outline_width": 0.0, + "points": [ + { + "x": 114.124664306641, + "y": 228.24934387207 + }, + { + "x": 112.392044067383, + "y": 232.303375244141 + }, + { + "x": 107.240875244141, + "y": 236.232894897461 + }, + { + "x": 98.8345489501953, + "y": 239.921188354492 + }, + { + "x": 87.4246826171875, + "y": 243.254150390625 + }, + { + "x": 73.3588180541992, + "y": 246.130584716797 + }, + { + "x": 57.0623321533203, + "y": 248.464965820313 + }, + { + "x": 39.0332298278809, + "y": 250.184631347656 + }, + { + "x": 19.8187847137451, + "y": 251.237670898438 + }, + { + "x": 0.0, + "y": 251.593017578125 + }, + { + "x": -19.8187847137451, + "y": 251.237670898438 + }, + { + "x": -39.0332298278809, + "y": 250.184631347656 + }, + { + "x": -57.0623321533203, + "y": 248.464965820313 + }, + { + "x": -73.3588180541992, + "y": 246.130584716797 + }, + { + "x": -87.4246826171875, + "y": 243.254150390625 + }, + { + "x": -98.8345489501953, + "y": 239.921188354492 + }, + { + "x": -107.240875244141, + "y": 236.232894897461 + }, + { + "x": -112.392044067383, + "y": 232.303375244141 + }, + { + "x": -114.124664306641, + "y": 228.24934387207 + }, + { + "x": -112.392044067383, + "y": 224.1953125 + }, + { + "x": -107.240875244141, + "y": 220.26579284668 + }, + { + "x": -98.8345489501953, + "y": 216.577499389648 + }, + { + "x": -87.4246826171875, + "y": 213.244537353516 + }, + { + "x": -73.3588180541992, + "y": 210.368087768555 + }, + { + "x": -57.0623321533203, + "y": 208.033721923828 + }, + { + "x": -39.0332298278809, + "y": 206.314056396484 + }, + { + "x": -19.8187847137451, + "y": 205.261001586914 + }, + { + "x": 0.0, + "y": 204.905670166016 + }, + { + "x": 19.8187847137451, + "y": 205.261001586914 + }, + { + "x": 39.0332298278809, + "y": 206.314056396484 + }, + { + "x": 57.0623321533203, + "y": 208.033721923828 + }, + { + "x": 73.3588180541992, + "y": 210.368087768555 + }, + { + "x": 87.4246826171875, + "y": 213.244537353516 + }, + { + "x": 98.8345489501953, + "y": 216.577499389648 + }, + { + "x": 107.240875244141, + "y": 220.26579284668 + }, + { + "x": 112.392044067383, + "y": 224.1953125 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B64F25", + "outline_color": "#B64F25", + "outline_width": 0.0, + "points": [ + { + "x": -119.312149047852, + "y": 225.655609130859 + }, + { + "x": -126.828811645508, + "y": 213.405364990234 + }, + { + "x": -131.811401367188, + "y": 202.138137817383 + }, + { + "x": -134.581512451172, + "y": 191.721664428711 + }, + { + "x": -135.46337890625, + "y": 182.023666381836 + }, + { + "x": -134.781234741211, + "y": 172.917037963867 + }, + { + "x": -132.856674194336, + "y": 164.266906738281 + }, + { + "x": -130.011337280273, + "y": 155.948776245117 + }, + { + "x": -126.572036743164, + "y": 147.827774047852 + }, + { + "x": -122.860389709473, + "y": 139.776794433594 + }, + { + "x": -119.198028564453, + "y": 131.660980224609 + }, + { + "x": -115.911750793457, + "y": 123.353210449219 + }, + { + "x": -113.323204040527, + "y": 114.721237182617 + }, + { + "x": -111.753982543945, + "y": 105.637954711914 + }, + { + "x": -111.530921936035, + "y": 95.9684829711914 + }, + { + "x": -106.981491088867, + "y": 99.0835647583008 + }, + { + "x": -103.251693725586, + "y": 102.782241821289 + }, + { + "x": -100.198860168457, + "y": 106.934829711914 + }, + { + "x": -97.6933059692383, + "y": 111.409027099609 + }, + { + "x": -95.5949630737305, + "y": 116.077758789063 + }, + { + "x": -93.7689743041992, + "y": 120.808746337891 + }, + { + "x": -92.0778503417969, + "y": 125.472305297852 + }, + { + "x": -90.3867340087891, + "y": 129.936141967773 + }, + { + "x": -88.5607452392578, + "y": 134.073150634766 + }, + { + "x": -86.46240234375, + "y": 137.748489379883 + }, + { + "x": -83.9568481445313, + "y": 140.835037231445 + }, + { + "x": -80.9040069580078, + "y": 143.200531005859 + }, + { + "x": -77.1742095947266, + "y": 144.715270996094 + }, + { + "x": -72.6247863769531, + "y": 145.249588012695 + }, + { + "x": -73.7867813110352, + "y": 130.85173034668 + }, + { + "x": -73.0320053100586, + "y": 117.56657409668 + }, + { + "x": -70.6691055297852, + "y": 105.230735778809 + }, + { + "x": -66.9989547729492, + "y": 93.6911849975586 + }, + { + "x": -62.3302230834961, + "y": 82.7870788574219 + }, + { + "x": -56.971549987793, + "y": 72.360237121582 + }, + { + "x": -51.2264099121094, + "y": 62.2498321533203 + }, + { + "x": -45.4008674621582, + "y": 52.2976455688477 + }, + { + "x": -39.8035697937012, + "y": 42.3480453491211 + }, + { + "x": -34.7405853271484, + "y": 32.2376403808594 + }, + { + "x": -30.5153789520264, + "y": 21.8082008361816 + }, + { + "x": -27.4340114593506, + "y": 10.904109954834 + }, + { + "x": -25.8077354431152, + "y": -0.635451316833496 + }, + { + "x": -25.9374237060547, + "y": -12.9686965942383 + }, + { + "x": -16.9215755462646, + "y": -6.48174667358398 + }, + { + "x": -9.70837783813477, + "y": 0.651044845581055 + }, + { + "x": -4.04105043411255, + "y": 8.28183364868164 + }, + { + "x": 0.347561478614807, + "y": 16.2653732299805 + }, + { + "x": 3.7142391204834, + "y": 24.4538192749023 + }, + { + "x": 6.32095003128052, + "y": 32.6967353820801 + }, + { + "x": 8.42966270446777, + "y": 40.8514556884766 + }, + { + "x": 10.2997512817383, + "y": 48.7675628662109 + }, + { + "x": 12.1905889511108, + "y": 56.2971954345703 + }, + { + "x": 14.3667392730713, + "y": 63.2925186157227 + }, + { + "x": 17.087574005127, + "y": 69.608283996582 + }, + { + "x": 20.6150646209717, + "y": 75.0966339111328 + }, + { + "x": 25.2059898376465, + "y": 79.609748840332 + }, + { + "x": 31.1249084472656, + "y": 82.9997711181641 + }, + { + "x": 35.3008346557617, + "y": 81.0285339355469 + }, + { + "x": 38.9735717773438, + "y": 78.5074157714844 + }, + { + "x": 42.1924095153809, + "y": 75.5220108032227 + }, + { + "x": 45.0092124938965, + "y": 72.1553344726563 + }, + { + "x": 47.4732666015625, + "y": 68.4955673217773 + }, + { + "x": 49.6364440917969, + "y": 64.6231079101563 + }, + { + "x": 51.5506286621094, + "y": 60.6287460327148 + }, + { + "x": 53.2650909423828, + "y": 56.5928802490234 + }, + { + "x": 54.8343086242676, + "y": 52.6037063598633 + }, + { + "x": 56.3049583435059, + "y": 48.7442169189453 + }, + { + "x": 57.731517791748, + "y": 45.1000099182129 + }, + { + "x": 59.1632614135742, + "y": 41.7566757202148 + }, + { + "x": 60.6520729064941, + "y": 38.7998046875 + }, + { + "x": 62.2498168945313, + "y": 36.3124084472656 + }, + { + "x": 68.4877700805664, + "y": 46.044132232666 + }, + { + "x": 72.93603515625, + "y": 55.254508972168 + }, + { + "x": 75.8617782592773, + "y": 63.9772644042969 + }, + { + "x": 77.5399322509766, + "y": 72.2461166381836 + }, + { + "x": 78.2428359985352, + "y": 80.0947799682617 + }, + { + "x": 78.2428359985352, + "y": 87.5595703125 + }, + { + "x": 77.8122711181641, + "y": 94.6716156005859 + }, + { + "x": 77.2234954833984, + "y": 101.467216491699 + }, + { + "x": 76.7462463378906, + "y": 107.977516174316 + }, + { + "x": 76.6554641723633, + "y": 114.238800048828 + }, + { + "x": 77.2234954833984, + "y": 120.284820556641 + }, + { + "x": 78.720085144043, + "y": 126.149276733398 + }, + { + "x": 81.420166015625, + "y": 131.865875244141 + }, + { + "x": 85.5934982299805, + "y": 137.468353271484 + }, + { + "x": 90.7654190063477, + "y": 138.046768188477 + }, + { + "x": 95.2059097290039, + "y": 137.65771484375 + }, + { + "x": 98.9746170043945, + "y": 136.43864440918 + }, + { + "x": 102.138984680176, + "y": 134.53483581543 + }, + { + "x": 104.758659362793, + "y": 132.086349487305 + }, + { + "x": 106.895904541016, + "y": 129.23323059082 + }, + { + "x": 108.61296081543, + "y": 126.120742797852 + }, + { + "x": 109.972076416016, + "y": 122.888946533203 + }, + { + "x": 111.038116455078, + "y": 119.680465698242 + }, + { + "x": 111.870704650879, + "y": 116.63542175293 + }, + { + "x": 112.534706115723, + "y": 113.896423339844 + }, + { + "x": 113.089767456055, + "y": 111.606155395508 + }, + { + "x": 113.598136901855, + "y": 109.907257080078 + }, + { + "x": 114.124664306641, + "y": 108.937194824219 + }, + { + "x": 120.974731445313, + "y": 122.95378112793 + }, + { + "x": 126.050689697266, + "y": 136.106643676758 + }, + { + "x": 129.479614257813, + "y": 148.411361694336 + }, + { + "x": 131.396392822266, + "y": 159.873107910156 + }, + { + "x": 131.928100585938, + "y": 170.510040283203 + }, + { + "x": 131.207046508789, + "y": 180.329956054688 + }, + { + "x": 129.362899780273, + "y": 189.343200683594 + }, + { + "x": 126.525344848633, + "y": 197.562774658203 + }, + { + "x": 122.826667785645, + "y": 204.999038696289 + }, + { + "x": 118.396553039551, + "y": 211.664962768555 + }, + { + "x": 113.364700317383, + "y": 217.570907592773 + }, + { + "x": 107.86336517334, + "y": 222.729858398438 + }, + { + "x": 102.019668579102, + "y": 227.14958190918 + }, + { + "x": 95.9684677124023, + "y": 230.843078613281 + }, + { + "x": 82.5821685791016, + "y": 234.969741821289 + }, + { + "x": 68.1168594360352, + "y": 238.450531005859 + }, + { + "x": 52.7696876525879, + "y": 241.272521972656 + }, + { + "x": 36.7273902893066, + "y": 243.425323486328 + }, + { + "x": 20.1896896362305, + "y": 244.898559570313 + }, + { + "x": 3.34333395957947, + "y": 245.679290771484 + }, + { + "x": -13.6171474456787, + "y": 245.757110595703 + }, + { + "x": -30.4972229003906, + "y": 245.119049072266 + }, + { + "x": -47.1049537658691, + "y": 243.75732421875 + }, + { + "x": -63.2484092712402, + "y": 241.656402587891 + }, + { + "x": -78.7330474853516, + "y": 238.808471679688 + }, + { + "x": -93.3669509887695, + "y": 235.197982788086 + }, + { + "x": -106.958152770996, + "y": 230.819732666016 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#EE852C", + "outline_color": "#EE852C", + "outline_width": 0.0, + "points": [ + { + "x": -90.7809829711914, + "y": 228.24934387207 + }, + { + "x": -97.1512145996094, + "y": 218.071487426758 + }, + { + "x": -100.935485839844, + "y": 208.596542358398 + }, + { + "x": -102.486541748047, + "y": 199.733734130859 + }, + { + "x": -102.154541015625, + "y": 191.392272949219 + }, + { + "x": -100.289642333984, + "y": 183.483932495117 + }, + { + "x": -97.247184753418, + "y": 175.912811279297 + }, + { + "x": -93.3747253417969, + "y": 168.59326171875 + }, + { + "x": -89.0276184082031, + "y": 161.431945800781 + }, + { + "x": -84.5534057617188, + "y": 154.33805847168 + }, + { + "x": -80.3074493408203, + "y": 147.223434448242 + }, + { + "x": -76.6399002075195, + "y": 139.994659423828 + }, + { + "x": -73.9035034179688, + "y": 132.561004638672 + }, + { + "x": -72.4458160400391, + "y": 124.831649780273 + }, + { + "x": -72.6247863769531, + "y": 116.718414306641 + }, + { + "x": -68.6304244995117, + "y": 120.238128662109 + }, + { + "x": -65.4478988647461, + "y": 124.069091796875 + }, + { + "x": -62.9345664978027, + "y": 128.125701904297 + }, + { + "x": -60.9503517150879, + "y": 132.324966430664 + }, + { + "x": -59.3500137329102, + "y": 136.583892822266 + }, + { + "x": -57.9934844970703, + "y": 140.81169128418 + }, + { + "x": -56.7381134033203, + "y": 144.925369262695 + }, + { + "x": -55.4438362121582, + "y": 148.841918945313 + }, + { + "x": -53.9679946899414, + "y": 152.473175048828 + }, + { + "x": -52.1705360412598, + "y": 155.738677978516 + }, + { + "x": -49.9061965942383, + "y": 158.547698974609 + }, + { + "x": -47.0349273681641, + "y": 160.81982421875 + }, + { + "x": -43.4166526794434, + "y": 162.466857910156 + }, + { + "x": -38.906135559082, + "y": 163.405792236328 + }, + { + "x": -40.2211647033691, + "y": 150.717208862305 + }, + { + "x": -39.8891639709473, + "y": 139.221725463867 + }, + { + "x": -38.1747016906738, + "y": 128.774139404297 + }, + { + "x": -35.3371467590332, + "y": 119.213607788086 + }, + { + "x": -31.6384696960449, + "y": 110.392288208008 + }, + { + "x": -27.33544921875, + "y": 102.154556274414 + }, + { + "x": -22.6952457427979, + "y": 94.3473968505859 + }, + { + "x": -17.9746341705322, + "y": 86.817756652832 + }, + { + "x": -13.4355850219727, + "y": 79.4152221679688 + }, + { + "x": -9.34006690979004, + "y": 71.9815521240234 + }, + { + "x": -5.94485759735107, + "y": 64.3689193725586 + }, + { + "x": -3.51711440086365, + "y": 56.4190979003906 + }, + { + "x": -2.31102442741394, + "y": 47.9842491149902 + }, + { + "x": -2.59374237060547, + "y": 38.9061508178711 + }, + { + "x": 5.27307844161987, + "y": 47.1983451843262 + }, + { + "x": 11.274998664856, + "y": 55.3842010498047 + }, + { + "x": 15.6869535446167, + "y": 63.4325790405273 + }, + { + "x": 18.7916641235352, + "y": 71.3175506591797 + }, + { + "x": 20.8614711761475, + "y": 79.0079956054688 + }, + { + "x": 22.1790904998779, + "y": 86.4779815673828 + }, + { + "x": 23.0194625854492, + "y": 93.6989593505859 + }, + { + "x": 23.6601181030273, + "y": 100.642402648926 + }, + { + "x": 24.3837718963623, + "y": 107.277198791504 + }, + { + "x": 25.4601745605469, + "y": 113.580001831055 + }, + { + "x": 27.1746387481689, + "y": 119.519668579102 + }, + { + "x": 29.8020992279053, + "y": 125.067687988281 + }, + { + "x": 33.6200866699219, + "y": 130.195510864258 + }, + { + "x": 38.906135559082, + "y": 134.874618530273 + }, + { + "x": 44.5968055725098, + "y": 135.377807617188 + }, + { + "x": 49.4937934875488, + "y": 134.768280029297 + }, + { + "x": 53.6749038696289, + "y": 133.206832885742 + }, + { + "x": 57.2205543518066, + "y": 130.85173034668 + }, + { + "x": 60.2111358642578, + "y": 127.861145019531 + }, + { + "x": 62.7270660400391, + "y": 124.393310546875 + }, + { + "x": 64.8435592651367, + "y": 120.609039306641 + }, + { + "x": 66.6436157226563, + "y": 116.666549682617 + }, + { + "x": 68.2050476074219, + "y": 112.721466064453 + }, + { + "x": 69.6082611083984, + "y": 108.937194824219 + }, + { + "x": 70.9310760498047, + "y": 105.469360351563 + }, + { + "x": 72.2538833618164, + "y": 102.478775024414 + }, + { + "x": 73.657096862793, + "y": 100.123664855957 + }, + { + "x": 75.2185287475586, + "y": 98.5622253417969 + }, + { + "x": 76.8059005737305, + "y": 108.304321289063 + }, + { + "x": 79.181770324707, + "y": 117.587326049805 + }, + { + "x": 82.099723815918, + "y": 126.507202148438 + }, + { + "x": 85.321159362793, + "y": 135.162521362305 + }, + { + "x": 88.5996475219727, + "y": 143.64665222168 + }, + { + "x": 91.6887893676758, + "y": 152.055572509766 + }, + { + "x": 94.3473815917969, + "y": 160.487823486328 + }, + { + "x": 96.3315887451172, + "y": 169.03938293457 + }, + { + "x": 97.3976135253906, + "y": 177.806243896484 + }, + { + "x": 97.2990570068359, + "y": 186.884338378906 + }, + { + "x": 95.7946853637695, + "y": 196.372253417969 + }, + { + "x": 92.6406936645508, + "y": 206.365951538086 + }, + { + "x": 87.5932769775391, + "y": 216.958786010742 + }, + { + "x": 80.4060134887695, + "y": 228.24934387207 + }, + { + "x": 70.8325119018555, + "y": 231.826110839844 + }, + { + "x": 60.0347595214844, + "y": 234.782974243164 + }, + { + "x": 48.2150802612305, + "y": 237.138107299805 + }, + { + "x": 35.5783653259277, + "y": 238.912216186523 + }, + { + "x": 22.3295288085938, + "y": 240.115707397461 + }, + { + "x": 8.67347431182861, + "y": 240.771942138672 + }, + { + "x": -5.18748474121094, + "y": 240.893829345703 + }, + { + "x": -19.0484428405762, + "y": 240.499572753906 + }, + { + "x": -32.7044982910156, + "y": 239.607330322266 + }, + { + "x": -45.9533309936523, + "y": 238.230056762695 + }, + { + "x": -58.5900497436523, + "y": 236.391098022461 + }, + { + "x": -70.4097290039063, + "y": 234.103424072266 + }, + { + "x": -81.2074813842773, + "y": 231.382583618164 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#FFC45B", + "outline_color": "#FFC45B", + "outline_width": 0.0, + "points": [ + { + "x": -64.8435592651367, + "y": 228.24934387207 + }, + { + "x": -69.7405471801758, + "y": 220.294326782227 + }, + { + "x": -72.4120941162109, + "y": 213.050018310547 + }, + { + "x": -73.1798477172852, + "y": 206.428176879883 + }, + { + "x": -72.3602294921875, + "y": 200.345855712891 + }, + { + "x": -70.2696685791016, + "y": 194.717437744141 + }, + { + "x": -67.224609375, + "y": 189.457336425781 + }, + { + "x": -63.546688079834, + "y": 184.479934692383 + }, + { + "x": -59.5497283935547, + "y": 179.702255249023 + }, + { + "x": -55.5527725219727, + "y": 175.0361328125 + }, + { + "x": -51.8748474121094, + "y": 170.401107788086 + }, + { + "x": -48.8323860168457, + "y": 165.706436157227 + }, + { + "x": -46.7392387390137, + "y": 160.871704101563 + }, + { + "x": -45.9196166992188, + "y": 155.811309814453 + }, + { + "x": -46.6873626708984, + "y": 150.437072753906 + }, + { + "x": -40.6283798217773, + "y": 153.728530883789 + }, + { + "x": -35.6924896240234, + "y": 156.924026489258 + }, + { + "x": -31.7318439483643, + "y": 160.007995605469 + }, + { + "x": -28.5986042022705, + "y": 162.959671020508 + }, + { + "x": -26.147518157959, + "y": 165.763488769531 + }, + { + "x": -24.2281455993652, + "y": 168.403930664063 + }, + { + "x": -22.6952457427979, + "y": 170.86279296875 + }, + { + "x": -21.4009685516357, + "y": 173.121948242188 + }, + { + "x": -20.1948776245117, + "y": 175.168411254883 + }, + { + "x": -18.9343204498291, + "y": 176.97883605957 + }, + { + "x": -17.4688549041748, + "y": 178.542861938477 + }, + { + "x": -15.6532344818115, + "y": 179.837142944336 + }, + { + "x": -13.3370237350464, + "y": 180.851287841797 + }, + { + "x": -10.3749694824219, + "y": 181.561981201172 + }, + { + "x": -10.8418426513672, + "y": 172.055908203125 + }, + { + "x": -10.1493139266968, + "y": 163.473220825195 + }, + { + "x": -8.47894382476807, + "y": 155.707550048828 + }, + { + "x": -6.02007627487183, + "y": 148.652572631836 + }, + { + "x": -2.95946002006531, + "y": 142.196762084961 + }, + { + "x": 0.513561010360718, + "y": 136.236343383789 + }, + { + "x": 4.21483135223389, + "y": 130.659790039063 + }, + { + "x": 7.95500755310059, + "y": 125.360778808594 + }, + { + "x": 11.5473413467407, + "y": 120.232940673828 + }, + { + "x": 14.805082321167, + "y": 115.167358398438 + }, + { + "x": 17.5414791107178, + "y": 110.057685852051 + }, + { + "x": 19.5697860717773, + "y": 104.79239654541 + }, + { + "x": 20.7006568908691, + "y": 99.2677230834961 + }, + { + "x": 20.7499389648438, + "y": 93.3747406005859 + }, + { + "x": 27.5792636871338, + "y": 101.148193359375 + }, + { + "x": 32.6759643554688, + "y": 108.869758605957 + }, + { + "x": 36.283863067627, + "y": 116.487579345703 + }, + { + "x": 38.6571388244629, + "y": 123.954971313477 + }, + { + "x": 40.0421943664551, + "y": 131.217437744141 + }, + { + "x": 40.6906280517578, + "y": 138.225738525391 + }, + { + "x": 40.8514404296875, + "y": 144.925369262695 + }, + { + "x": 40.7736320495605, + "y": 151.269668579102 + }, + { + "x": 40.7087860107422, + "y": 157.204147338867 + }, + { + "x": 40.9033164978027, + "y": 162.679534912109 + }, + { + "x": 41.608814239502, + "y": 167.643966674805 + }, + { + "x": 43.0716896057129, + "y": 172.048141479492 + }, + { + "x": 45.5461158752441, + "y": 175.840179443359 + }, + { + "x": 49.2811050415039, + "y": 178.968231201172 + }, + { + "x": 56.0170555114746, + "y": 185.110214233398 + }, + { + "x": 60.7610092163086, + "y": 191.265167236328 + }, + { + "x": 63.6660003662109, + "y": 197.360473632813 + }, + { + "x": 64.8876571655273, + "y": 203.333847045898 + }, + { + "x": 64.5790023803711, + "y": 209.112716674805 + }, + { + "x": 62.8930625915527, + "y": 214.629608154297 + }, + { + "x": 59.9802932739258, + "y": 219.819686889648 + }, + { + "x": 55.9963073730469, + "y": 224.612915039063 + }, + { + "x": 51.0941276550293, + "y": 228.939270019531 + }, + { + "x": 45.4242095947266, + "y": 232.733917236328 + }, + { + "x": 39.1421661376953, + "y": 235.926818847656 + }, + { + "x": 32.403621673584, + "y": 238.450531005859 + }, + { + "x": 25.3564262390137, + "y": 240.23762512207 + }, + { + "x": 18.1561965942383, + "y": 241.218048095703 + }, + { + "x": 11.8949022293091, + "y": 242.172546386719 + }, + { + "x": 5.39239025115967, + "y": 242.813201904297 + }, + { + "x": -1.28390252590179, + "y": 243.150390625 + }, + { + "x": -8.05356979370117, + "y": 243.184112548828 + }, + { + "x": -14.8465814590454, + "y": 242.924743652344 + }, + { + "x": -21.5903129577637, + "y": 242.374847412109 + }, + { + "x": -28.2069473266602, + "y": 241.542266845703 + }, + { + "x": -34.6264610290527, + "y": 240.432144165039 + }, + { + "x": -40.7736320495605, + "y": 239.049697875977 + }, + { + "x": -46.5732345581055, + "y": 237.40007019043 + }, + { + "x": -51.9552536010742, + "y": 235.488479614258 + }, + { + "x": -56.8418655395508, + "y": 233.322708129883 + }, + { + "x": -61.1630363464355, + "y": 230.907928466797 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#FFE3A0", + "outline_color": "#FFE3A0", + "outline_width": 0.0, + "points": [ + { + "x": -33.7186508178711, + "y": 233.436828613281 + }, + { + "x": -37.6533584594727, + "y": 227.032867431641 + }, + { + "x": -39.632381439209, + "y": 221.119140625 + }, + { + "x": -39.9202880859375, + "y": 215.623016357422 + }, + { + "x": -38.7842292785645, + "y": 210.479614257813 + }, + { + "x": -36.493953704834, + "y": 205.618942260742 + }, + { + "x": -33.3114356994629, + "y": 200.973556518555 + }, + { + "x": -29.5038185119629, + "y": 196.475997924805 + }, + { + "x": -25.3408641815186, + "y": 192.058853149414 + }, + { + "x": -21.0845317840576, + "y": 187.652084350586 + }, + { + "x": -17.0071697235107, + "y": 183.188247680664 + }, + { + "x": -13.3707427978516, + "y": 178.59992980957 + }, + { + "x": -10.4424066543579, + "y": 173.81965637207 + }, + { + "x": -8.49191188812256, + "y": 168.777420043945 + }, + { + "x": -7.78122711181641, + "y": 163.405792236328 + }, + { + "x": -1.83377587795258, + "y": 168.287216186523 + }, + { + "x": 2.80642938613892, + "y": 172.92741394043 + }, + { + "x": 6.3313250541687, + "y": 177.316024780273 + }, + { + "x": 8.94581699371338, + "y": 181.447860717773 + }, + { + "x": 10.8444375991821, + "y": 185.3203125 + }, + { + "x": 12.2269020080566, + "y": 188.928207397461 + }, + { + "x": 13.292929649353, + "y": 192.261169433594 + }, + { + "x": 14.2396450042725, + "y": 195.316604614258 + }, + { + "x": 15.2641744613647, + "y": 198.089309692383 + }, + { + "x": 16.568826675415, + "y": 200.571517944336 + }, + { + "x": 18.3481330871582, + "y": 202.760635375977 + }, + { + "x": 20.801815032959, + "y": 204.648880004883 + }, + { + "x": 24.1321792602539, + "y": 206.231063842773 + }, + { + "x": 28.5311660766602, + "y": 207.499404907227 + }, + { + "x": 32.932746887207, + "y": 213.369049072266 + }, + { + "x": 35.1555824279785, + "y": 218.735504150391 + }, + { + "x": 35.4045829772949, + "y": 223.570236206055 + }, + { + "x": 33.884651184082, + "y": 227.842132568359 + }, + { + "x": 30.8006896972656, + "y": 231.522644042969 + }, + { + "x": 26.3524227142334, + "y": 234.585861206055 + }, + { + "x": 20.7499389648438, + "y": 237.003219604492 + }, + { + "x": 14.1929588317871, + "y": 238.746215820313 + }, + { + "x": 6.88897943496704, + "y": 239.783706665039 + }, + { + "x": -0.959684669971466, + "y": 240.092376708984 + }, + { + "x": -9.15072345733643, + "y": 239.638458251953 + }, + { + "x": -17.476634979248, + "y": 238.398666381836 + }, + { + "x": -25.7325172424316, + "y": 236.339233398438 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#52301E", + "outline_color": "#2F2119", + "outline_width": 2.5937424601, + "points": [ + { + "x": -122.645111083984, + "y": 240.222045898438 + }, + { + "x": 90.8172988891602, + "y": 212.315994262695 + }, + { + "x": 101.223388671875, + "y": 231.880584716797 + }, + { + "x": -111.232635498047, + "y": 267.505645751953 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A56A38", + "outline_width": 3.37186519813, + "points": [ + { + "x": -112.021133422852, + "y": 241.448883056641 + }, + { + "x": 83.4380950927734, + "y": 215.895355224609 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#76492A", + "outline_width": 1.81561972207, + "points": [ + { + "x": -103.298385620117, + "y": 248.156311035156 + }, + { + "x": -57.3398628234863, + "y": 239.532119750977 + }, + { + "x": -2.8271791934967, + "y": 236.328842163086 + }, + { + "x": 74.1602783203125, + "y": 224.957885742188 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#76492A", + "outline_width": 1.81561972207, + "points": [ + { + "x": -102.289421081543, + "y": 255.872711181641 + }, + { + "x": -56.3308982849121, + "y": 247.248504638672 + }, + { + "x": -1.8182133436203, + "y": 244.045227050781 + }, + { + "x": 75.1692504882813, + "y": 232.671676635742 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AA7445", + "outline_color": "#3A281D", + "outline_width": 1.81561972207, + "points": [ + { + "x": 101.534637451172, + "y": 224.254974365234 + }, + { + "x": 101.661727905273, + "y": 227.624252319336 + }, + { + "x": 101.168922424316, + "y": 230.843078613281 + }, + { + "x": 100.087333679199, + "y": 233.693603515625 + }, + { + "x": 98.4895858764648, + "y": 235.981292724609 + }, + { + "x": 96.4872207641602, + "y": 237.547912597656 + }, + { + "x": 94.2151031494141, + "y": 238.29231262207 + }, + { + "x": 91.8262634277344, + "y": 238.157440185547 + }, + { + "x": 89.4892959594727, + "y": 237.15625 + }, + { + "x": 87.3572463989258, + "y": 235.356185913086 + }, + { + "x": 85.5779342651367, + "y": 232.881774902344 + }, + { + "x": 84.2732849121094, + "y": 229.896377563477 + }, + { + "x": 83.5314788818359, + "y": 226.610092163086 + }, + { + "x": 83.4017791748047, + "y": 223.240829467773 + }, + { + "x": 83.8971939086914, + "y": 220.022003173828 + }, + { + "x": 84.9787826538086, + "y": 217.171463012695 + }, + { + "x": 86.576530456543, + "y": 214.8837890625 + }, + { + "x": 88.5788955688477, + "y": 213.314559936523 + }, + { + "x": 90.8510131835938, + "y": 212.572769165039 + }, + { + "x": 93.2398529052734, + "y": 212.705062866211 + }, + { + "x": 95.5768127441406, + "y": 213.70622253418 + }, + { + "x": 97.708869934082, + "y": 215.506286621094 + }, + { + "x": 99.4881744384766, + "y": 217.983306884766 + }, + { + "x": 100.792831420898, + "y": 220.966125488281 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#69482C", + "outline_width": 1.55624547606, + "points": [ + { + "x": 89.255859375, + "y": 220.366958618164 + }, + { + "x": 97.6440277099609, + "y": 224.501388549805 + }, + { + "x": 90.9366073608398, + "y": 233.22673034668 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#52301E", + "outline_color": "#2F2119", + "outline_width": 2.5937424601, + "points": [ + { + "x": -93.7637939453125, + "y": 185.600433349609 + }, + { + "x": 117.226776123047, + "y": 228.36865234375 + }, + { + "x": 120.730926513672, + "y": 250.252075195313 + }, + { + "x": -91.8055114746094, + "y": 215.109436035156 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#A56A38", + "outline_width": 3.37186519813, + "points": [ + { + "x": -84.1098785400391, + "y": 190.204330444336 + }, + { + "x": 109.085021972656, + "y": 229.364654541016 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#76492A", + "outline_width": 1.81561972207, + "points": [ + { + "x": -78.030143737793, + "y": 199.375793457031 + }, + { + "x": -31.7577819824219, + "y": 206.109146118164 + }, + { + "x": 20.8536891937256, + "y": 220.743057250977 + }, + { + "x": 97.3716812133789, + "y": 234.930816650391 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#76492A", + "outline_width": 1.81561972207, + "points": [ + { + "x": -79.5760192871094, + "y": 207.001403808594 + }, + { + "x": -33.303653717041, + "y": 213.734756469727 + }, + { + "x": 19.3078174591064, + "y": 228.36865234375 + }, + { + "x": 95.825813293457, + "y": 242.556427001953 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AA7445", + "outline_color": "#3A281D", + "outline_width": 1.81561972207, + "points": [ + { + "x": 123.495849609375, + "y": 243.137420654297 + }, + { + "x": 122.52579498291, + "y": 246.366638183594 + }, + { + "x": 121.016242980957, + "y": 249.250885009766 + }, + { + "x": 119.068336486816, + "y": 251.598205566406 + }, + { + "x": 116.816963195801, + "y": 253.242645263672 + }, + { + "x": 114.412567138672, + "y": 254.077819824219 + }, + { + "x": 112.021133422852, + "y": 254.044097900391 + }, + { + "x": 109.806083679199, + "y": 253.144073486328 + }, + { + "x": 107.917831420898, + "y": 251.440002441406 + }, + { + "x": 106.48609161377, + "y": 249.045959472656 + }, + { + "x": 105.604217529297, + "y": 246.127990722656 + }, + { + "x": 105.337066650391, + "y": 242.880645751953 + }, + { + "x": 105.702781677246, + "y": 239.529525756836 + }, + { + "x": 106.672836303711, + "y": 236.302917480469 + }, + { + "x": 108.182395935059, + "y": 233.416076660156 + }, + { + "x": 110.130302429199, + "y": 231.071334838867 + }, + { + "x": 112.38166809082, + "y": 229.42431640625 + }, + { + "x": 114.783477783203, + "y": 228.589111328125 + }, + { + "x": 117.174903869629, + "y": 228.622833251953 + }, + { + "x": 119.389961242676, + "y": 229.522857666016 + }, + { + "x": 121.278205871582, + "y": 231.229553222656 + }, + { + "x": 122.712547302246, + "y": 233.620986938477 + }, + { + "x": 123.591827392578, + "y": 236.541534423828 + }, + { + "x": 123.858978271484, + "y": 239.78630065918 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#69482C", + "outline_width": 1.55624547606, + "points": [ + { + "x": 113.139038085938, + "y": 235.480697631836 + }, + { + "x": 119.734924316406, + "y": 242.110321044922 + }, + { + "x": 110.563453674316, + "y": 248.190032958984 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#DA6B31", + "outline_color": "#DA6B31", + "outline_width": 0.0, + "points": [ + { + "x": -85.5934982299805, + "y": 247.845062255859 + }, + { + "x": -85.6713104248047, + "y": 248.293762207031 + }, + { + "x": -85.9073333740234, + "y": 248.732147216797 + }, + { + "x": -86.2886276245117, + "y": 249.141937255859 + }, + { + "x": -86.8073654174805, + "y": 249.51025390625 + }, + { + "x": -87.4454345703125, + "y": 249.831878662109 + }, + { + "x": -88.1872406005859, + "y": 250.091247558594 + }, + { + "x": -89.0068664550781, + "y": 250.280609130859 + }, + { + "x": -89.8809509277344, + "y": 250.397308349609 + }, + { + "x": -90.7809829711914, + "y": 250.438812255859 + }, + { + "x": -91.6810150146484, + "y": 250.397308349609 + }, + { + "x": -92.5550994873047, + "y": 250.280609130859 + }, + { + "x": -93.3747253417969, + "y": 250.091247558594 + }, + { + "x": -94.1165313720703, + "y": 249.831878662109 + }, + { + "x": -94.7546005249023, + "y": 249.51025390625 + }, + { + "x": -95.2733383178711, + "y": 249.141937255859 + }, + { + "x": -95.6546325683594, + "y": 248.732147216797 + }, + { + "x": -95.8906555175781, + "y": 248.293762207031 + }, + { + "x": -95.9684677124023, + "y": 247.845062255859 + }, + { + "x": -95.8906555175781, + "y": 247.393737792969 + }, + { + "x": -95.6546325683594, + "y": 246.9580078125 + }, + { + "x": -95.2733383178711, + "y": 246.548187255859 + }, + { + "x": -94.7546005249023, + "y": 246.177307128906 + }, + { + "x": -94.1165313720703, + "y": 245.858276367188 + }, + { + "x": -93.3747253417969, + "y": 245.598907470703 + }, + { + "x": -92.5550994873047, + "y": 245.406951904297 + }, + { + "x": -91.6810150146484, + "y": 245.290222167969 + }, + { + "x": -90.7809829711914, + "y": 245.251312255859 + }, + { + "x": -89.8809509277344, + "y": 245.290222167969 + }, + { + "x": -89.0068664550781, + "y": 245.406951904297 + }, + { + "x": -88.1872406005859, + "y": 245.598907470703 + }, + { + "x": -87.4454345703125, + "y": 245.858276367188 + }, + { + "x": -86.8073654174805, + "y": 246.177307128906 + }, + { + "x": -86.2886276245117, + "y": 246.548187255859 + }, + { + "x": -85.9073333740234, + "y": 246.9580078125 + }, + { + "x": -85.6713104248047, + "y": 247.393737792969 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#DA6B31", + "outline_color": "#DA6B31", + "outline_width": 0.0, + "points": [ + { + "x": -49.2811050415039, + "y": 250.438812255859 + }, + { + "x": -49.3589172363281, + "y": 250.890106201172 + }, + { + "x": -49.5949478149414, + "y": 251.325866699219 + }, + { + "x": -49.9762268066406, + "y": 251.735687255859 + }, + { + "x": -50.4949760437012, + "y": 252.106567382813 + }, + { + "x": -51.1330375671387, + "y": 252.425628662109 + }, + { + "x": -51.8748474121094, + "y": 252.684997558594 + }, + { + "x": -52.6944694519043, + "y": 252.876922607422 + }, + { + "x": -53.5685615539551, + "y": 252.99365234375 + }, + { + "x": -54.4685897827148, + "y": 253.032562255859 + }, + { + "x": -55.3686180114746, + "y": 252.99365234375 + }, + { + "x": -56.2427101135254, + "y": 252.876922607422 + }, + { + "x": -57.0623321533203, + "y": 252.684997558594 + }, + { + "x": -57.804141998291, + "y": 252.425628662109 + }, + { + "x": -58.4422035217285, + "y": 252.106567382813 + }, + { + "x": -58.9609527587891, + "y": 251.735687255859 + }, + { + "x": -59.3422317504883, + "y": 251.325866699219 + }, + { + "x": -59.5782623291016, + "y": 250.890106201172 + }, + { + "x": -59.6560745239258, + "y": 250.438812255859 + }, + { + "x": -59.5782623291016, + "y": 249.990112304688 + }, + { + "x": -59.3422317504883, + "y": 249.5517578125 + }, + { + "x": -58.9609527587891, + "y": 249.141937255859 + }, + { + "x": -58.4422035217285, + "y": 248.773620605469 + }, + { + "x": -57.804141998291, + "y": 248.451995849609 + }, + { + "x": -57.0623321533203, + "y": 248.192626953125 + }, + { + "x": -56.2427101135254, + "y": 248.003265380859 + }, + { + "x": -55.3686180114746, + "y": 247.886566162109 + }, + { + "x": -54.4685897827148, + "y": 247.845062255859 + }, + { + "x": -53.5685615539551, + "y": 247.886566162109 + }, + { + "x": -52.6944694519043, + "y": 248.003265380859 + }, + { + "x": -51.8748474121094, + "y": 248.192626953125 + }, + { + "x": -51.1330375671387, + "y": 248.451995849609 + }, + { + "x": -50.4949760437012, + "y": 248.773620605469 + }, + { + "x": -49.9762268066406, + "y": 249.141937255859 + }, + { + "x": -49.5949478149414, + "y": 249.5517578125 + }, + { + "x": -49.3589172363281, + "y": 249.990112304688 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#DA6B31", + "outline_color": "#DA6B31", + "outline_width": 0.0, + "points": [ + { + "x": -20.7499389648438, + "y": 248.918884277344 + }, + { + "x": -20.827751159668, + "y": 249.370178222656 + }, + { + "x": -21.0637817382813, + "y": 249.805938720703 + }, + { + "x": -21.4450607299805, + "y": 250.215759277344 + }, + { + "x": -21.9638118743896, + "y": 250.586639404297 + }, + { + "x": -22.6018695831299, + "y": 250.905670166016 + }, + { + "x": -23.3436813354492, + "y": 251.165069580078 + }, + { + "x": -24.1633033752441, + "y": 251.356994628906 + }, + { + "x": -25.0373954772949, + "y": 251.473724365234 + }, + { + "x": -25.9374237060547, + "y": 251.512634277344 + }, + { + "x": -26.8374519348145, + "y": 251.473724365234 + }, + { + "x": -27.7115440368652, + "y": 251.356994628906 + }, + { + "x": -28.5311660766602, + "y": 251.165069580078 + }, + { + "x": -29.2729778289795, + "y": 250.905670166016 + }, + { + "x": -29.9110355377197, + "y": 250.586639404297 + }, + { + "x": -30.4297866821289, + "y": 250.215759277344 + }, + { + "x": -30.8110656738281, + "y": 249.805938720703 + }, + { + "x": -31.0470962524414, + "y": 249.370178222656 + }, + { + "x": -31.1249084472656, + "y": 248.918884277344 + }, + { + "x": -31.0470962524414, + "y": 248.470153808594 + }, + { + "x": -30.8110656738281, + "y": 248.031799316406 + }, + { + "x": -30.4297866821289, + "y": 247.622009277344 + }, + { + "x": -29.9110355377197, + "y": 247.251098632813 + }, + { + "x": -29.2729778289795, + "y": 246.932067871094 + }, + { + "x": -28.5311660766602, + "y": 246.672698974609 + }, + { + "x": -27.7115440368652, + "y": 246.483337402344 + }, + { + "x": -26.8374519348145, + "y": 246.364044189453 + }, + { + "x": -25.9374237060547, + "y": 246.325134277344 + }, + { + "x": -25.0373954772949, + "y": 246.364044189453 + }, + { + "x": -24.1633033752441, + "y": 246.483337402344 + }, + { + "x": -23.3436813354492, + "y": 246.672698974609 + }, + { + "x": -22.6018695831299, + "y": 246.932067871094 + }, + { + "x": -21.9638118743896, + "y": 247.251098632813 + }, + { + "x": -21.4450607299805, + "y": 247.622009277344 + }, + { + "x": -21.0637817382813, + "y": 248.031799316406 + }, + { + "x": -20.827751159668, + "y": 248.470153808594 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#DA6B31", + "outline_color": "#DA6B31", + "outline_width": 0.0, + "points": [ + { + "x": 28.5311660766602, + "y": 247.645355224609 + }, + { + "x": 28.4533538818359, + "y": 248.096649169922 + }, + { + "x": 28.2173233032227, + "y": 248.535003662109 + }, + { + "x": 27.8360443115234, + "y": 248.942199707031 + }, + { + "x": 27.3172931671143, + "y": 249.313140869141 + }, + { + "x": 26.679235458374, + "y": 249.634765625 + }, + { + "x": 25.9374237060547, + "y": 249.894134521484 + }, + { + "x": 25.1178016662598, + "y": 250.083465576172 + }, + { + "x": 24.243709564209, + "y": 250.200164794922 + }, + { + "x": 23.3436813354492, + "y": 250.239074707031 + }, + { + "x": 22.4436531066895, + "y": 250.200164794922 + }, + { + "x": 21.5695610046387, + "y": 250.083465576172 + }, + { + "x": 20.7499389648438, + "y": 249.894134521484 + }, + { + "x": 20.008129119873, + "y": 249.634765625 + }, + { + "x": 19.3700675964355, + "y": 249.313140869141 + }, + { + "x": 18.8513202667236, + "y": 248.942199707031 + }, + { + "x": 18.4700393676758, + "y": 248.535003662109 + }, + { + "x": 18.2340087890625, + "y": 248.096649169922 + }, + { + "x": 18.1561965942383, + "y": 247.645355224609 + }, + { + "x": 18.2340087890625, + "y": 247.196624755859 + }, + { + "x": 18.4700393676758, + "y": 246.75830078125 + }, + { + "x": 18.8513202667236, + "y": 246.348480224609 + }, + { + "x": 19.3700675964355, + "y": 245.980163574219 + }, + { + "x": 20.008129119873, + "y": 245.658538818359 + }, + { + "x": 20.7499389648438, + "y": 245.399169921875 + }, + { + "x": 21.5695610046387, + "y": 245.209838867188 + }, + { + "x": 22.4436531066895, + "y": 245.093109130859 + }, + { + "x": 23.3436813354492, + "y": 245.051605224609 + }, + { + "x": 24.243709564209, + "y": 245.093109130859 + }, + { + "x": 25.1178016662598, + "y": 245.209838867188 + }, + { + "x": 25.9374237060547, + "y": 245.399169921875 + }, + { + "x": 26.679235458374, + "y": 245.658538818359 + }, + { + "x": 27.3172931671143, + "y": 245.980163574219 + }, + { + "x": 27.8360443115234, + "y": 246.348480224609 + }, + { + "x": 28.2173233032227, + "y": 246.75830078125 + }, + { + "x": 28.4533538818359, + "y": 247.196624755859 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#DA6B31", + "outline_color": "#DA6B31", + "outline_width": 0.0, + "points": [ + { + "x": 64.8435592651367, + "y": 248.688018798828 + }, + { + "x": 64.7657470703125, + "y": 249.136749267578 + }, + { + "x": 64.5297164916992, + "y": 249.575103759766 + }, + { + "x": 64.1484375, + "y": 249.984893798828 + }, + { + "x": 63.6296882629395, + "y": 250.355804443359 + }, + { + "x": 62.991626739502, + "y": 250.674835205078 + }, + { + "x": 62.2498168945313, + "y": 250.934234619141 + }, + { + "x": 61.4301948547363, + "y": 251.123565673828 + }, + { + "x": 60.5561027526855, + "y": 251.242858886719 + }, + { + "x": 59.6560745239258, + "y": 251.281768798828 + }, + { + "x": 58.756046295166, + "y": 251.242858886719 + }, + { + "x": 57.8819541931152, + "y": 251.123565673828 + }, + { + "x": 57.0623321533203, + "y": 250.934234619141 + }, + { + "x": 56.3205223083496, + "y": 250.674835205078 + }, + { + "x": 55.6824607849121, + "y": 250.355804443359 + }, + { + "x": 55.1637115478516, + "y": 249.984893798828 + }, + { + "x": 54.7824325561523, + "y": 249.575103759766 + }, + { + "x": 54.5464019775391, + "y": 249.136749267578 + }, + { + "x": 54.4685897827148, + "y": 248.688018798828 + }, + { + "x": 54.5464019775391, + "y": 248.236724853516 + }, + { + "x": 54.7824325561523, + "y": 247.800964355469 + }, + { + "x": 55.1637115478516, + "y": 247.391143798828 + }, + { + "x": 55.6824607849121, + "y": 247.020263671875 + }, + { + "x": 56.3205223083496, + "y": 246.701232910156 + }, + { + "x": 57.0623321533203, + "y": 246.441864013672 + }, + { + "x": 57.8819541931152, + "y": 246.249908447266 + }, + { + "x": 58.756046295166, + "y": 246.133178710938 + }, + { + "x": 59.6560745239258, + "y": 246.094299316406 + }, + { + "x": 60.5561027526855, + "y": 246.133178710938 + }, + { + "x": 61.4301948547363, + "y": 246.249908447266 + }, + { + "x": 62.2498168945313, + "y": 246.441864013672 + }, + { + "x": 62.991626739502, + "y": 246.701232910156 + }, + { + "x": 63.6296882629395, + "y": 247.020263671875 + }, + { + "x": 64.1484375, + "y": 247.391143798828 + }, + { + "x": 64.5297164916992, + "y": 247.800964355469 + }, + { + "x": 64.7657470703125, + "y": 248.236724853516 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#DA6B31", + "outline_color": "#DA6B31", + "outline_width": 0.0, + "points": [ + { + "x": 93.3747253417969, + "y": 250.106811523438 + }, + { + "x": 93.2969131469727, + "y": 250.55810546875 + }, + { + "x": 93.0608901977539, + "y": 250.993865966797 + }, + { + "x": 92.6795959472656, + "y": 251.403686523438 + }, + { + "x": 92.1608581542969, + "y": 251.774597167969 + }, + { + "x": 91.5227890014648, + "y": 252.093627929688 + }, + { + "x": 90.7809829711914, + "y": 252.352996826172 + }, + { + "x": 89.9613571166992, + "y": 252.544921875 + }, + { + "x": 89.087272644043, + "y": 252.661651611328 + }, + { + "x": 88.1872406005859, + "y": 252.700561523438 + }, + { + "x": 87.2872085571289, + "y": 252.661651611328 + }, + { + "x": 86.4131240844727, + "y": 252.544921875 + }, + { + "x": 85.5934982299805, + "y": 252.352996826172 + }, + { + "x": 84.851692199707, + "y": 252.093627929688 + }, + { + "x": 84.213623046875, + "y": 251.774597167969 + }, + { + "x": 83.6948852539063, + "y": 251.403686523438 + }, + { + "x": 83.313591003418, + "y": 250.993865966797 + }, + { + "x": 83.0775680541992, + "y": 250.55810546875 + }, + { + "x": 82.999755859375, + "y": 250.106811523438 + }, + { + "x": 83.0775680541992, + "y": 249.655487060547 + }, + { + "x": 83.313591003418, + "y": 249.219757080078 + }, + { + "x": 83.6948852539063, + "y": 248.809936523438 + }, + { + "x": 84.213623046875, + "y": 248.439025878906 + }, + { + "x": 84.851692199707, + "y": 248.120025634766 + }, + { + "x": 85.5934982299805, + "y": 247.860626220703 + }, + { + "x": 86.4131240844727, + "y": 247.668701171875 + }, + { + "x": 87.2872085571289, + "y": 247.551971435547 + }, + { + "x": 88.1872406005859, + "y": 247.513061523438 + }, + { + "x": 89.087272644043, + "y": 247.551971435547 + }, + { + "x": 89.9613571166992, + "y": 247.668701171875 + }, + { + "x": 90.7809829711914, + "y": 247.860626220703 + }, + { + "x": 91.5227890014648, + "y": 248.120025634766 + }, + { + "x": 92.1608581542969, + "y": 248.439025878906 + }, + { + "x": 92.6795959472656, + "y": 248.809936523438 + }, + { + "x": 93.0608901977539, + "y": 249.219757080078 + }, + { + "x": 93.2969131469727, + "y": 249.655487060547 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#946033", + "outline_color": "#543B27", + "outline_width": 2.5937424601, + "points": [ + { + "x": -331.9990234375, + "y": -233.436798095703 + }, + { + "x": -269.749206542969, + "y": -233.436798095703 + }, + { + "x": -269.749206542969, + "y": 256.780517578125 + }, + { + "x": -331.9990234375, + "y": 256.780517578125 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AD7944", + "outline_color": "#AD7944", + "outline_width": 0.0, + "points": [ + { + "x": -324.217803955078, + "y": -212.686859130859 + }, + { + "x": -277.530426025391, + "y": -212.686859130859 + }, + { + "x": -277.530426025391, + "y": 241.218048095703 + }, + { + "x": -324.217803955078, + "y": 241.218048095703 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18D52", + "outline_color": "#C18D52", + "outline_width": 0.0, + "points": [ + { + "x": -319.030303955078, + "y": -204.905639648438 + }, + { + "x": -311.249084472656, + "y": -204.905639648438 + }, + { + "x": -311.249084472656, + "y": 233.436828613281 + }, + { + "x": -319.030303955078, + "y": 233.436828613281 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#7F512C", + "outline_color": "#7F512C", + "outline_width": 0.0, + "points": [ + { + "x": -282.717926025391, + "y": -207.499374389648 + }, + { + "x": -274.936706542969, + "y": -207.499374389648 + }, + { + "x": -274.936706542969, + "y": 241.218048095703 + }, + { + "x": -282.717926025391, + "y": 241.218048095703 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BA854C", + "outline_color": "#76502C", + "outline_width": 1.55624547606, + "points": [ + { + "x": -339.780242919922, + "y": -228.249313354492 + }, + { + "x": -261.967987060547, + "y": -228.249313354492 + }, + { + "x": -261.967987060547, + "y": -220.468078613281 + }, + { + "x": -339.780242919922, + "y": -220.468078613281 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BA854C", + "outline_color": "#76502C", + "outline_width": 1.55624547606, + "points": [ + { + "x": -339.780242919922, + "y": -204.905639648438 + }, + { + "x": -261.967987060547, + "y": -204.905639648438 + }, + { + "x": -261.967987060547, + "y": -197.124404907227 + }, + { + "x": -339.780242919922, + "y": -197.124404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BA854C", + "outline_color": "#76502C", + "outline_width": 1.55624547606, + "points": [ + { + "x": -339.780242919922, + "y": 217.874374389648 + }, + { + "x": -261.967987060547, + "y": 217.874374389648 + }, + { + "x": -261.967987060547, + "y": 225.655609130859 + }, + { + "x": -339.780242919922, + "y": 225.655609130859 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BA854C", + "outline_color": "#76502C", + "outline_width": 1.55624547606, + "points": [ + { + "x": -339.780242919922, + "y": 243.811798095703 + }, + { + "x": -261.967987060547, + "y": 243.811798095703 + }, + { + "x": -261.967987060547, + "y": 251.593017578125 + }, + { + "x": -339.780242919922, + "y": 251.593017578125 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#986536", + "outline_width": 1.29687123005, + "points": [ + { + "x": -306.061584472656, + "y": -186.749435424805 + }, + { + "x": -304.50537109375, + "y": -67.4372863769531 + }, + { + "x": -307.099090576172, + "y": 67.4373168945313 + }, + { + "x": -305.283477783203, + "y": 199.718170166016 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#986536", + "outline_width": 1.29687123005, + "points": [ + { + "x": -298.280364990234, + "y": -186.749435424805 + }, + { + "x": -296.72412109375, + "y": -67.4372863769531 + }, + { + "x": -299.31787109375, + "y": 67.4373168945313 + }, + { + "x": -297.502227783203, + "y": 199.718170166016 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#986536", + "outline_width": 1.29687123005, + "points": [ + { + "x": -290.499145507813, + "y": -186.749435424805 + }, + { + "x": -288.942901611328, + "y": -67.4372863769531 + }, + { + "x": -291.536651611328, + "y": 67.4373168945313 + }, + { + "x": -289.721008300781, + "y": 199.718170166016 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#946033", + "outline_color": "#543B27", + "outline_width": 2.5937424601, + "points": [ + { + "x": 269.749206542969, + "y": -233.436798095703 + }, + { + "x": 331.9990234375, + "y": -233.436798095703 + }, + { + "x": 331.9990234375, + "y": 256.780517578125 + }, + { + "x": 269.749206542969, + "y": 256.780517578125 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#AD7944", + "outline_color": "#AD7944", + "outline_width": 0.0, + "points": [ + { + "x": 277.530426025391, + "y": -212.686859130859 + }, + { + "x": 324.217803955078, + "y": -212.686859130859 + }, + { + "x": 324.217803955078, + "y": 241.218048095703 + }, + { + "x": 277.530426025391, + "y": 241.218048095703 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#C18D52", + "outline_color": "#C18D52", + "outline_width": 0.0, + "points": [ + { + "x": 282.717926025391, + "y": -204.905639648438 + }, + { + "x": 290.499145507813, + "y": -204.905639648438 + }, + { + "x": 290.499145507813, + "y": 233.436828613281 + }, + { + "x": 282.717926025391, + "y": 233.436828613281 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#7F512C", + "outline_color": "#7F512C", + "outline_width": 0.0, + "points": [ + { + "x": 319.030303955078, + "y": -207.499374389648 + }, + { + "x": 326.8115234375, + "y": -207.499374389648 + }, + { + "x": 326.8115234375, + "y": 241.218048095703 + }, + { + "x": 319.030303955078, + "y": 241.218048095703 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BA854C", + "outline_color": "#76502C", + "outline_width": 1.55624547606, + "points": [ + { + "x": 261.967987060547, + "y": -228.249313354492 + }, + { + "x": 339.780242919922, + "y": -228.249313354492 + }, + { + "x": 339.780242919922, + "y": -220.468078613281 + }, + { + "x": 261.967987060547, + "y": -220.468078613281 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BA854C", + "outline_color": "#76502C", + "outline_width": 1.55624547606, + "points": [ + { + "x": 261.967987060547, + "y": -204.905639648438 + }, + { + "x": 339.780242919922, + "y": -204.905639648438 + }, + { + "x": 339.780242919922, + "y": -197.124404907227 + }, + { + "x": 261.967987060547, + "y": -197.124404907227 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BA854C", + "outline_color": "#76502C", + "outline_width": 1.55624547606, + "points": [ + { + "x": 261.967987060547, + "y": 217.874374389648 + }, + { + "x": 339.780242919922, + "y": 217.874374389648 + }, + { + "x": 339.780242919922, + "y": 225.655609130859 + }, + { + "x": 261.967987060547, + "y": 225.655609130859 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BA854C", + "outline_color": "#76502C", + "outline_width": 1.55624547606, + "points": [ + { + "x": 261.967987060547, + "y": 243.811798095703 + }, + { + "x": 339.780242919922, + "y": 243.811798095703 + }, + { + "x": 339.780242919922, + "y": 251.593017578125 + }, + { + "x": 261.967987060547, + "y": 251.593017578125 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#986536", + "outline_width": 1.29687123005, + "points": [ + { + "x": 295.686645507813, + "y": -186.749435424805 + }, + { + "x": 297.242858886719, + "y": -67.4372863769531 + }, + { + "x": 294.649139404297, + "y": 67.4373168945313 + }, + { + "x": 296.464752197266, + "y": 199.718170166016 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#986536", + "outline_width": 1.29687123005, + "points": [ + { + "x": 303.467864990234, + "y": -186.749435424805 + }, + { + "x": 305.024108886719, + "y": -67.4372863769531 + }, + { + "x": 302.430358886719, + "y": 67.4373168945313 + }, + { + "x": 304.246002197266, + "y": 199.718170166016 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#986536", + "outline_width": 1.29687123005, + "points": [ + { + "x": 311.249084472656, + "y": -186.749435424805 + }, + { + "x": 312.805328369141, + "y": -67.4372863769531 + }, + { + "x": 310.211578369141, + "y": 67.4373168945313 + }, + { + "x": 312.027221679688, + "y": 199.718170166016 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#BB8953", + "outline_color": "#62452D", + "outline_width": 3.89061369015, + "points": [ + { + "x": -360.530181884766, + "y": -303.467834472656 + }, + { + "x": 360.530181884766, + "y": -303.467834472656 + }, + { + "x": 360.530181884766, + "y": -277.530426025391 + }, + { + "x": -360.530181884766, + "y": -277.530426025391 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#9B6839", + "outline_color": "#71502F", + "outline_width": 1.81561972207, + "points": [ + { + "x": -352.748962402344, + "y": -277.530426025391 + }, + { + "x": 352.748962402344, + "y": -277.530426025391 + }, + { + "x": 352.748962402344, + "y": -254.186737060547 + }, + { + "x": -352.748962402344, + "y": -254.186737060547 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#B27C44", + "outline_color": "#775330", + "outline_width": 1.81561972207, + "points": [ + { + "x": -342.373992919922, + "y": -254.186737060547 + }, + { + "x": 342.373992919922, + "y": -254.186737060547 + }, + { + "x": 342.373992919922, + "y": -233.436798095703 + }, + { + "x": -342.373992919922, + "y": -233.436798095703 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#D5A369", + "outline_width": 3.89061369015, + "points": [ + { + "x": -350.155212402344, + "y": -295.686614990234 + }, + { + "x": 350.155212402344, + "y": -295.686614990234 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#C99558", + "outline_width": 2.5937424601, + "points": [ + { + "x": -337.1865234375, + "y": -259.374206542969 + }, + { + "x": 337.1865234375, + "y": -259.374206542969 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#A27D5D", + "outline_color": "#59483B", + "outline_width": 3.89061369015, + "points": [ + { + "x": -383.873870849609, + "y": 254.186767578125 + }, + { + "x": 383.873870849609, + "y": 254.186767578125 + }, + { + "x": 383.873870849609, + "y": 293.092895507813 + }, + { + "x": 373.498901367188, + "y": 303.467864990234 + }, + { + "x": -373.498901367188, + "y": 303.467864990234 + }, + { + "x": -383.873870849609, + "y": 293.092895507813 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "closed": true, + "fill_color": "#CEAA7C", + "outline_color": "#CEAA7C", + "outline_width": 0.0, + "points": [ + { + "x": -376.092651367188, + "y": 256.780517578125 + }, + { + "x": 376.092651367188, + "y": 256.780517578125 + }, + { + "x": 376.092651367188, + "y": 267.155487060547 + }, + { + "x": -376.092651367188, + "y": 267.155487060547 + } + ], + "reshaped": true, + "shape_type": "polygon", + "vertex_flags": [ + 0, + 0, + 0, + 0 + ] + }, + { + "closed": false, + "fill_color": "#00000000", + "outline_color": "#80634D", + "outline_width": 2.5937424601, + "points": [ + { + "x": -365.717681884766, + "y": 293.092895507813 + }, + { + "x": 365.717681884766, + "y": 293.092895507813 + } + ], + "reshaped": true, + "shape_type": "line", + "vertex_flags": [ + 0, + 0 + ] + } + ], + "tags": [ + "walls", + "objects" + ], + "version": "1.1" +} \ No newline at end of file diff --git a/content/props/art_framed_picture-v1.prop b/content/props/art_framed_picture-v1.prop new file mode 100644 index 0000000..d10cc09 --- /dev/null +++ b/content/props/art_framed_picture-v1.prop @@ -0,0 +1,274 @@ +{ + "version": "1.1", + "prop_name": "Framed Picture", + "material_preset": "none", + "tags": ["objects", "walls"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": -110, + "y": -82 + }, + { + "x": 110, + "y": -82 + }, + { + "x": 110, + "y": 82 + }, + { + "x": -110, + "y": 82 + } + ], + "fill_color": "#B68165", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -91, + "y": -64 + }, + { + "x": 91, + "y": -64 + }, + { + "x": 91, + "y": 64 + }, + { + "x": -91, + "y": 64 + } + ], + "fill_color": "#F4DEAF", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -80, + "y": -54 + }, + { + "x": 80, + "y": -54 + }, + { + "x": 80, + "y": 54 + }, + { + "x": -80, + "y": 54 + } + ], + "fill_color": "#A4CFCD", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 58.0, + "y": -30.0 + }, + { + "x": 56.86, + "y": -24.26 + }, + { + "x": 53.61, + "y": -19.39 + }, + { + "x": 48.74, + "y": -16.14 + }, + { + "x": 43.0, + "y": -15.0 + }, + { + "x": 37.26, + "y": -16.14 + }, + { + "x": 32.39, + "y": -19.39 + }, + { + "x": 29.14, + "y": -24.26 + }, + { + "x": 28.0, + "y": -30.0 + }, + { + "x": 29.14, + "y": -35.74 + }, + { + "x": 32.39, + "y": -40.61 + }, + { + "x": 37.26, + "y": -43.86 + }, + { + "x": 43.0, + "y": -45.0 + }, + { + "x": 48.74, + "y": -43.86 + }, + { + "x": 53.61, + "y": -40.61 + }, + { + "x": 56.86, + "y": -35.74 + } + ], + "fill_color": "#F3D68C", + "outline_color": "#F3D68C", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -80, + "y": 54 + }, + { + "x": -20, + "y": -11 + }, + { + "x": 10, + "y": 23 + }, + { + "x": 40, + "y": -18 + }, + { + "x": 80, + "y": 39 + }, + { + "x": 80, + "y": 54 + } + ], + "fill_color": "#88B798", + "outline_color": "#88B798", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -64, + "y": 42 + }, + { + "x": -8, + "y": 42 + } + ], + "fill_color": "#00000000", + "outline_color": "#F0E1BC", + "outline_width": 4, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/props/art_potted_plant-v1.prop b/content/props/art_potted_plant-v1.prop new file mode 100644 index 0000000..b811177 --- /dev/null +++ b/content/props/art_potted_plant-v1.prop @@ -0,0 +1,236 @@ +{ + "version": "1.1", + "prop_name": "Potted Plant", + "material_preset": "none", + "tags": ["nature", "objects"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": -45, + "y": 16 + }, + { + "x": -55, + "y": -6 + }, + { + "x": -39, + "y": -74 + }, + { + "x": -4, + "y": -28 + }, + { + "x": 8, + "y": 11 + } + ], + "fill_color": "#70B481", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 3, + "y": 8 + }, + { + "x": 1, + "y": -31 + }, + { + "x": 38, + "y": -91 + }, + { + "x": 48, + "y": -48 + }, + { + "x": 36, + "y": 4 + } + ], + "fill_color": "#83C290", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -18, + "y": 14 + }, + { + "x": -4, + "y": -112 + }, + { + "x": 18, + "y": -56 + }, + { + "x": 16, + "y": 15 + } + ], + "fill_color": "#9CCF90", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 0, + "y": 30 + }, + { + "x": 0, + "y": -69 + } + ], + "fill_color": "#00000000", + "outline_color": "#5C946F", + "outline_width": 5, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -55, + "y": 17 + }, + { + "x": 55, + "y": 17 + }, + { + "x": 43, + "y": 102 + }, + { + "x": -43, + "y": 102 + } + ], + "fill_color": "#C47761", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -60, + "y": 14 + }, + { + "x": 60, + "y": 14 + }, + { + "x": 60, + "y": 31 + }, + { + "x": -60, + "y": 31 + } + ], + "fill_color": "#DC9670", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -21, + "y": 59 + }, + { + "x": 21, + "y": 59 + } + ], + "fill_color": "#00000000", + "outline_color": "#EFC09A", + "outline_width": 4, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/props/art_rug-v1.prop b/content/props/art_rug-v1.prop new file mode 100644 index 0000000..15ff920 --- /dev/null +++ b/content/props/art_rug-v1.prop @@ -0,0 +1,304 @@ +{ + "version": "1.1", + "prop_name": "Patterned Rug", + "material_preset": "none", + "tags": ["objects"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": -158, + "y": -58 + }, + { + "x": 158, + "y": -58 + }, + { + "x": 177, + "y": 63 + }, + { + "x": -177, + "y": 63 + } + ], + "fill_color": "#C77377", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -132, + "y": -38 + }, + { + "x": 132, + "y": -38 + }, + { + "x": 147, + "y": 43 + }, + { + "x": -147, + "y": 43 + } + ], + "fill_color": "#F0D29A", + "outline_color": "#F0D29A", + "outline_width": 2, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -97, + "y": -24 + }, + { + "x": 97, + "y": -24 + }, + { + "x": 106, + "y": 29 + }, + { + "x": -106, + "y": 29 + } + ], + "fill_color": "#7E9B9C", + "outline_color": "#7E9B9C", + "outline_width": 2, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 0, + "y": -19 + }, + { + "x": 35, + "y": 3 + }, + { + "x": 0, + "y": 24 + }, + { + "x": -35, + "y": 3 + } + ], + "fill_color": "#F6D69A", + "outline_color": "#F6D69A", + "outline_width": 1, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -138, + "y": 64 + }, + { + "x": -138, + "y": 77 + } + ], + "fill_color": "#00000000", + "outline_color": "#DBA389", + "outline_width": 3, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -92, + "y": 64 + }, + { + "x": -92, + "y": 77 + } + ], + "fill_color": "#00000000", + "outline_color": "#DBA389", + "outline_width": 3, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -46, + "y": 64 + }, + { + "x": -46, + "y": 77 + } + ], + "fill_color": "#00000000", + "outline_color": "#DBA389", + "outline_width": 3, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 0, + "y": 64 + }, + { + "x": 0, + "y": 77 + } + ], + "fill_color": "#00000000", + "outline_color": "#DBA389", + "outline_width": 3, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 46, + "y": 64 + }, + { + "x": 46, + "y": 77 + } + ], + "fill_color": "#00000000", + "outline_color": "#DBA389", + "outline_width": 3, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 92, + "y": 64 + }, + { + "x": 92, + "y": 77 + } + ], + "fill_color": "#00000000", + "outline_color": "#DBA389", + "outline_width": 3, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 138, + "y": 64 + }, + { + "x": 138, + "y": 77 + } + ], + "fill_color": "#00000000", + "outline_color": "#DBA389", + "outline_width": 3, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/props/art_storefront-v1.prop b/content/props/art_storefront-v1.prop new file mode 100644 index 0000000..6745b6a --- /dev/null +++ b/content/props/art_storefront-v1.prop @@ -0,0 +1,546 @@ +{ + "version": "1.1", + "prop_name": "Sunny Storefront", + "material_preset": "none", + "tags": ["buildings"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": -155, + "y": -160 + }, + { + "x": 155, + "y": -160 + }, + { + "x": 155, + "y": 116 + }, + { + "x": -155, + "y": 116 + } + ], + "fill_color": "#E8B571", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -170, + "y": -176 + }, + { + "x": 170, + "y": -176 + }, + { + "x": 170, + "y": -148 + }, + { + "x": -170, + "y": -148 + } + ], + "fill_color": "#6C4E69", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -115, + "y": -203 + }, + { + "x": 115, + "y": -203 + }, + { + "x": 115, + "y": -177 + }, + { + "x": -115, + "y": -177 + } + ], + "fill_color": "#FAE6AF", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -50, + "y": -189 + }, + { + "x": 50, + "y": -189 + } + ], + "fill_color": "#00000000", + "outline_color": "#B97768", + "outline_width": 5, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -142, + "y": -145 + }, + { + "x": 142, + "y": -145 + }, + { + "x": 142, + "y": -104 + }, + { + "x": -142, + "y": -104 + } + ], + "fill_color": "#F3D77A", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -142, + "y": -145 + }, + { + "x": -95, + "y": -145 + }, + { + "x": -103, + "y": -105 + }, + { + "x": -134, + "y": -105 + } + ], + "fill_color": "#E37C70", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -95, + "y": -145 + }, + { + "x": -48, + "y": -145 + }, + { + "x": -56, + "y": -105 + }, + { + "x": -87, + "y": -105 + } + ], + "fill_color": "#FFF2D4", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -48, + "y": -145 + }, + { + "x": -1, + "y": -145 + }, + { + "x": -9, + "y": -105 + }, + { + "x": -40, + "y": -105 + } + ], + "fill_color": "#E37C70", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -1, + "y": -145 + }, + { + "x": 46, + "y": -145 + }, + { + "x": 38, + "y": -105 + }, + { + "x": 7, + "y": -105 + } + ], + "fill_color": "#FFF2D4", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 46, + "y": -145 + }, + { + "x": 93, + "y": -145 + }, + { + "x": 85, + "y": -105 + }, + { + "x": 54, + "y": -105 + } + ], + "fill_color": "#E37C70", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 93, + "y": -145 + }, + { + "x": 140, + "y": -145 + }, + { + "x": 132, + "y": -105 + }, + { + "x": 101, + "y": -105 + } + ], + "fill_color": "#FFF2D4", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -126, + "y": -91 + }, + { + "x": -20, + "y": -91 + }, + { + "x": -20, + "y": 101 + }, + { + "x": -126, + "y": 101 + } + ], + "fill_color": "#81B5BD", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -112, + "y": -76 + }, + { + "x": -34, + "y": -76 + }, + { + "x": -34, + "y": 75 + }, + { + "x": -112, + "y": 75 + } + ], + "fill_color": "#D4F1E8", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 22, + "y": -91 + }, + { + "x": 121, + "y": -91 + }, + { + "x": 121, + "y": 101 + }, + { + "x": 22, + "y": 101 + } + ], + "fill_color": "#835B73", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 34, + "y": -78 + }, + { + "x": 108, + "y": -78 + }, + { + "x": 108, + "y": 76 + }, + { + "x": 34, + "y": 76 + } + ], + "fill_color": "#C4DCDD", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 71, + "y": -78 + }, + { + "x": 71, + "y": 76 + } + ], + "fill_color": "#00000000", + "outline_color": "#835B73", + "outline_width": 5, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -160, + "y": 101 + }, + { + "x": 160, + "y": 101 + }, + { + "x": 160, + "y": 119 + }, + { + "x": -160, + "y": 119 + } + ], + "fill_color": "#C99672", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/props/art_town_house-v1.prop b/content/props/art_town_house-v1.prop new file mode 100644 index 0000000..a357308 --- /dev/null +++ b/content/props/art_town_house-v1.prop @@ -0,0 +1,473 @@ +{ + "version": "1.1", + "prop_name": "Town House", + "material_preset": "none", + "tags": ["buildings"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": -118, + "y": -178 + }, + { + "x": 118, + "y": -178 + }, + { + "x": 118, + "y": 120 + }, + { + "x": -118, + "y": 120 + } + ], + "fill_color": "#B3C99C", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -138, + "y": -168 + }, + { + "x": 0, + "y": -270 + }, + { + "x": 138, + "y": -168 + } + ], + "fill_color": "#CF735F", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -24, + "y": -246 + }, + { + "x": 24, + "y": -246 + }, + { + "x": 24, + "y": -177 + }, + { + "x": -24, + "y": -177 + } + ], + "fill_color": "#F3E3B2", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -20, + "y": -242 + }, + { + "x": 20, + "y": -242 + }, + { + "x": 20, + "y": -181 + }, + { + "x": -20, + "y": -181 + } + ], + "fill_color": "#85ADBC", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -88, + "y": -124 + }, + { + "x": -28, + "y": -124 + }, + { + "x": -28, + "y": -38 + }, + { + "x": -88, + "y": -38 + } + ], + "fill_color": "#E9D6A9", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 28, + "y": -124 + }, + { + "x": 88, + "y": -124 + }, + { + "x": 88, + "y": -38 + }, + { + "x": 28, + "y": -38 + } + ], + "fill_color": "#E9D6A9", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -78, + "y": -113 + }, + { + "x": -38, + "y": -113 + }, + { + "x": -38, + "y": -49 + }, + { + "x": -78, + "y": -49 + } + ], + "fill_color": "#B9DFE3", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 38, + "y": -113 + }, + { + "x": 78, + "y": -113 + }, + { + "x": 78, + "y": -49 + }, + { + "x": 38, + "y": -49 + } + ], + "fill_color": "#B9DFE3", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -58, + "y": -113 + }, + { + "x": -58, + "y": -49 + } + ], + "fill_color": "#00000000", + "outline_color": "#E9D6A9", + "outline_width": 4, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 58, + "y": -113 + }, + { + "x": 58, + "y": -49 + } + ], + "fill_color": "#00000000", + "outline_color": "#E9D6A9", + "outline_width": 4, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -36, + "y": -10 + }, + { + "x": 36, + "y": -10 + }, + { + "x": 36, + "y": 120 + }, + { + "x": -36, + "y": 120 + } + ], + "fill_color": "#8A6273", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 27.0, + "y": 61.0 + }, + { + "x": 26.62, + "y": 62.91 + }, + { + "x": 25.54, + "y": 64.54 + }, + { + "x": 23.91, + "y": 65.62 + }, + { + "x": 22.0, + "y": 66.0 + }, + { + "x": 20.09, + "y": 65.62 + }, + { + "x": 18.46, + "y": 64.54 + }, + { + "x": 17.38, + "y": 62.91 + }, + { + "x": 17.0, + "y": 61.0 + }, + { + "x": 17.38, + "y": 59.09 + }, + { + "x": 18.46, + "y": 57.46 + }, + { + "x": 20.09, + "y": 56.38 + }, + { + "x": 22.0, + "y": 56.0 + }, + { + "x": 23.91, + "y": 56.38 + }, + { + "x": 25.54, + "y": 57.46 + }, + { + "x": 26.62, + "y": 59.09 + } + ], + "fill_color": "#F5D98B", + "outline_color": "#F5D98B", + "outline_width": 1, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -128, + "y": 109 + }, + { + "x": 128, + "y": 109 + }, + { + "x": 128, + "y": 128 + }, + { + "x": -128, + "y": 128 + } + ], + "fill_color": "#E8B38A", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/props/art_tree-v2.prop b/content/props/art_tree-v2.prop new file mode 100644 index 0000000..0eacc75 --- /dev/null +++ b/content/props/art_tree-v2.prop @@ -0,0 +1,1749 @@ +{ + "version": "1.1", + "prop_name": "Round Tree", + "material_preset": "none", + "tags": ["nature"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": -29, + "y": 103 + }, + { + "x": -25, + "y": -64 + }, + { + "x": -16, + "y": -106 + }, + { + "x": 10, + "y": -110 + }, + { + "x": 27, + "y": -64 + }, + { + "x": 25, + "y": 98 + }, + { + "x": 53, + "y": 112 + }, + { + "x": 7, + "y": 104 + }, + { + "x": -58, + "y": 113 + } + ], + "fill_color": "#A95D31", + "outline_color": "#79482F", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -12, + "y": -83 + }, + { + "x": -49, + "y": -116 + }, + { + "x": -91, + "y": -130 + } + ], + "fill_color": "#00000000", + "outline_color": "#8D4E2D", + "outline_width": 14, + "closed": false, + "vertex_flags": [ + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 5, + "y": -86 + }, + { + "x": 40, + "y": -125 + }, + { + "x": 78, + "y": -138 + } + ], + "fill_color": "#00000000", + "outline_color": "#8D4E2D", + "outline_width": 13, + "closed": false, + "vertex_flags": [ + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -1, + "y": -106 + }, + { + "x": -3, + "y": -151 + }, + { + "x": -28, + "y": -171 + } + ], + "fill_color": "#00000000", + "outline_color": "#8D4E2D", + "outline_width": 10, + "closed": false, + "vertex_flags": [ + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 15, + "y": -115 + }, + { + "x": 28, + "y": -163 + }, + { + "x": 48, + "y": -182 + } + ], + "fill_color": "#00000000", + "outline_color": "#8D4E2D", + "outline_width": 9, + "closed": false, + "vertex_flags": [ + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -25, + "y": 64 + }, + { + "x": -32, + "y": 96 + }, + { + "x": -61, + "y": 113 + }, + { + "x": -15, + "y": 109 + }, + { + "x": 1, + "y": 95 + } + ], + "fill_color": "#8F4E2B", + "outline_color": "#79482F", + "outline_width": 2, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 13, + "y": 71 + }, + { + "x": 18, + "y": 100 + }, + { + "x": 51, + "y": 113 + }, + { + "x": 9, + "y": 108 + }, + { + "x": -3, + "y": 97 + } + ], + "fill_color": "#B36937", + "outline_color": "#79482F", + "outline_width": 2, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -21.0, + "y": -146.0 + }, + { + "x": -25.57, + "y": -122.27 + }, + { + "x": -38.57, + "y": -102.16 + }, + { + "x": -58.04, + "y": -88.72 + }, + { + "x": -81.0, + "y": -84.0 + }, + { + "x": -103.96, + "y": -88.72 + }, + { + "x": -123.43, + "y": -102.16 + }, + { + "x": -136.43, + "y": -122.27 + }, + { + "x": -141.0, + "y": -146.0 + }, + { + "x": -136.43, + "y": -169.73 + }, + { + "x": -123.43, + "y": -189.84 + }, + { + "x": -103.96, + "y": -203.28 + }, + { + "x": -81.0, + "y": -208.0 + }, + { + "x": -58.04, + "y": -203.28 + }, + { + "x": -38.57, + "y": -189.84 + }, + { + "x": -25.57, + "y": -169.73 + } + ], + "fill_color": "#237B3B", + "outline_color": "#356D3C", + "outline_width": 3, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 27.0, + "y": -189.0 + }, + { + "x": 22.28, + "y": -163.36 + }, + { + "x": 8.84, + "y": -141.62 + }, + { + "x": -11.27, + "y": -127.1 + }, + { + "x": -35.0, + "y": -122.0 + }, + { + "x": -58.73, + "y": -127.1 + }, + { + "x": -78.84, + "y": -141.62 + }, + { + "x": -92.28, + "y": -163.36 + }, + { + "x": -97.0, + "y": -189.0 + }, + { + "x": -92.28, + "y": -214.64 + }, + { + "x": -78.84, + "y": -236.38 + }, + { + "x": -58.73, + "y": -250.9 + }, + { + "x": -35.0, + "y": -256.0 + }, + { + "x": -11.27, + "y": -250.9 + }, + { + "x": 8.84, + "y": -236.38 + }, + { + "x": 22.28, + "y": -214.64 + } + ], + "fill_color": "#288E3D", + "outline_color": "#356D3C", + "outline_width": 3, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 88.0, + "y": -197.0 + }, + { + "x": 83.2, + "y": -170.98 + }, + { + "x": 69.55, + "y": -148.92 + }, + { + "x": 49.11, + "y": -134.18 + }, + { + "x": 25.0, + "y": -129.0 + }, + { + "x": 0.89, + "y": -134.18 + }, + { + "x": -19.55, + "y": -148.92 + }, + { + "x": -33.2, + "y": -170.98 + }, + { + "x": -38.0, + "y": -197.0 + }, + { + "x": -33.2, + "y": -223.02 + }, + { + "x": -19.55, + "y": -245.08 + }, + { + "x": 0.89, + "y": -259.82 + }, + { + "x": 25.0, + "y": -265.0 + }, + { + "x": 49.11, + "y": -259.82 + }, + { + "x": 69.55, + "y": -245.08 + }, + { + "x": 83.2, + "y": -223.02 + } + ], + "fill_color": "#27873B", + "outline_color": "#356D3C", + "outline_width": 3, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 147.0, + "y": -163.0 + }, + { + "x": 142.36, + "y": -137.74 + }, + { + "x": 129.13, + "y": -116.33 + }, + { + "x": 109.34, + "y": -102.02 + }, + { + "x": 86.0, + "y": -97.0 + }, + { + "x": 62.66, + "y": -102.02 + }, + { + "x": 42.87, + "y": -116.33 + }, + { + "x": 29.64, + "y": -137.74 + }, + { + "x": 25.0, + "y": -163.0 + }, + { + "x": 29.64, + "y": -188.26 + }, + { + "x": 42.87, + "y": -209.67 + }, + { + "x": 62.66, + "y": -223.98 + }, + { + "x": 86.0, + "y": -229.0 + }, + { + "x": 109.34, + "y": -223.98 + }, + { + "x": 129.13, + "y": -209.67 + }, + { + "x": 142.36, + "y": -188.26 + } + ], + "fill_color": "#267F3D", + "outline_color": "#356D3C", + "outline_width": 3, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 152.0, + "y": -119.0 + }, + { + "x": 148.65, + "y": -99.87 + }, + { + "x": 139.11, + "y": -83.64 + }, + { + "x": 124.84, + "y": -72.81 + }, + { + "x": 108.0, + "y": -69.0 + }, + { + "x": 91.16, + "y": -72.81 + }, + { + "x": 76.89, + "y": -83.64 + }, + { + "x": 67.35, + "y": -99.87 + }, + { + "x": 64.0, + "y": -119.0 + }, + { + "x": 67.35, + "y": -138.13 + }, + { + "x": 76.89, + "y": -154.36 + }, + { + "x": 91.16, + "y": -165.19 + }, + { + "x": 108.0, + "y": -169.0 + }, + { + "x": 124.84, + "y": -165.19 + }, + { + "x": 139.11, + "y": -154.36 + }, + { + "x": 148.65, + "y": -138.13 + } + ], + "fill_color": "#236F39", + "outline_color": "#356D3C", + "outline_width": 3, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -66.0, + "y": -114.0 + }, + { + "x": -69.43, + "y": -94.87 + }, + { + "x": -79.18, + "y": -78.64 + }, + { + "x": -93.78, + "y": -67.81 + }, + { + "x": -111.0, + "y": -64.0 + }, + { + "x": -128.22, + "y": -67.81 + }, + { + "x": -142.82, + "y": -78.64 + }, + { + "x": -152.57, + "y": -94.87 + }, + { + "x": -156.0, + "y": -114.0 + }, + { + "x": -152.57, + "y": -133.13 + }, + { + "x": -142.82, + "y": -149.36 + }, + { + "x": -128.22, + "y": -160.19 + }, + { + "x": -111.0, + "y": -164.0 + }, + { + "x": -93.78, + "y": -160.19 + }, + { + "x": -79.18, + "y": -149.36 + }, + { + "x": -69.43, + "y": -133.13 + } + ], + "fill_color": "#2E8944", + "outline_color": "#356D3C", + "outline_width": 3, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 2.0, + "y": -121.0 + }, + { + "x": -2.34, + "y": -100.72 + }, + { + "x": -14.69, + "y": -83.52 + }, + { + "x": -33.19, + "y": -72.03 + }, + { + "x": -55.0, + "y": -68.0 + }, + { + "x": -76.81, + "y": -72.03 + }, + { + "x": -95.31, + "y": -83.52 + }, + { + "x": -107.66, + "y": -100.72 + }, + { + "x": -112.0, + "y": -121.0 + }, + { + "x": -107.66, + "y": -141.28 + }, + { + "x": -95.31, + "y": -158.48 + }, + { + "x": -76.81, + "y": -169.97 + }, + { + "x": -55.0, + "y": -174.0 + }, + { + "x": -33.19, + "y": -169.97 + }, + { + "x": -14.69, + "y": -158.48 + }, + { + "x": -2.34, + "y": -141.28 + } + ], + "fill_color": "#3C994A", + "outline_color": "#3C8744", + "outline_width": 2, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 76.0, + "y": -143.0 + }, + { + "x": 70.75, + "y": -119.27 + }, + { + "x": 55.79, + "y": -99.16 + }, + { + "x": 33.41, + "y": -85.72 + }, + { + "x": 7.0, + "y": -81.0 + }, + { + "x": -19.41, + "y": -85.72 + }, + { + "x": -41.79, + "y": -99.16 + }, + { + "x": -56.75, + "y": -119.27 + }, + { + "x": -62.0, + "y": -143.0 + }, + { + "x": -56.75, + "y": -166.73 + }, + { + "x": -41.79, + "y": -186.84 + }, + { + "x": -19.41, + "y": -200.28 + }, + { + "x": 7.0, + "y": -205.0 + }, + { + "x": 33.41, + "y": -200.28 + }, + { + "x": 55.79, + "y": -186.84 + }, + { + "x": 70.75, + "y": -166.73 + } + ], + "fill_color": "#4BA84E", + "outline_color": "#3C8744", + "outline_width": 2, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 120.0, + "y": -130.0 + }, + { + "x": 115.89, + "y": -110.1 + }, + { + "x": 104.18, + "y": -93.23 + }, + { + "x": 86.66, + "y": -81.96 + }, + { + "x": 66.0, + "y": -78.0 + }, + { + "x": 45.34, + "y": -81.96 + }, + { + "x": 27.82, + "y": -93.23 + }, + { + "x": 16.11, + "y": -110.1 + }, + { + "x": 12.0, + "y": -130.0 + }, + { + "x": 16.11, + "y": -149.9 + }, + { + "x": 27.82, + "y": -166.77 + }, + { + "x": 45.34, + "y": -178.04 + }, + { + "x": 66.0, + "y": -182.0 + }, + { + "x": 86.66, + "y": -178.04 + }, + { + "x": 104.18, + "y": -166.77 + }, + { + "x": 115.89, + "y": -149.9 + } + ], + "fill_color": "#3F9F49", + "outline_color": "#3C8744", + "outline_width": 2, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 31.0, + "y": -205.0 + }, + { + "x": 27.73, + "y": -190.08 + }, + { + "x": 18.41, + "y": -177.42 + }, + { + "x": 4.46, + "y": -168.97 + }, + { + "x": -12.0, + "y": -166.0 + }, + { + "x": -28.46, + "y": -168.97 + }, + { + "x": -42.41, + "y": -177.42 + }, + { + "x": -51.73, + "y": -190.08 + }, + { + "x": -55.0, + "y": -205.0 + }, + { + "x": -51.73, + "y": -219.92 + }, + { + "x": -42.41, + "y": -232.58 + }, + { + "x": -28.46, + "y": -241.03 + }, + { + "x": -12.0, + "y": -244.0 + }, + { + "x": 4.46, + "y": -241.03 + }, + { + "x": 18.41, + "y": -232.58 + }, + { + "x": 27.73, + "y": -219.92 + } + ], + "fill_color": "#46A249", + "outline_color": "#3C8744", + "outline_width": 2, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -113, + "y": -140 + }, + { + "x": -99, + "y": -156 + }, + { + "x": -84, + "y": -140 + }, + { + "x": -91, + "y": -125 + }, + { + "x": -109, + "y": -123 + } + ], + "fill_color": "#6BB85A", + "outline_color": "#6BB85A", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -55, + "y": -201 + }, + { + "x": -38, + "y": -216 + }, + { + "x": -22, + "y": -200 + }, + { + "x": -30, + "y": -183 + }, + { + "x": -49, + "y": -183 + } + ], + "fill_color": "#64B657", + "outline_color": "#64B657", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 10, + "y": -184 + }, + { + "x": 26, + "y": -201 + }, + { + "x": 44, + "y": -185 + }, + { + "x": 34, + "y": -168 + }, + { + "x": 16, + "y": -168 + } + ], + "fill_color": "#69B95A", + "outline_color": "#69B95A", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 61, + "y": -136 + }, + { + "x": 78, + "y": -151 + }, + { + "x": 93, + "y": -136 + }, + { + "x": 82, + "y": -119 + }, + { + "x": 66, + "y": -119 + } + ], + "fill_color": "#6EBA5C", + "outline_color": "#6EBA5C", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -35, + "y": -113 + }, + { + "x": -19, + "y": -128 + }, + { + "x": -4, + "y": -114 + }, + { + "x": -12, + "y": -98 + }, + { + "x": -28, + "y": -96 + } + ], + "fill_color": "#317F3D", + "outline_color": "#317F3D", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 24, + "y": -105 + }, + { + "x": 40, + "y": -121 + }, + { + "x": 55, + "y": -107 + }, + { + "x": 47, + "y": -92 + }, + { + "x": 31, + "y": -91 + } + ], + "fill_color": "#34863F", + "outline_color": "#34863F", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -4, + "y": -79 + }, + { + "x": -33, + "y": -103 + }, + { + "x": -63, + "y": -121 + } + ], + "fill_color": "#00000000", + "outline_color": "#8D4E2D", + "outline_width": 8, + "closed": false, + "vertex_flags": [ + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 7, + "y": -78 + }, + { + "x": 34, + "y": -105 + }, + { + "x": 66, + "y": -123 + } + ], + "fill_color": "#00000000", + "outline_color": "#8D4E2D", + "outline_width": 8, + "closed": false, + "vertex_flags": [ + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -1, + "y": -96 + }, + { + "x": -14, + "y": -125 + }, + { + "x": -24, + "y": -141 + } + ], + "fill_color": "#00000000", + "outline_color": "#995733", + "outline_width": 6, + "closed": false, + "vertex_flags": [ + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 10, + "y": -101 + }, + { + "x": 20, + "y": -133 + }, + { + "x": 32, + "y": -150 + } + ], + "fill_color": "#00000000", + "outline_color": "#995733", + "outline_width": 6, + "closed": false, + "vertex_flags": [ + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -47.0, + "y": -126.0 + }, + { + "x": -48.22, + "y": -121.03 + }, + { + "x": -51.69, + "y": -116.81 + }, + { + "x": -56.88, + "y": -113.99 + }, + { + "x": -63.0, + "y": -113.0 + }, + { + "x": -69.12, + "y": -113.99 + }, + { + "x": -74.31, + "y": -116.81 + }, + { + "x": -77.78, + "y": -121.03 + }, + { + "x": -79.0, + "y": -126.0 + }, + { + "x": -77.78, + "y": -130.97 + }, + { + "x": -74.31, + "y": -135.19 + }, + { + "x": -69.12, + "y": -138.01 + }, + { + "x": -63.0, + "y": -139.0 + }, + { + "x": -56.88, + "y": -138.01 + }, + { + "x": -51.69, + "y": -135.19 + }, + { + "x": -48.22, + "y": -130.97 + } + ], + "fill_color": "#469D4B", + "outline_color": "#469D4B", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 83.0, + "y": -128.0 + }, + { + "x": 81.78, + "y": -123.03 + }, + { + "x": 78.31, + "y": -118.81 + }, + { + "x": 73.12, + "y": -115.99 + }, + { + "x": 67.0, + "y": -115.0 + }, + { + "x": 60.88, + "y": -115.99 + }, + { + "x": 55.69, + "y": -118.81 + }, + { + "x": 52.22, + "y": -123.03 + }, + { + "x": 51.0, + "y": -128.0 + }, + { + "x": 52.22, + "y": -132.97 + }, + { + "x": 55.69, + "y": -137.19 + }, + { + "x": 60.88, + "y": -140.01 + }, + { + "x": 67.0, + "y": -141.0 + }, + { + "x": 73.12, + "y": -140.01 + }, + { + "x": 78.31, + "y": -137.19 + }, + { + "x": 81.78, + "y": -132.97 + } + ], + "fill_color": "#4AA34C", + "outline_color": "#4AA34C", + "outline_width": 0, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -15, + "y": 77 + }, + { + "x": -13, + "y": 27 + }, + { + "x": -6, + "y": -1 + } + ], + "fill_color": "#00000000", + "outline_color": "#7B482B", + "outline_width": 3, + "closed": false, + "vertex_flags": [ + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 13, + "y": 80 + }, + { + "x": 12, + "y": 40 + }, + { + "x": 17, + "y": 19 + } + ], + "fill_color": "#00000000", + "outline_color": "#D38B49", + "outline_width": 3, + "closed": false, + "vertex_flags": [ + 0, + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/props/art_wall_clock-v1.prop b/content/props/art_wall_clock-v1.prop new file mode 100644 index 0000000..e5eae9c --- /dev/null +++ b/content/props/art_wall_clock-v1.prop @@ -0,0 +1,430 @@ +{ + "version": "1.1", + "prop_name": "Wall Clock", + "material_preset": "none", + "tags": ["walls", "objects"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": 70.0, + "y": 0.0 + }, + { + "x": 64.67, + "y": 26.79 + }, + { + "x": 49.5, + "y": 49.5 + }, + { + "x": 26.79, + "y": 64.67 + }, + { + "x": 0.0, + "y": 70.0 + }, + { + "x": -26.79, + "y": 64.67 + }, + { + "x": -49.5, + "y": 49.5 + }, + { + "x": -64.67, + "y": 26.79 + }, + { + "x": -70.0, + "y": 0.0 + }, + { + "x": -64.67, + "y": -26.79 + }, + { + "x": -49.5, + "y": -49.5 + }, + { + "x": -26.79, + "y": -64.67 + }, + { + "x": -0.0, + "y": -70.0 + }, + { + "x": 26.79, + "y": -64.67 + }, + { + "x": 49.5, + "y": -49.5 + }, + { + "x": 64.67, + "y": -26.79 + } + ], + "fill_color": "#DCA678", + "outline_color": "#332B45", + "outline_width": 5, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 54.0, + "y": 0.0 + }, + { + "x": 49.89, + "y": 20.66 + }, + { + "x": 38.18, + "y": 38.18 + }, + { + "x": 20.66, + "y": 49.89 + }, + { + "x": 0.0, + "y": 54.0 + }, + { + "x": -20.66, + "y": 49.89 + }, + { + "x": -38.18, + "y": 38.18 + }, + { + "x": -49.89, + "y": 20.66 + }, + { + "x": -54.0, + "y": 0.0 + }, + { + "x": -49.89, + "y": -20.66 + }, + { + "x": -38.18, + "y": -38.18 + }, + { + "x": -20.66, + "y": -49.89 + }, + { + "x": -0.0, + "y": -54.0 + }, + { + "x": 20.66, + "y": -49.89 + }, + { + "x": 38.18, + "y": -38.18 + }, + { + "x": 49.89, + "y": -20.66 + } + ], + "fill_color": "#FCF2D8", + "outline_color": "#332B45", + "outline_width": 3, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 0, + "y": -45 + }, + { + "x": 0, + "y": -31 + } + ], + "fill_color": "#00000000", + "outline_color": "#75586C", + "outline_width": 4, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 44, + "y": 0 + }, + { + "x": 30, + "y": 0 + } + ], + "fill_color": "#00000000", + "outline_color": "#75586C", + "outline_width": 4, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 0, + "y": 45 + }, + { + "x": 0, + "y": 31 + } + ], + "fill_color": "#00000000", + "outline_color": "#75586C", + "outline_width": 4, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -44, + "y": 0 + }, + { + "x": -30, + "y": 0 + } + ], + "fill_color": "#00000000", + "outline_color": "#75586C", + "outline_width": 4, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": -24 + } + ], + "fill_color": "#00000000", + "outline_color": "#75586C", + "outline_width": 5, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 0, + "y": 0 + }, + { + "x": 24, + "y": 15 + } + ], + "fill_color": "#00000000", + "outline_color": "#75586C", + "outline_width": 5, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 5.0, + "y": 0.0 + }, + { + "x": 4.62, + "y": 1.91 + }, + { + "x": 3.54, + "y": 3.54 + }, + { + "x": 1.91, + "y": 4.62 + }, + { + "x": 0.0, + "y": 5.0 + }, + { + "x": -1.91, + "y": 4.62 + }, + { + "x": -3.54, + "y": 3.54 + }, + { + "x": -4.62, + "y": 1.91 + }, + { + "x": -5.0, + "y": 0.0 + }, + { + "x": -4.62, + "y": -1.91 + }, + { + "x": -3.54, + "y": -3.54 + }, + { + "x": -1.91, + "y": -4.62 + }, + { + "x": -0.0, + "y": -5.0 + }, + { + "x": 1.91, + "y": -4.62 + }, + { + "x": 3.54, + "y": -3.54 + }, + { + "x": 4.62, + "y": -1.91 + } + ], + "fill_color": "#75586C", + "outline_color": "#332B45", + "outline_width": 1, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/props/art_window_frame-v1.prop b/content/props/art_window_frame-v1.prop new file mode 100644 index 0000000..4ca1f37 --- /dev/null +++ b/content/props/art_window_frame-v1.prop @@ -0,0 +1,246 @@ +{ + "version": "1.1", + "prop_name": "Open Window Frame", + "material_preset": "wood", + "tags": ["walls", "buildings"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": -129, + "y": -152 + }, + { + "x": -109, + "y": -152 + }, + { + "x": -109, + "y": 112 + }, + { + "x": -129, + "y": 112 + } + ], + "fill_color": "#EBCF9E", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 109, + "y": -152 + }, + { + "x": 129, + "y": -152 + }, + { + "x": 129, + "y": 112 + }, + { + "x": 109, + "y": 112 + } + ], + "fill_color": "#EBCF9E", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -129, + "y": -152 + }, + { + "x": 129, + "y": -152 + }, + { + "x": 129, + "y": -132 + }, + { + "x": -129, + "y": -132 + } + ], + "fill_color": "#EBCF9E", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -129, + "y": 92 + }, + { + "x": 129, + "y": 92 + }, + { + "x": 129, + "y": 112 + }, + { + "x": -129, + "y": 112 + } + ], + "fill_color": "#EBCF9E", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -10, + "y": -132 + }, + { + "x": 10, + "y": -132 + }, + { + "x": 10, + "y": 92 + }, + { + "x": -10, + "y": 92 + } + ], + "fill_color": "#EBCF9E", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -109, + "y": -30 + }, + { + "x": 109, + "y": -30 + }, + { + "x": 109, + "y": -10 + }, + { + "x": -109, + "y": -10 + } + ], + "fill_color": "#EBCF9E", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -145, + "y": 112 + }, + { + "x": 145, + "y": 112 + }, + { + "x": 145, + "y": 128 + }, + { + "x": -145, + "y": 128 + } + ], + "fill_color": "#C89670", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/props/prop_armchair-v1.prop b/content/props/prop_armchair-v1.prop new file mode 100644 index 0000000..6406e9b --- /dev/null +++ b/content/props/prop_armchair-v1.prop @@ -0,0 +1,276 @@ +{ + "version": "1.1", + "prop_name": "Cozy Armchair", + "material_preset": "wood", + "tags": ["objects"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": -94, + "y": 70 + }, + { + "x": -94, + "y": -82 + }, + { + "x": -71, + "y": -110 + }, + { + "x": 71, + "y": -110 + }, + { + "x": 94, + "y": -82 + }, + { + "x": 94, + "y": 70 + }, + { + "x": 76, + "y": 91 + }, + { + "x": -76, + "y": 91 + } + ], + "fill_color": "#80A9AF", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -65, + "y": -84 + }, + { + "x": 65, + "y": -84 + }, + { + "x": 65, + "y": 22 + }, + { + "x": -65, + "y": 22 + } + ], + "fill_color": "#A8CED0", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -73, + "y": 18 + }, + { + "x": 73, + "y": 18 + }, + { + "x": 82, + "y": 69 + }, + { + "x": -82, + "y": 69 + } + ], + "fill_color": "#C3DDCF", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -95, + "y": -3 + }, + { + "x": -72, + "y": -17 + }, + { + "x": -59, + "y": 2 + }, + { + "x": -59, + "y": 79 + }, + { + "x": -95, + "y": 79 + } + ], + "fill_color": "#80A9AF", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 95, + "y": -3 + }, + { + "x": 72, + "y": -17 + }, + { + "x": 59, + "y": 2 + }, + { + "x": 59, + "y": 79 + }, + { + "x": 95, + "y": 79 + } + ], + "fill_color": "#80A9AF", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -71, + "y": 85 + }, + { + "x": -48, + "y": 85 + }, + { + "x": -48, + "y": 107 + }, + { + "x": -71, + "y": 107 + } + ], + "fill_color": "#755369", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 48, + "y": 85 + }, + { + "x": 71, + "y": 85 + }, + { + "x": 71, + "y": 107 + }, + { + "x": 48, + "y": 107 + } + ], + "fill_color": "#755369", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/props/prop_coffee_table-v1.prop b/content/props/prop_coffee_table-v1.prop new file mode 100644 index 0000000..07be85c --- /dev/null +++ b/content/props/prop_coffee_table-v1.prop @@ -0,0 +1,204 @@ +{ + "version": "1.1", + "prop_name": "Coffee Table", + "material_preset": "wood", + "tags": ["objects"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": -150, + "y": -46 + }, + { + "x": 150, + "y": -46 + }, + { + "x": 150, + "y": -18 + }, + { + "x": -150, + "y": -18 + } + ], + "fill_color": "#B9805D", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -128, + "y": -18 + }, + { + "x": 128, + "y": -18 + }, + { + "x": 128, + "y": -6 + }, + { + "x": -128, + "y": -6 + } + ], + "fill_color": "#D69E70", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -124, + "y": -7 + }, + { + "x": -98, + "y": -7 + }, + { + "x": -90, + "y": 94 + }, + { + "x": -116, + "y": 94 + } + ], + "fill_color": "#A66F58", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 98, + "y": -7 + }, + { + "x": 124, + "y": -7 + }, + { + "x": 116, + "y": 94 + }, + { + "x": 90, + "y": 94 + } + ], + "fill_color": "#A66F58", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -101, + "y": 46 + }, + { + "x": 101, + "y": 46 + }, + { + "x": 101, + "y": 58 + }, + { + "x": -101, + "y": 58 + } + ], + "fill_color": "#C38C68", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": -115, + "y": -38 + }, + { + "x": 115, + "y": -38 + } + ], + "fill_color": "#00000000", + "outline_color": "#E8B98A", + "outline_width": 3, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/props/prop_couch-v1.prop b/content/props/prop_couch-v1.prop new file mode 100644 index 0000000..e711d11 --- /dev/null +++ b/content/props/prop_couch-v1.prop @@ -0,0 +1,298 @@ +{ + "version": "1.1", + "prop_name": "Cozy Couch", + "material_preset": "wood", + "tags": ["objects"], + "shapes": [ + { + "shape_type": "polygon", + "points": [ + { + "x": -168, + "y": 74 + }, + { + "x": -168, + "y": -56 + }, + { + "x": -145, + "y": -91 + }, + { + "x": 145, + "y": -91 + }, + { + "x": 168, + "y": -56 + }, + { + "x": 168, + "y": 74 + }, + { + "x": 146, + "y": 94 + }, + { + "x": -146, + "y": 94 + } + ], + "fill_color": "#B7767B", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -137, + "y": -62 + }, + { + "x": 137, + "y": -62 + }, + { + "x": 137, + "y": 26 + }, + { + "x": -137, + "y": 26 + } + ], + "fill_color": "#D9908A", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "line", + "points": [ + { + "x": 0, + "y": -61 + }, + { + "x": 0, + "y": 25 + } + ], + "fill_color": "#00000000", + "outline_color": "#985C71", + "outline_width": 4, + "closed": false, + "vertex_flags": [ + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -142, + "y": 19 + }, + { + "x": 142, + "y": 19 + }, + { + "x": 151, + "y": 72 + }, + { + "x": -151, + "y": 72 + } + ], + "fill_color": "#EDB09A", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -167, + "y": -13 + }, + { + "x": -144, + "y": -24 + }, + { + "x": -131, + "y": -10 + }, + { + "x": -131, + "y": 82 + }, + { + "x": -168, + "y": 82 + } + ], + "fill_color": "#B7767B", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 167, + "y": -13 + }, + { + "x": 144, + "y": -24 + }, + { + "x": 131, + "y": -10 + }, + { + "x": 131, + "y": 82 + }, + { + "x": 168, + "y": 82 + } + ], + "fill_color": "#B7767B", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": -139, + "y": 88 + }, + { + "x": -113, + "y": 88 + }, + { + "x": -113, + "y": 111 + }, + { + "x": -139, + "y": 111 + } + ], + "fill_color": "#755369", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + }, + { + "shape_type": "polygon", + "points": [ + { + "x": 113, + "y": 88 + }, + { + "x": 139, + "y": 88 + }, + { + "x": 139, + "y": 111 + }, + { + "x": 113, + "y": 111 + } + ], + "fill_color": "#755369", + "outline_color": "#332B45", + "outline_width": 4, + "closed": true, + "vertex_flags": [ + 0, + 0, + 0, + 0 + ], + "reshaped": false + } + ], + "anchors": { + "origin": { + "role": "pivot", + "x": 0, + "y": 0, + "rotation_deg": 0, + "z_index_offset": 0, + "allowed_hands": "both" + } + }, + "metadata": { + "created_at": "2026-10-15T00:00:00", + "modified_at": "2026-10-15T00:00:00" + } +} diff --git a/content/stages/background01.scene b/content/stages/background01.scene new file mode 100644 index 0000000..5184bd5 --- /dev/null +++ b/content/stages/background01.scene @@ -0,0 +1,641 @@ +{ + "version": "1.5", + "scene_name": "Living Room", + "sky_id": "sky_clear_day", + "camera": { + "position": { + "x": 194.703018188477, + "y": -486.092163085938 + }, + "zoom": 0.665500044822693 + }, + "objects": [ + { + "file_id": "wall_panel_1", + "type": "wall_panel", + "position": { + "x": 0.0, + "y": -820.0 + }, + "rotation": 0.0, + "size": { + "x": 1120.0, + "y": 170.0 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_2", + "type": "wall_panel", + "position": { + "x": -360.0, + "y": -535.0 + }, + "rotation": 0.0, + "size": { + "x": 440.0, + "y": 400.0 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_3", + "type": "wall_panel", + "position": { + "x": 360.0, + "y": -535.0 + }, + "rotation": 0.0, + "size": { + "x": 440.0, + "y": 400.0 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "scenery_artwork_1", + "type": "scenery_artwork", + "position": { + "x": 0.0, + "y": -530.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_window_frame-v1.prop", + "display_name": "Window frame", + "depth": "near", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -147.0, + "y": -154.0 + }, + "size": { + "x": 294.0, + "y": 284.0 + } + } + }, + { + "file_id": "scenery_artwork_2", + "type": "scenery_artwork", + "position": { + "x": -430.0, + "y": -570.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_wall_clock-v1.prop", + "display_name": "Wall clock", + "depth": "near", + "scale": { + "x": 0.800000011920929, + "y": 0.800000011920929 + }, + "last_known_local_bounds": { + "position": { + "x": -72.5, + "y": -72.5 + }, + "size": { + "x": 145.0, + "y": 145.0 + } + } + }, + { + "file_id": "scenery_artwork_3", + "type": "scenery_artwork", + "position": { + "x": 365.0, + "y": -460.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_fireplace_mantel-v2.prop", + "display_name": "Fireplace", + "depth": "near", + "scale": { + "x": 0.819999992847443, + "y": 0.819999992847443 + }, + "last_known_local_bounds": { + "position": { + "x": -153.0, + "y": -116.0 + }, + "size": { + "x": 306.0, + "y": 244.0 + } + } + }, + { + "file_id": "scenery_artwork_4", + "type": "scenery_artwork", + "position": { + "x": -475.0, + "y": -310.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_bookshelf-v1.prop", + "display_name": "Bookshelf", + "depth": "near", + "scale": { + "x": 0.819999992847443, + "y": 0.819999992847443 + }, + "last_known_local_bounds": { + "position": { + "x": -104.0, + "y": -190.0 + }, + "size": { + "x": 208.0, + "y": 315.0 + } + } + }, + { + "file_id": "scenery_artwork_5", + "type": "scenery_artwork", + "position": { + "x": 185.0, + "y": -575.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_framed_picture-v1.prop", + "display_name": "Framed picture", + "depth": "near", + "scale": { + "x": 0.75, + "y": 0.75 + }, + "last_known_local_bounds": { + "position": { + "x": -112.0, + "y": -84.0 + }, + "size": { + "x": 224.0, + "y": 168.0 + } + } + }, + { + "file_id": "terrain_1", + "type": "terrain", + "position": { + "x": -774.0, + "y": 832.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_2", + "type": "terrain", + "position": { + "x": -588.0, + "y": 840.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_3", + "type": "terrain", + "position": { + "x": -374.0, + "y": 832.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_4", + "type": "terrain", + "position": { + "x": -174.0, + "y": 832.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_5", + "type": "terrain", + "position": { + "x": 26.0, + "y": 832.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_6", + "type": "terrain", + "position": { + "x": 226.0, + "y": 832.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_7", + "type": "terrain", + "position": { + "x": 426.0, + "y": 832.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "scenery_artwork_6", + "type": "scenery_artwork", + "position": { + "x": 0.0, + "y": 112.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_rug-v1.prop", + "display_name": "Floor rug", + "depth": "floor", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -179.0, + "y": -60.0 + }, + "size": { + "x": 358.0, + "y": 139.0 + } + } + }, + { + "file_id": "scenery_artwork_7", + "type": "scenery_artwork", + "position": { + "x": 500.0, + "y": 84.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_potted_plant-v1.prop", + "display_name": "Potted plant", + "depth": "floor", + "scale": { + "x": 0.800000011920929, + "y": 0.800000011920929 + }, + "last_known_local_bounds": { + "position": { + "x": -62.5, + "y": -114.5 + }, + "size": { + "x": 125.0, + "y": 219.0 + } + } + }, + { + "file_id": "prop_1", + "type": "prop", + "position": { + "x": -364.0, + "y": 680.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/prop_couch-v1.prop", + "stage_object_id": "starter_couch", + "roles": { + "depth": "behind", + "start_motion": "still", + "holds_other_props": false, + "blocks_stickmen": false, + "can_pick_up": false, + "released_blocks_stickmen": true, + "released_bumps_other_props": true, + "legacy_draw_order": false + }, + "use_spots": [], + "next_use_spot_number": 1, + "wobble_on_impact": false, + "scale": { + "x": 0.819999992847443, + "y": 0.819999992847443 + }, + "material_preset": 1 + }, + { + "file_id": "prop_2", + "type": "prop", + "position": { + "x": 36.0, + "y": 684.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/prop_armchair-v1.prop", + "stage_object_id": "starter_armchair", + "roles": { + "depth": "behind", + "start_motion": "still", + "holds_other_props": false, + "blocks_stickmen": false, + "can_pick_up": false, + "released_blocks_stickmen": true, + "released_bumps_other_props": true, + "legacy_draw_order": false + }, + "use_spots": [], + "next_use_spot_number": 1, + "wobble_on_impact": false, + "scale": { + "x": 0.779999971389771, + "y": 0.779999971389771 + }, + "material_preset": 1 + }, + { + "file_id": "prop_3", + "type": "prop", + "position": { + "x": -159.0, + "y": 748.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/prop_coffee_table-v1.prop", + "stage_object_id": "starter_coffee_table", + "roles": { + "depth": "front", + "start_motion": "still", + "holds_other_props": true, + "blocks_stickmen": false, + "can_pick_up": false, + "released_blocks_stickmen": true, + "released_bumps_other_props": true, + "legacy_draw_order": false + }, + "use_spots": [], + "next_use_spot_number": 1, + "wobble_on_impact": false, + "scale": { + "x": 0.75, + "y": 0.75 + }, + "material_preset": 1 + }, + { + "file_id": "stickman_1", + "type": "stickman", + "position": { + "x": -690.0, + "y": 152.0 + }, + "rotation": 0.0, + "stk_path": "res://content/stickmen/break.stk", + "facing_profile": "FORWARD", + "action_queue": [ + { + "type": "move_to", + "target": { + "x": 588.195678710938, + "y": 144.184204101563 + }, + "style": "walk" + } + ] + }, + { + "file_id": "wall_panel_4", + "type": "wall_panel", + "position": { + "x": 206.112670898438, + "y": -758.285827636719 + }, + "rotation": 0.0, + "size": { + "x": 634.58154296875, + "y": 125.426147460938 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_5", + "type": "wall_panel", + "position": { + "x": 16.1107177734375, + "y": -777.534423828125 + }, + "rotation": 0.0, + "size": { + "x": 281.898193359375, + "y": 181.309020996094 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_6", + "type": "wall_panel", + "position": { + "x": -37.2885131835938, + "y": -739.658203125 + }, + "rotation": 0.0, + "size": { + "x": 276.930786132813, + "y": 100.589233398438 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_7", + "type": "wall_panel", + "position": { + "x": 218.531005859375, + "y": -634.1015625 + }, + "rotation": 0.0, + "size": { + "x": 180.067138671875, + "y": 257.061401367188 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_8", + "type": "wall_panel", + "position": { + "x": -193.760681152344, + "y": -507.433654785156 + }, + "rotation": 0.0, + "size": { + "x": 142.811889648438, + "y": 428.435668945313 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_9", + "type": "wall_panel", + "position": { + "x": -5.0006103515625, + "y": -762.63232421875 + }, + "rotation": 0.0, + "size": { + "x": 321.63720703125, + "y": 211.113220214844 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_10", + "type": "wall_panel", + "position": { + "x": 29.7709350585938, + "y": -198.21484375 + }, + "rotation": 0.0, + "size": { + "x": 736.412475585938, + "y": 403.598876953125 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_11", + "type": "wall_panel", + "position": { + "x": 458.206604003906, + "y": -332.954742431641 + }, + "rotation": 0.0, + "size": { + "x": 661.901977539063, + "y": 986.023010253906 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_12", + "type": "wall_panel", + "position": { + "x": -522.22802734375, + "y": -360.275299072266 + }, + "rotation": 0.0, + "size": { + "x": 789.811767578125, + "y": 1020.79449462891 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_13", + "type": "wall_panel", + "position": { + "x": 44.6731567382813, + "y": -16.9058227539063 + }, + "rotation": 0.0, + "size": { + "x": 1493.93664550781, + "y": 430.919311523438 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_14", + "type": "wall_panel", + "position": { + "x": -199.348937988281, + "y": 81.19970703125 + }, + "rotation": 0.0, + "size": { + "x": 1577.14001464844, + "y": 259.545166015625 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_15", + "type": "wall_panel", + "position": { + "x": 61.60986328125, + "y": 297.579467773438 + }, + "rotation": 0.0, + "size": { + "x": 1281.58154296875, + "y": 192.485595703125 + }, + "finish_id": "finish_wall_wood", + "color": "#5a9d70ff" + } + ], + "event_rules": [], + "metadata": { + "created_unix": 1792022400, + "modified_unix": 1792163812.927, + "autosave": false, + "source_path": "" + } +} \ No newline at end of file diff --git a/content/stages/background02.scene b/content/stages/background02.scene new file mode 100644 index 0000000..d5324c2 --- /dev/null +++ b/content/stages/background02.scene @@ -0,0 +1,659 @@ +{ + "version": "1.5", + "scene_name": "Living Room", + "sky_id": "sky_clear_day", + "camera": { + "position": { + "x": -43.7599487304688, + "y": 247.024810791016 + }, + "zoom": 0.665500164031982 + }, + "objects": [ + { + "file_id": "wall_panel_1", + "type": "wall_panel", + "position": { + "x": 0.0, + "y": -820.0 + }, + "rotation": 0.0, + "size": { + "x": 1120.0, + "y": 170.0 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_2", + "type": "wall_panel", + "position": { + "x": -360.0, + "y": -535.0 + }, + "rotation": 0.0, + "size": { + "x": 440.0, + "y": 400.0 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_3", + "type": "wall_panel", + "position": { + "x": 360.0, + "y": -535.0 + }, + "rotation": 0.0, + "size": { + "x": 440.0, + "y": 400.0 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "scenery_artwork_1", + "type": "scenery_artwork", + "position": { + "x": 0.0, + "y": -530.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_window_frame-v1.prop", + "display_name": "Window frame", + "depth": "near", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -147.0, + "y": -154.0 + }, + "size": { + "x": 294.0, + "y": 284.0 + } + } + }, + { + "file_id": "scenery_artwork_2", + "type": "scenery_artwork", + "position": { + "x": -430.0, + "y": -570.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_wall_clock-v1.prop", + "display_name": "Wall clock", + "depth": "near", + "scale": { + "x": 0.800000011920929, + "y": 0.800000011920929 + }, + "last_known_local_bounds": { + "position": { + "x": -72.5, + "y": -72.5 + }, + "size": { + "x": 145.0, + "y": 145.0 + } + } + }, + { + "file_id": "scenery_artwork_3", + "type": "scenery_artwork", + "position": { + "x": 365.0, + "y": -460.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_fireplace_mantel-v2.prop", + "display_name": "Fireplace", + "depth": "near", + "scale": { + "x": 0.819999992847443, + "y": 0.819999992847443 + }, + "last_known_local_bounds": { + "position": { + "x": -153.0, + "y": -116.0 + }, + "size": { + "x": 306.0, + "y": 244.0 + } + } + }, + { + "file_id": "scenery_artwork_4", + "type": "scenery_artwork", + "position": { + "x": -475.0, + "y": -310.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_bookshelf-v1.prop", + "display_name": "Bookshelf", + "depth": "near", + "scale": { + "x": 0.819999992847443, + "y": 0.819999992847443 + }, + "last_known_local_bounds": { + "position": { + "x": -104.0, + "y": -190.0 + }, + "size": { + "x": 208.0, + "y": 315.0 + } + } + }, + { + "file_id": "scenery_artwork_5", + "type": "scenery_artwork", + "position": { + "x": 185.0, + "y": -575.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_framed_picture-v1.prop", + "display_name": "Framed picture", + "depth": "near", + "scale": { + "x": 0.75, + "y": 0.75 + }, + "last_known_local_bounds": { + "position": { + "x": -112.0, + "y": -84.0 + }, + "size": { + "x": 224.0, + "y": 168.0 + } + } + }, + { + "file_id": "scenery_artwork_6", + "type": "scenery_artwork", + "position": { + "x": 0.0, + "y": 112.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_rug-v1.prop", + "display_name": "Floor rug", + "depth": "floor", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -179.0, + "y": -60.0 + }, + "size": { + "x": 358.0, + "y": 139.0 + } + } + }, + { + "file_id": "scenery_artwork_7", + "type": "scenery_artwork", + "position": { + "x": 500.0, + "y": 84.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_potted_plant-v1.prop", + "display_name": "Potted plant", + "depth": "floor", + "scale": { + "x": 0.800000011920929, + "y": 0.800000011920929 + }, + "last_known_local_bounds": { + "position": { + "x": -62.5, + "y": -114.5 + }, + "size": { + "x": 125.0, + "y": 219.0 + } + } + }, + { + "file_id": "stickman_1", + "type": "stickman", + "position": { + "x": -690.0, + "y": 152.0 + }, + "rotation": 0.0, + "stk_path": "res://content/stickmen/break.stk", + "facing_profile": "FORWARD", + "action_queue": [ + { + "type": "move_to", + "target": { + "x": 588.195678710938, + "y": 144.184204101563 + }, + "style": "walk" + } + ] + }, + { + "file_id": "wall_panel_4", + "type": "wall_panel", + "position": { + "x": 206.112670898438, + "y": -758.285827636719 + }, + "rotation": 0.0, + "size": { + "x": 634.58154296875, + "y": 125.426147460938 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_5", + "type": "wall_panel", + "position": { + "x": 16.1107177734375, + "y": -777.534423828125 + }, + "rotation": 0.0, + "size": { + "x": 281.898193359375, + "y": 181.309020996094 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_6", + "type": "wall_panel", + "position": { + "x": -37.2885131835938, + "y": -739.658203125 + }, + "rotation": 0.0, + "size": { + "x": 276.930786132813, + "y": 100.589233398438 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_7", + "type": "wall_panel", + "position": { + "x": 218.531005859375, + "y": -634.1015625 + }, + "rotation": 0.0, + "size": { + "x": 180.067138671875, + "y": 257.061401367188 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_8", + "type": "wall_panel", + "position": { + "x": -193.760681152344, + "y": -507.433654785156 + }, + "rotation": 0.0, + "size": { + "x": 142.811889648438, + "y": 428.435668945313 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_9", + "type": "wall_panel", + "position": { + "x": -5.0006103515625, + "y": -762.63232421875 + }, + "rotation": 0.0, + "size": { + "x": 321.63720703125, + "y": 211.113220214844 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_10", + "type": "wall_panel", + "position": { + "x": 29.7709350585938, + "y": -198.21484375 + }, + "rotation": 0.0, + "size": { + "x": 736.412475585938, + "y": 403.598876953125 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_11", + "type": "wall_panel", + "position": { + "x": 458.206604003906, + "y": -332.954742431641 + }, + "rotation": 0.0, + "size": { + "x": 661.901977539063, + "y": 986.023010253906 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_12", + "type": "wall_panel", + "position": { + "x": -522.22802734375, + "y": -360.275299072266 + }, + "rotation": 0.0, + "size": { + "x": 789.811767578125, + "y": 1020.79449462891 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_13", + "type": "wall_panel", + "position": { + "x": 44.6731567382813, + "y": -16.9058227539063 + }, + "rotation": 0.0, + "size": { + "x": 1493.93664550781, + "y": 430.919311523438 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_14", + "type": "wall_panel", + "position": { + "x": -199.348937988281, + "y": 81.19970703125 + }, + "rotation": 0.0, + "size": { + "x": 1577.14001464844, + "y": 259.545166015625 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_15", + "type": "wall_panel", + "position": { + "x": 61.60986328125, + "y": 297.579467773438 + }, + "rotation": 0.0, + "size": { + "x": 1281.58154296875, + "y": 192.485595703125 + }, + "finish_id": "finish_wall_wood", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_16", + "type": "wall_panel", + "position": { + "x": -1040.0, + "y": -848.0 + }, + "rotation": 0.0, + "size": { + "x": 32.0, + "y": 32.0 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_17", + "type": "wall_panel", + "position": { + "x": -64.0, + "y": -176.0 + }, + "rotation": 0.0, + "size": { + "x": 2048.0, + "y": 1504.0 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_18", + "type": "wall_panel", + "position": { + "x": -64.0, + "y": 592.0 + }, + "rotation": 0.0, + "size": { + "x": 2048.0, + "y": 32.0 + }, + "finish_id": "finish_wall_wood", + "color": "#5a9d70ff" + }, + { + "file_id": "terrain_1", + "type": "terrain", + "position": { + "x": -960.0, + "y": 640.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_2", + "type": "terrain", + "position": { + "x": -768.0, + "y": 640.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_3", + "type": "terrain", + "position": { + "x": -576.0, + "y": 640.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_4", + "type": "terrain", + "position": { + "x": -384.0, + "y": 640.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_5", + "type": "terrain", + "position": { + "x": -192.0, + "y": 640.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_6", + "type": "terrain", + "position": { + "x": 0.0, + "y": 640.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_7", + "type": "terrain", + "position": { + "x": 192.0, + "y": 640.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_8", + "type": "terrain", + "position": { + "x": 384.0, + "y": 640.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_9", + "type": "terrain", + "position": { + "x": 576.0, + "y": 640.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_10", + "type": "terrain", + "position": { + "x": 768.0, + "y": 640.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_11", + "type": "terrain", + "position": { + "x": 960.0, + "y": 640.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + } + ], + "event_rules": [], + "metadata": { + "created_unix": 1792022400, + "modified_unix": 1792165751.76, + "autosave": false, + "source_path": "" + } +} \ No newline at end of file diff --git a/content/stages/background03.scene b/content/stages/background03.scene new file mode 100644 index 0000000..9f53033 --- /dev/null +++ b/content/stages/background03.scene @@ -0,0 +1,319 @@ +{ + "version": "1.5", + "scene_name": "Untitled", + "sky_id": "sky_neutral", + "camera": { + "position": { + "x": 143.607360839844, + "y": -79.9739379882813 + }, + "zoom": 0.732050061225891 + }, + "objects": [ + { + "file_id": "terrain_1", + "type": "terrain", + "position": { + "x": -768.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_2", + "type": "terrain", + "position": { + "x": -576.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_3", + "type": "terrain", + "position": { + "x": -384.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_4", + "type": "terrain", + "position": { + "x": -192.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_5", + "type": "terrain", + "position": { + "x": 0.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_6", + "type": "terrain", + "position": { + "x": 192.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_7", + "type": "terrain", + "position": { + "x": 384.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_8", + "type": "terrain", + "position": { + "x": 576.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_9", + "type": "terrain", + "position": { + "x": 768.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "terrain_10", + "type": "terrain", + "position": { + "x": 960.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_wood" + }, + { + "file_id": "scenery_artwork_1", + "type": "scenery_artwork", + "position": { + "x": -93.5323791503906, + "y": -461.938385009766 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_fireplace_mantel-v2.prop", + "display_name": "Cozy Brick Fireplace Mantel", + "depth": "near", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -386.467620849609, + "y": -306.061584472656 + }, + "size": { + "x": 772.935241699219, + "y": 612.123168945313 + } + } + }, + { + "file_id": "stickman_1", + "type": "stickman", + "position": { + "x": 600.0, + "y": -457.0 + }, + "rotation": 0.0, + "stk_path": "res://content/stickmen/break.stk", + "facing_profile": "FORWARD", + "action_queue": [ + { + "type": "move_to", + "target": { + "x": -711.525268554688, + "y": -142.811157226563 + }, + "style": "run" + } + ] + }, + { + "file_id": "wall_panel_1", + "type": "wall_panel", + "position": { + "x": 36.0, + "y": -612.0 + }, + "rotation": 0.0, + "size": { + "x": 1560.0, + "y": 840.0 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_2", + "type": "wall_panel", + "position": { + "x": -36.0, + "y": -528.0 + }, + "rotation": 0.0, + "size": { + "x": 1656.0, + "y": 960.0 + }, + "finish_id": "finish_wall_paint", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_3", + "type": "wall_panel", + "position": { + "x": 120.0, + "y": -84.0 + }, + "rotation": 0.0, + "size": { + "x": 1728.0, + "y": 168.0 + }, + "finish_id": "finish_wall_wood", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_4", + "type": "wall_panel", + "position": { + "x": -684.0, + "y": -156.0 + }, + "rotation": 0.0, + "size": { + "x": 360.0, + "y": 24.0 + }, + "finish_id": "finish_wall_wood", + "color": "#5a9d70ff" + }, + { + "file_id": "wall_panel_5", + "type": "wall_panel", + "position": { + "x": -792.0, + "y": -84.0 + }, + "rotation": 0.0, + "size": { + "x": 144.0, + "y": 168.0 + }, + "finish_id": "finish_wall_wood", + "color": "#5a9d70ff" + }, + { + "file_id": "scenery_artwork_2", + "type": "scenery_artwork", + "position": { + "x": -109.0, + "y": -108.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_rug-v1.prop", + "display_name": "Patterned Rug", + "depth": "near", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -179.0, + "y": -60.0 + }, + "size": { + "x": 358.0, + "y": 139.0 + } + } + } + ], + "event_rules": [], + "metadata": { + "created_unix": 1792178012.798, + "modified_unix": 1792178012.798, + "autosave": false, + "source_path": "" + } +} \ No newline at end of file diff --git a/content/stages/starters/blank.scene b/content/stages/starters/blank.scene new file mode 100644 index 0000000..b59e1db --- /dev/null +++ b/content/stages/starters/blank.scene @@ -0,0 +1,9 @@ +{ + "version": "1.5", + "scene_name": "Blank Scene", + "sky_id": "sky_neutral", + "camera": {"position": {"x": 0.0, "y": -400.0}, "zoom": 0.5}, + "objects": [], + "event_rules": [], + "metadata": {"created_unix": 1792022400, "modified_unix": 1792022400, "autosave": false, "source_path": ""} +} diff --git a/content/stages/starters/living_room.scene b/content/stages/starters/living_room.scene new file mode 100644 index 0000000..2097f96 --- /dev/null +++ b/content/stages/starters/living_room.scene @@ -0,0 +1,30 @@ +{ + "version": "1.5", + "scene_name": "Living Room", + "sky_id": "sky_clear_day", + "camera": {"position": {"x": 0.0, "y": -420.0}, "zoom": 0.5}, + "objects": [ + {"file_id":"wall_top","type":"wall_panel","position":{"x":0.0,"y":-820.0},"rotation":0.0,"size":{"x":1120.0,"y":170.0},"finish_id":"finish_wall_paint","color":"#5A9D70"}, + {"file_id":"wall_left","type":"wall_panel","position":{"x":-360.0,"y":-535.0},"rotation":0.0,"size":{"x":440.0,"y":400.0},"finish_id":"finish_wall_paint","color":"#5A9D70"}, + {"file_id":"wall_right","type":"wall_panel","position":{"x":360.0,"y":-535.0},"rotation":0.0,"size":{"x":440.0,"y":400.0},"finish_id":"finish_wall_paint","color":"#5A9D70"}, + {"file_id":"window_frame","type":"scenery_artwork","asset_id":"res://content/props/art_window_frame-v1.prop","display_name":"Window frame","depth":"near","position":{"x":0.0,"y":-530.0},"rotation":0.0,"scale":{"x":1.0,"y":1.0}}, + {"file_id":"wall_clock","type":"scenery_artwork","asset_id":"res://content/props/art_wall_clock-v1.prop","display_name":"Wall clock","depth":"near","position":{"x":-430.0,"y":-570.0},"rotation":0.0,"scale":{"x":0.8,"y":0.8}}, + {"file_id":"fireplace","type":"scenery_artwork","asset_id":"res://content/props/art_fireplace_mantel-v2.prop","display_name":"Fireplace","depth":"near","position":{"x":365.0,"y":-460.0},"rotation":0.0,"scale":{"x":0.82,"y":0.82}}, + {"file_id":"bookshelf","type":"scenery_artwork","asset_id":"res://content/props/art_bookshelf-v1.prop","display_name":"Bookshelf","depth":"near","position":{"x":-475.0,"y":-310.0},"rotation":0.0,"scale":{"x":0.82,"y":0.82}}, + {"file_id":"picture","type":"scenery_artwork","asset_id":"res://content/props/art_framed_picture-v1.prop","display_name":"Framed picture","depth":"near","position":{"x":185.0,"y":-575.0},"rotation":0.0,"scale":{"x":0.75,"y":0.75}}, + {"file_id":"ground_wood_1","type":"terrain","asset_id":"ground","position":{"x":-600.0,"y":200.0},"rotation":0.0,"scale":{"x":1.0,"y":1.0},"finish_id":"finish_ground_wood"}, + {"file_id":"ground_wood_2","type":"terrain","asset_id":"ground","position":{"x":-400.0,"y":200.0},"rotation":0.0,"scale":{"x":1.0,"y":1.0},"finish_id":"finish_ground_wood"}, + {"file_id":"ground_wood_3","type":"terrain","asset_id":"ground","position":{"x":-200.0,"y":200.0},"rotation":0.0,"scale":{"x":1.0,"y":1.0},"finish_id":"finish_ground_wood"}, + {"file_id":"ground_wood_4","type":"terrain","asset_id":"ground","position":{"x":0.0,"y":200.0},"rotation":0.0,"scale":{"x":1.0,"y":1.0},"finish_id":"finish_ground_wood"}, + {"file_id":"ground_wood_5","type":"terrain","position":{"x":200.0,"y":200.0},"rotation":0.0,"scale":{"x":1.0,"y":1.0},"finish_id":"finish_ground_wood","asset_id":"ground"}, + {"file_id":"ground_wood_6","type":"terrain","asset_id":"ground","position":{"x":400.0,"y":200.0},"rotation":0.0,"scale":{"x":1.0,"y":1.0},"finish_id":"finish_ground_wood"}, + {"file_id":"ground_wood_7","type":"terrain","asset_id":"ground","position":{"x":600.0,"y":200.0},"rotation":0.0,"scale":{"x":1.0,"y":1.0},"finish_id":"finish_ground_wood"}, + {"file_id":"rug","type":"scenery_artwork","asset_id":"res://content/props/art_rug-v1.prop","display_name":"Floor rug","depth":"floor","position":{"x":0.0,"y":112.0},"rotation":0.0,"scale":{"x":1.0,"y":1.0}}, + {"file_id":"plant","type":"scenery_artwork","asset_id":"res://content/props/art_potted_plant-v1.prop","display_name":"Potted plant","depth":"floor","position":{"x":500.0,"y":84.0},"rotation":0.0,"scale":{"x":0.8,"y":0.8}}, + {"file_id":"couch","type":"prop","asset_id":"res://content/props/prop_couch-v1.prop","position":{"x":-190.0,"y":48.0},"rotation":0.0,"scale":{"x":0.82,"y":0.82},"stage_object_id":"starter_couch","roles":{"depth":"behind","start_motion":"still","holds_other_props":false,"blocks_stickmen":true,"can_pick_up":false,"released_blocks_stickmen":true,"released_bumps_other_props":true,"legacy_draw_order":false},"use_spots":[],"next_use_spot_number":1,"wobble_on_impact":false,"material_preset":"WOOD"}, + {"file_id":"armchair","type":"prop","asset_id":"res://content/props/prop_armchair-v1.prop","position":{"x":210.0,"y":52.0},"rotation":0.0,"scale":{"x":0.78,"y":0.78},"stage_object_id":"starter_armchair","roles":{"depth":"behind","start_motion":"still","holds_other_props":false,"blocks_stickmen":true,"can_pick_up":false,"released_blocks_stickmen":true,"released_bumps_other_props":true,"legacy_draw_order":false},"use_spots":[],"next_use_spot_number":1,"wobble_on_impact":false,"material_preset":"WOOD"}, + {"file_id":"coffee_table","type":"prop","asset_id":"res://content/props/prop_coffee_table-v1.prop","position":{"x":15.0,"y":116.0},"rotation":0.0,"scale":{"x":0.75,"y":0.75},"stage_object_id":"starter_coffee_table","roles":{"depth":"front","start_motion":"still","holds_other_props":true,"blocks_stickmen":true,"can_pick_up":false,"released_blocks_stickmen":true,"released_bumps_other_props":true,"legacy_draw_order":false},"use_spots":[],"next_use_spot_number":1,"wobble_on_impact":false,"material_preset":"WOOD"} + ], + "event_rules": [], + "metadata": {"created_unix": 1792022400, "modified_unix": 1792022400, "autosave": false, "source_path": ""} +} diff --git a/content/stages/starters/town_street.scene b/content/stages/starters/town_street.scene new file mode 100644 index 0000000..7dea243 --- /dev/null +++ b/content/stages/starters/town_street.scene @@ -0,0 +1,17 @@ +{ + "version": "1.5", + "scene_name": "Town Street", + "sky_id": "sky_cloudy_day", + "camera": {"position": {"x": 0.0, "y": -420.0}, "zoom": 0.5}, + "objects": [ + {"file_id":"scenery_skyline","type":"scenery_artwork","asset_id":"res://content/props/art_city_skyline-v1.prop","display_name":"City skyline","depth":"far","position":{"x":0.0,"y":-600.0},"rotation":0.0,"scale":{"x":1.0,"y":1.0}}, + {"file_id":"scenery_tree_left","type":"scenery_artwork","asset_id":"res://content/props/art_tree-v2.prop","display_name":"Tree","depth":"near","position":{"x":-520.0,"y":-415.0},"rotation":0.0,"scale":{"x":1.0,"y":1.0}}, + {"file_id":"scenery_town_house","type":"scenery_artwork","asset_id":"res://content/props/art_town_house-v1.prop","display_name":"Town house","depth":"near","position":{"x":-255.0,"y":-390.0},"rotation":0.0,"scale":{"x":0.88,"y":0.88}}, + {"file_id":"scenery_storefront","type":"scenery_artwork","asset_id":"res://content/props/art_storefront-v1.prop","display_name":"Storefront","depth":"near","position":{"x":130.0,"y":-390.0},"rotation":0.0,"scale":{"x":1.0,"y":1.0}}, + {"file_id":"scenery_tree_right","type":"scenery_artwork","asset_id":"res://content/props/art_tree-v2.prop","display_name":"Tree","depth":"near","position":{"x":500.0,"y":-405.0},"rotation":0.0,"scale":{"x":0.88,"y":0.88}}, + {"file_id":"ground_sidewalk","type":"terrain","asset_id":"ground","position":{"x":0.0,"y":0.0},"rotation":0.0,"scale":{"x":7.0,"y":1.0},"finish_id":"finish_ground_sidewalk"}, + {"file_id":"ground_asphalt","type":"terrain","asset_id":"ground","position":{"x":0.0,"y":32.0},"rotation":0.0,"scale":{"x":7.0,"y":1.0},"finish_id":"finish_ground_asphalt"} + ], + "event_rules": [], + "metadata": {"created_unix": 1792022400, "modified_unix": 1792022400, "autosave": false, "source_path": ""} +} diff --git a/docs/architecture/tech_debt_and_optimizations.md b/docs/architecture/tech_debt_and_optimizations.md index c163187..e9b7ea4 100644 --- a/docs/architecture/tech_debt_and_optimizations.md +++ b/docs/architecture/tech_debt_and_optimizations.md @@ -81,6 +81,8 @@ This document tracks known technical debt, optimization opportunities, and minor | 69 | **Contact entry alone can miss a stickman hitting a fixed prop** — grounded locomotion stops at a shape-sweep safe fraction, sometimes before `body_entered` fires; resting contacts can also chatter after pause or quick separation. | Medium | Implemented; user-verified; automated result undocumented | Phase 17c routes identified blocking-prop sweep hits and actual body contacts through one relative approach-speed scorer and pair gate. Multiple stickman limbs collapse into one rig/prop pair; separation/rearm and cooldown gates suppress repeat hits. The user has verified Phase 17c; no automated behavior verification result is documented. Keep existing collision-rule dispatch independent. See `docs/specs/phase_17c_impact_wobble.md`. (2026-10-14) | | 70 | **Impact strength is initially speed based while prop mass is preset based** — material presets set `RigidBody2D.mass`, but fixed props are frozen and mass is not a child-facing setting. | Low | Implemented; future design | Phase 17c keeps the Wobble boolean and visual-only response independent of mass. Impact scoring is centralized in `StageImpactResolver` so a future editable-mass or impulse design can be evaluated without altering the Stage switch or migrating old scene choices. Do not infer a physical impulse from a frozen prop's preset mass; verify any future mass effect separately. See `docs/specs/phase_17c_impact_wobble.md`. (2026-10-14) | | 71 | **Palette art is supplied at much higher resolution than its display size** — six 1254 × 1254 PNGs appear in 64 × 64 card image areas. | Low | ✅ Resolved | Phase 18 is implemented and user-verified. The original source PNGs remain 1254 × 1254, Godot imports are capped at 256 px, and `StageChrome` maps the six images by placement ID. The focused palette suite passed; the headless Sandbox Stage loaded, and `git diff --check` passed. Keep the map local to the fixed Stage palette; revisit a data-driven icon descriptor only if tools or themed art become extensible. See `docs/specs/phase_18_palette_icons.md`. (2026-10-14) | +| 72 | **Stage-wide Undo/Redo needs its own phase** — Phase 19 placement, erase, style, repair, selected-artwork depth, direct single-piece moves/resizes, and group Keep actions change the document without an integrated command history. | Medium | Deferred to separate phase | Design one Stage-level history with coherent commands across Scenery, Ground, Props, terrain, characters, scene naming, and directed content, with safe boundaries around PLAY/paused edits and save/recovery. The Phase 19 sizing amendment keeps single-piece gestures commit-on-release with cancellable active snapshots and retains group Keep/Cancel; neither is an Undo implementation or shortcut. Selected-artwork depth choices also commit immediately. User explicitly deferred Stage Undo/Redo to its own phase. See `docs/specs/phase_19_scenery_backgrounds_surface_finishes.md` and `docs/specs/phase_19_selected_artwork_depth_inspector.md`. (2026-10-17) | +| 73 | **Whole-set transform requires cross-system invariants** — Phase 19's temporary group transform covers wall panels and Scenery artwork only. | Medium | Future feature | Moving/scaling Ground, characters, interactive Props, Sky, camera composition, waypoints, and rules together needs separate physics, navigation, event-target, and save contracts. Do not expand the Scenery group tool into a whole-set transform without that design. See `docs/specs/phase_19_scenery_backgrounds_surface_finishes.md`. (2026-10-15) | --- @@ -169,5 +171,8 @@ This document tracks known technical debt, optimization opportunities, and minor | 2026-10-14 | Phase 17c Impact wobble implementation is present against the approved UI and architecture contracts. The prop card has a per-placement switch and EDIT sample; PLAY routes contacts and identified blocking sweeps through the approach-speed resolver, including ragdoll and prop pairs, while wobbling artwork only. Scene v1.4 stores the setting and migrates older scenes to Off; STOP/reset clears transient motion. Automated behavior verification and in-editor user verification have not been completed. Updated #69–#70; mass-aware scoring remains a future design seam. | | 2026-10-14 | User verified Phase 17c Impact wobble. Updated #69 and the Phase 17 plan; no automated behavior verification result was supplied. | | 2026-10-14 | Phase 18 palette image icons implemented and user-verified (`docs/specs/phase_18_palette_icons.md`). `StageChrome` maps the six palette placement IDs to preloaded images; the three plural filenames are now singular, original 1254 × 1254 PNGs are preserved, and Godot imports are capped at 256 px for display in the existing 64 × 64 card areas. The focused palette suite passed, the headless Sandbox Stage loaded, and `git diff --check` passed. Godot import exited 0 with Windows certificate-store and global editor-settings warnings. User verified the palette appearance and placement behavior in the Editor; #71 is resolved. | +| 2026-10-15 | Phase 19 architecture specification drafted for final user sign-off (`docs/specs/phase_19_scenery_backgrounds_surface_finishes.md`) after approval of live `.prop` references with a repairable missing-art placement, adaptive viewport Sky, and a Stage-hosted Prop Editor route. The design adds inert wall/artwork nodes, vector finishes, scene v1.5 migration, `.prop` v1.1 tags, Scenery-only selection and snapshot group transforms, shared Stage UI generalizations, and ordinary editable starter scenes. Logged #72 for a separate Stage Undo/Redo phase and #73 for a separately designed whole-set transform. No runtime code or assets changed. | +| 2026-10-16 | Added the Phase 19 Stage-relative sizing architecture amendment to `docs/specs/phase_19_scenery_backgrounds_surface_finishes.md` for final sign-off. Prop Editor Size for Stage uses a reversible snapshot, longest-edge presets of 0.35/1/2 standing-stickman heights, origin-based uniform geometry/stroke/editable-anchor scaling, and an editor-only foreground figure/floor comparison. A selected Scenery piece uses its own snapshot transaction and commits on release; a group still uses Keep/Cancel. Existing `.scene` v1.5 placement scale and `.prop` v1.1 geometry need no schema change or load-time normalization. Explicit Save at an existing `.prop` path retains live-reference updates for all users of that source; no asset-version enforcement or new save prompt was requested. Updated #72 for the deferred Stage Undo boundary. The amendment needs no new art and Phase 19 user/editor verification remains pending. No runtime code changed by this specification entry. | +| 2026-10-17 | Wrote the Phase 19 selected-artwork depth inspector specification (`docs/specs/phase_19_selected_artwork_depth_inspector.md`) for final sign-off. It reuses the existing `StageInspectorPanel` and `StagePanelChoiceGroup`, applies Far/Near/On the floor immediately to one live or missing Scenery placement through `StageSceneryArtwork.set_depth()`, and follows the Stage's existing dirty/save path. The current `.scene` depth field, `StageDrawOrder`, and `stage_panels` tokens need no changes. Updated #72 to include immediate depth edits in a future Stage-wide Undo phase. No runtime code changed by this specification entry. | _End of Log_ diff --git a/docs/ideas/feature_backlog.md b/docs/ideas/feature_backlog.md index 538b246..7c897c3 100644 --- a/docs/ideas/feature_backlog.md +++ b/docs/ideas/feature_backlog.md @@ -36,6 +36,10 @@ Priorities are recommendations for discussion, not a committed release schedule. 9. **Group props into set pieces** — **Status:** Parked · **Suggested priority:** Low. **Intent:** Let children group objects for organization and moving together within one scene. **Rationale:** Deferred until later at the user's direction. Scene-only groups make arranging furnished sets faster. In PLAY, each item keeps its own physics and interaction; picking up one prop does not move the rest, and stopping PLAY returns the grouped items to their authored arrangement. Reusable set-piece assets are deferred. This is **Phase 17d** in the [Phase 17 — Props for Storytelling plan](../plans/phase_17_props_for_storytelling.md). **Connections:** `scripts/stage/stage_selection.gd`, `scripts/editors/stage/sandbox_stage.gd`, `scripts/stage/stage_scene_io.gd`. +10. **Stage-relative prop and scenery scale** — **Status:** Planned — Phase 19 addition; UI approved · **Suggested priority:** Medium. **Intent:** Establish one recognizable Stage scale based on the standard stickman, then author each asset at the size its role calls for so ordinary placements start correctly without routine resizing. **Rationale:** The Prop Editor already shows a stickman guide, but its current Normalize Size target is 96 world units against an approximately 940-unit standing figure; current artwork dimensions also vary widely. The approved Phase 19 addition replaces Normalize Size with a reversible **Size for Stage** preview: Small/Room/Big starts plus fine adjustment, a foreground 1:1 figure, and a floor guide. Artist chooses ordinary dimensions from asset intent; new placements use authored size. Individual Scenery resize commits on release, while group resize keeps its existing Keep/Cancel preview. Loading performs no automatic normalization. Ordinary Save and live-reference behavior remain, with no new copy/version prompts. Phase 19 user/editor verification remains pending. **Connections:** [Phase 10a Prop Scaling](../specs/phase_10a_prop_scaling.md), [Phase 19 Scenery, Backgrounds, and Surface Finishes](../plans/phase_19_scenery_backgrounds_surface_finishes.md), [approved Phase 19 UI](../specs/phase_19_scenery_backgrounds_ui_ux.md), `scripts/editors/prop/prop_editor.gd`, `scripts/editors/prop/prop_canvas.gd`, `scripts/editors/stickman/whole_stickman_preview.gd`. + +11. **Size-aware artwork previews** — **Status:** Idea · **Suggested priority:** Low. **Intent:** Show a stickman silhouette or simple relative-size cue in artwork catalog previews so children can judge an asset before placing it. **Rationale:** Once asset dimensions are authored consistently, a size cue could reduce trial-and-error in the Stage without forcing objects to a fixed size. This depends on a settled Stage scale convention and should remain a browse-time hint, not an automatic resize. **Connections:** Stage-relative prop and scenery scale (backlog item 10), `scripts/editors/stage/stage_asset_browser.gd`, `scripts/shared/ui/asset_card.gd`. + ## Entry format For each feature, record: diff --git a/docs/plans/phase_19_handoff.md b/docs/plans/phase_19_handoff.md new file mode 100644 index 0000000..215eb48 --- /dev/null +++ b/docs/plans/phase_19_handoff.md @@ -0,0 +1,34 @@ +# Phase 19 handoff + +Phase 19 is in progress on the local branch `feature/phase-19-scenery`. This is a continuation note for the next work session/computer; the detailed product, UI, and architecture contracts remain in the linked documents below. The branch is local and must be pushed after the reviewed changes are committed before another computer can fetch it. + +## Current state + +The integrated feature lets children choose a Sky, place and edit inert Scenery artwork and wall panels, paint Ground finishes, use editable starter scenes, and save/reopen the result. Scenery artwork can be placed Far, Near, or On the floor and resized independently. The Prop Editor has a stickman-relative size guide and reversible size presets for authoring art at a useful scale. Missing artwork remains visible and repairable. + +Recent reported editor issues were addressed during Phase 19 iteration, including scene loading, grid snapping/placement previews, starter-scene thumbnails, artwork cursor alignment, wall-panel erasing and tile rendering, and scenery size/depth controls. Treat those fixes as needing a focused regression pass on the next machine; the user has not completed overall Phase 19 testing. The selected-artwork depth inspector was the latest specifically approved piece. + +Stage-wide Undo/Redo is deliberately out of scope and recorded as technical debt for a separate phase. No new scene save workflow was requested; the current ordinary scene and Prop save flows remain authoritative. + +## Start here + +- [Phase 19 product plan](phase_19_scenery_backgrounds_surface_finishes.md) — child flow, feature scope, content manifest, acceptance criteria, and remaining user checks. +- [Phase 19 architecture and implementation spec](../specs/phase_19_scenery_backgrounds_surface_finishes.md) — runtime/file contracts, Stage-relative sizing amendment, implementation details, and verification expectations. +- [Phase 19 UI/UX spec](../specs/phase_19_scenery_backgrounds_ui_ux.md) — child-facing interaction and reusable UI guidance. +- [Selected Scenery artwork depth spec](../specs/phase_19_selected_artwork_depth_inspector.md) — Far/Near/On the floor inspector behavior. +- [Stage panel UI system](../specs/stage_panel_ui_system.md) — shared in-stage inspector components and visual conventions. +- [Technical debt log](../architecture/tech_debt_and_optimizations.md) — includes deferred Stage Undo/Redo. + +## Verification entry point + +Run the focused Phase 19 suite with Godot 4.7.1 from the repository root: + +```powershell +& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --path . --script res://tests/test_phase19_scenery.gd +``` + +Also run affected compatibility suites when changing shared Stage, Prop, or asset-browser code: `tests/test_phase10_prop_integration.gd`, `tests/test_phase17a_prop_roles.gd`, `tests/test_phase7_palette.gd`, plus the relevant Prop Editor and scene-I/O tests. Headless success is not the final acceptance gate: open the Sandbox Stage in the GUI, exercise the plan's focused scenarios, and inspect the actual rendering. Keep Phase 19 marked in progress until the user's overall editor verification is complete. + +## Handoff hygiene + +Commit the implementation, production `.prop` assets and `.scene` starters, specifications, handoff/plan/debt updates, test source and required `.uid` metadata, plus relevant regression-test updates. Do not include generated test captures/logs or `tests/godot_test_user_data/` unless a later task explicitly promotes selected evidence into maintained documentation. Review the complete diff before staging; this working tree intentionally remains unstaged and uncommitted for that review. diff --git a/docs/plans/phase_19_scenery_backgrounds_surface_finishes.md b/docs/plans/phase_19_scenery_backgrounds_surface_finishes.md index 8c42ae4..dd2c00e 100644 --- a/docs/plans/phase_19_scenery_backgrounds_surface_finishes.md +++ b/docs/plans/phase_19_scenery_backgrounds_surface_finishes.md @@ -1,6 +1,6 @@ # Phase 19 — Scenery, Backgrounds, and Vector Surface Finishes -> **Status:** Accepted product/design brief. Technical specifications and implementation remain to be developed through the spec-pipeline. +> **Status:** Phase 19 implementation, including Stage-relative asset sizing and the selected-artwork depth inspector, is integrated; automated and render-capable checks pass. Full user/editor verification remains pending. See the [Phase 19 handoff](phase_19_handoff.md) for continuation context and verification entry points. > **Audience:** UI/UX, architecture, implementation, and art contributors. > **Purpose:** Define the child experience and the content required for indoor and outdoor sets. Rendering, file contracts, and GDScript APIs belong in later specifications. @@ -81,7 +81,7 @@ Sky Every Scenery tier is visual only. Its depth never grants collision, support, pickup, Use spots, or event participation. An On the floor rug is visible above a wood Ground finish without covering furniture or the character's feet. -**Edit Scenery** exposes the scenery pieces for selection. A clear outline and large handles let the child move and resize art, change its depth, or erase it; a wall panel also exposes Finish and Color. Broad panels must not steal clicks or box selections from characters, Ground, and Props in ordinary EDIT. Within Edit Scenery, a cutout in front of a panel should be easy to choose, and the panel must still be reachable through its exposed area, edge, or an overlapping-items choice. Sky remains in its own chooser. A visible Cancel closes a placement or sizing gesture before commit, and leaving Edit Scenery returns to ordinary selection. Selection and focus cannot rely on color alone. +**Edit Scenery** exposes the scenery pieces for selection. A clear outline and large handles let the child move and resize art, change its depth, or erase it; a wall panel also exposes Finish and Color. Broad panels must not steal clicks or box selections from characters, Ground, and Props in ordinary EDIT. Within Edit Scenery, a cutout in front of a panel should be easy to choose, and the panel must still be reachable through its exposed area, edge, or an overlapping-items choice. A selected wall panel also offers **Erase wall area**: drag a rectangle to remove that area from one or more wall panels while keeping the surrounding pieces and their finish. This is a manual visual cutout; it does not create glass, collision, or doorway behavior. Each released cut is immediate; Cancel/Escape/right-click discard only the current drag, and Stage Undo remains deferred to its own future phase. Sky remains in its own chooser. Placement and group-resize previews have visible Cancel actions. A single-piece move or resize previews during its direct-manipulation gesture and commits on release; while the gesture is active it can be canceled through the capture bar (touch), Escape, or right-click. There is no second Keep confirmation. Leaving Edit Scenery returns to ordinary selection. Selection and focus cannot rely on color alone. ### Finish Ground and play @@ -93,13 +93,13 @@ In **PLAY**, edit handles and placement ghosts disappear. Sky and Scenery stay s ## Included scope and exclusions -Included in Phase 19: one fixed scene-wide Sky preset; four initial Sky designs; finite editable wall panels; inert `.prop` Scenery placements with Far, Near, and On the floor depth; built-in multi-select browse tags; Ground-only visual finishes; editable Blank/Town Street/Living Room New Scene choices; Scenery-safe selection; scene save/reopen and legacy defaults; and the built-in vector content listed below. +Included in Phase 19: one fixed scene-wide Sky preset; four initial Sky designs; finite editable wall panels with manually erasable rectangular openings; inert `.prop` Scenery placements with Far, Near, and On the floor depth; built-in multi-select browse tags; Ground-only visual finishes; editable Blank/Town Street/Living Room New Scene choices; Scenery-safe selection; scene save/reopen and legacy defaults; built-in vector content listed below; and a stage-relative size-authoring guide/workflow shared by Props and Scenery artwork, with single Scenery artwork placements resizable independently in Edit Scenery. The new authoring convention does not silently rescale existing assets or saved placements. Excluded from Phase 19: a new Scenery Editor or `.scenery` file; child-authored repeatable finish patterns or reusable whole-Sky themes; animated weather, parallax, or directed Sky transitions; whole-scene filters such as sepia or black-and-white; functional doors or scene travel; automatic window cutouts or glass; Ramp/Step finishes or terrain redesign; inserting a starter into an existing scene; user-authored reusable set templates; and functional stairs or multi-level travel. [Multi-level walkable scenes](../ideas/feature_backlog.md) and [whole-scene visual filters](../ideas/feature_backlog.md) are separate future ideas, not Phase 19 work. ## Asset production manifest -This is the authoritative content checklist for the two starters and the shared pickers. The initial status of **every** row is **Needed**. Progress through **Needed → Concept ready → Art approved → Final vector ready → Integrated → Verified**. A raster sketch may reach **Concept ready**; it is not a final `.prop`, Sky theme, or repeatable vector finish. Final cutouts must be repo-native vector/`.prop` artwork, and Sky/finish visuals must remain crisp through Stage zoom. Art contributors may use raster concept images as reference, then produce final assets in coordination with implementation. +This is the authoritative content checklist for the two starters and the shared pickers. The initial status of every row was **Needed**. Progress through **Needed → Concept ready → Art approved → Final vector ready → Integrated → Verified**. Following user approval on 2026-10-15, the approved `.prop` vectors, Sky themes, and finishes are integrated into the production Stage, shared picker, and starter scenes. Evidence below records the current Phase 2 checks; final user/editor verification remains pending. A raster sketch may reach **Concept ready**; it is not a final `.prop`, Sky theme, or repeatable vector finish. Final cutouts must be repo-native vector/`.prop` artwork, and Sky/finish visuals must remain crisp through Stage zoom. Art contributors may use raster concept images as reference, then produce final assets in coordination with implementation. In the tables, **TBD** in Source/rights means the creator, reference provenance, and license or permission still need recording. **TBD** in Final location and Evidence means those fields must be filled as content is produced. For each row, validation evidence should include a thumbnail or Sky/finish preview, the asset in the Stage at normal and zoomed views, and save/reopen of its starter use where applicable. Stable IDs identify deliverables; they are not prescribed file names or runtime contracts. @@ -107,10 +107,10 @@ In the tables, **TBD** in Source/rights means the creator, reference provenance, | Stable ID | Class / final format | Tags / default layer | Starter use | Behavior / collision | Status | Source / rights | Visual acceptance | Final location / definition | Validation evidence | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `sky_clear_day` | Adaptive vector Sky theme; not `.prop` | N/A; Sky | Living Room; picker | Scene-wide, static; none | Needed | TBD | Appealing blue depth and varied light cloud/sun detail; window gap reads clearly; no edge on pan. | TBD | TBD | -| `sky_cloudy_day` | Adaptive vector Sky theme; not `.prop` | N/A; Sky | Town Street; picker | Scene-wide, static; none | Needed | TBD | Layered, varied clouds leave town silhouettes readable; no obvious tile or edge. | TBD | TBD | -| `sky_sunset` | Adaptive vector Sky theme; not `.prop` | N/A; Sky | Picker | Scene-wide, static; none | Needed | TBD | Warm atmospheric color and sun/cloud composition remain distinct from Clear Day without tinting scene objects. | TBD | TBD | -| `sky_starry_night` | Adaptive vector Sky theme; not `.prop` | N/A; Sky | Picker | Scene-wide, static; none | Needed | TBD | Dark graduated sky, varied stars, and moon remain legible while foreground contrast survives. | TBD | TBD | +| `sky_clear_day` | Adaptive vector Sky theme; not `.prop` | N/A; Sky | Living Room; picker | Scene-wide, static; none | Integrated | Original vector concept | Appealing blue depth and varied light cloud/sun detail; window gap reads clearly; no edge on pan. | `scripts/stage/scenery/stage_sky_view.gd` | Production card and Living Room Stage render inspected: `tests/phase19_visual_background_gallery.png`, `tests/phase19_visual_living_room.png`; user pan/zoom check pending. | +| `sky_cloudy_day` | Adaptive vector Sky theme; not `.prop` | N/A; Sky | Town Street; picker | Scene-wide, static; none | Integrated | Original vector concept | Layered, varied clouds leave town silhouettes readable; no obvious tile or edge. | `scripts/stage/scenery/stage_sky_view.gd` | Production card and Town Street Stage render inspected: `tests/phase19_visual_background_gallery.png`, `tests/phase19_visual_town_street.png`; user pan/zoom check pending. | +| `sky_sunset` | Adaptive vector Sky theme; not `.prop` | N/A; Sky | Picker | Scene-wide, static; none | Integrated | Original vector concept | Warm atmospheric color and sun/cloud composition remain distinct from Clear Day without tinting scene objects. | `scripts/stage/scenery/stage_sky_view.gd` | Production card and Stage render inspected: `tests/phase19_visual_background_gallery.png`, `tests/phase19_visual_finishes_and_repair.png`; user pan/zoom check pending. | +| `sky_starry_night` | Adaptive vector Sky theme; not `.prop` | N/A; Sky | Picker | Scene-wide, static; none | Integrated | Original vector concept | Dark graduated sky, varied stars, and moon remain legible while foreground contrast survives. | `scripts/stage/scenery/stage_sky_view.gd` | Distinct production gallery preview inspected: `tests/phase19_visual_background_gallery.png`; full Stage user check pending. | ### Repeatable vector surface finishes @@ -118,16 +118,16 @@ These are finish definitions and previews, not ordinary Props. Ground finishes c | Stable ID | Class / final format | Tags / default layer | Starter use | Behavior / collision | Status | Source / rights | Visual acceptance | Final location / definition | Validation evidence | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `finish_wall_paint` | Vector wall finish | N/A; wall panel | Living Room; picker | Inert panel; none | Needed | TBD | Smooth green room wall and other chosen colors without seams at panel edges. | TBD | TBD | -| `finish_wall_brick` | Repeatable vector wall finish | N/A; wall panel | Picker | Inert panel; none | Needed | TBD | Bricks keep coherent size and joins as panel is resized. | TBD | TBD | -| `finish_wall_wood` | Repeatable vector wall finish | N/A; wall panel | Picker | Inert panel; none | Needed | TBD | Boards read as wood at common Stage zoom and do not stretch with the panel. | TBD | TBD | -| `finish_ground_paint` | Vector Ground finish | N/A; Ground | Picker | Solid, walkable Ground | Needed | TBD | Plain color is clean and visually distinct from wall-only backdrop. | TBD | TBD | -| `finish_ground_brick` | Repeatable vector Ground finish | N/A; Ground | Picker | Solid, walkable Ground | Needed | TBD | Neighboring pieces form a continuous brick surface. | TBD | TBD | -| `finish_ground_wood` | Repeatable vector Ground finish | N/A; Ground | Living Room; picker | Solid, walkable Ground | Needed | TBD | Wood plank floor reads continuously beneath rug and furniture. | TBD | TBD | -| `finish_ground_grass` | Repeatable vector Ground finish | N/A; Ground | Picker | Solid, walkable Ground | Needed | TBD | Grassy surface is readable and not noisy behind feet. | TBD | TBD | -| `finish_ground_dirt` | Repeatable vector Ground finish | N/A; Ground | Picker | Solid, walkable Ground | Needed | TBD | Earthy surface remains distinct from Grass at thumbnail and Stage size. | TBD | TBD | -| `finish_ground_sidewalk` | Repeatable vector Ground finish | N/A; Ground | Town Street; picker | Solid, walkable Ground | Needed | TBD | Light concrete sidewalk forms a broad, aligned run beneath shops. | TBD | TBD | -| `finish_ground_asphalt` | Repeatable vector Ground finish | N/A; Ground | Town Street; picker | Solid, walkable Ground | Needed | TBD | Darker road contrasts with sidewalk while preserving visible character edges. | TBD | TBD | +| `finish_wall_paint` | Vector wall finish | N/A; wall panel | Living Room; picker | Inert panel; none | Integrated | Original vector concept | Smooth green room wall and other chosen colors without seams at panel edges. | `scripts/stage/scenery/stage_surface_renderer.gd` | Production gallery preview and Living Room render inspected: `tests/phase19_visual_background_gallery.png`, `tests/phase19_visual_living_room.png`; user resize check pending. | +| `finish_wall_brick` | Repeatable vector wall finish | N/A; wall panel | Picker | Inert panel; none | Integrated | Original vector concept; user image used as inspiration for v2 | Bricks keep coherent size and joins as panel is resized. | `scripts/stage/scenery/stage_surface_renderer.gd` | Production gallery preview and Stage wall render inspected: `tests/phase19_visual_background_gallery.png`, `tests/phase19_visual_finishes_and_repair.png`; user resize check pending. | +| `finish_wall_wood` | Repeatable vector wall finish | N/A; wall panel | Picker | Inert panel; none | Integrated | Original vector concept | Boards read as wood at common Stage zoom and do not stretch with the panel. | `scripts/stage/scenery/stage_surface_renderer.gd` | Production gallery preview inspected: `tests/phase19_visual_background_gallery.png`; user resize check pending. | +| `finish_ground_paint` | Vector Ground finish | N/A; Ground | Picker | Solid, walkable Ground | Integrated | Original vector concept | Plain color is clean and visually distinct from wall-only backdrop. | `scripts/stage/scenery/stage_surface_renderer.gd` | Production Ground renderer is integrated with Stage; Phase 19 runtime suite passed; user visual check pending. | +| `finish_ground_brick` | Repeatable vector Ground finish | N/A; Ground | Picker | Solid, walkable Ground | Integrated | Original vector concept | Neighboring pieces form a continuous brick surface. | `scripts/stage/scenery/stage_surface_renderer.gd` | Production Ground renderer is integrated with Stage; Phase 19 runtime suite passed; user tiling check pending. | +| `finish_ground_wood` | Repeatable vector Ground finish | N/A; Ground | Living Room; picker | Solid, walkable Ground | Integrated | Original vector concept | Wood plank floor reads continuously beneath rug and furniture. | `scripts/stage/scenery/stage_surface_renderer.gd` | Production Living Room scene and Ground renderer integrated; Phase 19 runtime suite passed; user visual check pending. | +| `finish_ground_grass` | Repeatable vector Ground finish | N/A; Ground | Picker | Solid, walkable Ground | Integrated | Original vector concept | Grassy surface is readable and not noisy behind feet. | `scripts/stage/scenery/stage_surface_renderer.gd` | Production Ground render inspected: `tests/phase19_visual_finishes_and_repair.png`; runtime suite passed; user visual check pending. | +| `finish_ground_dirt` | Repeatable vector Ground finish | N/A; Ground | Picker | Solid, walkable Ground | Integrated | Original vector concept | Earthy surface remains distinct from Grass at thumbnail and Stage size. | `scripts/stage/scenery/stage_surface_renderer.gd` | Production Ground renderer is integrated with Stage; Phase 19 runtime suite passed; user visual check pending. | +| `finish_ground_sidewalk` | Repeatable vector Ground finish | N/A; Ground | Town Street; picker | Solid, walkable Ground | Integrated | Original vector concept | Light concrete sidewalk forms a broad, aligned run beneath shops. | `scripts/stage/scenery/stage_surface_renderer.gd` | Town Street starter load and runtime suite passed; user visual/tiling check pending. | +| `finish_ground_asphalt` | Repeatable vector Ground finish | N/A; Ground | Town Street; picker | Solid, walkable Ground | Integrated | Original vector concept | Darker road contrasts with sidewalk while preserving visible character edges. | `scripts/stage/scenery/stage_surface_renderer.gd` | Town Street starter load and runtime suite passed; user visual/tiling check pending. | ### Reusable Scenery artwork @@ -135,17 +135,17 @@ Each item below is a `.prop` vector artwork asset offered through Scenery. Its S | Stable ID | Class / final format | Tags / default layer | Starter use | Behavior / collision | Status | Source / rights | Visual acceptance | Final location / definition | Validation evidence | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `art_city_skyline` | Vector `.prop` cutout | Buildings; Far | Town Street ×1 | Inert; none | Needed | TBD | Long, muted overlapping building silhouette reads as distant and leaves cloud sky visible. | TBD | TBD | -| `art_storefront` | Vector `.prop` cutout | Buildings; Near | Town Street ×1 or more | Inert; none | Needed | TBD | Colorful, readable shop front contrasts with skyline; repeat use does not look like one flat strip. | TBD | TBD | -| `art_town_house` | Vector `.prop` cutout | Buildings; Near | Town Street ×1 or more | Inert; none | Needed | TBD | House silhouette and palette differ clearly from storefront at normal zoom. | TBD | TBD | -| `art_tree` | Vector `.prop` cutout | Nature; Near | Town Street ×2 or more | Inert; none | Needed | TBD | Friendly, readable crown/trunk; repeated copies tolerate size and position variation. | TBD | TBD | -| `art_window_frame` | Vector `.prop` cutout | Walls, Buildings; Near | Living Room ×1 | Inert; none | Needed | TBD | Frame is transparent at its center and aligns over the wall-panel gap to reveal actual Sky. | TBD | TBD | -| `art_fireplace_mantel` | Vector `.prop` cutout | Walls, Objects; Near | Living Room ×1 | Inert; none | Needed | TBD | Fireplace and mantel read clearly as decoration without suggesting this asset itself blocks movement. | TBD | TBD | -| `art_wall_clock` | Vector `.prop` cutout | Walls, Objects; Near | Living Room ×1 | Inert; none | Needed | TBD | Clock reads at room scale without tiny illegible markings. | TBD | TBD | -| `art_bookshelf` | Vector `.prop` cutout | Objects, Walls; Near | Living Room ×1 | Inert; none | Needed | TBD | Shelf, books, and plant details remain legible without dense tracing. | TBD | TBD | -| `art_framed_picture` | Vector `.prop` cutout | Objects, Walls; Near | Living Room ×1 or more | Inert; none | Needed | TBD | Clear frame and image contrast on green paint; reuse can vary size/position. | TBD | TBD | -| `art_potted_plant` | Vector `.prop` cutout | Nature, Objects; Near | Living Room ×1 | Inert; none | Needed | TBD | Leaf and pot shapes remain distinct from wall and furniture. | TBD | TBD | -| `art_rug` | Vector `.prop` cutout | Objects; On the floor | Living Room ×1 | Inert; none | Needed | TBD | Visible over Wood Ground and below furniture/feet; border survives zoom. | TBD | TBD | +| `art_city_skyline` | Vector `.prop` cutout | Buildings; Far | Town Street ×1 | Inert; none | Integrated | Original vector art | Long, muted overlapping building silhouette reads as distant and leaves cloud sky visible. | `content/props/art_city_skyline-v1.prop` | Town Street production Stage render inspected: `tests/phase19_visual_town_street.png`; save/reopen suite passed; user zoom check pending. | +| `art_storefront` | Vector `.prop` cutout | Buildings; Near | Town Street ×1 or more | Inert; none | Integrated | Original vector art | Colorful, readable shop front contrasts with skyline; repeat use does not look like one flat strip. | `content/props/art_storefront-v1.prop` | Town Street production Stage render inspected: `tests/phase19_visual_town_street.png`; save/reopen suite passed; user zoom/repeat check pending. | +| `art_town_house` | Vector `.prop` cutout | Buildings; Near | Town Street ×1 or more | Inert; none | Integrated | Original vector art | House silhouette and palette differ clearly from storefront at normal zoom. | `content/props/art_town_house-v1.prop` | Town Street production Stage render inspected: `tests/phase19_visual_town_street.png`; save/reopen suite passed; user zoom check pending. | +| `art_tree` | Vector `.prop` cutout | Nature; Near | Town Street ×2 or more | Inert; none | Integrated | Original vector art; user image used as inspiration for v2 | Friendly, readable crown/trunk; repeated copies tolerate size and position variation. | `content/props/art_tree-v2.prop` | Revised v2 tree inspected in Town Street and repair view: `tests/phase19_visual_town_street.png`, `tests/phase19_visual_finishes_and_repair.png`; user zoom/repeat check pending. | +| `art_window_frame` | Vector `.prop` cutout | Walls, Buildings; Near | Living Room ×1 | Inert; none | Integrated | Original vector art | Frame is transparent at its center and aligns over the wall-panel gap to reveal actual Sky. | `content/props/art_window_frame-v1.prop` | Living Room production render inspected: `tests/phase19_visual_living_room.png`; save/reopen suite passed; user window alignment check pending. | +| `art_fireplace_mantel` | Vector `.prop` cutout | Walls, Objects; Near | Living Room ×1 | Inert; none | Integrated | Original vector art | Fireplace and mantel read clearly as decoration without suggesting this asset itself blocks movement. | `content/props/art_fireplace_mantel-v2.prop` | Revised v2 fireplace inspected in Living Room: `tests/phase19_visual_living_room.png`; user zoom check pending. | +| `art_wall_clock` | Vector `.prop` cutout | Walls, Objects; Near | Living Room ×1 | Inert; none | Integrated | Original vector art | Clock reads at room scale without tiny illegible markings. | `content/props/art_wall_clock-v1.prop` | Living Room production render inspected: `tests/phase19_visual_living_room.png`; user zoom check pending. | +| `art_bookshelf` | Vector `.prop` cutout | Objects, Walls; Near | Living Room ×1 | Inert; none | Integrated | Original vector art | Shelf, books, and plant details remain legible without dense tracing. | `content/props/art_bookshelf-v1.prop` | Living Room production render inspected: `tests/phase19_visual_living_room.png`; user zoom check pending. | +| `art_framed_picture` | Vector `.prop` cutout | Objects, Walls; Near | Living Room ×1 or more | Inert; none | Integrated | Original vector art | Clear frame and image contrast on green paint; reuse can vary size/position. | `content/props/art_framed_picture-v1.prop` | Living Room production render inspected: `tests/phase19_visual_living_room.png`; user reuse/zoom check pending. | +| `art_potted_plant` | Vector `.prop` cutout | Nature, Objects; Near | Living Room ×1 | Inert; none | Integrated | Original vector art | Leaf and pot shapes remain distinct from wall and furniture. | `content/props/art_potted_plant-v1.prop` | Living Room production render inspected: `tests/phase19_visual_living_room.png`; user zoom check pending. | +| `art_rug` | Vector `.prop` cutout | Objects; On the floor | Living Room ×1 | Inert; none | Integrated | Original vector art | Visible over Wood Ground and below furniture/feet; border survives zoom. | `content/props/art_rug-v1.prop` | Living Room production render inspected: `tests/phase19_visual_living_room.png`; starter runtime suite passed; user depth/zoom check pending. | ### Interactive furniture Props @@ -153,11 +153,11 @@ These are `.prop` assets placed through Props in Living Room. Their authored sha | Stable ID | Class / final format | Tags / default layer | Starter use | Behavior / collision | Status | Source / rights | Visual acceptance | Final location / definition | Validation evidence | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `prop_couch` | Vector `.prop` furniture | Objects; Prop depth per placement | Living Room ×1 | Still, appropriately character-blocking; valid collision | Needed | TBD | Comfortable couch silhouette sized for stickman and room; no implied Sit action. | TBD | TBD | -| `prop_armchair` | Vector `.prop` furniture | Objects; Prop depth per placement | Living Room ×1 | Still, appropriately character-blocking; valid collision | Needed | TBD | Distinct chair silhouette, matching couch style and character scale. | TBD | TBD | -| `prop_coffee_table` | Vector `.prop` furniture | Objects; Prop depth per placement | Living Room ×1 | Still, can support small props; valid collision | Needed | TBD | Tabletop/support reads clearly and leaves rug visible. | TBD | TBD | +| `prop_couch` | Vector `.prop` furniture | Objects; Prop depth per placement | Living Room ×1 | Still, appropriately character-blocking; valid collision | Integrated | Original vector art | Comfortable couch silhouette sized for stickman and room; no implied Sit action. | `content/props/prop_couch-v1.prop` | Living Room production render inspected: `tests/phase19_visual_living_room.png`; prop integration regression available; user collision/scale check pending. | +| `prop_armchair` | Vector `.prop` furniture | Objects; Prop depth per placement | Living Room ×1 | Still, appropriately character-blocking; valid collision | Integrated | Original vector art | Distinct chair silhouette, matching couch style and character scale. | `content/props/prop_armchair-v1.prop` | Living Room production render inspected: `tests/phase19_visual_living_room.png`; prop integration regression available; user collision/scale check pending. | +| `prop_coffee_table` | Vector `.prop` furniture | Objects; Prop depth per placement | Living Room ×1 | Still, can support small props; valid collision | Integrated | Original vector art | Tabletop/support reads clearly and leaves rug visible. | `content/props/prop_coffee_table-v1.prop` | Living Room production render inspected: `tests/phase19_visual_living_room.png`; prop integration regression available; user support/scale check pending. | -Existing built-in crate and ball artwork may be used in Town Street if an interactive object benefits that starter; no new interactive town asset is required to achieve its visual reference. The artist should establish a coherent cartoon style and scale against the live stickman. Far art is quieter than Near art; outlines and silhouettes must remain clear at selector-thumbnail size and normal Stage zoom. Avoid excessive vector point counts, hard-to-read microdetail, and busy contrast behind character faces or speech bubbles. The source/rights field must distinguish original art, licensed sources, and generated raster concepts. Reference art is not automatically licensed final content. +Existing built-in crate and ball artwork may be used in Town Street if an interactive object benefits that starter; no new interactive town asset is required to achieve its visual reference. The artist should establish a coherent cartoon style and author each asset at its intended Stage size relative to the standard approximately 940-world-unit standing stickman. Infer ordinary dimensions from a recognizable object's name and use, and record approximate intended bounds in stickman units (height, plus width when useful) with a scale-check preview; ask only when the intended use makes size genuinely ambiguous. Far art is quieter than Near art; outlines and silhouettes must remain clear at selector-thumbnail size and normal Stage zoom. Avoid excessive vector point counts, hard-to-read microdetail, and busy contrast behind character faces or speech bubbles. The source/rights field must distinguish original art, licensed sources, and generated raster concepts. Reference art is not automatically licensed final content. ## Success criteria @@ -165,10 +165,11 @@ Existing built-in crate and ball artwork may be used in Town Street if an intera 2. A child can create Living Room from New Scene, see the actual selected Sky through its window gap, change the Sky without tinting the room, restyle the wall or Wood Ground, move the rug and furniture independently, and save/reopen the revised set. 3. A child can place one `.prop` drawing through Scenery and through Props and understand that the two copies behave differently. The Scenery copy never blocks, supports, moves, is picked up or used, or triggers an event in PLAY. 4. Far skyline, Near buildings/trees, Ground, On the floor rug, and Props preserve their intended order through pan, zoom, PLAY, stop, and reload. Character and speech readability survives the four Sky styles and common finishes. -5. Ordinary EDIT selection still reaches foreground characters, Props, and Ground over broad panels. Edit Scenery makes every cutout and panel discoverable and correctable; placement and resize can be canceled before commit. Labels, icons, outlines, and focus reveal state without depending on color alone. +5. Ordinary EDIT selection still reaches foreground characters, Props, and Ground over broad panels. Edit Scenery makes every cutout and panel discoverable and correctable; placement and an active resize gesture can be canceled before commit, while a single-piece resize commits on release and group resize retains Keep/Cancel. Labels, icons, outlines, and focus reveal state without depending on color alone. 6. Wall panel and Ground finish choices preview correctly; wall patterns do not stretch as panels resize, and adjacent Ground pieces do not show distracting pattern restarts. Changing a palette finish does not repaint existing pieces; a selected piece can be changed deliberately. 7. Older `.scene` documents retain their prior neutral background and Ground appearance when opened. Older untagged `.prop` assets remain visible in All artwork, and a missing referenced Scenery asset yields a visible recovery cue rather than silently removing the child's placement. 8. Every required manifest item reaches **Verified**, with source/rights and final location recorded. A raster concept alone cannot satisfy a final vector content requirement. +9. New and revised `.prop` artwork is authored at its intended Stage size relative to the standard approximately 940-world-unit standing stickman, avoiding a mandatory resize on ordinary placement. The Prop Editor's Size for Stage guide is visible over artwork and above a floor/contact cue. Loading performs no automatic normalization; deliberate asset overwrites retain the usual live-reference behavior and existing Save workflow, without new copy/version prompts. Individual Scenery pieces resize independently, preserve proportions, and save/reopen at the committed scale. ## Current project context, risks, and dependencies @@ -178,12 +179,13 @@ Existing built-in crate and ball artwork may be used in Town Street if an intera - Stage selection currently uses World-object bounds and an overlapping-object chooser. Large wall panels and floor art can otherwise catch almost every click or box selection. A dedicated, forgiving Scenery edit flow and predictable hit order are product requirements. The approved [Stage panel UI system](../specs/stage_panel_ui_system.md) supplies the in-stage inspector visual contract; adapt it for selected Scenery/Ground controls where it fits. - Finite panels, tiled vector finishes, and detailed Sky motifs must stay crisp and performant across Stage zoom and camera pan. Ground pieces need aligned world-scale patterns. The Town Street and Living Room references should guide composition and mood, not be treated as final licensed assets. - The two starters require the manifest content as real deliverables. Missing art, inconsistent scale, or raster-only placeholders would leave a child with a visually incomplete feature even if the controls work. +- Stage-relative authoring scale must account for both tall and wide props/artwork without forcing every asset to one generic height. The stickman comparison guides intent; the artist records an approximate scale, and the Prop Editor provides reversible correction. Loading does not normalize existing assets. Deliberately saving revised geometry over a live reference updates that drawing in referencing scenes; the user chose to retain the existing Save workflow because scenes are not yet in production. ## Remaining questions for the spec-pipeline These are design and architecture tasks for the specification stage, not unresolved user choices about Phase 19's product goal: -1. What exact UI presents Backgrounds, Artwork tags, placement layer, Ground finish, and Edit Scenery without crowding the existing Stage palette? Define keyboard, touch, narrow-viewport, dismissal, and undo/cancel behavior using the established Stage panel patterns. +1. What exact UI presents Backgrounds, Artwork tags, placement layer, Ground finish, and Edit Scenery without crowding the existing Stage palette? Define keyboard, touch, narrow-viewport, dismissal, and cancellation behavior using the established Stage panel patterns; Stage Undo remains out of scope for Phase 19 and is tracked separately. 2. How will an adaptive Sky fill unbounded camera views while keeping motifs stable and detailed, and how will wall and Ground finishes keep repeatable pattern scale and alignment through resizing, drag painting, and zoom? 3. What saved scene and optional `.prop` tag fields, validation, migration defaults, missing-art recovery, and starter creation flow preserve existing documents and the same artwork's independent Scenery/Prop placements? 4. How will selected panels, cutouts, rugs, Ground, and ordinary objects remain reachable where bounds overlap, including the window gap and a room-sized wall? diff --git a/docs/specs/phase_19_scenery_backgrounds_surface_finishes.md b/docs/specs/phase_19_scenery_backgrounds_surface_finishes.md new file mode 100644 index 0000000..30c5604 --- /dev/null +++ b/docs/specs/phase_19_scenery_backgrounds_surface_finishes.md @@ -0,0 +1,252 @@ +# Phase 19 — Scenery, backgrounds, and surface finishes: architecture and implementation specification + +> **Status:** Original Phase 19 scenery architecture and the approved Stage-relative sizing amendment are integrated; automated and render-capable checks pass. This specification follows the [accepted product brief](../plans/phase_19_scenery_backgrounds_surface_finishes.md), the [approved UI/UX specification](phase_19_scenery_backgrounds_ui_ux.md), and the [Stage panel UI system](stage_panel_ui_system.md). Full Phase 19 user/editor verification remains pending. The earlier sections describe the original implementation contract; the sizing amendment starts from the current `.scene` v1.5, `.prop` v1.1, Stage controller, and shared UI. + +## Decisions and scope + +1. Use independent, inert `Node2D` placements for wall panels and `.prop` Scenery artwork. The source `.prop` is a **live reference**: its current vector art is used on reopen. Missing art remains a saved, visible, repairable placement. A given `.prop` may be used as inert Scenery and, when it has valid collision geometry, as a functional Prop. +2. Sky is one adaptive, viewport-filling, screen-space vector drawing behind the world. Its details stay fixed during camera movement; it does not tint other objects. Existing scenes and Blank Scene use an internal neutral Sky. The four child-facing Sky presets are Clear Day, Cloudy Day, Sunset, and Starry Night. +3. The existing Prop Editor opens **inside the live Stage session** for Make artwork. The Stage and its unsaved document remain in memory, with Stage input suspended until the editor returns. The standalone F6 route remains valid. +4. Select several creates a temporary Scenery selection and snapshot-based transform preview. It does not create a persistent group or a parent that changes the saved hierarchy. Sky, Ground, characters, and functional Props are outside this transform. +5. Phase 19 adds neither Stage Undo/Redo nor a whole-set transform. Group preview has its own exact Cancel/Keep boundary. Stage-wide Undo/Redo is a separately planned phase and is logged in [technical debt](../architecture/tech_debt_and_optimizations.md). + +## Existing constraints and ownership + +`scenes/stage/sandbox_stage.tscn` contains a `SandboxStage` root, `Camera2D`, and `World`. The scene adapter serializes direct `World` children; `StageSceneIO` currently writes v1.4. `StageSelection` currently considers every direct world child selectable by world AABB. `StageSpatialIndex` indexes AABBs into 16 px cells and `StageTerrainPainter` treats any indexed overlap as an occupancy conflict. `PropBlock` currently draws at `z_index` -1, 0 (legacy), or 1, while terrain is 0. These are explicit integration points, not implicit behavior to rely on. + +```mermaid +flowchart TD + Chrome[StageChrome / shared gallery] --> Intent[SandboxStage input arbitration] + Intent --> Decor[StageSceneryController] + Intent --> Terrain[StageTerrainPainter] + Decor --> World[World: WallPanel + SceneryArtwork] + Terrain --> World + World --> Adapter[StageSceneAdapter] + Sky[StageSkyView] --> Adapter + Adapter --> IO[StageSceneIO v1.5] + Art[PropFile + PropLibrary visual scan] --> Decor + Art --> Editor[Stage-hosted PropEditor] +``` + +| Owner / location | Typed contract or change | Boundary | +| --- | --- | --- | +| `scripts/stage/stage_draw_order.gd`, `class_name StageDrawOrder extends RefCounted` | Constants `WALL := -40`, `FAR := -30`, `NEAR := -20`, `TERRAIN := -10`, `FLOOR := -5`; `static func scenery_z(depth: StringName) -> int`; `static func apply_terrain(node: TerrainBlock) -> void` | One drawing policy. Leave the existing Prop `-1/0/1` and character relative order intact; rebase terrain drawing only. Never serialize raw `z_index` in place of a semantic depth/finish. | +| `scripts/stage/scenery/stage_sky_view.gd`, `class_name StageSkyView extends Control` | `func set_sky(id: StringName) -> void`; `func get_sky() -> StringName`; `func apply_theme(theme: SandboxTheme) -> void` | Full-rect child of a dedicated `CanvasLayer` with `layer = -1`, mouse ignored. Deterministic vector `_draw()` on viewport resize/theme/choice only. No world hit test or physics. | +| `scripts/stage/scenery/stage_wall_panel.gd`, `class_name StageWallPanel extends Node2D` | `var finish_id: StringName`; `var tint: Color`; `var panel_size: Vector2`; optional `var motif_origin_offset: Vector2`; `func configure(size: Vector2, finish: StringName, color: Color) -> void`; `func get_local_bounds() -> Rect2` | Finite world rectangle centered at `position`. No `CollisionObject2D`, navigation, support, or interaction registration. Resize changes `panel_size`, never root scale, so motifs retain their world pitch. The renderer lays the pattern out from each panel's current world transform, so eraser-created fragments keep continuous brick courses; the optional offset preserves source-wall lineage in saved data. | +| `scripts/stage/scenery/stage_scenery_artwork.gd`, `class_name StageSceneryArtwork extends Node2D` | `var asset_path: String`; `var display_name: String`; `var depth: StringName`; `var last_known_local_bounds: Rect2`; `var missing: bool`; `func load_visual(path: String) -> bool`; `func set_depth(value: StringName) -> void`; `func get_visual_world_bounds() -> Rect2`; `func get_saved_footprint() -> Rect2` | Build a `Visuals` child via `PropVisuals.build()` from the complete shape stack. No `PropBlock`, collision hull, physics, Use spots, anchors, or event identity. If loading fails, render a neutral marker/known footprint from saved data and preserve path/name/transform. | +| `scripts/stage/scenery/stage_surface_renderer.gd`, `class_name StageSurfaceRenderer extends RefCounted` | `static func is_wall_finish(id: StringName) -> bool`; `static func is_ground_finish(id: StringName) -> bool`; `static func draw_wall(canvas: CanvasItem, rect: Rect2, finish: StringName, tint: Color, world_origin: Vector2) -> void`; `static func draw_ground(canvas: CanvasItem, rect: Rect2, finish: StringName, tint: Color, world_origin: Vector2) -> void` | Shared vector definitions for Paint/Brick/Wood, with Grass/Dirt/Sidewalk/Asphalt for Ground. One motif vocabulary and world-coordinate phase; no raster scale blur. Drawing helpers clip motifs to finite panel/block bounds and bound work to visible tiles. | +| `scripts/terrain/terrain_block.gd`, `class_name TerrainBlock extends StaticBody2D` | Add `var finish_id: StringName` and `func set_finish(id: StringName) -> void` for `spawn_id == "ground"`; preserve `polygon_points`, fill, outline, and `CollisionPolygon2D` | Ground appearance changes without altering collision geometry, navigation, or terrain occupancy. Ramp/Step retain the old look. | +| `scripts/editors/stage/stage_scenery_controller.gd`, `class_name StageSceneryController extends Node` | `signal placement_committed(node: Node2D)`; `signal document_change_requested`; `signal preview_state_changed(active: bool)`; `func begin_artwork(path: String, depth: StringName) -> bool`; `func begin_wall(finish: StringName, initial_world_position: Vector2, tint: Color) -> void`; `func begin_wall_eraser(initial_world_position: Vector2) -> bool`; `func cancel_active_gesture() -> void`; `func exit_tool() -> void`; `func begin_group_transform(kind: StringName, members: Array[Node2D]) -> bool`; `func preview_group(delta: Vector2, factor: float) -> bool`; `func commit_group() -> void`; `func cancel_group() -> void` | Owns Scenery-only tool/substate and snapshots. A wall-eraser release subtracts its snapped rectangle from every intersected wall panel and emits one document change when anything changed. `SandboxStage` retains top-level mode, Save, dirty, and input priority. Typed intent signals do not pass UI controls into persistence. | +| `scripts/editors/stage/sandbox_stage.gd`, `class_name SandboxStage extends Node2D` | Route Scenery tools before ordinary selection/placement; gate save/PLAY while a group preview needs resolution; apply controller commits through existing `_mark_scene_dirty()` and `_save_object_state()` as appropriate | Do not append all Phase 19 interaction state to the root controller. Cancel pending drafts on tool/mode changes before existing direct/play transitions. | +| `scripts/stage/stage_selection.gd`, `class_name StageSelection extends RefCounted` | Add a typed selection context, for example `enum SelectionScope { ACTION, SCENERY }` plus `func set_scope(scope: SelectionScope) -> void`; make hit/hover/box use the same predicate | Ordinary EDIT excludes wall/artwork, including missing broad bounds. Edit Scenery includes only those; smaller cutouts outrank panels, with the overlap chooser for remaining ambiguity. Sky is never selectable. | +| `scripts/stage/stage_scene_io.gd`, `class_name StageSceneIO extends RefCounted`; `scripts/editors/stage/stage_scene_adapter.gd`, `class_name StageSceneAdapter extends RefCounted` | v1.5 migration/validation, sky and new object encode/decode, known footprint, Ground finish | Validate document before `_reset_scene_content()`. Invalid new fields cannot silently erase the current scene. Preserve missing Scenery entries across save cycles. | + +`StageSpawner` remains the factory for physical terrain, Props, stickmen, and Areas. A small Scenery factory may live in `StageSceneryController` or `scripts/stage/scenery/` to create inert nodes; it must not route visual artwork through `spawn_prop_asset()`. `StageRunSession` and the event/rule engine must treat Scenery nodes as nonparticipants even in PLAY and paused EDIT. Keep Scenery outside the physical spatial index and terrain-paint candidate set, but include it in `World` serialization and Scenery-only selection. + +## Scene v1.5 contract + +All scene objects remain independent entries in the existing `objects` array. `file_id` is unique within a file, allocated by the adapter; it is not a durable group ID. Scenery object transforms use world coordinates. The Sky ID is a root property. The examples omit unrelated current fields. + +```json +{ + "version": "1.5", + "scene_name": "Living Room", + "sky_id": "sky_clear_day", + "camera": {"position": {"x": 0, "y": -400}, "zoom": 0.5}, + "objects": [ + {"file_id": "wall_panel_1", "type": "wall_panel", "position": {"x": -280, "y": -420}, "size": {"x": 320, "y": 400}, "finish_id": "finish_wall_paint", "color": "#5A9D70"}, + {"file_id": "scenery_artwork_1", "type": "scenery_artwork", "asset_id": "res://content/props/window_frame.prop", "display_name": "Window frame", "depth": "near", "position": {"x": 0, "y": -450}, "rotation": 0, "scale": {"x": 1, "y": 1}, "last_known_local_bounds": {"position": {"x": -80, "y": -70}, "size": {"x": 160, "y": 140}}}, + {"file_id": "terrain_1", "type": "terrain", "asset_id": "ground", "position": {"x": 0, "y": 0}, "rotation": 0, "scale": {"x": 1, "y": 1}, "finish_id": "finish_ground_wood"} + ], + "event_rules": [], + "metadata": {} +} +``` + +The document may contain internal `sky_neutral` and `finish_ground_legacy` IDs to reproduce the prior blank/background and green Ground. They are valid for loading/migration but are not extra child-facing picker cards. For wall panels, `position` is the rectangle center, `size` has strictly positive finite x/y, and rotation is fixed at zero. Group resize writes the new size and center while leaving wall root scale at `(1, 1)`. For artwork, `scale` is finite, positive, and uniform for Phase 19 editing; `rotation` is finite and preserved for existing/scripted entries and repair previews, though the Phase 19 UI offers no rotation control. Depth IDs are exactly `far`, `near`, `floor`; Near is the placement default. + +The manual **Erase wall area** tool adds one optional wall field, `motif_origin_offset: {x, y}`, recording the vector from a fragment's saved `position` to its source wall's origin; absent means `(0, 0)`, so existing v1.5 scenes remain valid and no version bump or migration is needed. When a rectangle intersects a wall panel, transform its four corners into that panel's local space, intersect with the finite local bounds, and replace the original with up to four non-overlapping rectangular fragments outside the intersection. Copy the finish, tint, placement ordering, and source-wall lineage to every fragment. Render each fragment's surface pattern using its current world origin, not the inherited source origin, so brick courses remain on the same world-space lattice across the new fragment boundaries. Remove the source panel when the intersection covers it fully; do not touch artwork or non-wall objects. This is visual editing only: no glass, collision, doorway, Ground, or physics behavior is inferred. Each valid release commits immediately through the existing document-dirty signal; Done/Cancel/RMB/Esc exits the tool, and canceling a held drag discards only that uncommitted drag. There is no Stage Undo in Phase 19. + +`last_known_local_bounds` is cached geometry metadata, updated when a live asset loads and retained unchanged if it later goes missing. Include stroke width when deriving it, so horizontal/vertical line-only artwork still has a positive visible footprint without inventing collision geometry. It supports a repair footprint and safe group movement; it is not embedded artwork. Missing or malformed optional bounds fall back to a compact world marker and disable group resize until repaired. `display_name` is a short saved fallback label. A repaired placement changes only its selected `asset_id`, name, and known bounds after explicit confirmation; its position, scale, rotation, depth, and unrelated placements remain. The default repair is one placement. A future or optional explicit “Use for all matching places” path must show a count and ask separately before changing other references. + +`StageSceneIO._migrate_to_current()` accepts v1.0 through v1.4 and adds `sky_neutral` plus `finish_ground_legacy` to old Ground entries, without changing old Prop role and draw-order migrations. v1.5 validation checks top-level Sky ID, object type payloads, finite vectors and numbers, positive wall sizes and artwork scales, allowed finish/depth IDs, type-safe color strings, unique `file_id`, and canonicalized asset paths. Malformed optional cached art bounds produce a warning and the no-footprint repair marker, rather than rejecting otherwise valid scene content. The adapter creates a missing Scenery node when the source cannot load; missing source is a warning and repair state, not document rejection. Unknown object types follow the existing warning/skip convention only after structural validation. The existing safe write and autosave paths write the same v1.5 snapshot. Manual Save asks to Keep or Cancel a pending group preview; background autosave waits until the preview is resolved. On mode switch, cancel the preview before the current PLAY-entry autosave. Draft values are never serialized. + +## Visual layers, Sky, and finishes + +The semantic draw order is Sky → wall panels → Far art → Near art → terrain → floor art → characters and Props. `StageDrawOrder` applies bands to direct `World` children. Set all existing terrain varieties to the terrain band for consistent physical-surface drawing; their collision and input remain unchanged. Prop `-1/0/1` choices and World child order among legacy depth-zero Props and stickmen are preserved. Reapply the policy on spawn, load, depth change, and reset. Selection must use the same semantic tier order plus smaller actual art footprint preference, rather than assuming child insertion order expresses depth. + +`StageSkyView` lives in its own `CanvasLayer` behind `World`, not under the camera or a wall. Its draw rectangle follows viewport size, so panning/zooming cannot reveal an edge. Use deterministic vector gradient bands, stars, clouds, sun/moon and shapes with bounded point counts. The preset controls only this view. Rendering should be static between sky or viewport changes; no per-frame random placement, animation, parallax, or tint propagation. Sky thumbnails use the same drawing definitions at card size so the choice predicts the Stage. + +`StageSurfaceRenderer` defines finite vector motifs with a world-aligned origin and fixed world-unit pitch. Wall and Ground may share Brick/Wood definitions but use context-aware fill/outline colors and scale. `StageWallPanel._draw()` uses its current finite rectangle as a clip and asks the renderer for only intersecting motif cells. For brick walls, cull to the camera-visible local rectangle; if that visible area exceeds 12,000 cells, increase the brick pitch uniformly for all visible courses instead of truncating rows or columns. This keeps large walls fully patterned and fragments continuous without unbounded draw work. Ground renders within its existing rectangle geometry; existing `CollisionPolygon2D` and navigation surfaces are never rebuilt merely for a finish change. The selected **New Ground look** is passed into both `StageTerrainPainter` ghost creation and each committed Ground block; it never repaints earlier blocks. During group resize, wall motif lines retain their pitch and phase. Ground blocks adjacent on a shared edge use the same world phase; shared same-finish interior borders are suppressed or visually joined so a painted run reads as one surface. The neighbor check refreshes on place, delete, move, load, and per-piece finish change, and considers only nearby Ground rectangles; it does not change occupancy/collision. Other motif types remain bounded by the camera-visible region or a maximum cell count per finite panel so `_draw()` work stays limited. + +## Selection, input, and transform state + +```mermaid +stateDiagram-v2 + [*] --> OrdinaryEdit + OrdinaryEdit --> Chooser: Scenery palette + Chooser --> PlaceArt: artwork chosen + Chooser --> DrawWall: wall look chosen + Chooser --> EditScenery: Edit placed scenery + PlaceArt --> OrdinaryEdit: Done / Cancel / RMB / Esc + DrawWall --> OrdinaryEdit: Done / Cancel / RMB / Esc + EditScenery --> GroupPreview: Move together / Resize together + GroupPreview --> EditScenery: Keep changes / Cancel / RMB / Esc + EditScenery --> OrdinaryEdit: Done editing / Esc +``` + +The controller tracks one active Scenery tool at a time, the number of committed copies this session, and whether a wall/touch draft is active. A passive artwork ghost is never a committed edit. Right-click/Done/Cancel follow the approved [exit matrix](phase_19_scenery_backgrounds_ui_ux.md#placement-exit-cancel-and-mode-changes): committed pieces stay; an active draft is discarded; a late release cannot commit it. Existing terrain and Prop placement are canceled when a Scenery tool takes input. DIRECT/PLAY clears Scenery ghosts and Edit Scenery selection. + +`StageSelection` filters both click and box paths. Ordinary EDIT includes existing actionable objects and excludes Scenery broad bounds. In Edit Scenery, candidate geometry comes from a wall rectangle or the full transformed vector art: filled polygons use point-in-polygon, open strokes use a screen-scaled segment tolerance, and missing art uses its marker or known footprint. A small cutout wins over a broad wall even if both are candidates; unresolved overlaps use the shared “Pieces here” popup. The small explicit missing-art **Find artwork** badge is a separate 44 px screen-space hit target that can open Edit Scenery without making the entire missing footprint selectable in ordinary EDIT. Sky never enters either selection scope. + +For one selected artwork, a uniform scale changes root `scale` within finite positive bounds. For a wall, resizing changes `panel_size`, with a minimum visible world size and no mirrored/negative rectangle. Pointer/touch handle hit areas are at least 44 logical pixels after camera zoom compensation. `StageGizmos` can share outline/handle drawing helpers; a Scenery-specific adapter is justified for rectangle sizing, no rotation control, and the pending group preview. It must still use `SandboxTheme.overlays` selection colors and the Stage halo style, rather than duplicating visual tokens. + +Group preview snapshots `global_position`, `rotation`, artwork `scale` or wall `panel_size`, and the initial world bounds of each selected member. For move, derive one delta from the selected union anchor, apply Snap once to that anchor, then add the same delta to every snapshot position. For resize, compute one positive factor around the opposite union corner `P`; for each artwork center `x`, set `x' = P + factor * (x - P)` and `scale' = factor * scale`; for each wall center/size, use the same center formula and `size' = factor * size`. Derive every preview frame from the snapshots, never the previous preview frame. Reject factor `<= 0`, any collapsed member, or any out-of-range art scale. Buttons use the same formula with top-left fixed. Missing members with no known footprint may move but block group resize with the approved explanation. Mouse release leaves the candidate preview pending. `Keep changes` saves each authored transform, marks dirty once, and returns to Edit Scenery. `Cancel`, RMB, or Esc restores all snapshots exactly and leaves the same selection. A Save request opens a Keep/Cancel decision for the preview and resumes Save only after explicit resolution; tool/mode changes cancel the preview. A temporary group selection and preview are never written to `.scene`. + +## Phase 19 addition: authoring size and single-placement transforms + +This addition implements the approved [Stage-relative sizing flow](phase_19_scenery_backgrounds_ui_ux.md#stage-relative-authoring-size-in-prop-editor) inside Phase 19. The standard standing guide spans **939.5 world units** from head-circle top to ankle line; write `H := 939.5` as one Prop Editor reference constant derived from `WholeStickmanPreview.GUIDE_JOINTS` and `GUIDE_HEAD_RADIUS`, so the UI never assumes the old 96-unit Normalize Size target. The presets are starting points based on the artwork's **longest visible dimension**, including stroke: Small = `0.35 H`, Room = `1.0 H`, Big = `2.0 H`. This covers both tall and wide drawings without forcing a wide rug or skyline to be one stickman tall. Each **Make bigger** step multiplies by `1.1`; **Make smaller** multiplies by `1 / 1.1`, so opposite steps return to the same size within floating-point tolerance. The labels are approximate comparison choices, not an inferred real-world category or a maximum size. Artist size inference is part of authoring/review instructions; no runtime name parsing, asset size tag, or automatic normalization is added. + +### Prop Editor authoring transaction + +`PropEditor` (`class_name PropEditor extends Control`) replaces the Arrange toolbar's immediate `_on_normalize_size_pressed()` action and `NORMALIZED_MAX_EXTENT` with **Size for Stage**. The action is disabled when the document has no drawable geometry or its longest visible extent is not finite and positive; its tooltip says **“Add visible artwork to use Size for Stage.”** It owns the toolbar controls, Keep/Cancel routing, and the existing `_dirty`/autosave boundary. `PropCanvas` (`class_name PropCanvas extends Control`) owns a temporary deep copy of its complete shape stack and the supplied editable-anchor dictionary; its normal `shapes` and the editor's `_anchors` remain the committed document while previewing. A small typed API is sufficient: + +```gdscript +func begin_stage_size_preview(anchors: Dictionary) -> bool +func preview_stage_size(factor: float) -> bool +func commit_stage_size_preview() -> Dictionary # {"changed": bool, "anchors": Dictionary} +func cancel_stage_size_preview() -> void +func is_stage_size_preview_active() -> bool +func get_visible_shapes_bounds() -> Rect2 # Includes outline/stroke width. +func fit_stage_size_view(bounds: Rect2, padding_px: float) -> void +``` + +The preview records original internal shape dictionaries, anchors, canvas pan/zoom, and the entry bounds once. Preset factor is `target_longest_edge / entry_longest_edge`; fine buttons adjust the current absolute factor, but **every rendered candidate is derived from the entry snapshot**, never scaled from the previous candidate. The shared `origin` anchor remains at `(0, 0)`. Scale all polygon/line points, circle points and radius, each shape's `outline_width` (including line-only art), and every editable anchor position by the same factor about the document origin. Preserve colors, shape order, vertex flags, `reshaped`, anchor roles, and anchor angles. This is the established whole-document pivot, so Size for Stage does not silently recenter the saved origin or detach grips from their art. Reject a candidate atomically if factor, coordinate, radius, stroke width, anchor position, or visible extent is nonfinite or outside the existing representable coordinate/size limits; leave the last valid preview visible and the committed document untouched. Do not clamp one shape or anchor independently. Zero-width strokes remain zero. A no-op factor of 1 leaves the document clean when it was clean on entry. + +Draw the size candidate in `PropCanvas._on_canvas_draw()` and then draw the guide **after all artwork**, with dark halo and light core for the bones, head, and floor line. Derive the comparison floor from the **candidate's current visual bottom**; align both guide ankles to that y and place the figure just to the artwork's left (or over its left portion when width is tight), so offset and centered authoring coordinates compare correctly without translating the artwork. With an empty canvas, draw the guide at its normal origin and ankle line; Size for Stage remains disabled until artwork exists. The floor cue can follow a changed candidate bottom. On entry, compute a union of candidate artwork plus the guide/floor and frame it with a bounded margin. Refit that union after a preset choice and when a fine step would clip it; do not reframe on every draw or override a child's deliberate pan/zoom while the union remains visible. The sizing view may temporarily fit below the canvas's normal manual `MIN_ZOOM` on narrow viewports and restores the prior pan/zoom on exit. Outside the sizing view, show the same foreground guide and floor whenever **Show Stickman Guide** is enabled. During sizing, force guide drawing on without changing or persisting that checkbox; restore its user preference on exit. The guide is canvas drawing only, never a saved shape, anchor, or Stage object. + +**Keep size** validates the full candidate, swaps the complete shapes/anchors once, emits one document change, refreshes inspector/thumbnail state, and starts the existing dirty/autosave timer once if the result differs from the snapshot. **Cancel**, Esc, mode switch, New/Load, return to Stage, or closing the editor discards the candidate before another document action; no preview geometry enters Save/autosave. A Save request while the sizing view is open asks the child in the existing sizing controls to **Keep size** or **✕ Cancel** first, then the child can use ordinary Save/Save As; no new save dialog or asset-copy policy is introduced. `Transform Whole Prop`, Attach, Reshape, and canvas editing cannot run concurrently with this sizing transaction. The present whole-prop Transform action remains available in Arrange after sizing. + +```mermaid +stateDiagram-v2 + Arrange --> SizePreview: Size for Stage / snapshot + SizePreview --> SizePreview: Small, Room, Big, smaller, bigger / derive from snapshot + SizePreview --> Arrange: Keep size / validate, commit once, dirty once + SizePreview --> Arrange: Cancel or Esc / discard, no dirty + SizePreview --> Arrange: leave editor or switch mode / discard, no dirty +``` + +### Direct Stage move and resize of one Scenery piece + +`StageSceneryController` (`class_name StageSceneryController extends Node`) takes ownership of the currently selected Scenery drag, replacing `SandboxStage`'s local `_scenery_drag_snapshots`/`_scenery_dragging` move path. It reuses its existing group snapshot math and `document_change_requested` signal but keeps the single gesture's commit boundary separate: release commits immediately; group release still leaves a candidate requiring **Keep changes**. Suggested typed controller contract: + +```gdscript +signal single_gesture_changed(active: bool) +func begin_single_transform(node: Node2D, kind: StringName, pointer: Vector2, dragged_corner: Vector2 = Vector2.INF) -> bool +func preview_single_transform(pointer: Vector2) -> bool +func commit_single_transform() -> bool +func cancel_single_transform() -> void +func is_single_transform_active() -> bool +func step_single_size(node: Node2D, factor: float) -> bool +``` + +`kind` is exactly `&"move"` or `&"resize"`; the controller accepts only one selected `StageWallPanel` or `StageSceneryArtwork`. Snapshot `{node: Node2D, position: Vector2, rotation: float, scale: Vector2, panel_size: Vector2 when wall, bounds: Rect2, pointer: Vector2, dragged_corner: Vector2}`. This is transient in-memory state, never a `.scene` payload. A missing artwork without known bounds can move but cannot resize. Before applying any candidate, validate the whole result: finite position and scale, positive artwork scale components within the current Stage range, positive wall dimensions above `MIN_PANEL_SIZE`, valid node still in Edit Scenery, and a nondegenerate starting AABB **for resize**. A move preserves any existing positive nonuniform scale. A resize multiplies both scale components by one uniform factor, preserving the ratio of an imported/scripted placement without normalizing it. Rejecting a frame retains the last valid preview. An unchanged release emits no document change. + +For move, derive `raw_delta = pointer - pointer0` from the snapshot. With Snap on, align the **visible world AABB's top-left corner** rather than the node's often offset origin: `delta = snap(bounds0.position + raw_delta) - bounds0.position`; apply `position0 + delta`. This preserves the existing top-left grid-alignment promise for placed wall panels and artwork. For uniform resize, take the world AABB corner opposite the dragged corner as the fixed point `P`. With `D = dragged_corner - P`, use `factor = 1 + (pointer - pointer0).dot(D) / D.length_squared()`; reject nonpositive/invalid factors. For artwork with saved local geometry, `position' = P + factor * (position0 - P)` and `scale' = scale0 * factor`, preserving rotation and depth. For walls, use the same position formula and `panel_size' = panel_size0 * factor`, leaving root scale at 1 so Brick/Wood motif pitch does not stretch. The fixed world corner stays fixed even when artwork's authored origin is offset. **Smaller**/**Bigger** in the selected-piece card call `step_single_size()` with reciprocal factors `1/1.1` and `1.1`, top-left pinned; each valid press is an immediate independent commit for that placement only. Other artwork instances, walls, Ground, Props, characters, and their interaction state are untouched. + +`StageGizmos` supplies selection outline and corner drawing/hit geometry, using existing `SandboxTheme.overlays` colors and dark-halo/light-core selection language. Extend its Scenery adapter with at least **44 × 44 logical-pixel** hit regions converted through camera zoom, prioritizing a corner hit over the body; do not reuse its current `PropBlock`-only SCALE gesture or origin-pivot formula. The Stage root arbitrates input once: selected corner starts resize, selected body starts move, and group preview has priority while active. `StageSceneryController` previews directly on the selected node, then emits `document_change_requested` exactly once on changed release; `SandboxStage` updates its authored-state/save path and card. Esc, right-click, visible capture-bar **✕ Cancel**, mode/tool switch, selection removal, or node deletion restores the snapshot and clears the gesture. An input-generation token or equivalent active-state guard must consume a mouse/touch release after cancellation so it cannot commit an old draft. Manual Save first cancels an active single gesture and saves only prior commits; background autosave waits for it to end. Group preview retains its existing separate Keep/Cancel save gate. + +```mermaid +flowchart LR + Pointer[Corner/body press] --> Hit[StageGizmos hit: corner first] + Hit --> Begin[SceneryController snapshot] + Begin --> Preview[Pointer motion from snapshot] + Preview --> Release[Release: commit once] + Preview --> Cancel[Esc/RMB/capture Cancel: restore] + Release --> Dirty[Stage dirty + scene v1.5 placement transform] + Cancel --> Idle[No dirty; stale release ignored] +``` + +The selected artwork card adopts `StageInspectorPanel` for fixed header/scrolling body, `StagePanelChoiceGroup` for Far/Near/On the floor, and `StageInspectorPanel.style_action_button()` for Smaller/Bigger and Erase. The selected wall card uses the same shell and action styling. Both call `set_safe_rect()` and preserve the compact Settings reopen behavior. `StagePanelToggleRow` is reserved for actual booleans; no new panel control is needed for size. The already shared `StageUseTargetCaptureBar.set_capture_actions()` shows **✕ Cancel** only while a single drag is held; group Keep/Cancel remains unchanged. New sizing controls in Prop Editor use its existing Arrange toolbar and `PropTheme`, because that editor is a distinct workflow from an in-stage selected-object card. No duplicate dialog, capture-bar, choice, or button styling is introduced. + +The existing `.scene` v1.5 `position`, `scale`, `rotation`, and wall `size` fields already persist these per-placement changes; `.prop` v1.1 already stores points, circle radius, outline width, and anchors. This addition requires no schema bump and performs **no load-time resizing or migration of asset geometry or placement scale**. Saving an explicitly revised `.prop` at its existing path intentionally updates all live-reference Scenery and Props that use that path when reloaded; ordinary Save and Save As remain the only asset save actions. A scene's saved placement scale continues to multiply its current referenced art. There is no new asset-version enforcement, copy/update prompt, automatic compensation factor, or runtime name-based size inference. + +## Asset catalog, tags, thumbnails, and missing-art repair + +`PropFile` advances to v1.1 with optional top-level `tags: Array[String]`. It accepts v1.0 files with `tags = []`, normalizes a fixed set `nature`, `buildings`, `walls`, `roads`, `objects`, deduplicates, and writes only known IDs. Tags describe appearance, never physics. The same file remains the only vector authoring format; no `.scenery` file or new asset library root appears. Keep cutouts as immediate children of `content/props/` for existing scans; unapproved art stays under ignored `content/review/artist//` and must not enter browser results. `ContentPaths.canonicalize()` handles saved paths, including old `props/` paths, while preserving external paths. + +Split library eligibility by role without duplicating parsing: a shared parse/cache step returns `{id: String, path: String, name: String, shapes: Array[Dictionary], tags: Array[String], visual_bounds: Rect2, material_preset: int, collision_hull: PackedVector2Array}`. Scenery accepts nonempty visible vector geometry, including open lines and transparent-window frames, via `PropVisuals.build()`/shape bounds. A closed filled shape needs three noncollinear points; an open stroke needs two distinct points, positive width, and visible color. These rules accept line art without accepting a blank `.prop`. Props retain the existing valid closed-hull requirement. The existing `StageAssetBrowser` scan, asynchronous thumbnail queue, `ThumbnailCache`, and `PropThumbnail.render_shapes()` are reused for either role; a line-only artwork thumbnail must not go through `flatten_to_payload()` as a fake collision shape. Scenery keeps authored per-shape colors rather than applying a Prop material tint. All artwork includes untagged assets; Unsorted appears when appropriate. The same tag filters are available in Props, but its entries remain physically eligible only. Do not scan review directories or recursively change the existing immediate-child library behavior. + +On scene open, load every live artwork reference. A missing/invalid source leaves a `StageSceneryArtwork` node with `missing = true`, saved transform/depth, remembered name, and last known footprint. Render a neutral translucent footprint with dashed light/dark edge when bounds exist, otherwise a compact world marker, with a constant-screen-size picture/question icon and “Artwork not found” label. It stays subdued in DIRECT/PLAY and is never solid. Show one themed missing-assets summary for the load with the UI spec’s **Show scenery** / **Later** actions, while retaining separate Prop/stickman warning semantics. The Scenery piece list has a **Needs artwork** section. + +**Find artwork…** uses the established `.prop` chooser; **Choose another artwork** uses the common Artwork gallery. The candidate must pass Scenery visual validation before a ghost appears at the saved position/size/orientation/depth. The explicit **Use this artwork here?** confirmation commits only the selected placement. Cancel/failure leaves the original path, marker, and scene dirty state untouched. Replacing art marks dirty once and refreshes the known bounds, name, and visual tree; the original position, scale, rotation, and depth remain. A changed source file is picked up on reopen for every live reference; there is no embedded snapshot. The optional File details disclosure may show the path for an adult. The simple one-placement repair is required; an all-matching bulk repair is optional and must be explicitly confirmed with its count. + +## Starter scenes and document lifecycle + +Keep versioned starter documents under `content/stages/starters/` with stable IDs for Blank, Town Street, and Living Room. They are ordinary `.scene` snapshots, not locked nested instances. Blank may be assembled from the current neutral empty-scene defaults; Town Street and Living Room data use the same wall, artwork, terrain, Prop, Sky, and camera schema as a saved child scene. The bundled files reference only explicitly approved production art. A window is a genuine gap between wall rectangles with a transparent frame and Sky behind it; no cutting or glass behavior is inferred. Furniture Props receive deliberate still/block/support roles from the normal Prop contract, without promising Sit. + +The current `PendingFileAction.NEW_SCENE` unsaved Save/Discard/Cancel gate remains authoritative. After it resolves, show the shared starter gallery **before** replacing the current document. Canceling the gallery leaves the existing scene and post-gate dirty state. Selecting a starter preflights its scene data, loads ordinary editable objects, resets `current_path` to empty, stamps a fresh creation time, proposes its editable name, and marks the new document unsaved. Failed starter validation leaves the prior scene intact and shows one themed error. No starter is inserted into another scene. A first-save path follows the existing Save As flow. CLI scene playback continues to load `.scene` directly, with no starter-specific runtime branch. + +## UI component and theme reuse + +| Surface | Reuse or shared extension | Feature-local work / justified exception | +| --- | --- | --- | +| File → New gate | `StageChrome` File menu; `StageModalViews` unsaved dialog; `StageDocumentController` pending action | Starter-choice result and fresh-document handoff only. | +| Starter, Scenery, Sky, finish and replacement galleries | Extract a responsive shared card-gallery body from `AssetSelector`/`scenes/stage/asset_selector.tscn`; retain `AssetCard`, `StageAssetBrowser`, thumbnail pipeline | Tabs, descriptive text, and filter meaning are feature-specific. Replace the current fixed four-column/12-per-page and `FOCUS_NONE` card limitation in the shared path: adaptive columns, scroll, native focus, visible focus ring, and keyboard activation. Non-placeable cards disable card drag. | +| Browse tags and authoring tags | One shared wrapping chip/choice treatment using native Buttons and `FlowContainer`/`HFlowContainer`, `SandboxTheme` popup tokens | Single-select browser filter versus multi-select authoring tags have distinct state rules. Do not use vertical `StagePanelChoiceGroup` as a chip row. | +| Wall color | Extend `ColorSwatchSelector` for wrap/selected/focus; native color picker in a `StageModalViews` themed window | Chosen wall color remains local to the placement/selected panel. | +| Selected artwork, missing art, wall, Ground, multi-selection | Direct use of `StageInspectorPanel`, `StagePanelChoiceGroup`, `stage_panels` tokens and `StagePropSettingsView` dismissal/compact Settings precedent | Domain rows and commands stay in a Stage Scenery/Ground view adapter. `StagePanelToggleRow` is used only for a real boolean, not depth or finish. | +| Member list / overlap popup / wall-area eraser | `StageInspectorPanel` action styling; generalize `StageModalViews.show_objects_here()` labels and candidate filter; reuse `StageUseTargetCaptureBar` for the eraser prompt and Done/Cancel | Membership, piece labels, the red rectangle preview, and wall subtraction are Scenery-specific. The selected wall card starts the tool; no new dialog, overlap popup, or capture-bar styling. | +| Placement/group capture | Extract themed responsive shell from `StageUseTargetCaptureBar`, preserving mode-pill-safe position, prompt/Cancel grouping, action stacking | Use-target and Scenery wording/actions stay in callers. No copied capture-bar CSS/StyleBox code. | +| Cursor and ghosts | `StageChrome` tooltip safe placement, `StagePlacementOverlay`, terrain ghost presentation, `SandboxTheme.overlays` | The draft geometry, validity, and missing marker are feature-specific. | +| Make artwork | Existing `PropEditor`, `PropCanvas`, `.prop` Save/recovery and `PropThumbnail` | Add a typed `AuthoringContext { PROP, SCENERY }`, `signal artwork_saved(path: String)`, and `signal return_requested`. In Scenery context hide material/mass/bounce, attachment, and other action settings; keep shape/color/name/tags/Save. | + +The shared UI contracts should remain small and typed. Extract `scripts/shared/ui/asset_gallery_popup.gd`, `class_name AssetGalleryPopup extends PopupPanel`, as the fixed header/Close and scrollable responsive `AssetCard` grid used by the current `AssetSelector` and Phase 19 choosers. It emits `item_selected(entry: Dictionary)` and `cancelled`, and exposes `open_gallery(title: String, entries: Array[Dictionary]) -> void`, `set_thumbnail(entry_id: String, texture: Texture2D) -> void`, and `focus_entry(entry_id: String) -> void`. A card entry has `{id: String, label: String, thumbnail: Texture2D|null, helper: String, draggable: bool}`; the caller maps `id` to its domain item. `AssetSelector` retains its public `open(kind, entries)` facade and uses the shared gallery, so existing Prop/Stickman callers are not broken. `StageAssetBrowser` retains scanning/caching and adds a role-safe filtered query such as `scan_entries(kind: String, tag: StringName = &"") -> Array[Dictionary]`; no gallery owns asset parsing. + +Extract `scripts/shared/stage_ui/stage_capture_bar.gd`, `class_name StageCaptureBar extends PanelContainer`, from the `StageUseTargetCaptureBar` shell. Its intended interface is `signal action_pressed(id: StringName)`, `func configure(stage_theme: SandboxTheme, mode: int, top_bar_height: float) -> void`, `func set_prompt(text: String) -> void`, `func set_actions(actions: Array[Dictionary]) -> void`, and `func hide_capture() -> void`; each action dictionary has `{id: StringName, label: String, prominent: bool}`. Keep `StageUseTargetCaptureBar` as a thin Use-specific wrapper or composition client preserving `cancel_requested` and `change_prop_requested`. Generalize `StageModalViews.show_objects_here()` with a typed title/label provider while retaining its existing facade and `object_selected(node: Node2D)` signal. Existing `StageInspectorPanel`/`StagePanelChoiceGroup` APIs need no new domain knowledge: caller-owned cards call `set_header`, `body`, `set_safe_rect`, `request_fit`, and `add_choice`/`select_no_signal` directly. `ColorSwatchSelector` may change its layout container to `FlowContainer` while preserving `configure(colors: Array[Color], selected: Color, theme: Variant)`, `set_selected`, and `color_selected(color: Color)`. + +All new popup/native-window instances receive `SandboxTheme.control_theme()` explicitly, then `StageModalViews.apply_dialog_theme()` for child font overrides. The latter does not assign a window theme. Extend `SandboxTheme.DEFAULT_CONFIG` and `sandbox_theme.json` only for shared popup/gallery/chip/capture/selection measurements and colors. Use local StyleBox copies; do not mutate the cached shared `Theme`. Selected, hover, focus, and disabled remain distinguishable. All actionable controls and handle hit regions meet the approved minimum 44 × 44 logical pixels. Viewport-safe rectangles keep the mode pill and Close reachable; long content scrolls under a fixed header. The existing queue/rule/speech popups are distinct workflows and are not recast as gallery or inspector shells. + +## Stage-hosted Prop Editor lifecycle + +Instantiate the existing `scenes/editors/prop/prop_editor.tscn` in a full-viewport Stage-hosted UI layer, set Scenery context before its first user interaction, then focus its first drawing/name control. While active, `SandboxStage` routes no world selection, placement, camera mode, or shortcuts to the hidden Stage; dialogs and editor input remain usable. The underlying Stage document, dirty flag, selection, and current scene path remain in memory. The editor keeps its own `.prop` autosave and unsaved-work gate. Its F6 standalone exit still quits its own run; in hosted context Close/Cancel emits `return_requested` to remove the editor view and restore Stage input/focus without quitting the Stage. A successful Save emits `artwork_saved(path)` after the file write; **Place it as Scenery** returns to the Artwork chooser with a fresh scan/thumbnail generation and the saved asset focused. Merely opening/canceling the editor never changes the Stage document. A saved new `.prop` outside the Stage is a separate asset write, so returning without placement leaves the Stage unchanged. + +## Implementation and art gates + +1. Extend data boundaries first: `.prop` tags and role-safe visual scanning; scene v1.5 migration/validation; semantic draw order; inert visual nodes and shared finish/Sky definitions. Validate old scene/Prop compatibility before UI wiring. +2. Generalize the existing card gallery, tag treatment, capture bar, overlap popup, color treatment, confirmation style, and theme tokens. Build the Phase 19 chooser and inspectors from those shared controls. Avoid a parallel popup/card system. +3. Connect placement, selection scopes, missing repair, Ground finish, and group snapshot previews through `StageSceneryController` and the existing Stage save/mode gates. Add the starter gallery and Stage-hosted Prop Editor return path. +4. The original Phase 19 art manifest used `$asset-creation` and approved production assets. **This sizing amendment requires no new vector/raster deliverables**: it draws the existing stickman guide and floor cue in Prop Editor and extends existing Stage handles. An artist may deliberately revise a particular `.prop` through the ordinary asset-review workflow, but implementation does not batch resize built-ins or reopen the original art approval gate merely because this amendment is signed off. +5. Run syntax/import checks and focused existing suites after each integration slice. Add focused behavior checks where actual world state is at issue. Perform render-capable Stage/editor captures and real input-path checks before reporting visual/interactive outcomes as agent-verified. + +Relevant existing regression starting points are `tests/test_phase19_scenery.gd`, `tests/test_phase10_prop_integration.gd`, `tests/test_prop_editor_file.gd`, `tests/test_prop_editor_canvas.gd`, `tests/test_prop_editor_round3.gd`, `tests/test_content_paths.gd`, and `tests/test_phase17a_prop_roles.gd`; run focused affected suites rather than the full historical matrix for each sizing edit. The original Phase 19 fixture covers scenery persistence and group behavior; extend focused coverage for Size for Stage's atomic Keep/Cancel and a real one-piece Stage resize, including late release after cancellation. Exercise production input and save/reopen paths, not only helper calculations. + +## Observable acceptance and verification routes + +Each row names a child path and a repeatable observation. Automated checks may create in-memory fixtures, but should use actual Stage spawning, document, input, and PLAY paths where practical; node/signal existence alone does not establish behavior. + +| Child path / scenario | Observable outcome and measurement | +| --- | --- | +| File → New → cancel starter, then Town Street; save/reopen; repeat Living Room | Existing scene survives chooser Cancel. Each chosen starter is unsaved, individually editable, and preserves Sky, separate panels/art/Props/Ground and positions after save/reopen. Living Room Sky is visible through an actual wall gap. | +| Scenery → Artwork → Nature → Tree; place twice, change depth, RMB; reopen | Two distinct inert copies persist at saved transforms/depths. RMB removes ghost, preserves committed copies, returns to ordinary EDIT. Check `PhysicsDirectSpaceState2D` contacts, navigation candidates, and event participation remain absent for scenery while a normal Prop remains functional. | +| Make line-only artwork in Stage-hosted editor; Save → Place it as Scenery | `.prop` with open strokes appears in Scenery and has a valid thumbnail; it does not enter Props without valid closed collision geometry. Stage unsaved state and focus survive authoring return. Tags filter it without changing its behavior. | +| Draw Brick wall, resize it; paint a Wood Ground run, restyle one block | Wall is finite/inert; brick pitch stays constant. Ground collision and walking path remain valid; wood motifs line up across neighbors, shared seams do not interrupt the run, and only the selected block changes to the new finish. Measure character feet/contact over several physics ticks before and after restyling. | +| Ordinary EDIT click/box in front of broad wall; enter Edit Scenery; select art and wall through Pieces here | Gameplay objects remain reachable in ordinary EDIT. Scenery-only selection finds art ahead of broad panels, includes missing art in Edit Scenery, and never selects Sky. Real pointer/touch/keyboard activation produces the expected selection and popup dismissal. | +| Living Room: select two walls, window frame, rug; move preview then RMB; resize preview then Keep; Save/reopen | RMB restores each exact snapshot transform. Keep applies one positive factor around the opposite fixed corner, preserves artwork proportions/depth and wall motif pitch, leaves Ground/furniture untouched, and persists member transforms without a saved group. Save while pending asks Keep/Cancel and never writes draft values. | +| Prop Editor → Arrange → Size for Stage on a tall refrigerator, wide rug, and small item; try each preset/fine buttons, Cancel once and Keep once | The artwork and standard guide remain fully visible at ordinary and narrow viewport sizes. Longest visible edge reaches approximately `0.35H`, `1H`, or `2H` while proportions, circle radii, line widths, and editable anchor positions share the same factor; the foreground halo guide's ankles meet the floor cue at the candidate bottom. Cancel restores byte-equivalent authored geometry/anchors and the prior dirty state; Keep commits once. A Big preset cannot clip the guide merely because opening began on small artwork. Save/reopen the `.prop` through the normal path and compare geometry/anchors. | +| Edit Scenery → select two copies of one artwork; drag a corner on one, release; then cancel another drag with Esc/RMB/touch Cancel and release the pointer | The opposite visible corner stays at the same world position; only the edited placement changes, and it commits on release without Keep. A canceled drag restores its snapshot and the late release makes no change. With Snap on, a moved artwork or wall's visible top-left lands on a grid intersection. Smaller then Bigger returns near the initial bounds. Save/reopen and check the two copies retain distinct scales, while Props/Ground/PLAY behavior remains unchanged. A missing artwork without known bounds shows no resize handle. | +| Open a pre-amendment `.prop` and `.scene`, then explicitly overwrite one referenced `.prop` through ordinary Save | Opening alone does not rescale artwork or placement values. The explicit source overwrite changes every live reference to that source on reopen, while each scene placement retains its saved scale; there is no hidden compensation, copy prompt, or version migration. | +| Move a referenced `.prop` aside, reopen, repair selected missing artwork; cancel once then confirm | One neutral marker remains at saved position/depth, summary appears once, ordinary EDIT targets are not stolen, cancel keeps path/dirty state, and confirmed repair changes only that placement. Missing footprint/no-footprint group resize rules and PLAY marker presentation hold. Restore source and reopen to confirm live-reference visual update. | +| Select each Sky, pan/zoom, PLAY/stop, reopen | Sky fills screen without an edge or flicker; foreground pixel colors are unchanged by Sky choice. Layer order remains Sky/wall/Far/Near/Ground/floor/actors through camera movement, simulation, and reload; ghosts/handles/capture bars disappear in PLAY. | + +Visual acceptance requires opening the affected scene in a render-capable Godot GUI, capturing the named chooser, normal/zoomed Stage, missing-art, group-preview, **Prop Editor sizing guide at Small/Big and narrow width, active single-piece handles/capture bar**, and PLAY states, and inspecting the pixels. Record capture location, scene/state, and observation. Interactive acceptance requires exercising the actual controls and input events, including RMB/Esc/touch cancellation, button sizing, grid-aligned move, commit on release, and a late release after cancellation. If GUI or input automation is unavailable after an attempted route, name the unverified outcomes and hand the user a focused reproducible check; a headless parse or screenshot file alone is insufficient. Phase 19 user/editor verification remains open until the child checks the integrated behavior. + +## Trade-offs and future work + +- Independent nodes preserve the current editable direct-child document model and avoid using `PropBlock` as a fake inert body. The cost is a separate artwork visual node and explicit scene-object decoding. +- Live `.prop` references keep scenes compact and make art edits visible on reopen. They can change a scene's appearance after the source file changes; the saved name/footprint and repair flow cover missing files, not version pinning. A future immutable asset-version policy would need a separate decision. +- Stage-relative authoring size is geometry stored in `.prop`, while a Stage Scenery resize is an instance transform stored in `.scene`. This keeps legacy loads stable and lets two placements of one source have different sizes. It also means an intentional overwrite of a live `.prop` updates all references; the user chose to retain ordinary Save/Save As without a new copy/update decision. The authoring preview scales outline width and editable anchors with geometry and uses origin as its pivot; its floor guide follows visual bounds solely for comparison. +- A one-piece Stage drag commits on release for speed; a multi-piece group retains Keep/Cancel because its wider effect is harder to judge during a gesture. Both use exact snapshots before commit, while Stage-wide Undo remains deferred to the separate phase logged in technical debt. +- Rebased terrain drawing enables the required floor-art tier without changing Prop-to-character depth semantics. It changes terrain's relative visual position beneath Props; physics and navigation stay unchanged and must be regression checked. +- Vector motifs retain clarity and world alignment without repeated raster textures, at the cost of careful clipping/culling and seam management. Keep motif definitions centralized so wall/Ground previews match runtime drawing. +- Stage-wide Undo/Redo deserves one command history across placement, deletion, style, repair, and group commits. The Phase 19 Keep/Cancel preview is a local transaction only. Whole-set transform needs its own design because Ground physics, character/Prop placement, rules, and camera all have different transform invariants. diff --git a/docs/specs/phase_19_scenery_backgrounds_ui_ux.md b/docs/specs/phase_19_scenery_backgrounds_ui_ux.md new file mode 100644 index 0000000..f04d0ac --- /dev/null +++ b/docs/specs/phase_19_scenery_backgrounds_ui_ux.md @@ -0,0 +1,277 @@ +# Phase 19 — Scenery, backgrounds, and surface finishes: UI/UX specification + +> **Status:** Approved and implemented, including the Stage-relative asset-sizing addition (2026-10-16). Based on the [accepted product brief](../plans/phase_19_scenery_backgrounds_surface_finishes.md). Full Phase 19 user/editor verification remains pending. +> +> **Decision:** Use a responsive Scenery chooser followed by canvas tools. Placement stays active for repeated copies. Right-click exits placement. Scenery artwork keeps live `.prop` references, with a visible, repairable placeholder if a referenced file cannot be found. The Phase 19 addition standardizes new artwork around a standing-stickman size guide and adds single-placement Scenery resizing; direct single-piece transforms commit on release, while the existing group preview keeps its Keep/Cancel boundary. Phase 19 does not add Undo; Stage Undo is deferred to its own future phase and should be recorded in tech debt during architecture planning. + +> **Reuse gate for every new dialog or control:** Before building it, inspect the existing shared UI, Stage panel kit, Stage modal/capture controls, and `SandboxTheme` tokens. Use a matching component directly; extend or extract shared behavior when Phase 19 uses it in more than one place. Keep only content and interaction that are genuinely unique to this feature local. Do not add a parallel card grid, overlap popup, capture bar, dialog skin, swatch style, or selected-object inspector. The later architecture specification must name any justified exception before implementation. + +## Child promise and visual language + +The Stage teaches three roles through entry points and short, visible words: **Scenery stays put**, **Props can be part of the action**, and **Ground is where stickmen walk**. An identical drawing can be placed through Scenery or Props without implying the copies behave alike. Sky is chosen through its card, never by selecting an enormous rectangle in the Stage. + +Keep the existing mode language: EDIT uses cyan/blue and a visible grid, DIRECT uses amber/gold and its camera frame, and PLAY uses green with clean artwork. Each active tool also has an icon and words; color alone never identifies it. All buttons, cards, chips, handles' pointer hit regions, and dialog actions have at least a 44 × 44 logical-pixel target. Control heights come from `SandboxTheme` tokens rather than local constants. + +## Flow at a glance + +```mermaid +flowchart LR + New[File → New] --> Gate[Protect unsaved scene] + Gate --> Starter[Choose Blank, Town Street, or Living Room] + Starter --> Edit[EDIT Stage] + Edit --> Scenery[Scenery chooser] + Scenery --> Sky[Choose Sky: changes immediately] + Scenery --> Wall[Choose wall look → draw panels] + Scenery --> Art[Filter artwork → choose drawing → place copies] + Scenery --> Dress[Edit Scenery → select and change pieces] + Dress --> Several[Select several → move or resize scenery together] + Several --> Review[Keep changes or Cancel preview] + Review --> Dress + Edit --> Ground[Ground → choose look → paint walkable pieces] + Wall --> Edit + Art --> Edit + Dress --> Edit + Ground --> Edit +``` + +The chooser is for discovery and choice. Once an artwork or wall tool is chosen, it closes so the Stage is visible. A small capture bar below the top toolbar and mode pill carries the current task, **Done**, and **✕ Cancel**. The cursor pill and ghost carry the immediate instruction at the pointer. + +## Concrete screen examples + +Desktop, while placing a Tree: + +```text +┌ File ─────────────── ✏️ Edit | 🎬 Direct | ▶️ Play ─── Grid Snap ┐ +│ [Ground] │ +│ [Ramp] ✏️ EDIT │ +│ [Step] ┌──────────────────────────────────────────────┐ │ +│ [Props] │ 🌳 Tree stays put [Far] [● Near] [Floor] │ │ +│ [People] │ Place another tree [Done] [✕ Cancel] │ │ +│ [Scenery] └──────────────────────────────────────────────┘ │ +│ [Areas] │ +│ (translucent Tree ghost) │ +│ 🌳 Tree · Near │ +│ ─── ground / existing set ─── │ +└────────────────────────────────────────────────────────────────┘ +``` + +The Scenery chooser, opened from the left palette: + +```text +┌ Scenery: stays put ─────────────────────────────── [✕ Close] ┐ +│ [Backgrounds] [● Artwork] [Edit placed scenery] │ +│ [All artwork] [Nature] [Buildings] [Walls] │ +│ [Roads & paths] [Objects] [Unsorted, when needed] │ +│ │ +│ [ Tree preview ] [ Storefront preview ] │ +│ [ Tree ] [ Storefront ] │ +│ [ Skyline preview ] [ Rug preview ] │ +│ [ Skyline ] [ Rug ] │ +│ │ +│ [Make artwork…] │ +└──────────────────────────────────────────────────────────────┘ +``` + +Edit Scenery, with several pieces selected and a group resize preview: + +```text +┌ File ─────────────── ✏️ Edit | 🎬 Direct | ▶️ Play ─── Grid Snap ┐ +│ [Scenery] ✏️ EDIT [Edit Scenery · 4 pieces] [Done editing] │ +│ ┌──────────────────────────────────────────┐ │ +│ │ ◇ fixed corner old dashed bounds │ │ +│ │ [wall] [window] [clock] [rug] │ │ +│ │ new bright group outline ◉ │ +│ └──────────────────────────────────────────┘ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Resize 4 scenery pieces · Ground and Props stay put │ │ +│ │ [Smaller] [Bigger] [Keep changes] [✕ Cancel] │ │ +│ └────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +These are relationship examples, not fixed pixel geometry. On a narrow viewport the chooser becomes a bounded, scrollable sheet with one card column if needed; filter chips wrap. It never opens wider or taller than the unobstructed screen. The top mode pill remains visible, and the chooser always has a reachable Close button. The capture bar wraps its text and stacks actions only when needed; it never hides the mode pill or the Stage point being placed. + +## Control layout and navigation + +### Stage palette and Scenery chooser + +- Add one illustrated **Scenery** card to the existing vertically scrollable left palette. Keep **Props** and **Ground** as separate cards, with a short helper line in their chooser/tool: “Stays put”, “Can be part of the action”, or “Stickmen walk here.” The palette card opens the Scenery chooser rather than directly arming a tool. +- The chooser has a fixed title/Close row; **Backgrounds** and **Artwork** tabs; a scrollable body; and a fixed lower **Make artwork…** action when relevant. It is a chooser workflow, not a selected-object inspector. It receives `SandboxTheme.control_theme()` and its own popup tokens, while borrowing the Stage panel system's font, surface, selected, hover, focus, and target-size rules. +- **Backgrounds** shows two large category choices, **Sky** and **Wall panel**. **Artwork** shows a single-select filter row with **All artwork**, **Nature**, **Buildings**, **Walls**, **Roads & paths**, **Objects**, and conditional **Unsorted**. One drawing may carry several tags, so it appears under every matching filter. All artwork includes old, untagged drawings. Filters describe appearance, not behavior. Apply the same filters in the Props browser while keeping its title and placement role distinct. +- Use a `FlowContainer` for wrapping filter buttons and an adaptive card grid for previews. Each card has a large illustration, a readable name, and a visible keyboard focus ring. No card depends on hover to reveal its action. Changing tabs or tags preserves the chooser and moves focus to the first relevant control; closing returns focus to the Scenery palette card. +- A chooser Close or Esc dismisses only the chooser. Clicking outside may dismiss it when safe, but never chooses an asset or changes a Sky. A blank result shows **“No artwork here yet”** and offers **All artwork** and **Make artwork…** rather than an empty panel. + +### File → New starter chooser + +- Run the existing unsaved-work Save/Discard/Cancel gate before offering a starter. Do not replace the open scene until a starter card is chosen. If the starter chooser is canceled, the current scene remains open; its dirty state reflects whether the child saved it at the gate. +- The themed modal uses three large illustrated cards: **Blank Scene**, **Town Street**, and **Living Room**, plus **✕ Cancel**. Each card pairs a preview with one plain sentence: “Start from scratch”, “Shops, trees, sidewalk, and road”, or “A room with a window, rug, and furniture.” The cards wrap to fewer columns on narrower screens and remain scrollable. No tiny menu inside a card. +- Choosing a card creates a new unsaved scene with its suggested name in an editable name field. The name may be changed there or through the existing Rename Scene command. A small, dismissible on-Stage cue can say **“Try Scenery to change this place”** after Town Street or Living Room opens. It must not cover the Stage or block input. +- Blank starts with the existing neutral view and no Ground; the other two show editable ordinary pieces described in the accepted brief. Their cards must not promise a functional door, sitting, or automatic window cutting. + +## Backgrounds and placement + +### Sky + +- **Backgrounds → Sky** opens four designed thumbnail cards: **Clear Day**, **Cloudy Day**, **Sunset**, **Starry Night**. The currently selected card has a strong border, filled selected state, and the text **“Current Sky”**; keyboard focus remains a separate outline. +- One tap or Enter/Space changes the Sky immediately; the chooser stays open so another card can be tried. Close means “stop choosing”, not “revert”. With no Undo in Phase 19, the child can restore the previous look by choosing its card again. Changing Sky never recolors or tints other pieces. Sky has no Stage selection handles or overlap entry. +- The thumbnail shows enough clouds, sun/moon, and color variation to predict the Stage result. A chosen Sky is static during PLAY and stable through camera movement. + +### Wall panel + +- **Backgrounds → Wall panel** offers picture choices **Paint**, **Brick**, and **Wood**, plus a large **Color** swatch/button. Common colors appear first; **More colors…** opens a themed color dialog with a visible Cancel. Finish and color choices are reflected in a small preview and then in the translucent Stage draft. +- Choosing the look closes the chooser and arms **Draw wall panel**. The capture bar says **“Drag across the space for a wall panel”**, with **Done** and **✕ Cancel**. The pointer has a corner reticle and a nearby pill **“Drag to draw wall”**. A press sets the first corner; dragging draws a world-anchored translucent patterned rectangle; release commits one finite panel. The pattern preview remains at sensible scale rather than stretching with the draft. +- Snap, when enabled, visibly aligns the draft to nearby grid lines. An unusably small draft shows a red outline and **“Make the wall bigger”**; release leaves no panel. A valid draft uses the EDIT accent plus a neutral outline and dimension cue. This is appearance only: a helper in the chooser says **“Looks like a wall. Stickmen can pass through.”** +- Touch can drag or tap the first and second corners. The first tap leaves a visible anchored corner and continuous rectangle preview; the second tap commits. The same visible Cancel and Done remain available. Right-click cancels a draft and exits the wall tool under the exit rules below. + +### Artwork and depth + +- Choosing an artwork card closes the chooser, starts a translucent ghost under the pointer, and shows a capture bar with the drawing's name, **Far**, **Near**, **On the floor**, **Done**, and **✕ Cancel**. **Near** is the default for a newly chosen drawing. Each depth choice has a simple layer illustration plus text; selected, hover, and focus states differ without relying on color. Changing depth updates the ghost's visible placement layer before commit. +- The cursor pill says **“Click to place [name]”**; on touch it becomes a small anchored Stage hint after the asset is chosen. Tap/click commits one copy. The ghost remains ready for another; after a commit the bar says **“Place another [name]”** and shows the count of copies placed in this tool session. Copies already committed remain when the tool closes. A repeat placement starts as another distinct piece. +- The selected artwork's source card may carry tag badges in the chooser, but tag words never suggest collision or interaction. The Stage preview says **“Stays put”**. Its depth affects appearance only; it does not grant blocking, pickup, support, Use, or event behavior. + +### Opening a wall panel for a window or doorway + +- Wall-area erasing is easy to discover from either entry point: a selected wall panel's shared Scenery inspector includes a touch-sized **Erase wall area** action, and the Edit Scenery capture bar shows **Erase wall area** whenever nothing is selected. Either starts a dedicated tool with a translucent red, crossed rectangle preview and a visible capture bar: **“Drag over the wall · release to erase”**, **Done**, and **✕ Cancel**. Snap-to-grid, when enabled, applies to both drag corners. +- Dragging a rectangle through one or more wall panels removes only the covered area. Each intersected panel is split into up to four remaining rectangles that retain its look and color. Brick/wood motifs keep a shared anchor, so the surrounding pattern does not restart at every cut edge. Existing artwork and other scenery are untouched; this is a visual opening, not a collision or doorway mechanic. +- Each valid release applies that opening immediately. **Done**, **✕ Cancel**, right-click, or Esc exits the tool; canceling an in-progress drag discards only that draft, while previously completed openings remain. Phase 19 has no Undo, so keep the capture copy clear about this commit-on-release behavior. + +### Placement exit, cancel, and mode changes + +| State | **Done** | **✕ Cancel** | Right-click / Esc | +| --- | --- | --- | --- | +| Chooser open, no tool | Close chooser | Close chooser | Close chooser | +| Tool armed, only passive artwork ghost or wall corner cursor, no placement yet | Exit without placing | Exit without placing | Exit without placing | +| Active uncommitted wall draw or resize | Discard the draft, then exit the tool | Discard the draft, then exit the tool | Discard the draft, then exit the tool | +| One or more pieces committed, no active gesture | Keep pieces and exit | Keep pieces; discard only the next passive ghost and exit | **Done**: keep pieces and exit | +| One or more pieces committed, another gesture active | Keep prior pieces; discard current draft and exit | Keep prior pieces; discard current draft and exit | **Cancel current draft**, keep prior pieces, then exit | + +A passive artwork ghost does not count as an active gesture; a pressed wall corner, two-tap first corner, or resize drag does. Releasing a mouse button after cancellation cannot commit the old draft. The capture bar keeps a short line **“Placed pieces stay”** once any copy has committed, so Cancel does not falsely imply that all prior placements are erased. Right-click is an optional pointer shortcut; Done and Cancel stay visible for touch and keyboard users. Switching to DIRECT/PLAY or opening another Stage tool discards an active draft, preserves completed pieces, and removes all ghosts, handles, and capture prompts. No new Undo control or shortcut is introduced here. + +## Edit Scenery and selected-object cards + +- The Scenery chooser has a large **Edit placed scenery** action. It closes the chooser and enters a clearly labeled **“Edit Scenery”** state inside EDIT, with an on-Stage pill and prominent **Done editing** button. The global mode remains EDIT and its mode pill remains visible. Done editing, Esc when no edit gesture is active, or switching modes returns to ordinary selection. Esc during a resize or move first cancels that draft and leaves Edit Scenery active. The child can reopen Scenery to place more pieces. +- Ordinary EDIT click/box selection favors characters, Props, Ground, and existing interactive objects; broad wall panels, Far/Near artwork, and floor art do not enter those results. In Edit Scenery, only wall panels and Scenery artwork enter click/box selection. The selected piece gets a bright outline backed by a dark halo, a text name/depth label, and large move/resize handles with at least 44 × 44 logical-pixel hit regions at common zoom. A single-piece drag previews live and commits immediately on release; uniform resize keeps the opposite corner visibly pinned, and the transform affects only that placement. It has no extra Keep confirmation. While the pointer/finger is held, a compact capture bar offers **✕ Cancel** for touch users; Escape, right-click, or leaving Edit Scenery also cancels the active gesture and restores the pre-drag position/size. Releasing the gesture commits without waiting for another choice. A later Stage Undo action is deferred to its own phase and tracked in technical debt. The selected-artwork card also offers large **Smaller** and **Bigger** actions for users who prefer buttons; each applies one immediate, proportion-preserving size step to that placement only. Existing placement sizes are not automatically changed when this feature is added. +- When pieces overlap, a small cutout under the pointer takes priority over a broad panel. Clicking a panel's exposed area or edge selects it. If several pieces still match, show a themed **“Pieces here”** chooser with one large labeled row per candidate and a visible Close. A selected panel also remains reachable through that chooser when its visible area is covered. Box selection in Edit Scenery includes only Scenery pieces; ordinary box selection does not gather broad scenery. +- A selected Scenery artwork card uses `StageInspectorPanel` and `StagePanelChoiceGroup`. The header has the drawing name, icon, and Close. The body offers an illustrated **Where it appears** group with **Far**, **Near**, and **On the floor**, large **Smaller** / **Bigger** size steps, and **Erase scenery**. A selected wall card uses the same shell, with **Wall panel** header, **Look**, **Color**, and **Erase wall panel**. Each change applies only to that selected piece. These cards have a fixed Close, scrollable body, and the established compact **Settings** reopen button while the same piece remains selected. Closing the card does not exit Edit Scenery. +- An Erase action for a wall or artwork asks **“Erase this piece?”** with **Keep it** and **Erase** buttons before removing it, because Phase 19 has no Undo. The Delete key follows the same safeguard for selected Scenery. +- Sky is never listed in **Pieces here**, never box selected, and never shown in the selected-piece card. Its only edit path is **Backgrounds → Sky**. + +### Artwork file missing after reopen + +- A missing live `.prop` reference leaves a **real, saved Scenery placement** at its authored position and Far/Near/On the floor depth. On the Stage, draw a soft neutral translucent footprint with a dashed light-and-dark outline when the saved extent is known; otherwise draw a compact world-anchored marker at the saved point. A constant-screen-size picture/question icon and label say **“Artwork not found”**, followed by a short remembered drawing name or file name when available. Do not use a large red error block, expose a full file path on the canvas, or show an empty gap that looks like the child erased the art. Keep the marker legible on all four Skies and on common Ground/wall looks. In DIRECT and PLAY, the placement remains a subdued neutral marker at the same layer, without edit handles or repair controls, so the missing set piece is still evident without covering the action. +- On scene open, reuse the existing missing-assets summary dialog with calmer copy: **“Some artwork could not be found. Your scenery spots are still here.”** List the friendly names and offer **“Show scenery”** and **“Later”**. Show scenery enters Edit Scenery and focuses the first missing placement; Later closes the dialog and leaves every marker visible. This summary appears once for the load, not on every frame or selection. It does not replace the existing stickman/Prop warning semantics; the missing-Scenery wording is scoped to Scenery. +- In ordinary EDIT, the footprint and label are visible but the missing placement's broad bounds do **not** enter normal click or box selection. Its small, explicit **Find artwork** badge has at least a 44 × 44 hit target and can switch to Edit Scenery with that placement selected; the badge alone takes input, so a child can still select a character, Ground, or Prop in front of the marker. The Stage palette's **Edit placed scenery** path and a **Needs artwork** section in the Scenery piece list provide alternatives when the badge is covered or offscreen. +- In Edit Scenery, the missing placement participates in Scenery selection and **Pieces here** with a distinct picture/question icon and **“Artwork not found · [name]”** row. Selecting it shows the normal `StageInspectorPanel` shell headed **“Artwork not found”**, with the remembered name, its saved depth, and large actions **Find artwork…**, **Choose another artwork**, **Keep for now**, and **Erase scenery**. An optional **File details** disclosure may show the saved path for a helping adult; the path is not the primary instruction. The neutral marker remains visible when the card is closed, and the compact Settings reopen control works as for other Scenery. If the original size is unavailable, do not show misleading resize handles; explain **“Find artwork to change its size.”** Moving the marker and changing its depth remain possible without making it solid or interactive. It can join Select several for group moves; if any selected member lacks a known footprint, disable Resize together with the same plain-language reason until that artwork is repaired. +- **Find artwork…** opens the established themed `.prop` file chooser so the child can point to a moved file. **Choose another artwork** opens the same responsive Artwork gallery used for placement, explicitly labeled as a replacement choice. After either choice, show a ghost of the candidate at the saved position, size/scale, orientation, and depth where those saved values exist, and ask **“Use this artwork here?”** with **Use artwork** and **✕ Cancel**. The replacement can be adjusted afterward; do not silently alter Ground, Props, or other Scenery. Canceling the chooser, file dialog, or preview leaves the missing marker and saved reference untouched. An unsuccessful file load returns to the same marker/card with a plain **“Could not open that artwork. Try another file.”** message. Erase uses the shared **“Erase this piece?”** confirmation; only confirmed Erase removes the placement. If several places reference the same missing file, default to repairing the selected place only; an explicit **Use for all matching places** choice may be offered, with the affected count shown before confirmation. +- The placeholder, badge, piece-list row, preview actions, and dialog work by touch and keyboard: focus is visible and distinct from the missing state; Tab reaches the badge/list, actions are at least 44 × 44, Enter/Space activates them, Esc closes a chooser or cancels an uncommitted preview, and focus returns to the selected placeholder/card. On a narrow viewport, the label truncates gracefully to the remembered name, the inspector body scrolls below a fixed header/Close, and its actions wrap or stack without hiding the mode pill. A missing marker is identified by icon and words as well as outline, never by color alone. + +### Move or resize several Scenery pieces + +- **Select several** is a large, explicit action within Edit Scenery. While it is active, tapping pieces adds/removes them without beginning a move. A touch-friendly, scrollable piece list offers each piece's thumbnail/name/depth and a large selection row; it provides **Select all scenery** and **Clear selection**. Box selection may add Scenery pieces under the same filter. **Select all scenery** means every wall panel plus every Far, Near, and On the floor Scenery artwork placement in the open scene. Sky, Ground, characters, and interactive Props are never included. There is no automatic starter-wide group or persistent lock between pieces. +- With two or more pieces selected, show a halo-backed union outline, a separate outline on each member, and the text **“N scenery pieces selected”**. The selected-object card uses the shared `StageInspectorPanel` shell with **Move together**, **Resize together**, **Clear selection**, and a short explanation **“Ground and Props stay put.”** It does not show a single shared depth, finish, or color control, because mixed members may have different settings. The group outline and count appear on the canvas, not only in the card. Closing the card follows the normal compact Settings reopen behavior and leaves the selection visible. +- **Move together** starts a live preview. Dragging the group or using large on-screen arrow buttons applies the same world-space offset to every selected Scenery piece, preserving their relative spacing and Far/Near/On the floor choices. The original outlines remain faint/dashed while the candidate positions have bright outlines. Snap, if enabled, moves the selection as a unit from its anchor without rounding members independently. A cursor pill says **“Move N scenery pieces”**. +- **Resize together** starts a live uniform-size preview. Drag a group corner inward/outward; the opposite corner shows a fixed pin and does not move. Every selected artwork piece changes size and position by the same positive scale around that anchor, retaining its proportions and relative layout. Wall panel rectangles change size and position with the group, while Brick/Wood motifs retain their world-scale size and alignment instead of stretching. Far/Near/On the floor choices remain unchanged. Clamp or reject a preview that would collapse or flip any piece; show **“Too small”** with a blocked outline. The cursor pill says **“Resize N scenery pieces”** and identifies the fixed corner with a visible pin. +- During either group preview, the on-canvas capture bar shows **Keep changes** and **✕ Cancel**, the selection count, and **“Ground and Props stay put.”** Releasing a pointer leaves the preview pending; it does not save a change or newly mark the scene dirty. Keep changes commits the whole preview as one edit and marks the scene dirty once. Cancel, right-click, or Esc restores the exact pre-preview positions and sizes and returns to the same multi-selection in Edit Scenery. A later mouse/touch release cannot commit a canceled preview. **Done editing** or switching modes while a preview is pending first cancels it, then exits Edit Scenery. A Save request while a preview is pending asks the child to Keep changes or Cancel before saving; it never silently saves the draft. +- The group tools are operable without precise dragging: touch targets and corner hit areas are at least 44 × 44 logical pixels; large move arrows and **Smaller/Bigger** buttons give touch and keyboard users repeatable steps. Pointer users may drag any corner and see its opposite fixed pin; the button path starts with the top-left corner fixed and labels that choice. Every control is focusable, has a visible focus ring and text label, and responds to Enter/Space. On a narrow viewport, the multi-selection card scrolls beneath its fixed Close, the capture bar stacks its actions, and arrow/size buttons remain reachable even when a group corner lies offscreen. The mode pill and Stage preview remain unobstructed. +- A committed group transform saves each member's new position and size through the normal scene save; depth, finish, physics, and interaction roles do not change. The temporary multi-selection, preview, and group outline do not persist on reopen. A whole-set transform that also moves/scales Sky, Ground, characters, or interactive Props is outside Phase 19 and belongs to a separately designed future feature. + +Single-artwork direct resizing and multi-piece group resizing intentionally have different commit gestures: one artwork placement commits on handle release, while a group retains the existing snapshot preview with **Keep changes** / **✕ Cancel**. Do not add a single-piece confirmation bar as a substitute for deferred Stage Undo. + +### Stage-relative authoring size in Prop Editor + +- In Arrange, replace the fixed-target **Normalize Size** action with **Size for Stage**. It opens a reversible sizing view with **Small**, **Room**, and **Big** starting sizes and fine **Make smaller** / **Make bigger** adjustments. The presets are rough visual starting points relative to the standard standing stickman, not rigid limits; preserve the asset's proportions and let the child inspect the actual comparison before keeping it. Exact preset ratios are an architecture/implementation detail, not new UI copy. +- Frame the complete artwork and guide together when the sizing view opens. Show a standing stickman at the standard 1:1 reference scale **in front of the artwork**, plus a clear floor/contact line near ankle level. Use a dark halo with a light core so the guide remains visible over any fill. The guide is editor-only and never becomes part of the saved artwork. The guide is visible whenever **Show Stickman Guide** is on and is forced on while **Size for Stage** is active. +- **Keep size** applies the preview to the authored shapes and returns to Arrange; **✕ Cancel** restores the exact pre-preview shape geometry. Existing whole-prop Transform behavior remains the way to move/rotate/resize all shapes and editable anchors together. Keep this sizing action within the established Prop Editor Arrange toolbar and `PropTheme`; do not reuse the Stage inspector card for a distinct editor workflow. +- Artist-created and newly authored vector artwork should already be drawn at its intended Stage size, so first placement is normally scale 1 relative to the authored artwork. The standard figure is about 940 world units tall. Prop names should guide an ordinary-object size estimate; the asset review records approximate intended bounds relative to the stickman (height, and width when useful) and includes a scale-check preview. The sizing view is a correction/authoring aid, not a required step for every asset. +- Loading existing `.prop` files and scenes performs no automatic normalization and retains saved placement transforms. **Keep size** deliberately changes the authored geometry; saving over the same `.prop` uses the existing live-reference behavior, so scenes using that drawing show its revised artwork on reopen. Keep the current Save and Save As workflow; this addition introduces no extra copy/version prompt or overwrite confirmation. + +## Ground finish workflow + +- The existing **Ground** palette entry gains a **New Ground look** chooser with large picture swatches: **Paint**, **Brick**, **Wood**, **Grass**, **Dirt**, **Sidewalk**, **Asphalt**. Paint, Brick, and Wood use the same names and comparable preview motif as wall finishes. A chosen swatch becomes the active look for future Ground only. The chooser states **“New Ground will look like this”** and never silently repaints existing pieces. +- Returning to the Stage shows a patterned Ground ghost for one tap or the existing drag-paint run. A small cursor pill says **“Click or drag to add Ground”** and the capture bar provides Done and Cancel under the exit rules above. Valid/blocked stroke previews use outline, symbols, and short words as well as color. Adjacent ghost pieces show a continuous-looking pattern. Ramp and Step keep their existing appearance and palette behavior. +- Selecting an existing Ground piece in ordinary EDIT opens a Ground `StageInspectorPanel` card with **This Ground's look**, the same finish swatches in a `StagePanelChoiceGroup`, and any existing Ground controls. Selecting a finish here changes only the selected piece. The different headings **New Ground look** and **This Ground's look** make scope visible before a tap. +- In PLAY, Ground keeps its walkable behavior and chosen look. Wall panels, road markings, and rugs never look like a selectable walking surface in the UI. + +## Prop Editor route for Scenery artwork + +- **Make artwork…** in the Scenery chooser uses the existing vector `.prop` authoring tool; there is no separate Scenery Editor or file type. When entered from Scenery, its first screen is framed as **“Make artwork for your scene”** and foregrounds vector drawing, color, shape arrangement, naming, and Save. Hide prop-only material, mass, bounce, collision, Use/attachment, and other action controls from this entry route. Do not require a pretend solid shape to save visual-only artwork. +- The Props entry route retains prop-relevant controls and its distinct **“Make a Prop”** framing. If a drawing is valid for both roles, the child may find it in both browsers; the Stage placement entry point decides what the copy does. After a successful artwork save, offer **“Place it as Scenery”** and return to the Artwork chooser focused on the saved drawing. Cancel/exit returns without changing the open Stage. +- Tag editing uses the same large multi-select choices **Nature**, **Buildings**, **Walls**, **Roads & paths**, and **Objects** in artwork authoring. The words are descriptive, optional, and never gate the All artwork view. An untagged saved asset remains discoverable under All artwork and conditional Unsorted. + +The **Size for Stage** workflow above applies to both Scenery artwork and Props because they share the same `.prop` authoring canvas. It does not change the Stage role distinction: a Scenery placement remains inert and non-colliding, while a Prop retains its existing interaction and physics settings. + +## Theme, components, and accessibility contract + +- For selected wall, artwork, and Ground cards, reuse `StageInspectorPanel` for the fixed header, safe rect, body scroll, Close, focus, and reopened Settings button pattern. Reuse `StagePanelChoiceGroup` for exclusive depth/finish choices with stable labels and decorative preview cues. `StagePanelToggleRow` and `StagePanelSwitchVisual` remain available for true on/off settings but are not forced into this feature's depth or finish choices. +- The multi-selection card also reuses `StageInspectorPanel` and `stage_panels.action` for its large Move/Resize/Clear actions; its member list and preview controls add tokens under `stage_panels` rather than hardcoded colors or sizes. The group capture bar extends the same transient Stage capture pattern used for placement, keeping its instruction and Cancel together below the mode pill. +- Extend the `stage_panels` `SandboxTheme` tokens only where these cards need new preview-cue size, card spacing, or scenery-specific accent/selection outlines. Continue using `stage_panels.shell`, `.type`, `.choice`, and `.action` for geometry and typography, and `mode_colors.edit` for the EDIT accent. The Scenery chooser, starter modal, color dialog, and Ground chooser use popup/theme tokens and `SandboxTheme.control_theme()`; their layout is distinct from an in-stage inspector. Generalize the existing Stage capture-bar look for wall/artwork/Ground placement instead of copying fixed styling. +- On narrow or short viewports, chooser contents scroll, filter chips wrap, card columns collapse, the inspector header/Close remain fixed, and the capture bar wraps/stacks within safe margins. Never let a panel cover the EDIT/DIRECT/PLAY mode pill. Decorative thumbnails and cue drawings ignore pointer input; each actual choice is one native focusable Button with Space/Enter activation. Focus moves into an opened chooser/card and returns to the opener on Close when possible. +- Pointer, touch, and keyboard all reveal the selected state, hover where applicable, focus, disabled reasons, and confirmation. No instructional step exists only in a top header, bottom status bar, hover tooltip, or shortcut. Cursor pills remain near the pointer without covering it; they flip to stay in view. Touch users get the same instruction anchored near the draft plus always-visible Done/Cancel. + +### Reuse and standardization audit + +This matrix identifies current UI that already exists. “Extend/share” is a design requirement to converge repeated controls; it does not prescribe a class, signal, or file contract for the architect. + +| Phase 19 surface | Existing source to use | Reuse decision and boundary | +| --- | --- | --- | +| **File → New** and unsaved-work gate | `StageChrome` File menu; `StageModalViews` unsaved `ConfirmationDialog`; Stage document flow | **Direct reuse.** Preserve Save/Discard/Cancel, focus, and the current scene until a starter is chosen. Do not make a second unsaved-work dialog. | +| Illustrated starter chooser | `AssetCard`, `AssetSelector` scene/shell, `StageModalViews` dialog theming and Rename Scene name entry | **Extend/share the card-gallery presentation.** The three starter cards need a responsive grid, scroll, Close/Cancel, and selected focus behavior also needed by Scenery/Sky/finishes. The editable suggested name follows existing name-entry styling/validation; starter composition remains feature-local. This is a modal choice, not `StageInspectorPanel`. | +| Scenery chooser and Artwork cards | `AssetSelector`, `AssetCard`, `StageAssetBrowser`, thumbnail cache/renderers, `scenes/stage/asset_selector.tscn` | **Extend/share the gallery shell and card behavior, reuse browsing/thumbnail work.** The current selector is a fixed four-column paged popup and sets card focus to `FOCUS_NONE`; these must be corrected for responsive layout and keyboard use in the common path rather than copied into a Scenery-only popup. Backgrounds/Artwork tabs use native button choice states and shared popup styling; placement-role text stays feature-local. | +| Sky and wall/Ground finish cards | `AssetCard` and the responsive gallery behavior above; `SandboxTheme` popup/card tokens | **Share the same preview-card and focus/selected-state rules.** Sky and finishes supply their own preview imagery and one-tap selection meaning; card dragging is disabled for these choices. They do not pretend to be `.prop` assets or reuse `StageAssetBrowser` asset scanning. Wall and Ground share names/preview motifs for Paint, Brick, and Wood. | +| Artwork tags, Props filters, authoring tags | No existing reusable tag-chip control; `StagePanelChoiceGroup` is a vertical exclusive inspector control; Stage speech UI demonstrates `HFlowContainer` wrapping | **Extract one shared wrapping filter/choice treatment.** Use native focusable buttons and one theme contract across Scenery browsing, Props browsing, and optional multi-tag authoring. Single-choice browser filters and multi-choice authoring use the same visual states but different selection rules. Do not clone local chip styling or force the vertical inspector group into a wrapping browser row. | +| Wall Color and More colors dialog | `ColorSwatchSelector`; native `ColorPickerButton` already used by the Prop Editor; `StageModalViews` dialog theme | **Extend/share the color-swatch treatment** so it wraps on narrow screens, has visible chosen/focus states, and uses Stage tokens. Use native color picker behavior in a themed Stage dialog. The Prop Editor's `PropTheme` styling is not a Stage style source; wall/color values remain feature-local. | +| Scenery overlap **Pieces here** | `StageModalViews.show_objects_here()` and its themed, viewport-clamped, scrollable `PopupPanel` | **Generalize the existing overlap chooser.** Supply Scenery-only candidates and child-facing piece names/depth, while retaining the same placement, 44 px rows, dismiss, and focus behavior. Do not create a second overlapping-items popup. | +| Missing Scenery artwork | Existing `MissingPropPlaceholder`/`MissingStickmanPlaceholder` preserve missing placements; `StageModalViews` missing-assets dialog; `StageInspectorPanel`; `StageAssetBrowser` gallery/file chooser | **Reuse and generalize the recovery path.** Keep saved placement continuity and one themed load summary, but give Scenery its neutral labeled marker, selection-card repair actions, and the existing artwork/file choosers. Do not copy the older red marker styling or create a second asset gallery or dialog skin. | +| **Erase this piece?** | `StageModalViews` confirmation/dialog theming; existing Erase Use spot `ConfirmationDialog` and safe/danger treatment in `SandboxStage` | **Reuse the confirmation pattern.** Share the button sizing, themed window, and danger/safe styles instead of copying the spot-erase styling into another private function. The affected piece and wording are local. | +| Placement and group-preview bar | `StageUseTargetCaptureBar` and the transient capture-bar contract in `stage_panel_ui_system.md` | **Generalize/extract the capture-bar shell** for EDIT prompts, mode accent, Done/Keep changes/Cancel, responsive stacking, and a safe position below the mode pill. Keep Use-target-specific wording/actions in its caller; do not build a second bar with copied colors/geometry. | +| Cursor pill and Stage ghosts | `StageChrome` action tooltip/cursor, current placement ghost/terrain overlay, `SandboxTheme.overlays` | **Extend/share presentation and safe cursor positioning** while the wall/artwork/Ground draft visuals remain feature-local. Keep one visual language for valid/blocked, mouse/touch feedback, and cancellation; do not copy the current tooltip's hardcoded colors into Phase 19. | +| Selected artwork, wall, Ground, and multi-selection card | `StageInspectorPanel`, `StagePanelChoiceGroup`, `StagePropSettingsView` dismissal/reopen behavior, `stage_panels` tokens | **Direct reuse of inspector shell and exclusive choice group; extend shared action styling only as needed.** These are in-stage selected-object cards. Their data, headings, member list, and commands stay feature-local. `StagePanelToggleRow`/switch are reserved for real on/off values and are not substitutes for depth or finish choices. | +| **Erase wall area** tool | Selected wall's `StageInspectorPanel` or the no-selection Edit Scenery `StageUseTargetCaptureBar`; wall ghost/placement logic in `StageSceneryController` | **Reuse the selected-object action styling and existing transient capture bar.** Only the cutout rectangle preview and rectangle subtraction are feature-specific; do not add another toolbar or dialog. | +| Select-several member list | `StageInspectorPanel` body/action styling; `StageModalViews` large selectable-row precedent | **Reuse row sizing, focus, selected, and scroll rules.** Membership logic and piece labels remain local to Edit Scenery; do not invent another inspector shell or tiny checkbox list. | +| **Make artwork…** and return | Existing Prop Editor scene/controller and `.prop` authoring; `StageAssetBrowser` refresh/thumbnail path | **Reuse the existing editor and asset browser.** The Scenery entry changes the visible authoring framing and hides prop-only controls; it does not create another editor, file picker, art card system, or `.scenery` format. The return-to-Stage handoff is feature-local. | +| Stage-relative size in Prop Editor | Existing Prop Editor Arrange toolbar, whole-prop transform, Normalize Size path, stickman guide, and `PropTheme` | **Extend the existing authoring workflow.** Replace the fixed 96-unit normalization target with the reversible Size for Stage preview and front-drawn guide; keep the authoring figure/editor chrome local to Prop Editor. Do not adopt `StageInspectorPanel` for this editor-only interaction or change existing saved assets automatically. | + +For every row, new dialogs/popups explicitly receive `SandboxTheme.control_theme()` and the established Stage dialog style, with their visual values read from `SandboxTheme`/`sandbox_theme.json` (`popups`, `palette`, `stage_panels`, `chrome.dialogs`, `mode_colors`, and `overlays` as appropriate). The existing `StageModalViews.apply_dialog_theme()` handles font/child overrides but is not, by itself, a substitute for assigning the control theme to a new window. Reuse local stylebox instances rather than mutating the cached shared `Theme`. Keep popup gallery behavior separate from the in-stage inspector layout while making their type, spacing, focus, and selected states consistent. + +The existing `QueuePanel`, `RulePanel`, `ActionEditor`, and speech popups in `scenes/editors/stage/` solve different editing tasks. They can inform theme and responsive-container choices, but are not replacement shells for a starter or art gallery. Any new Stage popup should pass the same theme, sizing, focus, and dismissal checks as those existing workflows. + +## Exact child-facing copy map + +| Context | Text | +| --- | --- | +| Palette entry | **Scenery**; helper **Stays put** | +| Props distinction | **Props**; helper **Can be part of the action** | +| Ground distinction | **Ground**; helper **Stickmen walk here** | +| Scenery tabs | **Backgrounds**, **Artwork** | +| Edit action/state | **Edit placed scenery**, **Edit Scenery**, **Done editing** | +| Multi-selection | **Select several**, **Select all scenery**, **Clear selection**, **N scenery pieces selected** | +| Group action/preview | **Move together**, **Resize together**, **Ground and Props stay put**, **Keep changes**, **✕ Cancel** | +| Group adjustment | **Smaller**, **Bigger**, **Too small** | +| Wall category/helper | **Wall panel**; **Looks like a wall. Stickmen can pass through.** | +| Wall cursor/draft | **Drag to draw wall**; **Make the wall bigger** | +| Artwork cursor | **Click to place [name]**; after first copy **Place another [name]**; during direct resize **Resize [name]** | +| Placement actions | **Done**, **✕ Cancel**; after a commit **Placed pieces stay** | +| Ground palette/inspector | **New Ground look**, **This Ground's look** | +| Overlap chooser | **Pieces here** | +| Missing artwork | **Artwork not found**, **Find artwork**, **Needs artwork**, **Show scenery**, **Later** | +| Missing artwork repair | **Find artwork…**, **Choose another artwork**, **Keep for now**, **Use this artwork here?**, **Use artwork**, **File details** | +| Repair failure | **Could not open that artwork. Try another file.** | +| Erase safeguard | **Erase this piece?**, **Keep it**, **Erase** | +| Starter cards | **Blank Scene**, **Town Street**, **Living Room** | +| Stage-relative size | **Size for Stage**, **Small**, **Room**, **Big**, **Make smaller**, **Make bigger**, **Keep size**, **✕ Cancel**, **Show Stickman Guide** | +| Single Scenery resize | **Smaller**, **Bigger**; a handle drag commits on release | +| Wall opening tool | **Erase wall area**, **Drag over the wall · release to erase**; **Erase another area · earlier openings stay** | + +Do not expose “entity”, “collision hull”, “z-index”, “physics layer”, “finish ID”, or “depth tier” as child-facing copy. + +## Observable UI review scenarios + +1. Create Town Street from File → New, cancel at the starter chooser once, then choose it. The old scene persists on cancel; the starter appears as an editable unsaved scene with its sky, Ground, and separate set pieces. +2. Choose Tree under Scenery → Artwork → Nature, change Far/Near/On the floor, place two copies, and right-click. Both copies remain, the passive ghost disappears, and ordinary EDIT selection returns. Repeat with an unfinished wall drag: right-click discards only that draft. +3. In ordinary EDIT, click and box-select a character and Prop in front of a room wall without selecting the wall. Enter Edit Scenery, select a window cutout, then choose the broad wall through its exposed area or Pieces here. Start a direct resize and cancel the still-active gesture before release; its old bounds return. Release a second resize and verify it commits immediately, with no Keep prompt. +4. Change Sky through all four cards while the Living Room window remains visible. The room colors stay unchanged; Close leaves the last chosen Sky, and keyboard focus returns to Scenery. +5. Select Wood for New Ground, paint a run, then select one existing Ground piece and change only that piece to Brick through **This Ground's look**. A selected wall panel's own Finish/Color changes only that panel. +6. At a narrow viewport and with touch/keyboard input, reach every chooser card, depth choice, Cancel, Done, inspector Close, and overlapping-piece choice without clipping or relying on hover. Enter PLAY and confirm all ghosts, handles, and edit-only bars disappear. +7. In Living Room, use Select several to choose two wall panels, the window frame, and rug. Preview a shared move, cancel with right-click, and verify all four return exactly to their original locations while Wood Ground and furniture remain still. Preview a uniform corner resize, check the opposite anchor stays fixed and wall motifs do not stretch, then Keep changes; save and reopen. The four pieces retain their new positions/sizes and original depth/finish, while the temporary group selection is gone. Repeat the preview with touch or keyboard controls in a narrow viewport, using Esc to cancel. +8. Save a scene with two copies of one live `.prop` Scenery drawing, then make the file unavailable and reopen the scene. The friendly missing-art summary appears once; both labeled markers remain at their saved positions/depths through EDIT and PLAY. In ordinary EDIT, select a character in front of a marker without selecting Scenery. Use a marker's Find artwork badge or the Edit Scenery **Needs artwork** list to open its card. Cancel a file choice and verify both markers remain. Choose a replacement for one copy, preview then cancel once, then confirm it; only that copy changes. Confirm the second still has a marker after save/reopen until it too is repaired or deliberately erased. Repeat the repair path with keyboard and on a narrow touch viewport. +9. In Prop Editor, open **Size for Stage** on representative tall, wide, and small artwork. Confirm the standard stickman is drawn in front, the floor/contact guide is visible over every fill, the preview frames both together, presets and fine adjustments preserve proportions, **Keep size** applies, and **✕ Cancel** restores the exact prior geometry. Confirm new placement uses the authored size and loading an existing file performs no automatic normalization. Deliberately save revised geometry over a referenced asset and confirm the usual Save flow and live-reference update on reopen, with scene placement transforms retained. In Edit Scenery, resize one placement and confirm it commits on release and saves/reopens; cancel a still-active drag with Escape/right-click and with the touch capture bar; verify other scenery and Props stay unchanged. Compare this with the separate group Keep/Cancel flow. Repeat sizing-guide visibility and Stage handle/button access with keyboard/touch and at a narrow viewport. diff --git a/docs/specs/phase_19_selected_artwork_depth_inspector.md b/docs/specs/phase_19_selected_artwork_depth_inspector.md new file mode 100644 index 0000000..38d6737 --- /dev/null +++ b/docs/specs/phase_19_selected_artwork_depth_inspector.md @@ -0,0 +1,82 @@ +# Phase 19 addition — selected Scenery artwork depth + +> **Status:** Implemented and approved; focused automated and render-capable checks pass. Full Phase 19 user/editor verification remains open. **Scope:** Edit Scenery's in-stage card for exactly one selected `StageSceneryArtwork`, including a missing-artwork marker. This is an addition to Phase 19. It does not change placement, wall panels, physical Props, or the scene schema. + +## Child-facing behavior + +After placing artwork, a child enters **Edit Scenery** and selects one drawing. The existing card keeps its artwork icon, name, fixed **Close** button, and **Smaller** / **Bigger** controls. Above the size controls, it shows **Where it appears** with three large, full-width choices: + +1. **Far** +2. **Near** +3. **On the floor** + +The selected choice matches that particular drawing's current layer. A short note below the choices says: **“Far is behind Near. On the floor is in front of the ground.”** Pressing another choice updates the Stage immediately and keeps the same card open. It changes only that selected placement; other copies of the same artwork keep their own layers. Repeatedly pressing the current choice has no effect on the document. The child's placement cursor and its Far/Near/On the floor capture-bar choices keep their current behavior. + +A missing-artwork marker has the same depth section, using its saved depth. Changing it moves the marker's draw layer and is retained if the picture is repaired. A wall panel has no depth section. No selection or several selected scenery pieces has no single-artwork depth section or active depth command. Existing repair actions and the missing-artwork explanation remain visible where applicable. + +The three buttons use the shared Stage inspector's selected, hover, focus, and disabled visual states; their words never gain checkmarks. Each native button meets the shared 44-pixel minimum hit height and responds to click, touch, Enter, and Space. On a short viewport, the body scrolls while the header and Close remain visible. A depth change preserves the body's scroll position. The card stays within the Stage's safe rectangle and does not cover the mode pill. + +## Existing contracts and target changes + +| Class / file | Typed contract | Change | +| --- | --- | --- | +| `StageInspectorPanel extends PanelContainer`, `scripts/shared/stage_ui/stage_inspector_panel.gd` | `func body() -> VBoxContainer`; `func set_header(title: String, icon_text: String = "") -> void`; `func set_safe_rect(rect: Rect2) -> void`; `func request_fit() -> void`; `func reset_scroll() -> void` | Reuse the present scenery card shell, its fixed header/Close, fit, and scroll. No new component API. | +| `StagePanelChoiceGroup extends VBoxContainer`, `scripts/shared/stage_ui/stage_panel_choice_group.gd` | `signal choice_selected(id: StringName)`; `func add_choice(id: StringName, caption: String, cue: Control = null) -> Button`; `func select_no_signal(id: StringName) -> void`; `func apply_theme(stage_theme: SandboxTheme) -> void` | Instantiate one artwork depth group with IDs `&"far"`, `&"near"`, `&"floor"`. The caller owns the mapping to child-facing captions and the domain mutation. No new component API. | +| `SandboxStage extends Node2D`, `scripts/editors/stage/sandbox_stage.gd` | Existing `_build_scenery_inspector()`, `_refresh_scenery_inspector()`, `_mark_scene_dirty()`; add a feature-local handler such as `func _on_selected_scenery_depth_choice(id: StringName) -> void` | Own the section, selection guard, refresh, and dirty call. Keep it in the existing scenery inspector body before `_scenery_size_row`. | +| `StageSceneryArtwork extends Node2D`, `scripts/stage/scenery/stage_scenery_artwork.gd` | `var depth: StringName`; `func set_depth(value: StringName) -> void` | Existing setter remains authoritative. It applies `StageDrawOrder.scenery_z(depth)` immediately to live art and missing markers. No new API. | +| `StageSceneAdapter extends RefCounted`, `scripts/editors/stage/stage_scene_adapter.gd` | Existing Scenery artwork entry's `depth: String` | Existing snapshot/load round trip persists the changed layer. No version, migration, or payload change. | + +The section is feature-local composition inside the shared shell: + +```text +StageInspectorPanel (existing scenery card) +├─ fixed header: artwork name or “Artwork not found” + Close +└─ scrolling body + ├─ missing-artwork explanation (only when missing) + ├─ Where it appears (only for one selected artwork) + │ ├─ StagePanelChoiceGroup + │ │ ├─ Far [id: far] + │ │ ├─ Near [id: near] + │ │ └─ On the floor [id: floor] + │ └─ helper: Far is behind Near. On the floor is in front of the ground. + ├─ Smaller / Bigger (existing availability rules) + └─ existing wall-erase or missing-artwork actions, as applicable +``` + +Style the heading and helper with the existing `SandboxTheme` `stage_panels.type.section_size`, `stage_panels.type.helper_size`, and text/muted colors. Apply the theme to the choice group so it uses `stage_panels.choice` sizing and states and `mode_colors.edit` accent. `StagePanelToggleRow` is for booleans and does not represent this three-way choice. No parallel button styling, theme token, popup, or dialog is required. This follows the approved [Stage panel UI system](stage_panel_ui_system.md) and [Phase 19 UI/UX specification](phase_19_scenery_backgrounds_ui_ux.md). + +## Signal and document flow + +```mermaid +flowchart LR + Press[Far / Near / On the floor pressed] --> Choice[StagePanelChoiceGroup.choice_selected: StringName] + Choice --> Guard[SandboxStage validates Edit Scenery and exactly one selected artwork] + Guard --> Same{New depth?} + Same -- No --> Sync[Refresh selected button without signal] + Same -- Yes --> Setter[StageSceneryArtwork.set_depth] + Setter --> Draw[StageDrawOrder z-index changes now] + Setter --> Dirty[SandboxStage._mark_scene_dirty] + Dirty --> Sync + Dirty --> Save[Normal .scene save snapshot stores depth] +``` + +The handler re-reads the current selection when the signal arrives; it must not retain a previously selected artwork reference in a button callback. It returns without mutation unless Stage is in Edit Scenery, the selection has exactly one valid `StageSceneryArtwork`, and the ID belongs to `far/near/floor`. It also returns without marking dirty when the requested ID already equals `artwork.depth`. For a real change, it calls `artwork.set_depth(id)` once, calls `_mark_scene_dirty()` once, and refreshes the card. Refresh uses `select_no_signal(artwork.depth)`, avoiding a second handler call. It does not call `reset_scroll()` for this value change or close/recreate the card; normal fitting can be requested without moving the child's reading position. + +For example, a selected tree with `depth == &"near"` receives `&"far"` from the button. Its `z_index` changes from `StageDrawOrder.NEAR` (`-20`) to `StageDrawOrder.FAR` (`-30`), its position and scale stay the same, and the Stage becomes dirty. Saving writes `"depth": "far"` in that tree's scenery artwork entry. Reopening selects Far for that tree; another tree placement can still be Near. A missing-art marker follows the same layer path and keeps its saved `asset_id`, footprint, and repair state. + +## Acceptance and verification + +1. Place two overlapping copies of a drawing, enter Edit Scenery, select one, and change Near to Far and then On the floor. The selected button and visible overlap order change immediately; the other copy keeps its former layer and neither copy moves or resizes. +2. With one drawing selected, press its already selected layer. The document's dirty state does not change and no redundant depth mutation is applied. Change to another layer; the document becomes dirty, the card stays open, and its scroll position remains stable. +3. Save the changed scene and reopen it. The same placement retains the new layer and its selected-artwork card highlights the saved choice. No scene-version migration or new field is needed. +4. Select a missing-art marker with known or unknown bounds. Its saved layer is highlighted and can be changed before repair; the marker visibly follows the chosen layer, and a later successful repair keeps that latest layer. Existing Find/Choose/Keep actions and size availability remain intact. +5. Select a wall panel, clear selection, and select multiple Scenery pieces. None offers a single-artwork depth choice. The wall's existing controls and group selection behavior remain available. +6. On a short or narrow viewport and by keyboard or touch, reach all three depth choices and Close. The fixed header remains accessible; selected/focus styles are distinct, and the Stage mode pill remains clear. + +## Architectural decisions and limits + +- **Reuse the existing inspector and choice group.** The UI is one exclusive three-way setting inside a card that already exists, so an additional panel class, signal bus, generic inspector schema, or `Resource` model adds indirection without simplifying this change. +- **Keep the Stage controller responsible for mutations.** The shared UI emits a `StringName` intent. `SandboxStage` verifies the live selection, changes one `StageSceneryArtwork`, and uses its existing dirty path; the UI control knows nothing about scene persistence. +- **Keep per-placement depth as the source of truth.** The `.scene` adapter already saves this field and `set_depth()` already maps it to Godot draw order. Missing-art recovery preserves the placement's depth. The placement tool's default and capture-bar setting remain separate from editing an existing piece. +- **Keep Undo in its own phase.** Each depth press is an immediate committed Stage edit. A future Stage-wide Undo command can capture old/new depth alongside other scenery actions under the existing Phase 19 debt item. + diff --git a/scenes/stage/asset_selector.tscn b/scenes/stage/asset_selector.tscn index 395cb96..cff0333 100644 --- a/scenes/stage/asset_selector.tscn +++ b/scenes/stage/asset_selector.tscn @@ -34,6 +34,29 @@ unique_name_in_owner = true layout_mode = 2 text = "×" +[node name="GalleryTabs" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +visible = false + +[node name="BackgroundsTab" type="Button" parent="MarginContainer/VBoxContainer/GalleryTabs"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "🌤 Backgrounds" + +[node name="ArtworkTab" type="Button" parent="MarginContainer/VBoxContainer/GalleryTabs"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "🎨 Artwork" + +[node name="TagFilter" type="OptionButton" parent="MarginContainer/VBoxContainer/GalleryTabs"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +visible = false + [node name="GridContainer" type="GridContainer" parent="MarginContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 @@ -79,3 +102,15 @@ text = "Browse…" unique_name_in_owner = true layout_mode = 2 text = "Refresh" + +[node name="MakeArtworkButton" type="Button" parent="MarginContainer/VBoxContainer/Footer"] +unique_name_in_owner = true +layout_mode = 2 +visible = false +text = "✏️ Make artwork" + +[node name="EditSceneryButton" type="Button" parent="MarginContainer/VBoxContainer/Footer"] +unique_name_in_owner = true +layout_mode = 2 +visible = false +text = "🎛 Edit placed Scenery" diff --git a/scripts/editors/prop/prop_canvas.gd b/scripts/editors/prop/prop_canvas.gd index 4d4a1c1..9d09b0b 100644 --- a/scripts/editors/prop/prop_canvas.gd +++ b/scripts/editors/prop/prop_canvas.gd @@ -33,6 +33,7 @@ signal anchor_selected(anchor_id: String) signal whole_prop_resize_requested(factor: float) signal whole_prop_move_requested(delta: Vector2) signal whole_prop_rotate_requested(angle_rad: float) +signal stage_size_cancel_requested # --------------------------------------------------------------------------- # Enums / constants @@ -141,6 +142,14 @@ var _whole_prop_delta: Vector2 = Vector2.ZERO var _whole_prop_total_angle: float = 0.0 var _whole_prop_previous_angle: float = 0.0 var _whole_prop_preview_angle: float = 0.0 +var _stage_size_preview_active: bool = false +var _stage_size_start_shapes: Array[Dictionary] = [] +var _stage_size_preview_shapes: Array[Dictionary] = [] +var _stage_size_start_anchors: Dictionary = {} +var _stage_size_preview_anchors: Dictionary = {} +var _stage_size_start_pan: Vector2 = Vector2.ZERO +var _stage_size_start_zoom: float = 1.0 +var _stage_size_factor: float = 1.0 const SCALE_COORDINATE_LIMIT: float = 1000000.0 const WHOLE_PROP_HANDLE_HIT_RADIUS: float = 16.0 @@ -202,6 +211,8 @@ func set_edit_mode(mode: int) -> void: var m: int = mode if mode >= EditMode.TRANSFORM and mode <= EditMode.ATTACH else EditMode.TRANSFORM if m == _edit_mode: return + if _stage_size_preview_active: + cancel_stage_size_preview() _edit_mode = m if _edit_mode != EditMode.TRANSFORM: set_whole_prop_resize_enabled(false) @@ -280,6 +291,188 @@ func set_show_stickman_guide(enabled: bool) -> void: func get_show_stickman_guide() -> bool: return _show_stickman_guide + +func begin_stage_size_preview(anchors: Dictionary) -> bool: + if _stage_size_preview_active or _whole_prop_resize_enabled or _edit_mode != EditMode.TRANSFORM or not has_shape_geometry(): + return false + var bounds: Rect2 = get_visible_shapes_bounds() + var extent: float = maxf(bounds.size.x, bounds.size.y) + if not _valid_rect(bounds) or not is_finite(extent) or extent <= 0.0: + return false + _stage_size_start_shapes = _duplicate_shapes(shapes) + _stage_size_preview_shapes = _duplicate_shapes(shapes) + _stage_size_start_anchors = anchors.duplicate(true) + _stage_size_preview_anchors = anchors.duplicate(true) + _stage_size_start_pan = _pan_offset + _stage_size_start_zoom = _zoom + _stage_size_factor = 1.0 + _stage_size_preview_active = true + queue_redraw() + return true + + +func preview_stage_size(factor: float) -> bool: + if not _stage_size_preview_active or not is_finite(factor) or factor <= 0.0: + return false + var candidate: Array[Dictionary] = _scaled_stage_size_shapes(_stage_size_start_shapes, factor) + if candidate.is_empty() or not _shapes_are_representable(candidate): + return false + var anchor_result: Dictionary = PropAnchorDataScript.try_scale_non_origin(_stage_size_start_anchors, factor) + if not bool(anchor_result.get("ok", false)): + return false + var candidate_bounds: Rect2 = _visible_bounds_for_shapes(candidate) + if not _valid_rect(candidate_bounds): + return false + _stage_size_factor = factor + _stage_size_preview_shapes = candidate + _stage_size_preview_anchors = (anchor_result.get("anchors", {}) as Dictionary).duplicate(true) + queue_redraw() + return true + + +func commit_stage_size_preview() -> Dictionary: + if not _stage_size_preview_active: + return {"changed": false, "anchors": get_anchor_data()} + var changed: bool = not is_equal_approx(_stage_size_factor, 1.0) + var committed_anchors: Dictionary = _stage_size_preview_anchors.duplicate(true) + if changed: + shapes = _duplicate_shapes(_stage_size_preview_shapes) + set_anchor_data(committed_anchors) + _end_stage_size_preview() + if changed: + shape_changed.emit(get_shape_data()) + return {"changed": changed, "anchors": committed_anchors} + + +func cancel_stage_size_preview() -> void: + if _stage_size_preview_active: + _end_stage_size_preview() + + +func is_stage_size_preview_active() -> bool: + return _stage_size_preview_active + + +func get_visible_shapes_bounds() -> Rect2: + return _visible_bounds_for_shapes(_stage_size_preview_shapes if _stage_size_preview_active else shapes) + + +func get_stage_size_comparison_bounds() -> Rect2: + var artwork_bounds: Rect2 = _visible_bounds_for_shapes(_stage_size_preview_shapes if _stage_size_preview_active else shapes) + if not _valid_rect(artwork_bounds): + return Rect2() + return artwork_bounds.merge(_stage_size_guide_bounds(artwork_bounds)) + + +func fit_stage_size_view(bounds: Rect2, padding_px: float, force: bool = true) -> void: + if not _stage_size_preview_active or not _valid_rect(bounds): + return + if not force and _stage_size_view_contains(bounds, padding_px): + return + var available: Vector2 = size - Vector2.ONE * maxf(0.0, padding_px) * 2.0 + if available.x <= 0.0 or available.y <= 0.0: + return + var fit_x: float = available.x / maxf(bounds.size.x, 1.0) + var fit_y: float = available.y / maxf(bounds.size.y, 1.0) + _zoom = clampf(minf(fit_x, fit_y), 0.02, MAX_ZOOM) + _pan_offset = size * 0.5 - bounds.get_center() * _zoom + queue_redraw() + + +func _end_stage_size_preview() -> void: + _stage_size_preview_active = false + _stage_size_start_shapes.clear() + _stage_size_preview_shapes.clear() + _stage_size_start_anchors.clear() + _stage_size_preview_anchors.clear() + _stage_size_factor = 1.0 + _pan_offset = _stage_size_start_pan + _zoom = _stage_size_start_zoom + queue_redraw() + + +func _stage_size_view_contains(bounds: Rect2, padding_px: float) -> bool: + var padding: float = maxf(0.0, padding_px) + var available: Vector2 = size - Vector2.ONE * padding * 2.0 + if available.x <= 0.0 or available.y <= 0.0: + return false + var world_origin: Vector2 = -_pan_offset / maxf(_zoom, 0.001) + var world_size: Vector2 = size / maxf(_zoom, 0.001) + var world_padding: float = padding / maxf(_zoom, 0.001) + var visible_bounds: Rect2 = Rect2(world_origin + Vector2.ONE * world_padding, world_size - Vector2.ONE * world_padding * 2.0) + return visible_bounds.encloses(bounds) + + +func _visible_bounds_for_shapes(source: Array[Dictionary]) -> Rect2: + var result: Rect2 = Rect2() + var found: bool = false + for shape: Dictionary in source: + if not _shape_has_geometry(shape): + continue + var bounds: Rect2 = _shape_bbox(shape).grow(maxf(0.0, float(shape.get("outline_width", 0.0))) * 0.5) + if not found: + result = bounds + found = true + else: + result = result.merge(bounds) + return result if found else Rect2() + + +func _stage_size_guide_bounds(artwork_bounds: Rect2) -> Rect2: + var head_center: Vector2 = WholeStickmanPreviewScript.GUIDE_JOINTS["Head"] + var ankle: Vector2 = WholeStickmanPreviewScript.GUIDE_JOINTS["LeftAnkle"] + var height: float = ankle.y - (head_center.y - WholeStickmanPreviewScript.GUIDE_HEAD_RADIUS) + var guide_half_width: float = 170.0 + var figure_center_x: float + if artwork_bounds.size.x <= guide_half_width * 2.0 + 40.0: + figure_center_x = artwork_bounds.position.x + minf(artwork_bounds.size.x * 0.25, 40.0) + else: + figure_center_x = artwork_bounds.position.x - guide_half_width - 32.0 + var floor_y: float = artwork_bounds.end.y + return Rect2(Vector2(figure_center_x - guide_half_width, floor_y - height), Vector2(guide_half_width * 2.0, height)) + + +func _shapes_are_representable(source: Array[Dictionary]) -> bool: + if source.is_empty(): + return false + for shape: Dictionary in source: + var points: PackedVector2Array = shape.get("points", PackedVector2Array()) + for point: Vector2 in points: + if not is_finite(point.x) or not is_finite(point.y) or absf(point.x) > SCALE_COORDINATE_LIMIT or absf(point.y) > SCALE_COORDINATE_LIMIT: + return false + var radius: float = float(shape.get("radius", 0.0)) + var outline_width: float = float(shape.get("outline_width", 0.0)) + if not is_finite(radius) or radius < 0.0 or radius > SCALE_COORDINATE_LIMIT: + return false + if not is_finite(outline_width) or outline_width < 0.0 or outline_width > SCALE_COORDINATE_LIMIT: + return false + return true + + +func _scaled_stage_size_shapes(source: Array[Dictionary], factor: float) -> Array[Dictionary]: + var result: Array[Dictionary] = [] + for source_shape: Dictionary in source: + var shape: Dictionary = source_shape.duplicate(true) + var points: PackedVector2Array = shape.get("points", PackedVector2Array()) + for index: int in points.size(): + points[index] *= factor + shape["points"] = points + shape["radius"] = float(shape.get("radius", 0.0)) * factor + shape["outline_width"] = float(shape.get("outline_width", 0.0)) * factor + result.append(shape) + return result + + +func _duplicate_shapes(source: Array[Dictionary]) -> Array[Dictionary]: + var result: Array[Dictionary] = [] + for shape: Dictionary in source: + result.append(shape.duplicate(true)) + return result + + +func _valid_rect(rect: Rect2) -> bool: + return rect.position.is_finite() and rect.size.is_finite() and rect.size.x >= 0.0 and rect.size.y >= 0.0 and (rect.size.x > 0.0 or rect.size.y > 0.0) + func get_shapes_bounds() -> Rect2: var found: bool = false; var result: Rect2 = Rect2() for shape: Dictionary in shapes: @@ -378,6 +571,7 @@ func set_origin_marker_color(color: Color) -> void: func set_shape_data(data: Variant) -> void: ## Import shapes. Accepts an Array[Dictionary] (internal or plain {x,y} ## format) or a single Dictionary. + cancel_stage_size_preview() set_whole_prop_resize_enabled(false) shapes.clear() if data is Array: @@ -403,6 +597,7 @@ func get_shape_data() -> Array[Dictionary]: func clear() -> void: ## Remove all shapes without emitting shape_changed. + cancel_stage_size_preview() set_whole_prop_resize_enabled(false) shapes.clear() _selected_shape_idx = -1 @@ -788,7 +983,6 @@ func global_to_world(global_pos: Vector2) -> Vector2: func _on_canvas_draw() -> void: draw_set_transform(_pan_offset, 0.0, Vector2(_zoom, _zoom)) _draw_grid() - _draw_stickman_guide() if shapes.is_empty() and _edit_mode == EditMode.TRANSFORM: var rect := get_rect() @@ -797,10 +991,14 @@ func _on_canvas_draw() -> void: if font != null: draw_string(font, hint_pos, "Right-click to add a shape", HORIZONTAL_ALIGNMENT_CENTER, -1, 14, Color(0.5, 0.5, 0.5, 0.6)) - var drawn_shapes: Array[Dictionary] = _whole_prop_preview_shapes if _whole_prop_dragging else shapes + var drawn_shapes: Array[Dictionary] = _stage_size_preview_shapes if _stage_size_preview_active else (_whole_prop_preview_shapes if _whole_prop_dragging else shapes) for i: int in drawn_shapes.size(): - var selected: bool = not _whole_prop_resize_enabled and i == _selected_shape_idx + var selected: bool = not _stage_size_preview_active and not _whole_prop_resize_enabled and i == _selected_shape_idx _draw_shape(drawn_shapes[i], selected, _dragging_vertex if selected else -1) + if _stage_size_preview_active: + _draw_stage_size_guide(_visible_bounds_for_shapes(drawn_shapes)) + else: + _draw_stickman_guide() _draw_origin_marker() if _whole_prop_resize_enabled: @@ -836,6 +1034,54 @@ func _draw_stickman_guide() -> void: draw_line(joints[first] as Vector2, joints[second] as Vector2, color, width) if joints.has("Head"): draw_arc(joints["Head"] as Vector2, WholeStickmanPreviewScript.GUIDE_HEAD_RADIUS, 0.0, TAU, 48, color, width) + var ankle: Vector2 = joints["LeftAnkle"] + _draw_reference_figure(0.0, ankle.y, ankle.x - 250.0, ankle.x + 250.0) + + +func _draw_stage_size_guide(artwork_bounds: Rect2) -> void: + var guide_bounds: Rect2 = _stage_size_guide_bounds(artwork_bounds) + var floor_y: float = artwork_bounds.end.y + _draw_reference_figure( + guide_bounds.get_center().x, + floor_y, + minf(artwork_bounds.position.x, guide_bounds.position.x - 28.0), + maxf(artwork_bounds.end.x, guide_bounds.end.x + 28.0) + ) + + +func _draw_reference_figure(center_x: float, floor_y: float, floor_start: float, floor_end: float) -> void: + var joints: Dictionary = WholeStickmanPreviewScript.GUIDE_JOINTS + var ankle_y: float = (joints["LeftAnkle"] as Vector2).y + var offset: Vector2 = Vector2(center_x, floor_y - ankle_y) + var inv_zoom: float = 1.0 / maxf(_zoom, 0.001) + for bone: Array in WholeStickmanPreviewScript.GUIDE_BONES: + var first: String = str(bone[0]) + var second: String = str(bone[1]) + if not joints.has(first) or not joints.has(second): + continue + var color: Color = WholeStickmanPreviewScript.GUIDE_COLOR_CENTRAL + if first in WholeStickmanPreviewScript.GUIDE_LEFT_JOINTS or second in WholeStickmanPreviewScript.GUIDE_LEFT_JOINTS: + color = WholeStickmanPreviewScript.GUIDE_COLOR_LEFT + elif first in WholeStickmanPreviewScript.GUIDE_RIGHT_JOINTS or second in WholeStickmanPreviewScript.GUIDE_RIGHT_JOINTS: + color = WholeStickmanPreviewScript.GUIDE_COLOR_RIGHT + var start: Vector2 = (joints[first] as Vector2) + offset + var finish: Vector2 = (joints[second] as Vector2) + offset + draw_line(start, finish, Color("#111821D9"), 8.0 * inv_zoom, true) + draw_line(start, finish, color, 3.0 * inv_zoom, true) + var head: Vector2 = (joints["Head"] as Vector2) + offset + draw_arc(head, WholeStickmanPreviewScript.GUIDE_HEAD_RADIUS, 0.0, TAU, 48, Color("#111821E8"), 8.0 * inv_zoom, true) + draw_arc(head, WholeStickmanPreviewScript.GUIDE_HEAD_RADIUS, 0.0, TAU, 48, WholeStickmanPreviewScript.GUIDE_COLOR_CENTRAL, 3.0 * inv_zoom, true) + for joint_name: String in joints: + var point: Vector2 = (joints[joint_name] as Vector2) + offset + var color: Color = WholeStickmanPreviewScript.GUIDE_COLOR_CENTRAL + if joint_name in WholeStickmanPreviewScript.GUIDE_LEFT_JOINTS: + color = WholeStickmanPreviewScript.GUIDE_JOINT_COLOR_LEFT + elif joint_name in WholeStickmanPreviewScript.GUIDE_RIGHT_JOINTS: + color = WholeStickmanPreviewScript.GUIDE_JOINT_COLOR_RIGHT + draw_circle(point, 6.5 * inv_zoom, Color("#111821E8")) + draw_circle(point, 3.2 * inv_zoom, color) + draw_line(Vector2(floor_start, floor_y), Vector2(floor_end, floor_y), Color("#111821E8"), 8.0 * inv_zoom, true) + draw_line(Vector2(floor_start, floor_y), Vector2(floor_end, floor_y), Color("#F8FAFC"), 2.5 * inv_zoom, true) func _draw_whole_prop_resize_gizmos() -> void: var bounds: Rect2 = _whole_preview_bounds() if _whole_prop_dragging else get_shapes_bounds() @@ -852,7 +1098,7 @@ func _draw_whole_prop_resize_gizmos() -> void: var rotation_dot: Vector2 = Vector2(bounds.get_center().x, bounds.end.y + ROTATION_CIRCLE_OFFSET) draw_circle(rotation_dot, (ROTATION_CIRCLE_RADIUS + 2.0) / _zoom, SELECTION_HALO_COLOR) draw_circle(rotation_dot, ROTATION_CIRCLE_RADIUS / _zoom, SELECTION_CORE_COLOR) - var anchors: Dictionary = _whole_prop_preview_anchors if _whole_prop_dragging else get_anchor_data() + var anchors: Dictionary = _stage_size_preview_anchors if _stage_size_preview_active else (_whole_prop_preview_anchors if _whole_prop_dragging else get_anchor_data()) for id: String in anchors: if id == "origin": continue diff --git a/scripts/editors/prop/prop_editor.gd b/scripts/editors/prop/prop_editor.gd index 2bac60c..719a2ae 100644 --- a/scripts/editors/prop/prop_editor.gd +++ b/scripts/editors/prop/prop_editor.gd @@ -36,7 +36,13 @@ const SETTINGS_PATH: String = "user://prop_editor_settings.json" const RECENT_MAX: int = 5 const OFF_ORIGIN_WARNING_MIN_DISTANCE: float = 1024.0 const OFF_ORIGIN_WARNING_BOUNDS_MULTIPLIER: float = 2.0 -const NORMALIZED_MAX_EXTENT: float = 96.0 +const STAGE_REFERENCE_HEIGHT: float = 939.5 # WholeStickmanPreview head top (-463.5 - 100) to ankle line (376). +const STAGE_SIZE_STEP: float = 1.1 +const ART_TAG_IDS: Array[String] = ["nature", "buildings", "walls", "roads", "objects"] +const ART_TAG_LABELS: Array[String] = ["Nature", "Buildings", "Walls", "Roads", "Objects"] + +signal artwork_saved(path: String) +signal return_requested const MENU_NEW := 0 const MENU_LOAD := 2 @@ -113,6 +119,7 @@ var _current_path: String = "" var _dirty: bool = false var _clipboard: Dictionary = {} var _anchors: Dictionary = PropAnchorDataScript.default_anchors() +var _tags: Array[String] = [] var _selected_anchor_id: String = "" var _thumb: PropThumbnail var _thumb_cache: ThumbnailCache @@ -138,15 +145,26 @@ var _params_box: VBoxContainer var _param_spins: Dictionary = {} var _add_button: Button var _updating_inspector: bool = false +var _tag_checks: Dictionary = {} +var _tag_section: VBoxContainer +var _stage_authoring: bool = false +var _stage_return_pending: bool = false +var _stage_place_button: Button # Grid settings dialog (removed Round 4 - replaced by top-right controls) var _grid_spin: SpinBox var _snap_check: CheckBox var _center_prop_button: Button -var _normalize_size_button: Button +var _size_for_stage_button: Button +var _stage_size_controls: FlowContainer +var _stage_size_status: Label var _show_stickman_guide_check: CheckBox var _show_stickman_guide: bool = false var _whole_prop_resize_button: Button +var _stage_size_preview_active: bool = false +var _stage_size_preview_factor: float = 1.0 +var _stage_size_entry_extent: float = 0.0 +var _pending_save_after_stage_size: int = -1 # Edit-mode toolbar (Round 2/3) var _mode_group: ButtonGroup @@ -192,7 +210,8 @@ var _recent_props: Array[String] = [] # --------------------------------------------------------------------------- func _ready() -> void: - get_tree().auto_accept_quit = false + if not _stage_authoring: + get_tree().auto_accept_quit = false _theme = PropThemeScript.new() _theme.load_from(PropThemeScript.DEFAULT_PATH) _thumb_cache = ThumbnailCacheScript.new() @@ -225,6 +244,7 @@ func _ready() -> void: _canvas.whole_prop_resize_requested.connect(_on_whole_prop_resize_requested) _canvas.whole_prop_move_requested.connect(_on_whole_prop_move_requested) _canvas.whole_prop_rotate_requested.connect(_on_whole_prop_rotate_requested) + _canvas.stage_size_cancel_requested.connect(func() -> void: _finish_stage_size_preview(false)) _canvas.set_grid_size(_theme.grid_size()) _canvas.set_snap_enabled(false) _canvas.set_has_clipboard(false) @@ -250,12 +270,24 @@ func _ready() -> void: _canvas.call_deferred("reset_view") call_deferred("_initialize_inspector_split") _refresh_center_prop_button() - _check_autosave_recovery() + if _stage_authoring: + if _canvas.get_edit_mode() == PropCanvasScript.EditMode.ATTACH: + _canvas.set_edit_mode(PropCanvasScript.EditMode.TRANSFORM) + _do_new() + _build_stage_authoring_controls() + _apply_authoring_context() + else: + _check_autosave_recovery() + + +func configure_stage_authoring() -> void: + _stage_authoring = true + process_mode = Node.PROCESS_MODE_ALWAYS func _process(delta: float) -> void: _update_cursor_coords() - if _autosave_remaining > 0.0: + if not _stage_authoring and _autosave_remaining > 0.0: _autosave_remaining -= delta if _autosave_remaining <= 0.0: _autosave_remaining = 0.0 @@ -266,6 +298,17 @@ func _unhandled_key_input(event: InputEvent) -> void: if event is InputEventKey: var key := event as InputEventKey if key.pressed and not key.echo and key.keycode == KEY_ESCAPE: + if _stage_size_preview_active: + _finish_stage_size_preview(false) + get_viewport().set_input_as_handled() + return + if _stage_authoring: + var stage_focus: Control = get_viewport().gui_get_focus_owner() if get_viewport() != null else null + if stage_focus is LineEdit or stage_focus is TextEdit: + return + _request_stage_return() + get_viewport().set_input_as_handled() + return var focus_owner: Control = get_viewport().gui_get_focus_owner() if get_viewport() != null else null if not (focus_owner is LineEdit or focus_owner is TextEdit): if _canvas.is_whole_prop_resize_dragging(): @@ -349,6 +392,11 @@ func _on_attach_mode_pressed() -> void: func _on_canvas_mode_changed(mode: int) -> void: + if _stage_size_preview_active: + _cancel_stage_size_preview() + if _stage_authoring and mode == PropCanvasScript.EditMode.ATTACH: + _canvas.set_edit_mode(PropCanvasScript.EditMode.TRANSFORM) + return if mode != PropCanvasScript.EditMode.TRANSFORM: _set_whole_prop_resize_enabled(false) var entering_attach: bool = mode == PropCanvasScript.EditMode.ATTACH and _last_canvas_mode != PropCanvasScript.EditMode.ATTACH @@ -373,11 +421,11 @@ func _sync_mode_buttons(mode: int) -> void: func _sync_mode_controls(mode: int) -> void: if _whole_prop_resize_button != null: - _whole_prop_resize_button.visible = mode == PropCanvasScript.EditMode.TRANSFORM - if _normalize_size_button != null: - _normalize_size_button.visible = mode == PropCanvasScript.EditMode.TRANSFORM + _whole_prop_resize_button.visible = mode == PropCanvasScript.EditMode.TRANSFORM and not _stage_size_preview_active + if _size_for_stage_button != null: + _size_for_stage_button.visible = mode == PropCanvasScript.EditMode.TRANSFORM and not _stage_size_preview_active if _attach_tools != null: - _attach_tools.visible = mode == PropCanvasScript.EditMode.ATTACH + _attach_tools.visible = mode == PropCanvasScript.EditMode.ATTACH and not _stage_authoring if _add_section != null: _add_section.visible = mode == PropCanvasScript.EditMode.TRANSFORM if _delete_button != null: @@ -385,9 +433,9 @@ func _sync_mode_controls(mode: int) -> void: if _shape_section != null: _shape_section.visible = mode != PropCanvasScript.EditMode.ATTACH and not _canvas.is_whole_prop_resize_enabled() and _canvas.get_selected_index() >= 0 if _material_section != null: - _material_section.visible = mode != PropCanvasScript.EditMode.ATTACH + _material_section.visible = mode != PropCanvasScript.EditMode.ATTACH and not _stage_authoring if _anchor_section != null: - _anchor_section.visible = mode == PropCanvasScript.EditMode.ATTACH + _anchor_section.visible = mode == PropCanvasScript.EditMode.ATTACH and not _stage_authoring if _attach_preview != null: _attach_preview.visible = mode == PropCanvasScript.EditMode.ATTACH and (_show_preview_check == null or _show_preview_check.button_pressed) if _attach_preview.has_method("set_document"): @@ -666,11 +714,12 @@ func _build_size_tools() -> void: _whole_prop_resize_button.custom_minimum_size.y = float(_theme.button_min_height()) _whole_prop_resize_button.toggled.connect(_set_whole_prop_resize_enabled) _size_tools.add_child(_whole_prop_resize_button) - _normalize_size_button = Button.new() - _normalize_size_button.text = "📏 Normalize Size" - _normalize_size_button.custom_minimum_size.y = float(_theme.button_min_height()) - _normalize_size_button.pressed.connect(_on_normalize_size_pressed) - _size_tools.add_child(_normalize_size_button) + _size_for_stage_button = Button.new() + _size_for_stage_button.text = "📏 Size for Stage" + _size_for_stage_button.tooltip_text = "Compare this drawing with a standing stickman." + _size_for_stage_button.custom_minimum_size = Vector2(148.0, maxf(44.0, float(_theme.button_min_height()))) + _size_for_stage_button.pressed.connect(_on_size_for_stage_pressed) + _size_tools.add_child(_size_for_stage_button) _center_prop_button = Button.new() _center_prop_button.text = "🎯 Center Prop" _center_prop_button.tooltip_text = "Move the whole prop and its anchors to (0,0)." @@ -683,9 +732,57 @@ func _build_size_tools() -> void: _show_stickman_guide_check.custom_minimum_size.y = float(_theme.button_min_height()) _show_stickman_guide_check.toggled.connect(_on_stickman_guide_toggled) _size_tools.add_child(_show_stickman_guide_check) + _stage_size_controls = FlowContainer.new() + _stage_size_controls.name = "StageSizeControls" + _stage_size_controls.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _stage_size_controls.add_theme_constant_override("h_separation", 6) + _stage_size_controls.add_theme_constant_override("v_separation", 6) + _stage_size_controls.hide() + _size_tools.add_child(_stage_size_controls) + for preset: Dictionary in [ + {"label": "Small", "factor": 0.35}, + {"label": "Room", "factor": 1.0}, + {"label": "Big", "factor": 2.0}, + ]: + var preset_button: Button = _make_stage_size_button(str(preset["label"])) + var preset_factor: float = float(preset["factor"]) + preset_button.pressed.connect(func() -> void: _choose_stage_size(preset_factor)) + _stage_size_controls.add_child(preset_button) + var smaller_button: Button = _make_stage_size_button("Make smaller") + smaller_button.pressed.connect(func() -> void: _adjust_stage_size(1.0 / STAGE_SIZE_STEP)) + _stage_size_controls.add_child(smaller_button) + var bigger_button: Button = _make_stage_size_button("Make bigger") + bigger_button.pressed.connect(func() -> void: _adjust_stage_size(STAGE_SIZE_STEP)) + _stage_size_controls.add_child(bigger_button) + var keep_button: Button = _make_stage_size_button("Keep size", true) + keep_button.pressed.connect(func() -> void: _finish_stage_size_preview(true)) + _stage_size_controls.add_child(keep_button) + var cancel_button: Button = _make_stage_size_button("✕ Cancel") + cancel_button.pressed.connect(func() -> void: _finish_stage_size_preview(false)) + _stage_size_controls.add_child(cancel_button) + _stage_size_status = Label.new() + _stage_size_status.name = "StageSizeStatus" + # This label lives in the size-tools FlowContainer. With an auto-wrapping + # label and no minimum width, FlowContainer can collapse it to a sliver and + # grow the toolbar to thousands of pixels tall when the preview opens. + _stage_size_status.custom_minimum_size.x = 360.0 + _stage_size_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _stage_size_status.hide() + _size_tools.add_child(_stage_size_status) _refresh_scale_buttons() +func _make_stage_size_button(label: String, prominent: bool = false) -> Button: + var button: Button = Button.new() + button.text = label + button.custom_minimum_size.y = maxf(44.0, float(_theme.button_min_height())) + button.focus_mode = Control.FOCUS_ALL + button.theme = _ui_theme + if prominent: + button.add_theme_color_override("font_color", _theme.accent_color()) + return button + + func _on_top_grid_changed(value: float) -> void: _canvas.set_grid_size(int(value)) @@ -693,12 +790,110 @@ func _on_top_grid_changed(value: float) -> void: func _on_top_snap_toggled(pressed: bool) -> void: _canvas.set_snap_enabled(pressed) -func _on_normalize_size_pressed() -> void: - var bounds: Rect2 = _canvas.get_shapes_bounds() - var extent: float = maxf(bounds.size.x, bounds.size.y) - if is_finite(extent) and extent > 0.0: - _scale_document(NORMALIZED_MAX_EXTENT / extent) +func _on_size_for_stage_pressed() -> void: + if _stage_size_preview_active or _canvas.get_edit_mode() != PropCanvasScript.EditMode.TRANSFORM: + return + _set_whole_prop_resize_enabled(false) + var bounds: Rect2 = _canvas.get_visible_shapes_bounds() + _stage_size_entry_extent = maxf(bounds.size.x, bounds.size.y) + if not is_finite(_stage_size_entry_extent) or _stage_size_entry_extent <= 0.0: + return + if not _canvas.begin_stage_size_preview(_anchors): + return + _stage_size_preview_factor = 1.0 + _stage_size_preview_active = true + _stage_size_status.text = "Compare the drawing with the guide, then keep it or cancel." + _update_stage_size_controls_visibility() + # Hiding the inspector and mode bar changes the canvas allocation. Wait for + # that layout pass before centering/fitting, otherwise the guide can be + # pushed outside the newly-sized viewport until a preset is clicked. + await get_tree().process_frame + if not _stage_size_preview_active: + return + _canvas.fit_stage_size_view(_canvas.get_stage_size_comparison_bounds(), 48.0, true) + + +func _choose_stage_size(relative_height: float) -> void: + if not _stage_size_preview_active or not is_finite(relative_height) or relative_height <= 0.0: + return + var factor: float = STAGE_REFERENCE_HEIGHT * relative_height / _stage_size_entry_extent + if _canvas.preview_stage_size(factor): + _stage_size_preview_factor = factor + _stage_size_status.text = "" + _canvas.fit_stage_size_view(_canvas.get_stage_size_comparison_bounds(), 48.0, true) + else: + _stage_size_status.text = "That size is too large to display. Try a smaller choice." + + +func _adjust_stage_size(multiplier: float) -> void: + if not _stage_size_preview_active: + return + var factor: float = _stage_size_preview_factor * multiplier + if _canvas.preview_stage_size(factor): + _stage_size_preview_factor = factor + _stage_size_status.text = "" + _canvas.fit_stage_size_view(_canvas.get_stage_size_comparison_bounds(), 48.0, false) + else: + _stage_size_status.text = "That size is outside the drawing area. Try a smaller change." + + +func _finish_stage_size_preview(keep: bool) -> void: + if not _stage_size_preview_active: + return + var pending_save: int = _pending_save_after_stage_size if keep else -1 + _pending_save_after_stage_size = -1 + if keep: + var result: Dictionary = _canvas.commit_stage_size_preview() + _anchors = (result.get("anchors", _anchors) as Dictionary).duplicate(true) + _selected_anchor_id = PropAnchorDataScript.preferred_editable_id(_anchors, _selected_anchor_id) + _canvas.set_anchor_data(_anchors) + _canvas.select_anchor(_selected_anchor_id) + _refresh_anchor_inspector() + _sync_attach_preview() + else: + _canvas.cancel_stage_size_preview() + _stage_size_preview_active = false + _stage_size_preview_factor = 1.0 + _stage_size_entry_extent = 0.0 + _stage_size_status.text = "" + _update_stage_size_controls_visibility() _refresh_scale_buttons() + if pending_save >= 0: + _continue_save_after_stage_size(pending_save) + + +func _cancel_stage_size_preview() -> void: + if _stage_size_preview_active: + _pending_save_after_stage_size = -1 + _finish_stage_size_preview(false) + + +func _request_save_after_stage_size(menu_id: int) -> void: + _pending_save_after_stage_size = menu_id + _stage_size_status.text = "Keep size before saving, or cancel to leave the drawing unchanged." + + +func _continue_save_after_stage_size(menu_id: int) -> void: + if menu_id == MENU_SAVE: + if _current_path.is_empty(): + _save_dialog.popup_centered() + else: + _begin_save_request(_current_path, true) + elif menu_id == MENU_SAVE_AS: + _save_dialog.popup_centered() + + +func _update_stage_size_controls_visibility() -> void: + if _stage_size_controls == null: + return + _stage_size_controls.visible = _stage_size_preview_active + _stage_size_status.visible = _stage_size_preview_active + _size_for_stage_button.visible = not _stage_size_preview_active and _canvas.get_edit_mode() == PropCanvasScript.EditMode.TRANSFORM + _whole_prop_resize_button.visible = not _stage_size_preview_active and _canvas.get_edit_mode() == PropCanvasScript.EditMode.TRANSFORM + _center_prop_button.visible = not _stage_size_preview_active + _show_stickman_guide_check.visible = not _stage_size_preview_active and not _stage_authoring + _mode_bar.visible = not _stage_size_preview_active + _inspector_column.visible = not _stage_size_preview_active func _on_stickman_guide_toggled(enabled: bool) -> void: _show_stickman_guide = enabled @@ -755,6 +950,9 @@ func _notification(what: int) -> void: func _request_close() -> void: + if _stage_authoring: + _request_stage_return() + return if not _dirty: get_tree().quit() return @@ -765,6 +963,7 @@ func _request_close() -> void: func _request_new() -> void: + _cancel_stage_size_preview() if not _dirty: _do_new() return @@ -774,6 +973,22 @@ func _request_new() -> void: _exit_dialog.popup_centered() +func _request_stage_return() -> void: + if not _stage_authoring: + return + _cancel_stage_size_preview() + if not _dirty: + if not _current_path.is_empty(): + artwork_saved.emit(_current_path) + return_requested.emit() + return + _stage_return_pending = true + _pending_new = false + _pending_quit = false + _exit_dialog.dialog_text = "Save your artwork before returning? Saving places it on the Stage." + _exit_dialog.popup_centered() + + func _on_exit_save() -> void: _exit_dialog.hide() if _current_path.is_empty(): @@ -785,8 +1000,12 @@ func _on_exit_save() -> void: func _on_exit_custom_action(action: StringName) -> void: if action == &"discard": _exit_dialog.hide() - _clear_autosave() - if _pending_quit: + if not _stage_authoring: + _clear_autosave() + if _stage_return_pending: + _stage_return_pending = false + return_requested.emit() + elif _pending_quit: _pending_quit = false get_tree().quit() elif _pending_new: @@ -797,6 +1016,7 @@ func _on_exit_custom_action(action: StringName) -> void: func _on_exit_cancel() -> void: _pending_quit = false _pending_new = false + _stage_return_pending = false _cancel_pending_save_request() @@ -805,6 +1025,8 @@ func _do_new() -> void: _set_whole_prop_resize_enabled(false) _prop_name_edit.text = "" _material_preset = PropBlockScript.MaterialPreset.NONE + _tags.clear() + _refresh_tag_controls() _preset_option.selected = _material_preset _update_physics_labels() _canvas.clear() @@ -820,21 +1042,29 @@ func _do_new() -> void: _refresh_anchor_inspector() _refresh_center_prop_button() _update_save_status() + _refresh_tag_controls() func _on_menu_id_pressed(id: int) -> void: match id: MENU_NEW: _request_new() - MENU_LOAD: - _load_dialog.popup_centered() MENU_SAVE: + if _stage_size_preview_active: + _request_save_after_stage_size(MENU_SAVE) + return if _current_path.is_empty(): _save_dialog.popup_centered() else: _begin_save_request(_current_path, true) MENU_SAVE_AS: + if _stage_size_preview_active: + _request_save_after_stage_size(MENU_SAVE_AS) + return _save_dialog.popup_centered() + MENU_LOAD: + _cancel_stage_size_preview() + _load_dialog.popup_centered() MENU_RESET_VIEWS: _canvas.reset_view() @@ -983,7 +1213,95 @@ func _build_inspector() -> void: _apply_control_theme(_inspector_list) _rebuild_primitive_params() + _build_tag_inspector() _build_anchor_inspector() + _refresh_tag_controls() + + +func _build_stage_authoring_controls() -> void: + var top_bar: HBoxContainer = get_node_or_null(NodePath("TopBar")) as HBoxContainer + if top_bar == null: + return + var name_label: Label = top_bar.get_node_or_null(NodePath("NameLabel")) as Label + if name_label != null: + name_label.text = "Artwork name:" + _prop_name_edit.placeholder_text = "Name this artwork" + _stage_place_button = Button.new() + _stage_place_button.text = "✨ Save & Place as Scenery" + _stage_place_button.tooltip_text = "Save this drawing and put a picture of it on your Stage." + _stage_place_button.custom_minimum_size.y = float(_theme.button_min_height()) + _stage_place_button.size_flags_horizontal = Control.SIZE_SHRINK_END + _stage_place_button.theme = _ui_theme + _stage_place_button.pressed.connect(_request_stage_return) + top_bar.add_child(_stage_place_button) + + +func _apply_authoring_context() -> void: + if not _stage_authoring: + return + if _material_section != null: + _material_section.hide() + if _anchor_section != null: + _anchor_section.hide() + if _attach_tools != null: + _attach_tools.hide() + if _attach_button != null: + _attach_button.hide() + if _show_stickman_guide_check != null: + _show_stickman_guide_check.hide() + if _tag_section != null: + _tag_section.show() + _sync_mode_buttons(_canvas.get_edit_mode()) + _sync_mode_controls(_canvas.get_edit_mode()) + _save_dialog.title = "Save Scenery Artwork" + + +func _build_tag_inspector() -> void: + _tag_section = VBoxContainer.new() + _tag_section.add_theme_constant_override("separation", 4) + _inspector_list.add_child(_tag_section) + var title := Label.new() + title.text = "Artwork Tags" + title.add_theme_font_size_override("font_size", _theme.font_header()) + _tag_section.add_child(title) + var hint := Label.new() + hint.text = "Choose a few so this art is easy to find." + hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _tag_section.add_child(hint) + var rows := FlowContainer.new() + rows.add_theme_constant_override("h_separation", 6) + rows.add_theme_constant_override("v_separation", 4) + _tag_section.add_child(rows) + for index: int in ART_TAG_IDS.size(): + var tag_id: String = ART_TAG_IDS[index] + var check := CheckBox.new() + check.text = ART_TAG_LABELS[index] + check.custom_minimum_size.y = float(_theme.button_min_height()) + check.toggled.connect(_on_art_tag_toggled.bind(tag_id)) + rows.add_child(check) + _tag_checks[tag_id] = check + + +func _on_art_tag_toggled(enabled: bool, tag_id: String) -> void: + if _updating_inspector: + return + if enabled and not _tags.has(tag_id): + _tags.append(tag_id) + elif not enabled: + _tags.erase(tag_id) + _tags = PropFileScript.normalize_tags(_tags) + _mark_dirty() + + +func _refresh_tag_controls() -> void: + if _tag_checks.is_empty(): + return + _updating_inspector = true + for tag_id: String in ART_TAG_IDS: + var check: CheckBox = _tag_checks.get(tag_id) as CheckBox + if check != null: + check.set_pressed_no_signal(_tags.has(tag_id)) + _updating_inspector = false func _build_anchor_inspector() -> void: _anchor_section = VBoxContainer.new() @@ -1313,11 +1631,12 @@ func _refresh_center_prop_button() -> void: _refresh_scale_buttons() func _refresh_scale_buttons() -> void: - if _normalize_size_button == null: + if _size_for_stage_button == null: return - var bounds: Rect2 = _canvas.get_shapes_bounds() + var bounds: Rect2 = _canvas.get_visible_shapes_bounds() var extent: float = maxf(bounds.size.x, bounds.size.y) - _normalize_size_button.disabled = not _canvas.has_shape_geometry() or not is_finite(extent) or extent <= 0.0 + _size_for_stage_button.disabled = not _canvas.has_shape_geometry() or not is_finite(extent) or extent <= 0.0 + _size_for_stage_button.tooltip_text = "Add visible artwork to use Size for Stage." if _size_for_stage_button.disabled else "Compare this drawing with a standing stickman." if _whole_prop_resize_button != null: _whole_prop_resize_button.disabled = _canvas.get_edit_mode() != PropCanvasScript.EditMode.TRANSFORM or not _canvas.has_shape_geometry() @@ -1422,6 +1741,8 @@ func _write_autosave() -> void: func _clear_autosave() -> void: _autosave_remaining = 0.0 + if _stage_authoring: + return if FileAccess.file_exists(AUTOSAVE_PATH): DirAccess.remove_absolute(AUTOSAVE_PATH) @@ -1469,7 +1790,7 @@ func _check_autosave_recovery() -> void: # --------------------------------------------------------------------------- func _build_json_data() -> Dictionary: - return PropFileScript.to_file_dict(_prop_name_edit.text, _material_preset, _canvas.get_shape_data(), _anchors) + return PropFileScript.to_file_dict(_prop_name_edit.text, _material_preset, _canvas.get_shape_data(), _anchors, _tags) func _apply_json_data(json: Variant, is_recovery: bool = false) -> String: @@ -1479,6 +1800,8 @@ func _apply_json_data(json: Variant, is_recovery: bool = false) -> String: if model.is_empty(): return "The file could not be read: unsupported version or malformed data." _prop_name_edit.text = str(model.get("prop_name", "")) + _tags = model.get("tags", []) as Array[String] + _refresh_tag_controls() _material_preset = int(model.get("material_preset", PropBlockScript.MaterialPreset.NONE)) if _preset_option != null: _preset_option.selected = _material_preset @@ -1500,6 +1823,9 @@ func _apply_json_data(json: Variant, is_recovery: bool = false) -> String: func _save_to(path: String, with_thumbnail: bool = true) -> bool: + if _stage_size_preview_active: + _request_save_after_stage_size(MENU_SAVE) + return false path = ContentPathsScript.canonicalize(path) var ok: bool = PropFileScript.save(path, _build_json_data()) if not ok: @@ -1507,7 +1833,8 @@ func _save_to(path: String, with_thumbnail: bool = true) -> bool: return false _current_path = path _dirty = false - _clear_autosave() + if not _stage_authoring: + _clear_autosave() _push_recent(path) _update_save_status() if with_thumbnail: @@ -1554,21 +1881,27 @@ func _execute_pending_save(center_first: bool) -> void: elif _pending_new: _pending_new = false _do_new() + elif _stage_return_pending: + _stage_return_pending = false + artwork_saved.emit(_current_path) + return_requested.emit() func _cancel_pending_save_request() -> void: _pending_save_path = "" _pending_save_with_thumbnail = true _pending_quit = false _pending_new = false + _stage_return_pending = false func _on_save_file_selected(path: String) -> void: path = ContentPathsScript.canonicalize(path) if path.get_extension().to_lower() != "prop": path = path.get_basename() + ".prop" - _begin_save_request(path, not (_pending_quit or _pending_new)) + _begin_save_request(path, not (_pending_quit or _pending_new or _stage_return_pending)) func _load_from(path: String) -> void: + _cancel_stage_size_preview() path = ContentPathsScript.canonicalize(path) var raw: Dictionary = PropFileScript.load(path) if raw.is_empty(): diff --git a/scripts/editors/prop/prop_file.gd b/scripts/editors/prop/prop_file.gd index d0b3fa5..3bd3969 100644 --- a/scripts/editors/prop/prop_file.gd +++ b/scripts/editors/prop/prop_file.gd @@ -2,7 +2,7 @@ class_name PropFile extends RefCounted ## PropFile - Static schema + converters for .prop vector prop assets. ## -## The file stores the editor's full multi-shape stack (version "1.0") plus +## The file stores the editor's full multi-shape stack (version "1.1") plus ## enough provenance for later sandbox placement. flatten_to_payload() converts ## the stack into the single-shape payload PropUtils.spawn_prop() already ## consumes: frontmost closed polygon -> else circle -> else first shape. @@ -12,10 +12,11 @@ const TerrainUtilsScript := preload("res://scripts/terrain/terrain_utils.gd") const PropAnchorDataScript := preload("res://scripts/props/prop_anchor_data.gd") const ContentPathsScript := preload("res://scripts/shared/content_paths.gd") -const FILE_VERSION: String = "1.0" +const FILE_VERSION: String = "1.1" const FILE_EXT: String = ".prop" const DEFAULT_DIR: String = "res://content/props" -const SUPPORTED_VERSIONS: Array[String] = ["1.0"] +const SUPPORTED_VERSIONS: Array[String] = ["1.0", "1.1"] +const KNOWN_TAGS: Array[String] = ["nature", "buildings", "walls", "roads", "objects"] # index == PropBlock.MaterialPreset const PRESET_STRINGS: PackedStringArray = ["none", "wood", "rubber", "cardboard", "metal"] const SHAPE_STRINGS: PackedStringArray = ["polygon", "circle", "line"] @@ -63,7 +64,7 @@ static func sanitize_shape(shape: Dictionary, grid_size: float = TerrainUtilsScr return result -static func to_file_dict(prop_name: String, material_preset: int, shapes: Array[Dictionary], anchors: Dictionary = {}) -> Dictionary: +static func to_file_dict(prop_name: String, material_preset: int, shapes: Array[Dictionary], anchors: Dictionary = {}, tags: Array[String] = []) -> Dictionary: var serialized: Array = [] for shape: Dictionary in shapes: serialized.append(_serialize_shape(shape)) @@ -74,6 +75,7 @@ static func to_file_dict(prop_name: String, material_preset: int, shapes: Array[ "material_preset": preset_to_string(material_preset), "shapes": serialized, "anchors": PropAnchorDataScript.serialize_anchors(anchors), + "tags": normalize_tags(tags), "metadata": {"created_at": now, "modified_at": now}, } @@ -98,6 +100,7 @@ static func from_file_dict(data: Variant) -> Dictionary: return {} var preset: int = preset_from_string(str(dict.get("material_preset", "none"))) + var tags: Array[String] = normalize_tags(dict.get("tags", [])) var shapes: Array[Dictionary] = [] var raw_shapes: Variant = dict.get("shapes", []) if raw_shapes is Array: @@ -109,9 +112,21 @@ static func from_file_dict(data: Variant) -> Dictionary: "material_preset": preset, "shapes": shapes, "anchors": PropAnchorDataScript.normalize_anchors(dict.get("anchors", {})), + "tags": tags, } +static func normalize_tags(value: Variant) -> Array[String]: + var tags: Array[String] = [] + if not value is Array: + return tags + for raw_tag: Variant in value as Array: + var tag: String = str(raw_tag).strip_edges().to_lower() + if KNOWN_TAGS.has(tag) and not tags.has(tag): + tags.append(tag) + return tags + + ## Deterministic conversion of the multi-shape stack into a single ## PropUtils.spawn_prop payload. Rule: frontmost closed polygon -> else circle ## -> else first shape. Returns {} when shapes is empty. diff --git a/scripts/editors/stage/sandbox_stage.gd b/scripts/editors/stage/sandbox_stage.gd index 8800cb6..5b9fe9f 100644 --- a/scripts/editors/stage/sandbox_stage.gd +++ b/scripts/editors/stage/sandbox_stage.gd @@ -49,6 +49,14 @@ const STAGE_ASSET_BROWSER_SCRIPT := preload("res://scripts/editors/stage/stage_a const STAGE_MODAL_VIEWS_SCRIPT := preload("res://scripts/editors/stage/stage_modal_views.gd") const STAGE_THEME_CONTROLLER_SCRIPT := preload("res://scripts/editors/stage/stage_theme_controller.gd") const STAGE_TARGET_CAPTURE_SCRIPT := preload("res://scripts/editors/stage/stage_target_capture_session.gd") +const STAGE_SCENERY_CONTROLLER_SCRIPT := preload("res://scripts/editors/stage/stage_scenery_controller.gd") +const STAGE_SKY_VIEW_SCRIPT := preload("res://scripts/stage/scenery/stage_sky_view.gd") +const STAGE_DRAW_ORDER_SCRIPT := preload("res://scripts/stage/stage_draw_order.gd") +const STAGE_WALL_PANEL_SCRIPT := preload("res://scripts/stage/scenery/stage_wall_panel.gd") +const STAGE_SCENERY_ARTWORK_SCRIPT := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd") +const STAGE_INSPECTOR_PANEL_SCRIPT := preload("res://scripts/shared/stage_ui/stage_inspector_panel.gd") +const STAGE_PANEL_CHOICE_GROUP_SCRIPT := preload("res://scripts/shared/stage_ui/stage_panel_choice_group.gd") +const PROP_EDITOR_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn") # Phase 3c editor tools. const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd") @@ -120,6 +128,8 @@ const RECENT_SCENE_LIMIT: int = STAGE_DOCUMENT_CONTROLLER_SCRIPT.RECENT_LIMIT const DEFAULT_SCENE_NAME: String = "Untitled" const DEFAULT_CAMERA_POSITION: Vector2 = Vector2(0.0, -400.0) const DEFAULT_CAMERA_ZOOM: float = 0.5 +const GROUND_FINISH_IDS: Array[StringName] = [&"finish_ground_grass", &"finish_ground_dirt", &"finish_ground_sidewalk", &"finish_ground_asphalt", &"finish_ground_brick", &"finish_ground_wood", &"finish_ground_paint"] +const GROUND_FINISH_LABELS: Array[String] = ["Grass", "Dirt", "Sidewalk", "Asphalt", "Brick", "Wood", "Paint"] ## Set true temporarily when diagnosing prop interaction flow in the stage. const PROP_TRACE_ENABLED: bool = false @@ -241,6 +251,41 @@ var _gizmos: StageGizmos var _grid: StageGrid var _spatial_index: StageSpatialIndex var _terrain_painter: StageTerrainPainter +var _scenery_controller: StageSceneryController +var _sky_layer: CanvasLayer +var _sky_view: StageSkyView +var _sky_id: StringName = &"sky_neutral" +var _new_ground_finish: StringName = &"finish_ground_grass" +var _hosted_prop_editor: PropEditor +var _hosted_artwork_path: String = "" +var _ground_finish_popup: PopupMenu +var _scenery_edit_mode: bool = false +var _scenery_multi_select: bool = false +var _scenery_single_dragging: bool = false +var _scenery_group_dragging: bool = false +var _scenery_group_mode: StringName = &"" +var _scenery_group_start: Vector2 = Vector2.ZERO +var _scenery_placed_count: int = 0 +var _scenery_inspector: StageInspectorPanel +var _scenery_repair_message: Label +var _scenery_depth_section: VBoxContainer +var _scenery_depth_heading: Label +var _scenery_depth_group: StagePanelChoiceGroup +var _scenery_depth_helper: Label +var _scenery_size_row: HBoxContainer +var _scenery_smaller_button: Button +var _scenery_bigger_button: Button +var _scenery_erase_wall_area_button: Button +var _scenery_find_button: Button +var _scenery_choose_button: Button +var _scenery_keep_button: Button +var _scenery_repair_target: StageSceneryArtwork +var _scenery_repair_candidate: StageSceneryArtwork +var _scenery_repair_path: String = "" +var _missing_scenery_dialog: ConfirmationDialog +var _scenery_repair_confirm_dialog: ConfirmationDialog +var _group_preview_save_dialog: ConfirmationDialog +var _pending_group_save_path: String = "" var _rule_engine: StageRuleEngine var _impact_resolver: StageImpactResolver var _rule_authoring: StageRuleAuthoringController @@ -1240,14 +1285,17 @@ func _ready() -> void: _selection.selection_changed.connect(_on_selection_changed) _selection.hover_changed.connect(_on_hover_changed) + _build_sky_layer() _build_grid_layer() _build_gizmo_layer() _build_director_visuals() _build_navigation() _build_ghost_holder() _build_placement_overlay() + _build_scenery_controller() _terrain_painter = STAGE_TERRAIN_PAINTER.new() _terrain_painter.configure(_spawner, _world, _camera, _ghost_holder, _placement_overlay as StagePlacementOverlay, _spatial_index) + _terrain_painter.set_ground_finish(_new_ground_finish) _terrain_painter.placement_requested.connect(_on_terrain_placement_requested) _terrain_painter.stroke_finished.connect(_on_terrain_stroke_finished) add_child(_terrain_painter) @@ -1285,6 +1333,8 @@ func _ready() -> void: func _process(delta: float) -> void: + if _hosted_prop_editor != null: + return _update_speech_composition() if _speech_session != null: _speech_session.update(delta) @@ -1315,6 +1365,8 @@ func _process(delta: float) -> void: func _physics_process(delta: float) -> void: + if _hosted_prop_editor != null: + return if _impact_resolver != null: _impact_resolver.finish_sweep_tick(Engine.get_physics_frames()) _update_throw_preview_cache() @@ -1348,6 +1400,8 @@ func _update_throw_preview_cache() -> void: # --------------------------------------------------------------------------- func _input(event: InputEvent) -> void: + if _hosted_prop_editor != null: + return if _speech_session != null: _speech_session.set_camera_panning(_panning) if _speech_session.handle_input(event): @@ -1423,6 +1477,8 @@ func _update_speech_badge_cursor() -> void: else: _apply_cursor() func _unhandled_input(event: InputEvent) -> void: + if _hosted_prop_editor != null: + return if _speech_session != null and _speech_session.handle_unhandled_input(event): if _speech_session.should_mark_input_handled(): get_viewport().set_input_as_handled() @@ -1439,6 +1495,8 @@ func _place_speech_bubble_at(viewport_position: Vector2) -> void: func _unhandled_key_input(event: InputEvent) -> void: + if _hosted_prop_editor != null: + return if not (event is InputEventKey) or not event.pressed or event.echo: return if _speech_composer_is_open(): @@ -1449,6 +1507,29 @@ func _unhandled_key_input(event: InputEvent) -> void: _speech_style_toolbar.call("close_chooser") get_viewport().set_input_as_handled() return + if event.keycode == KEY_ESCAPE: + if _scenery_controller != null and _scenery_controller.is_active(): + _scenery_controller.handle_key(event as InputEventKey) + _hide_scenery_capture_bar() + get_viewport().set_input_as_handled() + return + if _scenery_controller != null and _scenery_controller.is_single_transform_active(): + _scenery_controller.cancel_single_transform() + _scenery_single_dragging = false + _refresh_scenery_inspector() + _refresh_scenery_capture_bar() + get_viewport().set_input_as_handled() + return + if _scenery_controller != null and _scenery_controller.is_group_preview_pending(): + _scenery_controller.cancel_group() + _scenery_group_mode = &"" + _refresh_scenery_capture_bar() + get_viewport().set_input_as_handled() + return + if _scenery_edit_mode: + _exit_scenery_edit_mode() + get_viewport().set_input_as_handled() + return match event.keycode: KEY_DELETE, KEY_BACKSPACE: if current_mode == StageMode.EDIT: @@ -1523,7 +1604,7 @@ func _space_should_pass_to_gui() -> bool: var focus: Control = get_viewport().gui_get_focus_owner() if focus is LineEdit or focus is TextEdit or focus is SpinBox: return true - for window: Window in [_browse_dialog, _speak_dialog, _wait_dialog, _scene_open_dialog, _scene_save_dialog, _speech_save_gate_dialog, _rename_scene_dialog, _unsaved_changes_dialog, _missing_assets_dialog, _cli_error_dialog, _confirm_dialog]: + for window: Window in [_browse_dialog, _speak_dialog, _wait_dialog, _scene_open_dialog, _scene_save_dialog, _speech_save_gate_dialog, _group_preview_save_dialog, _scenery_repair_confirm_dialog, _missing_scenery_dialog, _rename_scene_dialog, _unsaved_changes_dialog, _missing_assets_dialog, _cli_error_dialog, _confirm_dialog]: if window != null and window.visible: return true if _selector_open or (_queue_panel != null and _queue_panel.visible) or (_rule_panel != null and _rule_panel.visible) or (_action_editor != null and _action_editor.visible) or (_rule_editor != null and _rule_editor.visible): @@ -1563,6 +1644,32 @@ func _handle_world_click(mb: InputEventMouseButton) -> void: return if current_mode == StageMode.PLAY: return + if _scenery_controller != null and _scenery_controller.is_active(): + var scenery_world_position: Vector2 = _viewport_to_world(mb.position) + if _scenery_controller.handle_mouse_button(mb, scenery_world_position, _is_mouse_over_ui()): + get_viewport().set_input_as_handled() + _refresh_scenery_capture_bar() + return + if _scenery_edit_mode: + var edit_world_position: Vector2 = _viewport_to_world(mb.position) + if mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed: + if _scenery_controller != null and _scenery_controller.is_single_transform_active(): + _scenery_controller.cancel_single_transform() + _scenery_single_dragging = false + _refresh_scenery_inspector() + _refresh_scenery_capture_bar() + elif _scenery_controller != null and _scenery_controller.is_group_preview_pending(): + _scenery_controller.cancel_group() + _scenery_group_mode = &"" + _refresh_scenery_capture_bar() + else: + _exit_scenery_edit_mode() + get_viewport().set_input_as_handled() + return + if mb.button_index == MOUSE_BUTTON_LEFT: + _handle_scenery_edit_click(mb, edit_world_position) + get_viewport().set_input_as_handled() + return if _direct_move_target.is_finite(): if mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed: _cancel_direct_move_draft() @@ -1651,6 +1758,13 @@ func _handle_mouse_motion(mm: InputEventMouseMotion) -> void: if current_mode == StageMode.PLAY: return var world_pos := _viewport_to_world(mm.position) + if _scenery_controller != null and _scenery_controller.is_active(): + if not _is_mouse_over_ui(): + _scenery_controller.handle_mouse_motion(world_pos) + return + if _scenery_edit_mode: + _handle_scenery_edit_motion(world_pos) + return if _gizmos.is_dragging(): _gizmos.drag_to(world_pos) _director_visuals.mark_dirty() @@ -1681,6 +1795,14 @@ func set_mode(mode: StageMode) -> void: _show_toast("Finish or cancel speech editing first.") _sync_mode_buttons() return + if _scenery_controller != null: + if _scenery_controller.is_group_preview_pending(): + _scenery_controller.cancel_group() + if _scenery_controller.is_active(): + _scenery_controller.exit_tool() + if _scenery_edit_mode: + _exit_scenery_edit_mode() + _hide_scenery_capture_bar() var previous_mode: StageMode = current_mode _cancel_edit_wobble_preview() if mode == StageMode.PLAY and _run_state == RunState.STOPPED and not _cli_startup: @@ -1885,7 +2007,7 @@ func _pause_toggle_blocked() -> bool: var focus: Control = get_viewport().gui_get_focus_owner() if focus is LineEdit or focus is TextEdit or focus is SpinBox: return true - for dialog: Window in [_browse_dialog, _speak_dialog, _wait_dialog, _scene_open_dialog, _scene_save_dialog, _speech_save_gate_dialog, _rename_scene_dialog, _unsaved_changes_dialog, _missing_assets_dialog, _cli_error_dialog, _confirm_dialog]: + for dialog: Window in [_browse_dialog, _speak_dialog, _wait_dialog, _scene_open_dialog, _scene_save_dialog, _speech_save_gate_dialog, _group_preview_save_dialog, _scenery_repair_confirm_dialog, _missing_scenery_dialog, _rename_scene_dialog, _unsaved_changes_dialog, _missing_assets_dialog, _cli_error_dialog, _confirm_dialog]: if dialog != null and dialog.visible: return true return (_queue_panel != null and _queue_panel.visible) or (_action_editor != null and _action_editor.visible) or (_rule_editor != null and _rule_editor.visible) @@ -1928,6 +2050,12 @@ func _restore_authored_state() -> void: # --------------------------------------------------------------------------- func set_placement_mode(id: String) -> void: + if id != "": + if _scenery_controller != null and _scenery_controller.is_active(): + _scenery_controller.exit_tool() + _hide_scenery_capture_bar() + if _scenery_edit_mode: + _exit_scenery_edit_mode() _placement_id = id for pid: String in _palette_buttons: var btn: Button = _palette_buttons[pid] @@ -2080,6 +2208,8 @@ func _spawn_ghost() -> void: var ghost := _spawner.spawn(_placement_id, Vector2.ZERO) if ghost == null: return + if ghost is TerrainBlock and (ghost as TerrainBlock).spawn_id == "ground": + (ghost as TerrainBlock).set_finish(_new_ground_finish) # Reparent out of World so the ghost is not selectable/counted. if ghost.get_parent() == _world: _world.remove_child(ghost) @@ -2346,12 +2476,27 @@ func _collect_scene_data(is_autosave: bool = false) -> Dictionary: var camera_state: Dictionary = {"position": _camera.global_position, "zoom": _camera.zoom.x} var document_metadata: Dictionary = { "scene_name": _scene_name, + "sky_id": String(_sky_id), "metadata": _document.metadata_for_snapshot(is_autosave), } return _scene_adapter.collect(_world, _authored, _event_rules, camera_state, document_metadata) func _save_scene_to_path(path: String, is_autosave: bool = false) -> bool: path = CONTENT_PATHS.canonicalize(path) + if _scenery_controller != null and _scenery_controller.is_single_transform_active(): + if is_autosave: + return false + _scenery_controller.cancel_single_transform() + _scenery_single_dragging = false + _refresh_scenery_inspector() + _refresh_scenery_capture_bar() + if _scenery_controller != null and _scenery_controller.is_group_preview_pending(): + if is_autosave: + return false + _pending_group_save_path = path + if _group_preview_save_dialog != null and not _group_preview_save_dialog.visible: + _group_preview_save_dialog.popup_centered() + return false if _speech_session != null and _speech_session.is_save_blocked(): if is_autosave: return false @@ -2386,12 +2531,43 @@ func _save_scene_to_path(path: String, is_autosave: bool = false) -> bool: _speech_recovery_matches_loaded_scene() return true + +func _on_group_preview_save_confirmed() -> void: + var path: String = _pending_group_save_path + _pending_group_save_path = "" + if _scenery_controller != null and _scenery_controller.is_group_preview_pending(): + _scenery_controller.commit_group() + _scenery_group_mode = &"" + _refresh_scenery_capture_bar() + if not path.is_empty() and _save_scene_to_path(path) and _pending_file_action != PendingFileAction.NONE: + _continue_pending_file_action() + + +func _on_group_preview_save_cancelled() -> void: + _pending_group_save_path = "" + if _pending_file_action != PendingFileAction.NONE: + _pending_file_action = PendingFileAction.NONE + _pending_open_path = "" + func _validate_queues_for_save() -> String: return "" if _document == null else _document.validate_queues(_world) func _reset_scene_content() -> void: set_mode(StageMode.EDIT) + if _scenery_repair_confirm_dialog != null: + _scenery_repair_confirm_dialog.hide() + _cancel_scenery_repair_preview() + _scenery_repair_target = null + _sky_id = &"sky_neutral" + if _sky_view != null: + _sky_view.set_sky(_sky_id) + if _scenery_controller != null: + _scenery_controller.exit_tool() + _scenery_controller.cancel_group() + _hide_scenery_capture_bar() + _scenery_edit_mode = false + _selection.set_scope(StageSelection.SelectionScope.ACTION) _free_ghost(); _free_terrain_ghosts(); _end_terrain_drag() _selection.clear_selection(); _selection.clear_hover(); _authored.clear(); _edit_physics_targets.clear(); _event_rules.clear(); _next_rule_id = 0 _clear_director_pending(); _reset_rule_builder(); _nav_dirty = false @@ -2421,6 +2597,9 @@ func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool) _suppress_dirty = true _reset_scene_content() var adapter_result: Dictionary = _scene_adapter.apply(data, _world, _spawner) + _sky_id = StringName(String(adapter_result.get("sky_id", "sky_neutral"))) + if _sky_view != null: + _sky_view.set_sky(_sky_id) var loaded_nodes: Array[Node2D] = adapter_result.get("loaded_nodes", []) for node: Node2D in loaded_nodes: _finalize_placed_node(node) @@ -2430,6 +2609,10 @@ func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool) var warnings: Array[String] = adapter_result.get("warnings", []) var missing_stickmen: int = int(adapter_result.get("missing_stickmen", 0)) var missing_paths: Array[String] = adapter_result.get("missing_paths", []) + var missing_scenery_count: int = 0 + for loaded_node: Node2D in loaded_nodes: + if loaded_node.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT and (loaded_node as StageSceneryArtwork).missing: + missing_scenery_count += 1 var decoded_rules: Dictionary = adapter_result.get("decoded_rules", {}) _event_rules.assign(decoded_rules.get("rules", [])) _next_rule_id = 0 @@ -2461,6 +2644,8 @@ func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool) _show_scene_load_warnings(warnings) if not missing_paths.is_empty(): _show_missing_assets_dialog(missing_paths) + if missing_scenery_count > 0: + _show_missing_scenery_dialog(missing_scenery_count) if not is_recovery: _add_recent_scene(source_path) if _document != null: @@ -2494,6 +2679,13 @@ func _show_missing_assets_dialog(paths: Array[String]) -> void: _missing_assets_dialog.popup_centered() +func _show_missing_scenery_dialog(count: int) -> void: + if _missing_scenery_dialog == null: + return + _missing_scenery_dialog.dialog_text = "%d scenery picture%s could not be found. The scene still has a marker in each spot, so nothing was lost." % [count, "" if count == 1 else "s"] + _missing_scenery_dialog.popup_centered() + + func _add_recent_scene(path: String) -> void: if _document == null: return @@ -2587,7 +2779,10 @@ func _notification(what: int) -> void: _space_key_down = false _space_press_owned = false if what == NOTIFICATION_WM_CLOSE_REQUEST: - _request_pending_file_action(PendingFileAction.QUIT) + if _hosted_prop_editor != null and is_instance_valid(_hosted_prop_editor): + _hosted_prop_editor.call("_request_stage_return") + else: + _request_pending_file_action(PendingFileAction.QUIT) func _refresh_status() -> void: if _chrome == null: @@ -2754,6 +2949,7 @@ func _build_ui() -> void: _modal_views.configure(_sandbox_theme as SandboxTheme, _ui_font) _modal_views.object_selected.connect(_on_objects_here_selected) _build_prop_settings_view(ui) + _build_scenery_inspector(ui) _build_set_down_cancel_button(ui) _build_use_target_capture_bar(ui) _build_use_spot_overlay(ui) @@ -2828,14 +3024,23 @@ func _build_ui() -> void: _asset_browser.asset_selected_requested.connect(_on_asset_selected) _asset_browser.selector_cancel_requested.connect(_on_selector_cancelled) _asset_browser.asset_load_failed.connect(func(kind: String, path: String) -> void: - _show_toast("Could not load %s: %s" % [kind, path]) + if _scenery_repair_target != null and is_instance_valid(_scenery_repair_target): + _show_toast("Could not open that artwork. Try another file.") + else: + _show_toast("Could not load %s: %s" % [kind, path]) ) _asset_browser.refresh_requested.connect(_on_refresh_requested) + _asset_browser.make_artwork_requested.connect(_open_stage_artwork_editor) _asset_browser.build_ui(ui, _ui_font, _emoji_font, _sandbox_theme) + _asset_browser.edit_scenery_requested.connect(_enter_scenery_edit_mode) + _build_ground_finish_popup(ui) _modal_views.build_file_dialogs(ui) _scene_open_dialog = _modal_views.scene_open_dialog _scene_save_dialog = _modal_views.scene_save_dialog + _group_preview_save_dialog = _modal_views.group_preview_save_dialog + _group_preview_save_dialog.confirmed.connect(_on_group_preview_save_confirmed) + _group_preview_save_dialog.canceled.connect(_on_group_preview_save_cancelled) _speech_save_gate_dialog = _modal_views.speech_save_gate_dialog _speech_save_gate_dialog.confirmed.connect(_on_speech_save_gate_confirmed) _speech_save_gate_dialog.canceled.connect(_on_speech_save_gate_cancelled) @@ -2903,6 +3108,111 @@ func _build_prop_settings_view(ui: CanvasLayer) -> void: ui.add_child(_prop_settings_view) _prop_settings_view.update_layout(_prop_settings_safe_rect()) + +func _build_scenery_inspector(ui: CanvasLayer) -> void: + _scenery_inspector = STAGE_INSPECTOR_PANEL_SCRIPT.new() as StageInspectorPanel + _scenery_inspector.name = "SceneryRepairCard" + _scenery_inspector.visible = false + ui.add_child(_scenery_inspector) + _scenery_inspector.apply_theme(_sandbox_theme as SandboxTheme) + _scenery_inspector.close_requested.connect(_keep_scenery_repair_for_now) + var body: VBoxContainer = _scenery_inspector.body() + body.add_theme_constant_override("separation", 10) + _scenery_repair_message = Label.new() + _scenery_repair_message.name = "MissingArtworkMessage" + _scenery_repair_message.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + body.add_child(_scenery_repair_message) + _scenery_depth_section = VBoxContainer.new() + _scenery_depth_section.name = "SceneryDepthSection" + _scenery_depth_section.add_theme_constant_override("separation", 6) + _scenery_depth_heading = Label.new() + _scenery_depth_heading.name = "SceneryDepthHeading" + _scenery_depth_heading.text = "Where it appears" + _scenery_depth_heading.add_theme_font_size_override("font_size", _stage_panel_integer("type.section_size", 20, 12, 40)) + _scenery_depth_heading.add_theme_color_override("font_color", _stage_panel_color("type.text_color", Color("#F5F7FA"))) + _apply_ui_font(_scenery_depth_heading) + _scenery_depth_section.add_child(_scenery_depth_heading) + _scenery_depth_group = STAGE_PANEL_CHOICE_GROUP_SCRIPT.new() as StagePanelChoiceGroup + _scenery_depth_group.name = "SceneryDepthChoices" + _scenery_depth_group.add_choice(&"far", "Far") + _scenery_depth_group.add_choice(&"near", "Near") + _scenery_depth_group.add_choice(&"floor", "On the floor") + _scenery_depth_group.apply_theme(_sandbox_theme as SandboxTheme) + _scenery_depth_group.choice_selected.connect(_on_selected_scenery_depth_choice) + _scenery_depth_section.add_child(_scenery_depth_group) + _scenery_depth_helper = Label.new() + _scenery_depth_helper.name = "SceneryDepthHelper" + _scenery_depth_helper.text = "Far is behind Near. On the floor is in front of the ground." + _scenery_depth_helper.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _scenery_depth_helper.add_theme_font_size_override("font_size", _stage_panel_integer("type.helper_size", 16, 12, 32)) + _scenery_depth_helper.add_theme_color_override("font_color", _stage_panel_color("type.muted_text_color", Color("#AAB2C0"))) + _apply_ui_font(_scenery_depth_helper) + _scenery_depth_section.add_child(_scenery_depth_helper) + body.add_child(_scenery_depth_section) + _scenery_size_row = HBoxContainer.new() + _scenery_size_row.name = "ScenerySizeActions" + _scenery_size_row.add_theme_constant_override("separation", 8) + _scenery_smaller_button = _make_scenery_repair_button("ScenerySmaller", "Smaller", false) + _scenery_smaller_button.pressed.connect(func() -> void: _step_selected_scenery_size(1.0 / 1.1)) + _scenery_size_row.add_child(_scenery_smaller_button) + _scenery_bigger_button = _make_scenery_repair_button("SceneryBigger", "Bigger", true) + _scenery_bigger_button.pressed.connect(func() -> void: _step_selected_scenery_size(1.1)) + _scenery_size_row.add_child(_scenery_bigger_button) + body.add_child(_scenery_size_row) + _scenery_erase_wall_area_button = _make_scenery_repair_button("EraseWallArea", "Erase wall area", true) + _scenery_erase_wall_area_button.pressed.connect(_begin_selected_wall_erase) + body.add_child(_scenery_erase_wall_area_button) + _scenery_find_button = _make_scenery_repair_button("FindArtwork", "Find artwork…", true) + _scenery_find_button.pressed.connect(func() -> void: _begin_scenery_repair(true)) + body.add_child(_scenery_find_button) + _scenery_choose_button = _make_scenery_repair_button("ChooseArtwork", "Choose another artwork", false) + _scenery_choose_button.pressed.connect(func() -> void: _begin_scenery_repair(false)) + body.add_child(_scenery_choose_button) + _scenery_keep_button = _make_scenery_repair_button("KeepMissingArtwork", "Keep for now", false) + _scenery_keep_button.pressed.connect(_keep_scenery_repair_for_now) + body.add_child(_scenery_keep_button) + + _missing_scenery_dialog = ConfirmationDialog.new() + _missing_scenery_dialog.name = "MissingSceneryDialog" + _missing_scenery_dialog.title = "Artwork not found" + _missing_scenery_dialog.dialog_text = "This scene has artwork that could not be found. You can still explore the scene and fix it later." + _missing_scenery_dialog.ok_button_text = "Show Scenery" + _missing_scenery_dialog.cancel_button_text = "Later" + _missing_scenery_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(620, 300)) + _missing_scenery_dialog.confirmed.connect(_enter_scenery_edit_mode) + ui.add_child(_missing_scenery_dialog) + _apply_dialog_theme(_missing_scenery_dialog) + _scenery_repair_confirm_dialog = ConfirmationDialog.new() + _scenery_repair_confirm_dialog.name = "SceneryRepairConfirmDialog" + _scenery_repair_confirm_dialog.title = "Use this artwork here?" + _scenery_repair_confirm_dialog.ok_button_text = "Use artwork" + _scenery_repair_confirm_dialog.cancel_button_text = "Keep for now" + _scenery_repair_confirm_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(620, 300)) + _scenery_repair_confirm_dialog.confirmed.connect(_confirm_scenery_repair) + _scenery_repair_confirm_dialog.canceled.connect(_cancel_scenery_repair_preview) + ui.add_child(_scenery_repair_confirm_dialog) + _apply_dialog_theme(_scenery_repair_confirm_dialog) + if not get_viewport().size_changed.is_connected(_refresh_scenery_inspector_layout): + get_viewport().size_changed.connect(_refresh_scenery_inspector_layout) + + +func _make_scenery_repair_button(node_name: String, label: String, prominent: bool) -> Button: + var button: Button = Button.new() + button.name = node_name + button.text = label + button.focus_mode = Control.FOCUS_ALL + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button.custom_minimum_size.y = float(_stage_panel_integer("action.min_target_size", 48, 44, 96)) + _scenery_inspector.style_action_button(button, prominent) + return button + + +func _refresh_scenery_inspector_layout() -> void: + if _scenery_inspector != null and _scenery_inspector.visible: + _scenery_inspector.set_safe_rect(_prop_settings_safe_rect()) + if _scenery_edit_mode: + _refresh_scenery_capture_bar() + func _build_set_down_cancel_button(ui: CanvasLayer) -> void: var button_height: float = maxf(44.0, float(_stage_panel_integer("action.min_target_size", 48, 44, 96))) var palette_config: Dictionary = _theme.get("palette", {}) as Dictionary @@ -2930,9 +3240,47 @@ func _build_use_target_capture_bar(ui: CanvasLayer) -> void: _use_target_capture_bar.configure(_sandbox_theme as SandboxTheme, TOP_BAR_HEIGHT) _use_target_capture_bar.cancel_requested.connect(_cancel_capture) _use_target_capture_bar.change_prop_requested.connect(_choose_different_use_prop) + _use_target_capture_bar.capture_action_requested.connect(_on_capture_action_requested) ui.add_child(_use_target_capture_bar) +func _build_ground_finish_popup(ui: CanvasLayer) -> void: + _ground_finish_popup = PopupMenu.new() + _ground_finish_popup.name = "NewGroundLook" + _ground_finish_popup.theme = _sandbox_theme.control_theme() if _sandbox_theme != null else null + for index: int in GROUND_FINISH_LABELS.size(): + _ground_finish_popup.add_item(GROUND_FINISH_LABELS[index], index) + _ground_finish_popup.set_item_as_checkable(index, true) + _ground_finish_popup.id_pressed.connect(_on_ground_finish_selected) + ui.add_child(_ground_finish_popup) + + +func _open_ground_finish_popup() -> void: + if _ground_finish_popup == null: + return + for index: int in GROUND_FINISH_IDS.size(): + _ground_finish_popup.set_item_checked(index, GROUND_FINISH_IDS[index] == _new_ground_finish) + var origin: Vector2 = get_viewport().get_mouse_position() + var ground_button: Button = _palette_buttons.get("ground") as Button + if ground_button != null: + var button_rect: Rect2 = ground_button.get_global_rect() + origin = Vector2(button_rect.position.x, button_rect.end.y) + var viewport_size: Vector2 = get_viewport_rect().size + origin.x = clampf(origin.x, 8.0, maxf(8.0, viewport_size.x - 288.0)) + origin.y = clampf(origin.y, 8.0, maxf(8.0, viewport_size.y - 320.0)) + _ground_finish_popup.popup(Rect2i(Vector2i(origin), Vector2i(280, 312))) + + +func _on_ground_finish_selected(index: int) -> void: + if index < 0 or index >= GROUND_FINISH_IDS.size(): + return + _new_ground_finish = GROUND_FINISH_IDS[index] + if _terrain_painter != null: + _terrain_painter.set_ground_finish(_new_ground_finish) + set_placement_mode("ground") + _show_toast("New Ground look: %s" % GROUND_FINISH_LABELS[index]) + + func _build_use_spot_overlay(ui: CanvasLayer) -> void: _use_spot_overlay = STAGE_USE_SPOT_OVERLAY_SCRIPT.new() as StageUseSpotOverlay _use_spot_overlay.name = "UseSpotOverlay" @@ -3667,6 +4015,28 @@ func _build_ghost_holder() -> void: add_child(_ghost_holder) +func _build_scenery_controller() -> void: + _scenery_controller = STAGE_SCENERY_CONTROLLER_SCRIPT.new() as StageSceneryController + _scenery_controller.name = "StageSceneryController" + _scenery_controller.configure(_world, _ghost_holder, _camera) + _scenery_controller.placement_committed.connect(_on_scenery_placement_committed) + _scenery_controller.document_change_requested.connect(_mark_scene_dirty) + _scenery_controller.preview_state_changed.connect(_on_scenery_preview_state_changed) + add_child(_scenery_controller) + + +func _build_sky_layer() -> void: + _sky_layer = CanvasLayer.new() + _sky_layer.name = "StageSkyLayer" + _sky_layer.layer = -1 + _sky_view = STAGE_SKY_VIEW_SCRIPT.new() as StageSkyView + _sky_view.name = "StageSkyView" + _sky_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _sky_view.set_sky(_sky_id) + _sky_layer.add_child(_sky_view) + add_child(_sky_layer) + + func _build_placement_overlay() -> void: _placement_overlay = STAGE_PLACEMENT_OVERLAY.new() _placement_overlay.name = "PlacementOverlay" @@ -3770,9 +4140,12 @@ func _on_mode_segment_toggled(pressed: bool, mode: StageMode) -> void: func _on_palette_toggled(pressed: bool, id: String) -> void: if pressed: - if id == "stickman" or id == "prop": + if id == "stickman" or id == "prop" or id == "scenery": _open_selector(id) return + if id == "ground": + _open_ground_finish_popup() + return set_placement_mode(id) else: if _selector_open and _selector_kind == id: @@ -3786,6 +4159,8 @@ func _on_palette_card_activated(payload: Dictionary) -> void: if id.is_empty(): return if bool(payload.get("opens_selector", false)): _open_selector(id) + elif id == "ground": + _open_ground_finish_popup() else: set_placement_mode(id) @@ -3833,9 +4208,20 @@ func _is_stage_drop_position(viewport_pos: Vector2) -> bool: func _open_selector(kind: String) -> void: + if kind == "scenery": + if _scenery_edit_mode: + _exit_scenery_edit_mode() + if _scenery_controller != null and _scenery_controller.is_active(): + _scenery_controller.exit_tool() + _scenery_placed_count = 0 + elif _scenery_edit_mode: + _exit_scenery_edit_mode() + if _scenery_controller != null and _scenery_controller.is_group_preview_pending(): + _scenery_controller.cancel_group() + _scenery_group_mode = &"" for pid: String in _palette_buttons: (_palette_buttons[pid] as Button).set_pressed_no_signal(pid == kind) - var selector_state: Dictionary = _asset_browser.open_selector(kind, _spawner.get_selected_stickman_path()) + var selector_state: Dictionary = _asset_browser.open_selector(kind, _spawner.get_selected_stickman_path(), _sky_id) if kind == "stickman": var selected_stickman_path: String = String(selector_state.get("selected_stickman_path", _spawner.get_selected_stickman_path())) if selected_stickman_path != _spawner.get_selected_stickman_path(): @@ -3847,6 +4233,47 @@ func _open_selector(kind: String) -> void: func _on_asset_selected(entry: Dictionary) -> void: var kind := _selector_kind + if kind == "starters": + _load_starter_scene(entry) + return + if kind == "scenery": + if _scenery_repair_target != null and is_instance_valid(_scenery_repair_target): + if not String(entry.get("background_kind", "")).is_empty(): + var repair_selector: AssetSelector = _asset_browser.get_selector() + if repair_selector != null: + repair_selector.set_current_sky(_sky_id) + _show_toast("Choose a drawing from Artwork to repair this spot.") + return + _close_selector() + _start_scenery_repair_preview(entry) + return + var background_kind: String = String(entry.get("background_kind", "")) + if background_kind == "sky": + _sky_id = StringName(String(entry.get("id", "sky_neutral"))) + _sky_view.set_sky(_sky_id) + var scenery_selector: AssetSelector = _asset_browser.get_selector() + if scenery_selector != null: + scenery_selector.set_current_sky(_sky_id) + _mark_scene_dirty() + _refresh_scenery_capture_bar() + return + if background_kind == "wall": + _close_selector() + if _scenery_controller != null: + _scenery_controller.begin_wall( + StringName(String(entry.get("finish_id", "finish_wall_paint"))), + _viewport_to_world(get_viewport().get_mouse_position()) + ) + _refresh_scenery_capture_bar() + return + var art_path: String = String(entry.get("path", entry.get("id", ""))) + _close_selector() + if _scenery_controller == null or not _scenery_controller.begin_artwork(art_path, &"near"): + _show_toast("Could not open that artwork. Try another file.") + return + _scenery_placed_count = 0 + _refresh_scenery_capture_bar() + return if kind == "stickman": _spawner.selected_stickman_path = String(entry.get("path", "")) else: @@ -3855,6 +4282,547 @@ func _on_asset_selected(entry: Dictionary) -> void: set_placement_mode(kind) +func _load_starter_scene(entry: Dictionary) -> void: + var path: String = CONTENT_PATHS.canonicalize(String(entry.get("path", ""))) + var read_result: Dictionary = _document.read(path) if _document != null else {} + if not bool(read_result.get("ok", false)): + _show_toast(String(read_result.get("error", "Could not open this starter scene."))) + return + var data: Dictionary = read_result.get("data", {}) as Dictionary + var raw_metadata: Variant = data.get("metadata", {}) + var metadata: Dictionary = raw_metadata as Dictionary if raw_metadata is Dictionary else {} + _close_selector() + if not _apply_scene_data(data, "", false): + _show_toast("Could not open this starter scene.") + return + if _document != null: + _document.accept_loaded_document("", String(entry.get("name", data.get("scene_name", "New Scene"))), int(metadata.get("created_unix", Time.get_unix_time_from_system())), "", false) + _document.remove_autosave() + _set_scene_dirty(true) + _refresh_status() + + +func _open_stage_artwork_editor() -> void: + if _hosted_prop_editor != null or _selector_kind != "scenery": + return + _close_selector() + if _scenery_controller != null and _scenery_controller.is_active(): + _scenery_controller.exit_tool() + set_placement_mode("") + _hosted_artwork_path = "" + var editor: PropEditor = PROP_EDITOR_SCENE.instantiate() as PropEditor + if editor == null: + _show_toast("Could not open the artwork editor.") + return + editor.configure_stage_authoring() + editor.artwork_saved.connect(_on_hosted_artwork_saved) + editor.return_requested.connect(_on_hosted_artwork_return) + _hosted_prop_editor = editor + var ui_layer: CanvasLayer = _chrome.get_canvas_layer() + ui_layer.add_child(editor) + set_process(false) + set_physics_process(false) + set_process_input(false) + set_process_unhandled_input(false) + set_process_unhandled_key_input(false) + + +func _on_hosted_artwork_saved(path: String) -> void: + _hosted_artwork_path = CONTENT_PATHS.canonicalize(path) + + +func _on_hosted_artwork_return() -> void: + var editor: PropEditor = _hosted_prop_editor + _hosted_prop_editor = null + if editor != null and is_instance_valid(editor): + editor.hide() + editor.queue_free() + set_process(true) + set_physics_process(true) + set_process_input(true) + set_process_unhandled_input(true) + set_process_unhandled_key_input(true) + var saved_path: String = _hosted_artwork_path + _hosted_artwork_path = "" + if saved_path.is_empty(): + _open_selector("scenery") + return + if _scenery_controller == null or not _scenery_controller.begin_artwork(saved_path, &"near"): + _show_toast("Artwork saved, but could not be placed. Choose it from Scenery to try again.") + _open_selector("scenery") + return + _scenery_placed_count = 0 + _refresh_scenery_capture_bar() + + +func _refresh_scenery_inspector() -> void: + if _scenery_inspector == null: + return + if _scenery_depth_section != null: + _scenery_depth_section.visible = false + if _scenery_depth_group != null: + _scenery_depth_group.clear_no_signal() + var selected: Array[Node2D] = _selection.get_selected() if _selection != null else [] + if not _scenery_edit_mode or selected.size() != 1 or not _is_scenery_node(selected[0]): + if _scenery_repair_candidate != null: + _cancel_scenery_repair_preview() + _scenery_repair_target = null + _scenery_inspector.hide() + return + var node: Node2D = selected[0] + var is_artwork: bool = node.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT + var artwork: StageSceneryArtwork = node as StageSceneryArtwork if is_artwork else null + var is_missing: bool = artwork != null and artwork.missing + _scenery_depth_section.visible = is_artwork + if artwork != null: + _scenery_depth_group.select_no_signal(artwork.depth) + _scenery_size_row.visible = not is_missing or artwork.has_known_bounds + _scenery_smaller_button.disabled = is_missing and not artwork.has_known_bounds + _scenery_bigger_button.disabled = is_missing and not artwork.has_known_bounds + _scenery_erase_wall_area_button.visible = not is_artwork + _scenery_find_button.visible = is_missing + _scenery_choose_button.visible = is_missing + _scenery_keep_button.visible = is_missing + _scenery_repair_message.visible = is_missing + if is_missing and artwork != null: + _scenery_repair_target = artwork + _scenery_inspector.set_header("Artwork not found", "🖼️") + _scenery_repair_message.text = "“%s” is missing its picture file. Choose artwork to put it back in this spot." % artwork.display_name + else: + _scenery_repair_target = null + var scenery_name: String = (artwork.display_name if artwork != null else "Wall panel") + _scenery_inspector.set_header(scenery_name, "🖼️" if artwork != null else "🧱") + _scenery_repair_message.text = "Make this scenery smaller or bigger. The other scenery stays where it is." + _scenery_inspector.set_safe_rect(_prop_settings_safe_rect()) + _scenery_inspector.visible = true + _scenery_inspector.request_fit() + + +func _on_selected_scenery_depth_choice(id: StringName) -> void: + if not _scenery_edit_mode or current_mode != StageMode.EDIT or id not in [&"far", &"near", &"floor"]: + _refresh_scenery_inspector() + return + var selected: Array[Node2D] = _selection.get_selected() if _selection != null else [] + if selected.size() != 1 or selected[0] == null or not is_instance_valid(selected[0]) or selected[0].get_script() != STAGE_SCENERY_ARTWORK_SCRIPT: + _refresh_scenery_inspector() + return + var artwork: StageSceneryArtwork = selected[0] as StageSceneryArtwork + if artwork == null or not is_instance_valid(artwork): + _refresh_scenery_inspector() + return + if artwork.depth == id: + _scenery_depth_group.select_no_signal(artwork.depth) + return + artwork.set_depth(id) + _mark_scene_dirty() + _refresh_scenery_inspector() + + +func _is_scenery_node(node: Node2D) -> bool: + return node != null and (node.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT or node.get_script() == STAGE_WALL_PANEL_SCRIPT) + + +func _step_selected_scenery_size(factor: float) -> void: + var selected: Array[Node2D] = _selection.get_selected() if _selection != null else [] + if selected.size() != 1 or not _is_scenery_node(selected[0]) or _scenery_controller == null: + return + if not _scenery_controller.step_single_size(selected[0], factor): + _show_toast("This scenery piece cannot be resized any further.") + return + _refresh_scenery_inspector() + _refresh_scenery_capture_bar() + + +func _begin_selected_wall_erase() -> void: + var selected: Array[Node2D] = _selection.get_selected() if _selection != null else [] + if selected.size() != 1 or selected[0].get_script() != STAGE_WALL_PANEL_SCRIPT or _scenery_controller == null: + return + _begin_wall_erase_tool() + + +func _begin_wall_erase_tool() -> void: + if _scenery_controller == null or not _scenery_edit_mode: + return + _selection.clear_selection() + _scenery_inspector.hide() + if not _scenery_controller.begin_wall_eraser(_viewport_to_world(get_viewport().get_mouse_position())): + _show_toast("Could not start the wall eraser. Try again.") + _refresh_scenery_capture_bar() + + +func _begin_scenery_repair(use_file_chooser: bool) -> void: + var selected: Array[Node2D] = _selection.get_selected() + if selected.size() != 1 or selected[0].get_script() != STAGE_SCENERY_ARTWORK_SCRIPT or not (selected[0] as StageSceneryArtwork).missing: + return + _scenery_repair_target = selected[0] as StageSceneryArtwork + if use_file_chooser: + _asset_browser.set_selector_kind("scenery") + _asset_browser.show_browse_dialog() + return + _asset_browser.open_selector("scenery", _spawner.get_selected_stickman_path(), _sky_id) + var selector: AssetSelector = _asset_browser.get_selector() + if selector != null: + selector.call("_set_gallery_tab", "artwork") + + +func _start_scenery_repair_preview(entry: Dictionary) -> void: + var target: StageSceneryArtwork = _scenery_repair_target + var path: String = CONTENT_PATHS.canonicalize(String(entry.get("path", ""))) + if target == null or not is_instance_valid(target) or path.is_empty(): + _show_toast("Could not open that artwork. Try another file.") + return + var candidate: StageSceneryArtwork = STAGE_SCENERY_ARTWORK_SCRIPT.new() as StageSceneryArtwork + if not candidate.load_visual(path): + candidate.free() + _show_toast("Could not open that artwork. Try another file.") + return + candidate.display_name = String(entry.get("name", path.get_file().get_basename())) + candidate.set_depth(target.depth) + candidate.global_transform = target.global_transform + candidate.modulate = Color(1.0, 1.0, 1.0, 0.62) + _ghost_holder.add_child(candidate) + _scenery_repair_candidate = candidate + _scenery_repair_path = path + if _scenery_repair_confirm_dialog != null: + _scenery_repair_confirm_dialog.dialog_text = "Use %s in this spot? Only this scenery piece will change." % candidate.display_name + call_deferred("_popup_scenery_repair_confirm_after_frame") + + +func _popup_scenery_repair_confirm_after_frame() -> void: + await get_tree().process_frame + if _scenery_repair_confirm_dialog != null and _scenery_repair_candidate != null and is_instance_valid(_scenery_repair_candidate): + _scenery_repair_confirm_dialog.popup_centered() + + +func _confirm_scenery_repair() -> void: + var target: StageSceneryArtwork = _scenery_repair_target + var candidate: StageSceneryArtwork = _scenery_repair_candidate + var path: String = _scenery_repair_path + if target == null or not is_instance_valid(target) or candidate == null or not is_instance_valid(candidate) or path.is_empty(): + _cancel_scenery_repair_preview() + return + var repaired_name: String = candidate.display_name + var repaired_depth: StringName = target.depth + if not target.load_visual(path): + _cancel_scenery_repair_preview() + _show_toast("Could not open that artwork. Try another file.") + return + target.display_name = repaired_name + target.set_depth(repaired_depth) + _cancel_scenery_repair_preview() + _mark_scene_dirty() + _refresh_scenery_inspector() + + +func _cancel_scenery_repair_preview() -> void: + if _scenery_repair_candidate != null and is_instance_valid(_scenery_repair_candidate): + _scenery_repair_candidate.queue_free() + _scenery_repair_candidate = null + _scenery_repair_path = "" + + +func _keep_scenery_repair_for_now() -> void: + _cancel_scenery_repair_preview() + _scenery_repair_target = null + if _scenery_inspector != null: + _scenery_inspector.hide() + + +func _enter_scenery_edit_mode() -> void: + if current_mode != StageMode.EDIT: + return + _close_selector() + if _scenery_controller != null and _scenery_controller.is_active(): + _scenery_controller.exit_tool() + set_placement_mode("") + _scenery_edit_mode = true + _scenery_multi_select = false + _scenery_group_mode = &"" + _selection.set_scope(StageSelection.SelectionScope.SCENERY) + _gizmos.set_enabled(true) + _gizmos.set_transform_handles_visible(true) + _refresh_scenery_inspector() + _refresh_scenery_capture_bar() + + +func _exit_scenery_edit_mode() -> void: + if _scenery_repair_confirm_dialog != null: + _scenery_repair_confirm_dialog.hide() + _cancel_scenery_repair_preview() + _scenery_repair_target = null + if _scenery_inspector != null: + _scenery_inspector.hide() + if _scenery_controller != null and _scenery_controller.is_group_preview_pending(): + _scenery_controller.cancel_group() + if _scenery_controller != null and _scenery_controller.is_single_transform_active(): + _scenery_controller.cancel_single_transform() + _scenery_edit_mode = false + _scenery_multi_select = false + _scenery_single_dragging = false + _scenery_group_dragging = false + _scenery_group_mode = &"" + _box_selecting = false + _selection.set_scope(StageSelection.SelectionScope.ACTION) + _gizmos.set_transform_handles_visible(true) + _gizmos.set_enabled(current_mode != StageMode.PLAY) + _hide_scenery_capture_bar() + + +func _on_scenery_placement_committed(node: Node2D) -> void: + if node == null or not is_instance_valid(node): + return + _scenery_placed_count += 1 + _mark_scene_dirty() + _refresh_scenery_capture_bar() + + +func _on_scenery_preview_state_changed(_active: bool) -> void: + _refresh_scenery_capture_bar() + + +func _refresh_scenery_capture_bar() -> void: + if _use_target_capture_bar == null or _capture_kind != CaptureKind.NONE: + return + if _scenery_controller != null and _scenery_controller.is_active(): + _use_target_capture_bar.clear_horizontal_safe_bounds() + var actions: Array[Dictionary] = [] + if _scenery_controller.get_active_tool() == &"artwork": + actions.append({"id": "depth_far", "label": "Far"}) + actions.append({"id": "depth_near", "label": "Near"}) + actions.append({"id": "depth_floor", "label": "On the floor"}) + var artwork_name: String = String(_scenery_controller.get_active_artwork_name()) + var prompt: String = "Click to place %s" % artwork_name if _scenery_placed_count == 0 else "Place another %s · placed pieces stay" % artwork_name + actions.append({"id": "done_tool", "label": "Done", "primary": true}) + actions.append({"id": "cancel_tool", "label": "✕ Cancel"}) + _use_target_capture_bar.set_capture_actions(prompt, actions) + elif _scenery_controller.get_active_tool() == &"wall_eraser": + var erase_prompt: String = "Drag over the wall · release to erase" + if _scenery_controller.get_wall_areas_erased() > 0: + erase_prompt = "Erase another area · earlier openings stay" + actions.append({"id": "done_tool", "label": "Done", "primary": true}) + actions.append({"id": "cancel_tool", "label": "✕ Cancel"}) + _use_target_capture_bar.set_capture_actions(erase_prompt, actions) + else: + actions.append({"id": "done_tool", "label": "Done", "primary": true}) + actions.append({"id": "cancel_tool", "label": "✕ Cancel"}) + _use_target_capture_bar.set_capture_actions("Drag to draw wall · Stickmen can pass through", actions) + return + if _scenery_controller != null and _scenery_controller.is_group_preview_pending(): + _use_target_capture_bar.clear_horizontal_safe_bounds() + var preview_actions: Array[Dictionary] = [ + {"id": "keep_group", "label": "Keep changes", "primary": true}, + {"id": "cancel_group", "label": "✕ Cancel"}, + ] + var group_prompt: String = "Move together · Ground and Props stay put" if _scenery_group_mode == &"move" else "Resize together · Ground and Props stay put" + _use_target_capture_bar.set_capture_actions(group_prompt, preview_actions) + return + if _scenery_controller != null and _scenery_controller.is_single_transform_active(): + _set_scenery_selection_capture_bounds() + var target: Node2D = _scenery_controller.get_single_transform_node() + var target_name: String = "scenery" + if target != null and target.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT: + target_name = (target as StageSceneryArtwork).display_name + elif target != null: + target_name = "wall panel" + var is_move: bool = _scenery_controller.get_single_transform_kind() == &"move" + var prompt_verb: String = "Move" if is_move else "Resize" + var release_verb: String = "place" if is_move else "resize" + var cancel_actions: Array[Dictionary] = [{"id": "cancel_single", "label": "✕ Cancel"}] + _use_target_capture_bar.set_capture_actions("%s %s · release to %s" % [prompt_verb, target_name, release_verb], cancel_actions) + return + if _scenery_edit_mode: + var count: int = _selection.get_selected().size() + if count == 1: + _set_scenery_selection_capture_bounds() + else: + _use_target_capture_bar.clear_horizontal_safe_bounds() + var prompt: String = "%d scenery piece%s selected" % [count, "" if count == 1 else "s"] + var edit_actions: Array[Dictionary] = [] + if count > 1: + edit_actions.append({"id": "move_group", "label": "Move together"}) + edit_actions.append({"id": "resize_group", "label": "Resize together"}) + edit_actions.append({"id": "clear_selection", "label": "Clear selection"}) + else: + edit_actions.append({"id": "select_several", "label": "Select several"}) + edit_actions.append({"id": "select_all", "label": "Select all scenery"}) + if count > 0: + edit_actions.append({"id": "clear_selection", "label": "Clear selection"}) + else: + edit_actions.append({"id": "erase_wall_area", "label": "Erase wall area"}) + edit_actions.append({"id": "done_edit", "label": "Done editing", "primary": true}) + _use_target_capture_bar.set_capture_actions(prompt, edit_actions) + return + _use_target_capture_bar.clear_horizontal_safe_bounds() + _use_target_capture_bar.hide_capture() + + +func _set_scenery_selection_capture_bounds() -> void: + if _use_target_capture_bar == null: + return + var viewport_width: float = get_viewport_rect().size.x + var inset: float = float(_stage_panel_integer("shell.edge_inset", 12, 0, 48)) + var palette_config: Dictionary = _theme.get("palette", {}) as Dictionary + var palette_width: float = float(palette_config.get("dock_width", 116.0)) + var panel_width: float = float(_stage_panel_integer("shell.preferred_width", 380, 220, 720)) + var left: float = palette_width + inset + var right: float = viewport_width - panel_width - inset * 2.0 + _use_target_capture_bar.set_horizontal_safe_bounds(left, right) + + +func _hide_scenery_capture_bar() -> void: + if _use_target_capture_bar != null: + _use_target_capture_bar.clear_horizontal_safe_bounds() + _use_target_capture_bar.hide_capture() + + +func _on_capture_action_requested(action_id: StringName) -> void: + match action_id: + &"depth_far": + _scenery_controller.set_depth(&"far") + _refresh_scenery_capture_bar() + &"depth_near": + _scenery_controller.set_depth(&"near") + _refresh_scenery_capture_bar() + &"depth_floor": + _scenery_controller.set_depth(&"floor") + _refresh_scenery_capture_bar() + &"done_tool", &"cancel_tool": + _scenery_controller.exit_tool() + _hide_scenery_capture_bar() + &"select_several": + _scenery_multi_select = true + _show_toast("Tap scenery pieces to add them. Shift-click also works.") + _refresh_scenery_capture_bar() + &"select_all": + _select_all_scenery() + &"clear_selection": + _selection.clear_selection() + _refresh_scenery_capture_bar() + &"erase_wall_area": + _begin_wall_erase_tool() + &"move_group": + _begin_scenery_group_transform(&"move") + &"resize_group": + _begin_scenery_group_transform(&"resize") + &"keep_group": + _scenery_controller.commit_group() + _scenery_group_mode = &"" + _refresh_scenery_capture_bar() + &"cancel_group": + _scenery_controller.cancel_group() + _scenery_group_mode = &"" + _refresh_scenery_capture_bar() + &"cancel_single": + _scenery_controller.cancel_single_transform() + _scenery_single_dragging = false + _refresh_scenery_inspector() + _refresh_scenery_capture_bar() + &"done_edit": + _exit_scenery_edit_mode() + + +func _begin_scenery_group_transform(kind: StringName) -> void: + var selected: Array[Node2D] = _selection.get_selected().duplicate() + if _scenery_controller == null or not _scenery_controller.begin_group_transform(kind, selected): + _show_toast("This group needs artwork before it can be resized.") + return + # A pending restore is only a post-PLAY safety net. Once the user starts an + # explicit scenery transform, it must not overwrite that new edit. + _restore_frames_left = 0 + _scenery_group_mode = kind + _refresh_scenery_capture_bar() + + +func _select_all_scenery() -> void: + _selection.clear_selection() + for child: Node in _world.get_children(): + var node: Node2D = child as Node2D + if node != null and (node.get_script() == STAGE_WALL_PANEL_SCRIPT or node.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT): + _selection.add_to_selection(node) + _refresh_scenery_capture_bar() + + +func _handle_scenery_edit_click(event: InputEventMouseButton, world_position: Vector2) -> void: + if event.button_index != MOUSE_BUTTON_LEFT: + return + if event.pressed and _scenery_controller != null: + _scenery_controller.consume_cancelled_single_release() + if not event.pressed and _scenery_controller != null and _scenery_controller.consume_cancelled_single_release(): + _scenery_single_dragging = false + _refresh_scenery_capture_bar() + return + if _scenery_group_mode != &"" and _scenery_controller != null and _scenery_controller.is_group_preview_pending(): + if event.pressed: + _scenery_group_start = world_position + _scenery_group_dragging = true + else: + _scenery_group_dragging = false + _refresh_scenery_capture_bar() + return + if event.pressed: + var selected_before_press: Array[Node2D] = _selection.get_selected() + if not event.shift_pressed and not _scenery_multi_select and selected_before_press.size() == 1 and _scenery_controller != null: + var resize_corner: Vector2 = _gizmos.get_scenery_resize_corner(world_position) + if resize_corner.is_finite() and _scenery_controller.begin_single_transform(selected_before_press[0], &"resize", world_position, resize_corner): + _scenery_single_dragging = true + _refresh_scenery_capture_bar() + return + var candidates: Array[Node2D] = _selection.hit_test_all(world_position) + var hit: Node2D = candidates[0] if not candidates.is_empty() else null + if hit == null: + if not event.shift_pressed and not _scenery_multi_select: + _selection.clear_selection() + _box_selecting = true + _box_start = world_position + _box_rect = Rect2(world_position, Vector2.ZERO) + _gizmos.set_box_rect(_box_rect) + return + if candidates.size() > 1 and not event.shift_pressed and not _scenery_multi_select: + _selection.select_only(hit) + _show_objects_here_popup(candidates) + return + if event.shift_pressed or _scenery_multi_select: + _selection.toggle_selection(hit) + _scenery_single_dragging = false + return + _selection.select_only(hit) + if _scenery_controller != null: + _scenery_single_dragging = _scenery_controller.begin_single_transform(hit, &"move", world_position) + _refresh_scenery_capture_bar() + else: + if _scenery_single_dragging and _scenery_controller != null and _scenery_controller.is_single_transform_active(): + _scenery_controller.commit_single_transform() + _scenery_single_dragging = false + elif _box_selecting: + _finish_box_select(event.shift_pressed or _scenery_multi_select) + _refresh_scenery_capture_bar() + + +func _handle_scenery_edit_motion(world_position: Vector2) -> void: + if _scenery_group_dragging and _scenery_controller != null: + var delta: Vector2 = world_position - _scenery_group_start + var valid: bool + if _scenery_group_mode == &"move": + if _snap_enabled: + delta = _snap_to_grid(_scenery_group_start + delta) - _scenery_group_start + valid = _scenery_controller.preview_group(delta, 1.0) + else: + var bounds: Rect2 = _scenery_controller.get_group_bounds() + var factor: float = 1.0 + delta.x / maxf(bounds.size.x, StageSceneryController.MIN_PANEL_SIZE) + valid = _scenery_controller.preview_group(Vector2.ZERO, factor) + if not valid: + _show_toast("Too small for this group. Try a smaller change.") + return + if _scenery_single_dragging and _scenery_controller != null and _scenery_controller.is_single_transform_active(): + _scenery_controller.preview_single_transform(world_position) + return + if _box_selecting: + _box_rect = Rect2(_box_start, world_position - _box_start) + _gizmos.set_box_rect(_box_rect) + elif not _is_mouse_over_ui(): + _selection.update_hover(world_position) + + func _close_selector() -> void: if _asset_browser != null: _asset_browser.close() @@ -3863,8 +4831,10 @@ func _close_selector() -> void: func _on_selector_cancelled() -> void: if not _selector_open: return + var kind: String = _selector_kind _close_selector() - set_placement_mode("") + if kind != "scenery" and kind != "starters": + set_placement_mode("") func _on_refresh_requested() -> void: @@ -3900,6 +4870,9 @@ func _on_grid_size_changed(value: float) -> void: func _on_selection_changed(nodes: Array[Node2D]) -> void: _cancel_edit_wobble_preview() + if _scenery_controller != null and _scenery_controller.is_single_transform_active() and not nodes.has(_scenery_controller.get_single_transform_node()): + _scenery_controller.cancel_single_transform() + _scenery_single_dragging = false if _spot_edit_active and (_spot_edit_prop == null or not is_instance_valid(_spot_edit_prop) or not nodes.has(_spot_edit_prop)): _cancel_spot_work() _gizmos.set_targets(nodes) @@ -3909,6 +4882,9 @@ func _on_selection_changed(nodes: Array[Node2D]) -> void: object_selected.emit(nodes) _refresh_status() _refresh_prop_settings_card() + if _scenery_edit_mode: + _refresh_scenery_inspector() + _refresh_scenery_capture_bar() func _on_hover_changed(node: Node2D) -> void: @@ -4127,8 +5103,7 @@ func _continue_pending_file_action() -> void: _pending_file_action = PendingFileAction.NONE; _pending_open_path = "" match action: PendingFileAction.NEW_SCENE: - _reset_scene_content(); _current_scene_path = ""; _scene_name = DEFAULT_SCENE_NAME; _scene_created_unix = Time.get_unix_time_from_system(); _camera.global_position = DEFAULT_CAMERA_POSITION; _camera.zoom = Vector2.ONE * DEFAULT_CAMERA_ZOOM; _set_scene_dirty(false) - if _document != null: _document.remove_autosave() + _open_selector("starters") PendingFileAction.OPEN_DIALOG: _scene_open_dialog.popup_centered_ratio(0.7) PendingFileAction.OPEN_PATH: _load_scene_from_path(path) PendingFileAction.QUIT: @@ -5922,6 +6897,8 @@ func _apply_grid_settings() -> void: _grid.visible = _show_grid and current_mode == StageMode.EDIT if _gizmos != null: _gizmos.set_snap_size(_grid_size if _snap_enabled else 0.0) + if _scenery_controller != null: + _scenery_controller.set_grid_snap(_snap_enabled, _grid_size) func _set_edit_controls_visible(visible: bool) -> void: @@ -5943,6 +6920,8 @@ func _world_children_selectable() -> Array[Node2D]: continue if node.is_in_group("stickman_ragdoll_container") or bool(node.get_meta("stickman_ragdoll_container", false)): continue + if node.get_script() == STAGE_WALL_PANEL_SCRIPT or node.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT: + continue result.append(node) return result @@ -5971,6 +6950,8 @@ func _load_font(path: String) -> Font: return _theme_controller.load_font(path) func _apply_theme_to_visuals() -> void: + if _sky_view != null and _sandbox_theme != null: + _sky_view.apply_theme(_sandbox_theme as SandboxTheme) if _director_visuals != null: _director_visuals.set_style(_theme) _director_visuals.completed_path_alpha = _sandbox_theme.number("overlays.completed_path_alpha", 0.20, 0.0, 1.0) if _sandbox_theme != null else 0.20 diff --git a/scripts/editors/stage/stage_asset_browser.gd b/scripts/editors/stage/stage_asset_browser.gd index ec8be9e..4f75a14 100644 --- a/scripts/editors/stage/stage_asset_browser.gd +++ b/scripts/editors/stage/stage_asset_browser.gd @@ -6,6 +6,8 @@ signal asset_selected_requested(entry: Dictionary) signal asset_load_failed(kind: String, path: String) signal selector_cancel_requested signal refresh_requested +signal make_artwork_requested +signal edit_scenery_requested const ASSET_SELECTOR_SCENE := preload("res://scenes/stage/asset_selector.tscn") const STICKMAN_LIBRARY_SCRIPT := preload("res://scripts/characters/stickman/stickman_library.gd") @@ -55,6 +57,8 @@ func build_ui(ui_layer: CanvasLayer, ui_font: Font, emoji_font: Font, sandbox_th selector.popup_hide.connect(_on_selector_cancelled) selector.browse_requested.connect(show_browse_dialog) selector.refresh_requested.connect(func() -> void: refresh_requested.emit()) + selector.make_artwork_requested.connect(func() -> void: make_artwork_requested.emit()) + selector.edit_scenery_requested.connect(func() -> void: edit_scenery_requested.emit()) ui_layer.add_child(selector) # apply_font AFTER add_child so the selector's @onready nodes resolve first. selector.apply_font(ui_font, emoji_font) @@ -73,24 +77,64 @@ func build_ui(ui_layer: CanvasLayer, ui_font: Font, emoji_font: Font, sandbox_th func scan_entries(kind: String) -> Array[Dictionary]: if kind == "stickman": return stickman_library.scan() + if kind == "scenery": + return PROP_LIBRARY_SCRIPT.scan_visual_entries() + if kind == "starters": + return _starter_scene_entries() return PROP_LIBRARY_SCRIPT.scan() +func _starter_scene_entries() -> Array[Dictionary]: + return [ + { + "id": "starter_blank_scene", + "name": "Blank Scene", + "description": "Start fresh with an open stage and a clear sky.", + "category": "Make your own", + "path": "res://content/stages/starters/blank.scene", + "icon": "✨", + }, + { + "id": "starter_town_street", + "name": "Town Street", + "description": "Cloudy sky, shops, trees, sidewalk, and road.", + "category": "Ready to explore", + "path": "res://content/stages/starters/town_street.scene", + "icon": "🏘️", + }, + { + "id": "starter_living_room", + "name": "Living Room", + "description": "A cozy room with a window, rug, and furniture.", + "category": "Ready to explore", + "path": "res://content/stages/starters/living_room.scene", + "icon": "🛋️", + }, + ] + + func make_browse_entry(kind: String, path: String) -> Dictionary: if kind == "prop": return PROP_LIBRARY_SCRIPT.make_custom_entry(path) + if kind == "scenery": + return PROP_LIBRARY_SCRIPT.make_visual_entry(path) return stickman_library.make_entry(path) -func open_selector(kind: String, current_stickman_path: String) -> Dictionary: +func open_selector(kind: String, current_stickman_path: String, current_sky_id: StringName = &"") -> Dictionary: set_selector_kind(kind) var entries: Array[Dictionary] = scan_entries(kind) + if kind == "starters": + open(kind, entries) + return {"entries": entries, "selected_stickman_path": current_stickman_path, "auto_selection": {}} + if kind == "scenery": + entries.append_array(_scenery_background_entries()) var selected_stickman_path: String = current_stickman_path if kind == "stickman" and not entries.is_empty() and stickman_library.find_by_path(current_stickman_path).is_empty(): selected_stickman_path = String(entries[0].get("path", "")) var auto_selection: Dictionary = entries[0] if kind == "stickman" and entries.size() == 1 else {} if auto_selection.is_empty(): - open(kind, entries) + open(kind, entries, current_sky_id) return { "entries": entries, "selected_stickman_path": selected_stickman_path, @@ -98,17 +142,29 @@ func open_selector(kind: String, current_stickman_path: String) -> Dictionary: } +func _scenery_background_entries() -> Array[Dictionary]: + return [ + {"id": "sky_clear_day", "name": "Clear Day", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "☀️"}, + {"id": "sky_cloudy_day", "name": "Cloudy Day", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "☁️"}, + {"id": "sky_sunset", "name": "Sunset", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "🌅"}, + {"id": "sky_starry_night", "name": "Starry Night", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "🌙"}, + {"id": "finish_wall_paint", "name": "Wall panel · Paint", "category": "Wall finish", "gallery_tab": "backgrounds", "background_kind": "wall", "finish_id": "finish_wall_paint", "icon": "🟩"}, + {"id": "finish_wall_brick", "name": "Wall panel · Brick", "category": "Wall finish", "gallery_tab": "backgrounds", "background_kind": "wall", "finish_id": "finish_wall_brick", "icon": "🧱"}, + {"id": "finish_wall_wood", "name": "Wall panel · Wood", "category": "Wall finish", "gallery_tab": "backgrounds", "background_kind": "wall", "finish_id": "finish_wall_wood", "icon": "🪵"} + ] + + func set_selector_kind(kind: String) -> void: selector_kind = kind -func open(kind: String, entries: Array[Dictionary]) -> void: +func open(kind: String, entries: Array[Dictionary], current_sky_id: StringName = &"") -> void: selector_kind = kind selector_open = true if selector_dim != null: selector_dim.visible = true if selector != null: - selector.open(kind, entries) + selector.open(kind, entries, String(current_sky_id)) load_or_enqueue_thumbnails(kind, entries) @@ -126,7 +182,7 @@ func close() -> void: func show_browse_dialog() -> void: if browse_dialog == null: return - if selector_kind == "prop": + if selector_kind == "prop" or selector_kind == "scenery": browse_dialog.title = "Open .prop" browse_dialog.filters = PackedStringArray(["*.prop ; Prop Files"]) else: @@ -160,6 +216,8 @@ func load_or_enqueue_thumbnails(kind: String, entries: Array[Dictionary]) -> voi thumbnail_generation += 1 var generation: int = thumbnail_generation for entry: Dictionary in entries: + if kind == "starters": + continue if kind == "stickman": var path: String = String(entry.get("path", "")) var key: String = thumbnail_cache.stickman_key(path) @@ -171,6 +229,8 @@ func load_or_enqueue_thumbnails(kind: String, entries: Array[Dictionary]) -> voi else: thumbnail_queue.append({"kind": kind, "entry": entry, "key": key, "cache_path": png, "generation": generation}) else: + if kind == "starters" or String(entry.get("gallery_tab", "")) == "backgrounds": + continue var id: String = String(entry.get("id", "")) var png: String = thumbnail_cache.prop_file_png(id) if bool(entry.get("is_custom", false)) else thumbnail_cache.prop_png(id) var texture: Texture2D = thumbnail_cache.get_texture(png) diff --git a/scripts/editors/stage/stage_chrome.gd b/scripts/editors/stage/stage_chrome.gd index 96a37b5..f0fb9be 100644 --- a/scripts/editors/stage/stage_chrome.gd +++ b/scripts/editors/stage/stage_chrome.gd @@ -395,10 +395,15 @@ func _build_top_bar() -> void: chrome.add_child(left_group) var right_group := HBoxContainer.new() right_group.name = "RightControls" - right_group.mouse_filter = Control.MOUSE_FILTER_PASS - right_group.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT) + # This row spans the top bar so it can align controls at the right edge; + # ignore empty space so it cannot steal clicks from File or Mode buttons. + right_group.mouse_filter = Control.MOUSE_FILTER_IGNORE + right_group.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE) + right_group.offset_left = 8.0 right_group.offset_right = -8.0 right_group.offset_top = 8.0 + right_group.offset_bottom = TOP_BAR_HEIGHT - 8.0 + right_group.alignment = BoxContainer.ALIGNMENT_END right_group.add_theme_constant_override("separation", 8) chrome.add_child(right_group) _build_file_menu(left_group) diff --git a/scripts/editors/stage/stage_modal_views.gd b/scripts/editors/stage/stage_modal_views.gd index e768e88..c326093 100644 --- a/scripts/editors/stage/stage_modal_views.gd +++ b/scripts/editors/stage/stage_modal_views.gd @@ -22,6 +22,7 @@ var wait_spin: SpinBox var scene_open_dialog: FileDialog var scene_save_dialog: FileDialog var speech_save_gate_dialog: ConfirmationDialog +var group_preview_save_dialog: ConfirmationDialog var rename_scene_dialog: ConfirmationDialog var scene_name_edit: LineEdit var missing_assets_dialog: AcceptDialog @@ -103,6 +104,16 @@ func build_file_dialogs(parent: Node) -> void: parent.add_child(speech_save_gate_dialog) apply_dialog_theme(speech_save_gate_dialog) + group_preview_save_dialog = ConfirmationDialog.new() + group_preview_save_dialog.name = "GroupPreviewSaveDialog" + group_preview_save_dialog.title = "Finish scenery preview?" + group_preview_save_dialog.dialog_text = "Keep the scenery changes before saving, or cancel this save." + group_preview_save_dialog.ok_button_text = "Keep changes" + group_preview_save_dialog.cancel_button_text = "Cancel save" + group_preview_save_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300)) + parent.add_child(group_preview_save_dialog) + apply_dialog_theme(group_preview_save_dialog) + rename_scene_dialog = ConfirmationDialog.new() rename_scene_dialog.name = "RenameSceneDialog" rename_scene_dialog.title = "Rename Scene" diff --git a/scripts/editors/stage/stage_scene_adapter.gd b/scripts/editors/stage/stage_scene_adapter.gd index f22afae..766cf14 100644 --- a/scripts/editors/stage/stage_scene_adapter.gd +++ b/scripts/editors/stage/stage_scene_adapter.gd @@ -10,6 +10,9 @@ const MISSING_PROP_PLACEHOLDER := preload("res://scripts/props/missing_prop_plac const PROP_LIBRARY := preload("res://scripts/props/prop_library.gd") const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd") const PROP_USE_SPOT_DATA := preload("res://scripts/props/prop_use_spot_data.gd") +const STAGE_WALL_PANEL := preload("res://scripts/stage/scenery/stage_wall_panel.gd") +const STAGE_SCENERY_ARTWORK := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd") +const STAGE_DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd") func collect( @@ -21,7 +24,7 @@ func collect( ) -> Dictionary: var objects: Array[Dictionary] = [] var runtime_to_file: Dictionary = {} - var counters: Dictionary = {"terrain": 0, "prop": 0, "stickman": 0, "area": 0} + var counters: Dictionary = {"terrain": 0, "prop": 0, "stickman": 0, "area": 0, "wall_panel": 0, "scenery_artwork": 0} for node: Node in world.get_children(): if not node is Node2D: continue @@ -33,8 +36,12 @@ func collect( var file_id: String = "%s_%d" % [type, int(counters[type])] runtime_to_file[stage_node.get_instance_id()] = file_id var authored_state: Dictionary = authored.get(stage_node.get_instance_id(), {}) - var position: Vector2 = authored_state.get("position", stage_node.global_position) - var rotation_degrees: float = rad_to_deg(float(authored_state.get("rotation", stage_node.global_rotation))) + # Scenery artwork is editor-authored and never receives runtime physics + # movement. Its live transform is therefore the committed document state; + # a cached pre-PLAY authored snapshot would erase later group edits. + var position: Vector2 = stage_node.global_position if type == "scenery_artwork" else authored_state.get("position", stage_node.global_position) + var rotation: float = stage_node.global_rotation if type == "scenery_artwork" else float(authored_state.get("rotation", stage_node.global_rotation)) + var rotation_degrees: float = rad_to_deg(rotation) var entry: Dictionary = { "file_id": file_id, "type": type, @@ -44,6 +51,25 @@ func collect( if type == "terrain": entry["asset_id"] = String(stage_node.get("spawn_id")) entry["scale"] = STAGE_SCENE_IO.vector_to_data(stage_node.scale) + if stage_node is TerrainBlock and String(stage_node.get("spawn_id")) == "ground": + var ground_finish: StringName = (stage_node as TerrainBlock).finish_id + entry["finish_id"] = String(ground_finish) if not ground_finish.is_empty() else "finish_ground_legacy" + elif stage_node.get_script() == STAGE_WALL_PANEL: + var wall: StageWallPanel = stage_node as StageWallPanel + entry["size"] = STAGE_SCENE_IO.vector_to_data(wall.panel_size) + entry["finish_id"] = String(wall.finish_id) + entry["color"] = "#" + wall.tint.to_html(true) + if not wall.motif_origin_offset.is_zero_approx(): + entry["motif_origin_offset"] = STAGE_SCENE_IO.vector_to_data(wall.motif_origin_offset) + elif stage_node.get_script() == STAGE_SCENERY_ARTWORK: + var artwork: StageSceneryArtwork = stage_node as StageSceneryArtwork + entry["asset_id"] = CONTENT_PATHS.canonicalize(artwork.asset_path) + entry["display_name"] = artwork.display_name + entry["depth"] = String(artwork.depth) + entry["scale"] = STAGE_SCENE_IO.vector_to_data(artwork.scale) + if artwork.has_known_bounds: + var known_bounds: Rect2 = artwork.get_saved_footprint() + entry["last_known_local_bounds"] = {"position": STAGE_SCENE_IO.vector_to_data(known_bounds.position), "size": STAGE_SCENE_IO.vector_to_data(known_bounds.size)} elif type == "prop" and stage_node.get_script() == MISSING_PROP_PLACEHOLDER: var original_entry: Dictionary = stage_node.get("original_entry") as Dictionary entry = original_entry.duplicate(true) @@ -95,6 +121,7 @@ func collect( return { "version": STAGE_SCENE_IO.FILE_VERSION, "scene_name": scene_name, + "sky_id": String(document_metadata.get("sky_id", "sky_neutral")), "camera": {"position": STAGE_SCENE_IO.vector_to_data(camera_position), "zoom": camera_zoom}, "objects": objects, "event_rules": rules_result["rules"], @@ -131,6 +158,29 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary match type: "terrain": node = spawner.spawn_terrain_asset(String(entry.get("asset_id", "")), position) + if node is TerrainBlock and String(entry.get("asset_id", "")) == "ground": + (node as TerrainBlock).set_finish(StringName(String(entry.get("finish_id", "finish_ground_legacy")))) + "wall_panel": + var wall := STAGE_WALL_PANEL.new() as StageWallPanel + wall.position = position + wall.configure(STAGE_SCENE_IO.data_to_vector(entry.get("size"), Vector2(320.0, 400.0)), StringName(String(entry.get("finish_id", "finish_wall_paint"))), STAGE_SCENE_IO.color_from_data(entry.get("color", "#5A9D70"), Color("#5A9D70"))) + if entry.has("motif_origin_offset") and STAGE_SCENE_IO.is_vector_data(entry.get("motif_origin_offset")): + wall.motif_origin_offset = STAGE_SCENE_IO.data_to_vector(entry.get("motif_origin_offset"), Vector2.ZERO) + world.add_child(wall) + node = wall + "scenery_artwork": + var artwork := STAGE_SCENERY_ARTWORK.new() as StageSceneryArtwork + var raw_bounds: Variant = entry.get("last_known_local_bounds", {}) + var footprint := Rect2() + if raw_bounds is Dictionary and STAGE_SCENE_IO._valid_rect_data(raw_bounds): + var bounds_data: Dictionary = raw_bounds + footprint = Rect2(STAGE_SCENE_IO.data_to_vector(bounds_data.get("position"), Vector2.ZERO), STAGE_SCENE_IO.data_to_vector(bounds_data.get("size"), Vector2(64.0, 64.0))) + artwork.position = position + artwork.rotation = deg_to_rad(float(entry.get("rotation", 0.0))) + artwork.scale = STAGE_SCENE_IO.data_to_vector(entry.get("scale"), Vector2.ONE) + artwork.configure_missing(String(entry.get("asset_id", "")), String(entry.get("display_name", "Scenery")), footprint, StringName(String(entry.get("depth", "near")))) + world.add_child(artwork) + node = artwork "prop": node = spawner.spawn_prop_asset(String(entry.get("asset_id", "")), position) if node == null and PROP_LIBRARY.is_custom_path(String(entry.get("asset_id", ""))): @@ -158,7 +208,7 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary warnings.append("Unknown object asset") continue node.global_rotation = deg_to_rad(float(entry.get("rotation", 0.0))) - if type != "stickman": + if type not in ["stickman", "wall_panel", "scenery_artwork"]: if entry.has("scale") and not STAGE_SCENE_IO.is_vector_data(entry["scale"]): warnings.append("Malformed object scale") world.remove_child(node) @@ -216,6 +266,8 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary warnings.append("%s: invalid action queue. Open this character's queue and review its actions." % file_id) (node as StickmanRig).reset_collision_proxy_velocity_baselines() loaded_nodes.append(node) + if node is TerrainBlock: + STAGE_DRAW_ORDER.apply_terrain(node as TerrainBlock) file_to_runtime[file_id] = node.get_instance_id() var decoded_rules: Dictionary = STAGE_SCENE_IO.decode_rules(data.get("event_rules", []) as Array, file_to_runtime, unavailable) return { @@ -226,10 +278,15 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary "missing_stickmen": missing_stickmen, "missing_paths": missing_paths, "decoded_rules": decoded_rules, + "sky_id": String(data.get("sky_id", "sky_neutral")), } func _object_type(node: Node2D) -> String: + if node.get_script() == STAGE_WALL_PANEL: + return "wall_panel" + if node.get_script() == STAGE_SCENERY_ARTWORK: + return "scenery_artwork" if node is TerrainBlock: return "terrain" if node is PropBlock or node.get_script() == MISSING_PROP_PLACEHOLDER: diff --git a/scripts/editors/stage/stage_scenery_controller.gd b/scripts/editors/stage/stage_scenery_controller.gd new file mode 100644 index 0000000..494c12b --- /dev/null +++ b/scripts/editors/stage/stage_scenery_controller.gd @@ -0,0 +1,801 @@ +class_name StageSceneryController +extends Node +## Owns inert Scenery placement drafts and snapshot-based group transforms. + +signal placement_committed(node: Node2D) +signal document_change_requested +signal preview_state_changed(active: bool) +signal single_gesture_changed(active: bool) + +const WALL_PANEL := preload("res://scripts/stage/scenery/stage_wall_panel.gd") +const SCENERY_ARTWORK := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd") +const PROP_FILE := preload("res://scripts/editors/prop/prop_file.gd") +const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd") + +const MIN_PANEL_SIZE: float = 24.0 +const MIN_WALL_ERASE_SIZE: float = 8.0 +const DEFAULT_PANEL_SIZE: Vector2 = Vector2(240.0, 280.0) +const WALL_ERASER_CURSOR_SIZE: Vector2 = Vector2(120.0, 120.0) +const MIN_ART_SCALE: float = 0.08 +const MAX_ART_SCALE: float = 12.0 + +var _world: Node2D +var _ghost_holder: Node2D +var _camera: Camera2D +var _ghost: Node2D +var _active_tool: StringName = &"" +var _asset_path: String = "" +var _depth: StringName = &"near" +var _wall_finish: StringName = &"finish_wall_paint" +var _wall_tint: Color = Color("#5A9D70") +var _wall_anchor: Vector2 = Vector2.INF +var _wall_drag_start: Vector2 = Vector2.INF +var _wall_dragging: bool = false +var _wall_anchor_active: bool = false +var _wall_erase_start: Vector2 = Vector2.INF +var _wall_erase_dragging: bool = false +var _wall_erase_pointer: Vector2 = Vector2.ZERO +var _wall_areas_erased: int = 0 +var _snap_enabled: bool = false +var _grid_size: float = 15.0 +var _group_kind: StringName = &"" +var _group_snapshots: Array[Dictionary] = [] +var _group_bounds: Rect2 = Rect2() +var _group_pending: bool = false +var _single_kind: StringName = &"" +var _single_snapshot: Dictionary = {} +var _single_active: bool = false +var _single_release_guard: bool = false + + +func configure(world: Node2D, ghost_holder: Node2D, camera: Camera2D) -> void: + _world = world + _ghost_holder = ghost_holder + _camera = camera + + +func set_grid_snap(enabled: bool, grid_size: float) -> void: + _snap_enabled = enabled + _grid_size = maxf(1.0, grid_size) + + +func set_depth(depth: StringName) -> void: + if depth not in [&"far", &"near", &"floor"]: + return + _depth = depth + if _ghost != null and is_instance_valid(_ghost) and _ghost.get_script() == SCENERY_ARTWORK: + (_ghost as StageSceneryArtwork).set_depth(_depth) + + +func is_active() -> bool: + return not _active_tool.is_empty() + + +func is_group_preview_pending() -> bool: + return _group_pending + + +func is_single_transform_active() -> bool: + return _single_active + + +func get_single_transform_node() -> Node2D: + return _single_snapshot.get("node") as Node2D if _single_active else null + + +func get_single_transform_kind() -> StringName: + return _single_kind if _single_active else &"" + + +func consume_cancelled_single_release() -> bool: + if not _single_release_guard: + return false + _single_release_guard = false + return true + + +func get_active_tool() -> StringName: + return _active_tool + + +func get_wall_areas_erased() -> int: + return _wall_areas_erased + + +func get_group_bounds() -> Rect2: + return _group_bounds + + +func get_active_artwork_name() -> String: + if _ghost != null and is_instance_valid(_ghost) and _ghost.get_script() == SCENERY_ARTWORK: + return String((_ghost as StageSceneryArtwork).display_name) + return _asset_path.get_file().get_basename() + + +func handle_key(event: InputEventKey) -> bool: + if is_active() and event.pressed and not event.echo and event.keycode == KEY_ESCAPE: + exit_tool() + return true + if _group_pending and event.pressed and not event.echo and event.keycode == KEY_ESCAPE: + cancel_group() + return true + return false + + +func begin_artwork(path: String, depth: StringName = &"near") -> bool: + if _world == null or _ghost_holder == null: + return false + exit_tool() + _active_tool = &"artwork" + _asset_path = path + _depth = depth if depth in [&"far", &"near", &"floor"] else &"near" + var visual := SCENERY_ARTWORK.new() as StageSceneryArtwork + if not visual.load_visual(path): + visual.queue_free() + _active_tool = &"" + return false + var asset_data: Dictionary = PROP_FILE.from_file_dict(PROP_FILE.load(path)) + visual.display_name = String(asset_data.get("prop_name", path.get_file().get_basename())) + visual.set_depth(_depth) + visual.modulate = Color(1.0, 1.0, 1.0, 0.52) + _ghost_holder.add_child(visual) + _ghost = visual + return true + + +func begin_wall(finish: StringName, initial_world_position: Vector2, tint: Color = Color("#5A9D70")) -> void: + if _world == null or _ghost_holder == null: + return + exit_tool() + _active_tool = &"wall" + _wall_finish = finish + _wall_tint = tint + _wall_anchor = Vector2.INF + _wall_dragging = false + _wall_anchor_active = false + var panel := WALL_PANEL.new() as StageWallPanel + panel.configure(DEFAULT_PANEL_SIZE, _wall_finish, _wall_tint) + panel.modulate = Color(1.0, 1.0, 1.0, 0.45) + _ghost_holder.add_child(panel) + _ghost = panel + # The palette may close without generating a Stage mouse-motion event. + # Position the preview from the current cursor immediately so the visible + # ghost and the first press use the same starting corner. + update_pointer(initial_world_position) + + +func begin_wall_eraser(initial_world_position: Vector2) -> bool: + if _world == null or _ghost_holder == null: + return false + exit_tool() + _active_tool = &"wall_eraser" + _wall_erase_start = Vector2.INF + _wall_erase_dragging = false + _wall_areas_erased = 0 + _wall_erase_pointer = initial_world_position + var preview := Node2D.new() + preview.name = "WallEraserPreview" + var fill := Polygon2D.new() + fill.name = "EraseArea" + fill.color = Color("#FF5B62", 0.2) + preview.add_child(fill) + var outline := Line2D.new() + outline.name = "EraseOutline" + outline.width = 3.0 + outline.default_color = Color("#FF7278") + preview.add_child(outline) + var cross_a := Line2D.new() + cross_a.name = "EraseCrossA" + cross_a.width = 2.0 + cross_a.default_color = Color("#FF7278", 0.9) + preview.add_child(cross_a) + var cross_b := Line2D.new() + cross_b.name = "EraseCrossB" + cross_b.width = 2.0 + cross_b.default_color = Color("#FF7278", 0.9) + preview.add_child(cross_b) + _ghost_holder.add_child(preview) + _ghost = preview + update_pointer(initial_world_position) + return true + + +func update_pointer(world_position: Vector2) -> void: + if not is_active() or _ghost == null or not is_instance_valid(_ghost): + return + var snapped_position: Vector2 = _snap(world_position) + if _active_tool == &"wall" and _wall_dragging: + var size: Vector2 = Vector2(absf(snapped_position.x - _wall_drag_start.x), absf(snapped_position.y - _wall_drag_start.y)) + size.x = maxf(MIN_PANEL_SIZE, size.x) + size.y = maxf(MIN_PANEL_SIZE, size.y) + (_ghost as StageWallPanel).configure(size, _wall_finish, _wall_tint) + var top_left: Vector2 = Vector2(minf(_wall_drag_start.x, snapped_position.x), minf(_wall_drag_start.y, snapped_position.y)) + _ghost.global_position = top_left + size * 0.5 + elif _active_tool == &"wall": + var preview_top_left: Vector2 = _snap(world_position) if _snap_enabled else world_position + _ghost.global_position = _move_node_visible_top_left(_ghost, preview_top_left) + elif _active_tool == &"wall_eraser": + _wall_erase_pointer = world_position + if _wall_erase_dragging: + _set_wall_eraser_preview_rect(_drag_rect(_wall_erase_start, _snap(world_position))) + else: + var cursor_origin: Vector2 = _snap(world_position) if _snap_enabled else world_position + _set_wall_eraser_preview_rect(Rect2(cursor_origin, WALL_ERASER_CURSOR_SIZE)) + else: + _ghost.global_position = _snap_visual_center(_ghost, world_position) + + +func handle_mouse_button(event: InputEventMouseButton, world_position: Vector2, over_ui: bool) -> bool: + if not is_active(): + return false + if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: + if _wall_dragging or _wall_anchor_active: + cancel_active_gesture() + exit_tool() + return true + var completes_drag: bool = not event.pressed and event.button_index == MOUSE_BUTTON_LEFT and ( + (_active_tool == &"wall" and _wall_dragging) + or (_active_tool == &"wall_eraser" and _wall_erase_dragging) + ) + if (over_ui and not completes_drag) or event.button_index != MOUSE_BUTTON_LEFT: + return false + if _active_tool == &"artwork": + if event.pressed: + _commit_artwork(world_position) + return true + if _active_tool == &"wall": + if event.pressed: + # Refresh from the press coordinates in case no mouse-motion event was + # delivered after the palette closed. The visible ghost corner is the + # drawing anchor, so preview and committed geometry cannot jump apart. + update_pointer(world_position) + _wall_anchor = _world_bounds(_ghost).position + _wall_anchor_active = true + _wall_drag_start = _wall_anchor + _wall_dragging = true + update_pointer(world_position) + return true + if _wall_dragging: + var end: Vector2 = _snap(world_position) + var size: Vector2 = Vector2(absf(end.x - _wall_drag_start.x), absf(end.y - _wall_drag_start.y)) + _wall_dragging = false + if size.x < MIN_PANEL_SIZE or size.y < MIN_PANEL_SIZE: + _wall_anchor_active = false + _wall_anchor = Vector2.INF + _restore_wall_ghost(world_position) + return true + _commit_wall(_wall_drag_start, end) + _wall_anchor = Vector2.INF + _wall_anchor_active = false + _restore_wall_ghost(world_position) + return true + if _active_tool == &"wall_eraser": + if event.pressed: + _wall_erase_start = _snap(world_position) if _snap_enabled else world_position + _wall_erase_dragging = true + update_pointer(world_position) + return true + if _wall_erase_dragging: + var erase_end: Vector2 = _snap(world_position) if _snap_enabled else world_position + var erase_rect: Rect2 = _drag_rect(_wall_erase_start, erase_end) + _wall_erase_dragging = false + _wall_erase_start = Vector2.INF + if erase_rect.size.x >= MIN_WALL_ERASE_SIZE and erase_rect.size.y >= MIN_WALL_ERASE_SIZE: + var changed: int = _erase_wall_region(erase_rect) + if changed > 0: + _wall_areas_erased += 1 + document_change_requested.emit() + update_pointer(world_position) + return true + return false + + +func handle_mouse_motion(world_position: Vector2) -> bool: + if not is_active(): + return false + update_pointer(world_position) + return true + + +func cancel_active_gesture() -> void: + _wall_dragging = false + _wall_anchor = Vector2.INF + _wall_drag_start = Vector2.INF + _wall_anchor_active = false + _wall_erase_dragging = false + _wall_erase_start = Vector2.INF + if _active_tool == &"wall" and _ghost != null and is_instance_valid(_ghost): + (_ghost as StageWallPanel).configure(DEFAULT_PANEL_SIZE, _wall_finish, _wall_tint) + elif _active_tool == &"wall_eraser" and _ghost != null and is_instance_valid(_ghost): + _set_wall_eraser_preview_rect(Rect2(_wall_erase_pointer, WALL_ERASER_CURSOR_SIZE)) + + +func exit_tool() -> void: + if _single_active: + cancel_single_transform() + cancel_active_gesture() + _free_ghost() + _active_tool = &"" + _asset_path = "" + _group_kind = &"" + _group_snapshots.clear() + _group_pending = false + preview_state_changed.emit(false) + + +func begin_group_transform(kind: StringName, members: Array[Node2D]) -> bool: + if _single_active or members.is_empty() or kind not in [&"move", &"resize"]: + return false + var snapshots: Array[Dictionary] = [] + var union_bounds: Rect2 = Rect2() + var has_bounds: bool = false + for member: Node2D in members: + if member == null or not is_instance_valid(member): + return false + var snapshot: Dictionary = {"node": member, "position": member.global_position, "rotation": member.global_rotation, "scale": member.scale} + var bounds: Rect2 + if member.get_script() == WALL_PANEL: + var wall: StageWallPanel = member as StageWallPanel + snapshot["panel_size"] = wall.panel_size + bounds = member.global_transform * wall.get_local_bounds() + elif member.get_script() == SCENERY_ARTWORK: + var artwork: StageSceneryArtwork = member as StageSceneryArtwork + bounds = STAGE_SELECTION.get_world_aabb(member) + if artwork.missing and not artwork.has_known_bounds: + if kind == &"resize": + return false + else: + return false + if kind == &"resize" and (bounds.size.x <= 0.0 or bounds.size.y <= 0.0): + return false + snapshots.append(snapshot) + union_bounds = bounds if not has_bounds else union_bounds.merge(bounds) + has_bounds = true + _group_snapshots = snapshots + _group_kind = kind + _group_bounds = union_bounds + _group_pending = true + preview_state_changed.emit(true) + return true + + +func preview_group(delta: Vector2, factor: float) -> bool: + if not _group_pending or factor <= 0.0 or not delta.is_finite(): + return false + # Validate the whole preview before moving any member, so a rejected factor + # never leaves a partially transformed group. + for snapshot: Dictionary in _group_snapshots: + var member: Node2D = snapshot["node"] as Node2D + if member == null or not is_instance_valid(member): + return false + if _group_kind == &"resize": + if member.get_script() == WALL_PANEL: + var next_size: Vector2 = (snapshot["panel_size"] as Vector2) * factor + if next_size.x < MIN_PANEL_SIZE or next_size.y < MIN_PANEL_SIZE: + return false + elif member.get_script() == SCENERY_ARTWORK: + var next_scale: Vector2 = (snapshot["scale"] as Vector2) * factor + if next_scale.x < 0.08 or next_scale.x > 12.0 or next_scale.y < 0.08 or next_scale.y > 12.0: + return false + var anchor: Vector2 = _group_bounds.position + for snapshot: Dictionary in _group_snapshots: + var member: Node2D = snapshot["node"] as Node2D + if member == null or not is_instance_valid(member): + return false + if _group_kind == &"move": + member.global_position = (snapshot["position"] as Vector2) + delta + continue + var art_scale: Vector2 = snapshot["scale"] as Vector2 + var new_scale: Vector2 = art_scale * factor + member.global_position = anchor + ((snapshot["position"] as Vector2) - anchor) * factor + member.global_rotation = float(snapshot["rotation"]) + if member.get_script() == WALL_PANEL: + var wall: StageWallPanel = member as StageWallPanel + wall.configure((snapshot["panel_size"] as Vector2) * factor, wall.finish_id, wall.tint) + else: + member.scale = new_scale + return true + + +func commit_group() -> void: + if not _group_pending: + return + _group_pending = false + _group_snapshots.clear() + _group_kind = &"" + preview_state_changed.emit(false) + document_change_requested.emit() + + +func begin_single_transform(node: Node2D, kind: StringName, pointer: Vector2, dragged_corner: Vector2 = Vector2.INF) -> bool: + if _single_active or _group_pending or node == null or not is_instance_valid(node) or kind not in [&"move", &"resize"]: + return false + if node.get_script() != WALL_PANEL and node.get_script() != SCENERY_ARTWORK: + return false + if not pointer.is_finite(): + return false + var bounds: Rect2 = _world_bounds(node) + if kind == &"resize" and (not bounds.position.is_finite() or not bounds.size.is_finite() or bounds.size.x <= 0.0 or bounds.size.y <= 0.0): + return false + if node.get_script() == SCENERY_ARTWORK: + var artwork: StageSceneryArtwork = node as StageSceneryArtwork + if artwork.missing and not artwork.has_known_bounds and kind == &"resize": + return false + var corner_index: int = _nearest_corner_index(bounds, dragged_corner if dragged_corner.is_finite() else pointer) + var corners: Array[Vector2] = _corners(bounds) + var snapshot: Dictionary = { + "node": node, + "position": node.global_position, + "rotation": node.global_rotation, + "scale": node.scale, + "bounds": bounds, + "pointer": pointer, + "corner_index": corner_index, + "dragged_corner": corners[corner_index], + "pivot": _opposite_corner(bounds, corner_index), + } + if node.get_script() == WALL_PANEL: + snapshot["panel_size"] = (node as StageWallPanel).panel_size + else: + var initial_scale: Vector2 = node.scale + if not initial_scale.is_finite() or initial_scale.x <= 0.0 or initial_scale.y <= 0.0: + return false + _single_snapshot = snapshot + _single_kind = kind + _single_active = true + _single_release_guard = false + single_gesture_changed.emit(true) + return true + + +func preview_single_transform(pointer: Vector2) -> bool: + if not _single_active or not pointer.is_finite(): + return false + var node: Node2D = _single_snapshot.get("node") as Node2D + if node == null or not is_instance_valid(node): + return false + if _single_kind == &"move": + var start_bounds: Rect2 = _single_snapshot["bounds"] + var raw_delta: Vector2 = pointer - (_single_snapshot["pointer"] as Vector2) + var delta: Vector2 = raw_delta + if _snap_enabled: + delta = _snap(start_bounds.position + raw_delta) - start_bounds.position + var next_position: Vector2 = (_single_snapshot["position"] as Vector2) + delta + if not next_position.is_finite(): + return false + node.global_position = next_position + return true + var dragged_corner: Vector2 = _single_snapshot["dragged_corner"] + var pivot: Vector2 = _single_snapshot["pivot"] + var diagonal: Vector2 = dragged_corner - pivot + var denominator: float = diagonal.length_squared() + if denominator <= 0.0001: + return false + var raw_factor: float = 1.0 + (pointer - (_single_snapshot["pointer"] as Vector2)).dot(diagonal) / denominator + return _apply_single_scale(raw_factor, pivot) + + +func commit_single_transform() -> bool: + if not _single_active: + return false + var changed: bool = _single_transform_changed() + _single_active = false + _single_kind = &"" + _single_snapshot.clear() + single_gesture_changed.emit(false) + if changed: + document_change_requested.emit() + return changed + + +func cancel_single_transform() -> void: + if not _single_active: + return + _restore_single_snapshot() + _single_active = false + _single_kind = &"" + _single_snapshot.clear() + _single_release_guard = true + single_gesture_changed.emit(false) + + +func step_single_size(node: Node2D, factor: float) -> bool: + if node == null or not is_instance_valid(node) or not is_finite(factor) or factor <= 0.0 or _single_active or _group_pending: + return false + if node.get_script() != WALL_PANEL and node.get_script() != SCENERY_ARTWORK: + return false + var bounds: Rect2 = _world_bounds(node) + if bounds.size.x <= 0.0 or bounds.size.y <= 0.0: + return false + if node.get_script() == SCENERY_ARTWORK: + var artwork: StageSceneryArtwork = node as StageSceneryArtwork + if artwork.missing and not artwork.has_known_bounds: + return false + var snapshot: Dictionary = { + "node": node, + "position": node.global_position, + "rotation": node.global_rotation, + "scale": node.scale, + "bounds": bounds, + "pivot": bounds.position, + "panel_size": (node as StageWallPanel).panel_size if node.get_script() == WALL_PANEL else Vector2.ZERO, + } + var pivot: Vector2 = bounds.position + if not _can_apply_single_scale(snapshot, factor): + return false + _apply_single_scale_to_snapshot(snapshot, factor, pivot) + if _snapshot_matches_current(snapshot): + return false + document_change_requested.emit() + return true + + +func cancel_group() -> void: + if not _group_pending: + return + for snapshot: Dictionary in _group_snapshots: + var member: Node2D = snapshot["node"] as Node2D + if member == null or not is_instance_valid(member): + continue + member.global_position = snapshot["position"] as Vector2 + member.global_rotation = float(snapshot["rotation"]) + member.scale = snapshot["scale"] as Vector2 + if member.get_script() == WALL_PANEL and snapshot.has("panel_size"): + var wall: StageWallPanel = member as StageWallPanel + wall.configure(snapshot["panel_size"] as Vector2, wall.finish_id, wall.tint) + _group_pending = false + _group_snapshots.clear() + _group_kind = &"" + preview_state_changed.emit(false) + + +static func bounds_for(members: Array[Node2D]) -> Rect2: + var result: Rect2 = Rect2() + var has_rect: bool = false + for member: Node2D in members: + var rect: Rect2 = STAGE_SELECTION.get_world_aabb(member) if member.get_script() == SCENERY_ARTWORK else (member.global_transform * (member as StageWallPanel).get_local_bounds()) + result = rect if not has_rect else result.merge(rect) + has_rect = true + return result + + +func _commit_artwork(position: Vector2) -> void: + var artwork := SCENERY_ARTWORK.new() as StageSceneryArtwork + if not artwork.load_visual(_asset_path): + artwork.free() + return + artwork.display_name = String(PROP_FILE.from_file_dict(PROP_FILE.load(_asset_path)).get("prop_name", artwork.display_name)) + artwork.set_depth(_depth) + artwork.global_position = _snap_visual_center(artwork, position) + _world.add_child(artwork) + placement_committed.emit(artwork) + document_change_requested.emit() + + +func _commit_wall(first: Vector2, second: Vector2) -> void: + var size: Vector2 = Vector2(maxf(MIN_PANEL_SIZE, absf(second.x - first.x)), maxf(MIN_PANEL_SIZE, absf(second.y - first.y))) + var panel := WALL_PANEL.new() as StageWallPanel + panel.configure(size, _wall_finish, _wall_tint) + panel.global_position = (first + second) * 0.5 + _world.add_child(panel) + placement_committed.emit(panel) + document_change_requested.emit() + + +func _restore_wall_ghost(world_position: Vector2) -> void: + if _ghost == null or not is_instance_valid(_ghost) or _ghost.get_script() != WALL_PANEL: + return + (_ghost as StageWallPanel).configure(DEFAULT_PANEL_SIZE, _wall_finish, _wall_tint) + update_pointer(world_position) + + +func _erase_wall_region(world_rect: Rect2) -> int: + if _world == null or world_rect.size.x <= 0.0 or world_rect.size.y <= 0.0: + return 0 + var changed_panels: int = 0 + for child: Node in _world.get_children().duplicate(): + if child.get_script() != WALL_PANEL: + continue + var wall: StageWallPanel = child as StageWallPanel + var panel_rect: Rect2 = wall.get_local_bounds() + var erase_corners: Array[Vector2] = [world_rect.position, Vector2(world_rect.end.x, world_rect.position.y), world_rect.end, Vector2(world_rect.position.x, world_rect.end.y)] + var local_erase := Rect2() + for corner_index: int in erase_corners.size(): + var local_point: Vector2 = wall.to_local(erase_corners[corner_index]) + if corner_index == 0: + local_erase = Rect2(local_point, Vector2.ZERO) + else: + local_erase = local_erase.expand(local_point) + if not panel_rect.intersects(local_erase): + continue + var cut: Rect2 = panel_rect.intersection(local_erase) + if cut.size.x <= 0.0 or cut.size.y <= 0.0: + continue + var fragments: Array[Rect2] = _wall_rect_fragments(panel_rect, cut) + var parent: Node = wall.get_parent() + if parent == null: + continue + var sibling_index: int = wall.get_index() + var source_transform: Transform2D = wall.transform + var source_motif_origin: Vector2 = wall.get_effective_motif_origin_world() + for fragment_rect: Rect2 in fragments: + var fragment := WALL_PANEL.new() as StageWallPanel + fragment.transform = source_transform + fragment.position += source_transform.basis_xform(fragment_rect.get_center()) + fragment.configure(fragment_rect.size, wall.finish_id, wall.tint) + parent.add_child(fragment) + fragment.motif_origin_offset = source_motif_origin - fragment.global_position + parent.move_child(fragment, sibling_index) + sibling_index += 1 + parent.remove_child(wall) + wall.queue_free() + changed_panels += 1 + return changed_panels + + +func _wall_rect_fragments(panel_rect: Rect2, cut: Rect2) -> Array[Rect2]: + var fragments: Array[Rect2] = [] + _add_wall_fragment(fragments, Rect2(panel_rect.position, Vector2(panel_rect.size.x, cut.position.y - panel_rect.position.y))) + _add_wall_fragment(fragments, Rect2(Vector2(panel_rect.position.x, cut.end.y), Vector2(panel_rect.size.x, panel_rect.end.y - cut.end.y))) + _add_wall_fragment(fragments, Rect2(Vector2(panel_rect.position.x, cut.position.y), Vector2(cut.position.x - panel_rect.position.x, cut.size.y))) + _add_wall_fragment(fragments, Rect2(Vector2(cut.end.x, cut.position.y), Vector2(panel_rect.end.x - cut.end.x, cut.size.y))) + return fragments + + +func _add_wall_fragment(fragments: Array[Rect2], rect: Rect2) -> void: + if rect.size.x > 1.0 and rect.size.y > 1.0: + fragments.append(rect) + + +func _drag_rect(first: Vector2, second: Vector2) -> Rect2: + var top_left: Vector2 = Vector2(minf(first.x, second.x), minf(first.y, second.y)) + var size: Vector2 = Vector2(absf(first.x - second.x), absf(first.y - second.y)) + return Rect2(top_left, size) + + +func _set_wall_eraser_preview_rect(rect: Rect2) -> void: + if _ghost == null or not is_instance_valid(_ghost): + return + var preview: Node2D = _ghost + var normalized: Rect2 = rect.abs() + var size: Vector2 = Vector2(maxf(1.0, normalized.size.x), maxf(1.0, normalized.size.y)) + var half: Vector2 = size * 0.5 + preview.global_position = normalized.position + half + var points := PackedVector2Array([Vector2(-half.x, -half.y), Vector2(half.x, -half.y), Vector2(half.x, half.y), Vector2(-half.x, half.y)]) + var fill: Polygon2D = preview.get_node("EraseArea") as Polygon2D + fill.polygon = points + var outline: Line2D = preview.get_node("EraseOutline") as Line2D + outline.points = PackedVector2Array([points[0], points[1], points[2], points[3], points[0]]) + var inset: Vector2 = Vector2(minf(8.0, half.x * 0.35), minf(8.0, half.y * 0.35)) + var cross_a: Line2D = preview.get_node("EraseCrossA") as Line2D + cross_a.points = PackedVector2Array([Vector2(-half.x + inset.x, -half.y + inset.y), Vector2(half.x - inset.x, half.y - inset.y)]) + var cross_b: Line2D = preview.get_node("EraseCrossB") as Line2D + cross_b.points = PackedVector2Array([Vector2(half.x - inset.x, -half.y + inset.y), Vector2(-half.x + inset.x, half.y - inset.y)]) + + +func _snap(position: Vector2) -> Vector2: + if not _snap_enabled: + return position + return Vector2(roundf(position.x / _grid_size) * _grid_size, roundf(position.y / _grid_size) * _grid_size) + + +func _apply_single_scale(factor: float, pivot: Vector2) -> bool: + if not _can_apply_single_scale(_single_snapshot, factor): + return false + _apply_single_scale_to_snapshot(_single_snapshot, factor, pivot) + return true + + +func _can_apply_single_scale(snapshot: Dictionary, factor: float) -> bool: + if not is_finite(factor) or factor <= 0.0: + return false + var node: Node2D = snapshot.get("node") as Node2D + if node == null or not is_instance_valid(node): + return false + var next_position: Vector2 = (snapshot["pivot"] as Vector2) + ((snapshot["position"] as Vector2) - (snapshot["pivot"] as Vector2)) * factor + if not next_position.is_finite(): + return false + if node.get_script() == WALL_PANEL: + var next_size: Vector2 = (snapshot["panel_size"] as Vector2) * factor + return next_size.is_finite() and next_size.x >= MIN_PANEL_SIZE and next_size.y >= MIN_PANEL_SIZE + var start_scale: Vector2 = snapshot["scale"] as Vector2 + var next_scale: Vector2 = start_scale * factor + return start_scale.is_finite() and start_scale.x > 0.0 and start_scale.y > 0.0 and next_scale.is_finite() and next_scale.x >= MIN_ART_SCALE and next_scale.x <= MAX_ART_SCALE and next_scale.y >= MIN_ART_SCALE and next_scale.y <= MAX_ART_SCALE + + +func _apply_single_scale_to_snapshot(snapshot: Dictionary, factor: float, pivot: Vector2) -> void: + var node: Node2D = snapshot["node"] as Node2D + node.global_position = pivot + ((snapshot["position"] as Vector2) - pivot) * factor + node.global_rotation = float(snapshot["rotation"]) + if node.get_script() == WALL_PANEL: + var wall: StageWallPanel = node as StageWallPanel + wall.scale = snapshot["scale"] as Vector2 + wall.configure((snapshot["panel_size"] as Vector2) * factor, wall.finish_id, wall.tint) + else: + node.scale = (snapshot["scale"] as Vector2) * factor + + +func _restore_single_snapshot() -> void: + var node: Node2D = _single_snapshot.get("node") as Node2D + if node == null or not is_instance_valid(node): + return + node.global_position = _single_snapshot["position"] as Vector2 + node.global_rotation = float(_single_snapshot["rotation"]) + node.scale = _single_snapshot["scale"] as Vector2 + if node.get_script() == WALL_PANEL: + var wall: StageWallPanel = node as StageWallPanel + wall.configure(_single_snapshot["panel_size"] as Vector2, wall.finish_id, wall.tint) + + +func _single_transform_changed() -> bool: + return not _snapshot_matches_current(_single_snapshot) + + +func _snapshot_matches_current(snapshot: Dictionary) -> bool: + var node: Node2D = snapshot.get("node") as Node2D + if node == null or not is_instance_valid(node): + return false + if not node.global_position.is_equal_approx(snapshot["position"] as Vector2): + return false + if not is_equal_approx(node.global_rotation, float(snapshot["rotation"])) or not node.scale.is_equal_approx(snapshot["scale"] as Vector2): + return false + if node.get_script() == WALL_PANEL: + return (node as StageWallPanel).panel_size.is_equal_approx(snapshot["panel_size"] as Vector2) + return true + + +func _world_bounds(node: Node2D) -> Rect2: + if node.get_script() == WALL_PANEL: + return node.global_transform * (node as StageWallPanel).get_local_bounds() + if node.get_script() == SCENERY_ARTWORK: + return (node as StageSceneryArtwork).get_visual_world_bounds() + return STAGE_SELECTION.get_world_aabb(node) + + +func _corners(bounds: Rect2) -> Array[Vector2]: + return [bounds.position, Vector2(bounds.end.x, bounds.position.y), bounds.end, Vector2(bounds.position.x, bounds.end.y)] + + +func _nearest_corner_index(bounds: Rect2, point: Vector2) -> int: + var corners: Array[Vector2] = _corners(bounds) + var nearest_index: int = 0 + var nearest_distance: float = INF + for index: int in corners.size(): + var distance: float = corners[index].distance_squared_to(point) + if distance < nearest_distance: + nearest_distance = distance + nearest_index = index + return nearest_index + + +func _opposite_corner(bounds: Rect2, corner_index: int) -> Vector2: + match corner_index: + 0: + return bounds.end + 1: + return Vector2(bounds.position.x, bounds.end.y) + 2: + return bounds.position + _: + return Vector2(bounds.end.x, bounds.position.y) + + +func _snap_visual_center(node: Node2D, desired_center: Vector2) -> Vector2: + var target_center: Vector2 = _snap(desired_center) if _snap_enabled and _grid_size > 0.0 else desired_center + if node.get_script() != SCENERY_ARTWORK: + return target_center + var bounds: Rect2 = _world_bounds(node) + return node.global_position + target_center - bounds.get_center() + + +func _move_node_visible_top_left(node: Node2D, desired_top_left: Vector2) -> Vector2: + if node.get_script() not in [WALL_PANEL, SCENERY_ARTWORK]: + return desired_top_left + var bounds: Rect2 = _world_bounds(node) + return node.global_position + desired_top_left - bounds.position + + +func _free_ghost() -> void: + if _ghost != null and is_instance_valid(_ghost): + _ghost.queue_free() + _ghost = null diff --git a/scripts/editors/stage/stage_scenery_controller.gd.uid b/scripts/editors/stage/stage_scenery_controller.gd.uid new file mode 100644 index 0000000..c86d363 --- /dev/null +++ b/scripts/editors/stage/stage_scenery_controller.gd.uid @@ -0,0 +1 @@ +uid://gvfhmaqednxv diff --git a/scripts/editors/stage/stage_terrain_painter.gd b/scripts/editors/stage/stage_terrain_painter.gd index 6e34e61..af2a8f6 100644 --- a/scripts/editors/stage/stage_terrain_painter.gd +++ b/scripts/editors/stage/stage_terrain_painter.gd @@ -20,6 +20,7 @@ var _overlay: StagePlacementOverlay var _spatial_index: StageSpatialIndex var _placement_id: String = "" +var _ground_finish: StringName = &"finish_ground_grass" var _dragging: bool = false var _painting: bool = false var _press_world: Vector2 = Vector2.ZERO @@ -46,6 +47,14 @@ func configure( _spatial_index = spatial_index +func set_ground_finish(finish_id: StringName) -> void: + _ground_finish = finish_id if finish_id in [ + &"finish_ground_grass", &"finish_ground_dirt", &"finish_ground_sidewalk", + &"finish_ground_asphalt", &"finish_ground_brick", &"finish_ground_wood", + &"finish_ground_paint" + ] else &"finish_ground_grass" + + func begin(placement_id: String, world_pos: Vector2, snap_enabled: bool, grid_size: float) -> void: if _spawner == null or not _spawner.is_terrain_id(placement_id): return @@ -94,6 +103,7 @@ func commit(placement_id: String, snap_enabled: bool, grid_size: float) -> void: var click_pos: Vector2 = _snap_to_grid(_press_world, grid_size) if snap_enabled else _press_world var click_node: Node2D = _spawner.spawn(_placement_id, click_pos) if click_node != null: + _apply_ground_finish(click_node) placement_requested.emit(click_node) placed_count += 1 for cell: Vector2i in _drag_cells: @@ -102,6 +112,7 @@ func commit(placement_id: String, snap_enabled: bool, grid_size: float) -> void: var node: Node2D = _spawner.spawn(_placement_id, terrain_cell_center(cell, _placement_id)) if node == null: continue + _apply_ground_finish(node) placement_requested.emit(node) placed_count += 1 _drag_painted[cell] = true @@ -217,6 +228,7 @@ func refresh_terrain_ghosts() -> void: var ghost: Node2D = _spawner.spawn(_placement_id, terrain_cell_center(cell, _placement_id)) if ghost == null: continue + _apply_ground_finish(ghost) if ghost.get_parent() == _world: _world.remove_child(ghost) _ghost_holder.add_child(ghost) @@ -272,6 +284,11 @@ func _configure_ghost_collision(ghost: Node2D) -> void: rig.set_collision_bodies_enabled(false) +func _apply_ground_finish(node: Node2D) -> void: + if node is TERRAIN_BLOCK and (node as TerrainBlock).spawn_id == "ground": + (node as TerrainBlock).set_finish(_ground_finish) + + func _snap_to_grid(pos: Vector2, grid_size: float) -> Vector2: if grid_size <= 0.0: return pos diff --git a/scripts/editors/stage/stage_use_target_capture_bar.gd b/scripts/editors/stage/stage_use_target_capture_bar.gd index d6ec369..90cf4ed 100644 --- a/scripts/editors/stage/stage_use_target_capture_bar.gd +++ b/scripts/editors/stage/stage_use_target_capture_bar.gd @@ -4,6 +4,7 @@ extends PanelContainer signal cancel_requested signal change_prop_requested +signal capture_action_requested(action_id: StringName) const MAX_BAR_WIDTH: float = 800.0 const VIEWPORT_WIDTH_RATIO: float = 0.68 @@ -16,6 +17,7 @@ var _stage_theme: SandboxTheme var _top_bar_height: float = 64.0 var _horizontal_layout: HBoxContainer var _vertical_layout: VBoxContainer +var _wrapped_layout: VBoxContainer var _active_layout: Container var _actions_row: HBoxContainer var _actions_column: VBoxContainer @@ -23,6 +25,10 @@ var _actions_parent: BoxContainer var _prompt: Label var _change_prop_button: Button var _cancel_button: Button +var _custom_action_buttons: Array[Button] = [] +var _has_horizontal_safe_bounds: bool = false +var _safe_left: float = 0.0 +var _safe_right: float = 0.0 func configure(stage_theme: SandboxTheme, top_bar_height: float) -> void: @@ -43,6 +49,7 @@ func _ready() -> void: func set_capture_state(prop_selected: bool, prop_has_use_spots: bool = true) -> void: if _prompt == null: return + _clear_custom_actions() if not prop_selected: _prompt.text = "Choose a prop to use" elif prop_has_use_spots: @@ -50,10 +57,48 @@ func set_capture_state(prop_selected: bool, prop_has_use_spots: bool = true) -> else: _prompt.text = "This prop has no Use spots. Add one in EDIT." _change_prop_button.visible = prop_selected + _cancel_button.visible = true visible = true _layout_capture_bar() +func set_capture_actions(prompt: String, actions: Array[Dictionary]) -> void: + if _prompt == null: + return + _prompt.text = prompt + _change_prop_button.visible = false + _cancel_button.visible = false + _clear_custom_actions() + for action: Dictionary in actions: + var action_id: StringName = StringName(String(action.get("id", ""))) + if action_id.is_empty(): + continue + var button := Button.new() + button.name = "Action_%s" % String(action_id) + button.text = String(action.get("label", action_id)) + button.custom_minimum_size.x = ACTION_BUTTON_WIDTH + button.focus_mode = Control.FOCUS_ALL + button.pressed.connect(func() -> void: capture_action_requested.emit(action_id)) + _style_button(button, bool(action.get("primary", false))) + _custom_action_buttons.append(button) + _actions_parent.add_child(button) + visible = true + apply_theme() + _layout_capture_bar() + + +func set_horizontal_safe_bounds(left: float, right: float) -> void: + _has_horizontal_safe_bounds = true + _safe_left = minf(left, right) + _safe_right = maxf(left, right) + _layout_capture_bar() + + +func clear_horizontal_safe_bounds() -> void: + _has_horizontal_safe_bounds = false + _layout_capture_bar() + + func hide_capture() -> void: visible = false @@ -92,9 +137,16 @@ func apply_theme() -> void: _cancel_button.add_theme_font_size_override("font_size", body_size) _style_button(_change_prop_button, false) _style_button(_cancel_button, true) + for button: Button in _custom_action_buttons: + if body_font != null: + button.add_theme_font_override("font", body_font) + button.add_theme_font_size_override("font_size", body_size) + _style_button(button, button.name.ends_with("Keep") or button.name.ends_with("Done")) var button_height: float = maxf(DEFAULT_BUTTON_HEIGHT, float(_integer("stage_panels.action.min_target_size", 56, 48, 96))) _change_prop_button.custom_minimum_size.y = button_height _cancel_button.custom_minimum_size.y = button_height + for button: Button in _custom_action_buttons: + button.custom_minimum_size.y = button_height _actions_row.add_theme_constant_override("separation", _integer("stage_panels.shell.header_gap", 12, 8, 24)) _actions_column.add_theme_constant_override("separation", _integer("stage_panels.shell.header_gap", 10, 8, 24)) _layout_capture_bar() @@ -108,6 +160,9 @@ func _build_controls() -> void: _vertical_layout = VBoxContainer.new() _vertical_layout.size_flags_horizontal = Control.SIZE_EXPAND_FILL _vertical_layout.add_theme_constant_override("separation", 12) + _wrapped_layout = VBoxContainer.new() + _wrapped_layout.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _wrapped_layout.add_theme_constant_override("separation", 10) _active_layout = _horizontal_layout add_child(_active_layout) _prompt = Label.new() @@ -141,6 +196,17 @@ func _build_controls() -> void: _actions_row.add_child(_cancel_button) +func _clear_custom_actions() -> void: + for button: Button in _custom_action_buttons: + if button == null or not is_instance_valid(button): + continue + var parent: Node = button.get_parent() + if parent != null: + parent.remove_child(button) + button.queue_free() + _custom_action_buttons.clear() + + func _style_button(button: Button, prominent: bool) -> void: var accent: Color = _color("mode_colors.direct", Color("#FFB300")) var text_color: Color = Color("#171B24") if prominent else _color("stage_panels.type.text_color", Color("#F5F7FA")) @@ -177,20 +243,33 @@ func _layout_capture_bar() -> void: return var viewport_size: Vector2 = get_viewport_rect().size var inset: float = float(_integer("stage_panels.shell.edge_inset", 16, 12, 48)) - var available_width: float = maxf(1.0, viewport_size.x - inset * 2.0) + var safe_left: float = inset + var safe_right: float = viewport_size.x - inset + if _has_horizontal_safe_bounds: + safe_left = clampf(_safe_left, inset, maxf(inset, viewport_size.x - inset)) + safe_right = clampf(_safe_right, safe_left + 1.0, maxf(safe_left + 1.0, viewport_size.x - inset)) + var available_width: float = maxf(1.0, safe_right - safe_left) var width: float = minf(MAX_BAR_WIDTH, minf(viewport_size.x * VIEWPORT_WIDTH_RATIO, available_width)) custom_minimum_size.x = width size.x = width - position = Vector2((viewport_size.x - width) * 0.5, _top_bar_height + 56.0) - _set_layout(width < HORIZONTAL_LAYOUT_MIN_WIDTH, width < ACTION_ROW_MIN_WIDTH) + var left: float = safe_left + (available_width - width) * 0.5 if not _has_horizontal_safe_bounds else safe_left + position = Vector2(left, _top_bar_height + 56.0) + var many_actions: bool = _custom_action_buttons.size() > 3 + var stack_actions: bool = width < ACTION_ROW_MIN_WIDTH or (many_actions and width < 620.0) + var wrapped: bool = (_has_horizontal_safe_bounds and width < HORIZONTAL_LAYOUT_MIN_WIDTH and width >= ACTION_ROW_MIN_WIDTH) or (many_actions and not stack_actions) + _set_layout(width < HORIZONTAL_LAYOUT_MIN_WIDTH or wrapped, stack_actions, wrapped) + _fit_custom_action_widths(width, wrapped) call_deferred("_fit_height_to_content") -func _set_layout(compact: bool, stack_actions: bool) -> void: - var target_layout: Container = _vertical_layout if compact else _horizontal_layout +func _set_layout(compact: bool, stack_actions: bool, wrapped: bool = false) -> void: + var target_layout: Container = _wrapped_layout if wrapped else (_vertical_layout if compact else _horizontal_layout) var target_actions: BoxContainer = _actions_column if stack_actions else _actions_row if _actions_parent != target_actions: var previous_actions: BoxContainer = _actions_parent + for button: Button in _custom_action_buttons: + if button.get_parent() == previous_actions: + previous_actions.remove_child(button) previous_actions.remove_child(_change_prop_button) previous_actions.remove_child(_cancel_button) _active_layout.remove_child(previous_actions) @@ -198,6 +277,8 @@ func _set_layout(compact: bool, stack_actions: bool) -> void: _active_layout.add_child(_actions_parent) _actions_parent.add_child(_change_prop_button) _actions_parent.add_child(_cancel_button) + for button: Button in _custom_action_buttons: + _actions_parent.add_child(button) if _active_layout != target_layout: _active_layout.remove_child(_prompt) _active_layout.remove_child(_actions_parent) @@ -212,6 +293,18 @@ func _set_layout(compact: bool, stack_actions: bool) -> void: _cancel_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL if stack_actions else Control.SIZE_FILL +func _fit_custom_action_widths(width: float, wrapped: bool) -> void: + var button_width: float = ACTION_BUTTON_WIDTH + var body_size: int = clampi(_integer("stage_panels.type.body_size", 20, 16, 32) + 2, 20, 30) + var action_font_size: int = maxi(18, body_size - (4 if wrapped else 0)) + if wrapped and not _custom_action_buttons.is_empty(): + var gaps: float = float(maxi(0, _custom_action_buttons.size() - 1) * 12) + button_width = minf(ACTION_BUTTON_WIDTH, maxf(144.0, (width - 44.0 - gaps) / float(_custom_action_buttons.size()))) + for button: Button in _custom_action_buttons: + button.custom_minimum_size.x = button_width + button.add_theme_font_size_override("font_size", action_font_size) + + func _fit_height_to_content() -> void: if not is_inside_tree() or not visible: return diff --git a/scripts/props/prop_library.gd b/scripts/props/prop_library.gd index 1884b31..fa95295 100644 --- a/scripts/props/prop_library.gd +++ b/scripts/props/prop_library.gd @@ -58,6 +58,14 @@ static func scan(dir_path: String = PROP_FILE.DEFAULT_DIR) -> Array[Dictionary]: static func make_custom_entry(path: String) -> Dictionary: + return _make_custom_entry(path, false) + + +static func make_visual_entry(path: String) -> Dictionary: + return _make_custom_entry(path, true) + + +static func _make_custom_entry(path: String, allow_visual_only: bool) -> Dictionary: var normalized_path: String = canonical_path(path) if not is_custom_path(normalized_path): return {} var raw: Dictionary = PROP_FILE.load(normalized_path) @@ -70,13 +78,52 @@ static func make_custom_entry(path: String) -> Dictionary: return {} var shapes: Array[Dictionary] = parsed.get("shapes", []) var hull: PackedVector2Array = PROP_VISUALS.collision_hull(shapes) - if shapes.is_empty() or hull.size() < 3: + if shapes.is_empty() or (not allow_visual_only and hull.size() < 3): push_warning("PropLibrary: custom prop '%s' has no valid closed collision geometry." % normalized_path) return {} var name := String(parsed.get("prop_name", "")).strip_edges() if name.is_empty(): name = normalized_path.get_file().get_basename() var preset: int = int(parsed.get("material_preset", PROP_BLOCK.MaterialPreset.NONE)) - return {"id": normalized_path, "path": normalized_path, "name": name, "material_preset": preset, "material_label": PROP_FILE.preset_to_string(preset).capitalize(), "is_custom": true, "payload": PROP_FILE.flatten_to_payload(shapes), "shapes": shapes, "collision_hull": hull, "anchors": parsed.get("anchors", {})} + var tags: Array[String] = parsed.get("tags", []) as Array[String] + if tags.is_empty(): + tags = _legacy_tags_for_path(normalized_path) + return {"id": normalized_path, "path": normalized_path, "name": name, "material_preset": preset, "material_label": PROP_FILE.preset_to_string(preset).capitalize(), "is_custom": true, "payload": PROP_FILE.flatten_to_payload(shapes), "shapes": shapes, "collision_hull": hull, "anchors": parsed.get("anchors", {}), "tags": tags, "visual_only": hull.size() < 3} + + +static func scan_visual_entries(dir_path: String = PROP_FILE.DEFAULT_DIR) -> Array[Dictionary]: + var entries: Array[Dictionary] = [] + if not DirAccess.dir_exists_absolute(dir_path): + return entries + var directory: DirAccess = DirAccess.open(dir_path) + if directory == null: + return entries + var paths: Array[String] = [] + directory.list_dir_begin() + var file_name: String = directory.get_next() + while not file_name.is_empty(): + if not directory.current_is_dir() and file_name.to_lower().ends_with(PROP_FILE.FILE_EXT): + paths.append(dir_path.path_join(file_name)) + file_name = directory.get_next() + directory.list_dir_end() + paths.sort_custom(func(a: String, b: String) -> bool: return a.nocasecmp_to(b) < 0) + for path: String in paths: + var entry: Dictionary = make_visual_entry(path) + if not entry.is_empty(): + entries.append(entry) + return entries + + +static func _legacy_tags_for_path(path: String) -> Array[String]: + var basename: String = path.get_file().get_basename().to_lower() + if basename.contains("tree") or basename.contains("plant"): + return ["nature", "objects"] if basename.contains("plant") else ["nature"] + if basename.contains("skyline") or basename.contains("storefront") or basename.contains("house"): + return ["buildings"] + if basename.contains("window") or basename.contains("fireplace") or basename.contains("clock"): + return ["walls", "objects"] + if basename.contains("bookshelf") or basename.contains("picture") or basename.contains("rug") or basename.contains("couch") or basename.contains("armchair") or basename.contains("table"): + return ["objects"] + return [] static func is_custom_path(path: String) -> bool: diff --git a/scripts/shared/ui/asset_card.gd b/scripts/shared/ui/asset_card.gd index 46e5c0c..cc7ea9b 100644 --- a/scripts/shared/ui/asset_card.gd +++ b/scripts/shared/ui/asset_card.gd @@ -10,7 +10,10 @@ var _dragging: bool = false var _drag_threshold: float = 8.0 var _icon: TextureRect = null var _glyph: Label = null +var _box: VBoxContainer = null var _label: Label = null +var _description: Label = null +var _status_label: Label = null func configure(payload: Dictionary, icon_texture: Texture2D = null, theme: Variant = null) -> void: _payload = payload.duplicate(true) @@ -25,11 +28,55 @@ func configure(payload: Dictionary, icon_texture: Texture2D = null, theme: Varia focus_mode = Control.FOCUS_ALL tooltip_text = String(_payload.get("label", "")) for child: Node in get_children(): child.queue_free() - var box := VBoxContainer.new(); box.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT); box.mouse_filter = Control.MOUSE_FILTER_IGNORE; box.clip_contents = true - box.alignment = BoxContainer.ALIGNMENT_CENTER; box.add_theme_constant_override("separation", 2); add_child(box) - _icon = TextureRect.new(); _icon.custom_minimum_size = thumbnail_size; _icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE; _icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED; _icon.texture = icon_texture; _icon.visible = icon_texture != null; _icon.mouse_filter = Control.MOUSE_FILTER_IGNORE; box.add_child(_icon) - _glyph = Label.new(); _glyph.text = String(_payload.get("icon", "")); _glyph.visible = icon_texture == null; _glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER; _glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE; _glyph.add_theme_font_size_override("font_size", 30); box.add_child(_glyph) - _label = Label.new(); _label.text = String(_payload.get("label", "")); _label.custom_minimum_size = Vector2(0, 28); _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER; _label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART; _label.clip_text = true; _label.mouse_filter = Control.MOUSE_FILTER_IGNORE; box.add_child(_label) + _box = VBoxContainer.new(); _box.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT); _box.mouse_filter = Control.MOUSE_FILTER_IGNORE; _box.clip_contents = true + _box.alignment = BoxContainer.ALIGNMENT_CENTER; _box.add_theme_constant_override("separation", 2); add_child(_box) + _icon = TextureRect.new(); _icon.custom_minimum_size = thumbnail_size; _icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE; _icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED; _icon.texture = icon_texture; _icon.visible = icon_texture != null; _icon.mouse_filter = Control.MOUSE_FILTER_IGNORE; _box.add_child(_icon) + _glyph = Label.new(); _glyph.text = String(_payload.get("icon", "")); _glyph.visible = icon_texture == null; _glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER; _glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE; _glyph.add_theme_font_size_override("font_size", 30); _box.add_child(_glyph) + _label = Label.new(); _label.text = String(_payload.get("label", "")); _label.custom_minimum_size = Vector2(0, 28); _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER; _label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART; _label.clip_text = true; _label.mouse_filter = Control.MOUSE_FILTER_IGNORE; _box.add_child(_label) + var description: String = String(_payload.get("description", "")) + var status: String = String(_payload.get("status", "")) + if not status.is_empty(): + _status_label = Label.new() + _status_label.name = "StatusLabel" + _status_label.text = status + _status_label.custom_minimum_size = Vector2(0.0, 22.0) + _status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _status_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _status_label.add_theme_font_size_override("font_size", 16) + _status_label.add_theme_color_override("font_color", _payload.get("status_color", Color.WHITE)) + _status_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + _box.add_child(_status_label) + if not description.is_empty(): + _description = Label.new() + _description.text = description + _description.custom_minimum_size = Vector2(0.0, 34.0) + _description.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _description.vertical_alignment = VERTICAL_ALIGNMENT_TOP + _description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _description.max_lines_visible = 2 + _description.add_theme_font_size_override("font_size", 14) + _description.mouse_filter = Control.MOUSE_FILTER_IGNORE + _box.add_child(_description) + + +func set_preview_control(preview: Control) -> void: + if preview == null or _box == null: + return + var preview_size: Vector2 = _icon.custom_minimum_size if _icon != null else Vector2(64.0, 64.0) + if _icon != null and _icon.get_parent() == _box: + _box.remove_child(_icon) + _icon.queue_free() + _icon = null + if _glyph != null and _glyph.get_parent() == _box: + _box.remove_child(_glyph) + _glyph.queue_free() + _glyph = null + preview.custom_minimum_size = preview_size + preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL + preview.mouse_filter = Control.MOUSE_FILTER_IGNORE + _box.add_child(preview) + _box.move_child(preview, 0) func set_thumbnail(texture: Texture2D) -> void: if _icon != null: _icon.texture = texture; _icon.visible = texture != null @@ -38,8 +85,20 @@ func set_thumbnail(texture: Texture2D) -> void: func thumbnail_rect() -> TextureRect: return _icon -func set_selected(selected: bool) -> void: - button_pressed = selected +func set_selected(selected: bool, selected_surface: Color = Color("#12394A"), selected_border: Color = Color("#22C6FF")) -> void: + toggle_mode = true + set_pressed_no_signal(selected) + if not selected: + return + var selected_style := StyleBoxFlat.new() + selected_style.bg_color = selected_surface + selected_style.border_color = selected_border + selected_style.set_border_width_all(3) + selected_style.set_corner_radius_all(8) + add_theme_stylebox_override("normal", selected_style) + add_theme_stylebox_override("pressed", selected_style) + add_theme_stylebox_override("hover", selected_style) + add_theme_stylebox_override("hover_pressed", selected_style) func payload() -> Dictionary: return _payload.duplicate(true) diff --git a/scripts/shared/ui/asset_selector.gd b/scripts/shared/ui/asset_selector.gd index 794fd20..0586cec 100644 --- a/scripts/shared/ui/asset_selector.gd +++ b/scripts/shared/ui/asset_selector.gd @@ -5,6 +5,8 @@ signal item_selected(entry: Dictionary) signal cancelled() signal browse_requested() signal refresh_requested() +signal make_artwork_requested() +signal edit_scenery_requested() const COLUMNS := 4 const ROWS := 3 @@ -13,6 +15,8 @@ const PAGE_SIZE := COLUMNS * ROWS const CELL_MIN_SIZE := Vector2(190.0, 220.0) const THUMB_SIZE := Vector2(156.0, 156.0) const ASSET_CARD := preload("res://scripts/shared/ui/asset_card.gd") +const BACKGROUND_CARD_PREVIEW := preload("res://scripts/stage/scenery/stage_background_card_preview.gd") +const STARTER_CARD_PREVIEW := preload("res://scripts/stage/scenery/stage_starter_card_preview.gd") var kind: String = "" @@ -24,6 +28,10 @@ var _placeholder_tex: ImageTexture = null var _ui_font: Font = null var _emoji_font: Font = null var _sandbox_theme: Variant = null +var _gallery_tab: String = "backgrounds" +var _tag_id: String = "all" +var _current_sky_id: String = "" +var _sky_card_group: ButtonGroup @onready var _title_label: Label = %TitleLabel @onready var _grid: GridContainer = %GridContainer @@ -34,15 +42,29 @@ var _sandbox_theme: Variant = null @onready var _browse_button: Button = %BrowseButton @onready var _refresh_button: Button = %RefreshButton @onready var _close_button: Button = %CloseButton +@onready var _tabs: HBoxContainer = %GalleryTabs +@onready var _backgrounds_tab: Button = %BackgroundsTab +@onready var _artwork_tab: Button = %ArtworkTab +@onready var _tag_filter: OptionButton = %TagFilter +@onready var _make_artwork_button: Button = %MakeArtworkButton +@onready var _edit_scenery_button: Button = %EditSceneryButton func _ready() -> void: exclusive = true + if _tag_filter.item_count == 0: + for label: String in ["All artwork", "Nature", "Buildings", "Walls", "Roads", "Objects"]: + _tag_filter.add_item(label) _prev_button.pressed.connect(_on_prev) _next_button.pressed.connect(_on_next) _browse_button.pressed.connect(func() -> void: browse_requested.emit()) _refresh_button.pressed.connect(func() -> void: refresh_requested.emit()) _close_button.pressed.connect(func() -> void: cancelled.emit()) + _backgrounds_tab.pressed.connect(func() -> void: _set_gallery_tab("backgrounds")) + _artwork_tab.pressed.connect(func() -> void: _set_gallery_tab("artwork")) + _tag_filter.item_selected.connect(_on_tag_filter_selected) + _make_artwork_button.pressed.connect(func() -> void: make_artwork_requested.emit()) + _edit_scenery_button.pressed.connect(func() -> void: edit_scenery_requested.emit()) get_tree().root.size_changed.connect(_on_root_size_changed) @@ -51,13 +73,25 @@ func _on_root_size_changed() -> void: popup_centered() -func open(p_kind: String, entries: Array[Dictionary]) -> void: +func open(p_kind: String, entries: Array[Dictionary], current_sky_id: String = "") -> void: kind = p_kind - _title_label.text = "Choose Your Stickman" if kind == "stickman" else "Choose a Prop" - _browse_button.visible = kind == "stickman" or kind == "prop" - _refresh_button.visible = kind == "stickman" or kind == "prop" - _empty_label.text = "No props found. Browse for a .prop file." if kind == "prop" else "No stickmen found." + _current_sky_id = current_sky_id if kind == "scenery" else "" + _title_label.text = "Choose Your Stickman" if kind == "stickman" else ("Choose Scenery" if kind == "scenery" else ("Choose a starter scene" if kind == "starters" else "Choose a Prop")) + _tabs.visible = kind == "scenery" + _tag_filter.visible = false + _make_artwork_button.visible = false + _edit_scenery_button.visible = false + _browse_button.visible = kind == "stickman" or kind == "prop" or kind == "scenery" + _refresh_button.visible = kind == "stickman" or kind == "prop" or kind == "scenery" + _empty_label.text = "No scenery found. Choose Make artwork to draw a new piece." if kind == "scenery" else ("No props found. Browse for a .prop file." if kind == "prop" else ("No starter scenes are available." if kind == "starters" else "No stickmen found.")) + _gallery_tab = "backgrounds" + _tag_id = "all" + _backgrounds_tab.set_pressed_no_signal(true) + _artwork_tab.set_pressed_no_signal(false) + _tag_filter.select(0) set_entries(entries) + if kind == "scenery": + _set_gallery_tab("backgrounds") popup_centered() @@ -76,6 +110,15 @@ func set_thumbnail(entry: Dictionary, tex: Texture2D) -> void: (_cell_texrects[key] as TextureRect).texture = tex +func set_current_sky(sky_id: StringName) -> void: + var next_id: String = String(sky_id) + if kind != "scenery" or _current_sky_id == next_id: + return + _current_sky_id = next_id + if _gallery_tab == "backgrounds": + _rebuild() + + func close() -> void: hide() @@ -129,6 +172,42 @@ func _entry_key(entry: Dictionary) -> String: return String(entry.get("id", "")) +func _on_tag_filter_selected(index: int) -> void: + var ids: Array[String] = ["all", "nature", "buildings", "walls", "roads", "objects"] + _tag_id = ids[clampi(index, 0, ids.size() - 1)] + _page = 0 + _rebuild() + + +func _set_gallery_tab(tab: String) -> void: + _gallery_tab = tab + _backgrounds_tab.set_pressed_no_signal(tab == "backgrounds") + _artwork_tab.set_pressed_no_signal(tab == "artwork") + _tag_filter.visible = kind == "scenery" and tab == "artwork" + _make_artwork_button.visible = kind == "scenery" and tab == "artwork" + _edit_scenery_button.visible = kind == "scenery" + _browse_button.visible = kind == "stickman" or kind == "prop" or (kind == "scenery" and tab == "artwork") + _refresh_button.visible = kind == "stickman" or kind == "prop" or (kind == "scenery" and tab == "artwork") + _empty_label.text = "No artwork in this group yet." if tab == "artwork" else "Choose a Sky look or wall finish." + _page = 0 + _rebuild() + + +func _filtered_entries() -> Array[Dictionary]: + if kind != "scenery": + return _entries + var filtered: Array[Dictionary] = [] + for entry: Dictionary in _entries: + if String(entry.get("gallery_tab", "artwork")) != _gallery_tab: + continue + if _gallery_tab == "artwork" and _tag_id != "all": + var tags: Array = entry.get("tags", []) as Array + if not tags.has(_tag_id): + continue + filtered.append(entry) + return filtered + + func _on_prev() -> void: if _page > 0: _page -= 1 @@ -136,36 +215,39 @@ func _on_prev() -> void: func _on_next() -> void: - if (_page + 1) * PAGE_SIZE < _entries.size(): + if (_page + 1) * PAGE_SIZE < _filtered_entries().size(): _page += 1 _rebuild() func _page_count() -> int: - if _entries.is_empty(): + var entries: Array[Dictionary] = _filtered_entries() + if entries.is_empty(): return 1 - return int(ceil(float(_entries.size()) / float(PAGE_SIZE))) + return int(ceil(float(entries.size()) / float(PAGE_SIZE))) func _rebuild() -> void: + _sky_card_group = ButtonGroup.new() for c: Node in _grid.get_children(): c.queue_free() _cell_texrects.clear() - _empty_label.visible = _entries.is_empty() - _grid.visible = not _entries.is_empty() + var filtered: Array[Dictionary] = _filtered_entries() + _empty_label.visible = filtered.is_empty() + _grid.visible = not filtered.is_empty() var total := _page_count() var single := total <= 1 _prev_button.visible = not single _next_button.visible = not single _page_label.visible = not single _prev_button.disabled = _page <= 0 - _next_button.disabled = (_page + 1) * PAGE_SIZE >= _entries.size() + _next_button.disabled = (_page + 1) * PAGE_SIZE >= filtered.size() _page_label.text = "Page %d/%d" % [_page + 1, total] - if _entries.is_empty(): + if filtered.is_empty(): return - var bounds := page_bounds(_entries.size(), _page) + var bounds := page_bounds(filtered.size(), _page) for i: int in range(int(bounds["start"]), int(bounds["end"])): - _grid.add_child(_build_cell(_entries[i])) + _grid.add_child(_build_cell(filtered[i])) func _build_cell(entry: Dictionary) -> Control: @@ -173,17 +255,52 @@ func _build_cell(entry: Dictionary) -> Control: var cell: Variant = ASSET_CARD.new() # Selector cells inherit the popup theme. Avoid a per-card state override so # opening a selector never implies that one asset is pre-selected. - cell.configure({"placement_id": _entry_key(entry), "label": String(entry.get("name", entry.get("id", ""))), "icon": "", "category": String(entry.get("material_label", "")), "thumbnail_size": _thumbnail_size}, _thumbnails.get(key, null), null) - cell.custom_minimum_size = _cell_min_size - cell.focus_mode = Control.FOCUS_NONE + var categories: Array = entry.get("tags", []) as Array + var category: String = ", ".join(PackedStringArray(categories)) if not categories.is_empty() else String(entry.get("category", entry.get("material_label", ""))) + var card_size: Vector2 = _cell_min_size + var thumb_size: Vector2 = _thumbnail_size + if kind == "starters": + card_size.y = maxf(card_size.y, 248.0) + thumb_size = Vector2(128.0, 128.0) + var card_payload: Dictionary = {"placement_id": _entry_key(entry), "label": String(entry.get("name", entry.get("id", ""))), "icon": String(entry.get("icon", "")), "category": category, "description": String(entry.get("description", "")), "thumbnail_size": thumb_size} + if entry.has("background_kind"): + card_payload["background_kind"] = entry["background_kind"] + card_payload["finish_id"] = entry.get("finish_id", "") + card_payload["id"] = entry.get("id", "") + var is_sky_choice: bool = String(entry.get("background_kind", "")) == "sky" + var is_current_sky: bool = is_sky_choice and String(entry.get("id", "")) == _current_sky_id + if is_current_sky: + card_payload["status"] = "Current Sky" + card_payload["status_color"] = _theme_color("mode_colors.edit", Color("#22C6FF")) + cell.configure(card_payload, _thumbnails.get(key, null), null) + cell.custom_minimum_size = card_size + cell.focus_mode = Control.FOCUS_ALL cell.activated.connect(func(_payload: Dictionary) -> void: _on_cell_pressed(entry)) + if is_sky_choice: + cell.button_group = _sky_card_group + cell.set_selected(is_current_sky, _theme_color("stage_panels.choice.selected_surface", Color("#12394A")), _theme_color("mode_colors.edit", Color("#22C6FF"))) var tex: Texture2D = _thumbnails.get(key, null) - cell.set_thumbnail(tex if tex != null else _get_placeholder()) - _cell_texrects[key] = cell.thumbnail_rect() + if kind == "starters": + var preview: Control = STARTER_CARD_PREVIEW.new() as Control + preview.call("configure", entry) + cell.set_preview_control(preview) + elif entry.has("background_kind"): + var preview: StageBackgroundCardPreview = BACKGROUND_CARD_PREVIEW.new() as StageBackgroundCardPreview + preview.configure(entry) + cell.set_preview_control(preview) + else: + cell.set_thumbnail(tex if tex != null else _get_placeholder()) + _cell_texrects[key] = cell.thumbnail_rect() return cell +func _theme_color(path: String, fallback: Color) -> Color: + if _sandbox_theme != null: + return _sandbox_theme.color(path, fallback) + return fallback + + func _on_cell_pressed(entry: Dictionary) -> void: item_selected.emit(entry) diff --git a/scripts/stage/scenery/stage_background_card_preview.gd b/scripts/stage/scenery/stage_background_card_preview.gd new file mode 100644 index 0000000..27d02c2 --- /dev/null +++ b/scripts/stage/scenery/stage_background_card_preview.gd @@ -0,0 +1,45 @@ +class_name StageBackgroundCardPreview +extends Control +## Reuses the Stage's Sky and wall-finish drawing rules in gallery cards. + +const SKY_VIEW: Script = preload("res://scripts/stage/scenery/stage_sky_view.gd") +const SURFACE_RENDERER: Script = preload("res://scripts/stage/scenery/stage_surface_renderer.gd") + +var _background_kind: String = "" +var _preview_id: StringName = &"" +var _sky_view: StageSkyView + + +func configure(entry: Dictionary) -> void: + _background_kind = String(entry.get("background_kind", "")) + _preview_id = StringName(String(entry.get("id", ""))) if _background_kind == "sky" else StringName(String(entry.get("finish_id", ""))) + clip_contents = true + mouse_filter = Control.MOUSE_FILTER_IGNORE + if _background_kind == "sky": + _sky_view = SKY_VIEW.new() as StageSkyView + _sky_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _sky_view.set_sky(_preview_id) + add_child(_sky_view) + var frame: Panel = Panel.new() + frame.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + frame.mouse_filter = Control.MOUSE_FILTER_IGNORE + var frame_style: StyleBoxFlat = StyleBoxFlat.new() + frame_style.bg_color = Color.TRANSPARENT + frame_style.draw_center = false + frame_style.set_border_width_all(2) + frame_style.border_color = Color("#D7DEE7") + frame_style.set_corner_radius_all(4) + frame.add_theme_stylebox_override("panel", frame_style) + add_child(frame) + queue_redraw() + + +func _draw() -> void: + if _background_kind == "wall" and SURFACE_RENDERER.is_wall_finish(_preview_id): + var wall_tint: Color = Color("#7FA36C") if _preview_id == &"finish_wall_paint" else Color.WHITE + SURFACE_RENDERER.draw_wall(self, Rect2(Vector2.ZERO, size), _preview_id, wall_tint, Vector2.ZERO) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_RESIZED: + queue_redraw() diff --git a/scripts/stage/scenery/stage_background_card_preview.gd.uid b/scripts/stage/scenery/stage_background_card_preview.gd.uid new file mode 100644 index 0000000..3c72652 --- /dev/null +++ b/scripts/stage/scenery/stage_background_card_preview.gd.uid @@ -0,0 +1 @@ +uid://csk4xjt1unoh diff --git a/scripts/stage/scenery/stage_scenery_artwork.gd b/scripts/stage/scenery/stage_scenery_artwork.gd new file mode 100644 index 0000000..b834763 --- /dev/null +++ b/scripts/stage/scenery/stage_scenery_artwork.gd @@ -0,0 +1,134 @@ +class_name StageSceneryArtwork +extends Node2D +## Visual-only live-reference Scenery placement. Never participates in physics. + +const PROP_FILE := preload("res://scripts/editors/prop/prop_file.gd") +const PROP_VISUALS := preload("res://scripts/props/prop_visuals.gd") +const DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd") +const CONTENT_PATHS := preload("res://scripts/shared/content_paths.gd") + +var asset_path: String = "" +var display_name: String = "Scenery" +var depth: StringName = &"near" +var last_known_local_bounds: Rect2 = Rect2(Vector2(-32.0, -32.0), Vector2(64.0, 64.0)) +var has_known_bounds: bool = false +var missing: bool = false +var _shapes: Array[Dictionary] = [] +var _visuals: Node2D + + +func load_visual(path: String) -> bool: + var normalized_path: String = CONTENT_PATHS.canonicalize(path) + if not FileAccess.file_exists(normalized_path): + _set_missing() + return false + var raw: Dictionary = PROP_FILE.load(normalized_path) + var parsed: Dictionary = PROP_FILE.from_file_dict(raw) + var parsed_shapes: Array[Dictionary] = parsed.get("shapes", []) as Array[Dictionary] + if parsed_shapes.is_empty(): + _set_missing() + return false + if _visuals != null and is_instance_valid(_visuals): + _visuals.queue_free() + _visuals = Node2D.new() + _visuals.name = "Visuals" + add_child(_visuals) + asset_path = normalized_path + _shapes = parsed_shapes + var bounds: Rect2 = PROP_VISUALS.build(_visuals, _shapes) + var stroke: float = _max_stroke_width(_shapes) * 0.5 + last_known_local_bounds = bounds.grow(stroke) if bounds.size.x > 0.0 or bounds.size.y > 0.0 else Rect2(Vector2(-32.0, -32.0), Vector2(64.0, 64.0)) + has_known_bounds = true + missing = false + set_process(false) + _apply_depth() + queue_redraw() + return true + + +func set_depth(value: StringName) -> void: + depth = value if value in [&"far", &"near", &"floor"] else &"near" + _apply_depth() + + +func _process(_delta: float) -> void: + if missing: + queue_redraw() + + +func get_visual_world_bounds() -> Rect2: + return global_transform * last_known_local_bounds + + +func get_saved_footprint() -> Rect2: + return last_known_local_bounds + + +func configure_missing(path: String, name: String, saved_bounds: Rect2, saved_depth: StringName) -> void: + asset_path = CONTENT_PATHS.canonicalize(path) + display_name = name.strip_edges() if not name.strip_edges().is_empty() else asset_path.get_file().get_basename() + if saved_bounds.size.x > 0.0 and saved_bounds.size.y > 0.0: + last_known_local_bounds = saved_bounds + has_known_bounds = true + else: + last_known_local_bounds = Rect2(Vector2(-32.0, -32.0), Vector2(64.0, 64.0)) + has_known_bounds = false + depth = saved_depth + load_visual(asset_path) + + +func _set_missing() -> void: + missing = true + set_process(true) + if _visuals != null and is_instance_valid(_visuals): + _visuals.queue_free() + _visuals = null + _apply_depth() + queue_redraw() + + +func _apply_depth() -> void: + z_index = DRAW_ORDER.scenery_z(depth) + + +func _draw() -> void: + if not missing: + return + var rect: Rect2 = last_known_local_bounds + draw_rect(rect, Color("#87909D30"), true) + _draw_dashed_line(rect.position, Vector2(rect.end.x, rect.position.y)) + _draw_dashed_line(Vector2(rect.end.x, rect.position.y), rect.end) + _draw_dashed_line(rect.end, Vector2(rect.position.x, rect.end.y)) + _draw_dashed_line(Vector2(rect.position.x, rect.end.y), rect.position) + var camera: Camera2D = get_viewport().get_camera_2d() if is_inside_tree() else null + var zoom: float = maxf(0.05, camera.zoom.x) if camera != null else 1.0 + draw_set_transform(rect.position + Vector2(0.0, -18.0), 0.0, Vector2.ONE / zoom) + draw_rect(Rect2(Vector2.ZERO, Vector2(188.0, 32.0)), Color("#242B37E8"), true) + draw_circle(Vector2(16.0, 16.0), 10.0, Color("#FFD166")) + var font: Font = ThemeDB.fallback_font + if font != null: + draw_string(font, Vector2(13.0, 22.0), "?", HORIZONTAL_ALIGNMENT_LEFT, -1.0, 16, Color("#242B37")) + draw_string(font, Vector2(32.0, 22.0), "Artwork not found", HORIZONTAL_ALIGNMENT_LEFT, -1.0, 18, Color("#F5F7FA")) + draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + + +func _draw_dashed_line(start: Vector2, finish: Vector2) -> void: + var length: float = start.distance_to(finish) + if length <= 0.0: + return + var direction: Vector2 = (finish - start) / length + var offset: float = 0.0 + var segment_index: int = 0 + while offset < length: + var segment_end: float = minf(length, offset + 9.0) + var color: Color = Color("#E2E6EC") if segment_index % 2 == 0 else Color("#687281") + draw_line(start + direction * offset, start + direction * segment_end, color, 2.0) + offset += 15.0 + segment_index += 1 + + +func _max_stroke_width(shapes: Array[Dictionary]) -> float: + var width: float = 0.0 + for shape: Dictionary in shapes: + width = maxf(width, float(shape.get("outline_width", 0.0))) + return width diff --git a/scripts/stage/scenery/stage_scenery_artwork.gd.uid b/scripts/stage/scenery/stage_scenery_artwork.gd.uid new file mode 100644 index 0000000..7cb6cc4 --- /dev/null +++ b/scripts/stage/scenery/stage_scenery_artwork.gd.uid @@ -0,0 +1 @@ +uid://ysop8p4ekoc4 diff --git a/scripts/stage/scenery/stage_sky_view.gd b/scripts/stage/scenery/stage_sky_view.gd new file mode 100644 index 0000000..85bf90a --- /dev/null +++ b/scripts/stage/scenery/stage_sky_view.gd @@ -0,0 +1,323 @@ +class_name StageSkyView +extends Control +## Adaptive screen-space Sky behind the world. + +const SKY_IDS: Array[StringName] = [&"sky_clear_day", &"sky_cloudy_day", &"sky_sunset", &"sky_starry_night", &"sky_neutral"] +const GRADIENT_TEXTURE_HEIGHT: int = 2048 +const CLOUD_VIEWBOX: Vector2 = Vector2(210.0, 110.0) +const NIGHT_STAR_COLOR: Color = Color("#FFF4CE") + +var _sky_id: StringName = &"sky_neutral" +var _stage_theme: SandboxTheme +var _gradient_texture: GradientTexture2D + + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE + texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR + _update_gradient_texture() + queue_redraw() + + +func set_sky(id: StringName) -> void: + var next_sky_id: StringName = id if SKY_IDS.has(id) else &"sky_neutral" + if _sky_id == next_sky_id: + return + _sky_id = next_sky_id + _update_gradient_texture() + queue_redraw() + + +func get_sky() -> StringName: + return _sky_id + + +func apply_theme(theme: SandboxTheme) -> void: + _stage_theme = theme + queue_redraw() + + +func _notification(what: int) -> void: + if what == NOTIFICATION_RESIZED: + queue_redraw() + + +func _draw() -> void: + if size.x <= 0.0 or size.y <= 0.0: + return + if _gradient_texture == null: + _update_gradient_texture() + if _gradient_texture != null: + draw_texture_rect(_gradient_texture, Rect2(Vector2.ZERO, size), false) + if _sky_id == &"sky_starry_night": + _draw_stars() + _draw_moon() + _draw_clouds() + else: + if _sky_id != &"sky_cloudy_day": + _draw_sun() + _draw_clouds() + + +func _update_gradient_texture() -> void: + var gradient: Gradient = Gradient.new() + var offsets: PackedFloat32Array = PackedFloat32Array() + var colors: PackedColorArray = PackedColorArray() + match _sky_id: + &"sky_clear_day": + offsets = PackedFloat32Array([0.0, 0.48, 1.0]) + colors = PackedColorArray([Color("#54ADEA"), Color("#95D4F4"), Color("#E0F5FF")]) + &"sky_cloudy_day": + offsets = PackedFloat32Array([0.0, 0.52, 1.0]) + colors = PackedColorArray([Color("#687D98"), Color("#A2B7C6"), Color("#D9E2E4")]) + &"sky_sunset": + offsets = PackedFloat32Array([0.0, 0.38, 0.71, 1.0]) + colors = PackedColorArray([Color("#644B86"), Color("#A96B99"), Color("#E89491"), Color("#FFC57F")]) + &"sky_starry_night": + offsets = PackedFloat32Array([0.0, 0.55, 1.0]) + colors = PackedColorArray([Color("#111A3C"), Color("#253963"), Color("#526E95")]) + _: + offsets = PackedFloat32Array([0.0, 0.5, 1.0]) + colors = PackedColorArray([Color("#323944"), Color("#6B737F"), Color("#AEB6C1")]) + gradient.offsets = offsets + gradient.colors = colors + _gradient_texture = GradientTexture2D.new() + _gradient_texture.gradient = gradient + _gradient_texture.width = 2 + _gradient_texture.height = GRADIENT_TEXTURE_HEIGHT + _gradient_texture.fill_from = Vector2.ZERO + _gradient_texture.fill_to = Vector2(0.0, 1.0) + + +func _draw_sun() -> void: + var center: Vector2 = Vector2(size.x * 0.78125, size.y * (0.241 if _sky_id == &"sky_sunset" else 0.242)) + var radius: float = size.y * (0.0979 if _sky_id == &"sky_sunset" else 0.0792) + var halo_color: Color = Color("#FFE1A2") if _sky_id == &"sky_sunset" else Color("#FFF1B3") + var sun_color: Color = Color("#FFE296") if _sky_id == &"sky_sunset" else Color("#FFE990") + draw_circle(center, radius * 1.5, Color(halo_color.r, halo_color.g, halo_color.b, 0.14)) + draw_circle(center, radius, sun_color) + draw_circle(center + Vector2(-radius * 0.19, -radius * 0.2), radius * 0.54, Color(1.0, 0.97, 0.82, 0.32)) + + +func _draw_stars() -> void: + for index: int in 48: + var x: float = fposmod(float(index * 137 + 39), size.x) + var y: float = fposmod(float(index * 83 + 21), size.y * 0.62) + draw_circle(Vector2(x, y), 1.3 if index % 4 != 0 else 2.0, NIGHT_STAR_COLOR) + for star_index: int in 3: + var center: Vector2 = Vector2(size.x * [0.3675, 0.9425, 0.22125][star_index], size.y * [0.085, 0.229, 0.348][star_index]) + var radius: float = maxf(1.3, size.y / 480.0 * 7.0) + draw_line(center - Vector2(0.0, radius), center + Vector2(0.0, radius), Color("#FFEABB"), maxf(1.0, radius * 0.22), true) + draw_line(center - Vector2(radius, 0.0), center + Vector2(radius, 0.0), Color("#FFEABB"), maxf(1.0, radius * 0.22), true) + + +func _draw_clouds() -> void: + var origins: Array[Vector4] = [] + var opacities: Array[float] = [] + var outer_color: Color + var underside_color: Color + var accent_color: Color + match _sky_id: + &"sky_clear_day": + origins = [Vector4(42.0, 66.0, 205.0, 108.0), Vector4(282.0, 167.0, 244.0, 127.0), Vector4(612.0, 288.0, 187.0, 98.0)] + opacities = [0.93, 0.88, 0.8] + outer_color = Color("#FFFFFF") + underside_color = Color("#CBE6FF", 0.72) + accent_color = Color("#D9EEFF", 0.86) + &"sky_cloudy_day": + origins = [Vector4(-16.0, 47.0, 246.0, 129.0), Vector4(188.0, 145.0, 242.0, 127.0), Vector4(508.0, 64.0, 281.0, 147.0), Vector4(74.0, 302.0, 190.0, 100.0), Vector4(535.0, 324.0, 231.0, 121.0)] + opacities = [1.0, 0.93, 0.95, 0.7, 0.69] + outer_color = Color("#F8FCFF") + underside_color = Color("#BED7EF", 0.9) + accent_color = Color("#D0E5F7", 1.0) + &"sky_sunset": + origins = [Vector4(24.0, 82.0, 235.0, 123.0), Vector4(294.0, 244.0, 258.0, 135.0), Vector4(585.0, 310.0, 218.0, 114.0)] + opacities = [0.85, 0.9, 0.81] + outer_color = Color("#FFE3D1") + underside_color = Color("#E9A8BA", 0.8) + accent_color = Color("#FFF2D7", 0.8) + _: + origins = [Vector4(-4.0, 331.0, 231.0, 121.0), Vector4(314.0, 380.0, 188.0, 98.0)] + opacities = [0.28, 0.23] + outer_color = Color("#A9BCE1") + underside_color = Color("#7898C5", 0.8) + accent_color = Color("#D7E3F7", 0.45) + var viewport_scale: Vector2 = Vector2(size.x / 800.0, size.y / 480.0) + for cloud_index: int in origins.size(): + var placement: Vector4 = origins[cloud_index] + var cloud_rect: Rect2 = Rect2( + Vector2(placement.x, placement.y) * viewport_scale, + Vector2(placement.z, placement.w) * viewport_scale + ) + _draw_cloud(cloud_rect, outer_color, underside_color, accent_color, opacities[cloud_index]) + + +func _draw_cloud(rect: Rect2, outer_color: Color, underside_color: Color, accent_color: Color, opacity: float) -> void: + var outline: PackedVector2Array = _map_cloud_points(_build_cloud_outline(), rect) + var cloud_color: Color = _with_opacity(outer_color, opacity) + draw_colored_polygon(outline, cloud_color) + draw_polyline(outline, cloud_color, maxf(1.0, rect.size.y / CLOUD_VIEWBOX.y * 1.5), true) + var underside: PackedVector2Array = _map_cloud_points(_build_cloud_underside(), rect) + draw_colored_polygon(underside, _with_opacity(underside_color, opacity)) + for accent_curve: PackedVector2Array in _build_cloud_accents(): + var accent_points: PackedVector2Array = _map_cloud_points(_sample_curve_chain([accent_curve], 12), rect) + draw_polyline(accent_points, _with_opacity(accent_color, opacity), maxf(1.0, rect.size.y / CLOUD_VIEWBOX.y * 3.0), true) + + +func _draw_moon() -> void: + var moon_curves: Array[PackedVector2Array] = _build_moon_curves() + var moon_local: PackedVector2Array = _sample_curve_chain(moon_curves, 16) + var outer_arc: PackedVector2Array = _sample_curve_chain(_build_moon_outer_arc_curves(), 16) + var moon_scale: float = size.y / 480.0 * 0.63 + var moon_origin: Vector2 = Vector2(size.x * 0.7625, size.y * 0.09583) + var moon_size: Vector2 = Vector2(150.0, 240.0) * moon_scale + var moon_rect: Rect2 = Rect2(moon_origin, moon_size) + var moon_outline: PackedVector2Array = _map_moon_points(moon_local, moon_rect) + draw_colored_polygon(moon_outline, Color("#17213A")) + draw_colored_polygon(moon_outline, Color("#FFEA31")) + var warm_rim: PackedVector2Array = _build_moon_warm_rim(outer_arc, 7.5) + draw_colored_polygon(_map_moon_points(warm_rim, moon_rect), Color("#F4A629")) + var highlight: PackedVector2Array = _sample_curve_chain(_build_moon_highlight(), 18) + draw_polyline(_map_moon_points(highlight, moon_rect), Color(1.0, 0.98, 0.63, 0.62), maxf(1.0, moon_scale * 8.0), true) + draw_polyline(moon_outline, Color("#17213A"), maxf(1.0, moon_scale * 5.5), true) + + +func _build_cloud_outline() -> PackedVector2Array: + return _sample_curve_chain(_build_cloud_outer_curves(), 10) + + +func _build_cloud_underside() -> PackedVector2Array: + return _sample_curve_chain(_build_cloud_underside_curves(), 10) + + +func _build_cloud_outer_curves() -> Array[PackedVector2Array]: + var curves: Array[PackedVector2Array] = [] + curves.append(PackedVector2Array([Vector2(18, 74), Vector2(7, 71), Vector2(2, 62), Vector2(5, 52)])) + curves.append(PackedVector2Array([Vector2(5, 52), Vector2(8, 42), Vector2(19, 36), Vector2(32, 39)])) + curves.append(PackedVector2Array([Vector2(32, 39), Vector2(29, 25), Vector2(40, 13), Vector2(54, 13)])) + curves.append(PackedVector2Array([Vector2(54, 13), Vector2(64, 13), Vector2(72, 18), Vector2(77, 25)])) + curves.append(PackedVector2Array([Vector2(77, 25), Vector2(84, 9), Vector2(101, 2), Vector2(116, 7)])) + curves.append(PackedVector2Array([Vector2(116, 7), Vector2(131, 11), Vector2(141, 24), Vector2(142, 38)])) + curves.append(PackedVector2Array([Vector2(142, 38), Vector2(153, 30), Vector2(167, 32), Vector2(176, 40)])) + curves.append(PackedVector2Array([Vector2(176, 40), Vector2(182, 45), Vector2(185, 51), Vector2(185, 57)])) + curves.append(PackedVector2Array([Vector2(185, 57), Vector2(197, 58), Vector2(205, 67), Vector2(204, 78)])) + curves.append(PackedVector2Array([Vector2(204, 78), Vector2(203, 89), Vector2(193, 97), Vector2(180, 97)])) + curves.append(PackedVector2Array([Vector2(180, 97), Vector2(171, 97), Vector2(164, 94), Vector2(159, 90)])) + curves.append(PackedVector2Array([Vector2(159, 90), Vector2(148, 100), Vector2(131, 101), Vector2(119, 93)])) + curves.append(PackedVector2Array([Vector2(119, 93), Vector2(107, 102), Vector2(88, 103), Vector2(75, 94)])) + curves.append(PackedVector2Array([Vector2(75, 94), Vector2(63, 101), Vector2(47, 98), Vector2(39, 89)])) + curves.append(PackedVector2Array([Vector2(39, 89), Vector2(30, 89), Vector2(23, 83), Vector2(18, 74)])) + return curves + + +func _build_cloud_underside_curves() -> Array[PackedVector2Array]: + var curves: Array[PackedVector2Array] = [] + curves.append(PackedVector2Array([Vector2(9, 69), Vector2(20, 79), Vector2(31, 79), Vector2(42, 75)])) + curves.append(PackedVector2Array([Vector2(42, 75), Vector2(57, 89), Vector2(71, 86), Vector2(82, 81)])) + curves.append(PackedVector2Array([Vector2(82, 81), Vector2(96, 92), Vector2(111, 89), Vector2(122, 82)])) + curves.append(PackedVector2Array([Vector2(122, 82), Vector2(139, 93), Vector2(152, 89), Vector2(163, 80)])) + curves.append(PackedVector2Array([Vector2(163, 80), Vector2(179, 89), Vector2(195, 83), Vector2(202, 74)])) + curves.append(PackedVector2Array([Vector2(202, 74), Vector2(205, 87), Vector2(195, 97), Vector2(180, 97)])) + curves.append(PackedVector2Array([Vector2(180, 97), Vector2(171, 97), Vector2(164, 94), Vector2(159, 90)])) + curves.append(PackedVector2Array([Vector2(159, 90), Vector2(148, 100), Vector2(131, 101), Vector2(119, 93)])) + curves.append(PackedVector2Array([Vector2(119, 93), Vector2(107, 102), Vector2(88, 103), Vector2(75, 94)])) + curves.append(PackedVector2Array([Vector2(75, 94), Vector2(63, 101), Vector2(47, 98), Vector2(39, 89)])) + curves.append(PackedVector2Array([Vector2(39, 89), Vector2(25, 91), Vector2(13, 82), Vector2(9, 69)])) + return curves + + +func _build_cloud_accents() -> Array[PackedVector2Array]: + var curves: Array[PackedVector2Array] = [] + curves.append(PackedVector2Array([Vector2(42, 43), Vector2(49, 54), Vector2(58, 59), Vector2(69, 58)])) + curves.append(PackedVector2Array([Vector2(85, 33), Vector2(96, 21), Vector2(114, 21), Vector2(126, 32)])) + curves.append(PackedVector2Array([Vector2(142, 58), Vector2(151, 48), Vector2(164, 47), Vector2(173, 53)])) + return curves + + +func _build_moon_curves() -> Array[PackedVector2Array]: + var curves: Array[PackedVector2Array] = [] + curves.append(PackedVector2Array([Vector2(15, 13), Vector2(69, 20), Vector2(113, 54), Vector2(129, 104)])) + curves.append(PackedVector2Array([Vector2(129, 104), Vector2(149, 166), Vector2(119, 224), Vector2(61, 235)])) + curves.append(PackedVector2Array([Vector2(61, 235), Vector2(36, 240), Vector2(13, 231), Vector2(2, 216)])) + curves.append(PackedVector2Array([Vector2(2, 216), Vector2(40, 218), Vector2(60, 193), Vector2(68, 158)])) + curves.append(PackedVector2Array([Vector2(68, 158), Vector2(85, 103), Vector2(63, 47), Vector2(15, 13)])) + return curves + + +func _build_moon_outer_arc_curves() -> Array[PackedVector2Array]: + var curves: Array[PackedVector2Array] = [] + curves.append(PackedVector2Array([Vector2(15, 13), Vector2(69, 20), Vector2(113, 54), Vector2(129, 104)])) + curves.append(PackedVector2Array([Vector2(129, 104), Vector2(149, 166), Vector2(119, 224), Vector2(61, 235)])) + curves.append(PackedVector2Array([Vector2(61, 235), Vector2(36, 240), Vector2(13, 231), Vector2(2, 216)])) + return curves + + +func _build_moon_highlight() -> Array[PackedVector2Array]: + var curves: Array[PackedVector2Array] = [] + curves.append(PackedVector2Array([Vector2(29, 37), Vector2(63, 58), Vector2(85, 98), Vector2(87, 128)])) + return curves + + +func _sample_curve_chain(curves: Array[PackedVector2Array], steps_per_curve: int) -> PackedVector2Array: + var sampled: PackedVector2Array = PackedVector2Array() + if steps_per_curve <= 0: + return sampled + for curve_index: int in curves.size(): + var curve: PackedVector2Array = curves[curve_index] + if curve.size() != 4: + continue + for step_index: int in range(steps_per_curve + 1): + if curve_index > 0 and step_index == 0: + continue + var t: float = float(step_index) / float(steps_per_curve) + sampled.append(_cubic_point(curve, t)) + return sampled + + +func _cubic_point(curve: PackedVector2Array, t: float) -> Vector2: + var inverse_t: float = 1.0 - t + return ( + curve[0] * inverse_t * inverse_t * inverse_t + + curve[1] * 3.0 * inverse_t * inverse_t * t + + curve[2] * 3.0 * inverse_t * t * t + + curve[3] * t * t * t + ) + + +func _map_cloud_points(points: PackedVector2Array, rect: Rect2) -> PackedVector2Array: + var mapped: PackedVector2Array = PackedVector2Array() + for point: Vector2 in points: + mapped.append(rect.position + Vector2(point.x / CLOUD_VIEWBOX.x * rect.size.x, point.y / CLOUD_VIEWBOX.y * rect.size.y)) + return mapped + + +func _map_moon_points(points: PackedVector2Array, rect: Rect2) -> PackedVector2Array: + var mapped: PackedVector2Array = PackedVector2Array() + for point: Vector2 in points: + mapped.append(rect.position + point * (rect.size.y / 240.0)) + return mapped + + +func _build_moon_warm_rim(outer_arc: PackedVector2Array, width: float) -> PackedVector2Array: + var rim: PackedVector2Array = PackedVector2Array() + if outer_arc.size() < 3: + return rim + for point: Vector2 in outer_arc: + rim.append(point) + for point_index: int in range(outer_arc.size() - 1, -1, -1): + var tangent: Vector2 + if point_index == 0: + tangent = outer_arc[1] - outer_arc[0] + elif point_index == outer_arc.size() - 1: + tangent = outer_arc[point_index] - outer_arc[point_index - 1] + else: + tangent = outer_arc[point_index + 1] - outer_arc[point_index - 1] + var inward: Vector2 = Vector2(-tangent.y, tangent.x).normalized() + var progress: float = float(point_index) / float(outer_arc.size() - 1) + var local_width: float = width * sin(PI * progress) + rim.append(outer_arc[point_index] + inward * local_width) + return rim + + +func _with_opacity(color: Color, opacity: float) -> Color: + return Color(color.r, color.g, color.b, color.a * opacity) diff --git a/scripts/stage/scenery/stage_sky_view.gd.uid b/scripts/stage/scenery/stage_sky_view.gd.uid new file mode 100644 index 0000000..9e0fd28 --- /dev/null +++ b/scripts/stage/scenery/stage_sky_view.gd.uid @@ -0,0 +1 @@ +uid://dxoqflho7kyik diff --git a/scripts/stage/scenery/stage_starter_card_preview.gd b/scripts/stage/scenery/stage_starter_card_preview.gd new file mode 100644 index 0000000..44b3c97 --- /dev/null +++ b/scripts/stage/scenery/stage_starter_card_preview.gd @@ -0,0 +1,116 @@ +class_name StageStarterCardPreview +extends SubViewportContainer +## Lightweight live preview of a saved starter Stage scene. + +const STAGE_SCENE_IO := preload("res://scripts/stage/stage_scene_io.gd") +const STAGE_SCENE_ADAPTER := preload("res://scripts/editors/stage/stage_scene_adapter.gd") +const STAGE_SPAWNER := preload("res://scripts/stage/stage_spawner.gd") +const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd") +const STAGE_SKY_VIEW := preload("res://scripts/stage/scenery/stage_sky_view.gd") +const STAGE_WALL_PANEL := preload("res://scripts/stage/scenery/stage_wall_panel.gd") +const PROP_BLOCK := preload("res://scripts/props/prop_block.gd") + +const PREVIEW_SIZE := Vector2i(256, 256) +const PREVIEW_MARGIN := 18.0 +const MIN_PREVIEW_ZOOM := 0.08 +const MAX_PREVIEW_ZOOM := 0.42 + +var _viewport: SubViewport +var _sky_view: StageSkyView +var _world: Node2D +var _loaded_nodes: Array[Node2D] = [] +var _scene_valid: bool = false + + +func configure(entry: Dictionary) -> void: + name = "StarterScenePreview" + custom_minimum_size = Vector2(128.0, 128.0) + mouse_filter = Control.MOUSE_FILTER_IGNORE + stretch = true + texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR + + var result: Dictionary = STAGE_SCENE_IO.read_file(String(entry.get("path", ""))) + if not bool(result.get("ok", false)): + return + var data: Dictionary = result.get("data", {}) as Dictionary + _scene_valid = true + _build_preview_viewport(data) + + +func has_preview_scene() -> bool: + return _scene_valid and _viewport != null and _sky_view != null and _world != null + + +func get_loaded_object_count() -> int: + return _loaded_nodes.size() + + +func _build_preview_viewport(data: Dictionary) -> void: + _viewport = SubViewport.new() + _viewport.name = "PreviewViewport" + _viewport.size = PREVIEW_SIZE + _viewport.world_2d = World2D.new() + _viewport.transparent_bg = false + _viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED + add_child(_viewport) + + var sky_layer := CanvasLayer.new() + sky_layer.name = "PreviewSky" + sky_layer.layer = -1 + _viewport.add_child(sky_layer) + _sky_view = STAGE_SKY_VIEW.new() as StageSkyView + _sky_view.name = "SkyView" + _sky_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _sky_view.mouse_filter = Control.MOUSE_FILTER_IGNORE + _sky_view.set_sky(StringName(String(data.get("sky_id", "sky_neutral")))) + sky_layer.add_child(_sky_view) + + _world = Node2D.new() + _world.name = "PreviewWorld" + _viewport.add_child(_world) + var spawner: StageSpawner = STAGE_SPAWNER.new(_world) as StageSpawner + var adapter: StageSceneAdapter = STAGE_SCENE_ADAPTER.new() as StageSceneAdapter + var applied: Dictionary = adapter.apply(data, _world, spawner) + for raw_node: Variant in applied.get("loaded_nodes", []): + var stage_node: Node2D = raw_node as Node2D + if stage_node == null: + continue + _loaded_nodes.append(stage_node) + if stage_node is PropBlock: + var prop: PropBlock = stage_node as PropBlock + prop.freeze = true + prop.gravity_scale = 0.0 + prop.collision_layer = 0 + prop.collision_mask = 0 + call_deferred("_fit_scene_to_preview") + + +func _fit_scene_to_preview() -> void: + if _world == null or not is_instance_valid(_world): + return + var content_bounds := Rect2() + var has_bounds: bool = false + for node: Node2D in _loaded_nodes: + if node == null or not is_instance_valid(node): + continue + var bounds: Rect2 + if node.get_script() == STAGE_WALL_PANEL: + bounds = node.global_transform * (node as StageWallPanel).get_local_bounds() + else: + bounds = STAGE_SELECTION.get_world_aabb(node) + bounds = bounds.abs() + if bounds.size.x <= 0.0 or bounds.size.y <= 0.0: + continue + content_bounds = bounds if not has_bounds else content_bounds.merge(bounds) + has_bounds = true + if not has_bounds: + _world.scale = Vector2.ONE * 0.2 + _world.position = Vector2(PREVIEW_SIZE) * 0.5 + _viewport.render_target_update_mode = SubViewport.UPDATE_ONCE + return + var usable_size: Vector2 = Vector2(PREVIEW_SIZE) - Vector2.ONE * PREVIEW_MARGIN * 2.0 + var fit_zoom: float = minf(usable_size.x / content_bounds.size.x, usable_size.y / content_bounds.size.y) + fit_zoom = clampf(fit_zoom, MIN_PREVIEW_ZOOM, MAX_PREVIEW_ZOOM) + _world.scale = Vector2.ONE * fit_zoom + _world.position = Vector2(PREVIEW_SIZE) * 0.5 - content_bounds.get_center() * fit_zoom + _viewport.render_target_update_mode = SubViewport.UPDATE_ONCE diff --git a/scripts/stage/scenery/stage_starter_card_preview.gd.uid b/scripts/stage/scenery/stage_starter_card_preview.gd.uid new file mode 100644 index 0000000..24c4ada --- /dev/null +++ b/scripts/stage/scenery/stage_starter_card_preview.gd.uid @@ -0,0 +1 @@ +uid://lw57y3sl5wfk diff --git a/scripts/stage/scenery/stage_surface_renderer.gd b/scripts/stage/scenery/stage_surface_renderer.gd new file mode 100644 index 0000000..24ffc47 --- /dev/null +++ b/scripts/stage/scenery/stage_surface_renderer.gd @@ -0,0 +1,267 @@ +class_name StageSurfaceRenderer +extends RefCounted +## World-aligned, bounded vector motifs for finite walls and Ground. + +const WALL_FINISHES: Array[StringName] = [&"finish_wall_paint", &"finish_wall_brick", &"finish_wall_wood"] +const GROUND_FINISHES: Array[StringName] = [&"finish_ground_paint", &"finish_ground_brick", &"finish_ground_wood", &"finish_ground_grass", &"finish_ground_dirt", &"finish_ground_sidewalk", &"finish_ground_asphalt"] +const MAX_CELLS: int = 1200 +const BRICK_WIDTH: float = 82.0 +const BRICK_HEIGHT: float = 34.0 +const MAX_VISIBLE_BRICK_CELLS: int = 12000 + + +static func is_wall_finish(id: StringName) -> bool: + return WALL_FINISHES.has(id) + + +static func is_ground_finish(id: StringName) -> bool: + return GROUND_FINISHES.has(id) or id == &"finish_ground_legacy" + + +static func draw_wall( + canvas: CanvasItem, + rect: Rect2, + finish: StringName, + tint: Color, + world_origin: Vector2, + drawing_scale: Vector2 = Vector2.ONE +) -> void: + _draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, true) + + +static func draw_ground( + canvas: CanvasItem, + rect: Rect2, + finish: StringName, + tint: Color, + world_origin: Vector2, + drawing_scale: Vector2 = Vector2.ONE +) -> void: + _draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false) + + +static func _draw_surface( + canvas: CanvasItem, + rect: Rect2, + finish: StringName, + tint: Color, + world_origin: Vector2, + drawing_scale: Vector2, + wall: bool +) -> void: + var normalized := rect.abs() + if normalized.size.x <= 0.0 or normalized.size.y <= 0.0: + return + var motif_scale: Vector2 = _normalized_scale(drawing_scale) + var base: Color = tint + if finish == &"finish_ground_legacy": + base = Color("#468B42") + elif finish.ends_with("brick"): + base = Color("#B95F3D") if wall else Color("#994D35") + elif finish.ends_with("wood"): + base = Color("#9A673C") if wall else Color("#B88951") + elif finish == &"finish_ground_grass": + base = Color("#72A84C") + elif finish == &"finish_ground_dirt": + base = Color("#8E633E") + elif finish == &"finish_ground_sidewalk": + base = Color("#B7B9B2") + elif finish == &"finish_ground_asphalt": + base = Color("#505A62") + canvas.draw_rect(normalized, base, true) + if finish.ends_with("brick"): + var view_rect: Rect2 = _visible_local_rect(canvas) + var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized + if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0: + var detail_scale: float = brick_detail_scale_for_view(view_rect.size if not view_rect.size.is_zero_approx() else normalized.size, motif_scale) + _draw_bricks(canvas, pattern_rect, world_origin, base, motif_scale, detail_scale) + elif finish.ends_with("wood"): + _draw_wood(canvas, normalized, world_origin, base, motif_scale) + elif finish == &"finish_ground_grass": + _draw_grass(canvas, normalized, world_origin, motif_scale) + elif finish == &"finish_ground_dirt": + _draw_dirt(canvas, normalized, world_origin, motif_scale) + elif finish == &"finish_ground_sidewalk": + _draw_sidewalk(canvas, normalized, world_origin, motif_scale) + elif finish == &"finish_ground_asphalt": + _draw_asphalt(canvas, normalized, world_origin, motif_scale) + elif finish != &"finish_wall_paint" and finish != &"finish_ground_paint" and finish != &"finish_ground_legacy": + push_warning("StageSurfaceRenderer: unknown finish '%s'; drawing paint." % String(finish)) + + +static func brick_detail_scale_for_view(view_size: Vector2, motif_scale: Vector2 = Vector2.ONE) -> float: + var scale: float = 1.0 + var safe_size: Vector2 = Vector2(maxf(1.0, view_size.x), maxf(1.0, view_size.y)) + var safe_motif_scale: Vector2 = _normalized_scale(motif_scale) + for _iteration: int in 8: + var columns: int = int(ceil(safe_size.x * safe_motif_scale.x / (BRICK_WIDTH * scale))) + 2 + var rows: int = int(ceil(safe_size.y * safe_motif_scale.y / (BRICK_HEIGHT * scale))) + 2 + var cell_count: int = rows * columns + if cell_count <= MAX_VISIBLE_BRICK_CELLS: + return scale + scale *= sqrt(float(cell_count) / float(MAX_VISIBLE_BRICK_CELLS)) * 1.01 + return scale + + +static func _draw_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, base: Color, motif_scale: Vector2, detail_scale: float = 1.0) -> void: + var brick_world_w: float = BRICK_WIDTH * detail_scale + var brick_world_h: float = BRICK_HEIGHT * detail_scale + var brick_w: float = brick_world_w / motif_scale.x + var brick_h: float = brick_world_h / motif_scale.y + var mortar := base.lightened(0.46) + var x_start: float = (floorf((rect.position.x * motif_scale.x + origin.x) / brick_world_w) * brick_world_w - origin.x) / motif_scale.x + var y_start: float = (floorf((rect.position.y * motif_scale.y + origin.y) / brick_world_h) * brick_world_h - origin.y) / motif_scale.y + var rows: int = int(ceil(rect.size.y / brick_h)) + 2 + var columns: int = int(ceil(rect.size.x / brick_w)) + 2 + for row: int in rows: + var y: float = y_start + float(row) * brick_h + var offset: float = brick_w * 0.5 if posmod(row + int(floori((rect.position.y * motif_scale.y + origin.y) / brick_world_h)), 2) == 1 else 0.0 + _draw_clipped_line(canvas, rect, Vector2(rect.position.x, y), Vector2(rect.end.x, y), mortar, 3.0 / motif_scale.y) + for column: int in columns: + var x: float = x_start + float(column) * brick_w + offset + if x < rect.position.x or x > rect.end.x: + continue + var next_row_y: float = y + brick_h + _draw_clipped_line(canvas, rect, Vector2(x, y), Vector2(x, next_row_y), mortar, 3.0 / motif_scale.x) + + +static func _visible_local_rect(canvas: CanvasItem) -> Rect2: + if canvas == null or not canvas.is_inside_tree(): + return Rect2() + var viewport: Viewport = canvas.get_viewport() + if viewport == null: + return Rect2() + var local_from_viewport: Transform2D = canvas.get_global_transform_with_canvas().affine_inverse() + var viewport_rect: Rect2 = viewport.get_visible_rect() + var corners: Array[Vector2] = [ + viewport_rect.position, + Vector2(viewport_rect.end.x, viewport_rect.position.y), + viewport_rect.end, + Vector2(viewport_rect.position.x, viewport_rect.end.y), + ] + var local_rect: Rect2 = Rect2(local_from_viewport * corners[0], Vector2.ZERO) + for corner_index: int in range(1, corners.size()): + local_rect = local_rect.expand(local_from_viewport * corners[corner_index]) + return local_rect + + +static func _draw_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, base: Color, motif_scale: Vector2) -> void: + const BOARD: float = 64.0 + var line_color := base.darkened(0.28) + var board_h: float = BOARD / motif_scale.y + var start_y: float = (floorf((rect.position.y * motif_scale.y + origin.y) / BOARD) * BOARD - origin.y) / motif_scale.y - board_h + var count: int = mini(int(ceil(rect.size.y / board_h)) + 3, MAX_CELLS) + for index: int in count: + var y: float = start_y + float(index) * board_h + _draw_clipped_line(canvas, rect, Vector2(rect.position.x, y), Vector2(rect.end.x, y), line_color, 2.0 / motif_scale.y) + var seed_x: float = fposmod(origin.x * 0.37 + float(index * 41), 90.0) / motif_scale.x + _draw_clipped_line( + canvas, + rect, + Vector2(rect.position.x + seed_x, y + board_h * 0.5), + Vector2(rect.position.x + seed_x + 28.0 / motif_scale.x, y + board_h * 0.5), + line_color, + 1.0 / motif_scale.y + ) + + +static func _draw_clipped_line(canvas: CanvasItem, rect: Rect2, from: Vector2, to: Vector2, color: Color, width: float) -> void: + var clipped: PackedVector2Array = _clip_line_to_rect(rect, from, to, width) + if clipped.size() == 2: + canvas.draw_line(clipped[0], clipped[1], color, width) + + +static func _clip_line_to_rect(rect: Rect2, from: Vector2, to: Vector2, width: float) -> PackedVector2Array: + var inset: float = maxf(0.0, width) * 0.5 + var safe_rect: Rect2 = Rect2(rect.position + Vector2.ONE * inset, rect.size - Vector2.ONE * inset * 2.0) + if safe_rect.size.x <= 0.0 or safe_rect.size.y <= 0.0: + return PackedVector2Array() + var delta: Vector2 = to - from + var p_values: Array[float] = [-delta.x, delta.x, -delta.y, delta.y] + var q_values: Array[float] = [ + from.x - safe_rect.position.x, + safe_rect.end.x - from.x, + from.y - safe_rect.position.y, + safe_rect.end.y - from.y, + ] + var t_min: float = 0.0 + var t_max: float = 1.0 + for edge_index: int in p_values.size(): + var p_value: float = p_values[edge_index] + var q_value: float = q_values[edge_index] + if is_zero_approx(p_value): + if q_value < 0.0: + return PackedVector2Array() + continue + var ratio: float = q_value / p_value + if p_value < 0.0: + if ratio > t_max: + return PackedVector2Array() + t_min = maxf(t_min, ratio) + else: + if ratio < t_min: + return PackedVector2Array() + t_max = minf(t_max, ratio) + if t_min > t_max: + return PackedVector2Array() + return PackedVector2Array([from + delta * t_min, from + delta * t_max]) + + +static func _draw_grass(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void: + var pitch: float = 18.0 / motif_scale.x + var phase: float = fposmod(origin.x, 18.0) / motif_scale.x + var count: int = mini(int(ceil(rect.size.x / pitch)) + 2, MAX_CELLS) + var baseline: float = rect.position.y + for index: int in count: + var x: float = rect.position.x + float(index) * pitch - phase + var seed: int = absi(int(floori((x * motif_scale.x + origin.x) * 0.23))) + var height: float = 5.0 + float(seed % 8) + var green := Color("#3D7E39") if seed % 3 == 0 else Color("#4D913F") + canvas.draw_line(Vector2(x, baseline + 4.0 / motif_scale.y), Vector2(x - 3.0 / motif_scale.x, baseline - height / motif_scale.y), green, 2.0 / motif_scale.x) + canvas.draw_line(Vector2(x + 4.0 / motif_scale.x, baseline + 4.0 / motif_scale.y), Vector2(x + 7.0 / motif_scale.x, baseline - height * 0.72 / motif_scale.y), green, 1.5 / motif_scale.x) + if seed % 5 == 0: + _draw_world_circle(canvas, Vector2(x + 2.0 / motif_scale.x, baseline - (height + 2.0) / motif_scale.y), 2.0, Color("#C6D96B"), motif_scale) + + +static func _draw_dirt(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void: + var pitch: float = 26.0 / motif_scale.x + var phase: float = fposmod(origin.x, 26.0) / motif_scale.x + for index: int in mini(int(ceil(rect.size.x / pitch)) + 1, MAX_CELLS): + var x: float = rect.position.x + float(index) * pitch - phase + var y: float = rect.position.y + fposmod(float(index * 17) + origin.y, 18.0) / motif_scale.y + _draw_world_circle(canvas, Vector2(x, y), 2.0, Color("#67492F"), motif_scale) + + +static func _draw_sidewalk(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void: + const SIZE: float = 72.0 + var local_size: float = SIZE / motif_scale.x + var color := Color("#92968F") + var x: float = rect.position.x - fposmod(origin.x, SIZE) / motif_scale.x + var count: int = mini(int(ceil(rect.size.x / local_size)) + 2, MAX_CELLS) + for index: int in count: + var px: float = x + float(index) * local_size + canvas.draw_line(Vector2(px, rect.position.y), Vector2(px, rect.end.y), color, 2.0 / motif_scale.x) + canvas.draw_line(Vector2(rect.position.x, rect.position.y), Vector2(rect.end.x, rect.position.y), color, 2.0 / motif_scale.y) + + +static func _draw_asphalt(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void: + var local_pitch: float = 80.0 / motif_scale.x + var phase: float = fposmod(origin.x, 80.0) / motif_scale.x + var count: int = mini(int(ceil(rect.size.x / local_pitch)) + 2, MAX_CELLS) + var world_height: float = maxf(rect.size.y * motif_scale.y, 1.0) + for index: int in count: + var x: float = rect.position.x + float(index) * local_pitch - phase + var y: float = rect.position.y + fposmod(float(index * 31) + origin.y, world_height) / motif_scale.y + _draw_world_circle(canvas, Vector2(x, y), 1.5, Color("#727A7D"), motif_scale) + + +static func _draw_world_circle(canvas: CanvasItem, center: Vector2, radius: float, color: Color, motif_scale: Vector2) -> void: + var points := PackedVector2Array() + for point_index: int in 12: + var angle: float = TAU * float(point_index) / 12.0 + points.append(center + Vector2(cos(angle) * radius / motif_scale.x, sin(angle) * radius / motif_scale.y)) + canvas.draw_colored_polygon(points, color) + + +static func _normalized_scale(scale: Vector2) -> Vector2: + return Vector2(maxf(absf(scale.x), 0.001), maxf(absf(scale.y), 0.001)) diff --git a/scripts/stage/scenery/stage_surface_renderer.gd.uid b/scripts/stage/scenery/stage_surface_renderer.gd.uid new file mode 100644 index 0000000..20d4388 --- /dev/null +++ b/scripts/stage/scenery/stage_surface_renderer.gd.uid @@ -0,0 +1 @@ +uid://cefj7bf1p3bq2 diff --git a/scripts/stage/scenery/stage_wall_panel.gd b/scripts/stage/scenery/stage_wall_panel.gd new file mode 100644 index 0000000..a9c017c --- /dev/null +++ b/scripts/stage/scenery/stage_wall_panel.gd @@ -0,0 +1,36 @@ +class_name StageWallPanel +extends Node2D +## Finite decorative wall panel with world-scale finish motifs. + +const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd") +const DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd") +var finish_id: StringName = &"finish_wall_paint" +var tint: Color = Color("#5A9D70") +var panel_size: Vector2 = Vector2(320.0, 400.0) +## Saved source-wall anchor offset, retained to preserve erased-fragment metadata. +## Motif drawing uses the fragment's current world origin so its brick courses stay aligned. +var motif_origin_offset: Vector2 = Vector2.ZERO + + +func configure(new_size: Vector2, finish: StringName, color: Color) -> void: + panel_size = Vector2(maxf(1.0, new_size.x), maxf(1.0, new_size.y)) + finish_id = finish if SURFACE_RENDERER.is_wall_finish(finish) else &"finish_wall_paint" + tint = color + z_index = DRAW_ORDER.WALL + queue_redraw() + + +func get_local_bounds() -> Rect2: + return Rect2(-panel_size * 0.5, panel_size) + + +func get_effective_motif_origin_world() -> Vector2: + return global_position + motif_origin_offset + + +func get_surface_draw_origin_world() -> Vector2: + return global_position + + +func _draw() -> void: + SURFACE_RENDERER.draw_wall(self, get_local_bounds(), finish_id, tint, get_surface_draw_origin_world()) diff --git a/scripts/stage/scenery/stage_wall_panel.gd.uid b/scripts/stage/scenery/stage_wall_panel.gd.uid new file mode 100644 index 0000000..20355fa --- /dev/null +++ b/scripts/stage/scenery/stage_wall_panel.gd.uid @@ -0,0 +1 @@ +uid://d1ttvnqumh2hd diff --git a/scripts/stage/stage_draw_order.gd b/scripts/stage/stage_draw_order.gd new file mode 100644 index 0000000..a9da51a --- /dev/null +++ b/scripts/stage/stage_draw_order.gd @@ -0,0 +1,21 @@ +class_name StageDrawOrder +extends RefCounted +## Semantic draw bands shared by scenery, terrain, Props, and characters. + +const WALL: int = -40 +const FAR: int = -30 +const NEAR: int = -20 +const TERRAIN: int = -10 +const FLOOR: int = -5 + + +static func scenery_z(depth: StringName) -> int: + match depth: + &"far": return FAR + &"floor": return FLOOR + _: return NEAR + + +static func apply_terrain(node: TerrainBlock) -> void: + if node != null: + node.z_index = TERRAIN diff --git a/scripts/stage/stage_draw_order.gd.uid b/scripts/stage/stage_draw_order.gd.uid new file mode 100644 index 0000000..a078b70 --- /dev/null +++ b/scripts/stage/stage_draw_order.gd.uid @@ -0,0 +1 @@ +uid://dsyoepjah36yk diff --git a/scripts/stage/stage_gizmos.gd b/scripts/stage/stage_gizmos.gd index acc0be2..da17aa5 100644 --- a/scripts/stage/stage_gizmos.gd +++ b/scripts/stage/stage_gizmos.gd @@ -11,6 +11,8 @@ extends Node2D const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd") const PropBlockScript := preload("res://scripts/props/prop_block.gd") +const StageWallPanelScript := preload("res://scripts/stage/scenery/stage_wall_panel.gd") +const StageSceneryArtworkScript := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd") # --------------------------------------------------------------------------- # Enums @@ -42,6 +44,7 @@ const ROTATE_HANDLE_MARGIN_PX := 48.0 const ROTATE_RING_TOLERANCE_PX := 12.0 const SCALE_CROSS_SIZE_PX := 5.0 const SCALE_HIT_RADIUS_PX := 12.0 +const SCENERY_SCALE_HIT_HALF_SIZE_PX := 22.0 const MIN_SCALE: float = 0.05 const MAX_SCALE: float = 20.0 @@ -55,6 +58,7 @@ var camera: Camera2D = null var snap_size: float = 0.0 var _enabled: bool = true +var _transform_handles_visible: bool = true var _targets: Array[Node2D] = [] var _hovered: Node2D = null var _dragging: Handle = Handle.NONE @@ -91,6 +95,11 @@ func set_enabled(enabled: bool) -> void: queue_redraw() +func set_transform_handles_visible(visible: bool) -> void: + _transform_handles_visible = visible + queue_redraw() + + func set_snap_size(size: float) -> void: snap_size = maxf(size, 0.0) @@ -102,12 +111,18 @@ func set_box_rect(rect: Rect2) -> void: ## Returns ROTATE if the cursor is on the rotate ring (single selection only). func hit_test(world_pos: Vector2) -> Handle: - if not _enabled or _targets.size() != 1: + if not _enabled or not _transform_handles_visible or _targets.size() != 1: return Handle.NONE var target := _targets[0] if target == null or not is_instance_valid(target): return Handle.NONE - var rect := STAGE_SELECTION.get_world_aabb(target) + var rect: Rect2 = _target_bounds(target) + if _is_scenery_target(target): + for corner: Vector2 in _corners(rect): + var offset: Vector2 = (world_pos - corner) * _zoom() + if absf(offset.x) <= SCENERY_SCALE_HIT_HALF_SIZE_PX and absf(offset.y) <= SCENERY_SCALE_HIT_HALF_SIZE_PX: + return Handle.SCALE + return Handle.NONE if target is PropBlockScript: for corner: Vector2 in _corners(rect): if world_pos.distance_to(corner) <= SCALE_HIT_RADIUS_PX / _zoom(): @@ -118,12 +133,29 @@ func hit_test(world_pos: Vector2) -> Handle: return Handle.NONE +func get_scenery_resize_corner(world_pos: Vector2) -> Vector2: + if not _enabled or not _transform_handles_visible or _targets.size() != 1 or not _is_scenery_target(_targets[0]): + return Vector2.INF + var bounds: Rect2 = _target_bounds(_targets[0]) + var closest: Vector2 = Vector2.INF + var closest_distance: float = INF + for corner: Vector2 in _corners(bounds): + var offset: Vector2 = (world_pos - corner) * _zoom() + if absf(offset.x) > SCENERY_SCALE_HIT_HALF_SIZE_PX or absf(offset.y) > SCENERY_SCALE_HIT_HALF_SIZE_PX: + continue + var distance: float = offset.length_squared() + if distance < closest_distance: + closest = corner + closest_distance = distance + return closest + + ## Begins a rotate drag (used when the ring is clicked). func begin_drag(world_pos: Vector2) -> void: _dragging = hit_test(world_pos) _drag_last = world_pos if _dragging == Handle.ROTATE and _targets.size() == 1 and is_instance_valid(_targets[0]): - _rotate_center = STAGE_SELECTION.get_world_aabb(_targets[0]).get_center() + _rotate_center = _target_bounds(_targets[0]).get_center() _rotate_start_rotation = _targets[0].global_rotation _rotate_start_angle = (world_pos - _rotate_center).angle() transform_started.emit(_targets.duplicate()) @@ -220,16 +252,21 @@ func _draw() -> void: return var zoom := _zoom() if _hovered != null and is_instance_valid(_hovered) and not _hovered.is_queued_for_deletion() and not _targets.has(_hovered): - draw_rect(STAGE_SELECTION.get_world_aabb(_hovered), HOVER_COLOR, false, HOVER_WIDTH / zoom) + draw_rect(_target_bounds(_hovered), HOVER_COLOR, false, HOVER_WIDTH / zoom) # A selection outline for every selected object. for node: Node2D in _targets: if node != null and is_instance_valid(node): - draw_rect(STAGE_SELECTION.get_world_aabb(node), SELECTION_COLOR, false, SELECTION_WIDTH / zoom) + draw_rect(_target_bounds(node), SELECTION_COLOR, false, SELECTION_WIDTH / zoom) # Rotate ring only for a single selection. - if _targets.size() == 1 and is_instance_valid(_targets[0]): - var rect := STAGE_SELECTION.get_world_aabb(_targets[0]) - draw_arc(rect.get_center(), _rotate_ring_radius(rect), 0.0, TAU, 64, ROTATE_COLOR, ROTATE_WIDTH / zoom, true) - if _targets[0] is PropBlockScript: + if _transform_handles_visible and _targets.size() == 1 and is_instance_valid(_targets[0]): + var rect: Rect2 = _target_bounds(_targets[0]) + if not _is_scenery_target(_targets[0]): + draw_arc(rect.get_center(), _rotate_ring_radius(rect), 0.0, TAU, 64, ROTATE_COLOR, ROTATE_WIDTH / zoom, true) + if _is_scenery_target(_targets[0]): + for corner: Vector2 in _corners(rect): + draw_circle(corner, 11.0 / zoom, Color("#111821E8")) + draw_circle(corner, 7.0 / zoom, Color("#F5F7FA")) + elif _targets[0] is PropBlockScript: var half: float = SCALE_CROSS_SIZE_PX / zoom for corner: Vector2 in _corners(rect): draw_line(corner + Vector2(-half, 0.0), corner + Vector2(half, 0.0), Color(0.0, 0.0, 0.0, 0.85), 3.5 / zoom) @@ -261,6 +298,18 @@ func _snap(v: Vector2) -> Vector2: func _corners(rect: Rect2) -> Array[Vector2]: return [rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)] + +func _is_scenery_target(node: Node2D) -> bool: + return node != null and (node.get_script() == StageWallPanelScript or node.get_script() == StageSceneryArtworkScript) + + +func _target_bounds(node: Node2D) -> Rect2: + if node != null and node.get_script() == StageWallPanelScript: + return node.global_transform * (node as StageWallPanel).get_local_bounds() + if node != null and node.get_script() == StageSceneryArtworkScript: + return (node as StageSceneryArtwork).get_visual_world_bounds() + return STAGE_SELECTION.get_world_aabb(node) + func _nearest_corner(rect: Rect2, world_pos: Vector2) -> Vector2: var nearest: Vector2 = rect.position var distance: float = INF diff --git a/scripts/stage/stage_scene_io.gd b/scripts/stage/stage_scene_io.gd index a30ae02..e088241 100644 --- a/scripts/stage/stage_scene_io.gd +++ b/scripts/stage/stage_scene_io.gd @@ -3,13 +3,17 @@ extends RefCounted ## JSON boundary for Sandbox Stage documents. This class deliberately has no ## scene-tree dependencies, which makes rejected documents non-destructive. -const FILE_VERSION: String = "1.4" +const FILE_VERSION: String = "1.5" const CONTENT_PATHS := preload("res://scripts/shared/content_paths.gd") const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd") const TRIGGER_REGISTRY := preload("res://scripts/stage/trigger_registry.gd") const SPEECH_PAYLOAD := preload("res://scripts/speech/speech_payload.gd") const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd") const PROP_USE_SPOT_DATA := preload("res://scripts/props/prop_use_spot_data.gd") +const WALL_FINISHES: Array[String] = ["finish_wall_paint", "finish_wall_brick", "finish_wall_wood"] +const GROUND_FINISHES: Array[String] = ["finish_ground_paint", "finish_ground_brick", "finish_ground_wood", "finish_ground_grass", "finish_ground_dirt", "finish_ground_sidewalk", "finish_ground_asphalt", "finish_ground_legacy"] +const SKY_IDS: Array[String] = ["sky_clear_day", "sky_cloudy_day", "sky_sunset", "sky_starry_night", "sky_neutral"] +const SCENERY_DEPTHS: Array[String] = ["far", "near", "floor"] static func vector_to_data(value: Vector2) -> Dictionary: return {"x": value.x, "y": value.y} @@ -26,6 +30,14 @@ static func data_to_vector(value: Variant, fallback: Vector2) -> Vector2: var data: Dictionary = value return Vector2(float(data["x"]), float(data["y"])) +static func color_from_data(value: Variant, fallback: Color) -> Color: + if not _valid_color(value): + return fallback + var html_color: String = String(value).strip_edges() + if not html_color.begins_with("#"): + html_color = "#" + html_color + return Color.from_string(html_color, fallback) + static func encode_queue(actions: Array[Dictionary]) -> Dictionary: var out: Array[Dictionary] = [] var warnings: Array[String] = [] @@ -171,9 +183,10 @@ static func _migrate_to_current(data: Dictionary) -> Dictionary: prop_entry["wobble_on_impact"] = false _assign_legacy_depths(current.get("objects", [])) return current - if version not in ["1.0", "1.1", "1.2", "1.3"]: + if version not in ["1.0", "1.1", "1.2", "1.3", "1.4"]: return {} var migrated: Dictionary = data.duplicate(true) + migrated["sky_id"] = "sky_neutral" for raw: Variant in migrated.get("objects", []): if not raw is Dictionary: continue @@ -187,6 +200,8 @@ static func _migrate_to_current(data: Dictionary) -> Dictionary: object["next_use_spot_number"] = object.get("next_use_spot_number", 1) if not object.has("wobble_on_impact"): object["wobble_on_impact"] = false + if String(object.get("type", "")) == "terrain" and String(object.get("asset_id", "")) == "ground": + object["finish_id"] = "finish_ground_legacy" migrated["version"] = FILE_VERSION _assign_legacy_depths(migrated.get("objects", [])) return migrated @@ -217,6 +232,9 @@ static func _assign_legacy_depths(raw_objects: Variant) -> void: roles["depth"] = PROP_STAGE_ROLES.DEPTH_FRONT if characters_before > characters_after else PROP_STAGE_ROLES.DEPTH_BEHIND static func _validate_root(data: Dictionary) -> Dictionary: + var sky_id: Variant = data.get("sky_id", "sky_neutral") + if not sky_id is String or not SKY_IDS.has(String(sky_id)): + return {"ok": false, "error": "Scene Sky is invalid."} var seen: Dictionary = {} var seen_stage_ids: Dictionary = {} for raw: Variant in data["objects"]: @@ -249,8 +267,62 @@ static func _validate_root(data: Dictionary) -> Dictionary: for key: String in ["holds_other_props", "blocks_stickmen", "can_pick_up", "released_blocks_stickmen", "released_bumps_other_props", "legacy_draw_order"]: if roles.has(key) and not roles[key] is bool: return {"ok": false, "error": "Prop role '%s' must be true or false." % key} + elif String(entry.get("type", "")) == "wall_panel": + if not is_vector_data(entry.get("size")): + return {"ok": false, "error": "Wall panel size is invalid."} + var wall_size: Vector2 = data_to_vector(entry.get("size"), Vector2.ZERO) + if wall_size.x <= 0.0 or wall_size.y <= 0.0 or not WALL_FINISHES.has(String(entry.get("finish_id", ""))): + return {"ok": false, "error": "Wall panel size or finish is invalid."} + if not _valid_color(entry.get("color", "#5A9D70")): + return {"ok": false, "error": "Wall panel color is invalid."} + if entry.has("motif_origin_offset") and not is_vector_data(entry.get("motif_origin_offset")): + return {"ok": false, "error": "Wall panel motif origin is invalid."} + elif String(entry.get("type", "")) == "scenery_artwork": + var asset_id: Variant = entry.get("asset_id", "") + if not asset_id is String or String(asset_id).strip_edges().is_empty(): + return {"ok": false, "error": "Scenery artwork reference is invalid."} + entry["asset_id"] = CONTENT_PATHS.canonicalize(String(asset_id)) + if not entry.get("display_name", "Scenery") is String or not SCENERY_DEPTHS.has(String(entry.get("depth", "near"))): + return {"ok": false, "error": "Scenery artwork label or depth is invalid."} + var art_scale: Vector2 = data_to_vector(entry.get("scale"), Vector2.ZERO) + if not is_vector_data(entry.get("scale")) or art_scale.x <= 0.0 or art_scale.y <= 0.0: + return {"ok": false, "error": "Scenery artwork scale is invalid."} + if not _finite_number(entry.get("rotation", 0.0)): + return {"ok": false, "error": "Scenery artwork rotation is invalid."} + var bounds_value: Variant = entry.get("last_known_local_bounds", null) + if bounds_value != null and not _valid_rect_data(bounds_value): + entry.erase("last_known_local_bounds") + elif String(entry.get("type", "")) == "terrain" and String(entry.get("asset_id", "")) == "ground": + var finish: String = String(entry.get("finish_id", "finish_ground_legacy")) + if not GROUND_FINISHES.has(finish): + return {"ok": false, "error": "Ground finish is invalid."} return {"ok": true, "error": ""} + +static func _valid_color(value: Variant) -> bool: + if not value is String: + return false + var raw: String = String(value).strip_edges() + if raw.begins_with("#"): + raw = raw.substr(1) + if raw.length() not in [6, 8]: + return false + const HEX_DIGITS: String = "0123456789abcdefABCDEF" + for index: int in range(raw.length()): + if not HEX_DIGITS.contains(raw.substr(index, 1)): + return false + return true + + +static func _valid_rect_data(value: Variant) -> bool: + if not value is Dictionary: + return false + var rect_data: Dictionary = value + if not is_vector_data(rect_data.get("position")) or not is_vector_data(rect_data.get("size")): + return false + var size: Vector2 = data_to_vector(rect_data.get("size"), Vector2.ZERO) + return size.x > 0.0 and size.y > 0.0 + static func _recover_backup(path: String) -> void: var bak := path + ".bak" if not FileAccess.file_exists(path) and FileAccess.file_exists(bak): DirAccess.rename_absolute(bak, path) diff --git a/scripts/stage/stage_selection.gd b/scripts/stage/stage_selection.gd index ee77c6e..c750898 100644 --- a/scripts/stage/stage_selection.gd +++ b/scripts/stage/stage_selection.gd @@ -19,6 +19,10 @@ signal selection_changed(nodes: Array[Node2D]) # --------------------------------------------------------------------------- const RAGDOLL_CONTAINER_NAME := "RagdollBodyContainer" +const STAGE_WALL_PANEL := preload("res://scripts/stage/scenery/stage_wall_panel.gd") +const STAGE_SCENERY_ARTWORK := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd") + +enum SelectionScope { ACTION, SCENERY } ## Rig-local bounds for stickman hit-testing (mirrors the rig's standing bounds). const RIG_LOCAL_RECT := Rect2(Vector2(-120.0, -1000.0), Vector2(240.0, 1000.0)) @@ -32,6 +36,7 @@ var _camera: Camera2D var _selected: Array[Node2D] = [] var _hovered: Node2D = null var _primary: Node2D = null +var _scope: SelectionScope = SelectionScope.ACTION # --------------------------------------------------------------------------- # Lifecycle @@ -53,6 +58,12 @@ func get_primary() -> Node2D: return _primary +func set_scope(scope: SelectionScope) -> void: + _scope = scope + clear_selection() + clear_hover() + + func clear_selection() -> void: _selected.clear() _primary = null @@ -110,7 +121,7 @@ func hit_test_all(world_pos: Vector2) -> Array[Node2D]: continue if get_world_aabb(node).has_point(world_pos): hits.append(node) - var ragdoll_hit: Node2D = _ragdoll_rig_at(world_pos) + var ragdoll_hit: Node2D = _ragdoll_rig_at(world_pos) if _scope == SelectionScope.ACTION else null if ragdoll_hit != null and not hits.has(ragdoll_hit): hits.append(ragdoll_hit) hits.sort_custom(func(a: Node2D, b: Node2D) -> bool: @@ -219,6 +230,11 @@ func _is_selectable(node: Node) -> bool: return false if node.is_in_group("stickman_ragdoll_container") or bool(node.get_meta("stickman_ragdoll_container", false)): return false + var is_scenery: bool = node.get_script() == STAGE_WALL_PANEL or node.get_script() == STAGE_SCENERY_ARTWORK + if _scope == SelectionScope.ACTION and is_scenery: + return false + if _scope == SelectionScope.SCENERY and not is_scenery: + return false return true diff --git a/scripts/stage/stage_spawner.gd b/scripts/stage/stage_spawner.gd index 8c4e70d..6d185f9 100644 --- a/scripts/stage/stage_spawner.gd +++ b/scripts/stage/stage_spawner.gd @@ -19,6 +19,7 @@ const PROP_LIBRARY := preload("res://scripts/props/prop_library.gd") const STICKMAN_FACTORY := preload("res://scripts/characters/stickman/stickman_factory.gd") const CONTENT_PATHS := preload("res://scripts/shared/content_paths.gd") const TRIGGER_AREA := preload("res://scripts/stage/trigger_area.gd") +const STAGE_DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd") # --------------------------------------------------------------------------- # Constants @@ -67,13 +68,14 @@ func get_spawnable_ids() -> Array[String]: ## UI-safe registry view. Callers receive no templates or mutable registry state. func get_palette_entries() -> Array[Dictionary]: - var icons: Dictionary = {"ground":"🧱", "ramp":"📐", "step":"🪜", "prop":"📦", "stickman":"🧍", "area":"🎯"} - var categories: Dictionary = {"terrain":"Terrain", "prop":"Props", "stickman":"Cast", "area":"Events"} + var icons: Dictionary = {"ground":"🧱", "ramp":"📐", "step":"🪜", "prop":"📦", "stickman":"🧍", "area":"🎯", "scenery":"🌄"} + var categories: Dictionary = {"terrain":"Terrain", "prop":"Props", "stickman":"Cast", "area":"Events", "scenery":"Backgrounds"} var result: Array[Dictionary] = [] for entry: Dictionary in _registry: var id: String = String(entry.get("id", "")) var kind: String = String(entry.get("kind", "")) result.append({"placement_id":id, "kind":kind, "label":String(entry.get("label", id)), "icon":String(icons.get(id, "")), "category":String(categories.get(kind, "")), "opens_selector":kind == "prop" or kind == "stickman"}) + result.append({"placement_id":"scenery", "kind":"scenery", "label":"Scenery", "icon":"🌄", "category":"Backgrounds", "opens_selector":true}) return result @@ -328,6 +330,10 @@ func _spawn_terrain(entry: Dictionary, world_position: Vector2) -> TerrainBlock: ) block.position = world_position block.spawn_id = String(entry.get("id", "")) + if block.spawn_id == "ground": + block.finish_id = &"finish_ground_legacy" + block.set_finish(&"finish_ground_legacy") + STAGE_DRAW_ORDER.apply_terrain(block) return block diff --git a/scripts/terrain/terrain_block.gd b/scripts/terrain/terrain_block.gd index 1bc3c2d..4da3a4f 100644 --- a/scripts/terrain/terrain_block.gd +++ b/scripts/terrain/terrain_block.gd @@ -16,6 +16,8 @@ extends StaticBody2D const POLYGON_NODE_NAME := "Polygon2D" const OUTLINE_NODE_NAME := "Outline" const COLLISION_NODE_NAME := "CollisionPolygon2D" +const STAGE_SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd") +const STAGE_DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd") # --------------------------------------------------------------------------- # Exported properties @@ -55,6 +57,9 @@ const COLLISION_NODE_NAME := "CollisionPolygon2D" ## overlaps. Plain var (not inspector-editable). var spawn_id: String = "" +## Appearance-only identifier for Ground. Empty preserves the legacy vector fill. +var finish_id: StringName = &"" + # --------------------------------------------------------------------------- # Internal node references (built in _ready, @tool-safe) # --------------------------------------------------------------------------- @@ -71,6 +76,15 @@ func _ready() -> void: _ensure_children() _apply_points() _apply_style() + if spawn_id == "ground": + STAGE_DRAW_ORDER.apply_terrain(self) + + +func set_finish(id: StringName) -> void: + finish_id = id if STAGE_SURFACE_RENDERER.is_ground_finish(id) else &"" + if _polygon != null: + _polygon.visible = finish_id.is_empty() + queue_redraw() # --------------------------------------------------------------------------- # Internal build / apply @@ -120,3 +134,14 @@ func _apply_style() -> void: if _outline != null: _outline.default_color = outline_color _outline.width = outline_width + if _polygon != null: + _polygon.visible = finish_id.is_empty() + + +func _draw() -> void: + if spawn_id != "ground" or finish_id.is_empty() or polygon_points.is_empty(): + return + var rect := Rect2(polygon_points[0], Vector2.ZERO) + for point: Vector2 in polygon_points: + rect = rect.expand(point) + STAGE_SURFACE_RENDERER.draw_ground(self, rect, finish_id, fill_color, global_position, global_scale) diff --git a/tests/phase19_depth_inspector_visual_capture.gd b/tests/phase19_depth_inspector_visual_capture.gd new file mode 100644 index 0000000..5ceec7c --- /dev/null +++ b/tests/phase19_depth_inspector_visual_capture.gd @@ -0,0 +1,54 @@ +extends SceneTree + +const SANDBOX_STAGE: PackedScene = preload("res://scenes/stage/sandbox_stage.tscn") +const STAGE_SCENERY_ARTWORK: Script = preload("res://scripts/stage/scenery/stage_scenery_artwork.gd") +const CAPTURE_PATH: String = "res://tests/phase19_selected_artwork_depth_inspector.png" + + +func _initialize() -> void: + call_deferred("_capture") + + +func _capture() -> void: + root.size = Vector2i(1280, 720) + var stage: SandboxStage = SANDBOX_STAGE.instantiate() as SandboxStage + root.add_child(stage) + await _wait_for_render_frames(3) + var artwork: StageSceneryArtwork = STAGE_SCENERY_ARTWORK.new() as StageSceneryArtwork + artwork.display_name = "Round Tree" + artwork.set_depth(&"near") + if not artwork.load_visual("res://content/props/art_tree-v2.prop"): + push_error("Depth inspector visual capture could not load the tree artwork.") + artwork.free() + stage.queue_free() + quit(1) + return + artwork.position = Vector2(-300.0, -120.0) + (stage.get_node("World") as Node2D).add_child(artwork) + var camera: Camera2D = stage.get_node("Camera2D") as Camera2D + camera.position = Vector2.ZERO + camera.zoom = Vector2(0.85, 0.85) + stage.call("_on_grid_toggled", false) + stage.call("_enter_scenery_edit_mode") + (stage.get("_selection") as StageSelection).select_only(artwork) + stage.call("_refresh_scenery_inspector") + await _wait_for_render_frames(6) + var inspector: StageInspectorPanel = stage.get("_scenery_inspector") as StageInspectorPanel + var section: Control = stage.get("_scenery_depth_section") as Control + if inspector == null or not inspector.visible or section == null or not section.visible: + push_error("Depth inspector visual capture did not show its selected-artwork section.") + stage.queue_free() + quit(1) + return + var capture: Image = root.get_texture().get_image() + var error: Error = capture.save_png(ProjectSettings.globalize_path(CAPTURE_PATH)) + print("VISUAL selected artwork depth inspector path=%s size=%s save_error=%s" % [CAPTURE_PATH, str(capture.get_size()), str(error)]) + stage.queue_free() + await process_frame + quit(0 if error == OK else 1) + + +func _wait_for_render_frames(count: int) -> void: + for _frame: int in count: + await process_frame + await RenderingServer.frame_post_draw diff --git a/tests/phase19_depth_inspector_visual_capture.gd.uid b/tests/phase19_depth_inspector_visual_capture.gd.uid new file mode 100644 index 0000000..7ec4e84 --- /dev/null +++ b/tests/phase19_depth_inspector_visual_capture.gd.uid @@ -0,0 +1 @@ +uid://c0sufu217116a diff --git a/tests/phase19_gui_visual_capture.gd b/tests/phase19_gui_visual_capture.gd new file mode 100644 index 0000000..45766b2 --- /dev/null +++ b/tests/phase19_gui_visual_capture.gd @@ -0,0 +1,146 @@ +extends SceneTree + +const SANDBOX_STAGE: PackedScene = preload("res://scenes/stage/sandbox_stage.tscn") +const PROP_EDITOR_SCENE: PackedScene = preload("res://scenes/editors/prop/prop_editor.tscn") +const STAGE_WALL_PANEL: Script = preload("res://scripts/stage/scenery/stage_wall_panel.gd") +const STAGE_SCENERY_ARTWORK: Script = preload("res://scripts/stage/scenery/stage_scenery_artwork.gd") + + +func _initialize() -> void: + call_deferred("_capture_scenes") + + +func _capture_scenes() -> void: + root.size = Vector2i(1280, 720) + var stage: SandboxStage = SANDBOX_STAGE.instantiate() as SandboxStage + root.add_child(stage) + await _wait_for_render_frames(3) + var loaded: bool = stage.call("_load_scene_from_path", "res://content/stages/starters/town_street.scene", false) + print("VISUAL town_street_loaded=", loaded) + stage.call("_on_grid_toggled", false) + await _wait_for_render_frames(4) + await _save_viewport_image("res://tests/phase19_visual_town_street.png") + + loaded = stage.call("_load_scene_from_path", "res://content/stages/starters/living_room.scene", false) + print("VISUAL living_room_loaded=", loaded) + stage.call("_on_grid_toggled", false) + await _wait_for_render_frames(4) + await _save_viewport_image("res://tests/phase19_visual_living_room.png") + + loaded = stage.call("_load_scene_from_path", "res://content/stages/starters/blank.scene", false) + print("VISUAL blank_loaded=", loaded) + stage.call("_on_grid_toggled", false) + await _wait_for_render_frames(2) + var world: Node2D = stage.get_node("World") as Node2D + var camera: Camera2D = stage.get_node("Camera2D") as Camera2D + camera.position = Vector2(0.0, -180.0) + camera.zoom = Vector2(0.72, 0.72) + var sky_view: StageSkyView = stage.get("_sky_view") as StageSkyView + sky_view.set_sky(&"sky_sunset") + var brick_wall: StageWallPanel = STAGE_WALL_PANEL.new() as StageWallPanel + brick_wall.position = Vector2(-430.0, -220.0) + brick_wall.configure(Vector2(390.0, 290.0), &"finish_wall_brick", Color.WHITE) + world.add_child(brick_wall) + var spawner: StageSpawner = stage.get("_spawner") as StageSpawner + var grass: TerrainBlock = spawner.spawn_terrain_asset("ground", Vector2(120.0, 18.0)) + grass.set_finish(&"finish_ground_grass") + var tree: StageSceneryArtwork = STAGE_SCENERY_ARTWORK.new() as StageSceneryArtwork + tree.name = "ApprovedTreeArtwork" + tree.position = Vector2(-140.0, -150.0) + tree.display_name = "Tree" + tree.set_depth(&"near") + var tree_loaded: bool = tree.load_visual("res://content/props/art_tree-v2.prop") + world.add_child(tree) + var missing: StageSceneryArtwork = STAGE_SCENERY_ARTWORK.new() as StageSceneryArtwork + missing.name = "MissingArtworkExample" + missing.position = Vector2(100.0, -145.0) + missing.configure_missing("res://content/props/phase19_missing_art_demo.prop", "Tree", Rect2(Vector2(-52.0, -72.0), Vector2(104.0, 144.0)), &"near") + world.add_child(missing) + print("VISUAL approved_tree_loaded=", tree_loaded, " missing_marker=", missing.missing) + stage.call("_enter_scenery_edit_mode") + (stage.get("_selection") as StageSelection).select_only(missing) + stage.call("_refresh_scenery_inspector") + await _wait_for_render_frames(5) + await _save_viewport_image("res://tests/phase19_visual_finishes_and_repair.png") + var scenery_controller: StageSceneryController = stage.get("_scenery_controller") as StageSceneryController + if scenery_controller.begin_single_transform(missing, &"move", missing.global_position): + stage.call("_refresh_scenery_capture_bar") + await _wait_for_render_frames(3) + await _save_viewport_image("res://tests/phase19_visual_scenery_drag_cancel.png") + scenery_controller.cancel_single_transform() + stage.call("_refresh_scenery_capture_bar") + missing.queue_free() + camera.position = Vector2(0.0, -270.0) + tree.position = Vector2(-140.0, -95.0) + (stage.get("_selection") as StageSelection).select_only(tree) + stage.call("_refresh_scenery_inspector") + await _wait_for_render_frames(4) + await _save_viewport_image("res://tests/phase19_visual_scenery_selected_size.png") + var tree_bounds: Rect2 = tree.get_visual_world_bounds() + if scenery_controller.begin_single_transform(tree, &"resize", tree_bounds.position, tree_bounds.position): + stage.call("_refresh_scenery_capture_bar") + await _wait_for_render_frames(3) + await _save_viewport_image("res://tests/phase19_visual_scenery_resize_cancel.png") + scenery_controller.cancel_single_transform() + stage.call("_refresh_scenery_capture_bar") + + var browser: StageAssetBrowser = stage.get("_asset_browser") as StageAssetBrowser + stage.call("_load_scene_from_path", "res://content/stages/starters/blank.scene", false) + (stage.get("_selection") as StageSelection).clear_selection() + stage.call("_exit_scenery_edit_mode") + stage.call("_on_grid_toggled", true) + scenery_controller.begin_wall(&"finish_wall_brick", Vector2(-180.0, -100.0)) + await _wait_for_render_frames(3) + await _save_viewport_image("res://tests/phase19_visual_wall_panel_preview.png") + scenery_controller.exit_tool() + stage.call("_on_grid_toggled", false) + await _wait_for_render_frames(3) + var background_entries: Array[Dictionary] = browser.call("_scenery_background_entries") as Array[Dictionary] + var selector: AssetSelector = browser.get_selector() + selector.open("scenery", background_entries, "sky_sunset") + selector.call("_set_gallery_tab", "backgrounds") + await _wait_for_render_frames(4) + await _save_viewport_image("res://tests/phase19_visual_background_gallery.png") + stage.queue_free() + await process_frame + await process_frame + var prop_editor: PropEditor = PROP_EDITOR_SCENE.instantiate() as PropEditor + root.add_child(prop_editor) + await _wait_for_render_frames(3) + var prop_canvas: PropCanvas = prop_editor.get("_canvas") as PropCanvas + var fireplace_file: FileAccess = FileAccess.open("res://content/props/art_fireplace_mantel-v2.prop", FileAccess.READ) + if fireplace_file != null: + prop_editor.call("_apply_json_data", fireplace_file.get_as_text()) + await _wait_for_render_frames(3) + prop_editor.call("_on_size_for_stage_pressed") + await _wait_for_render_frames(3) + await _save_viewport_image("res://tests/phase19_visual_size_for_stage_entry.png") + prop_editor.call("_finish_stage_size_preview", false) + prop_canvas.set_shape_data([{ + "shape_type": "polygon", + "points": PackedVector2Array([Vector2(-50.0, -200.0), Vector2(50.0, -200.0), Vector2(50.0, 0.0), Vector2(-50.0, 0.0)]), + "fill_color": "#9D6A4A", + "outline_color": "#2D2532", + "outline_width": 4.0, + "closed": true, + }]) + prop_editor.call("_on_size_for_stage_pressed") + prop_editor.call("_choose_stage_size", 1.0) + await _wait_for_render_frames(4) + await _save_viewport_image("res://tests/phase19_visual_size_for_stage.png") + prop_editor.queue_free() + await process_frame + quit(0) + + +func _wait_for_render_frames(count: int) -> void: + for _frame: int in count: + await process_frame + await RenderingServer.frame_post_draw + + +func _save_viewport_image(path: String) -> void: + await RenderingServer.frame_post_draw + var image: Image = root.get_texture().get_image() + var error: Error = image.save_png(ProjectSettings.globalize_path(path)) + print("VISUAL capture path=", path, " size=", image.get_size(), " error=", error) diff --git a/tests/phase19_gui_visual_capture.gd.uid b/tests/phase19_gui_visual_capture.gd.uid new file mode 100644 index 0000000..efe96bc --- /dev/null +++ b/tests/phase19_gui_visual_capture.gd.uid @@ -0,0 +1 @@ +uid://d1551tbed0cpw diff --git a/tests/test_phase10_prop_integration.gd b/tests/test_phase10_prop_integration.gd index 42a3d98..0ed6e49 100644 --- a/tests/test_phase10_prop_integration.gd +++ b/tests/test_phase10_prop_integration.gd @@ -22,7 +22,11 @@ func _run() -> void: var builtins: Array[Dictionary] = PROP_LIBRARY.get_entries() _check(builtins.size() == 4, "built-in templates remain four entries") var entries: Array[Dictionary] = PROP_LIBRARY.scan() - _check(entries.size() == 8, "scan returns built-ins and checked-in custom props") + var scanned_briefcase: bool = false + for scanned_entry: Dictionary in entries: + if String(scanned_entry.get("path", "")) == "res://content/props/briefcase.prop": + scanned_briefcase = true + _check(entries.size() >= builtins.size() + 1 and scanned_briefcase, "scan returns built-ins and checked-in custom props") var entry: Dictionary = PROP_LIBRARY.get_entry("res://content/props/briefcase.prop") _check(not entry.is_empty() and bool(entry.get("is_custom", false)), "custom path resolves without selector scan") _check((entry.get("shapes", []) as Array).size() > 1, "custom entry preserves the full shape stack") diff --git a/tests/test_phase17a_prop_roles.gd b/tests/test_phase17a_prop_roles.gd index ac67549..df11692 100644 --- a/tests/test_phase17a_prop_roles.gd +++ b/tests/test_phase17a_prop_roles.gd @@ -76,7 +76,7 @@ func _test_legacy_migration() -> void: var migrated: Dictionary = STAGE_SCENE_IO._migrate_to_current(legacy) var prop_entry: Dictionary = migrated["objects"][1] var roles: Dictionary = prop_entry["roles"] - _expect(migrated["version"] == "1.3", "old stage version migrates to 1.3") + _expect(migrated["version"] == STAGE_SCENE_IO.FILE_VERSION, "old stage version migrates to the current schema") _expect(prop_entry["stage_object_id"] == "prop_a", "1.0 prop IDs migrate from file IDs") _expect(String(roles["start_motion"]) == PROP_STAGE_ROLES.START_PHYSICS, "legacy prop keeps dynamic start behavior") _expect(bool(roles["blocks_stickmen"]) and bool(roles["legacy_draw_order"]), "legacy prop keeps collisions and draw order") diff --git a/tests/test_phase19_scenery.gd b/tests/test_phase19_scenery.gd new file mode 100644 index 0000000..f1ddb86 --- /dev/null +++ b/tests/test_phase19_scenery.gd @@ -0,0 +1,1248 @@ +extends SceneTree + +const SANDBOX_STAGE: PackedScene = preload("res://scenes/stage/sandbox_stage.tscn") +const STAGE_SCENE_IO: Script = preload("res://scripts/stage/stage_scene_io.gd") +const STAGE_SCENE_ADAPTER: Script = preload("res://scripts/editors/stage/stage_scene_adapter.gd") +const PROP_EDITOR_SCENE: PackedScene = preload("res://scenes/editors/prop/prop_editor.tscn") +const STAGE_SCENERY_ARTWORK: Script = preload("res://scripts/stage/scenery/stage_scenery_artwork.gd") +const STAGE_WALL_PANEL: Script = preload("res://scripts/stage/scenery/stage_wall_panel.gd") +const STAGE_SURFACE_RENDERER: Script = preload("res://scripts/stage/scenery/stage_surface_renderer.gd") +const BACKGROUND_CARD_PREVIEW: Script = preload("res://scripts/stage/scenery/stage_background_card_preview.gd") +const STARTER_CARD_PREVIEW: Script = preload("res://scripts/stage/scenery/stage_starter_card_preview.gd") +const STAGE_SKY_VIEW_SCRIPT: Script = preload("res://scripts/stage/scenery/stage_sky_view.gd") +const STARTER_PATHS: Array[String] = [ + "res://content/stages/starters/blank.scene", + "res://content/stages/starters/town_street.scene", + "res://content/stages/starters/living_room.scene", +] + +var _failures: Array[String] = [] +var _stage: SandboxStage + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + root.size = Vector2i(1280, 720) + _test_large_brick_pattern_budget() + _test_surface_stroke_clipping() + _test_scene_color_compatibility() + await _test_stage_sky_visual_model() + if OS.get_cmdline_user_args().has("--capture-sky-v2-only"): + print("PASS: Phase 19 Sky render capture") + quit(0) + return + for starter_path: String in STARTER_PATHS: + var result: Dictionary = STAGE_SCENE_IO.read_file(starter_path) + _expect(bool(result.get("ok", false)), "starter scene validates: %s" % starter_path) + if bool(result.get("ok", false)): + _expect(String((result["data"] as Dictionary).get("version", "")) == STAGE_SCENE_IO.FILE_VERSION, "starter uses current Stage schema: %s" % starter_path) + + _stage = SANDBOX_STAGE.instantiate() as SandboxStage + root.add_child(_stage) + await process_frame + await process_frame + _test_grid_controls_layout() + var baseline_spawner: StageSpawner = _stage.get("_spawner") as StageSpawner + var baseline_ground: TerrainBlock = baseline_spawner.spawn_terrain_asset("ground", Vector2.ZERO) + _expect(baseline_ground != null, "baseline navigation probe places one ordinary Ground block") + _stage.call("_rebake_navigation") + await physics_frame + await physics_frame + var baseline_nav_region: NavigationRegion2D = _stage.get_node("NavigationRegion2D") as NavigationRegion2D + var baseline_nav_map: RID = baseline_nav_region.get_navigation_map() + NavigationServer2D.map_force_update(baseline_nav_map) + var baseline_nav_start: Vector2 = Vector2(-50.0, -16.0) + var baseline_nav_goal: Vector2 = Vector2(50.0, -16.0) + var baseline_nav_path: PackedVector2Array = NavigationServer2D.map_get_path( + baseline_nav_map, + baseline_nav_start, + baseline_nav_goal, + true + ) + print( + "[BaselineGroundNavigationProbe] polygons=%d vertices=%s iteration=%d top_surface=%s -> %s closest=%s -> %s path_points=%d" + % [ + baseline_nav_region.navigation_polygon.get_polygon_count(), + str(baseline_nav_region.navigation_polygon.get_vertices()), + NavigationServer2D.map_get_iteration_id(baseline_nav_map), + str(baseline_nav_start), + str(baseline_nav_goal), + str(NavigationServer2D.map_get_closest_point(baseline_nav_map, baseline_nav_start)), + str(NavigationServer2D.map_get_closest_point(baseline_nav_map, baseline_nav_goal)), + baseline_nav_path.size(), + ] + ) + var browser: StageAssetBrowser = _stage.get("_asset_browser") as StageAssetBrowser + var starter_entries: Array[Dictionary] = browser.scan_entries("starters") + _expect(starter_entries.size() == 3, "New Scene gallery lists Blank, Town Street, and Living Room") + var original_name: String = String(_stage.get("_scene_name")) + var original_world_count: int = (_stage.get_node("World") as Node2D).get_child_count() + var original_sky: String = String(_stage.get("_sky_id")) + _stage.call("_on_file_menu_id_pressed", 0) + var selector: AssetSelector = browser.get_selector() + _expect(selector.visible and selector.kind == "starters", "New Scene opens the starter chooser without clearing the current document") + var starter_close_button: Button = selector.find_child("CloseButton", true, false) as Button + _expect(starter_close_button != null, "starter chooser exposes its real Close control") + if starter_close_button != null: + await _activate_button(starter_close_button) + _expect(not selector.visible, "Cancel closes the starter chooser") + _expect(String(_stage.get("_scene_name")) == original_name and String(_stage.get("_sky_id")) == original_sky and (_stage.get_node("World") as Node2D).get_child_count() == original_world_count, "Cancel leaves the existing Stage document intact") + _stage.call("_on_file_menu_id_pressed", 0) + selector = browser.get_selector() + var starter_grid: GridContainer = selector.find_child("GridContainer", true, false) as GridContainer + await _test_starter_card_previews(starter_grid) + var town_card: AssetCard = starter_grid.get_child(1) as AssetCard if starter_grid != null and starter_grid.get_child_count() > 1 else null + _expect(town_card != null, "Town Street is selectable as a real starter card") + if town_card != null: + await _activate_card(town_card) + await process_frame + _expect(String(_stage.get("_scene_name")) == "Town Street", "choosing Town Street loads it as a named, unsaved scene") + _expect(String(_stage.get("_sky_id")) == "sky_cloudy_day", "Town Street starter selects the approved cloudy sky") + var world: Node2D = _stage.get_node("World") as Node2D + var terrain_finish_count: int = 0 + var scaled_terrain_count: int = 0 + for child: Node in world.get_children(): + if child is TerrainBlock and String((child as TerrainBlock).finish_id) in ["finish_ground_sidewalk", "finish_ground_asphalt"]: + terrain_finish_count += 1 + if is_equal_approx((child as TerrainBlock).scale.x, 7.0): + scaled_terrain_count += 1 + _expect(terrain_finish_count == 2 and scaled_terrain_count == 2, "Town Street uses full-width sidewalk and asphalt strips") + await physics_frame + await physics_frame + var navigation_region: NavigationRegion2D = _stage.get_node("NavigationRegion2D") as NavigationRegion2D + var navigation_map: RID = navigation_region.get_navigation_map() + NavigationServer2D.map_force_update(navigation_map) + var street_terrain_summary: Array[String] = [] + for terrain_node: Node in world.get_children(): + if terrain_node is TerrainBlock: + var street_terrain: TerrainBlock = terrain_node as TerrainBlock + var transformed_points: Array[Vector2] = [] + for point: Vector2 in street_terrain.polygon_points: + transformed_points.append(street_terrain.transform * point) + var visible_top_y: float = INF + for point: Vector2 in transformed_points: + visible_top_y = minf(visible_top_y, point.y) + var visible_top_edges: Array[String] = [] + var navigation_up_edges: Array[String] = [] + for point_index: int in street_terrain.polygon_points.size(): + var a: Vector2 = transformed_points[point_index] + var b: Vector2 = transformed_points[(point_index + 1) % transformed_points.size()] + var edge: Vector2 = b - a + var outward: Vector2 = Vector2(edge.y, -edge.x).normalized() + if is_equal_approx(a.y, visible_top_y) and is_equal_approx(b.y, visible_top_y): + visible_top_edges.append("%s -> %s" % [str(a), str(b)]) + if outward.y < -0.15: + navigation_up_edges.append("%s -> %s" % [str(a), str(b)]) + street_terrain_summary.append("%s scale=%s transform=%s visible_top=%s nav_up=%s" % [String(street_terrain.finish_id), str(street_terrain.scale), str(street_terrain.transform), str(visible_top_edges), str(navigation_up_edges)]) + var sidewalk_start: Vector2 = Vector2(-600.0, -16.0) + var sidewalk_goal: Vector2 = Vector2(600.0, -16.0) + var road_start: Vector2 = Vector2(-600.0, 16.0) + var road_goal: Vector2 = Vector2(600.0, 16.0) + var sidewalk_path: PackedVector2Array = NavigationServer2D.map_get_path( + navigation_map, + sidewalk_start, + sidewalk_goal, + true + ) + var road_path: PackedVector2Array = NavigationServer2D.map_get_path( + navigation_map, + road_start, + road_goal, + true + ) + var sidewalk_to_road_path: PackedVector2Array = NavigationServer2D.map_get_path( + navigation_map, + Vector2(600.0, -16.0), + Vector2(600.0, 16.0), + true + ) + print( + "[TownStreetNavigationCheck] nav_dirty=%s polygons=%d agent_radius=%.1f vertices=%s iteration=%d terrain=%s sidewalk=(%s -> %s; closest %s -> %s; path=%s) road=(%s -> %s; closest %s -> %s; path=%s) curb_path=%s" + % [ + str(bool(_stage.get("_nav_dirty"))), + navigation_region.navigation_polygon.get_polygon_count(), + navigation_region.navigation_polygon.agent_radius, + str(navigation_region.navigation_polygon.get_vertices()), + NavigationServer2D.map_get_iteration_id(navigation_map), + str(street_terrain_summary), + str(sidewalk_start), + str(sidewalk_goal), + str(NavigationServer2D.map_get_closest_point(navigation_map, sidewalk_start)), + str(NavigationServer2D.map_get_closest_point(navigation_map, sidewalk_goal)), + str(sidewalk_path), + str(road_start), + str(road_goal), + str(NavigationServer2D.map_get_closest_point(navigation_map, road_start)), + str(NavigationServer2D.map_get_closest_point(navigation_map, road_goal)), + str(road_path), + str(sidewalk_to_road_path), + ] + ) + _expect( + NavigationServer2D.map_get_iteration_id(navigation_map) > 0 and navigation_region.navigation_polygon.get_polygon_count() > 0, + "Town Street navigation region synchronizes its starter terrain polygons" + ) + _expect(not bool(_stage.get("_nav_dirty")), "Town Street deferred navigation rebake completed after starter application") + + # Drive the Stage's existing palette/gallery callbacks and its real + # _unhandled_input(InputEventMouseButton) production entry point. + _stage.call("_on_palette_card_activated", {"placement_id": "scenery", "opens_selector": true}) + selector = browser.get_selector() + _expect(selector.visible and selector.kind == "scenery", "Scenery palette opens the shared gallery") + selector.call("_set_gallery_tab", "backgrounds") + await process_frame + var background_grid: GridContainer = selector.find_child("GridContainer", true, false) as GridContainer + var background_preview_count: int = 0 + var cloudy_card: AssetCard = _find_card_by_id(background_grid, "sky_cloudy_day") + if background_grid != null: + for card_node: Node in background_grid.get_children(): + for card_content: Node in card_node.get_children(): + for preview_node: Node in card_content.get_children(): + if preview_node.get_script() == BACKGROUND_CARD_PREVIEW: + background_preview_count += 1 + _expect(background_grid != null and background_grid.get_child_count() == 7 and background_preview_count == 7, "Sky and wall-finish cards render a distinct production-style preview") + var cloudy_status: Label = cloudy_card.find_child("StatusLabel", true, false) as Label if cloudy_card != null else null + _expect(cloudy_card != null and cloudy_card.button_pressed and cloudy_status != null and cloudy_status.text == "Current Sky", "gallery marks the loaded Town Street Sky as Current Sky") + var clear_sky_card: AssetCard = _find_card_by_id(background_grid, "sky_clear_day") + if clear_sky_card != null: + await _activate_card(clear_sky_card) + var selected_clear_card: AssetCard = _find_card_by_id(selector.find_child("GridContainer", true, false) as GridContainer, "sky_clear_day") + var clear_status: Label = selected_clear_card.find_child("StatusLabel", true, false) as Label if selected_clear_card != null else null + _expect(selector.visible and String(_stage.get("_sky_id")) == "sky_clear_day", "choosing a Sky updates the Stage and keeps the chooser open") + _expect(selected_clear_card != null and selected_clear_card.button_pressed and clear_status != null and clear_status.text == "Current Sky", "Current Sky marker follows the newly chosen card") + selector.call("_set_gallery_tab", "artwork") + await process_frame + var artwork_entries: Array[Dictionary] = browser.scan_entries("scenery") + _expect(not artwork_entries.is_empty(), "Artwork tab has approved cutouts") + var prior_art_count: int = _count_artwork(world) + var artwork_grid: GridContainer = selector.find_child("GridContainer", true, false) as GridContainer + var artwork_card: AssetCard = artwork_grid.get_child(0) as AssetCard if artwork_grid != null and artwork_grid.get_child_count() > 0 else null + _expect(artwork_card != null, "Artwork gallery provides a real artwork card") + if artwork_card != null: + await _activate_card(artwork_card) + await process_frame + var scenery_controller: StageSceneryController = _stage.get("_scenery_controller") as StageSceneryController + _expect(scenery_controller.is_active(), "choosing artwork starts the Scenery placement tool") + var world_click: Vector2 = Vector2(760.0, 390.0) + var art_count_before_placement: int = _count_artwork(world) + var committed_nodes: Array[Node2D] = [] + scenery_controller.placement_committed.connect(func(node: Node2D) -> void: committed_nodes.append(node)) + await _send_stage_click(world_click, MOUSE_BUTTON_LEFT) + var placed_artwork: StageSceneryArtwork = committed_nodes[0] as StageSceneryArtwork if not committed_nodes.is_empty() else null + _expect(placed_artwork != null, "left click places an independent Scenery artwork node") + _expect(_count_artwork(world) == art_count_before_placement + 1, "left click adds exactly one Scenery artwork copy") + if placed_artwork != null: + _expect(not _contains_collision_object(placed_artwork), "Scenery artwork is not a physics object") + _expect(placed_artwork.get_node_or_null("CollisionPolygon2D") == null, "Scenery artwork contains no collision polygon") + var original_position: Vector2 = placed_artwork.global_position + _stage.set_mode(SandboxStage.StageMode.PLAY) + await physics_frame + await physics_frame + _expect(placed_artwork.global_position.is_equal_approx(original_position), "Scenery remains stationary during PLAY physics") + _stage.set_mode(SandboxStage.StageMode.EDIT) + await _send_stage_click(world_click, MOUSE_BUTTON_RIGHT) + _expect(not scenery_controller.is_active(), "RMB finishes Scenery placement") + _expect(world.get_children().any(func(node: Node) -> bool: return node.get_script() == STAGE_SCENERY_ARTWORK), "RMB keeps pieces already placed") + _test_scenery_placement_snap(world, scenery_controller, String(artwork_entries[0].get("path", ""))) + await _test_wall_eraser(world, scenery_controller) + + var roundtrip_path: String = "res://tests/phase19_scenery_roundtrip_tmp.scene" + var saved: bool = _stage.call("_save_scene_to_path", roundtrip_path, false) + _expect(saved, "Stage save succeeds with Scenery in the document") + if saved: + var reopen_ok: bool = _stage.call("_load_scene_from_path", roundtrip_path, false) + _expect(reopen_ok, "Stage reopens the saved Scenery document") + var reopened_art_count: int = 0 + for child: Node in world.get_children(): + if child.get_script() == STAGE_SCENERY_ARTWORK: + reopened_art_count += 1 + _expect(reopened_art_count == prior_art_count + 1, "save/reopen preserves the placed artwork exactly once") + _expect(_count_walls_with_origin(world, Vector2(20000.0, 20000.0)) == 4, "save/reopen preserves the four wall pieces around an erased opening") + for child: Node in world.get_children(): + if child.get_script() == STAGE_WALL_PANEL and not (child as StageWallPanel).motif_origin_offset.is_zero_approx(): + _expect((child as StageWallPanel).get_effective_motif_origin_world().is_equal_approx(Vector2(20000.0, 20000.0)), "saved wall fragments keep their shared brick pattern anchor") + break + _test_wall_eraser_full_cut(world, _stage.get("_scenery_controller") as StageSceneryController) + DirAccess.remove_absolute(ProjectSettings.globalize_path(roundtrip_path)) + + await _test_group_preview_save_gate(world) + await _test_missing_artwork_repair(world, browser) + await _test_single_scenery_transforms(world, String(artwork_entries[0].get("path", ""))) + await _test_selected_scenery_depth(world, String(artwork_entries[0].get("path", ""))) + await _test_prop_size_for_stage() + if _stage != null and is_instance_valid(_stage): + _stage.queue_free() + await process_frame + await process_frame + + if _failures.is_empty(): + print("PASS: Phase 19 starter/gallery/input/physics/save roundtrip") + else: + for failure: String in _failures: + push_error("FAIL: " + failure) + quit(0 if _failures.is_empty() else 1) + + +func _test_large_brick_pattern_budget() -> void: + var view_size: Vector2 = Vector2(12000.0, 8000.0) + var detail_scale: float = STAGE_SURFACE_RENDERER.brick_detail_scale_for_view(view_size) + var columns: int = int(ceil(view_size.x / (82.0 * detail_scale))) + 2 + var rows: int = int(ceil(view_size.y / (34.0 * detail_scale))) + 2 + var covered_width: float = float(columns - 2) * 82.0 * detail_scale + var covered_height: float = float(rows - 2) * 34.0 * detail_scale + _expect(detail_scale > 1.0, "very large visible brick fields use one uniform detail scale instead of silently dropping rows and columns") + _expect(rows * columns <= STAGE_SURFACE_RENDERER.MAX_VISIBLE_BRICK_CELLS, "large brick fields stay within the visible-cell budget") + _expect(covered_width >= view_size.x and covered_height >= view_size.y, "large brick fields keep the full visible area covered after detail scaling") + + +func _test_surface_stroke_clipping() -> void: + var rect: Rect2 = Rect2(Vector2(10.0, 10.0), Vector2(100.0, 60.0)) + var horizontal: PackedVector2Array = STAGE_SURFACE_RENDERER._clip_line_to_rect( + rect, Vector2(-30.0, 20.0), Vector2(200.0, 20.0), 4.0 + ) + _expect(horizontal.size() == 2, "surface horizontal strokes crossing the panel are retained") + if horizontal.size() == 2: + _expect(horizontal[0].is_equal_approx(Vector2(12.0, 20.0)) and horizontal[1].is_equal_approx(Vector2(108.0, 20.0)), "surface horizontal strokes are clipped inside panel edges with stroke width accounted for") + var vertical: PackedVector2Array = STAGE_SURFACE_RENDERER._clip_line_to_rect( + rect, Vector2(20.0, -30.0), Vector2(20.0, 100.0), 4.0 + ) + _expect(vertical.size() == 2, "surface vertical strokes crossing the panel are retained") + if vertical.size() == 2: + _expect(vertical[0].is_equal_approx(Vector2(20.0, 12.0)) and vertical[1].is_equal_approx(Vector2(20.0, 68.0)), "surface vertical strokes are clipped inside panel edges with stroke width accounted for") + var outside: PackedVector2Array = STAGE_SURFACE_RENDERER._clip_line_to_rect( + rect, Vector2(-30.0, 9.0), Vector2(200.0, 9.0), 4.0 + ) + _expect(outside.is_empty(), "surface strokes outside the panel bounds are discarded") + + +func _test_stage_sky_visual_model() -> void: + var sky_view: StageSkyView = STAGE_SKY_VIEW_SCRIPT.new() as StageSkyView + sky_view.size = Vector2(root.size) + root.add_child(sky_view) + await process_frame + var capture_sky_views: bool = OS.get_cmdline_user_args().has("--capture-sky-v2") + for sky_id: StringName in StageSkyView.SKY_IDS: + sky_view.set_sky(sky_id) + await process_frame + var texture_value: Variant = sky_view.get("_gradient_texture") + var gradient_texture: GradientTexture2D = texture_value as GradientTexture2D + _expect(gradient_texture != null and gradient_texture.height >= 2048, "%s uses a high-resolution continuous gradient texture" % String(sky_id)) + if gradient_texture != null and gradient_texture.gradient != null: + var gradient: Gradient = gradient_texture.gradient + _expect(gradient.colors.size() >= 3 and gradient.offsets.size() == gradient.colors.size(), "%s retains its approved multi-stop palette" % String(sky_id)) + if capture_sky_views: + await RenderingServer.frame_post_draw + var capture: Image = root.get_texture().get_image() + var capture_path: String = ProjectSettings.globalize_path("user://phase19_sky_%s.png" % String(sky_id)) + var capture_error: Error = capture.save_png(capture_path) + _expect(capture_error == OK, "%s visual capture saved for inspection" % String(sky_id)) + var outline_value: Variant = sky_view.call("_build_cloud_outline") + var cloud_outline: PackedVector2Array = outline_value + _expect(cloud_outline.size() >= 100, "cloud silhouette is a detailed tessellated Bézier contour") + if cloud_outline.size() > 1: + _expect(cloud_outline[0].is_equal_approx(cloud_outline[cloud_outline.size() - 1]), "cloud contour is continuous and closed") + var underside_value: Variant = sky_view.call("_build_cloud_underside") + var cloud_underside: PackedVector2Array = underside_value + _expect(cloud_underside.size() >= 80, "cloud underside uses a connected curved silhouette") + var moon_outline_value: Variant = sky_view.call("_sample_curve_chain", sky_view.call("_build_moon_curves"), 16) + var moon_outline: PackedVector2Array = moon_outline_value + _expect(moon_outline.size() >= 70 and moon_outline[0].is_equal_approx(moon_outline[moon_outline.size() - 1]), "crescent outline is one continuous closed curve") + var moon_arc_value: Variant = sky_view.call("_sample_curve_chain", sky_view.call("_build_moon_outer_arc_curves"), 16) + var moon_outer_arc: PackedVector2Array = moon_arc_value + var warm_rim_value: Variant = sky_view.call("_build_moon_warm_rim", moon_outer_arc, 7.5) + var warm_rim: PackedVector2Array = warm_rim_value + _expect(warm_rim.size() == moon_outer_arc.size() * 2, "crescent warm shading follows the full outer arc as an inset band") + sky_view.queue_free() + await process_frame + + +func _expect(condition: bool, description: String) -> void: + if condition: + print("PASS: " + description) + else: + _failures.append(description) + + +func _test_scene_color_compatibility() -> void: + var wall_document: Dictionary = { + "version": STAGE_SCENE_IO.FILE_VERSION, + "sky_id": "sky_clear_day", + "objects": [{ + "file_id": "wall_panel_1", + "type": "wall_panel", + "position": {"x": 0.0, "y": 0.0}, + "rotation": 0.0, + "size": {"x": 320.0, "y": 400.0}, + "finish_id": "finish_wall_paint", + "color": "5a9d70ff", + }], + "event_rules": [], + } + var validation: Dictionary = STAGE_SCENE_IO._validate_root(wall_document) + _expect(bool(validation.get("ok", false)), "wall tint written without a # prefix is accepted") + var decoded: Color = STAGE_SCENE_IO.color_from_data("5a9d70ff", Color.MAGENTA) + _expect(decoded.is_equal_approx(Color("#5a9d70ff")), "wall tint without a # prefix decodes to its original color") + _expect(not STAGE_SCENE_IO._valid_color("5a9d70gg"), "malformed wall tint remains rejected") + + +func _test_grid_controls_layout() -> void: + var chrome: CanvasLayer = _stage.get("_chrome") as CanvasLayer + if chrome == null: + _expect(false, "Stage Chrome exists for grid controls") + return + var snap_check: CheckBox = chrome.call("get_snap_check") as CheckBox + var grid_check: CheckBox = chrome.call("get_grid_check") as CheckBox + var size_spin: SpinBox = chrome.call("get_grid_size_spin") as SpinBox + for control: Control in [grid_check, snap_check, size_spin]: + if control == null: + _expect(false, "Grid and Snap controls are present in the Stage toolbar") + continue + var control_rect: Rect2 = control.get_global_rect() + _expect(control.is_visible_in_tree(), "%s is visible in Edit mode" % control.name) + _expect(control_rect.size.x > 0.0 and control_rect.size.y > 0.0, "%s has a non-zero toolbar layout" % control.name) + _expect(control_rect.position.x >= 0.0 and control_rect.end.x <= root.size.x, "%s fits within the Stage window width" % control.name) + _expect(control_rect.position.y >= 0.0 and control_rect.end.y <= 64.0, "%s fits within the Stage top bar" % control.name) + var file_menu: MenuButton = chrome.call("get_file_menu") as MenuButton + if file_menu != null: + await _send_stage_click(file_menu.get_global_rect().get_center(), MOUSE_BUTTON_LEFT) + _expect(file_menu.get_popup().visible, "File menu opens when clicked beside the right-aligned Grid and Snap controls") + file_menu.get_popup().hide() + + +func _test_starter_card_previews(starter_grid: GridContainer) -> void: + if starter_grid == null: + _expect(false, "starter chooser has a grid for scene previews") + return + await process_frame + await process_frame + var live_card_count: int = 0 + for card_node: Node in starter_grid.get_children(): + if not card_node.is_queued_for_deletion(): + live_card_count += 1 + _expect(live_card_count == 3, "starter chooser has three scene cards to preview") + var preview_count: int = 0 + var populated_scene_count: int = 0 + for card_node: Node in starter_grid.get_children(): + if card_node.is_queued_for_deletion(): + continue + var preview: Control = card_node.find_child("StarterScenePreview", true, false) as Control + if preview == null: + continue + preview_count += 1 + _expect(bool(preview.call("has_preview_scene")), "starter card has a loaded scene preview: %s" % card_node.name) + if int(preview.call("get_loaded_object_count")) > 0: + populated_scene_count += 1 + var viewport: SubViewport = preview.get_child(0) as SubViewport if preview.get_child_count() > 0 else null + _expect(viewport != null and preview.find_child("SkyView", true, false) != null, "starter preview has an isolated viewport and sky renderer") + _expect(preview_count == 3, "all three starter cards use scene previews instead of placeholder tiles") + _expect(populated_scene_count == 2, "Town Street and Living Room previews load their scene objects") + + +func _contains_collision_object(node: Node) -> bool: + if node is CollisionObject2D: + return true + for child: Node in node.get_children(): + if _contains_collision_object(child): + return true + return false + + +func _count_artwork(world: Node2D) -> int: + var count: int = 0 + for child: Node in world.get_children(): + if child.get_script() == STAGE_SCENERY_ARTWORK: + count += 1 + return count + + +func _test_scenery_placement_snap(world: Node2D, controller: StageSceneryController, artwork_path: String) -> void: + var previous_snap: bool = bool(_stage.get("_snap_enabled")) + var previous_grid_size: float = float(_stage.get("_grid_size")) + _stage.call("_on_grid_size_changed", 20.0) + _stage.call("_on_snap_toggled", true) + var raw_art_position: Vector2 = Vector2(47.0, 58.0) + var click := InputEventMouseButton.new() + click.button_index = MOUSE_BUTTON_LEFT + click.pressed = true + _expect(controller.begin_artwork(artwork_path), "snap test starts an artwork placement tool") + controller.update_pointer(raw_art_position) + var artwork_ghost: StageSceneryArtwork = controller.get("_ghost") as StageSceneryArtwork + var ghost_bounds: Rect2 = artwork_ghost.get_visual_world_bounds() if artwork_ghost != null else Rect2() + _expect(artwork_ghost != null and ghost_bounds.get_center().is_equal_approx(Vector2(40.0, 60.0)), "artwork preview centers its visible bounds under the snapped cursor") + _expect(artwork_ghost != null and _is_grid_aligned(ghost_bounds.get_center(), 20.0), "artwork preview center aligns to a grid intersection when snapping is enabled") + controller.handle_mouse_button(click, raw_art_position, false) + var snapped_artwork: Node2D = _last_node_with_script(world, STAGE_SCENERY_ARTWORK) + var snapped_art_bounds: Rect2 = (snapped_artwork as StageSceneryArtwork).get_visual_world_bounds() if snapped_artwork != null else Rect2() + _expect(snapped_artwork != null and snapped_art_bounds.get_center().is_equal_approx(Vector2(40.0, 60.0)), "placed artwork center matches the preview and snapped cursor") + _expect(controller.get("_ghost") == artwork_ghost and controller.get_active_tool() == &"artwork", "placing artwork keeps its cursor ghost ready for another copy") + if snapped_artwork != null: + snapped_artwork.free() + + _stage.call("_on_snap_toggled", false) + _expect(controller.begin_artwork(artwork_path), "unsnapped test restarts artwork placement") + controller.handle_mouse_button(click, raw_art_position, false) + var unsnapped_artwork: Node2D = _last_node_with_script(world, STAGE_SCENERY_ARTWORK) + var unsnapped_art_bounds: Rect2 = (unsnapped_artwork as StageSceneryArtwork).get_visual_world_bounds() if unsnapped_artwork != null else Rect2() + _expect(unsnapped_artwork != null and unsnapped_art_bounds.get_center().is_equal_approx(raw_art_position), "artwork placement centers under the exact cursor when grid snapping is off") + if unsnapped_artwork != null: + unsnapped_artwork.free() + + _stage.call("_on_snap_toggled", true) + controller.begin_wall(&"finish_wall_brick", raw_art_position) + var wall_ghost: StageWallPanel = controller.get("_ghost") as StageWallPanel + var wall_ghost_bounds: Rect2 = wall_ghost.global_transform * wall_ghost.get_local_bounds() if wall_ghost != null else Rect2() + print("[WallPanelGhostStart] raw=%s global_position=%s bounds=%s" % [str(raw_art_position), str(wall_ghost.global_position if wall_ghost != null else Vector2.INF), str(wall_ghost_bounds)]) + _expect(wall_ghost != null and wall_ghost_bounds.position.is_equal_approx(Vector2(40.0, 60.0)), "wall-panel preview immediately uses the current cursor as its visible top-left") + _expect(_is_grid_aligned(wall_ghost_bounds.position, 20.0), "wall-panel preview aligns its visible top-left to a grid intersection") + var wall_start := InputEventMouseButton.new() + wall_start.button_index = MOUSE_BUTTON_LEFT + wall_start.pressed = true + var expected_wall_start: Vector2 = wall_ghost_bounds.position + controller.handle_mouse_button(wall_start, raw_art_position, false) + var actual_wall_start: Vector2 = controller.get("_wall_drag_start") + _expect(actual_wall_start.is_equal_approx(expected_wall_start), "wall-panel drawing starts at the preview ghost corner") + wall_ghost_bounds = wall_ghost.global_transform * wall_ghost.get_local_bounds() + _expect(_is_grid_aligned(wall_ghost_bounds.position, 20.0), "wall-panel preview stays top-left aligned when a drag begins") + var wall_end := InputEventMouseButton.new() + wall_end.button_index = MOUSE_BUTTON_LEFT + wall_end.pressed = false + controller.handle_mouse_button(wall_end, Vector2(113.0, 138.0), false) + var snapped_wall: Node2D = _last_node_with_script(world, STAGE_WALL_PANEL) + var snapped_wall_bounds: Rect2 = (snapped_wall.global_transform * (snapped_wall as StageWallPanel).get_local_bounds()) if snapped_wall != null else Rect2() + _expect(snapped_wall != null and snapped_wall_bounds.position.is_equal_approx(Vector2(40.0, 60.0)), "wall-panel placement aligns its top-left after snapping both drag endpoints") + _expect(wall_ghost != null and wall_ghost.panel_size.is_equal_approx(Vector2(240.0, 280.0)), "wall-panel placement restores the original preview size after drawing") + var restored_ghost_bounds: Rect2 = wall_ghost.global_transform * wall_ghost.get_local_bounds() if wall_ghost != null else Rect2() + _expect(restored_ghost_bounds.position.is_equal_approx(Vector2(120.0, 140.0)), "wall-panel preview returns to the cursor at its original size after drawing") + if snapped_wall != null: + _expect(Vector2(snapped_wall.get("panel_size")).is_equal_approx(Vector2(80.0, 80.0)), "wall-panel dimensions follow the snapped drag endpoints") + snapped_wall.free() + controller.exit_tool() + _stage.call("_on_snap_toggled", false) + controller.begin_wall(&"finish_wall_brick", raw_art_position) + wall_ghost = controller.get("_ghost") as StageWallPanel + wall_ghost_bounds = wall_ghost.global_transform * wall_ghost.get_local_bounds() if wall_ghost != null else Rect2() + _expect(wall_ghost != null and wall_ghost_bounds.position.is_equal_approx(raw_art_position), "unsnapped wall preview uses the raw cursor as its visible top-left") + controller.handle_mouse_button(wall_start, raw_art_position, false) + actual_wall_start = controller.get("_wall_drag_start") + _expect(actual_wall_start.is_equal_approx(raw_art_position), "unsnapped wall drawing starts at the preview ghost corner") + controller.cancel_active_gesture() + controller.exit_tool() + _stage.call("_on_grid_size_changed", previous_grid_size) + _stage.call("_on_snap_toggled", previous_snap) + + +func _test_wall_eraser(world: Node2D, controller: StageSceneryController) -> void: + var wall := STAGE_WALL_PANEL.new() as StageWallPanel + wall.position = Vector2(20000.0, 20000.0) + wall.configure(Vector2(400.0, 300.0), &"finish_wall_brick", Color("#B95F3D")) + world.add_child(wall) + var start := Vector2(19900.0, 19900.0) + var finish := Vector2(20100.0, 20100.0) + var snapped_start: Vector2 = controller.call("_snap", start) + var snapped_finish: Vector2 = controller.call("_snap", finish) + var expected_erase_rect: Rect2 = Rect2(snapped_start, snapped_finish - snapped_start).abs() + var local_cut_size: Vector2 = (wall.to_local(snapped_finish) - wall.to_local(snapped_start)).abs() + var expected_remaining_area: float = wall.panel_size.x * wall.panel_size.y - local_cut_size.x * local_cut_size.y + var document_changes: Array[int] = [0] + controller.document_change_requested.connect(func() -> void: document_changes[0] += 1) + _stage.call("_enter_scenery_edit_mode") + var selection: StageSelection = _stage.get("_selection") as StageSelection + selection.select_only(wall) + var erase_button: Button = _stage.get("_scenery_erase_wall_area_button") as Button + _expect(erase_button != null and erase_button.visible and erase_button.text == "Erase wall area" and erase_button.custom_minimum_size.y >= 44.0, "selected wall inspector exposes the touch-sized Erase wall area tool") + if erase_button != null: + erase_button.pressed.emit() + _expect(controller.get_active_tool() == &"wall_eraser", "Erase wall area starts the dedicated eraser from Edit Scenery") + var preview: Node2D = controller.get("_ghost") as Node2D + var capture_bar: StageUseTargetCaptureBar = _stage.get("_use_target_capture_bar") as StageUseTargetCaptureBar + _expect(controller.get_active_tool() == &"wall_eraser" and preview != null and preview.name == "WallEraserPreview", "wall eraser shows its distinct preview instead of a wall panel") + var prompt: Label = capture_bar.find_child("Prompt", true, false) as Label + var done_button: Button = capture_bar.find_child("Action_done_tool", true, false) as Button + var cancel_button: Button = capture_bar.find_child("Action_cancel_tool", true, false) as Button + _expect(capture_bar.visible and prompt != null and prompt.text == "Drag over the wall · release to erase" and done_button != null and cancel_button != null, "wall eraser capture bar explains release and keeps Done and Cancel visible") + var press := InputEventMouseButton.new() + press.button_index = MOUSE_BUTTON_LEFT + press.pressed = true + controller.handle_mouse_button(press, start, false) + controller.handle_mouse_motion(finish) + var release := InputEventMouseButton.new() + release.button_index = MOUSE_BUTTON_LEFT + release.pressed = false + controller.handle_mouse_button(release, finish, false) + var fragments: Array[StageWallPanel] = _wall_fragments_with_origin(world, Vector2(20000.0, 20000.0)) + _expect(fragments.size() == 4, "dragging the eraser through a wall makes one rectangular opening") + var remaining_area: float = 0.0 + var no_fragment_in_cut: bool = true + for fragment: StageWallPanel in fragments: + remaining_area += fragment.panel_size.x * fragment.panel_size.y + var fragment_bounds: Rect2 = fragment.global_transform * fragment.get_local_bounds() + var local_top_left: Vector2 = fragment.get_local_bounds().position + var rendered_top_left: Vector2 = local_top_left + fragment.get_surface_draw_origin_world() + _expect(rendered_top_left.is_equal_approx(fragment.to_global(local_top_left)), "brick pattern on an erased fragment keeps its world-space mortar phase") + if fragment_bounds.intersects(expected_erase_rect): + no_fragment_in_cut = false + _expect(fragment.finish_id == &"finish_wall_brick" and fragment.tint.is_equal_approx(Color("#B95F3D")), "wall fragments preserve their finish and color") + _expect(is_equal_approx(remaining_area, expected_remaining_area), "wall fragments preserve the area outside the opening") + _expect(no_fragment_in_cut, "no wall panel remains behind the window-sized opening") + _expect(document_changes[0] == 1, "one released wall opening emits one Stage document change") + controller.begin_wall_eraser(Vector2(19850.0, 19950.0)) + var cancel_press := InputEventMouseButton.new() + cancel_press.button_index = MOUSE_BUTTON_LEFT + cancel_press.pressed = true + controller.handle_mouse_button(cancel_press, Vector2(19850.0, 19950.0), false) + controller.handle_mouse_motion(Vector2(19950.0, 20050.0)) + controller.cancel_active_gesture() + _expect(_count_walls_with_origin(world, Vector2(20000.0, 20000.0)) == 4 and document_changes[0] == 1, "Cancel discards the unfinished eraser rectangle without changing earlier openings") + controller.exit_tool() + controller.begin_wall_eraser(Vector2(19850.0, 19950.0)) + controller.handle_mouse_button(cancel_press, Vector2(19850.0, 19950.0), false) + controller.handle_mouse_motion(Vector2(19950.0, 20050.0)) + var right_click := InputEventMouseButton.new() + right_click.button_index = MOUSE_BUTTON_RIGHT + right_click.pressed = true + controller.handle_mouse_button(right_click, Vector2(19950.0, 20050.0), false) + _expect(not controller.is_active() and _count_walls_with_origin(world, Vector2(20000.0, 20000.0)) == 4 and document_changes[0] == 1, "right-click cancels the eraser draft and exits without undoing earlier openings") + var serialized: Dictionary = (STAGE_SCENE_ADAPTER.new() as StageSceneAdapter).collect(world, {}, [], {"position": Vector2.ZERO, "zoom": 1.0}, {}) + var serialized_anchors: int = 0 + for entry_value: Variant in serialized.get("objects", []): + if entry_value is Dictionary and String((entry_value as Dictionary).get("type", "")) == "wall_panel" and (entry_value as Dictionary).has("motif_origin_offset"): + serialized_anchors += 1 + _expect(serialized_anchors == 4, "the current scene format records shared motif anchors for erased-wall fragments") + selection.clear_selection() + _stage.call("_refresh_scenery_capture_bar") + capture_bar = _stage.get("_use_target_capture_bar") as StageUseTargetCaptureBar + var unselected_erase_button: Button = capture_bar.find_child("Action_erase_wall_area", true, false) as Button + _expect(unselected_erase_button != null and unselected_erase_button.visible, "Edit Scenery exposes Erase wall area in the capture bar when nothing is selected") + if unselected_erase_button != null: + unselected_erase_button.pressed.emit() + _expect(controller.get_active_tool() == &"wall_eraser", "the always-visible Edit Scenery eraser action starts without requiring a wall selection") + controller.exit_tool() + _stage.call("_exit_scenery_edit_mode") + + +func _test_wall_eraser_full_cut(world: Node2D, controller: StageSceneryController) -> void: + if controller == null: + _expect(false, "wall eraser is available after scene reopen") + return + var start := Vector2(19790.0, 19840.0) + var finish := Vector2(20210.0, 20160.0) + controller.begin_wall_eraser(start) + var press := InputEventMouseButton.new() + press.button_index = MOUSE_BUTTON_LEFT + press.pressed = true + controller.handle_mouse_button(press, start, false) + var release := InputEventMouseButton.new() + release.button_index = MOUSE_BUTTON_LEFT + release.pressed = false + controller.handle_mouse_button(release, finish, false) + _expect(_count_walls_with_origin(world, Vector2(20000.0, 20000.0)) == 0, "erasing across the full wall removes all remaining fragments") + controller.exit_tool() + _stage.call("_exit_scenery_edit_mode") + + +func _wall_fragments_with_origin(world: Node2D, origin: Vector2) -> Array[StageWallPanel]: + var fragments: Array[StageWallPanel] = [] + for child: Node in world.get_children(): + if child.get_script() != STAGE_WALL_PANEL: + continue + var wall: StageWallPanel = child as StageWallPanel + if wall.get_effective_motif_origin_world().is_equal_approx(origin): + fragments.append(wall) + return fragments + + +func _count_walls_with_origin(world: Node2D, origin: Vector2) -> int: + return _wall_fragments_with_origin(world, origin).size() + + +func _last_node_with_script(parent: Node, script: Script) -> Node2D: + for index: int in range(parent.get_child_count() - 1, -1, -1): + var candidate: Node = parent.get_child(index) + if candidate.get_script() == script: + return candidate as Node2D + return null + + +func _is_grid_aligned(position: Vector2, grid_size: float) -> bool: + var snapped: Vector2 = Vector2(roundf(position.x / grid_size) * grid_size, roundf(position.y / grid_size) * grid_size) + return position.is_equal_approx(snapped) + + +func _test_group_preview_save_gate(world: Node2D) -> void: + var members: Array[Node2D] = [] + for child: Node in world.get_children(): + if child.get_script() == STAGE_SCENERY_ARTWORK: + members.append(child as Node2D) + if members.size() == 2: + break + _expect(members.size() == 2, "group-save fixture has two independent Scenery pieces") + if members.size() < 2: + return + var stage_selection: StageSelection = _stage.get("_selection") as StageSelection + var controller: StageSceneryController = _stage.get("_scenery_controller") as StageSceneryController + var pending_restore_frames: int = int(_stage.get("_restore_frames_left")) + _expect(pending_restore_frames > 0, "post-PLAY authored restore is pending before the group edit") + _stage.call("_enter_scenery_edit_mode") + stage_selection.select_only(members[0]) + stage_selection.add_to_selection(members[1]) + _stage.call("_begin_scenery_group_transform", &"move") + _expect(controller.is_group_preview_pending(), "multi-selection starts a live group preview") + _expect(int(_stage.get("_restore_frames_left")) == 0, "starting the group edit protects it from deferred post-PLAY restoration") + var preview_delta: Vector2 = Vector2(36.0, 21.0) + _expect(controller.preview_group(preview_delta, 1.0), "group preview accepts a valid move") + var expected_positions: Array[Vector2] = [members[0].global_position, members[1].global_position] + var path: String = "res://tests/phase19_pending_group_tmp.scene" + var auto_path: String = "res://tests/phase19_pending_group_autosave_tmp.scene" + var manual_saved: bool = _stage.call("_save_scene_to_path", path, false) + _expect(not manual_saved, "manual save is blocked while a group preview is pending") + var save_gate: ConfirmationDialog = _stage.get("_group_preview_save_dialog") as ConfirmationDialog + _expect(save_gate.visible, "manual save asks whether to keep or cancel the scenery preview") + _expect(not FileAccess.file_exists(path), "pending preview is not written before the user chooses") + var autosave_result: bool = _stage.call("_save_scene_to_path", auto_path, true) + _expect(not autosave_result and not FileAccess.file_exists(auto_path), "autosave also skips uncommitted preview values") + if save_gate.visible: + await _activate_button(save_gate.get_cancel_button()) + _expect(controller.is_group_preview_pending() and not FileAccess.file_exists(path), "Cancel save leaves the preview pending and writes no scene") + _expect(members[0].global_position.is_equal_approx(expected_positions[0]) and members[1].global_position.is_equal_approx(expected_positions[1]), "Cancel save keeps the visible group preview unchanged") + manual_saved = _stage.call("_save_scene_to_path", path, false) + _expect(not manual_saved and save_gate.visible, "a later save request asks again after Cancel") + if save_gate.visible: + await _activate_button(save_gate.get_ok_button()) + await process_frame + _expect(not controller.is_group_preview_pending(), "Keep changes commits the group before saving") + _expect(FileAccess.file_exists(path), "confirmed group preview is saved") + if FileAccess.file_exists(path): + var saved_read: Dictionary = STAGE_SCENE_IO.read_file(path) + var saved_data: Dictionary = saved_read.get("data", {}) as Dictionary + var saved_objects: Array = saved_data.get("objects", []) as Array + var serialized_positions: Array[Vector2] = [] + for raw_object: Variant in saved_objects: + if not raw_object is Dictionary: + continue + var object_data: Dictionary = raw_object as Dictionary + if String(object_data.get("type", "")) == "scenery_artwork": + serialized_positions.append(STAGE_SCENE_IO.data_to_vector(object_data.get("position"), Vector2.ZERO)) + _expect(_contains_positions(expected_positions, serialized_positions), "saved document contains both committed group preview positions") + var loaded: bool = _stage.call("_load_scene_from_path", path, false) + _expect(loaded, "group-transformed scene reopens") + var reopened_positions: Array[Vector2] = [] + for child: Node in world.get_children(): + if child.get_script() == STAGE_SCENERY_ARTWORK: + reopened_positions.append((child as Node2D).global_position) + var positions_restored: bool = _contains_positions(expected_positions, reopened_positions) + _expect(positions_restored, "saved group member transforms survive scene reopen") + DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) + if FileAccess.file_exists(auto_path): + DirAccess.remove_absolute(ProjectSettings.globalize_path(auto_path)) + _stage.call("_exit_scenery_edit_mode") + + +func _test_missing_artwork_repair(world: Node2D, browser: StageAssetBrowser) -> void: + var path: String = "res://tests/phase19_missing_artwork_tmp.scene" + var missing_path: String = "res://content/props/phase19_picture_not_here.prop" + var marker := STAGE_SCENERY_ARTWORK.new() as StageSceneryArtwork + marker.name = "MissingTree" + world.add_child(marker) + marker.global_position = Vector2(142.0, 213.0) + marker.rotation = 0.24 + marker.scale = Vector2(1.25, 0.82) + marker.configure_missing(missing_path, "My missing tree", Rect2(Vector2(-44.0, -51.0), Vector2(88.0, 102.0)), &"far") + _stage.call("_mark_scene_dirty") + var saved_missing: bool = _stage.call("_save_scene_to_path", path, false) + _expect(saved_missing, "scene with a missing artwork reference can be saved") + var loaded_missing: bool = _stage.call("_load_scene_from_path", path, false) + _expect(loaded_missing, "scene with a missing artwork reference reopens") + var summary: ConfirmationDialog = _stage.get("_missing_scenery_dialog") as ConfirmationDialog + _expect(summary.visible and summary.title == "Artwork not found", "missing-art load summary offers kid-readable recovery") + if summary.visible: + _expect(summary.ok_button_text == "Show Scenery" and summary.cancel_button_text == "Later", "missing-art summary exposes Show Scenery and Later") + await _activate_button(summary.get_cancel_button()) + var missing_target: StageSceneryArtwork = _find_missing_artwork(world) + _expect(missing_target != null, "missing art remains as a selectable placement marker") + if missing_target == null: + return + var saved_position: Vector2 = missing_target.global_position + var saved_rotation: float = missing_target.global_rotation + var saved_scale: Vector2 = missing_target.scale + var saved_depth: StringName = missing_target.depth + var dirty_before_repair: bool = bool(_stage.get("_dirty")) + _stage.call("_enter_scenery_edit_mode") + (_stage.get("_selection") as StageSelection).select_only(missing_target) + var repair_card: StageInspectorPanel = _stage.get("_scenery_inspector") as StageInspectorPanel + _expect(repair_card.visible, "selecting missing art in Edit Scenery opens the shared repair card") + await process_frame + await process_frame + var capture_bar: StageUseTargetCaptureBar = _stage.get("_use_target_capture_bar") as StageUseTargetCaptureBar + for _refresh_index: int in 3: + _stage.call("_refresh_scenery_capture_bar") + await process_frame + var action_buttons: Array[Node] = capture_bar.find_children("Action_*", "Button", true, false) + _expect(action_buttons.size() == 4, "repeated selection refresh keeps one capture button per action") + var capture_rect: Rect2 = capture_bar.get_global_rect() + var repair_rect: Rect2 = repair_card.get_global_rect() + _expect(capture_bar.visible and capture_rect.end.x <= repair_rect.position.x, "1280x720 scenery capture bar reserves a clear lane beside the repair inspector (bar=%s inspector=%s viewport=%s)" % [capture_rect, repair_rect, get_root().size]) + root.size = Vector2i(800, 600) + _stage.call("_refresh_scenery_inspector_layout") + await process_frame + await process_frame + capture_rect = capture_bar.get_global_rect() + repair_rect = repair_card.get_global_rect() + _expect(capture_bar.visible and repair_card.visible and capture_rect.end.x <= repair_rect.position.x, "narrow viewport keeps the stacked capture actions separate from the repair inspector") + root.size = Vector2i(1280, 720) + _stage.call("_refresh_scenery_inspector_layout") + await process_frame + await process_frame + var find_button: Button = _stage.get("_scenery_find_button") as Button + await _activate_button(find_button) + var browse_dialog: FileDialog = browser.get_browse_dialog() + _expect(browse_dialog.visible and browser.selector_kind == "scenery" and browse_dialog.filters.has("*.prop ; Prop Files"), "Find artwork opens the established .prop chooser") + var choose_button: Button = _stage.get("_scenery_choose_button") as Button + var entries: Array[Dictionary] = browser.scan_entries("scenery") + _expect(not entries.is_empty(), "repair chooser has valid artwork choices") + if entries.is_empty(): + return + if browse_dialog.visible: + browse_dialog.file_selected.emit(String(entries[0].get("path", ""))) + if browse_dialog.visible: + browse_dialog.hide() + await process_frame + await process_frame + var confirm_dialog: ConfirmationDialog = _stage.get("_scenery_repair_confirm_dialog") as ConfirmationDialog + _expect(confirm_dialog.visible and confirm_dialog.title == "Use this artwork here?", "candidate previews at the saved spot and requires explicit confirmation") + var candidate: StageSceneryArtwork = _stage.get("_scenery_repair_candidate") as StageSceneryArtwork + _expect(candidate != null and candidate.global_position.is_equal_approx(saved_position) and candidate.global_rotation == saved_rotation and candidate.scale.is_equal_approx(saved_scale), "repair candidate ghost matches the saved transform") + _expect(missing_target.missing and missing_target.asset_path == missing_path and bool(_stage.get("_dirty")) == dirty_before_repair, "preview has not changed the marker or dirty state") + if confirm_dialog.visible: + await _activate_button(confirm_dialog.get_cancel_button()) + _expect(missing_target.missing and missing_target.asset_path == missing_path and bool(_stage.get("_dirty")) == dirty_before_repair, "Keep for now cancels repair without altering the scene") + await _activate_button(choose_button) + var selector: AssetSelector = browser.get_selector() + _expect(selector.visible and selector.kind == "scenery", "Choose another artwork opens the shared Artwork chooser") + var gallery_grid: GridContainer = selector.find_child("GridContainer", true, false) as GridContainer + var first_card: AssetCard = gallery_grid.get_child(0) as AssetCard if gallery_grid != null and gallery_grid.get_child_count() > 0 else null + _expect(first_card != null, "shared Artwork chooser displays a selectable drawing card") + if first_card != null: + await _activate_card(first_card) + await process_frame + await process_frame + _expect(confirm_dialog.visible, "gallery artwork selection opens the same explicit repair confirmation") + if confirm_dialog.visible: + await _activate_button(confirm_dialog.get_ok_button()) + _expect(not missing_target.missing and missing_target.asset_path == String(entries[0].get("path", "")), "Use artwork replaces only the selected missing reference") + _expect(missing_target.global_position.is_equal_approx(saved_position) and is_equal_approx(missing_target.global_rotation, saved_rotation) and missing_target.scale.is_equal_approx(saved_scale) and missing_target.depth == saved_depth, "repair preserves saved placement, size, orientation, and depth") + _expect(bool(_stage.get("_dirty")), "confirmed artwork repair marks the scene dirty") + var saved_repair: bool = _stage.call("_save_scene_to_path", path, false) + var reopened_repair: bool = _stage.call("_load_scene_from_path", path, false) if saved_repair else false + var repaired_after_reopen: StageSceneryArtwork = _find_artwork(world, String(entries[0].get("path", ""))) + _expect(reopened_repair and repaired_after_reopen != null and not repaired_after_reopen.missing, "repaired live artwork path persists after save and reopen") + _stage.call("_exit_scenery_edit_mode") + DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) + + +func _test_single_scenery_transforms(world: Node2D, artwork_path: String) -> void: + var artwork: StageSceneryArtwork = STAGE_SCENERY_ARTWORK.new() as StageSceneryArtwork + _expect(artwork.load_visual(artwork_path), "single-transform fixture loads production scenery artwork") + if artwork.missing: + artwork.free() + return + artwork.display_name = "Phase 19 Transform Fixture" + artwork.set_depth(&"near") + world.add_child(artwork) + artwork.global_position = Vector2(2400.0, 1600.0) + artwork.global_rotation = 0.19 + artwork.scale = Vector2(1.25, 0.82) + var selection: StageSelection = _stage.get("_selection") as StageSelection + var controller: StageSceneryController = _stage.get("_scenery_controller") as StageSceneryController + var document_changes: Array[int] = [0] + controller.document_change_requested.connect(func() -> void: document_changes[0] += 1) + _stage.call("_enter_scenery_edit_mode") + selection.select_only(artwork) + _stage.call("_on_grid_size_changed", 20.0) + _stage.call("_on_snap_toggled", true) + var original_scale: Vector2 = artwork.scale + var original_bounds: Rect2 = artwork.get_visual_world_bounds() + _stage.set("_dirty", false) + var press: InputEventMouseButton = InputEventMouseButton.new() + press.button_index = MOUSE_BUTTON_LEFT + press.pressed = true + _stage.call("_handle_scenery_edit_click", press, original_bounds.get_center()) + await process_frame + _expect(controller.is_single_transform_active() and controller.get_single_transform_kind() == &"move", "single scenery body drag starts the production move transaction") + var capture_bar: StageUseTargetCaptureBar = _stage.get("_use_target_capture_bar") as StageUseTargetCaptureBar + var cancel_button: Button = capture_bar.find_child("Action_cancel_single", true, false) as Button + _expect(capture_bar.visible and cancel_button != null and cancel_button.text.contains("Cancel"), "active scenery drag exposes a visible Cancel action") + var scenery_inspector: StageInspectorPanel = _stage.get("_scenery_inspector") as StageInspectorPanel + _expect(capture_bar.get_global_rect().end.x <= scenery_inspector.get_global_rect().position.x, "active scenery Cancel bar stays in the clear lane beside the inspector") + _stage.call("_handle_scenery_edit_motion", original_bounds.get_center() + Vector2(37.0, 43.0)) + var moved_bounds: Rect2 = artwork.get_visual_world_bounds() + _expect(_is_grid_aligned(moved_bounds.position, 20.0), "snapped scenery move aligns the visible AABB top-left to the grid") + var release: InputEventMouseButton = InputEventMouseButton.new() + release.button_index = MOUSE_BUTTON_LEFT + release.pressed = false + _stage.call("_handle_scenery_edit_click", release, moved_bounds.get_center()) + _expect(not controller.is_single_transform_active() and bool(_stage.get("_dirty")), "mouse release commits one move and marks the Stage document dirty") + var committed_position: Vector2 = artwork.global_position + _stage.set("_dirty", false) + _stage.call("_handle_scenery_edit_click", press, moved_bounds.get_center()) + _stage.call("_handle_scenery_edit_motion", moved_bounds.get_center() + Vector2(63.0, 35.0)) + _expect(not artwork.global_position.is_equal_approx(committed_position), "second drag shows a live move preview before cancellation") + if cancel_button != null: + cancel_button.pressed.emit() + _expect(not controller.is_single_transform_active() and artwork.global_position.is_equal_approx(committed_position), "visible Cancel restores the exact committed transform") + _stage.call("_handle_scenery_edit_click", press, committed_position) + _expect(controller.is_single_transform_active(), "next canvas press after clicking Cancel is not swallowed by a stale release guard") + _stage.call("_handle_scenery_edit_motion", committed_position + Vector2(63.0, 35.0)) + var document_changes_after_cancel: int = document_changes[0] + var escape: InputEventKey = InputEventKey.new() + escape.keycode = KEY_ESCAPE + escape.pressed = true + _stage.call("_unhandled_key_input", escape) + _expect(not controller.is_single_transform_active() and artwork.global_position.is_equal_approx(committed_position), "Escape cancels the active single-piece transform") + _expect(document_changes[0] == document_changes_after_cancel, "Cancel and Escape leave the Stage document unchanged") + _stage.set("_dirty", false) + var resize_bounds: Rect2 = artwork.get_visual_world_bounds() + var resize_corner: Vector2 = resize_bounds.position + var opposite_corner: Vector2 = resize_bounds.end + _stage.call("_handle_scenery_edit_click", press, resize_corner) + _expect(controller.is_single_transform_active() and controller.get_single_transform_kind() == &"resize", "44-pixel scenery corner handle starts resize") + var resize_delta: Vector2 = (resize_corner - opposite_corner) * 0.25 + _stage.call("_handle_scenery_edit_motion", resize_corner + resize_delta) + _expect(artwork.scale.is_equal_approx(original_scale * 1.25), "corner resize applies a uniform factor while preserving non-uniform starting scale (actual %s expected %s)" % [str(artwork.scale), str(original_scale * 1.25)]) + var resized_bounds: Rect2 = artwork.get_visual_world_bounds() + _expect(resized_bounds.end.is_equal_approx(opposite_corner), "corner resize keeps the opposite visible AABB corner fixed (actual %s expected %s)" % [str(resized_bounds.end), str(opposite_corner)]) + document_changes[0] = 0 + release.position = resize_corner + resize_delta + _stage.call("_handle_scenery_edit_click", release, release.position) + _expect(not controller.is_single_transform_active() and bool(_stage.get("_dirty")), "resize commits on release without a separate Keep step") + _expect(document_changes[0] == 1, "resize release emits exactly one document change") + _stage.call("_handle_scenery_edit_click", release, release.position) + _expect(document_changes[0] == 1, "repeated resize release does not commit the same gesture twice") + var saved_scale: Vector2 = artwork.scale + var saved_position: Vector2 = artwork.global_position + var roundtrip_path: String = "res://tests/phase19_single_scenery_transform_tmp.scene" + var saved: bool = _stage.call("_save_scene_to_path", roundtrip_path, false) + var roundtrip_data: Dictionary = STAGE_SCENE_IO.read_file(roundtrip_path).get("data", {}) as Dictionary if saved else {} + _expect(saved and String(roundtrip_data.get("version", "")) == STAGE_SCENE_IO.FILE_VERSION, "single-piece transforms serialize in the current .scene version") + var loaded: bool = _stage.call("_load_scene_from_path", roundtrip_path, false) if saved else false + var restored: StageSceneryArtwork = _find_artwork_by_display_name(world, "Phase 19 Transform Fixture") + _expect(loaded and restored != null and restored.scale.is_equal_approx(saved_scale) and restored.global_position.is_equal_approx(saved_position), "single-piece size and placement survive save/reopen") + if FileAccess.file_exists(roundtrip_path): + DirAccess.remove_absolute(ProjectSettings.globalize_path(roundtrip_path)) + _stage.call("_enter_scenery_edit_mode") + var wall: StageWallPanel = STAGE_WALL_PANEL.new() as StageWallPanel + wall.configure(Vector2(160.0, 220.0), &"finish_wall_brick", Color.WHITE) + world.add_child(wall) + wall.global_position = Vector2(2800.0, 1600.0) + wall.scale = Vector2(1.4, 0.9) + selection.select_only(wall) + _stage.call("_refresh_scenery_inspector") + var size_row: HBoxContainer = _stage.get("_scenery_size_row") as HBoxContainer + var smaller_button: Button = _stage.get("_scenery_smaller_button") as Button + var bigger_button: Button = _stage.get("_scenery_bigger_button") as Button + _expect(size_row.visible and smaller_button.custom_minimum_size.y >= 44.0 and bigger_button.custom_minimum_size.y >= 44.0, "selected wall inspector uses shared, touch-sized Smaller and Bigger actions") + var wall_bounds: Rect2 = wall.global_transform * wall.get_local_bounds() + _stage.call("_step_selected_scenery_size", 1.1) + _expect(wall.panel_size.is_equal_approx(Vector2(176.0, 242.0)), "wall size step updates panel dimensions") + _expect(wall.scale.is_equal_approx(Vector2(1.4, 0.9)), "wall size step preserves stage-space motif pitch by leaving node scale unchanged") + _expect((wall.global_transform * wall.get_local_bounds()).position.is_equal_approx(wall_bounds.position), "wall size step pins visible top-left while resizing") + var unknown_marker: StageSceneryArtwork = STAGE_SCENERY_ARTWORK.new() as StageSceneryArtwork + unknown_marker.configure_missing("res://content/props/phase19_unknown_footprint.prop", "Unknown footprint", Rect2(), &"near") + world.add_child(unknown_marker) + unknown_marker.global_position = Vector2(2900.0, 1600.0) + selection.select_only(unknown_marker) + _stage.call("_refresh_scenery_inspector") + _expect(not size_row.visible and smaller_button.disabled and bigger_button.disabled, "unknown-footprint missing artwork hides and disables misleading size controls") + _expect(not controller.begin_single_transform(unknown_marker, &"resize", unknown_marker.global_position), "unknown-footprint missing artwork cannot start a corner resize") + _expect(not controller.step_single_size(unknown_marker, 1.1), "unknown-footprint missing artwork cannot use Smaller or Bigger size steps") + _stage.call("_exit_scenery_edit_mode") + _stage.call("_on_snap_toggled", false) + unknown_marker.free() + wall.free() + + +func _test_selected_scenery_depth(world: Node2D, artwork_path: String) -> void: + var artwork: StageSceneryArtwork = STAGE_SCENERY_ARTWORK.new() as StageSceneryArtwork + _expect(artwork.load_visual(artwork_path), "depth-inspector fixture loads production scenery artwork") + if artwork.missing: + artwork.free() + return + artwork.display_name = "Phase 19 Depth Inspector Fixture" + artwork.set_depth(&"near") + world.add_child(artwork) + artwork.global_position = Vector2(4600.0, 1800.0) + var selection: StageSelection = _stage.get("_selection") as StageSelection + _stage.call("_enter_scenery_edit_mode") + selection.select_only(artwork) + await process_frame + await process_frame + var inspector: StageInspectorPanel = _stage.get("_scenery_inspector") as StageInspectorPanel + var section: VBoxContainer = _stage.get("_scenery_depth_section") as VBoxContainer + var choices: StagePanelChoiceGroup = _stage.get("_scenery_depth_group") as StagePanelChoiceGroup + var far_button: Button = choices.find_child("Choice_far", true, false) as Button if choices != null else null + var near_button: Button = choices.find_child("Choice_near", true, false) as Button if choices != null else null + var floor_button: Button = choices.find_child("Choice_floor", true, false) as Button if choices != null else null + _expect(inspector.visible and section != null and section.visible, "one selected artwork shows the Where it appears section") + _expect(far_button != null and near_button != null and floor_button != null, "depth inspector offers Far, Near, and On the floor choices") + if far_button == null or near_button == null or floor_button == null: + _stage.call("_exit_scenery_edit_mode") + return + _expect(far_button.custom_minimum_size.y >= 44.0 and near_button.custom_minimum_size.y >= 44.0 and floor_button.custom_minimum_size.y >= 44.0, "depth choices meet the shared 44-pixel touch target minimum") + _expect(near_button.button_pressed and not far_button.button_pressed and not floor_button.button_pressed, "inspector silently highlights the selected artwork's saved depth") + var original_position: Vector2 = artwork.global_position + var original_scale: Vector2 = artwork.scale + _stage.set("_dirty", false) + await _send_button_key(near_button, KEY_SPACE) + _expect(artwork.depth == &"near" and not bool(_stage.get("_dirty")), "choosing the current depth is a real UI no-op and leaves the Stage clean") + var scroll_fixture: Control = Control.new() + scroll_fixture.name = "DepthInspectorScrollFixture" + scroll_fixture.custom_minimum_size = Vector2(1.0, 360.0) + inspector.body().add_child(scroll_fixture) + await process_frame + await process_frame + inspector.request_fit() + await process_frame + await process_frame + var scroll: ScrollContainer = inspector.get("_scroll") as ScrollContainer + var scrollbar: VScrollBar = scroll.get_v_scroll_bar() as VScrollBar if scroll != null else null + var scroll_limit: int = maxi(0, int(floor(scrollbar.max_value - scrollbar.page))) if scrollbar != null else 0 + _expect(scroll_limit > 0, "overflowing artwork inspector body scrolls independently of its fixed header") + if scroll != null and scroll_limit > 0: + scroll.scroll_vertical = mini(40, scroll_limit) + await process_frame + var scroll_before_change: int = scroll.scroll_vertical if scroll != null else 0 + var header: Control = inspector.get("_header") as Control + var header_position_before_change: Vector2 = header.global_position if header != null else Vector2.ZERO + _stage.set("_dirty", false) + await _send_button_key(far_button, KEY_SPACE) + _expect(artwork.depth == &"far" and artwork.z_index == -30, "focused keyboard input immediately changes the selected artwork's live Far draw layer") + _expect(bool(_stage.get("_dirty")), "changing artwork depth marks the Stage dirty") + _expect(artwork.global_position.is_equal_approx(original_position) and artwork.scale.is_equal_approx(original_scale), "changing depth leaves artwork position and size unchanged") + _expect(inspector.visible and section.visible and far_button.button_pressed, "depth change keeps the inspector open and highlights the new choice") + _expect(scroll == null or scroll.scroll_vertical == scroll_before_change, "depth refresh preserves the inspector body's scroll position") + _expect(header == null or header.global_position.is_equal_approx(header_position_before_change), "depth refresh keeps the inspector header fixed while its body is scrolled") + _stage.set("_dirty", false) + await _send_button_key(far_button, KEY_SPACE) + _expect(artwork.depth == &"far" and not bool(_stage.get("_dirty")), "repeating the selected depth through real keyboard input does not dirty the Stage") + if scroll_fixture.is_inside_tree(): + scroll_fixture.queue_free() + _stage.call("_refresh_scenery_inspector_layout") + await process_frame + var roundtrip_path: String = "res://tests/phase19_selected_depth_tmp.scene" + var saved: bool = _stage.call("_save_scene_to_path", roundtrip_path, false) + var loaded: bool = _stage.call("_load_scene_from_path", roundtrip_path, false) if saved else false + var restored: StageSceneryArtwork = _find_artwork_by_display_name(world, "Phase 19 Depth Inspector Fixture") + _expect(saved and loaded and restored != null and restored.depth == &"far", "selected artwork depth persists through production scene save and reopen") + if restored != null: + _stage.call("_enter_scenery_edit_mode") + selection.select_only(restored) + await process_frame + choices = _stage.get("_scenery_depth_group") as StagePanelChoiceGroup + far_button = choices.find_child("Choice_far", true, false) as Button if choices != null else null + _expect(far_button != null and far_button.button_pressed, "reopened artwork inspector highlights the saved depth") + _stage.set("_dirty", false) + var missing_marker: StageSceneryArtwork = STAGE_SCENERY_ARTWORK.new() as StageSceneryArtwork + missing_marker.configure_missing("res://content/props/phase19_depth_marker_missing.prop", "Phase 19 Depth Marker", Rect2(Vector2(-44.0, -51.0), Vector2(88.0, 102.0)), &"near") + world.add_child(missing_marker) + missing_marker.global_position = Vector2(5000.0, 1800.0) + selection.select_only(missing_marker) + await process_frame + choices = _stage.get("_scenery_depth_group") as StagePanelChoiceGroup + near_button = choices.find_child("Choice_near", true, false) as Button if choices != null else null + floor_button = choices.find_child("Choice_floor", true, false) as Button if choices != null else null + _expect(missing_marker.missing and section.visible and near_button != null and near_button.button_pressed, "missing-art marker exposes and highlights its saved depth before repair") + _stage.set("_dirty", false) + if floor_button != null: + await _send_button_key(floor_button, KEY_SPACE) + _expect(missing_marker.depth == &"floor" and missing_marker.z_index == -5, "keyboard input changes a missing-art marker's live draw layer") + _expect(bool(_stage.get("_dirty")), "changing missing-art depth marks the Stage dirty") + saved = _stage.call("_save_scene_to_path", roundtrip_path, false) + loaded = _stage.call("_load_scene_from_path", roundtrip_path, false) if saved else false + var restored_marker: StageSceneryArtwork = _find_artwork_by_display_name(world, "Phase 19 Depth Marker") + _expect(saved and loaded and restored_marker != null and restored_marker.missing and restored_marker.depth == &"floor", "missing-art marker keeps its edited depth after scene save and reopen") + _stage.call("_enter_scenery_edit_mode") + var wall: StageWallPanel = STAGE_WALL_PANEL.new() as StageWallPanel + wall.configure(Vector2(180.0, 220.0), &"finish_wall_brick", Color.WHITE) + world.add_child(wall) + selection.select_only(wall) + await process_frame + section = _stage.get("_scenery_depth_section") as VBoxContainer + _expect(not section.visible, "wall panels do not show artwork depth choices") + var other_artwork: StageSceneryArtwork = STAGE_SCENERY_ARTWORK.new() as StageSceneryArtwork + _expect(other_artwork.load_visual(artwork_path), "multi-selection guard fixture loads production scenery artwork") + world.add_child(other_artwork) + if restored_marker != null: + selection.select_only(restored_marker) + selection.add_to_selection(other_artwork) + await process_frame + _expect(not section.visible and not inspector.visible, "multiple selected scenery pieces do not offer a single-artwork depth choice") + selection.clear_selection() + await process_frame + _expect(not section.visible and not inspector.visible, "no selection hides the artwork depth section") + wall.free() + other_artwork.free() + _stage.call("_exit_scenery_edit_mode") + if FileAccess.file_exists(roundtrip_path): + DirAccess.remove_absolute(ProjectSettings.globalize_path(roundtrip_path)) + + +func _test_prop_size_for_stage() -> void: + var editor: PropEditor = PROP_EDITOR_SCENE.instantiate() as PropEditor + root.add_child(editor) + await process_frame + await process_frame + var canvas: PropCanvas = editor.get("_canvas") as PropCanvas + var original_shape: Dictionary = { + "shape_type": "polygon", + "points": PackedVector2Array([Vector2(-50.0, -200.0), Vector2(50.0, -200.0), Vector2(50.0, 0.0), Vector2(-50.0, 0.0)]), + "fill_color": "#9D6A4A", + "outline_color": "#2D2532", + "outline_width": 4.0, + "closed": true, + } + canvas.set_shape_data([original_shape]) + var original_shapes: Array[Dictionary] = canvas.get_shape_data() + var original_anchors: Dictionary = (editor.get("_anchors") as Dictionary).duplicate(true) + editor.set("_dirty", false) + editor.call("_on_size_for_stage_pressed") + var size_controls: FlowContainer = editor.get("_stage_size_controls") as FlowContainer + _expect(canvas.is_stage_size_preview_active() and size_controls.visible, "Size for Stage starts a separate preview transaction and reveals its controls") + var required_labels: Array[String] = ["Small", "Room", "Big", "Make smaller", "Make bigger", "Keep size", "✕ Cancel"] + for required_label: String in required_labels: + var button: Button = _find_button_by_text(size_controls, required_label) + _expect(button != null and button.custom_minimum_size.y >= 44.0, "Size for Stage exposes touch-sized %s control" % required_label) + for _frame: int in 4: + await process_frame + var entry_bounds: Rect2 = canvas.get_stage_size_comparison_bounds() + var entry_zoom: float = float(canvas.get("_zoom")) + var entry_pan: Vector2 = canvas.get("_pan_offset") + var entry_screen_bounds: Rect2 = Rect2(entry_bounds.position * entry_zoom + entry_pan, entry_bounds.size * entry_zoom) + var entry_viewport_size: Vector2 = Vector2(maxf(0.0, canvas.size.x - 96.0), maxf(0.0, canvas.size.y - 96.0)) + var entry_viewport: Rect2 = Rect2(Vector2(48.0, 48.0), entry_viewport_size) + var size_tools: FlowContainer = editor.get("_size_tools") as FlowContainer + _expect(size_tools.size.y < 160.0 and canvas.size.y > 96.0, "Size for Stage controls preserve a visible canvas instead of expanding the toolbar vertically") + _expect(entry_viewport.encloses(entry_screen_bounds), "Size for Stage entry fits artwork and the stickman guide after the editor layout settles") + editor.call("_choose_stage_size", 1.0) + var room_extent: float = maxf(canvas.get_visible_shapes_bounds().size.x, canvas.get_visible_shapes_bounds().size.y) + _expect(is_equal_approx(room_extent, 939.5), "Room preset targets the 939.5-unit standing-stickman height") + _expect(not bool(editor.get("_dirty")), "size preview leaves the prop draft and autosave state untouched") + editor.call("_adjust_stage_size", 1.1) + var bigger_extent: float = maxf(canvas.get_visible_shapes_bounds().size.x, canvas.get_visible_shapes_bounds().size.y) + _expect(is_equal_approx(bigger_extent, room_extent * 1.1), "Make bigger applies one 1.1 step") + editor.call("_adjust_stage_size", 1.0 / 1.1) + _expect(is_equal_approx(maxf(canvas.get_visible_shapes_bounds().size.x, canvas.get_visible_shapes_bounds().size.y), room_extent), "Make smaller applies the reciprocal 1.1 step") + var comparison_bounds: Rect2 = canvas.get_stage_size_comparison_bounds() + _expect(comparison_bounds.position.y < canvas.get_visible_shapes_bounds().position.y and comparison_bounds.end.y >= canvas.get_visible_shapes_bounds().end.y, "preview fit bounds include the foreground stickman guide and shared floor line") + editor.call("_finish_stage_size_preview", false) + _expect(not canvas.is_stage_size_preview_active() and canvas.get_shape_data() == original_shapes, "Cancel closes the preview and restores the original drawing exactly") + _expect((editor.get("_anchors") as Dictionary) == original_anchors and not bool(editor.get("_dirty")), "Cancel restores anchors and leaves the document clean") + editor.call("_on_size_for_stage_pressed") + editor.call("_choose_stage_size", 0.35) + var small_extent: float = maxf(canvas.get_visible_shapes_bounds().size.x, canvas.get_visible_shapes_bounds().size.y) + _expect(is_equal_approx(small_extent, 328.825), "Small preset targets 0.35 of the standing-stickman height") + editor.call("_finish_stage_size_preview", true) + var committed_extent: float = maxf(canvas.get_visible_shapes_bounds().size.x, canvas.get_visible_shapes_bounds().size.y) + _expect(not canvas.is_stage_size_preview_active() and is_equal_approx(committed_extent, small_extent), "Keep commits the chosen overall document scale") + _expect(bool(editor.get("_dirty")), "keeping a size change enters the existing dirty/save lifecycle") + editor.queue_free() + await process_frame + + +func _find_button_by_text(parent: Node, text: String) -> Button: + if parent == null: + return null + for candidate: Node in parent.find_children("*", "Button", true, false): + var button: Button = candidate as Button + if button != null and button.text == text: + return button + return null + + +func _find_missing_artwork(world: Node2D) -> StageSceneryArtwork: + for child: Node in world.get_children(): + if child.get_script() == STAGE_SCENERY_ARTWORK and (child as StageSceneryArtwork).missing: + return child as StageSceneryArtwork + return null + + +func _contains_positions(expected: Array[Vector2], actual: Array[Vector2]) -> bool: + var unmatched: Array[Vector2] = actual.duplicate() + for expected_position: Vector2 in expected: + var match_index: int = -1 + for index: int in range(unmatched.size()): + if unmatched[index].is_equal_approx(expected_position): + match_index = index + break + if match_index < 0: + return false + unmatched.remove_at(match_index) + return true + + +func _find_artwork(world: Node2D, asset_path: String) -> StageSceneryArtwork: + for child: Node in world.get_children(): + if child.get_script() == STAGE_SCENERY_ARTWORK and (child as StageSceneryArtwork).asset_path == asset_path: + return child as StageSceneryArtwork + return null + + +func _find_artwork_by_display_name(world: Node2D, display_name: String) -> StageSceneryArtwork: + for child: Node in world.get_children(): + if child.get_script() == STAGE_SCENERY_ARTWORK and (child as StageSceneryArtwork).display_name == display_name: + return child as StageSceneryArtwork + return null + + +func _find_card_by_id(grid: GridContainer, card_id: String) -> AssetCard: + if grid == null: + return null + for child: Node in grid.get_children(): + var card: AssetCard = child as AssetCard + if card != null and String(card.payload().get("placement_id", "")) == card_id: + return card + return null + + +func _send_stage_click(position: Vector2, button: int) -> void: + var press := InputEventMouseButton.new() + press.button_index = button + press.position = position + press.global_position = position + press.pressed = true + _stage.get_viewport().push_input(press) + await process_frame + var release := InputEventMouseButton.new() + release.button_index = button + release.position = position + release.global_position = position + release.pressed = false + _stage.get_viewport().push_input(release) + await process_frame + + +func _send_button_key(button: Button, keycode: Key) -> void: + button.grab_focus() + await process_frame + var viewport: Viewport = button.get_viewport() + var press: InputEventKey = InputEventKey.new() + press.keycode = keycode + press.pressed = true + viewport.push_input(press) + await process_frame + var release: InputEventKey = InputEventKey.new() + release.keycode = keycode + release.pressed = false + viewport.push_input(release) + await process_frame + + +func _activate_button(button: Button) -> void: + button.pressed.emit() + await process_frame + + +func _activate_card(card: AssetCard) -> void: + card.activated.emit(card.payload()) + await process_frame diff --git a/tests/test_phase19_scenery.gd.uid b/tests/test_phase19_scenery.gd.uid new file mode 100644 index 0000000..1f7377f --- /dev/null +++ b/tests/test_phase19_scenery.gd.uid @@ -0,0 +1 @@ +uid://crpxum1tvnphv diff --git a/tests/test_phase7_palette.gd b/tests/test_phase7_palette.gd index 4adda26..6ba5174 100644 --- a/tests/test_phase7_palette.gd +++ b/tests/test_phase7_palette.gd @@ -10,7 +10,7 @@ func _init() -> void: var world: Node2D = Node2D.new() var spawner: StageSpawner = SPAWNER.new(world) as StageSpawner var entries: Array[Dictionary] = spawner.get_palette_entries() - _check(entries.size() == 6, "six registry cards") + _check(entries.size() == 7, "six established cards plus the Phase 19 Scenery card") _check(String(entries[0].get("placement_id", "")) == "ground", "registry id retained") var card: AssetCard = CARD.new() as AssetCard card.configure(entries[0]) @@ -20,7 +20,7 @@ func _init() -> void: chrome.palette_activated.connect(_on_palette_activated) chrome.build(entries) var palette_buttons: Dictionary = chrome.get_palette_buttons() - _check(palette_buttons.size() == 6, "six stage palette cards") + _check(palette_buttons.size() == 7, "seven stage palette cards") for entry: Dictionary in entries: var placement_id: String = String(entry.get("placement_id", "")) var asset_card: AssetCard = palette_buttons.get(placement_id) as AssetCard @@ -28,6 +28,15 @@ func _init() -> void: if asset_card == null: continue var icon: TextureRect = asset_card.thumbnail_rect() + var card_box: VBoxContainer = asset_card.get_child(0) as VBoxContainer + var glyph: Label = card_box.get_child(1) as Label + var title: Label = card_box.get_child(2) as Label + if placement_id == "scenery": + _check(not icon.visible and icon.texture == null, "Scenery retains its themed 🌄 fallback without a dedicated image asset") + _check(glyph.visible and glyph.text == "🌄", "Scenery fallback is kid-readable") + _check(title.text == String(entry.get("label", "")), "Scenery text label remains") + _check(asset_card.tooltip_text == String(entry.get("label", "")), "Scenery tooltip remains") + continue _check(icon.visible, "%s image is visible" % placement_id) var icon_texture: Texture2D = icon.texture as Texture2D _check(icon_texture != null, "%s image texture is assigned" % placement_id) @@ -37,9 +46,6 @@ func _init() -> void: _check(icon_texture.get_width() <= 256 and icon_texture.get_height() <= 256, "%s import is capped at 256 px" % placement_id) _check(icon.custom_minimum_size == Vector2(64.0, 64.0), "%s image area stays 64 px" % placement_id) _check(icon.stretch_mode == TextureRect.STRETCH_KEEP_ASPECT_CENTERED, "%s image keeps aspect ratio" % placement_id) - var card_box: VBoxContainer = asset_card.get_child(0) as VBoxContainer - var glyph: Label = card_box.get_child(1) as Label - var title: Label = card_box.get_child(2) as Label _check(not glyph.visible, "%s emoji fallback is hidden" % placement_id) _check(title.text == String(entry.get("label", "")), "%s text label remains" % placement_id) _check(asset_card.tooltip_text == String(entry.get("label", "")), "%s tooltip remains" % placement_id)