valcore commited on
Commit
e306494
·
1 Parent(s): 163f9e5

feat: scaffold FloorPlan custom component, add Python backend

Browse files
floorplan/.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .eggs/
2
+ dist/
3
+ *.pyc
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+ __tmp/*
8
+ *.pyi
9
+ .mypycache
10
+ .ruff_cache
11
+ node_modules
12
+ backend/**/templates/
floorplan/README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # gradio_floorplan
3
+ A Custom Gradio component.
4
+
5
+ ## Example usage
6
+
7
+ ```python
8
+ import gradio as gr
9
+ from gradio_floorplan import FloorPlan
10
+ ```
floorplan/backend/gradio_floorplan/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+
2
+ from .floorplan import FloorPlan
3
+
4
+ __all__ = ['FloorPlan']
floorplan/backend/gradio_floorplan/floorplan.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import copy
3
+ from typing import TYPE_CHECKING
4
+ from gradio.components.base import Component
5
+
6
+ if TYPE_CHECKING:
7
+ from gradio.components import Timer
8
+
9
+
10
+ _DEFAULTS = {
11
+ "corners": [[50, 50], [550, 50], [550, 450], [50, 450]],
12
+ "furnitures": [
13
+ {
14
+ "object": "Sofa",
15
+ "localisation": [150, 100, 250, 300],
16
+ "description": "3-seat sofa",
17
+ },
18
+ {
19
+ "object": "Table",
20
+ "localisation": [300, 200, 380, 400],
21
+ "description": "Coffee table",
22
+ },
23
+ ],
24
+ }
25
+
26
+
27
+ class FloorPlan(Component):
28
+ """
29
+ A Gradio component that renders an interactive SVG floor plan.
30
+
31
+ Input/Output value shape:
32
+ {
33
+ "corners": [[x, y], ...],
34
+ "furnitures": [
35
+ {
36
+ "object": str,
37
+ "localisation": [ymin, xmin, ymax, xmax],
38
+ "description": str,
39
+ },
40
+ ...
41
+ ]
42
+ }
43
+ """
44
+
45
+ EVENTS = ["change"]
46
+
47
+ def __init__(
48
+ self,
49
+ value: dict | None = None,
50
+ *,
51
+ label: str | None = None,
52
+ info: str | None = None,
53
+ every: "Timer | float | None" = None,
54
+ show_label: bool | None = None,
55
+ container: bool = True,
56
+ scale: int | None = None,
57
+ min_width: int = 160,
58
+ interactive: bool | None = None,
59
+ visible: bool = True,
60
+ elem_id: str | None = None,
61
+ elem_classes: list[str] | str | None = None,
62
+ render: bool = True,
63
+ ):
64
+ super().__init__(
65
+ value=value,
66
+ label=label,
67
+ info=info,
68
+ every=every,
69
+ show_label=show_label,
70
+ container=container,
71
+ scale=scale,
72
+ min_width=min_width,
73
+ interactive=interactive,
74
+ visible=visible,
75
+ elem_id=elem_id,
76
+ elem_classes=elem_classes,
77
+ render=render,
78
+ )
79
+
80
+ def preprocess(self, payload: dict | None) -> dict | None:
81
+ """Pass the value dict through to the Python function unchanged."""
82
+ return payload
83
+
84
+ def postprocess(self, value: dict | None) -> dict | None:
85
+ """Pass the dict from Python through to the frontend unchanged."""
86
+ return value
87
+
88
+ def api_info(self) -> dict:
89
+ return {
90
+ "type": "object",
91
+ "properties": {
92
+ "corners": {
93
+ "type": "array",
94
+ "items": {
95
+ "type": "array",
96
+ "items": {"type": "integer"},
97
+ "minItems": 2,
98
+ "maxItems": 2,
99
+ },
100
+ },
101
+ "furnitures": {
102
+ "type": "array",
103
+ "items": {
104
+ "type": "object",
105
+ "properties": {
106
+ "object": {"type": "string"},
107
+ "localisation": {
108
+ "type": "array",
109
+ "items": {"type": "integer"},
110
+ "minItems": 4,
111
+ "maxItems": 4,
112
+ },
113
+ "description": {"type": "string"},
114
+ },
115
+ "required": ["object", "localisation", "description"],
116
+ },
117
+ },
118
+ },
119
+ "required": ["corners", "furnitures"],
120
+ }
121
+
122
+ def example_value(self) -> dict:
123
+ return copy.deepcopy(_DEFAULTS)
124
+
125
+ def example_payload(self) -> dict:
126
+ return copy.deepcopy(_DEFAULTS)
floorplan/backend/tests/__init__.py ADDED
File without changes
floorplan/backend/tests/test_floorplan.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ from gradio_floorplan import FloorPlan
3
+
4
+
5
+ SAMPLE_CORNERS = [[50, 50], [550, 50], [550, 450], [50, 450]]
6
+ SAMPLE_FURNITURES = [
7
+ {"object": "Sofa", "localisation": [150, 100, 250, 300], "description": "3-seat sofa"},
8
+ {"object": "Table", "localisation": [300, 200, 380, 400], "description": "Coffee table"},
9
+ ]
10
+ SAMPLE_VALUE = {"corners": SAMPLE_CORNERS, "furnitures": SAMPLE_FURNITURES}
11
+
12
+
13
+ def test_postprocess_returns_dict():
14
+ comp = FloorPlan()
15
+ result = comp.postprocess(SAMPLE_VALUE)
16
+ assert result is not None
17
+ assert result["corners"] == SAMPLE_CORNERS
18
+ assert len(result["furnitures"]) == 2
19
+
20
+
21
+ def test_postprocess_none():
22
+ comp = FloorPlan()
23
+ assert comp.postprocess(None) is None
24
+
25
+
26
+ def test_preprocess_passes_through_unchanged():
27
+ comp = FloorPlan()
28
+ payload = copy.deepcopy(SAMPLE_VALUE)
29
+ result = comp.preprocess(payload)
30
+ assert result == payload
31
+
32
+
33
+ def test_preprocess_none():
34
+ comp = FloorPlan()
35
+ assert comp.preprocess(None) is None
36
+
37
+
38
+ def test_example_value_shape():
39
+ comp = FloorPlan()
40
+ ex = comp.example_value()
41
+ assert "corners" in ex
42
+ assert "furnitures" in ex
43
+ assert len(ex["corners"]) >= 3
44
+ assert len(ex["furnitures"]) >= 1
45
+
46
+
47
+ def test_example_value_returns_independent_copies():
48
+ comp = FloorPlan()
49
+ a = comp.example_value()
50
+ b = comp.example_value()
51
+ a["corners"].append([999, 999])
52
+ assert [999, 999] not in b["corners"], "example_value must return a deep copy"
floorplan/demo/__init__.py ADDED
File without changes
floorplan/demo/app.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ from gradio_floorplan import FloorPlan
4
+
5
+
6
+ example = FloorPlan().example_value()
7
+
8
+ demo = gr.Interface(
9
+ lambda x:x,
10
+ FloorPlan(), # interactive version of your component
11
+ FloorPlan(), # static version of your component
12
+ # examples=[[example]], # uncomment this line to view the "example version" of your component
13
+ )
14
+
15
+
16
+ if __name__ == "__main__":
17
+ demo.launch()
floorplan/frontend/Example.svelte ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+
4
+ export let value: string | null;
5
+ export let type: "gallery" | "table";
6
+ export let selected = false;
7
+
8
+ let size: number;
9
+ let el: HTMLDivElement;
10
+
11
+ function set_styles(element: HTMLElement, el_width: number): void {
12
+ element.style.setProperty(
13
+ "--local-text-width",
14
+ `${el_width && el_width < 150 ? el_width : 200}px`
15
+ );
16
+ element.style.whiteSpace = "unset";
17
+ }
18
+
19
+ function truncate_text(text: string | null, max_length = 60): string {
20
+ if (!text) return "";
21
+ const str = String(text);
22
+ if (str.length <= max_length) return str;
23
+ return str.slice(0, max_length) + "...";
24
+ }
25
+
26
+ onMount(() => {
27
+ set_styles(el, size);
28
+ });
29
+ </script>
30
+
31
+ <div
32
+ bind:clientWidth={size}
33
+ bind:this={el}
34
+ class:table={type === "table"}
35
+ class:gallery={type === "gallery"}
36
+ class:selected
37
+ >
38
+ {truncate_text(value)}
39
+ </div>
40
+
41
+ <style>
42
+ .gallery {
43
+ padding: var(--size-1) var(--size-2);
44
+ }
45
+
46
+ div {
47
+ overflow: hidden;
48
+ min-width: var(--local-text-width);
49
+
50
+ white-space: nowrap;
51
+ }
52
+ </style>
floorplan/frontend/Index.svelte ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <svelte:options accessors={true} />
2
+
3
+ <script lang="ts">
4
+ import type { SimpleTextboxProps, SimpleTextboxEvents } from "./types";
5
+ import { Gradio } from "@gradio/utils";
6
+ import { BlockTitle } from "@gradio/atoms";
7
+ import { Block } from "@gradio/atoms";
8
+ import { StatusTracker } from "@gradio/statustracker";
9
+ import { tick } from "svelte";
10
+
11
+ const props = $props();
12
+ const gradio = new Gradio<SimpleTextboxEvents, SimpleTextboxProps>(props);
13
+
14
+ let el: HTMLTextAreaElement | HTMLInputElement;
15
+ const container = true;
16
+ let old_value = $state(gradio.props.value);
17
+
18
+ async function handle_keypress(e: KeyboardEvent): Promise<void> {
19
+ await tick();
20
+ if (e.key === "Enter") {
21
+ e.preventDefault();
22
+ gradio.dispatch("submit");
23
+ }
24
+ }
25
+
26
+ $effect(() => {
27
+ if (old_value != gradio.props.value) {
28
+ old_value = gradio.props.value;
29
+ gradio.dispatch("change");
30
+ }
31
+ });
32
+ </script>
33
+
34
+ <Block
35
+ visible={gradio.shared.visible}
36
+ elem_id={gradio.shared.elem_id}
37
+ elem_classes={gradio.shared.elem_classes}
38
+ scale={gradio.shared.scale}
39
+ min_width={gradio.shared.min_width}
40
+ allow_overflow={false}
41
+ padding={true}
42
+ rtl={gradio.props.rtl}
43
+ >
44
+ {#if gradio.shared.loading_status}
45
+ <StatusTracker
46
+ autoscroll={gradio.shared.autoscroll}
47
+ i18n={gradio.i18n}
48
+ {...gradio.shared.loading_status}
49
+ on_clear_status={() =>
50
+ gradio.dispatch("clear_status", gradio.shared.loading_status)}
51
+ />
52
+ {/if}
53
+
54
+ <label class:container>
55
+ <BlockTitle show_label={gradio.shared.show_label} info={undefined}
56
+ >{gradio.shared.label}</BlockTitle
57
+ >
58
+
59
+ <input
60
+ data-testid="textbox"
61
+ type="text"
62
+ class="scroll-hide"
63
+ bind:value={gradio.props.value}
64
+ bind:this={el}
65
+ placeholder={gradio.props.placeholder}
66
+ disabled={!gradio.shared.interactive}
67
+ dir={gradio.props.rtl ? "rtl" : "ltr"}
68
+ on:input={() => gradio.dispatch("input")}
69
+ on:keypress={handle_keypress}
70
+ />
71
+ </label>
72
+ </Block>
73
+
74
+ <style>
75
+ label {
76
+ display: block;
77
+ width: 100%;
78
+ }
79
+
80
+ input {
81
+ display: block;
82
+ position: relative;
83
+ outline: none !important;
84
+ box-shadow: var(--input-shadow);
85
+ background: var(--input-background-fill);
86
+ padding: var(--input-padding);
87
+ width: 100%;
88
+ color: var(--body-text-color);
89
+ font-weight: var(--input-text-weight);
90
+ font-size: var(--input-text-size);
91
+ line-height: var(--line-sm);
92
+ border: none;
93
+ }
94
+ .container > input {
95
+ border: var(--input-border-width) solid var(--input-border-color);
96
+ border-radius: var(--input-radius);
97
+ }
98
+ input:disabled {
99
+ -webkit-text-fill-color: var(--body-text-color);
100
+ -webkit-opacity: 1;
101
+ opacity: 1;
102
+ }
103
+
104
+ input:focus {
105
+ box-shadow: var(--input-shadow-focus);
106
+ border-color: var(--input-border-color-focus);
107
+ }
108
+
109
+ input::placeholder {
110
+ color: var(--input-placeholder-color);
111
+ }
112
+ </style>
floorplan/frontend/gradio.config.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: [],
3
+ svelte: {
4
+ preprocess: [],
5
+ },
6
+ build: {
7
+ target: "modules",
8
+ },
9
+ };
floorplan/frontend/package.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "gradio_floorplan",
3
+ "version": "0.3.37",
4
+ "description": "Gradio UI packages",
5
+ "type": "module",
6
+ "author": "",
7
+ "license": "ISC",
8
+ "private": false,
9
+ "main_changeset": true,
10
+ "exports": {
11
+ ".": {
12
+ "gradio": "./Index.svelte",
13
+ "svelte": "./dist/Index.svelte",
14
+ "types": "./dist/Index.svelte.d.ts"
15
+ },
16
+ "./example": {
17
+ "gradio": "./Example.svelte",
18
+ "svelte": "./dist/Example.svelte",
19
+ "types": "./dist/Example.svelte.d.ts"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "dependencies": {
24
+ "@gradio/atoms": "0.22.2",
25
+ "@gradio/icons": "0.15.1",
26
+ "@gradio/statustracker": "0.12.5",
27
+ "@gradio/utils": "0.12.0",
28
+ "svelte": "^5.48.0"
29
+ },
30
+ "devDependencies": {
31
+ "@gradio/preview": "0.16.0"
32
+ },
33
+ "peerDependencies": {
34
+ "svelte": "^5.48.0"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/gradio-app/gradio.git",
39
+ "directory": "js/simpletextbox"
40
+ }
41
+ }
floorplan/frontend/tsconfig.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "checkJs": true,
5
+ "esModuleInterop": true,
6
+ "forceConsistentCasingInFileNames": true,
7
+ "resolveJsonModule": true,
8
+ "skipLibCheck": true,
9
+ "sourceMap": true,
10
+ "strict": true,
11
+ "verbatimModuleSyntax": true
12
+ },
13
+ "exclude": ["node_modules", "dist", "./gradio.config.js"]
14
+ }
floorplan/frontend/types.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { LoadingStatus } from "@gradio/statustracker";
2
+
3
+ export interface SimpleTextboxProps {
4
+ value: string;
5
+ placeholder: string;
6
+ rtl: boolean;
7
+ }
8
+
9
+ export interface SimpleTextboxEvents {
10
+ change: never;
11
+ submit: never;
12
+ input: never;
13
+ clear_status: LoadingStatus;
14
+ }
floorplan/pyproject.toml ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = [
3
+ "hatchling",
4
+ "hatch-requirements-txt",
5
+ "hatch-fancy-pypi-readme>=22.5.0",
6
+ ]
7
+ build-backend = "hatchling.build"
8
+
9
+ [project]
10
+ name = "gradio_floorplan"
11
+ version = "0.0.1"
12
+ description = "Python library for easily interacting with trained machine learning models"
13
+ readme = "README.md"
14
+ license = "Apache-2.0"
15
+ requires-python = ">=3.10"
16
+ authors = [{ name = "YOUR NAME", email = "YOUREMAIL@domain.com" }]
17
+ keywords = [
18
+ "gradio-custom-component",
19
+ "gradio-template-SimpleTextbox"
20
+ ]
21
+ # Add dependencies here
22
+ dependencies = ["gradio>=6.0,<7.0"]
23
+ classifiers = [
24
+ 'Development Status :: 3 - Alpha',
25
+ 'Operating System :: OS Independent',
26
+ 'Programming Language :: Python :: 3',
27
+ 'Programming Language :: Python :: 3 :: Only',
28
+ 'Programming Language :: Python :: 3.8',
29
+ 'Programming Language :: Python :: 3.9',
30
+ 'Programming Language :: Python :: 3.10',
31
+ 'Programming Language :: Python :: 3.11',
32
+ 'Topic :: Scientific/Engineering',
33
+ 'Topic :: Scientific/Engineering :: Artificial Intelligence',
34
+ 'Topic :: Scientific/Engineering :: Visualization',
35
+ ]
36
+
37
+ # The repository and space URLs are optional, but recommended.
38
+ # Adding a repository URL will create a badge in the auto-generated README that links to the repository.
39
+ # Adding a space URL will create a badge in the auto-generated README that links to the space.
40
+ # This will make it easy for people to find your deployed demo or source code when they
41
+ # encounter your project in the wild.
42
+
43
+ # [project.urls]
44
+ # repository = "your github repository"
45
+ # space = "your space url"
46
+
47
+ [project.optional-dependencies]
48
+ dev = ["build", "twine"]
49
+
50
+ [tool.hatch.build]
51
+ artifacts = ["/backend/gradio_floorplan/templates", "*.pyi"]
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ packages = ["/backend/gradio_floorplan"]
55
+
56
+ [dependency-groups]
57
+ dev = [
58
+ "pytest>=9.0.2",
59
+ ]
floorplan/uv.lock ADDED
The diff for this file is too large to render. See raw diff