Skip to content
This repository has been archived by the owner on Apr 25, 2023. It is now read-only.

Commit

Permalink
feat: plugin system, refactoring visualizer (#50)
Browse files Browse the repository at this point in the history
* refactor visualizer

* add Plugin component

* enable plugins in visualizer

* update config

* format

* change default value of options for show method

* fix storytelling

* add dnd provider to published page

* fix property value conversion

* fix property value convertion bug

* update gql

* refactor storytelling

* update react-svg

* perf icon

* update why-did-you-render

* set up why-did-you-render

* perf treeview hooks

* fix errors from react

* refactor use-delyaed-count

* add primitive selection reason

* fix fov animation

* add selection reason

* cancel current delay in use-delayed-count

* refactor and fix photooverlay

* fix type error

* temporally disable primitive and widget plugin

* reduce load of plugin system

* make plugin block selectable
  • Loading branch information
rot1024 committed Aug 9, 2021
1 parent 0186744 commit 1729390
Show file tree
Hide file tree
Showing 169 changed files with 7,078 additions and 4,151 deletions.
39 changes: 39 additions & 0 deletions .storybook/public/plugins/block.js
@@ -0,0 +1,39 @@
const html = `
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
<script id="l" src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<div id="map" style="width: 100%; height: 300px;"></div>
<script>
document.getElementById("l").addEventListener("load", () => {
const map = L.map("map").setView([0, 0], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
const marker = L.marker();
const cb = (block) => {
if (block?.property?.location) {
const latlng = [block.property.location.lat, block.property.location.lng];
map.setView(latlng);
marker.setLatLng(latlng).addTo(map);
} else {
marker.remove();
}
parent.postMessage("updated", "*");
};
addEventListener("message", e => {
if (e.source !== parent) return;
cb(e.data);
});
cb(${JSON.stringify(reearth.block)});
});
</script>
`;

reearth.ui.show(html);
reearth.on("update", () => {
reearth.ui.postMessage(reearth.block);
});
reearth.on("message", msg => {
console.log("message received:", msg);
});
18 changes: 18 additions & 0 deletions .storybook/public/plugins/hidden.js
@@ -0,0 +1,18 @@
const html = `
<script>
addEventListener("message", e => {
if (e.source !== parent) return;
const p = document.createElement("p");
p.textContent = JSON.stringify(e.data);
document.body.appendChild(p);
console.log("plugin -> iframe", e.data);
parent.postMessage(e.data, "*");
});
</script>
`;

reearth.ui.show(html, { visible: false });
reearth.on("message", (message) => {
console.log("plugin <- iframe", message);
});
reearth.ui.postMessage("foo!");
11 changes: 0 additions & 11 deletions .storybook/public/plugins/hogehoge/index.js

This file was deleted.

19 changes: 19 additions & 0 deletions .storybook/public/plugins/plugin.js
@@ -0,0 +1,19 @@
const html = `
<h1>IFrame works</h1>
<script>
addEventListener("message", e => {
if (e.source !== parent) return;
const p = document.createElement("p");
p.textContent = JSON.stringify(e.data);
document.body.appendChild(p);
console.log("plugin -> iframe", e.data);
parent.postMessage(e.data, "*");
});
</script>
`;

reearth.ui.show(html);
reearth.on("message", (message) => {
console.log("plugin <- iframe", message);
});
reearth.ui.postMessage("foo!");
Binary file added .storybook/public/sample.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 5 additions & 14 deletions .storybook/webpack.config.js
@@ -1,25 +1,14 @@
const webpack = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path");
const fs = require("fs");
const pkg = require("../package.json");

module.exports = ({ config }) => {
config.externals = {
...config.externals,
cesium: "Cesium",
};

config.module.rules.push({
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: JSON.parse(fs.readFileSync(path.resolve(__dirname, "../.babelrc"))),
},
],
});

config.module.rules.push({
test: /\.yml$/,
use: [{ loader: "json-loader" }, { loader: "yaml-flat-loader" }],
Expand All @@ -39,17 +28,19 @@ module.exports = ({ config }) => {
}),
new webpack.DefinePlugin({
CESIUM_BASE_URL: JSON.stringify("cesium"),
REEARTH_WEB_VERSION: pkg.version,
}),
);

