feat: allow to edit the print layout

Merge branch 'movable-print'
main
Yanick Champoux 2022-04-10 18:24:40 -04:00
commit 381d497a15
19 changed files with 432 additions and 135 deletions

View File

@ -1 +1,2 @@
export const browser = true; export const browser = true;
export const dev = true;

1
fake/app/paths.js Normal file
View File

@ -0,0 +1 @@
export const base = "";

View File

@ -18,9 +18,6 @@
"@sveltejs/adapter-static": "^1.0.0-next.28", "@sveltejs/adapter-static": "^1.0.0-next.28",
"@sveltejs/kit": "^1.0.0-next.288", "@sveltejs/kit": "^1.0.0-next.288",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.38", "@sveltejs/vite-plugin-svelte": "^1.0.0-next.38",
"@vitebook/client": "^0.23.2",
"@vitebook/core": "^0.23.2",
"@vitebook/theme-default": "^0.23.2",
"eslint": "^8.10.0", "eslint": "^8.10.0",
"eslint-config-prettier": "^8.4.0", "eslint-config-prettier": "^8.4.0",
"eslint-plugin-svelte3": "^3.4.1", "eslint-plugin-svelte3": "^3.4.1",
@ -28,13 +25,12 @@
"prettier-plugin-svelte": "^2.6.0", "prettier-plugin-svelte": "^2.6.0",
"standard-version": "^9.3.2", "standard-version": "^9.3.2",
"storybook-builder-vite": "0.1.21", "storybook-builder-vite": "0.1.21",
"svelte": "^3.46.4", "svelte": "^3.46.4"
"vite": "^2.7.0"
}, },
"dependencies": { "dependencies": {
"@storybook/addon-essentials": "^6.4.19", "@storybook/addon-essentials": "^6.4.19",
"@storybook/addon-svelte-csf": "^1.1.0", "@storybook/addon-svelte-csf": "^1.1.0",
"@storybook/svelte": "^6.4.19", "@storybook/svelte": "^6.4.21",
"@sveltejs/adapter-node": "^1.0.0-next.0", "@sveltejs/adapter-node": "^1.0.0-next.0",
"chota": "^0.8.0", "chota": "^0.8.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View File

@ -1,4 +1,12 @@
<div> <div
use:movable={{
disabled: !isMovable,
}}
on:translate={({ detail: translate }) => {
ship.dispatch.setUITransform({ system: "hull", translate });
}}
style:transform={hull?.uiTransform}
>
{#each rows as row, i (i)} {#each rows as row, i (i)}
<div class="row"> <div class="row">
{#each row as threshold, j (j)} {#each row as threshold, j (j)}
@ -13,10 +21,18 @@
</div> </div>
<script> <script>
import { base } from '$app/paths'; import { base } from "$app/paths";
import { getContext } from "svelte";
import { movable } from "../../MainSystems/movable.js";
export let shipMass = 0; export let shipMass = 0;
export let rating = 0; export let rating = 0;
export let advanced = false; export let advanced = false;
export let hull = {};
export let isMovable = false;
const ship = getContext("ship");
let nbr_rows; let nbr_rows;
$: nbr_rows = advanced ? 3 : 4; $: nbr_rows = advanced ? 3 : 4;
@ -50,9 +66,9 @@
</script> </script>
<style> <style>
.row { .row {
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.cell { .cell {
display: inline-block; display: inline-block;
margin-right: 0.5em; margin-right: 0.5em;

View File

@ -1,6 +1,8 @@
<div> <div>
<Armour armour={structure.armour} /> <Armour armour={structure.armour} />
<Integrity <Integrity
{isMovable}
hull={structure?.hull}
rating={structure.hull.rating} rating={structure.hull.rating}
advanced={structure.hull.advanced} advanced={structure.hull.advanced}
{ship_mass} {ship_mass}
@ -13,4 +15,5 @@
export let structure = {}; export let structure = {};
export let ship_mass = 0; export let ship_mass = 0;
export let isMovable = false;
</script> </script>

View File

@ -1,28 +1,33 @@
<div class="main_systems"> <div class="main_systems">
{#if ftl !== "none"} {#if ftl !== "none"}
<img <img
bind:this={targetFTL}
class="ftl" class="ftl"
src="{base}/icons/ftl-drive.svg" src="{base}/icons/ftl-drive.svg"
alt="ftl drive" alt="ftl drive"
use:movable={{
disabled: !isMovable,
}}
on:translate={({ detail: translate }) => {
ship.dispatch.setUITransform({ system: "ftl", translate });
}}
style:transform={ftl?.uiTransform}
/> />
{#if movable}
<Movable target={targetFTL} />
{/if}
{/if} {/if}
{#if engine > 0} {#if engine > 0}
<div <div
bind:this={targetEngine}
class="thrust" class="thrust"
style="background-image: url({base}/icons/standard-drive.svg);" style="background-image: url({base}/icons/standard-drive.svg);"
use:movable={{
disabled: !isMovable,
}}
on:translate={({ detail: translate }) => {
ship.dispatch.setUITransform({ system: "drive", translate });
}}
style:transform={drive?.uiTransform}
> >
{engine} {engine}
</div> </div>
{#if movable}
<Movable target={targetEngine} />
{/if}
{/if} {/if}
<img <img
@ -30,20 +35,36 @@
src="{base}/icons/internal-systems.svg" src="{base}/icons/internal-systems.svg"
alt="internal systems" alt="internal systems"
bind:this={targetInternal} bind:this={targetInternal}
use:movable={{
disabled: !isMovable,
}}
on:translate={({ detail: translate }) => {
ship.dispatch.setUITransform({ system: "internalSystems", translate });
}}
style:transform={structure?.uiTransform}
/> />
{#if movable}
<Movable target={targetInternal} />
{/if}
</div> </div>
<script> <script>
import { getContext } from "svelte";
import { base } from "$app/paths"; import { base } from "$app/paths";
import Movable from "./Movable.svelte"; import Movable from "./Movable.svelte";
import { movable } from "./movable.js";
export let ftl = "none"; export let ftl = "none";
export let engine = 0; export let engine = 0;
export let movable = false; export let isMovable = false;
export let structure = {};
export let drive = {};
let internalTranslate = "translate(50px,50px)";
const ship = getContext("ship");
let frame = {
translate: [0, 0],
};
let targetFTL; let targetFTL;
let targetInternal; let targetInternal;

View File

@ -0,0 +1,64 @@
import VanillaMoveable, { PROPERTIES, EVENTS } from "moveable";
import { camelize, isUndefined } from "@daybrush/utils";
function createMoveable(node, options) {
let translate = [0, 0];
options = {
originDraggable: true,
originRelative: true,
draggable: true,
throttleDrag: 0,
zoom: 1,
origin: false,
onDrag(e) {
translate = e.beforeTranslate;
node.dispatchEvent(new CustomEvent("translate", { detail: translate }));
},
target: node,
...options,
};
const moveable = new VanillaMoveable(document.body, options);
EVENTS.forEach((name) => {
const onName = camelize(`on ${name}`);
moveable.on(name, (e) => {
const result = options[onName] && options[onName](e);
const result2 = node.dispatchEvent(new CustomEvent(name, { detail: e }));
return !isUndefined(result)
? result
: !isUndefined(result2)
? result2
: undefined;
});
});
return moveable;
}
export function movable(node, options) {
let moveable = options.disabled ? undefined : createMoveable(options);
const destroy = () => {
if (!moveable) return;
moveable.destroy();
moveable = undefined;
};
const update = async (params) => {
if (params.disabled) {
destroy();
} else {
if (!moveable) {
moveable = createMoveable(node, params);
}
}
};
return {
destroy,
update,
};
}

View File

@ -1,15 +1,16 @@
<Meta title="Output/Print" component={Print} argTypes={{ <Meta
ship: { title="Output/Print"
type: 'object', component={Print}
defaultValue: sample argTypes={{
} isMovable: { defaultValue: false },
}} /> }}
/>
<Story name="Primary" args={{}} /> <Story name="Primary" args={{}} />
<Template let:args> <Template let:args>
<div style="width: 50em"> <div style="width: 50em; positive: relative;">
<Print ship={sample}/> <Print ship={$shipState} {...args} />
</div> </div>
</Template> </Template>
@ -17,7 +18,16 @@
import { Meta, Template, Story } from "@storybook/addon-svelte-csf"; import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
import { action } from "@storybook/addon-actions"; import { action } from "@storybook/addon-actions";
import sample from './sample.js'; import { setContext } from "svelte";
import Print from './index.svelte'; import sample from "./sample.js";
import shipStore from "$lib/store/ship.js";
const ship = shipStore(sample);
setContext("ship", ship);
const shipState = ship.state;
import Print from "./index.svelte";
</script> </script>

View File

@ -1,14 +1,27 @@
<div> <div
{#each range(1,firecons) as firecon} style:transform={uiTransform}
use:movable={{
disabled: !isMovable,
ship,
system: "firecons",
}}
>
{#each range(1, stations) as firecon}
<img class="firecon" src="{base}/icons/firecon.svg" alt="firecon" /> <img class="firecon" src="{base}/icons/firecon.svg" alt="firecon" />
{/each} {/each}
</div> </div>
<script> <script>
import { base } from '$app/paths'; import { base } from "$app/paths";
import {range} from "$lib/utils.js"; import { range } from "$lib/utils.js";
import { getContext } from "svelte";
import { movable } from "../../movable.js";
export let firecons = 0; export let stations = 0;
export let isMovable = false;
export let uiTransform = "";
const ship = getContext("ship");
</script> </script>
<style> <style>

View File

@ -1,18 +1,33 @@
<div> <div
{#each range(1,standard) as i} use:movable={{
disabled: !isMovable,
}}
on:translate={({ detail: translate }) => {
ship.dispatch.setUITransform({ system: "screens", translate });
}}
style:transform={uiTransform}
>
{#each range(1, standard) as i}
<img src="{base}/icons/screen.svg" alt="screen" /> <img src="{base}/icons/screen.svg" alt="screen" />
{/each} {/each}
{#each range(1,advanced) as i} {#each range(1, advanced) as i}
<img src="{base}/icons/screen-advanced.svg" alt="advanced screen" /> <img src="{base}/icons/screen-advanced.svg" alt="advanced screen" />
{/each} {/each}
</div> </div>
<script> <script>
import { base } from '$app/paths'; import { base } from "$app/paths";
import {range} from "$lib/utils.js"; import { range } from "$lib/utils.js";
import { getContext } from "svelte";
import { movable } from "../../MainSystems/movable.js";
export let standard = 0; export let standard = 0;
export let advanced = 0; export let advanced = 0;
export let uiTransform = "";
export let isMovable = false;
const ship = getContext("ship");
</script> </script>
<style> <style>

View File

@ -1,7 +1,7 @@
<div> <div>
<Firecons {firecons} /> <Firecons {isMovable} {...firecons} />
<Screens {...screens} /> <Screens {isMovable} {...screens} />
</div> </div>
<script> <script>
@ -10,6 +10,7 @@
export let firecons = 0; export let firecons = 0;
export let screens = {}; export let screens = {};
export let isMovable = false;
</script> </script>
<style> <style>

View File

@ -1,4 +1,11 @@
<div> <div
style:transform={uiTransform}
use:movable={{
disabled: !isMovable,
ship,
system: ["weapon", id],
}}
>
<Arcs selected={arcs} size="40"> <Arcs selected={arcs} size="40">
<text x="50%" y="50%"> <text x="50%" y="50%">
{weaponClass} {weaponClass}
@ -7,9 +14,15 @@
</div> </div>
<script> <script>
import { getContext } from "svelte";
import { movable } from "../../movable.js";
import Arcs from "$lib/components/ShipEdit/Weaponry/Weapon/Arcs.svelte"; import Arcs from "$lib/components/ShipEdit/Weaponry/Weapon/Arcs.svelte";
export let weaponClass = 1; export let weaponClass = 1;
export let arcs = []; export let arcs = [];
export let uiTransform = "";
export let isMovable = false;
export let id = -1;
const ship = getContext("ship");
</script> </script>
<style> <style>

View File

@ -1,17 +1,22 @@
<div class="weapons"> <div class="weapons">
<div class="beams"> <div class="beams">
{#each beams as beam} {#each beams as beam}
<Beam {...beam} /> <Beam {isMovable} {...beam} />
{/each} {/each}
</div> </div>
</div> </div>
<script> <script>
import Beam from "./Beam/index.svelte"; import Beam from "./Beam/index.svelte";
import { getContext } from "svelte";
import { movable } from "../movable.js";
export let weapons = []; export let weapons = [];
export let isMovable = false;
let beams = []; let beams = [];
$: beams = weapons.filter(({ type }) => type === "beam"); $: beams = weapons.filter(({ type }) => type === "beam");
const ship = getContext("ship");
</script> </script>
<style> <style>

View File

@ -1,9 +1,8 @@
<div class="notice"> <div class="notice">
<label> <label>
<input type="checkbox" bind:checked={movable} /> enable wiggletron (<i <input type="checkbox" bind:checked={isMovable} /> edit layout
>alpha feature</i
>)
</label> </label>
<button class="button error" on:click={resetLayout}>reset layout</button>
</div> </div>
<div class="print-output"> <div class="print-output">
@ -15,26 +14,34 @@
/> />
<div class="section-2"> <div class="section-2">
<Hull structure={ship.structure} shipMass={ship.identification.mass} /> <Hull
structure={ship.structure}
shipMass={ship.identification.mass}
{isMovable}
/>
<Systems <Systems
firecons={ship.weaponry.firecons.nbr} {isMovable}
firecons={ship.weaponry.firecons}
screens={ship.structure.screens} screens={ship.structure.screens}
/> />
</div> </div>
<Weapons weapons={ship.weaponry.weapons} /> <Weapons {isMovable} weapons={ship.weaponry.weapons} />
<MainSystems <MainSystems
{movable} {isMovable}
ftl={ship?.propulsion?.ftl} ftl={ship?.propulsion?.ftl}
engine={ship?.propulsion?.drive?.rating} engine={ship?.propulsion?.drive?.rating}
drive={ship?.propulsion?.drive}
structure={ship?.structure}
/> />
</div> </div>
<div class="notice">Printing this page will only prints the ship sheet.</div> <div class="notice">Printing this page will only prints the ship sheet.</div>
<script> <script>
import { getContext } from "svelte";
import Identification from "./Identification/index.svelte"; import Identification from "./Identification/index.svelte";
import MainSystems from "./MainSystems/index.svelte"; import MainSystems from "./MainSystems/index.svelte";
import Hull from "./Hull/index.svelte"; import Hull from "./Hull/index.svelte";
@ -42,7 +49,14 @@
import Systems from "./Systems/index.svelte"; import Systems from "./Systems/index.svelte";
export let ship = {}; export let ship = {};
let movable = false; export let isMovable = false;
const { dispatch } = getContext("ship");
const resetLayout = () => {
isMovable = false;
dispatch.resetLayout();
};
</script> </script>
<style> <style>
@ -64,6 +78,10 @@
font-style: italic; font-style: italic;
margin-top: 1em; margin-top: 1em;
text-align: right; text-align: right;
display: flex;
justify-content: end;
align-items: center;
gap: 3em;
} }
@media print { @media print {

View File

@ -0,0 +1,76 @@
import VanillaMoveable, { PROPERTIES, EVENTS } from "moveable";
import { camelize, isUndefined } from "@daybrush/utils";
function createMoveable(node, options) {
let translate = [0, 0];
let ship = options.ship;
let system = options.system;
delete options.ship;
delete options.system;
if (Array.isArray(system)) {
system = { system: system[0], systemId: system[1] };
} else {
system = { system };
}
options = {
originDraggable: true,
originRelative: true,
draggable: true,
throttleDrag: 0,
zoom: 1,
origin: false,
onDrag(e) {
translate = e.beforeTranslate;
node.dispatchEvent(new CustomEvent("translate", { detail: translate }));
ship.dispatch.setUITransform({ ...system, translate });
},
target: node,
...options,
};
const moveable = new VanillaMoveable(document.body, options);
EVENTS.forEach((name) => {
const onName = camelize(`on ${name}`);
moveable.on(name, (e) => {
const result = options[onName] && options[onName](e);
const result2 = node.dispatchEvent(new CustomEvent(name, { detail: e }));
return !isUndefined(result)
? result
: !isUndefined(result2)
? result2
: undefined;
});
});
return moveable;
}
export function movable(node, options) {
let moveable = options.disabled ? undefined : createMoveable(options);
const destroy = () => {
if (!moveable) return;
moveable.destroy();
moveable = undefined;
};
const update = async (params) => {
if (params.disabled) {
destroy();
} else {
if (!moveable) {
moveable = createMoveable(node, params);
}
}
};
return {
destroy,
update,
};
}

View File

@ -11,48 +11,96 @@ import weaponry from "./weaponry/index.js";
import { screensReqsReaction } from "./structure/screens.js"; import { screensReqsReaction } from "./structure/screens.js";
const dux = new Updux({ const dux = new Updux({
subduxes: { subduxes: {
identification, identification,
propulsion, propulsion,
structure, structure,
carrier, carrier,
weaponry, weaponry,
}, },
initial: { initial: {
reqs: { cost: 0, mass: 10, usedMass: 0 }, reqs: { cost: 0, mass: 10, usedMass: 0 },
}, },
actions: { actions: {
setShipReqs: null, setShipReqs: null,
}, setUITransform: null,
resetLayout: null,
},
}); });
function resetUITransform(thing) {
if (typeof thing !== "object") return thing;
return u.map(
(v, k) => (k === "uiTransform" ? "" : resetUITransform(v)),
thing
);
}
dux.setMutation("resetLayout", () => resetUITransform);
dux.setMutation("setShipMass", (mass) => u({ reqs: { mass } })); dux.setMutation("setShipMass", (mass) => u({ reqs: { mass } }));
dux.setMutation('setShipReqs', reqs => u({reqs})); dux.setMutation("setShipReqs", (reqs) => u({ reqs }));
dux.setMutation("setUITransform", ({ system, systemId, translate }) => {
const transform = translate
? `translate(${translate[0]}px,${translate[1]}px)`
: "";
switch (system) {
case "firecons":
return u.updateIn("weaponry.firecons.uiTransform", transform);
case "weapon":
return u.updateIn(
"weaponry.weapons",
u.map(u.if(({ id }) => id === systemId, u({ uiTransform: transform })))
);
case "screens":
return u.updateIn("structure.screens.uiTransform", transform);
case "hull":
return u.updateIn("structure.hull.uiTransform", transform);
case "internalSystems":
const path = "structure.uiTransform";
return u.updateIn(path, transform);
case "ftl":
return u.updateIn("propulsion.ftl.uiTransform", transform);
case "drive":
return u.updateIn("propulsion.drive.uiTransform", transform);
default:
return (state) => state;
}
});
dux.addReaction(calculateDriveReqs); dux.addReaction(calculateDriveReqs);
dux.addReaction(ftlReqsReaction); dux.addReaction(ftlReqsReaction);
dux.addReaction(screensReqsReaction); dux.addReaction(screensReqsReaction);
dux.addReaction( (store) => (state) => { dux.addReaction((store) => (state) => {
let cost = 0; let cost = 0;
let mass = 0; let mass = 0;
let subsystems = Object.values(state); let subsystems = Object.values(state);
while(subsystems.length>0) { while (subsystems.length > 0) {
const subsystem = subsystems.shift(); const subsystem = subsystems.shift();
if( typeof subsystem !== 'object' ) continue; if (typeof subsystem !== "object") continue;
if( subsystem.reqs ) { if (subsystem.reqs) {
cost += subsystem.reqs.cost; cost += subsystem.reqs.cost;
mass += subsystem.reqs.mass; mass += subsystem.reqs.mass;
}
subsystems.push( ...Object.values(subsystem));
} }
store.dispatch.setShipReqs({cost,usedMass: mass}); subsystems.push(...Object.values(subsystem));
}
store.dispatch.setShipReqs({ cost, usedMass: mass });
}); });
export default dux; export default dux;

View File

@ -10,35 +10,32 @@ const dux = new Updux({
subduxes: { reqs }, subduxes: { reqs },
initial: { initial: {
type: "none", type: "none",
uiTransform: "",
}, },
actions: { actions: {
setFtl: null, setFtl: null,
setFtlReqs: null, setFtlReqs: null,
}, },
}); });
export default dux; export default dux;
dux.setMutation( 'setFtl', type => u({type}) ); dux.setMutation("setFtl", (type) => u({ type }));
dux.setMutation( 'setFtlReqs', reqs => u({reqs}) ); dux.setMutation("setFtlReqs", (reqs) => u({ reqs }));
export function calcFtlReqs(type,shipMass) { export function calcFtlReqs(type, shipMass) {
if(type==="none") return { cost: 0, mass: 0 }; if (type === "none") return { cost: 0, mass: 0 };
const mass = Math.ceil(shipMass / 10); const mass = Math.ceil(shipMass / 10);
return { return {
mass, mass,
cost: mass * ( type === 'advanced' ? 3 : 2 ), cost: mass * (type === "advanced" ? 3 : 2),
} };
} }
// needs to be at the top level // needs to be at the top level
export const ftlReqsReaction = store => export const ftlReqsReaction = (store) =>
createSelector( createSelector(
[ [(ship) => ship.propulsion.ftl.type, (ship) => ship.reqs.mass],
(ship) => ship.propulsion.ftl.type, (type, shipMass) => store.dispatch.setFtlReqs(calcFtlReqs(type, shipMass))
(ship) => ship.reqs.mass, );
],
(type,shipMass) =>
store.dispatch.setFtlReqs(calcFtlReqs(type,shipMass))
);

View File

@ -1,13 +1,15 @@
import { Updux } from 'updux'; import { Updux } from "updux";
import hull from './hull.js'; import hull from "./hull.js";
import screens from './screens.js'; import screens from "./screens.js";
import cargo from './cargo.js'; import cargo from "./cargo.js";
import armor from './armor.js'; import armor from "./armor.js";
import streamlining from './streamlining.js'; import streamlining from "./streamlining.js";
const dux = new Updux({ const dux = new Updux({
subduxes: { hull, screens, cargo, streamlining, armor } subduxes: { hull, screens, cargo, streamlining, armor },
initial: {
uiTransform: "",
},
}); });
export default dux; export default dux;

View File

@ -8,36 +8,33 @@ import { initial } from "lodash";
let composeEnhancers = compose; let composeEnhancers = compose;
if (dev && browser && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { if (dev && browser && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
} }
export default () => { export default (initialState = undefined) => {
if (browser) {
const i = localStorage.getItem("ship");
let initialState = undefined; if (i) initialState = JSON.parse(localStorage.getItem("ship"));
}
if( browser ) { const duxStore = shipDux.createStore(initialState, (mw) =>
const i =localStorage.getItem('ship'); composeEnhancers(applyMiddleware(mw))
);
if(i) initialState = JSON.parse(localStorage.getItem('ship')); let previous;
} const state = readable(duxStore.getState(), (set) => {
duxStore.subscribe(() => {
const duxStore = shipDux.createStore(initialState, (mw) => if (previous === duxStore.getState()) return;
composeEnhancers(applyMiddleware(mw)) previous = duxStore.getState();
); set(previous);
if (browser) localStorage.setItem("ship", JSON.stringify(previous));
let previous;
const state = readable(duxStore.getState(), (set) => {
duxStore.subscribe(() => {
if (previous === duxStore.getState()) return;
previous = duxStore.getState();
set(previous);
if( browser ) localStorage.setItem('ship', JSON.stringify(previous));
});
}); });
});
return { return {
dispatch: duxStore.dispatch, dispatch: duxStore.dispatch,
state, state,
shipMass: derived(state, (state) => state.reqs.mass), shipMass: derived(state, (state) => state.reqs.mass),
}; };
}; };