config.resolve.extensions.push(".ts", ".tsx");

config.resolve.alias = {
...config.resolve.alias,
"@reearth": path.resolve(__dirname, "..", "src"),
"@emotion/core": path.resolve(__dirname, "..", "node_modules", "@emotion", "react"),
"emotion-theming": path.resolve(__dirname, "..", "node_modules", "@emotion", "react"),
};

// For quickjs-emscripten
config.resolve.fallback = { ...config.resolve.fallback, fs: false, path: false };

return config;
};
118 changes: 118 additions & 0 deletions bin/pluginDoc.ts
@@ -0,0 +1,118 @@
// import { writeFileSync } from "fs";
import * as ts from "typescript";
import tsconfig from "../tsconfig.json";

const files = ["./src/plugin/api.ts"];
const program = ts.createProgram(files, tsconfig as any);
const tc = program.getTypeChecker();

type D = {
name: string;
desc: string;
properties: P[];
};

type P = {
name: string;
desc: string;
type: string;
required: boolean;
};

for (const s of program.getSourceFiles()) {
if (s.fileName.substr(-5) === ".d.ts") continue;
const decls: D[] = [];
ts.forEachChild(s, node => {
const d = parse(node);
if (d) {
decls.push(d);
}
});
const res = render(decls, n => decls.findIndex(d => d.name === n) != -1);
// writeFileSync("./docs/plugin.md", res);
console.log(res);
}

function parse(node: ts.Node): D | undefined {
if (!ts.isTypeAliasDeclaration(node)) return;
const sym = tc.getSymbolAtLocation(node.name);
if (!sym) return;

const properties = tc
.getTypeAtLocation(node)
.getApparentProperties()
.map((s): P | undefined => {
const d = s.getDeclarations()?.[0];
const t = d ? tc.getTypeAtLocation(d) : undefined;
if (!t) return undefined;
return {
name: s.name,
type: tc.typeToString(t),
required: (s.flags & ts.SymbolFlags.Optional) === 0,
desc: ts.displayPartsToString(s.getDocumentationComment(tc)),
};
})
.filter((p): p is P => !!p);

return {
name: node.name.text,
desc: ts.displayPartsToString(sym.getDocumentationComment(tc)),
properties,
};
}

function render(decls: D[], linkable: (name: string) => boolean): string {
return decls
.flatMap(d => [
`# ${d.name}`,
d.desc,
d.properties.map(p => `- ${renderHead(p, linkable)}`).join("\n"),
...d.properties.filter(p => p.desc).flatMap(p => [`## ${renderHead(p, linkable)}`, p.desc]),
])
.filter(Boolean)
.join("\n\n");
}

function renderHead(p: P, linkable: (name: string) => boolean): string {
return [p.name, ": ", renderType(p.type, p.required, linkable)].join("");
}

function renderType(type: string, required: boolean, linkable: (name: string) => boolean) {
const ts = split(type, [") => ", "[]", "?: ", ": ", " & ", " | ", "(", ")", ", "])
.map(s =>
typeof s === "string" && linkable(s)
? {
link: s,
}
: s,
)
.map(s =>
typeof s === "string" ? s : "link" in s ? `[${s.link}](#${s.link})` : "s" in s ? s.s : "",
)
.join("");
const func = / => /.test(type);
return [
!required && func ? "(" : "",
ts,
!required && func ? ")" : "",
!required ? " | undefined" : "",
].join("");
}

function split(text: string, splitter: string[]): (string | { s: string })[] {
if (!text.length) return [];
const res: (string | { s: string })[] = [];
let buf = "";
for (let i = 0; i < text.length; i++) {
buf += text[i];
const s = splitter.find(s => buf.includes(s));
if (s) {
res.push(buf.slice(0, -s.length), { s: buf.slice(-s.length) });
buf = "";
}
}
if (buf.length) {
res.push(buf);
}
return res;
}
9 changes: 6 additions & 3 deletions package.json
Expand Up @@ -24,7 +24,8 @@
"cypress:run": "cypress run",
"gen": "run-p gen:*",
"gen:gql": "graphql-codegen --config codegen.yml",
"gen:i18n": "extract-messages -d en -l=en,ja -o translations -f yaml 'src/**/*.tsx' --extractFromFormatMessageCall"
"gen:i18n": "extract-messages -d en -l=en,ja -o translations -f yaml 'src/**/*.tsx' --extractFromFormatMessageCall",
"gen:doc:plugin": "ts-node -O '{\"module\":\"CommonJS\"}' ./bin/pluginDoc"
},
"engines": {
"node": ">=12"
Expand Down Expand Up @@ -96,7 +97,7 @@
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.15.0",
"@typescript-eslint/parser": "^4.15.0",
"@welldone-software/why-did-you-render": "^6.0.5",
"@welldone-software/why-did-you-render": "^6.2.0",
"babel-loader": "^8.2.2",
"babel-plugin-react-intl-auto": "^3.1.0",
"clean-webpack-plugin": "^3.0.0",
Expand Down Expand Up @@ -175,6 +176,8 @@
"lodash-es": "^4.17.20",
"mini-svg-data-uri": "^1.2.3",
"parse-domain": "^3.0.3",
"quickjs-emscripten": "^0.13.0",
"quickjs-emscripten-sync": "^1.1.0",
"rc-slider": "9.7.1",
"react": "^17.0.1",
"react-accessible-accordion": "^3.3.4",
Expand All @@ -192,7 +195,7 @@
"react-popper": "^2.2.4",
"react-redux": "^7.2.2",
"react-spinners": "^0.10.4",
"react-svg": "^11.2.3",
"react-svg": "^14.0.7",
"react-use": "^17.1.1",
"redux-first-history": "^4.5.0",
"regenerator-runtime": "^0.13.7",
Expand Down
60 changes: 21 additions & 39 deletions src/components/atoms/Icon/index.tsx
@@ -1,4 +1,4 @@
import React from "react";
import React, { ComponentProps, memo } from "react";
import { ReactSVG } from "react-svg";

import { styled } from "@reearth/theme";
Expand All @@ -19,60 +19,42 @@ export type Props = {
const Icon: React.FC<Props> = ({ className, icon, color, size, alt, onClick }) => {
if (!icon) return null;

const LocalIconComponent = Icons[icon as Icons];
if (LocalIconComponent) {
return (
<IconComponent
className={className}
src={LocalIconComponent}
color={color}
size={size}
onClick={onClick}
alt={alt}
/>
);
}

if (icon.split(".").pop() !== "svg") {
return <ImageIconComponent src={icon} size={size} />;
const sizeStr = typeof size === "number" ? `${size}px` : size;
const builtin = Icons[icon as Icons];
if (!builtin) {
return <StyledImg src={icon} alt={alt} size={sizeStr} onClick={onClick} />;
}

return (
<IconComponent
<StyledSvg
className={className}
src={icon}
src={builtin}
color={color}
size={size}
size={sizeStr}
onClick={onClick}
alt={alt}
/>
);
};

// This is needed to avoid a type error between Emotion and ReactSVG props(emotion styled components need className)
export type ReactSVGProps = {
className?: string;
src: string | Icons;
onClick?: () => void;
alt?: string;
color?: string;
};
const StyledImg = styled.img<{ size?: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
`;

const ReactSVGComponent: React.FC<ReactSVGProps> = ({ color, ...props }) => {
return <ReactSVG {...props} style={{ color }} />;
const SVG: React.FC<
Pick<ComponentProps<typeof ReactSVG>, "className" | "src" | "onClick" | "alt">
> = props => {
return <ReactSVG {...props} wrapper="span" />;
};

const IconComponent = styled(ReactSVGComponent)<{ color?: string; size?: string | number }>`
const StyledSvg = styled(SVG)<{ color?: string; size?: string }>`
font-size: 0;
color: ${({ color }) => color};
svg {
width: ${props => (typeof props.size === "number" ? `${props.size}px` : props.size)};
height: ${props => (typeof props.size === "number" ? `${props.size}px` : props.size)};
width: ${({ size }) => size};
height: ${({ size }) => size};
}
`;

const ImageIconComponent = styled.img<{ size?: string | number }>`
width: ${({ size }) => size + "px"};
height: ${({ size }) => size + "px"};
`;

export default Icon;
export default memo(Icon);

0 comments on commit 1729390

Please sign in to comment.